botmux 2.28.0 → 2.29.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 +6 -3
- package/README.md +6 -3
- package/dist/adapters/cli/claude-code.d.ts.map +1 -1
- package/dist/adapters/cli/claude-code.js +26 -28
- package/dist/adapters/cli/claude-code.js.map +1 -1
- package/dist/adapters/cli/shared-hints.d.ts +3 -4
- package/dist/adapters/cli/shared-hints.d.ts.map +1 -1
- package/dist/adapters/cli/shared-hints.js +14 -13
- package/dist/adapters/cli/shared-hints.js.map +1 -1
- package/dist/adapters/cli/types.d.ts +2 -0
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/bot-registry.d.ts +5 -1
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +6 -1
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli.js +317 -27
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +0 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -1
- package/dist/config.js.map +1 -1
- package/dist/core/command-handler.d.ts +2 -1
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +139 -161
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/session-manager.d.ts +6 -2
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +52 -32
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +40 -17
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +17 -16
- package/dist/daemon.js.map +1 -1
- package/dist/global-config.d.ts +15 -0
- package/dist/global-config.d.ts.map +1 -0
- package/dist/global-config.js +73 -0
- package/dist/global-config.js.map +1 -0
- package/dist/i18n/en.d.ts +3 -0
- package/dist/i18n/en.d.ts.map +1 -0
- package/dist/i18n/en.js +251 -0
- package/dist/i18n/en.js.map +1 -0
- package/dist/i18n/index.d.ts +33 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +74 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/i18n/types.d.ts +4 -0
- package/dist/i18n/types.d.ts.map +1 -0
- package/dist/i18n/types.js +5 -0
- package/dist/i18n/types.js.map +1 -0
- package/dist/i18n/zh.d.ts +6 -0
- package/dist/i18n/zh.d.ts.map +1 -0
- package/dist/i18n/zh.js +254 -0
- package/dist/i18n/zh.js.map +1 -0
- package/dist/im/lark/card-builder.d.ts +10 -9
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +58 -53
- 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 +44 -51
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/client.d.ts +4 -2
- package/dist/im/lark/client.d.ts.map +1 -1
- package/dist/im/lark/client.js +1 -7
- package/dist/im/lark/client.js.map +1 -1
- package/dist/im/lark/message-parser.d.ts.map +1 -1
- package/dist/im/lark/message-parser.js +11 -3
- package/dist/im/lark/message-parser.js.map +1 -1
- package/dist/index-daemon.js +10 -0
- package/dist/index-daemon.js.map +1 -1
- package/dist/setup/bot-config-editor.d.ts +44 -0
- package/dist/setup/bot-config-editor.d.ts.map +1 -0
- package/dist/setup/bot-config-editor.js +170 -0
- package/dist/setup/bot-config-editor.js.map +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/worker.js +20 -1
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
|
@@ -20,6 +20,7 @@ import { discoverAdoptableSessions, validateAdoptTarget } from './session-discov
|
|
|
20
20
|
import { generateAuthUrl, getTokenStatus } from '../utils/user-token.js';
|
|
21
21
|
import { bindOncall, unbindOncall, getOncallStatus } from '../services/oncall-store.js';
|
|
22
22
|
import { sessionKey, sessionAnchorId } from './types.js';
|
|
23
|
+
import { t, localeForBot } from '../i18n/index.js';
|
|
23
24
|
// ─── Exported constants ──────────────────────────────────────────────────────
|
|
24
25
|
export const DAEMON_COMMANDS = new Set(['/close', '/restart', '/status', '/help', '/cd', '/repo', '/skip', '/schedule', '/login', '/adopt', '/oncall']);
|
|
25
26
|
/**
|
|
@@ -37,20 +38,20 @@ const MULTILINE_COMMANDS = new Set(['/schedule']);
|
|
|
37
38
|
* with full filesystem access, so an allowlist would be theater. We only do
|
|
38
39
|
* the typo guards: exists and is a directory.
|
|
39
40
|
*/
|
|
40
|
-
export function validateWorkingDir(input) {
|
|
41
|
+
export function validateWorkingDir(input, locale) {
|
|
41
42
|
const resolvedPath = resolve(expandHome(input));
|
|
42
43
|
if (!existsSync(resolvedPath)) {
|
|
43
|
-
return { ok: false, error:
|
|
44
|
+
return { ok: false, error: t('cmd.cd.dir_not_exist', { path: resolvedPath }, locale) };
|
|
44
45
|
}
|
|
45
46
|
let isDir = false;
|
|
46
47
|
try {
|
|
47
48
|
isDir = statSync(resolvedPath).isDirectory();
|
|
48
49
|
}
|
|
49
50
|
catch (e) {
|
|
50
|
-
return { ok: false, error:
|
|
51
|
+
return { ok: false, error: t('cmd.cd.cannot_read', { path: resolvedPath, msg: e?.message ?? String(e) }, locale) };
|
|
51
52
|
}
|
|
52
53
|
if (!isDir) {
|
|
53
|
-
return { ok: false, error:
|
|
54
|
+
return { ok: false, error: t('cmd.cd.not_a_directory', { path: resolvedPath }, locale) };
|
|
54
55
|
}
|
|
55
56
|
return { ok: true, resolvedPath };
|
|
56
57
|
}
|
|
@@ -124,22 +125,28 @@ async function handleScheduleCommand(args, rootId, chatId, deps, larkAppId) {
|
|
|
124
125
|
const { activeSessions } = deps;
|
|
125
126
|
const sessionReply = (rid, content, msgType) => deps.sessionReply(rid, content, msgType, larkAppId);
|
|
126
127
|
const trimmed = args.trim();
|
|
128
|
+
const loc = localeForBot(larkAppId);
|
|
129
|
+
// Format dates using a locale that matches the user's UI choice. Both
|
|
130
|
+
// forms include the wall-clock components the user cares about; the
|
|
131
|
+
// difference is just punctuation and digit order.
|
|
132
|
+
const timeLocale = loc === 'en' ? 'en-US' : 'zh-CN';
|
|
133
|
+
const timeZone = 'Asia/Shanghai';
|
|
127
134
|
// /schedule list | /schedule 列表
|
|
128
135
|
if (!trimmed || trimmed === 'list' || trimmed === '列表') {
|
|
129
136
|
const tasks = scheduleStore.listTasks();
|
|
130
137
|
if (tasks.length === 0) {
|
|
131
|
-
await sessionReply(rootId, '
|
|
138
|
+
await sessionReply(rootId, t('schedule.empty_with_examples', undefined, loc));
|
|
132
139
|
return;
|
|
133
140
|
}
|
|
134
|
-
const lines = tasks.map(
|
|
135
|
-
const status =
|
|
136
|
-
const next =
|
|
137
|
-
const nextStr = next ?
|
|
138
|
-
const lastStr =
|
|
139
|
-
const display =
|
|
140
|
-
return `${status} [${
|
|
141
|
+
const lines = tasks.map(task => {
|
|
142
|
+
const status = task.enabled ? '✅' : '⏸️';
|
|
143
|
+
const next = task.enabled ? scheduler.getNextRun(task.id) : null;
|
|
144
|
+
const nextStr = next ? t('schedule.next_label', { time: next.toLocaleString(timeLocale, { timeZone }) }, loc) : '';
|
|
145
|
+
const lastStr = task.lastRunAt ? t('schedule.last_label', { time: new Date(task.lastRunAt).toLocaleString(timeLocale, { timeZone }) }, loc) : '';
|
|
146
|
+
const display = task.parsed?.display ?? task.schedule;
|
|
147
|
+
return `${status} [${task.id}] ${display} | ${task.name}\n prompt: ${task.prompt.substring(0, 50)}${task.prompt.length > 50 ? '...' : ''}${nextStr}${lastStr}`;
|
|
141
148
|
});
|
|
142
|
-
await sessionReply(rootId,
|
|
149
|
+
await sessionReply(rootId, `${t('schedule.list_header', { count: tasks.length }, loc)}\n\n${lines.join('\n\n')}`);
|
|
143
150
|
return;
|
|
144
151
|
}
|
|
145
152
|
// /schedule remove <id> | /schedule 删除 <id>
|
|
@@ -147,10 +154,10 @@ async function handleScheduleCommand(args, rootId, chatId, deps, larkAppId) {
|
|
|
147
154
|
if (removeMatch) {
|
|
148
155
|
const id = removeMatch[1];
|
|
149
156
|
if (scheduler.removeTask(id)) {
|
|
150
|
-
await sessionReply(rootId,
|
|
157
|
+
await sessionReply(rootId, t('schedule.removed', { id }, loc));
|
|
151
158
|
}
|
|
152
159
|
else {
|
|
153
|
-
await sessionReply(rootId,
|
|
160
|
+
await sessionReply(rootId, t('schedule.not_found', { id }, loc));
|
|
154
161
|
}
|
|
155
162
|
return;
|
|
156
163
|
}
|
|
@@ -159,10 +166,10 @@ async function handleScheduleCommand(args, rootId, chatId, deps, larkAppId) {
|
|
|
159
166
|
if (enableMatch) {
|
|
160
167
|
const id = enableMatch[1];
|
|
161
168
|
if (scheduler.enableTask(id)) {
|
|
162
|
-
await sessionReply(rootId,
|
|
169
|
+
await sessionReply(rootId, t('schedule.enabled', { id }, loc));
|
|
163
170
|
}
|
|
164
171
|
else {
|
|
165
|
-
await sessionReply(rootId,
|
|
172
|
+
await sessionReply(rootId, t('schedule.not_found', { id }, loc));
|
|
166
173
|
}
|
|
167
174
|
return;
|
|
168
175
|
}
|
|
@@ -171,10 +178,10 @@ async function handleScheduleCommand(args, rootId, chatId, deps, larkAppId) {
|
|
|
171
178
|
if (disableMatch) {
|
|
172
179
|
const id = disableMatch[1];
|
|
173
180
|
if (scheduler.disableTask(id)) {
|
|
174
|
-
await sessionReply(rootId,
|
|
181
|
+
await sessionReply(rootId, t('schedule.disabled', { id }, loc));
|
|
175
182
|
}
|
|
176
183
|
else {
|
|
177
|
-
await sessionReply(rootId,
|
|
184
|
+
await sessionReply(rootId, t('schedule.not_found', { id }, loc));
|
|
178
185
|
}
|
|
179
186
|
return;
|
|
180
187
|
}
|
|
@@ -183,10 +190,10 @@ async function handleScheduleCommand(args, rootId, chatId, deps, larkAppId) {
|
|
|
183
190
|
if (runMatch) {
|
|
184
191
|
const id = runMatch[1];
|
|
185
192
|
if (scheduler.runTaskNow(id)) {
|
|
186
|
-
await sessionReply(rootId,
|
|
193
|
+
await sessionReply(rootId, t('schedule.triggered_now', { id }, loc));
|
|
187
194
|
}
|
|
188
195
|
else {
|
|
189
|
-
await sessionReply(rootId,
|
|
196
|
+
await sessionReply(rootId, t('schedule.not_found', { id }, loc));
|
|
190
197
|
}
|
|
191
198
|
return;
|
|
192
199
|
}
|
|
@@ -195,14 +202,10 @@ async function handleScheduleCommand(args, rootId, chatId, deps, larkAppId) {
|
|
|
195
202
|
if (parsed) {
|
|
196
203
|
const ds = larkAppId ? activeSessions.get(sessionKey(rootId, larkAppId)) : undefined;
|
|
197
204
|
const workingDir = ds?.workingDir ?? (ds?.larkAppId ? getBot(ds.larkAppId).config.workingDir ?? '~' : getAllBots()[0]?.config.workingDir ?? '~');
|
|
198
|
-
// For chat-scope sessions, `rootId` here is actually the chatId (the
|
|
199
|
-
// session's anchor). The scheduler keys cross-target routing on
|
|
200
|
-
// rootMessageId — for chat-scope tasks we set rootMessageId=undefined and
|
|
201
|
-
// rely on chatId + scope='chat' to do plain chat sends at fire time.
|
|
202
205
|
const taskScope = ds?.scope === 'chat' ? 'chat' : 'thread';
|
|
203
206
|
const task = scheduler.addTask({
|
|
204
207
|
name: parsed.name,
|
|
205
|
-
schedule: trimmed,
|
|
208
|
+
schedule: trimmed,
|
|
206
209
|
parsed: parsed.parsed,
|
|
207
210
|
prompt: parsed.prompt,
|
|
208
211
|
workingDir,
|
|
@@ -213,20 +216,28 @@ async function handleScheduleCommand(args, rootId, chatId, deps, larkAppId) {
|
|
|
213
216
|
larkAppId,
|
|
214
217
|
});
|
|
215
218
|
const next = scheduler.getNextRun(task.id);
|
|
216
|
-
const nextStr = next ? next.toLocaleString(
|
|
217
|
-
await sessionReply(rootId,
|
|
219
|
+
const nextStr = next ? next.toLocaleString(timeLocale, { timeZone }) : 'N/A';
|
|
220
|
+
await sessionReply(rootId, t('schedule.created', {
|
|
221
|
+
id: task.id,
|
|
222
|
+
name: task.name,
|
|
223
|
+
rule: parsed.parsed.display,
|
|
224
|
+
prompt: task.prompt,
|
|
225
|
+
dir: expandHome(workingDir),
|
|
226
|
+
next: nextStr,
|
|
227
|
+
}, loc));
|
|
218
228
|
return;
|
|
219
229
|
}
|
|
220
230
|
// Unrecognized format
|
|
221
|
-
await sessionReply(rootId,
|
|
231
|
+
await sessionReply(rootId, t('schedule.parse_failed', undefined, loc));
|
|
222
232
|
}
|
|
223
233
|
// ─── Main command handler ────────────────────────────────────────────────────
|
|
224
234
|
export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
|
|
225
235
|
const { activeSessions, getActiveCount, lastRepoScan } = deps;
|
|
226
236
|
const sessionReply = (rid, content, msgType) => deps.sessionReply(rid, content, msgType, larkAppId);
|
|
227
237
|
const ds = larkAppId ? activeSessions.get(sessionKey(rootId, larkAppId)) : undefined;
|
|
228
|
-
const
|
|
229
|
-
|
|
238
|
+
const logTag = ds ? tag(ds) : rootId.substring(0, 12);
|
|
239
|
+
const loc = localeForBot(ds?.larkAppId ?? larkAppId);
|
|
240
|
+
logger.info(`[${logTag}] Command: ${cmd}`);
|
|
230
241
|
logger.debug(`repo command`, message);
|
|
231
242
|
try {
|
|
232
243
|
switch (cmd) {
|
|
@@ -238,10 +249,6 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
|
|
|
238
249
|
const closedCliId = ds.session.cliId ?? botCfg.cliId;
|
|
239
250
|
const closedAnchor = sessionAnchorId(ds);
|
|
240
251
|
const closedWorkingDir = ds.session.workingDir;
|
|
241
|
-
// Resolve the CLI-native resume command BEFORE killing the worker
|
|
242
|
-
// — for codex this consults `~/.codex/history.jsonl` which is
|
|
243
|
-
// populated by the live worker; reading it post-kill still works
|
|
244
|
-
// (the file lives on disk) but capturing here keeps intent clear.
|
|
245
252
|
const cliResumeCommand = (() => {
|
|
246
253
|
try {
|
|
247
254
|
const adapter = createCliAdapterSync(closedCliId, botCfg.cliPathOverride);
|
|
@@ -257,12 +264,12 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
|
|
|
257
264
|
killWorker(ds);
|
|
258
265
|
sessionStore.closeSession(closedSessionId);
|
|
259
266
|
activeSessions.delete(sessionKey(rootId, larkAppId));
|
|
260
|
-
const card = buildSessionClosedCard(closedSessionId, closedAnchor, closedTitle, closedCliId, closedWorkingDir, cliResumeCommand);
|
|
267
|
+
const card = buildSessionClosedCard(closedSessionId, closedAnchor, closedTitle, closedCliId, closedWorkingDir, cliResumeCommand, loc);
|
|
261
268
|
await sessionReply(rootId, card, 'interactive');
|
|
262
|
-
logger.info(`[${
|
|
269
|
+
logger.info(`[${logTag}] Session closed by /close command`);
|
|
263
270
|
}
|
|
264
271
|
else {
|
|
265
|
-
await sessionReply(rootId, '
|
|
272
|
+
await sessionReply(rootId, t('cmd.no_active_session', undefined, loc));
|
|
266
273
|
}
|
|
267
274
|
break;
|
|
268
275
|
}
|
|
@@ -271,31 +278,31 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
|
|
|
271
278
|
if (ds.worker && !ds.worker.killed) {
|
|
272
279
|
ds.worker.send({ type: 'restart' });
|
|
273
280
|
const cliName = getCliDisplayName(getBot(ds.larkAppId).config.cliId);
|
|
274
|
-
await sessionReply(rootId,
|
|
281
|
+
await sessionReply(rootId, t('cmd.restart.in_progress', { cliName }, loc));
|
|
275
282
|
}
|
|
276
283
|
else {
|
|
277
284
|
killWorker(ds);
|
|
278
285
|
const cliName = getCliDisplayName(getBot(ds.larkAppId).config.cliId);
|
|
279
|
-
await sessionReply(rootId,
|
|
286
|
+
await sessionReply(rootId, t('cmd.restart.terminated', { cliName }, loc));
|
|
280
287
|
}
|
|
281
|
-
logger.info(`[${
|
|
288
|
+
logger.info(`[${logTag}] Restart by /restart command`);
|
|
282
289
|
}
|
|
283
290
|
else {
|
|
284
|
-
await sessionReply(rootId, '
|
|
291
|
+
await sessionReply(rootId, t('cmd.no_active_session', undefined, loc));
|
|
285
292
|
}
|
|
286
293
|
break;
|
|
287
294
|
}
|
|
288
295
|
case '/cd': {
|
|
289
296
|
const targetPath = message.content.replace(/^\/cd\s*/, '').trim();
|
|
290
297
|
if (!targetPath) {
|
|
291
|
-
await sessionReply(rootId, '
|
|
298
|
+
await sessionReply(rootId, t('cmd.cd.usage', undefined, loc));
|
|
292
299
|
break;
|
|
293
300
|
}
|
|
294
301
|
if (!ds) {
|
|
295
|
-
await sessionReply(rootId, '
|
|
302
|
+
await sessionReply(rootId, t('cmd.no_active_session', undefined, loc));
|
|
296
303
|
break;
|
|
297
304
|
}
|
|
298
|
-
const validation = validateWorkingDir(targetPath);
|
|
305
|
+
const validation = validateWorkingDir(targetPath, loc);
|
|
299
306
|
if (!validation.ok) {
|
|
300
307
|
await sessionReply(rootId, validation.error);
|
|
301
308
|
break;
|
|
@@ -305,22 +312,21 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
|
|
|
305
312
|
ds.workingDir = targetPath;
|
|
306
313
|
ds.session.workingDir = targetPath;
|
|
307
314
|
sessionStore.updateSession(ds.session);
|
|
308
|
-
await sessionReply(rootId,
|
|
309
|
-
logger.info(`[${
|
|
315
|
+
await sessionReply(rootId, t('cmd.cd.switched', { path: resolvedPath }, loc));
|
|
316
|
+
logger.info(`[${logTag}] Working directory changed to ${resolvedPath} by /cd command`);
|
|
310
317
|
break;
|
|
311
318
|
}
|
|
312
319
|
case '/repo': {
|
|
313
320
|
const repoArg = message.content.replace(/^\/repo\s*/, '').trim();
|
|
314
321
|
const repoIndex = repoArg ? parseInt(repoArg, 10) : NaN;
|
|
315
|
-
// /repo <N> — quick select from last scan
|
|
316
322
|
if (!isNaN(repoIndex) && ds) {
|
|
317
323
|
const cached = lastRepoScan.get(ds.chatId);
|
|
318
324
|
if (!cached || cached.length === 0) {
|
|
319
|
-
await sessionReply(rootId, '
|
|
325
|
+
await sessionReply(rootId, t('cmd.repo.no_prior_scan', undefined, loc));
|
|
320
326
|
break;
|
|
321
327
|
}
|
|
322
328
|
if (repoIndex < 1 || repoIndex > cached.length) {
|
|
323
|
-
await sessionReply(rootId,
|
|
329
|
+
await sessionReply(rootId, t('cmd.repo.index_out_of_range', { max: cached.length }, loc));
|
|
324
330
|
break;
|
|
325
331
|
}
|
|
326
332
|
const project = cached[repoIndex - 1];
|
|
@@ -334,61 +340,55 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
|
|
|
334
340
|
const botCfg = selfBot.config;
|
|
335
341
|
ds.pendingRepo = false;
|
|
336
342
|
const { buildNewTopicPrompt, getAvailableBots } = await import('./session-manager.js');
|
|
337
|
-
const prompt = buildNewTopicPrompt(ds.pendingPrompt ?? '', ds.session.sessionId, botCfg.cliId, botCfg.cliPathOverride, ds.pendingAttachments, ds.pendingMentions, await getAvailableBots(ds.larkAppId, ds.chatId), ds.pendingFollowUps, { name: selfBot.botName, openId: selfBot.botOpenId });
|
|
343
|
+
const prompt = buildNewTopicPrompt(ds.pendingPrompt ?? '', ds.session.sessionId, botCfg.cliId, botCfg.cliPathOverride, ds.pendingAttachments, ds.pendingMentions, await getAvailableBots(ds.larkAppId, ds.chatId), ds.pendingFollowUps, { name: selfBot.botName, openId: selfBot.botOpenId }, loc);
|
|
338
344
|
ds.pendingPrompt = undefined;
|
|
339
345
|
ds.pendingAttachments = undefined;
|
|
340
346
|
ds.pendingMentions = undefined;
|
|
341
347
|
ds.pendingFollowUps = undefined;
|
|
342
348
|
forkWorker(ds, prompt);
|
|
343
|
-
await sessionReply(rootId,
|
|
349
|
+
await sessionReply(rootId, t('cmd.repo.selected_in_pending', { name: displayName }, loc));
|
|
344
350
|
}
|
|
345
351
|
else {
|
|
346
352
|
killWorker(ds);
|
|
347
353
|
sessionStore.closeSession(ds.session.sessionId);
|
|
348
354
|
const session = sessionStore.createSession(ds.chatId, rootId, displayName, ds.chatType);
|
|
349
355
|
ds.session = session;
|
|
350
|
-
// Pin workingDir + larkAppId onto the freshly-created record before
|
|
351
|
-
// forkWorker — otherwise a daemon restart restores the session with
|
|
352
|
-
// an empty workingDir and falls back to the bot's default cwd,
|
|
353
|
-
// making `claude --resume` look in the wrong .claude/projects/ dir.
|
|
354
356
|
ds.session.workingDir = selectedPath;
|
|
355
357
|
ds.session.larkAppId = ds.larkAppId;
|
|
356
358
|
sessionStore.updateSession(ds.session);
|
|
357
359
|
ds.hasHistory = false;
|
|
358
360
|
forkWorker(ds, '', false);
|
|
359
|
-
await sessionReply(rootId,
|
|
361
|
+
await sessionReply(rootId, t('cmd.repo.switched_to', { name: displayName }, loc));
|
|
360
362
|
}
|
|
361
|
-
// Withdraw repo selection card
|
|
362
363
|
if (ds.repoCardMessageId) {
|
|
363
364
|
deleteMessage(ds.larkAppId, ds.repoCardMessageId);
|
|
364
365
|
ds.repoCardMessageId = undefined;
|
|
365
366
|
}
|
|
366
|
-
logger.info(`[${
|
|
367
|
+
logger.info(`[${logTag}] Repo selected via /repo ${repoIndex}: ${selectedPath}`);
|
|
367
368
|
break;
|
|
368
369
|
}
|
|
369
|
-
// /repo — show project list card
|
|
370
370
|
if (ds?.worker && !ds.worker.killed) {
|
|
371
|
-
await sessionReply(rootId, '
|
|
371
|
+
await sessionReply(rootId, t('cmd.repo.warning_running', undefined, loc));
|
|
372
372
|
}
|
|
373
373
|
const scanDirs = getProjectScanDirs(ds);
|
|
374
374
|
const validDirs = scanDirs.filter(d => existsSync(d));
|
|
375
375
|
if (validDirs.length === 0) {
|
|
376
|
-
await sessionReply(rootId,
|
|
376
|
+
await sessionReply(rootId, t('cmd.repo.scan_dir_not_exist', { dirs: scanDirs.join(', ') }, loc));
|
|
377
377
|
break;
|
|
378
378
|
}
|
|
379
379
|
const projects = scanMultipleProjects(validDirs);
|
|
380
380
|
if (projects.length === 0) {
|
|
381
|
-
await sessionReply(rootId,
|
|
381
|
+
await sessionReply(rootId, t('cmd.repo.no_git_repos', { dirs: validDirs.join(', ') }, loc));
|
|
382
382
|
break;
|
|
383
383
|
}
|
|
384
384
|
if (ds)
|
|
385
385
|
lastRepoScan.set(ds.chatId, projects);
|
|
386
386
|
const currentCwd = getSessionWorkingDir(ds);
|
|
387
|
-
const cardJson = buildRepoSelectCard(projects, currentCwd, rootId);
|
|
387
|
+
const cardJson = buildRepoSelectCard(projects, currentCwd, rootId, loc);
|
|
388
388
|
const repoCardMsgId = await sessionReply(rootId, cardJson, 'interactive');
|
|
389
389
|
if (ds)
|
|
390
390
|
ds.repoCardMessageId = repoCardMsgId;
|
|
391
|
-
logger.info(`[${
|
|
391
|
+
logger.info(`[${logTag}] Sent repo card with ${projects.length} project(s)`);
|
|
392
392
|
break;
|
|
393
393
|
}
|
|
394
394
|
case '/skip': {
|
|
@@ -397,22 +397,22 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
|
|
|
397
397
|
const botCfg = selfBot.config;
|
|
398
398
|
ds.pendingRepo = false;
|
|
399
399
|
const { buildNewTopicPrompt, getAvailableBots } = await import('./session-manager.js');
|
|
400
|
-
const prompt = buildNewTopicPrompt(ds.pendingPrompt ?? '', ds.session.sessionId, botCfg.cliId, botCfg.cliPathOverride, ds.pendingAttachments, ds.pendingMentions, await getAvailableBots(ds.larkAppId, ds.chatId), ds.pendingFollowUps, { name: selfBot.botName, openId: selfBot.botOpenId });
|
|
400
|
+
const prompt = buildNewTopicPrompt(ds.pendingPrompt ?? '', ds.session.sessionId, botCfg.cliId, botCfg.cliPathOverride, ds.pendingAttachments, ds.pendingMentions, await getAvailableBots(ds.larkAppId, ds.chatId), ds.pendingFollowUps, { name: selfBot.botName, openId: selfBot.botOpenId }, loc);
|
|
401
401
|
ds.pendingPrompt = undefined;
|
|
402
402
|
ds.pendingAttachments = undefined;
|
|
403
403
|
ds.pendingMentions = undefined;
|
|
404
404
|
ds.pendingFollowUps = undefined;
|
|
405
405
|
forkWorker(ds, prompt);
|
|
406
406
|
const cwd = getSessionWorkingDir(ds);
|
|
407
|
-
await sessionReply(rootId,
|
|
407
|
+
await sessionReply(rootId, t('cmd.skip.opened', { cwd }, loc));
|
|
408
408
|
if (ds.repoCardMessageId) {
|
|
409
409
|
deleteMessage(ds.larkAppId, ds.repoCardMessageId);
|
|
410
410
|
ds.repoCardMessageId = undefined;
|
|
411
411
|
}
|
|
412
|
-
logger.info(`[${
|
|
412
|
+
logger.info(`[${logTag}] Skip repo via /skip, spawning CLI in ${cwd}`);
|
|
413
413
|
}
|
|
414
414
|
else {
|
|
415
|
-
await sessionReply(rootId, '
|
|
415
|
+
await sessionReply(rootId, t('cmd.skip.no_pending', undefined, loc));
|
|
416
416
|
}
|
|
417
417
|
break;
|
|
418
418
|
}
|
|
@@ -423,7 +423,7 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
|
|
|
423
423
|
const termUrl = ds.workerPort ? `http://${config.web.externalHost}:${ds.workerPort}` : '-';
|
|
424
424
|
const lines = [
|
|
425
425
|
`Session: ${ds.session.sessionId}`,
|
|
426
|
-
`Status: ${alive ? '
|
|
426
|
+
`Status: ${alive ? t('cmd.status.running', undefined, loc) : t('cmd.status.waiting', undefined, loc)}`,
|
|
427
427
|
`Terminal: ${termUrl}`,
|
|
428
428
|
`CWD: ${getSessionWorkingDir(ds)}`,
|
|
429
429
|
`${getCliDisplayName(getBot(ds.larkAppId).config.cliId)}: v${ds.cliVersion}${ds.cliVersion !== getCurrentCliVersion() ? ` (latest: v${getCurrentCliVersion()})` : ''}`,
|
|
@@ -435,7 +435,11 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
|
|
|
435
435
|
}
|
|
436
436
|
else {
|
|
437
437
|
const fallbackCliName = larkAppId ? getCliDisplayName(getBot(larkAppId).config.cliId) : 'CLI';
|
|
438
|
-
await sessionReply(rootId,
|
|
438
|
+
await sessionReply(rootId, t('cmd.status.fallback_no_session', {
|
|
439
|
+
count: getActiveCount(),
|
|
440
|
+
cliName: fallbackCliName,
|
|
441
|
+
version: getCurrentCliVersion(),
|
|
442
|
+
}, loc));
|
|
439
443
|
}
|
|
440
444
|
break;
|
|
441
445
|
}
|
|
@@ -443,7 +447,7 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
|
|
|
443
447
|
const scheduleArgs = message.content.replace(/^\/schedule\s*/, '');
|
|
444
448
|
const chatId = ds?.chatId;
|
|
445
449
|
await handleScheduleCommand(scheduleArgs, rootId, chatId, deps, larkAppId);
|
|
446
|
-
logger.info(`[${
|
|
450
|
+
logger.info(`[${logTag}] Schedule command handled`);
|
|
447
451
|
break;
|
|
448
452
|
}
|
|
449
453
|
case '/login': {
|
|
@@ -452,63 +456,54 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
|
|
|
452
456
|
await sessionReply(rootId, getTokenStatus());
|
|
453
457
|
break;
|
|
454
458
|
}
|
|
455
|
-
// Generate OAuth URL
|
|
456
459
|
const botCfg2 = ds ? getBot(ds.larkAppId).config : (larkAppId ? getBot(larkAppId).config : getAllBots()[0]?.config);
|
|
457
460
|
if (!botCfg2?.larkAppId || !botCfg2?.larkAppSecret) {
|
|
458
|
-
await sessionReply(rootId, '
|
|
461
|
+
await sessionReply(rootId, t('cmd.login.no_credentials', undefined, loc));
|
|
459
462
|
break;
|
|
460
463
|
}
|
|
461
464
|
const { authUrl } = generateAuthUrl(botCfg2.larkAppId, botCfg2.larkAppSecret);
|
|
462
465
|
await sessionReply(rootId, [
|
|
463
|
-
'
|
|
466
|
+
t('cmd.login.title', undefined, loc),
|
|
464
467
|
'',
|
|
465
|
-
'
|
|
468
|
+
t('cmd.login.step1', undefined, loc),
|
|
466
469
|
authUrl,
|
|
467
470
|
'',
|
|
468
|
-
'
|
|
469
|
-
'
|
|
471
|
+
t('cmd.login.step2', undefined, loc),
|
|
472
|
+
t('cmd.login.step3', undefined, loc),
|
|
470
473
|
'',
|
|
471
|
-
'
|
|
472
|
-
'
|
|
474
|
+
t('cmd.login.footer', undefined, loc),
|
|
475
|
+
t('cmd.login.status_hint', undefined, loc),
|
|
473
476
|
].join('\n'));
|
|
474
477
|
break;
|
|
475
478
|
}
|
|
476
479
|
case '/adopt': {
|
|
477
480
|
const adoptArgs = message.content.replace(/^\/adopt\s*/i, '').trim();
|
|
478
|
-
// Refuse re-adopt when the thread is already bridged. Otherwise the
|
|
479
|
-
// user sees the misleading "未发现可接入 CLI 会话" branch whenever the
|
|
480
|
-
// discovery scan happens to return zero (e.g. the original CLI exited
|
|
481
|
-
// mid-bridge, or pane filters mismatch) — they have no idea why their
|
|
482
|
-
// working session was "lost". Force the explicit 断开 → /adopt swap.
|
|
483
481
|
if (ds?.adoptedFrom) {
|
|
484
482
|
const adopted = ds.adoptedFrom;
|
|
485
483
|
const cliName = getCliDisplayName(adopted.cliId ?? 'claude-code');
|
|
486
484
|
const project = adopted.cwd ? (adopted.cwd.split('/').pop() || adopted.cwd) : '';
|
|
487
485
|
const label = project ? `${cliName} · ${project}` : cliName;
|
|
488
|
-
await sessionReply(rootId,
|
|
489
|
-
'请先点击卡片上的「断开」按钮,再 /adopt 切换 CLI 会话(原 CLI 不受影响)。');
|
|
486
|
+
await sessionReply(rootId, t('cmd.adopt.already_adopted', { label, pane: adopted.tmuxTarget }, loc));
|
|
490
487
|
break;
|
|
491
488
|
}
|
|
492
|
-
// Only show sessions matching this bot's CLI type
|
|
493
489
|
const botCliId = ds ? getBot(ds.larkAppId).config.cliId : undefined;
|
|
494
490
|
const sessions = discoverAdoptableSessions(botCliId);
|
|
495
491
|
if (sessions.length === 0) {
|
|
496
|
-
await sessionReply(rootId, '
|
|
492
|
+
await sessionReply(rootId, t('cmd.adopt.no_sessions', undefined, loc));
|
|
497
493
|
break;
|
|
498
494
|
}
|
|
499
495
|
const directTarget = adoptArgs;
|
|
500
496
|
if (directTarget) {
|
|
501
497
|
const target = sessions.find(s => s.tmuxTarget === directTarget);
|
|
502
498
|
if (!target) {
|
|
503
|
-
await sessionReply(rootId,
|
|
499
|
+
await sessionReply(rootId, t('cmd.adopt.pane_not_found', { pane: directTarget }, loc));
|
|
504
500
|
break;
|
|
505
501
|
}
|
|
506
502
|
if (ds)
|
|
507
503
|
await startAdoptSession(target, ds, deps, larkAppId);
|
|
508
504
|
break;
|
|
509
505
|
}
|
|
510
|
-
|
|
511
|
-
const cardJson = buildAdoptSelectCard(sessions, rootId);
|
|
506
|
+
const cardJson = buildAdoptSelectCard(sessions, rootId, loc);
|
|
512
507
|
await sessionReply(rootId, cardJson, 'interactive');
|
|
513
508
|
break;
|
|
514
509
|
}
|
|
@@ -518,40 +513,26 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
|
|
|
518
513
|
const appId = larkAppId ?? ds?.larkAppId;
|
|
519
514
|
const chatId = ds?.chatId;
|
|
520
515
|
if (!appId || !chatId) {
|
|
521
|
-
await sessionReply(rootId, '
|
|
516
|
+
await sessionReply(rootId, t('cmd.oncall.need_group', undefined, loc));
|
|
522
517
|
break;
|
|
523
518
|
}
|
|
524
519
|
if (!sub || sub === 'status' || sub === '状态') {
|
|
525
520
|
const entry = getOncallStatus(appId, chatId);
|
|
526
521
|
if (!entry) {
|
|
527
|
-
await sessionReply(rootId,
|
|
528
|
-
'当前群尚未绑定 oncall 项目。',
|
|
529
|
-
'',
|
|
530
|
-
'用法:',
|
|
531
|
-
'/oncall bind <path> — 绑定当前群到某个项目目录,跳过仓库选择卡片',
|
|
532
|
-
'/oncall unbind — 解除当前群的 oncall 绑定',
|
|
533
|
-
'/oncall status — 查看当前绑定状态',
|
|
534
|
-
'',
|
|
535
|
-
'绑定后:群内任何成员都可以 @ 机器人提问;仅 allowedUsers 能点卡片按钮、执行 /cd /restart /close 等命令。',
|
|
536
|
-
].join('\n'));
|
|
522
|
+
await sessionReply(rootId, t('cmd.oncall.not_bound', undefined, loc));
|
|
537
523
|
}
|
|
538
524
|
else {
|
|
539
|
-
await sessionReply(rootId,
|
|
540
|
-
'🟢 已绑定 oncall',
|
|
541
|
-
`工作目录:${entry.workingDir}`,
|
|
542
|
-
'',
|
|
543
|
-
'/oncall unbind 可解除绑定;/cd <path> 切换工作目录(仍保留 oncall 模式)。',
|
|
544
|
-
].join('\n'));
|
|
525
|
+
await sessionReply(rootId, t('cmd.oncall.bound', { dir: entry.workingDir }, loc));
|
|
545
526
|
}
|
|
546
527
|
break;
|
|
547
528
|
}
|
|
548
529
|
if (sub === 'bind' || sub === '绑定') {
|
|
549
530
|
const target = rest.join(' ').trim();
|
|
550
531
|
if (!target) {
|
|
551
|
-
await sessionReply(rootId, '
|
|
532
|
+
await sessionReply(rootId, t('cmd.oncall.bind_usage', undefined, loc));
|
|
552
533
|
break;
|
|
553
534
|
}
|
|
554
|
-
const validation = validateWorkingDir(target);
|
|
535
|
+
const validation = validateWorkingDir(target, loc);
|
|
555
536
|
if (!validation.ok) {
|
|
556
537
|
await sessionReply(rootId, validation.error);
|
|
557
538
|
break;
|
|
@@ -560,82 +541,81 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
|
|
|
560
541
|
const result = await bindOncall(appId, chatId, target);
|
|
561
542
|
if (!result.ok) {
|
|
562
543
|
if (result.reason === 'bot_not_in_config') {
|
|
563
|
-
await sessionReply(rootId, '
|
|
544
|
+
await sessionReply(rootId, t('cmd.oncall.bind_failed_no_bot', undefined, loc));
|
|
564
545
|
}
|
|
565
546
|
else {
|
|
566
|
-
await sessionReply(rootId,
|
|
547
|
+
await sessionReply(rootId, t('cmd.oncall.bind_failed', { reason: result.reason }, loc));
|
|
567
548
|
}
|
|
568
549
|
break;
|
|
569
550
|
}
|
|
570
|
-
const verb = result.created
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
551
|
+
const verb = result.created
|
|
552
|
+
? t('cmd.oncall.verb_bound', undefined, loc)
|
|
553
|
+
: t('cmd.oncall.verb_updated', undefined, loc);
|
|
554
|
+
await sessionReply(rootId, t('cmd.oncall.bind_success', {
|
|
555
|
+
verb,
|
|
556
|
+
chatId,
|
|
557
|
+
target,
|
|
558
|
+
resolved: resolvedPath,
|
|
559
|
+
}, loc));
|
|
560
|
+
logger.info(`[${logTag}] /oncall bind chat=${chatId} dir=${target}`);
|
|
579
561
|
break;
|
|
580
562
|
}
|
|
581
563
|
if (sub === 'unbind' || sub === '解绑') {
|
|
582
564
|
const result = await unbindOncall(appId, chatId);
|
|
583
565
|
if (!result.ok) {
|
|
584
|
-
await sessionReply(rootId,
|
|
566
|
+
await sessionReply(rootId, t('cmd.oncall.unbind_failed', { reason: result.reason }, loc));
|
|
585
567
|
break;
|
|
586
568
|
}
|
|
587
569
|
if (!result.wasBound) {
|
|
588
|
-
|
|
589
|
-
// understand subsequent default-oncall won't re-bind this chat.
|
|
590
|
-
await sessionReply(rootId, '当前群未绑定 oncall。(已记录解绑意图,default-oncall 不会再自动绑此群)');
|
|
570
|
+
await sessionReply(rootId, t('cmd.oncall.unbind_not_bound', undefined, loc));
|
|
591
571
|
}
|
|
592
572
|
else {
|
|
593
|
-
await sessionReply(rootId, '
|
|
573
|
+
await sessionReply(rootId, t('cmd.oncall.unbind_success', undefined, loc));
|
|
594
574
|
}
|
|
595
|
-
logger.info(`[${
|
|
575
|
+
logger.info(`[${logTag}] /oncall unbind chat=${chatId} wasBound=${result.wasBound}`);
|
|
596
576
|
break;
|
|
597
577
|
}
|
|
598
|
-
await sessionReply(rootId,
|
|
578
|
+
await sessionReply(rootId, t('cmd.oncall.unknown_sub', { sub }, loc));
|
|
599
579
|
break;
|
|
600
580
|
}
|
|
601
581
|
case '/help': {
|
|
602
582
|
const botCfg = ds ? getBot(ds.larkAppId).config : getAllBots()[0]?.config;
|
|
603
583
|
const cliName = getCliDisplayName(botCfg?.cliId ?? 'claude-code');
|
|
604
584
|
const help = [
|
|
605
|
-
'
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
'
|
|
610
|
-
'
|
|
611
|
-
'
|
|
585
|
+
t('help.heading_session', undefined, loc),
|
|
586
|
+
t('help.close', { cliName }, loc),
|
|
587
|
+
t('help.restart', { cliName }, loc),
|
|
588
|
+
t('help.cd', { cliName }, loc),
|
|
589
|
+
t('help.repo_list', undefined, loc),
|
|
590
|
+
t('help.repo_n', undefined, loc),
|
|
591
|
+
t('help.status', undefined, loc),
|
|
612
592
|
'',
|
|
613
|
-
|
|
593
|
+
t('help.heading_passthrough', { cliName }, loc),
|
|
614
594
|
'/compact /model /clear /plugin /usage',
|
|
615
595
|
'',
|
|
616
|
-
'
|
|
617
|
-
'
|
|
618
|
-
'
|
|
619
|
-
'
|
|
620
|
-
'
|
|
621
|
-
'
|
|
596
|
+
t('help.heading_schedule', undefined, loc),
|
|
597
|
+
t('help.schedule_create', undefined, loc),
|
|
598
|
+
t('help.schedule_list', undefined, loc),
|
|
599
|
+
t('help.schedule_remove', undefined, loc),
|
|
600
|
+
t('help.schedule_toggle', undefined, loc),
|
|
601
|
+
t('help.schedule_run', undefined, loc),
|
|
622
602
|
'',
|
|
623
|
-
'
|
|
603
|
+
t('help.schedule_formats', undefined, loc),
|
|
624
604
|
'',
|
|
625
|
-
'
|
|
626
|
-
'
|
|
627
|
-
'
|
|
605
|
+
t('help.heading_adopt', undefined, loc),
|
|
606
|
+
t('help.adopt', undefined, loc),
|
|
607
|
+
t('help.adopt_pane', undefined, loc),
|
|
628
608
|
'',
|
|
629
|
-
'
|
|
630
|
-
'
|
|
631
|
-
'
|
|
609
|
+
t('help.heading_login', undefined, loc),
|
|
610
|
+
t('help.login', undefined, loc),
|
|
611
|
+
t('help.login_status', undefined, loc),
|
|
632
612
|
'',
|
|
633
|
-
'
|
|
634
|
-
'
|
|
635
|
-
'
|
|
636
|
-
'
|
|
613
|
+
t('help.heading_oncall', undefined, loc),
|
|
614
|
+
t('help.oncall_bind', undefined, loc),
|
|
615
|
+
t('help.oncall_unbind', undefined, loc),
|
|
616
|
+
t('help.oncall_status', undefined, loc),
|
|
637
617
|
'',
|
|
638
|
-
'
|
|
618
|
+
t('help.help', undefined, loc),
|
|
639
619
|
];
|
|
640
620
|
await sessionReply(rootId, help.join('\n'));
|
|
641
621
|
break;
|
|
@@ -643,19 +623,18 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
|
|
|
643
623
|
}
|
|
644
624
|
}
|
|
645
625
|
catch (err) {
|
|
646
|
-
logger.error(`[${
|
|
626
|
+
logger.error(`[${logTag}] Command ${cmd} error: ${err.message}`);
|
|
647
627
|
}
|
|
648
628
|
}
|
|
649
629
|
// ─── Adopt session helper ────────────────────────────────────────────────────
|
|
650
630
|
export async function startAdoptSession(target, ds, deps, larkAppId) {
|
|
651
631
|
const sessionReply = (rid, content, msgType) => deps.sessionReply(rid, content, msgType, larkAppId);
|
|
652
|
-
|
|
632
|
+
const loc = localeForBot(ds.larkAppId ?? larkAppId);
|
|
653
633
|
if (!validateAdoptTarget(target.tmuxTarget, target.cliPid)) {
|
|
654
|
-
await sessionReply(sessionAnchorId(ds), '
|
|
634
|
+
await sessionReply(sessionAnchorId(ds), t('cmd.adopt.target_exited', undefined, loc));
|
|
655
635
|
return;
|
|
656
636
|
}
|
|
657
637
|
const project = target.cwd.split('/').pop() || target.cwd;
|
|
658
|
-
// Update the existing DaemonSession with adopt info
|
|
659
638
|
ds.workingDir = target.cwd;
|
|
660
639
|
ds.session.workingDir = target.cwd;
|
|
661
640
|
ds.session.title = `Adopt: ${project}`;
|
|
@@ -668,11 +647,10 @@ export async function startAdoptSession(target, ds, deps, larkAppId) {
|
|
|
668
647
|
paneCols: target.paneCols,
|
|
669
648
|
paneRows: target.paneRows,
|
|
670
649
|
};
|
|
671
|
-
// Persist adopt metadata so the session can be restored after daemon restart
|
|
672
650
|
ds.session.adoptedFrom = { ...ds.adoptedFrom };
|
|
673
651
|
sessionStore.updateSession(ds.session);
|
|
674
652
|
forkAdoptWorker(ds);
|
|
675
653
|
const cliName = getCliDisplayName(target.cliId);
|
|
676
|
-
await sessionReply(sessionAnchorId(ds),
|
|
654
|
+
await sessionReply(sessionAnchorId(ds), t('cmd.adopt.success', { cliName, project, pane: target.tmuxTarget }, loc));
|
|
677
655
|
}
|
|
678
656
|
//# sourceMappingURL=command-handler.js.map
|