@ai-setting/roy-agent-core 1.7.65 → 1.7.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/env/agent/agent-component.d.ts +56 -3
- package/dist/env/agent/agent-component.js +2 -2
- package/dist/env/agent/index.js +2 -2
- package/dist/env/agent/types.d.ts +28 -0
- package/dist/env/environment.d.ts +13 -0
- package/dist/env/event-source/command-card-action.d.ts +105 -0
- package/dist/env/event-source/command-cards.d.ts +72 -0
- package/dist/env/event-source/command-registry.d.ts +181 -0
- package/dist/env/event-source/command-types.d.ts +8 -0
- package/dist/env/event-source/event-source-handlers.d.ts +66 -0
- package/dist/env/event-source/event-source-handlers.js +7 -4
- package/dist/env/event-source/fallback-model-command.d.ts +80 -0
- package/dist/env/event-source/index.d.ts +7 -0
- package/dist/env/event-source/index.js +48 -7
- package/dist/env/event-source/timer-handler.d.ts +93 -1
- package/dist/env/index.js +15 -14
- package/dist/env/llm/index.js +1 -1
- package/dist/env/prompt/index.js +3 -2
- package/dist/env/prompt/prompts-index.d.ts +3 -8
- package/dist/env/task/delegate/delegate-tool.d.ts +58 -2
- package/dist/env/task/delegate/delegate-tool.js +5 -2
- package/dist/env/task/delegate/index.js +3 -2
- package/dist/env/task/index.js +9 -8
- package/dist/env/task/mcp/index.js +3 -2
- package/dist/env/task/plugins/__e2e__/laya-completion-e2e.d.ts +16 -0
- package/dist/env/task/plugins/index.js +4 -3
- package/dist/env/task/plugins/laya-completion-plugin.d.ts +133 -0
- package/dist/env/task/plugins/laya-port.d.ts +23 -0
- package/dist/env/task/storage/index.js +2 -2
- package/dist/env/task/storage/migrations/003_add_process_description.d.ts +1 -1
- package/dist/env/task/storage/migrations/log-migration.d.ts +49 -3
- package/dist/env/task/task-component.d.ts +43 -0
- package/dist/env/task/tools/completion-copy.d.ts +27 -0
- package/dist/env/task/tools/index.js +2 -1
- package/dist/env/task/tools/operation/create-tool.d.ts +1 -1
- package/dist/env/task/tools/operation/index.js +1 -1
- package/dist/env/task/tools/operation/operation-types.d.ts +10 -8
- package/dist/env/task/types.d.ts +3 -3
- package/dist/env/tool/built-in/index.js +2 -2
- package/dist/env/tool/built-in/query-background-process-tool.d.ts +10 -0
- package/dist/env/tool/index.js +3 -3
- package/dist/env/tool/types.d.ts +6 -0
- package/dist/index.js +60 -19
- package/dist/shared/@ai-setting/{roy-agent-core-rjzdj0xb.js → roy-agent-core-02j24hrg.js} +2 -0
- package/dist/shared/@ai-setting/{roy-agent-core-hmdcz3g9.js → roy-agent-core-1k83dq3a.js} +16 -6
- package/dist/shared/@ai-setting/{roy-agent-core-e42v9bcq.js → roy-agent-core-1kxf910n.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-amkqnyzy.js → roy-agent-core-58804s3j.js} +10 -0
- package/dist/shared/@ai-setting/roy-agent-core-6m4bw8jy.js +6 -0
- package/dist/shared/@ai-setting/roy-agent-core-8zas6w5q.js +1083 -0
- package/dist/shared/@ai-setting/{roy-agent-core-9bs6xrav.js → roy-agent-core-abqhkbn3.js} +6 -3
- package/dist/shared/@ai-setting/{roy-agent-core-f6c0nadn.js → roy-agent-core-b7jbqfsy.js} +25 -11
- package/dist/shared/@ai-setting/{roy-agent-core-hjejpcg5.js → roy-agent-core-dnjm17sa.js} +51 -9
- package/dist/shared/@ai-setting/{roy-agent-core-t0ktaevq.js → roy-agent-core-h5vmff8w.js} +60 -4
- package/dist/shared/@ai-setting/{roy-agent-core-xhrrgfdy.js → roy-agent-core-hfr6bzts.js} +168 -52
- package/dist/shared/@ai-setting/{roy-agent-core-18am1h07.js → roy-agent-core-k4ps1hxp.js} +1085 -46
- package/dist/shared/@ai-setting/{roy-agent-core-jz8dsypm.js → roy-agent-core-kpg25s2q.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-6masakz4.js → roy-agent-core-ma35jyd9.js} +19 -8
- package/dist/shared/@ai-setting/{roy-agent-core-gxzxq1sf.js → roy-agent-core-ntq0gz7q.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-rn57psp0.js → roy-agent-core-p5cz9m8v.js} +3 -3
- package/dist/shared/@ai-setting/{roy-agent-core-w07ymg29.js → roy-agent-core-wksgtqf9.js} +1 -1
- package/package.json +1 -1
- package/dist/shared/@ai-setting/roy-agent-core-6z412atw.js +0 -654
- /package/dist/shared/@ai-setting/{roy-agent-core-5enxfga2.js → roy-agent-core-b4x6x4k2.js} +0 -0
- /package/dist/shared/@ai-setting/{roy-agent-core-es467wsz.js → roy-agent-core-dt104qa1.js} +0 -0
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
createPerTaskGroupChatPlugin,
|
|
14
14
|
extractPerTaskChatId,
|
|
15
15
|
extractPerTaskChatName
|
|
16
|
-
} from "./roy-agent-core-
|
|
16
|
+
} from "./roy-agent-core-b7jbqfsy.js";
|
|
17
17
|
import {
|
|
18
18
|
getSharedEventStore
|
|
19
19
|
} from "./roy-agent-core-na18zwj0.js";
|
|
@@ -311,6 +311,88 @@ var SELF_REMINDER_MESSAGE = `[Timer Self-Reminder]
|
|
|
311
311
|
|
|
312
312
|
请根据分析结果,创建 1-3 个有价值的任务并执行。`;
|
|
313
313
|
var TIMER_OPTIONS_KEY_GET_MANAGER = "getBackgroundTaskManager";
|
|
314
|
+
var SCHEDULE_TICK_INTERVAL_MS = 30 * 1000;
|
|
315
|
+
var TIMER_OPTIONS_KEY_SCHEDULE = "schedule";
|
|
316
|
+
var TIMER_OPTIONS_KEY_SESSION_ID = "sessionId";
|
|
317
|
+
var TIMER_OPTIONS_KEY_SKIP_WHEN_RUNNING = "skipWhenRunning";
|
|
318
|
+
var TIME_PATTERN = /^([01]\d|2[0-3]):([0-5]\d)$/;
|
|
319
|
+
function isValidScheduleTime(time) {
|
|
320
|
+
return typeof time === "string" && TIME_PATTERN.test(time);
|
|
321
|
+
}
|
|
322
|
+
function parseSchedule(raw) {
|
|
323
|
+
if (!Array.isArray(raw))
|
|
324
|
+
return [];
|
|
325
|
+
const result = [];
|
|
326
|
+
for (const item of raw) {
|
|
327
|
+
if (!item || typeof item !== "object")
|
|
328
|
+
continue;
|
|
329
|
+
const obj = item;
|
|
330
|
+
if (!isValidScheduleTime(obj.time))
|
|
331
|
+
continue;
|
|
332
|
+
if (typeof obj.prompt !== "string" || obj.prompt.trim().length === 0)
|
|
333
|
+
continue;
|
|
334
|
+
const entry = {
|
|
335
|
+
time: obj.time,
|
|
336
|
+
prompt: obj.prompt
|
|
337
|
+
};
|
|
338
|
+
if (typeof obj.label === "string" && obj.label.length > 0) {
|
|
339
|
+
entry.label = obj.label;
|
|
340
|
+
}
|
|
341
|
+
if (typeof obj.sessionId === "string" && obj.sessionId.length > 0) {
|
|
342
|
+
entry.sessionId = obj.sessionId;
|
|
343
|
+
}
|
|
344
|
+
result.push(entry);
|
|
345
|
+
}
|
|
346
|
+
return result;
|
|
347
|
+
}
|
|
348
|
+
function validateScheduleConfig(config) {
|
|
349
|
+
const errors = [];
|
|
350
|
+
const options = config.options ?? {};
|
|
351
|
+
const raw = options[TIMER_OPTIONS_KEY_SCHEDULE];
|
|
352
|
+
if (raw === undefined || raw === null)
|
|
353
|
+
return errors;
|
|
354
|
+
if (!Array.isArray(raw)) {
|
|
355
|
+
errors.push("Timer schedule must be an array of { time, prompt } entries");
|
|
356
|
+
return errors;
|
|
357
|
+
}
|
|
358
|
+
if (raw.length === 0) {
|
|
359
|
+
errors.push("Timer schedule must not be empty");
|
|
360
|
+
return errors;
|
|
361
|
+
}
|
|
362
|
+
raw.forEach((item, index) => {
|
|
363
|
+
if (!item || typeof item !== "object") {
|
|
364
|
+
errors.push(`Timer schedule[${index}] must be an object`);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const obj = item;
|
|
368
|
+
if (!isValidScheduleTime(obj.time)) {
|
|
369
|
+
errors.push(`Timer schedule[${index}].time must be in HH:MM format (24-hour), got: ${String(obj.time)}`);
|
|
370
|
+
}
|
|
371
|
+
if (typeof obj.prompt !== "string" || obj.prompt.trim().length === 0) {
|
|
372
|
+
errors.push(`Timer schedule[${index}].prompt is required and must be a non-empty string`);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
const seenTimes = new Set;
|
|
376
|
+
const duplicatedTimes = new Set;
|
|
377
|
+
for (const item of raw) {
|
|
378
|
+
if (!item || typeof item !== "object")
|
|
379
|
+
continue;
|
|
380
|
+
const time = item.time;
|
|
381
|
+
if (!isValidScheduleTime(time))
|
|
382
|
+
continue;
|
|
383
|
+
if (seenTimes.has(time)) {
|
|
384
|
+
duplicatedTimes.add(time);
|
|
385
|
+
}
|
|
386
|
+
seenTimes.add(time);
|
|
387
|
+
}
|
|
388
|
+
if (duplicatedTimes.size > 0) {
|
|
389
|
+
errors.push(`Timer schedule contains duplicate time entries: ${Array.from(duplicatedTimes).join(", ")}`);
|
|
390
|
+
}
|
|
391
|
+
if (config.interval !== undefined && config.interval !== null) {
|
|
392
|
+
errors.push("Timer schedule and interval are mutually exclusive: use schedule for fixed daily times, or interval for periodic triggering");
|
|
393
|
+
}
|
|
394
|
+
return errors;
|
|
395
|
+
}
|
|
314
396
|
|
|
315
397
|
class TimerInstance {
|
|
316
398
|
config;
|
|
@@ -318,21 +400,38 @@ class TimerInstance {
|
|
|
318
400
|
sessionId;
|
|
319
401
|
getRunningTasks;
|
|
320
402
|
customPrompt;
|
|
403
|
+
schedule;
|
|
404
|
+
scheduleMode;
|
|
405
|
+
invalidScheduleWarned = false;
|
|
406
|
+
scheduleSessionId;
|
|
407
|
+
skipWhenRunning;
|
|
408
|
+
firedKeys = new Set;
|
|
409
|
+
now;
|
|
321
410
|
status = "created";
|
|
322
411
|
timerId;
|
|
323
412
|
eventHandler;
|
|
324
|
-
constructor(config, getRunningTasks) {
|
|
413
|
+
constructor(config, getRunningTasks, now) {
|
|
325
414
|
this.config = config;
|
|
326
|
-
this.
|
|
415
|
+
this.now = now ?? (() => Date.now());
|
|
416
|
+
const options = config.options ?? {};
|
|
417
|
+
const rawSchedule = options[TIMER_OPTIONS_KEY_SCHEDULE];
|
|
418
|
+
const scheduleConfigured = rawSchedule !== undefined && rawSchedule !== null;
|
|
419
|
+
this.schedule = parseSchedule(rawSchedule);
|
|
420
|
+
this.scheduleMode = scheduleConfigured;
|
|
421
|
+
const isScheduleMode = this.scheduleMode;
|
|
422
|
+
this.interval = isScheduleMode ? SCHEDULE_TICK_INTERVAL_MS : typeof config.interval === "number" && config.interval > 0 ? config.interval : DEFAULT_TIMER_INTERVAL_MS;
|
|
327
423
|
this.sessionId = `timer-${config.id}-${process.pid}`;
|
|
328
|
-
const
|
|
424
|
+
const explicitSessionId = options[TIMER_OPTIONS_KEY_SESSION_ID];
|
|
425
|
+
this.scheduleSessionId = typeof explicitSessionId === "string" && explicitSessionId.length > 0 ? explicitSessionId : `timer-${config.id}`;
|
|
426
|
+
this.skipWhenRunning = options[TIMER_OPTIONS_KEY_SKIP_WHEN_RUNNING] === true;
|
|
427
|
+
const optionsPrompt = options.prompt;
|
|
329
428
|
if (typeof optionsPrompt === "string" && optionsPrompt.trim().length > 0) {
|
|
330
429
|
this.customPrompt = optionsPrompt;
|
|
331
430
|
}
|
|
332
431
|
if (getRunningTasks) {
|
|
333
432
|
this.getRunningTasks = getRunningTasks;
|
|
334
433
|
} else {
|
|
335
|
-
const factory =
|
|
434
|
+
const factory = options[TIMER_OPTIONS_KEY_GET_MANAGER];
|
|
336
435
|
if (typeof factory === "function") {
|
|
337
436
|
const manager = factory();
|
|
338
437
|
this.getRunningTasks = () => manager.listTasks();
|
|
@@ -341,6 +440,9 @@ class TimerInstance {
|
|
|
341
440
|
}
|
|
342
441
|
}
|
|
343
442
|
}
|
|
443
|
+
get isScheduleMode() {
|
|
444
|
+
return this.scheduleMode;
|
|
445
|
+
}
|
|
344
446
|
async start() {
|
|
345
447
|
if (this.status === "running")
|
|
346
448
|
return;
|
|
@@ -366,6 +468,10 @@ class TimerInstance {
|
|
|
366
468
|
this.eventHandler = undefined;
|
|
367
469
|
}
|
|
368
470
|
tick() {
|
|
471
|
+
if (this.isScheduleMode) {
|
|
472
|
+
this.tickSchedule();
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
369
475
|
let allTasks;
|
|
370
476
|
try {
|
|
371
477
|
allTasks = this.getRunningTasks();
|
|
@@ -380,19 +486,81 @@ class TimerInstance {
|
|
|
380
486
|
const event = this.buildSelfReminderEvent();
|
|
381
487
|
this.eventHandler?.(event);
|
|
382
488
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
489
|
+
tickSchedule() {
|
|
490
|
+
if (this.schedule.length === 0) {
|
|
491
|
+
if (!this.invalidScheduleWarned) {
|
|
492
|
+
this.invalidScheduleWarned = true;
|
|
493
|
+
console.warn(`[TimerInstance] Timer ${this.config.id} has an invalid or empty schedule configuration; ` + `no events will be fired. Check options.schedule (expected [{ time: "HH:MM", prompt: "..." }]).`);
|
|
494
|
+
}
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const now = new Date(this.now());
|
|
498
|
+
const hh = String(now.getHours()).padStart(2, "0");
|
|
499
|
+
const mm = String(now.getMinutes()).padStart(2, "0");
|
|
500
|
+
const currentTime = `${hh}:${mm}`;
|
|
501
|
+
const dateKey = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
502
|
+
for (const entry of this.schedule) {
|
|
503
|
+
if (entry.time !== currentTime)
|
|
504
|
+
continue;
|
|
505
|
+
const firedKey = `${dateKey}#${entry.time}`;
|
|
506
|
+
if (this.firedKeys.has(firedKey)) {
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
if (this.skipWhenRunning) {
|
|
510
|
+
let hasRunning = false;
|
|
511
|
+
try {
|
|
512
|
+
const allTasks = this.getRunningTasks();
|
|
513
|
+
hasRunning = Array.isArray(allTasks) && allTasks.some((t) => t.status === "running");
|
|
514
|
+
} catch (error) {
|
|
515
|
+
console.warn(`[TimerInstance] Failed to get running tasks, skipping tick: ${error}`);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (hasRunning) {
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
this.firedKeys.add(firedKey);
|
|
523
|
+
this.pruneFiredKeys(dateKey);
|
|
524
|
+
const event = this.buildSelfReminderEvent(entry);
|
|
525
|
+
this.eventHandler?.(event);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
pruneFiredKeys(currentDateKey) {
|
|
530
|
+
if (this.firedKeys.size <= 32)
|
|
531
|
+
return;
|
|
532
|
+
for (const key of this.firedKeys) {
|
|
533
|
+
const datePart = key.split("#")[0];
|
|
534
|
+
if (datePart !== currentDateKey) {
|
|
535
|
+
this.firedKeys.delete(key);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
buildSelfReminderEvent(entry) {
|
|
540
|
+
const timestamp = this.now();
|
|
541
|
+
const message = entry?.prompt ?? this.customPrompt ?? SELF_REMINDER_MESSAGE;
|
|
542
|
+
const triggerSessionId = entry !== undefined ? entry.sessionId ?? this.scheduleSessionId : undefined;
|
|
543
|
+
const metadata = {
|
|
544
|
+
source: "event-source",
|
|
545
|
+
sourceId: this.config.id,
|
|
546
|
+
sourceMessageType: "timer.selfReminder",
|
|
547
|
+
sessionId: this.sessionId
|
|
548
|
+
};
|
|
549
|
+
if (triggerSessionId !== undefined) {
|
|
550
|
+
metadata.trigger_session_id = triggerSessionId;
|
|
551
|
+
metadata.createSessionIfMissing = true;
|
|
552
|
+
}
|
|
553
|
+
if (entry?.label !== undefined) {
|
|
554
|
+
metadata.scheduleLabel = entry.label;
|
|
555
|
+
}
|
|
556
|
+
if (entry !== undefined) {
|
|
557
|
+
metadata.scheduleTime = entry.time;
|
|
558
|
+
}
|
|
386
559
|
return {
|
|
387
560
|
id: `es-${this.config.id}-${timestamp}-${Math.random().toString(36).slice(2, 8)}`,
|
|
388
561
|
type: "event-source.event.timer",
|
|
389
562
|
timestamp,
|
|
390
|
-
metadata
|
|
391
|
-
source: "event-source",
|
|
392
|
-
sourceId: this.config.id,
|
|
393
|
-
sourceMessageType: "timer.selfReminder",
|
|
394
|
-
sessionId: this.sessionId
|
|
395
|
-
},
|
|
563
|
+
metadata,
|
|
396
564
|
payload: {
|
|
397
565
|
sourceId: this.config.id,
|
|
398
566
|
sourceType: "timer",
|
|
@@ -415,6 +583,9 @@ class TimerInstance {
|
|
|
415
583
|
__legacyDecorateClassTS([
|
|
416
584
|
TracedAs("timer.tick", { recordParams: false, log: false })
|
|
417
585
|
], TimerInstance.prototype, "tick", null);
|
|
586
|
+
__legacyDecorateClassTS([
|
|
587
|
+
TracedAs("timer.tickSchedule", { recordParams: false, log: false })
|
|
588
|
+
], TimerInstance.prototype, "tickSchedule", null);
|
|
418
589
|
__legacyDecorateClassTS([
|
|
419
590
|
TracedAs("timer.buildSelfReminderEvent", { recordParams: false, log: false })
|
|
420
591
|
], TimerInstance.prototype, "buildSelfReminderEvent", null);
|
|
@@ -423,7 +594,9 @@ var timerHandler = {
|
|
|
423
594
|
validateConfig(config) {
|
|
424
595
|
const errors = [];
|
|
425
596
|
const { interval } = config;
|
|
426
|
-
|
|
597
|
+
errors.push(...validateScheduleConfig(config));
|
|
598
|
+
const hasSchedule = config.options?.[TIMER_OPTIONS_KEY_SCHEDULE] !== undefined;
|
|
599
|
+
if (!hasSchedule && interval !== undefined && interval !== null) {
|
|
427
600
|
if (typeof interval !== "number" || !Number.isFinite(interval)) {
|
|
428
601
|
errors.push("Timer interval must be a finite number");
|
|
429
602
|
} else if (interval <= 0) {
|
|
@@ -439,7 +612,7 @@ var timerHandler = {
|
|
|
439
612
|
return [
|
|
440
613
|
{
|
|
441
614
|
name: "interval",
|
|
442
|
-
description: `Timer interval in milliseconds (default: ${DEFAULT_TIMER_INTERVAL_MS} = 10 minutes)
|
|
615
|
+
description: `Timer interval in milliseconds (default: ${DEFAULT_TIMER_INTERVAL_MS} = 10 minutes). Mutually exclusive with schedule.`,
|
|
443
616
|
required: false,
|
|
444
617
|
type: "number"
|
|
445
618
|
},
|
|
@@ -448,6 +621,18 @@ var timerHandler = {
|
|
|
448
621
|
description: "Custom prompt message for SelfReminder events (replaces default message)",
|
|
449
622
|
required: false,
|
|
450
623
|
type: "string"
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
name: TIMER_OPTIONS_KEY_SCHEDULE,
|
|
627
|
+
description: "JSON array of { time: 'HH:MM', prompt: string, label?: string, sessionId?: string } " + "for fixed daily trigger times (e.g. 10:00 / 17:00 / 20:00). Mutually exclusive with interval.",
|
|
628
|
+
required: false,
|
|
629
|
+
type: "string"
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
name: TIMER_OPTIONS_KEY_SESSION_ID,
|
|
633
|
+
description: "Explicit session ID for timer events (schedule mode). Events are routed to this " + "session instead of the main REPL session.",
|
|
634
|
+
required: false,
|
|
635
|
+
type: "string"
|
|
451
636
|
}
|
|
452
637
|
];
|
|
453
638
|
}
|
|
@@ -1083,10 +1268,658 @@ var bountyCliHandler = {
|
|
|
1083
1268
|
}
|
|
1084
1269
|
};
|
|
1085
1270
|
|
|
1271
|
+
// src/env/event-source/command-registry.ts
|
|
1272
|
+
init_logger();
|
|
1273
|
+
|
|
1274
|
+
// src/env/event-source/fallback-model-command.ts
|
|
1275
|
+
import * as fs from "fs";
|
|
1276
|
+
import * as os from "os";
|
|
1277
|
+
import * as path from "path";
|
|
1278
|
+
import { modify, applyEdits, parse as parseJSONCInternal } from "jsonc-parser";
|
|
1279
|
+
|
|
1280
|
+
// src/env/event-source/command-cards.ts
|
|
1281
|
+
var THEME = {
|
|
1282
|
+
primary: "blue",
|
|
1283
|
+
success: "green",
|
|
1284
|
+
warning: "orange"
|
|
1285
|
+
};
|
|
1286
|
+
var FALLBACK_MODEL_ACTION_NS = "fbmodel";
|
|
1287
|
+
var FALLBACK_MODEL_ACTIONS = {
|
|
1288
|
+
add: `${FALLBACK_MODEL_ACTION_NS}_add`,
|
|
1289
|
+
up: `${FALLBACK_MODEL_ACTION_NS}_up`,
|
|
1290
|
+
down: `${FALLBACK_MODEL_ACTION_NS}_down`
|
|
1291
|
+
};
|
|
1292
|
+
function isFallbackModelCard(card) {
|
|
1293
|
+
if (!card || typeof card !== "object")
|
|
1294
|
+
return false;
|
|
1295
|
+
const c = card;
|
|
1296
|
+
const header = c.header;
|
|
1297
|
+
const title = header?.title;
|
|
1298
|
+
return title?.content === "⚙️ Fallback Model 列表";
|
|
1299
|
+
}
|
|
1300
|
+
function renderTextCard(title, content) {
|
|
1301
|
+
return {
|
|
1302
|
+
schema: "2.0",
|
|
1303
|
+
config: { wide_screen_mode: true },
|
|
1304
|
+
header: {
|
|
1305
|
+
title: { tag: "plain_text", content: title },
|
|
1306
|
+
template: THEME.primary
|
|
1307
|
+
},
|
|
1308
|
+
body: {
|
|
1309
|
+
direction: "vertical",
|
|
1310
|
+
elements: [
|
|
1311
|
+
{
|
|
1312
|
+
tag: "markdown",
|
|
1313
|
+
content
|
|
1314
|
+
}
|
|
1315
|
+
]
|
|
1316
|
+
}
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
function renderFallbackModelCard(list, primaryModel) {
|
|
1320
|
+
const elements = [];
|
|
1321
|
+
if (primaryModel) {
|
|
1322
|
+
elements.push({
|
|
1323
|
+
tag: "markdown",
|
|
1324
|
+
content: `**主模型**:\`${primaryModel}\``
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
if (list.length === 0) {
|
|
1328
|
+
elements.push({
|
|
1329
|
+
tag: "markdown",
|
|
1330
|
+
content: "**fallback 列表**:⚠️ 未配置。主模型失败时不会自动降级。"
|
|
1331
|
+
});
|
|
1332
|
+
} else {
|
|
1333
|
+
elements.push({
|
|
1334
|
+
tag: "markdown",
|
|
1335
|
+
content: "**fallback 列表**(顺序 = 优先级,1 最高):"
|
|
1336
|
+
});
|
|
1337
|
+
list.forEach((model, i) => {
|
|
1338
|
+
const row = [
|
|
1339
|
+
{
|
|
1340
|
+
tag: "markdown",
|
|
1341
|
+
content: `${i + 1}. \`${model}\``
|
|
1342
|
+
}
|
|
1343
|
+
];
|
|
1344
|
+
if (i > 0) {
|
|
1345
|
+
row.push({
|
|
1346
|
+
tag: "button",
|
|
1347
|
+
text: { tag: "plain_text", content: "↑" },
|
|
1348
|
+
type: "default",
|
|
1349
|
+
size: "small",
|
|
1350
|
+
behaviors: [
|
|
1351
|
+
{
|
|
1352
|
+
type: "callback",
|
|
1353
|
+
value: {
|
|
1354
|
+
action: FALLBACK_MODEL_ACTIONS.up,
|
|
1355
|
+
index: i + 1,
|
|
1356
|
+
model
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
]
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
if (i < list.length - 1) {
|
|
1363
|
+
row.push({
|
|
1364
|
+
tag: "button",
|
|
1365
|
+
text: { tag: "plain_text", content: "↓" },
|
|
1366
|
+
type: "default",
|
|
1367
|
+
size: "small",
|
|
1368
|
+
behaviors: [
|
|
1369
|
+
{
|
|
1370
|
+
type: "callback",
|
|
1371
|
+
value: {
|
|
1372
|
+
action: FALLBACK_MODEL_ACTIONS.down,
|
|
1373
|
+
index: i + 1,
|
|
1374
|
+
model
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
]
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
elements.push({
|
|
1381
|
+
tag: "column_set",
|
|
1382
|
+
horizontal_spacing: "small",
|
|
1383
|
+
columns: [
|
|
1384
|
+
{ tag: "column", width: "weighted", weight: 1, elements: row }
|
|
1385
|
+
]
|
|
1386
|
+
});
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
elements.push({ tag: "hr" });
|
|
1390
|
+
elements.push({
|
|
1391
|
+
tag: "form",
|
|
1392
|
+
name: "fbmodel_form",
|
|
1393
|
+
elements: [
|
|
1394
|
+
{
|
|
1395
|
+
tag: "input",
|
|
1396
|
+
name: "fbmodel_model",
|
|
1397
|
+
placeholder: { tag: "plain_text", content: "provider/model,如 tong/GLM-5.3-Flash" },
|
|
1398
|
+
required: true
|
|
1399
|
+
},
|
|
1400
|
+
{
|
|
1401
|
+
tag: "button",
|
|
1402
|
+
text: { tag: "plain_text", content: "新增到 fallback 列表" },
|
|
1403
|
+
type: "primary_filled",
|
|
1404
|
+
form_action_type: "submit",
|
|
1405
|
+
name: FALLBACK_MODEL_ACTIONS.add
|
|
1406
|
+
}
|
|
1407
|
+
]
|
|
1408
|
+
});
|
|
1409
|
+
elements.push({ tag: "hr" });
|
|
1410
|
+
elements.push({
|
|
1411
|
+
tag: "markdown",
|
|
1412
|
+
content: [
|
|
1413
|
+
"**文本指令**(按钮不可用时用这个):",
|
|
1414
|
+
"- 新增:`##command##: fallback-model add <provider/model>`",
|
|
1415
|
+
"- 删除:`##command##: fallback-model remove <序号>`",
|
|
1416
|
+
"- 调优先级:`##command##: fallback-model reorder <原序号> <新序号>`",
|
|
1417
|
+
"- 查看:`##command##: list model`"
|
|
1418
|
+
].join(`
|
|
1419
|
+
`)
|
|
1420
|
+
});
|
|
1421
|
+
return {
|
|
1422
|
+
schema: "2.0",
|
|
1423
|
+
config: { wide_screen_mode: true },
|
|
1424
|
+
header: {
|
|
1425
|
+
title: { tag: "plain_text", content: "⚙️ Fallback Model 列表" },
|
|
1426
|
+
template: THEME.primary
|
|
1427
|
+
},
|
|
1428
|
+
body: {
|
|
1429
|
+
direction: "vertical",
|
|
1430
|
+
elements
|
|
1431
|
+
}
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// src/env/event-source/fallback-model-command.ts
|
|
1436
|
+
var DEFAULT_CONFIG_PATH = path.join(os.homedir(), ".roy-agent", "config.jsonc");
|
|
1437
|
+
function readConfigFile(configPath) {
|
|
1438
|
+
if (!fs.existsSync(configPath))
|
|
1439
|
+
return {};
|
|
1440
|
+
const text = fs.readFileSync(configPath, "utf-8");
|
|
1441
|
+
const errors = [];
|
|
1442
|
+
const parsed = parseJSONCInternal(text, errors, {
|
|
1443
|
+
allowTrailingComma: true,
|
|
1444
|
+
disallowComments: false
|
|
1445
|
+
});
|
|
1446
|
+
if (errors.length > 0 && parsed === undefined) {
|
|
1447
|
+
throw new Error(`config.jsonc 解析失败:${configPath}`);
|
|
1448
|
+
}
|
|
1449
|
+
return parsed ?? {};
|
|
1450
|
+
}
|
|
1451
|
+
function assertObjectParent(configPath, parsed, dotPath) {
|
|
1452
|
+
let cursor = parsed;
|
|
1453
|
+
for (let i = 0;i < dotPath.length - 1; i++) {
|
|
1454
|
+
if (cursor === undefined || cursor === null) {
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
if (typeof cursor !== "object" || Array.isArray(cursor)) {
|
|
1458
|
+
throw new Error(`config.jsonc 的「${dotPath.slice(0, i + 1).join(".")}」不是对象,` + `无法写入 ${dotPath.join(".")}(${configPath})`);
|
|
1459
|
+
}
|
|
1460
|
+
cursor = cursor[dotPath[i]];
|
|
1461
|
+
}
|
|
1462
|
+
if (cursor === undefined)
|
|
1463
|
+
return;
|
|
1464
|
+
if (cursor === null || typeof cursor !== "object" || Array.isArray(cursor)) {
|
|
1465
|
+
throw new Error(`config.jsonc 的「${dotPath.slice(0, -1).join(".")}」不是对象,` + `无法写入 ${dotPath.join(".")}(${configPath})`);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
function writeConfigPath(configPath, dotPath, value) {
|
|
1469
|
+
let text;
|
|
1470
|
+
let parsedForCheck = {};
|
|
1471
|
+
if (fs.existsSync(configPath)) {
|
|
1472
|
+
text = fs.readFileSync(configPath, "utf-8");
|
|
1473
|
+
const errors = [];
|
|
1474
|
+
const parsed = parseJSONCInternal(text, errors, { allowTrailingComma: true });
|
|
1475
|
+
if (errors.length > 0) {
|
|
1476
|
+
throw new Error(`config.jsonc 已损坏(解析失败),拒绝写入:${configPath}`);
|
|
1477
|
+
}
|
|
1478
|
+
parsedForCheck = parsed ?? {};
|
|
1479
|
+
} else {
|
|
1480
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
1481
|
+
text = "{}";
|
|
1482
|
+
}
|
|
1483
|
+
assertObjectParent(configPath, parsedForCheck, dotPath);
|
|
1484
|
+
const edits = modify(text, dotPath, value, {
|
|
1485
|
+
formattingOptions: {
|
|
1486
|
+
tabSize: 2,
|
|
1487
|
+
insertSpaces: true
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
1490
|
+
const result = applyEdits(text, edits);
|
|
1491
|
+
fs.writeFileSync(configPath, result, "utf-8");
|
|
1492
|
+
}
|
|
1493
|
+
function readFallbackModelList(configPath) {
|
|
1494
|
+
const config = readConfigFile(configPath);
|
|
1495
|
+
const llm = config.llm ?? {};
|
|
1496
|
+
const arr = llm.fallbackModels;
|
|
1497
|
+
if (Array.isArray(arr)) {
|
|
1498
|
+
return arr.map((m) => typeof m === "string" ? m.trim() : "");
|
|
1499
|
+
}
|
|
1500
|
+
const single = llm.fallbackModel;
|
|
1501
|
+
if (typeof single === "string" && single.trim() !== "") {
|
|
1502
|
+
return [single.trim()];
|
|
1503
|
+
}
|
|
1504
|
+
return [];
|
|
1505
|
+
}
|
|
1506
|
+
function readRawFallbackModelArray(configPath) {
|
|
1507
|
+
const config = readConfigFile(configPath);
|
|
1508
|
+
const llm = config.llm ?? {};
|
|
1509
|
+
const arr = llm.fallbackModels;
|
|
1510
|
+
if (Array.isArray(arr))
|
|
1511
|
+
return [...arr];
|
|
1512
|
+
const single = llm.fallbackModel;
|
|
1513
|
+
if (typeof single === "string" && single.trim() !== "") {
|
|
1514
|
+
return [single.trim()];
|
|
1515
|
+
}
|
|
1516
|
+
return [];
|
|
1517
|
+
}
|
|
1518
|
+
function writeFallbackModelList(configPath, list) {
|
|
1519
|
+
const raw = readRawFallbackModelArray(configPath);
|
|
1520
|
+
const next = raw.slice(0, list.length);
|
|
1521
|
+
for (let i = 0;i < list.length; i++) {
|
|
1522
|
+
const existing = next[i];
|
|
1523
|
+
if (typeof existing !== "string" && existing !== undefined)
|
|
1524
|
+
continue;
|
|
1525
|
+
next[i] = list[i];
|
|
1526
|
+
}
|
|
1527
|
+
while (next.length < list.length)
|
|
1528
|
+
next.push(list[next.length]);
|
|
1529
|
+
writeConfigPath(configPath, ["llm", "fallbackModels"], next);
|
|
1530
|
+
}
|
|
1531
|
+
var USAGE = [
|
|
1532
|
+
"用法:##command##: fallback-model <子命令> [参数]",
|
|
1533
|
+
"",
|
|
1534
|
+
"子命令:",
|
|
1535
|
+
" list 查看兜底模型列表(顺序 = 优先级)",
|
|
1536
|
+
" add <provider/model> 新增到列表末尾(最低优先级)",
|
|
1537
|
+
" remove <序号> 按序号删除(1-based)",
|
|
1538
|
+
" reorder <原序号> <新序号> 调整优先级(1-based)",
|
|
1539
|
+
" move <模型标识> <up|down> 按模型标识上下移动(卡片按钮用)"
|
|
1540
|
+
].join(`
|
|
1541
|
+
`);
|
|
1542
|
+
function effectiveList(configPath) {
|
|
1543
|
+
return readFallbackModelList(configPath).filter((m) => m !== "");
|
|
1544
|
+
}
|
|
1545
|
+
function rawWithIndexMap(configPath) {
|
|
1546
|
+
const raw = readRawFallbackModelArray(configPath);
|
|
1547
|
+
const map = [];
|
|
1548
|
+
raw.forEach((m, i) => {
|
|
1549
|
+
if (typeof m === "string" && m.trim() !== "")
|
|
1550
|
+
map.push(i);
|
|
1551
|
+
});
|
|
1552
|
+
return { raw, map };
|
|
1553
|
+
}
|
|
1554
|
+
function moveInRaw(raw, map, fromEff, toEff) {
|
|
1555
|
+
const rawFrom = map[fromEff];
|
|
1556
|
+
const [moved] = raw.splice(rawFrom, 1);
|
|
1557
|
+
const rawTo = map[toEff] > rawFrom ? map[toEff] - 1 : map[toEff];
|
|
1558
|
+
raw.splice(rawTo, 0, moved);
|
|
1559
|
+
}
|
|
1560
|
+
function dualResult(text, configPath) {
|
|
1561
|
+
const list = effectiveList(configPath);
|
|
1562
|
+
const config = readConfigFile(configPath);
|
|
1563
|
+
const llm = config.llm ?? {};
|
|
1564
|
+
const primary = typeof llm.defaultModel === "string" ? llm.defaultModel : undefined;
|
|
1565
|
+
return {
|
|
1566
|
+
ok: true,
|
|
1567
|
+
output: {
|
|
1568
|
+
kind: "card",
|
|
1569
|
+
text,
|
|
1570
|
+
card: renderFallbackModelCard(list, primary)
|
|
1571
|
+
}
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
function parsePositiveIndex(raw) {
|
|
1575
|
+
const n = Number.parseInt(raw, 10);
|
|
1576
|
+
if (!Number.isFinite(n) || n < 1)
|
|
1577
|
+
return null;
|
|
1578
|
+
return n;
|
|
1579
|
+
}
|
|
1580
|
+
async function executeFallbackModel(ctx) {
|
|
1581
|
+
const configPath = ctx.configPath ?? DEFAULT_CONFIG_PATH;
|
|
1582
|
+
const [sub, ...rest] = ctx.args;
|
|
1583
|
+
switch (sub) {
|
|
1584
|
+
case "list":
|
|
1585
|
+
case "show": {
|
|
1586
|
+
const list = effectiveList(configPath);
|
|
1587
|
+
const config = readConfigFile(configPath);
|
|
1588
|
+
const llm = config.llm ?? {};
|
|
1589
|
+
const primary = typeof llm.defaultModel === "string" ? llm.defaultModel : undefined;
|
|
1590
|
+
const text = list.length ? `\uD83D\uDCCB 兜底模型列表(顺序 = 优先级,1 最高):
|
|
1591
|
+
${list.map((m, i) => ` ${i + 1}. ${m}`).join(`
|
|
1592
|
+
`)}` : "⚠️ 兜底模型列表未配置(主模型失败时不会自动降级)";
|
|
1593
|
+
return {
|
|
1594
|
+
ok: true,
|
|
1595
|
+
output: {
|
|
1596
|
+
kind: "card",
|
|
1597
|
+
text,
|
|
1598
|
+
card: renderFallbackModelCard(list, primary)
|
|
1599
|
+
}
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
case "add": {
|
|
1603
|
+
const model = rest[0]?.trim();
|
|
1604
|
+
if (!model) {
|
|
1605
|
+
return { ok: false, error: `用法:##command##: fallback-model add <provider/model>
|
|
1606
|
+
${USAGE}` };
|
|
1607
|
+
}
|
|
1608
|
+
const list = effectiveList(configPath);
|
|
1609
|
+
if (list.some((m) => m === model)) {
|
|
1610
|
+
return { ok: false, error: `「${model}」已在 fallback 列表中(重复添加被拒绝)` };
|
|
1611
|
+
}
|
|
1612
|
+
const raw = readRawFallbackModelArray(configPath);
|
|
1613
|
+
const next = [...raw, model];
|
|
1614
|
+
writeConfigPath(configPath, ["llm", "fallbackModels"], next);
|
|
1615
|
+
return dualResult(`✅ 已添加「${model}」到 fallback 列表末尾(当前 ${list.length + 1} 项:${list.join(" → ")} → ${model})`, configPath);
|
|
1616
|
+
}
|
|
1617
|
+
case "remove":
|
|
1618
|
+
case "rm": {
|
|
1619
|
+
const idx = rest[0] !== undefined ? parsePositiveIndex(rest[0]) : null;
|
|
1620
|
+
if (idx === null) {
|
|
1621
|
+
return { ok: false, error: `用法:##command##: fallback-model remove <序号>(1-based)
|
|
1622
|
+
${USAGE}` };
|
|
1623
|
+
}
|
|
1624
|
+
const list = effectiveList(configPath);
|
|
1625
|
+
if (idx > list.length) {
|
|
1626
|
+
return {
|
|
1627
|
+
ok: false,
|
|
1628
|
+
error: `序号 ${idx} 越界(当前列表 ${list.length} 项:${list.map((m, i) => `${i + 1}.${m}`).join(",") || "空"})`
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
const { raw, map } = rawWithIndexMap(configPath);
|
|
1632
|
+
const rawIdx = map[idx - 1];
|
|
1633
|
+
const removed = list[idx - 1];
|
|
1634
|
+
raw.splice(rawIdx, 1);
|
|
1635
|
+
writeConfigPath(configPath, ["llm", "fallbackModels"], raw);
|
|
1636
|
+
return dualResult(`✅ 已删除「${removed}」(剩余 ${list.length - 1} 项${list.length > 1 ? `:${list.filter((_, i) => i !== idx - 1).join(" → ")}` : ""})`, configPath);
|
|
1637
|
+
}
|
|
1638
|
+
case "move": {
|
|
1639
|
+
const model = rest[0]?.trim();
|
|
1640
|
+
const direction = rest[1]?.trim().toLowerCase();
|
|
1641
|
+
if (!model || direction !== "up" && direction !== "down") {
|
|
1642
|
+
return {
|
|
1643
|
+
ok: false,
|
|
1644
|
+
error: `用法:##command##: fallback-model move <模型标识> <up|down>
|
|
1645
|
+
${USAGE}`
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
const list = effectiveList(configPath);
|
|
1649
|
+
const idx = list.findIndex((m) => m === model);
|
|
1650
|
+
if (idx < 0) {
|
|
1651
|
+
return {
|
|
1652
|
+
ok: false,
|
|
1653
|
+
error: `列表中找不到「${model}」(当前:${list.map((m, i) => `${i + 1}.${m}`).join(",") || "空"})`
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
const targetIdx = direction === "up" ? idx - 1 : idx + 1;
|
|
1657
|
+
if (targetIdx < 0 || targetIdx >= list.length) {
|
|
1658
|
+
return {
|
|
1659
|
+
ok: false,
|
|
1660
|
+
error: `「${model}」已经在${direction === "up" ? "第 1 位" : "最后一位"},无法再${direction === "up" ? "上移" : "下移"}`
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
const { raw, map } = rawWithIndexMap(configPath);
|
|
1664
|
+
moveInRaw(raw, map, idx, targetIdx);
|
|
1665
|
+
writeConfigPath(configPath, ["llm", "fallbackModels"], raw);
|
|
1666
|
+
const nextOrder = effectiveList(configPath);
|
|
1667
|
+
return dualResult(`✅ 已把「${model}」移到第 ${targetIdx + 1} 位(当前顺序:${nextOrder.join(" → ")})`, configPath);
|
|
1668
|
+
}
|
|
1669
|
+
case "reorder":
|
|
1670
|
+
case "move-index": {
|
|
1671
|
+
const from = rest[0] !== undefined ? parsePositiveIndex(rest[0]) : null;
|
|
1672
|
+
const to = rest[1] !== undefined ? parsePositiveIndex(rest[1]) : null;
|
|
1673
|
+
if (from === null || to === null) {
|
|
1674
|
+
return {
|
|
1675
|
+
ok: false,
|
|
1676
|
+
error: `用法:##command##: fallback-model reorder <原序号> <新序号>(1-based)
|
|
1677
|
+
${USAGE}`
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
const list = effectiveList(configPath);
|
|
1681
|
+
if (from > list.length || to > list.length) {
|
|
1682
|
+
return {
|
|
1683
|
+
ok: false,
|
|
1684
|
+
error: `序号越界(当前列表 ${list.length} 项:${list.map((m, i) => `${i + 1}.${m}`).join(",") || "空"})`
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
const { raw, map } = rawWithIndexMap(configPath);
|
|
1688
|
+
const moved = raw[map[from - 1]];
|
|
1689
|
+
moveInRaw(raw, map, from - 1, to - 1);
|
|
1690
|
+
writeConfigPath(configPath, ["llm", "fallbackModels"], raw);
|
|
1691
|
+
const nextOrder = effectiveList(configPath);
|
|
1692
|
+
return dualResult(`✅ 已把「${moved}」移到第 ${to} 位(当前顺序:${nextOrder.join(" → ")})`, configPath);
|
|
1693
|
+
}
|
|
1694
|
+
case undefined:
|
|
1695
|
+
return { ok: false, error: USAGE };
|
|
1696
|
+
default:
|
|
1697
|
+
return { ok: false, error: `未知子命令「${sub}」。
|
|
1698
|
+
${USAGE}` };
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
var fallbackModelCommand = {
|
|
1702
|
+
name: "fallback-model",
|
|
1703
|
+
description: "管理兜底模型列表(llm.fallbackModels,顺序 = 优先级)",
|
|
1704
|
+
usage: "fallback-model list|add|remove|reorder [参数]",
|
|
1705
|
+
execute: executeFallbackModel
|
|
1706
|
+
};
|
|
1707
|
+
var listCommand = {
|
|
1708
|
+
name: "list",
|
|
1709
|
+
description: "查看资源列表(list model = fallback-model list)",
|
|
1710
|
+
usage: "list model",
|
|
1711
|
+
execute: async (ctx) => {
|
|
1712
|
+
const resource = ctx.args[0];
|
|
1713
|
+
if (resource === "model") {
|
|
1714
|
+
return executeFallbackModel({ ...ctx, args: ["list"] });
|
|
1715
|
+
}
|
|
1716
|
+
return {
|
|
1717
|
+
ok: false,
|
|
1718
|
+
error: `未知资源「${resource ?? "(空)"}」。可用:list model`
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
};
|
|
1722
|
+
|
|
1723
|
+
// src/env/event-source/command-registry.ts
|
|
1724
|
+
var logger2 = createLogger("command-registry");
|
|
1725
|
+
var COMMAND_PREFIX = "##command##:";
|
|
1726
|
+
var COMMAND_PREFIX_LOWER = COMMAND_PREFIX.toLowerCase();
|
|
1727
|
+
function stripLeadingMentions(text) {
|
|
1728
|
+
let rest = text;
|
|
1729
|
+
const atTagRe = /^\s*<at\s+user_id\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)\s*(?:\/>|>\s*<\/at\s*>|>[^<]*<\/at\s*>)/i;
|
|
1730
|
+
for (;; ) {
|
|
1731
|
+
const m = rest.match(atTagRe);
|
|
1732
|
+
if (!m)
|
|
1733
|
+
break;
|
|
1734
|
+
rest = rest.slice(m[0].length);
|
|
1735
|
+
}
|
|
1736
|
+
const legacyAtRe = /^\s*@_(?:user_\d+|all)\b/i;
|
|
1737
|
+
for (;; ) {
|
|
1738
|
+
const m = rest.match(legacyAtRe);
|
|
1739
|
+
if (!m)
|
|
1740
|
+
break;
|
|
1741
|
+
rest = rest.slice(m[0].length);
|
|
1742
|
+
}
|
|
1743
|
+
return rest;
|
|
1744
|
+
}
|
|
1745
|
+
function isCommandMessage(text) {
|
|
1746
|
+
return stripLeadingMentions(text).trimStart().toLowerCase().startsWith(COMMAND_PREFIX_LOWER);
|
|
1747
|
+
}
|
|
1748
|
+
function extractCommandInvocation(text) {
|
|
1749
|
+
const trimmed = stripLeadingMentions(text).trimStart();
|
|
1750
|
+
if (!trimmed.toLowerCase().startsWith(COMMAND_PREFIX_LOWER))
|
|
1751
|
+
return null;
|
|
1752
|
+
const rest = trimmed.slice(COMMAND_PREFIX.length).trim();
|
|
1753
|
+
if (!rest) {
|
|
1754
|
+
return { subcommand: "", args: [] };
|
|
1755
|
+
}
|
|
1756
|
+
const tokens = rest.split(/\s+/).filter(Boolean);
|
|
1757
|
+
return { subcommand: tokens[0], args: tokens.slice(1) };
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
class CommandRegistry {
|
|
1761
|
+
commands = new Map;
|
|
1762
|
+
register(def) {
|
|
1763
|
+
if (this.commands.has(def.name)) {
|
|
1764
|
+
throw new Error(`command "${def.name}" already registered`);
|
|
1765
|
+
}
|
|
1766
|
+
this.commands.set(def.name, def);
|
|
1767
|
+
}
|
|
1768
|
+
has(name) {
|
|
1769
|
+
return this.commands.has(name);
|
|
1770
|
+
}
|
|
1771
|
+
list() {
|
|
1772
|
+
return Array.from(this.commands.values());
|
|
1773
|
+
}
|
|
1774
|
+
async dispatch(subcommand, args, extra) {
|
|
1775
|
+
if (!subcommand) {
|
|
1776
|
+
return {
|
|
1777
|
+
ok: false,
|
|
1778
|
+
error: this.renderHelp()
|
|
1779
|
+
};
|
|
1780
|
+
}
|
|
1781
|
+
const def = this.commands.get(subcommand);
|
|
1782
|
+
if (!def) {
|
|
1783
|
+
return {
|
|
1784
|
+
ok: false,
|
|
1785
|
+
error: `未知命令「${subcommand}」。
|
|
1786
|
+
${this.renderHelp()}`
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
try {
|
|
1790
|
+
return await def.execute({
|
|
1791
|
+
args,
|
|
1792
|
+
rawText: extra?.rawText ?? "",
|
|
1793
|
+
configPath: extra?.configPath
|
|
1794
|
+
});
|
|
1795
|
+
} catch (err) {
|
|
1796
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1797
|
+
logger2.error(`[CommandRegistry] command "${subcommand}" failed: ${msg}`);
|
|
1798
|
+
return { ok: false, error: `命令执行失败:${msg}` };
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
renderHelp() {
|
|
1802
|
+
const lines = this.list().map((c) => ` ${c.usage.padEnd(40)} ${c.description}`);
|
|
1803
|
+
return `可用命令:
|
|
1804
|
+
${lines.join(`
|
|
1805
|
+
`)}
|
|
1806
|
+
` + `(通过「##command##: <命令> [参数]」调用)`;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
function getGlobalCommandRegistry() {
|
|
1810
|
+
const g = globalThis;
|
|
1811
|
+
if (!g.__royCommandRegistry) {
|
|
1812
|
+
const registry = new CommandRegistry;
|
|
1813
|
+
registry.register(fallbackModelCommand);
|
|
1814
|
+
registry.register(listCommand);
|
|
1815
|
+
g.__royCommandRegistry = registry;
|
|
1816
|
+
}
|
|
1817
|
+
return g.__royCommandRegistry;
|
|
1818
|
+
}
|
|
1819
|
+
async function handleCommandMessage(text, opts = {}) {
|
|
1820
|
+
if (!isCommandMessage(text))
|
|
1821
|
+
return null;
|
|
1822
|
+
const invocation = extractCommandInvocation(text);
|
|
1823
|
+
if (!invocation)
|
|
1824
|
+
return null;
|
|
1825
|
+
const registry = opts.registry ?? getGlobalCommandRegistry();
|
|
1826
|
+
logger2.info(`[handleCommandMessage] dispatching "${invocation.subcommand}" args=${JSON.stringify(invocation.args)}`);
|
|
1827
|
+
return registry.dispatch(invocation.subcommand, invocation.args, {
|
|
1828
|
+
rawText: text,
|
|
1829
|
+
configPath: opts.configPath
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
// src/env/event-source/command-card-action.ts
|
|
1834
|
+
init_logger();
|
|
1835
|
+
var logger3 = createLogger("command-card-action");
|
|
1836
|
+
function isCommandCardAction(event) {
|
|
1837
|
+
const av = event.actionValue;
|
|
1838
|
+
if (av && typeof av === "object") {
|
|
1839
|
+
const action = av.action;
|
|
1840
|
+
if (typeof action === "string" && action.startsWith(FALLBACK_MODEL_ACTION_NS)) {
|
|
1841
|
+
return true;
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
if (typeof event.actionName === "string" && event.actionName.startsWith(FALLBACK_MODEL_ACTION_NS)) {
|
|
1845
|
+
return true;
|
|
1846
|
+
}
|
|
1847
|
+
return false;
|
|
1848
|
+
}
|
|
1849
|
+
function translateCardActionToCommand(event) {
|
|
1850
|
+
if (event.actionName === FALLBACK_MODEL_ACTIONS.add) {
|
|
1851
|
+
const model2 = String(event.formValue?.["fbmodel_model"] ?? event.formValue?.model ?? "").trim();
|
|
1852
|
+
if (!model2)
|
|
1853
|
+
return null;
|
|
1854
|
+
return { subcommand: "fallback-model", args: ["add", model2] };
|
|
1855
|
+
}
|
|
1856
|
+
const av = event.actionValue;
|
|
1857
|
+
if (!av || typeof av !== "object")
|
|
1858
|
+
return null;
|
|
1859
|
+
const action = av.action;
|
|
1860
|
+
const rawIndex = av.index;
|
|
1861
|
+
if (action !== FALLBACK_MODEL_ACTIONS.up && action !== FALLBACK_MODEL_ACTIONS.down) {
|
|
1862
|
+
return null;
|
|
1863
|
+
}
|
|
1864
|
+
const index = Number.parseInt(String(rawIndex ?? ""), 10);
|
|
1865
|
+
if (!Number.isFinite(index) || index < 1)
|
|
1866
|
+
return null;
|
|
1867
|
+
const target = action === FALLBACK_MODEL_ACTIONS.up ? index - 1 : index + 1;
|
|
1868
|
+
if (target < 1)
|
|
1869
|
+
return null;
|
|
1870
|
+
const model = av.model;
|
|
1871
|
+
if (typeof model === "string" && model.trim() !== "") {
|
|
1872
|
+
return {
|
|
1873
|
+
subcommand: "fallback-model",
|
|
1874
|
+
args: [
|
|
1875
|
+
"move",
|
|
1876
|
+
model.trim(),
|
|
1877
|
+
action === FALLBACK_MODEL_ACTIONS.up ? "up" : "down"
|
|
1878
|
+
]
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
return {
|
|
1882
|
+
subcommand: "fallback-model",
|
|
1883
|
+
args: ["reorder", String(index), String(target)]
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
async function handleCommandCardAction(event, ports) {
|
|
1887
|
+
const call = translateCardActionToCommand(event);
|
|
1888
|
+
if (!call) {
|
|
1889
|
+
return { ok: false, error: "无法识别的 command 卡片动作" };
|
|
1890
|
+
}
|
|
1891
|
+
const executed = `${call.subcommand} ${call.args.join(" ")}`;
|
|
1892
|
+
logger3.info(`[handleCommandCardAction] 执行 ${executed}`);
|
|
1893
|
+
let result;
|
|
1894
|
+
try {
|
|
1895
|
+
result = await ports.executeCommand(call.subcommand, call.args, {
|
|
1896
|
+
configPath: ports.configPath
|
|
1897
|
+
});
|
|
1898
|
+
} catch (err) {
|
|
1899
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1900
|
+
logger3.error(`[handleCommandCardAction] ${executed} 执行异常:${msg}`);
|
|
1901
|
+
return { ok: false, executed, error: `命令执行异常:${msg}` };
|
|
1902
|
+
}
|
|
1903
|
+
if (!result.ok) {
|
|
1904
|
+
return { ok: false, executed, error: result.error ?? "命令执行失败" };
|
|
1905
|
+
}
|
|
1906
|
+
const card = result.output?.card;
|
|
1907
|
+
let patchError;
|
|
1908
|
+
if (card && ports.patchCard && event.token) {
|
|
1909
|
+
try {
|
|
1910
|
+
await ports.patchCard(event.token, card, event.profile);
|
|
1911
|
+
} catch (err) {
|
|
1912
|
+
patchError = err.message;
|
|
1913
|
+
logger3.error(`[handleCommandCardAction] patch 卡片失败:${patchError}`);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
return { ok: true, executed, card, patchError };
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1086
1919
|
// src/env/event-source/event-source-handlers.ts
|
|
1087
1920
|
init_logger();
|
|
1088
1921
|
var execAsync2 = promisify2(execSync);
|
|
1089
|
-
var
|
|
1922
|
+
var logger4 = createLogger("lark-cli:event-source");
|
|
1090
1923
|
var CHILD_READY_TIMEOUT_MS = 5000;
|
|
1091
1924
|
var GRACEFUL_STOP_TIMEOUT_MS = 5000;
|
|
1092
1925
|
var CARD_ACTION_EVENT_TYPE = "card.action.trigger";
|
|
@@ -1315,8 +2148,8 @@ class LarkCliInstance {
|
|
|
1315
2148
|
cardActionPorts;
|
|
1316
2149
|
setCardActionPorts(ports) {
|
|
1317
2150
|
this.cardActionPorts = ports;
|
|
1318
|
-
if (typeof
|
|
1319
|
-
|
|
2151
|
+
if (typeof logger4 !== "undefined" && logger4?.debug) {
|
|
2152
|
+
logger4.debug(`[LarkCliInstance] CardActionPorts injected: ` + `getTask=${typeof ports.getTask}, updateTaskContext=${typeof ports.updateTaskContext}, ` + `createGroupChat=${typeof ports.createGroupChat}, deleteGroupChat=${typeof ports.deleteGroupChat}, ` + `notifier=${typeof ports.notifier}`);
|
|
1320
2153
|
}
|
|
1321
2154
|
}
|
|
1322
2155
|
hasCardActionPorts() {
|
|
@@ -1343,7 +2176,7 @@ class LarkCliInstance {
|
|
|
1343
2176
|
if (this.status === "running")
|
|
1344
2177
|
return;
|
|
1345
2178
|
if (this.starting) {
|
|
1346
|
-
|
|
2179
|
+
logger4.warn(`[LarkCliInstance] start() 已在执行中,忽略重复调用`);
|
|
1347
2180
|
return;
|
|
1348
2181
|
}
|
|
1349
2182
|
this.starting = true;
|
|
@@ -1373,9 +2206,9 @@ class LarkCliInstance {
|
|
|
1373
2206
|
if (!consumesCardAction)
|
|
1374
2207
|
return;
|
|
1375
2208
|
if (this.config.eventTypes?.length && !matchEventType(CARD_ACTION_EVENT_TYPE, this.config.eventTypes)) {
|
|
1376
|
-
|
|
2209
|
+
logger4.warn(`[LarkCliInstance] eventKeys 含 ${CARD_ACTION_EVENT_TYPE},但 eventTypes=` + `${JSON.stringify(this.config.eventTypes)} 不含它 → 卡片点击会被**静默丢弃**。` + `请把 ${CARD_ACTION_EVENT_TYPE} 加进 eventTypes,或清空 eventTypes。`);
|
|
1377
2210
|
}
|
|
1378
|
-
|
|
2211
|
+
logger4.info(`[LarkCliInstance] 已监听 ${CARD_ACTION_EVENT_TYPE}。` + `⚠️ 若点击卡片无反应,先确认飞书开发者后台「事件与回调 → 回调配置」已开启该事件。`);
|
|
1379
2212
|
}
|
|
1380
2213
|
spawnConsumeChild(command) {
|
|
1381
2214
|
return spawn2("exec " + command, [], {
|
|
@@ -1415,7 +2248,7 @@ class LarkCliInstance {
|
|
|
1415
2248
|
const conflicts = keys.filter((k) => busy.has(k));
|
|
1416
2249
|
if (conflicts.length > 0) {
|
|
1417
2250
|
const profile = this.config.profile;
|
|
1418
|
-
|
|
2251
|
+
logger4.warn(`[LarkCliInstance] eventKey 已被其它 consumer 占用(lark-cli 一个 key 只允许一个 consumer):` + `${conflicts.join(", ")}。本 event-source 启动后可能 exit 2。` + `用 \`lark-cli${profile ? ` --profile ${profile}` : ""} event status\` 查看,必要时 \`event stop\`。`);
|
|
1419
2252
|
}
|
|
1420
2253
|
}
|
|
1421
2254
|
async probeBusyEventKeys() {
|
|
@@ -1466,16 +2299,16 @@ class LarkCliInstance {
|
|
|
1466
2299
|
});
|
|
1467
2300
|
const handleExit = (code, source) => {
|
|
1468
2301
|
if (code !== 0 && code !== null) {
|
|
1469
|
-
|
|
2302
|
+
logger4.warn(`[LarkCliInstance] consume 子进程异常退出 code=${code} (${source}) cmd="${command}"`);
|
|
1470
2303
|
this.status = "error";
|
|
1471
2304
|
} else {
|
|
1472
|
-
|
|
2305
|
+
logger4.info(`[LarkCliInstance] consume 子进程已退出 code=${code} (${source}) cmd="${command}"`);
|
|
1473
2306
|
}
|
|
1474
2307
|
settle();
|
|
1475
2308
|
queueMicrotask(() => {
|
|
1476
2309
|
if (this.children.length > 0 && this.children.every((c) => !isChildAlive(c))) {
|
|
1477
2310
|
if (this.status !== "error") {
|
|
1478
|
-
|
|
2311
|
+
logger4.warn(`[LarkCliInstance] 所有 consume 子进程都已退出,状态从 running 改为 stopped`);
|
|
1479
2312
|
this.status = "stopped";
|
|
1480
2313
|
}
|
|
1481
2314
|
}
|
|
@@ -1484,12 +2317,12 @@ class LarkCliInstance {
|
|
|
1484
2317
|
child.on("exit", (code) => handleExit(code, "exit"));
|
|
1485
2318
|
child.on("close", (code) => handleExit(code, "close"));
|
|
1486
2319
|
child.on("error", (error) => {
|
|
1487
|
-
|
|
2320
|
+
logger4.error(`[LarkCliInstance] consume 子进程 error: ${error.message}`);
|
|
1488
2321
|
this.status = "error";
|
|
1489
2322
|
settle();
|
|
1490
2323
|
});
|
|
1491
2324
|
const timer = setTimeout(() => {
|
|
1492
|
-
|
|
2325
|
+
logger4.warn(`[LarkCliInstance] consume 子进程 ${CHILD_READY_TIMEOUT_MS}ms 内未见 ready marker: cmd="${command}"`);
|
|
1493
2326
|
settle();
|
|
1494
2327
|
}, CHILD_READY_TIMEOUT_MS);
|
|
1495
2328
|
ready.then(() => clearTimeout(timer));
|
|
@@ -1512,7 +2345,7 @@ class LarkCliInstance {
|
|
|
1512
2345
|
try {
|
|
1513
2346
|
child.kill("SIGTERM");
|
|
1514
2347
|
} catch (error) {
|
|
1515
|
-
|
|
2348
|
+
logger4.warn(`[LarkCliInstance] Error sending SIGTERM to ${child.pid}: ${error}`);
|
|
1516
2349
|
}
|
|
1517
2350
|
}
|
|
1518
2351
|
const allExited = Promise.all(children.map((child) => new Promise((resolve) => {
|
|
@@ -1567,12 +2400,12 @@ class LarkCliInstance {
|
|
|
1567
2400
|
}
|
|
1568
2401
|
wireCardActionPorts() {
|
|
1569
2402
|
if (this.cardActionPorts) {
|
|
1570
|
-
|
|
2403
|
+
logger4.debug("[LarkCliInstance] cardActionPorts 已由外部注入,跳过默认 cross-wire");
|
|
1571
2404
|
return;
|
|
1572
2405
|
}
|
|
1573
2406
|
const taskComponent = this.environment?.getComponent?.("task");
|
|
1574
2407
|
if (!taskComponent?.getTask || !taskComponent?.updateTask) {
|
|
1575
|
-
|
|
2408
|
+
logger4.debug(`[LarkCliInstance] card.action.trigger 端口未注入:taskComponent 不可用` + `(getTask=${typeof taskComponent?.getTask}, updateTask=${typeof taskComponent?.updateTask})。` + `卡片按钮点击会被跳过。`);
|
|
1576
2409
|
return;
|
|
1577
2410
|
}
|
|
1578
2411
|
const sharedNotifier = this.pluginInstances.get("LarkCliTaskNotifyPlugin")?.cardNotifier ?? this.pluginInstances.get("PerTaskGroupChatPlugin")?.cardNotifier;
|
|
@@ -1601,7 +2434,7 @@ class LarkCliInstance {
|
|
|
1601
2434
|
deleteGroupChat: (chatId, profile) => deleteGroupChatViaLarkCli(chatId, profile),
|
|
1602
2435
|
notifier: sharedNotifier ?? new LarkCardPatchNotifier
|
|
1603
2436
|
});
|
|
1604
|
-
|
|
2437
|
+
logger4.info(`[LarkCliInstance] card.action.trigger 端口已注入` + `(notifier ${sharedNotifier ? "复用插件共享实例" : "新建实例"})`);
|
|
1605
2438
|
}
|
|
1606
2439
|
getPluginHookPoints(plugin) {
|
|
1607
2440
|
if (Array.isArray(plugin.hooks)) {
|
|
@@ -1799,7 +2632,7 @@ class LarkCliInstance {
|
|
|
1799
2632
|
}
|
|
1800
2633
|
return null;
|
|
1801
2634
|
} catch (err) {
|
|
1802
|
-
|
|
2635
|
+
logger4.warn(`[LarkCliInstance] resolveChatTaskBinding failed for chatId=${chatId}: ${err}. ` + `群消息将不会注入 parentTaskId。`);
|
|
1803
2636
|
return null;
|
|
1804
2637
|
}
|
|
1805
2638
|
}
|
|
@@ -1821,7 +2654,7 @@ class LarkCliInstance {
|
|
|
1821
2654
|
processStreamWith(data, buffer, opts = {}) {
|
|
1822
2655
|
buffer.value += data;
|
|
1823
2656
|
if (buffer.value.length > 2 * 1024 * 1024) {
|
|
1824
|
-
|
|
2657
|
+
logger4.warn(`[LarkCliInstance] stream 行缓冲超过 2MB 仍无换行,丢弃(疑似非 NDJSON 输出)`);
|
|
1825
2658
|
buffer.value = "";
|
|
1826
2659
|
return;
|
|
1827
2660
|
}
|
|
@@ -1857,12 +2690,12 @@ class LarkCliInstance {
|
|
|
1857
2690
|
const now = Date.now();
|
|
1858
2691
|
const lastShared = LarkCliInstance.SHARED_SEEN.get(dedupKey);
|
|
1859
2692
|
if (lastShared !== undefined && now - lastShared < LarkCliInstance.SHARED_MSG_TTL_MS) {
|
|
1860
|
-
|
|
2693
|
+
logger4.warn(`[LarkCliInstance] duplicate lark-cli event suppressed across instances (event_id=${dedupKey}, ` + `lastSeen=${now - lastShared}ms ago). ` + `Task #4137: SHARED_SEEN 跨实例去重。`);
|
|
1861
2694
|
return;
|
|
1862
2695
|
}
|
|
1863
2696
|
const last = this.recentEventIds.get(dedupKey);
|
|
1864
2697
|
if (last !== undefined && now - last < this.DEDUP_WINDOW_MS) {
|
|
1865
|
-
|
|
2698
|
+
logger4.warn(`[LarkCliInstance] duplicate lark-cli event suppressed (event_id=${dedupKey}, ` + `lastSeen=${now - last}ms ago). ` + `Task #3977/3978: 防止同一飞书事件触发两次任务创建。`);
|
|
1866
2699
|
return;
|
|
1867
2700
|
}
|
|
1868
2701
|
LarkCliInstance.SHARED_SEEN.set(dedupKey, now);
|
|
@@ -1924,6 +2757,7 @@ class LarkCliInstance {
|
|
|
1924
2757
|
};
|
|
1925
2758
|
const { metadata, replyChannel, recommendedAction } = extractMetadata(enrichedEvent, eventType);
|
|
1926
2759
|
failureReplyChannel = replyChannel;
|
|
2760
|
+
const commandText = isCardAction ? undefined : extractCommandText(enrichedEvent);
|
|
1927
2761
|
const parentTaskId = isCardAction ? null : await this.resolveChatTaskBinding(metadata?.chatId);
|
|
1928
2762
|
const userId = this.extractUserId(rawEvent);
|
|
1929
2763
|
let triggerEventId;
|
|
@@ -2021,6 +2855,10 @@ class LarkCliInstance {
|
|
|
2021
2855
|
this.handleCardActionEvent(rawEvent, envReplyChannel?.profile);
|
|
2022
2856
|
return;
|
|
2023
2857
|
}
|
|
2858
|
+
if (commandText !== undefined) {
|
|
2859
|
+
this.handleCommandMessage(commandText, envReplyChannel?.profile, replyChannel?.messageId, replyChannel?.chatId);
|
|
2860
|
+
return;
|
|
2861
|
+
}
|
|
2024
2862
|
this.eventHandler?.(evt);
|
|
2025
2863
|
});
|
|
2026
2864
|
} catch (error) {
|
|
@@ -2035,6 +2873,10 @@ class LarkCliInstance {
|
|
|
2035
2873
|
async handleCardActionEvent(rawEvent, profile) {
|
|
2036
2874
|
try {
|
|
2037
2875
|
const event = rawEvent;
|
|
2876
|
+
if (this.isCommandCardActionEvent(event)) {
|
|
2877
|
+
await this.handleCommandCardActionEvent(event, profile);
|
|
2878
|
+
return;
|
|
2879
|
+
}
|
|
2038
2880
|
const actionValueStr = event.action_value || "{}";
|
|
2039
2881
|
const actionValue = JSON.parse(actionValueStr);
|
|
2040
2882
|
const token = event.token || "";
|
|
@@ -2042,11 +2884,11 @@ class LarkCliInstance {
|
|
|
2042
2884
|
const chatId = event.chat_id || "";
|
|
2043
2885
|
const operatorId = event.operator_id?.open_id ?? event.operator_id ?? undefined;
|
|
2044
2886
|
if (!token || !messageId || !chatId) {
|
|
2045
|
-
|
|
2887
|
+
logger4.warn(`[LarkCliInstance] handleCardActionEvent: missing token/messageId/chatId, skip`);
|
|
2046
2888
|
return;
|
|
2047
2889
|
}
|
|
2048
2890
|
if (!this.cardActionPorts) {
|
|
2049
|
-
|
|
2891
|
+
logger4.warn(`[LarkCliInstance] handleCardActionEvent: cardActionPorts 未注入,` + `card.action.trigger 事件被跳过。` + `调 instance.setCardActionPorts({getTask, updateTaskContext, createGroupChat, deleteGroupChat, notifier}) 注入。`);
|
|
2050
2892
|
return;
|
|
2051
2893
|
}
|
|
2052
2894
|
const result = await handleCardAction({
|
|
@@ -2061,12 +2903,179 @@ class LarkCliInstance {
|
|
|
2061
2903
|
const patch = this.cardActionPorts.patchByToken ?? patchCardByLarkCli;
|
|
2062
2904
|
await patch(token, result.cardUpdate, profile);
|
|
2063
2905
|
} else {
|
|
2064
|
-
|
|
2906
|
+
logger4.warn(`[LarkCliInstance] handleCardActionEvent failed: ${result.error ?? "unknown"}`);
|
|
2065
2907
|
}
|
|
2066
2908
|
} catch (err) {
|
|
2067
|
-
|
|
2909
|
+
logger4.error(`[LarkCliInstance] handleCardActionEvent error: ${err.message}`);
|
|
2068
2910
|
}
|
|
2069
2911
|
}
|
|
2912
|
+
isCommandCardActionEvent(event) {
|
|
2913
|
+
let actionValue;
|
|
2914
|
+
const raw = event.action_value;
|
|
2915
|
+
if (typeof raw === "string") {
|
|
2916
|
+
try {
|
|
2917
|
+
const parsed = JSON.parse(raw);
|
|
2918
|
+
if (parsed && typeof parsed === "object") {
|
|
2919
|
+
actionValue = parsed;
|
|
2920
|
+
}
|
|
2921
|
+
} catch {
|
|
2922
|
+
actionValue = undefined;
|
|
2923
|
+
}
|
|
2924
|
+
} else if (raw && typeof raw === "object") {
|
|
2925
|
+
actionValue = raw;
|
|
2926
|
+
}
|
|
2927
|
+
let formValue;
|
|
2928
|
+
const rawForm = event.form_value;
|
|
2929
|
+
if (typeof rawForm === "string") {
|
|
2930
|
+
try {
|
|
2931
|
+
const parsed = JSON.parse(rawForm);
|
|
2932
|
+
if (parsed && typeof parsed === "object") {
|
|
2933
|
+
formValue = parsed;
|
|
2934
|
+
}
|
|
2935
|
+
} catch {
|
|
2936
|
+
formValue = undefined;
|
|
2937
|
+
}
|
|
2938
|
+
} else if (rawForm && typeof rawForm === "object") {
|
|
2939
|
+
formValue = rawForm;
|
|
2940
|
+
}
|
|
2941
|
+
return isCommandCardAction({
|
|
2942
|
+
actionValue,
|
|
2943
|
+
actionName: typeof event.action_name === "string" ? event.action_name : undefined
|
|
2944
|
+
});
|
|
2945
|
+
}
|
|
2946
|
+
async handleCommandCardActionEvent(event, profile) {
|
|
2947
|
+
let actionValue;
|
|
2948
|
+
try {
|
|
2949
|
+
actionValue = typeof event.action_value === "string" ? JSON.parse(event.action_value) : event.action_value;
|
|
2950
|
+
} catch {
|
|
2951
|
+
actionValue = undefined;
|
|
2952
|
+
}
|
|
2953
|
+
let formValue;
|
|
2954
|
+
try {
|
|
2955
|
+
formValue = typeof event.form_value === "string" ? JSON.parse(event.form_value) : event.form_value;
|
|
2956
|
+
} catch {
|
|
2957
|
+
formValue = undefined;
|
|
2958
|
+
}
|
|
2959
|
+
const result = await handleCommandCardAction({
|
|
2960
|
+
actionValue,
|
|
2961
|
+
formValue,
|
|
2962
|
+
actionName: typeof event.action_name === "string" ? event.action_name : undefined,
|
|
2963
|
+
chatId: typeof event.chat_id === "string" ? event.chat_id : undefined,
|
|
2964
|
+
messageId: typeof event.message_id === "string" ? event.message_id : undefined,
|
|
2965
|
+
token: typeof event.token === "string" ? event.token : undefined,
|
|
2966
|
+
profile
|
|
2967
|
+
}, {
|
|
2968
|
+
executeCommand: async (subcommand, args, extra) => {
|
|
2969
|
+
const registry = getGlobalCommandRegistry();
|
|
2970
|
+
return registry.dispatch(subcommand, args, {
|
|
2971
|
+
rawText: `##command##: ${subcommand} ${args.join(" ")}`,
|
|
2972
|
+
configPath: extra?.configPath ?? this.resolveCommandConfigPath()
|
|
2973
|
+
});
|
|
2974
|
+
},
|
|
2975
|
+
configPath: this.resolveCommandConfigPath(),
|
|
2976
|
+
patchCard: async (token, card, prof) => {
|
|
2977
|
+
await patchCardByLarkCli(token, card, prof);
|
|
2978
|
+
}
|
|
2979
|
+
});
|
|
2980
|
+
if (!result.ok) {
|
|
2981
|
+
const text = `⚠️ 命令卡片操作失败:${result.error ?? "未知错误"}`;
|
|
2982
|
+
const messageId = typeof event.message_id === "string" ? event.message_id : undefined;
|
|
2983
|
+
const chatId = typeof event.chat_id === "string" ? event.chat_id : undefined;
|
|
2984
|
+
try {
|
|
2985
|
+
await this.sendLarkText(text, profile, messageId, chatId);
|
|
2986
|
+
} catch {}
|
|
2987
|
+
return;
|
|
2988
|
+
}
|
|
2989
|
+
if (result.patchError) {
|
|
2990
|
+
const text = `⚠️ 操作已生效(${result.executed ?? "命令"}),但卡片刷新失败:` + `${result.patchError}
|
|
2991
|
+
` + `(飞书卡片 token 有效期 30 分钟、最多刷新 2 次;可用 ` + `「##command##: list model」重新获取一张新卡片)`;
|
|
2992
|
+
const messageId = typeof event.message_id === "string" ? event.message_id : undefined;
|
|
2993
|
+
const chatId = typeof event.chat_id === "string" ? event.chat_id : undefined;
|
|
2994
|
+
try {
|
|
2995
|
+
await this.sendLarkText(text, profile, messageId, chatId);
|
|
2996
|
+
} catch {}
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
async handleCommandMessage(text, profile, messageId, chatId) {
|
|
3000
|
+
try {
|
|
3001
|
+
const configPath = this.resolveCommandConfigPath();
|
|
3002
|
+
const result = await handleCommandMessage(text, { configPath });
|
|
3003
|
+
if (!result) {
|
|
3004
|
+
const card = renderTextCard("⚠️ 命令无法识别", "这条消息没有被识别为有效命令。请用「##command##: <命令> [参数]」格式。");
|
|
3005
|
+
await this.sendLarkCard(card, profile, messageId, chatId);
|
|
3006
|
+
return;
|
|
3007
|
+
}
|
|
3008
|
+
if (!result.ok) {
|
|
3009
|
+
const card = renderTextCard("⚠️ 命令执行失败", result.error ?? "未知错误");
|
|
3010
|
+
await this.sendLarkCard(card, profile, messageId, chatId);
|
|
3011
|
+
return;
|
|
3012
|
+
}
|
|
3013
|
+
const output = result.output;
|
|
3014
|
+
if (!output) {
|
|
3015
|
+
const card = renderTextCard("⚠️ 命令无输出", "命令执行成功但没有产生任何输出。");
|
|
3016
|
+
await this.sendLarkCard(card, profile, messageId, chatId);
|
|
3017
|
+
return;
|
|
3018
|
+
}
|
|
3019
|
+
if (output.card) {
|
|
3020
|
+
await this.sendLarkCard(output.card, profile, messageId, chatId);
|
|
3021
|
+
return;
|
|
3022
|
+
}
|
|
3023
|
+
await this.sendLarkText(output.text, profile, messageId, chatId);
|
|
3024
|
+
} catch (err) {
|
|
3025
|
+
logger4.error(`[LarkCliInstance] handleCommandMessage error: ${err.message}`);
|
|
3026
|
+
try {
|
|
3027
|
+
const card = renderTextCard("⚠️ 命令执行异常", `命令处理时发生异常:${err.message}`);
|
|
3028
|
+
await this.sendLarkCard(card, profile, messageId, chatId);
|
|
3029
|
+
} catch {}
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
resolveCommandConfigPath() {
|
|
3033
|
+
const configured = this.config?.options?.configPath;
|
|
3034
|
+
return typeof configured === "string" ? configured : undefined;
|
|
3035
|
+
}
|
|
3036
|
+
async sendLarkCard(card, profile, messageId, chatId) {
|
|
3037
|
+
const content = JSON.stringify(card);
|
|
3038
|
+
await this.spawnLarkCli(["im"], profile, messageId, chatId, [
|
|
3039
|
+
"--msg-type",
|
|
3040
|
+
"interactive",
|
|
3041
|
+
"--content",
|
|
3042
|
+
content
|
|
3043
|
+
]);
|
|
3044
|
+
}
|
|
3045
|
+
async sendLarkText(text, profile, messageId, chatId) {
|
|
3046
|
+
await this.spawnLarkCli(["im"], profile, messageId, chatId, ["--markdown", text]);
|
|
3047
|
+
}
|
|
3048
|
+
async spawnLarkCli(baseArgs, profile, messageId, chatId, contentArgs) {
|
|
3049
|
+
const args = [...baseArgs];
|
|
3050
|
+
if (messageId) {
|
|
3051
|
+
args.push("+messages-reply", "--message-id", messageId, "--as", "bot");
|
|
3052
|
+
} else if (chatId) {
|
|
3053
|
+
args.push("+messages-send", "--chat-id", chatId, "--as", "bot");
|
|
3054
|
+
} else {
|
|
3055
|
+
throw new Error("No target specified for sending message");
|
|
3056
|
+
}
|
|
3057
|
+
args.push(...contentArgs);
|
|
3058
|
+
if (profile) {
|
|
3059
|
+
args.unshift("--profile", profile);
|
|
3060
|
+
}
|
|
3061
|
+
await new Promise((resolve, reject) => {
|
|
3062
|
+
const proc = spawn2("lark-cli", args, {
|
|
3063
|
+
shell: false,
|
|
3064
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3065
|
+
});
|
|
3066
|
+
let stderr = "";
|
|
3067
|
+
proc.stderr?.on("data", (d) => {
|
|
3068
|
+
stderr += d.toString();
|
|
3069
|
+
});
|
|
3070
|
+
proc.on("close", (code) => {
|
|
3071
|
+
if (code === 0)
|
|
3072
|
+
resolve();
|
|
3073
|
+
else
|
|
3074
|
+
reject(new Error(`lark-cli exited with code ${code}: ${stderr}`));
|
|
3075
|
+
});
|
|
3076
|
+
proc.on("error", reject);
|
|
3077
|
+
});
|
|
3078
|
+
}
|
|
2070
3079
|
async patchCardByToken(token, card, profile) {
|
|
2071
3080
|
return patchCardByLarkCli(token, card, profile);
|
|
2072
3081
|
}
|
|
@@ -2238,6 +3247,34 @@ function renderRuleNoticeSection(ruleNotice, headline) {
|
|
|
2238
3247
|
return `❌ **回执未送达**(原因:${ruleNotice.error ?? "unknown"})
|
|
2239
3248
|
` + `→ 用户目前**还不知道**「${headline}」这件事,必须由你在回复里讲清楚,` + `并且不要假装系统已经通知过。`;
|
|
2240
3249
|
}
|
|
3250
|
+
function extractCommandText(rawEvent) {
|
|
3251
|
+
const event = rawEvent;
|
|
3252
|
+
const innerEvent = event.event;
|
|
3253
|
+
const message = innerEvent?.message || event.message;
|
|
3254
|
+
const rawContent = message?.content ?? event.content;
|
|
3255
|
+
if (rawContent === undefined || rawContent === null)
|
|
3256
|
+
return;
|
|
3257
|
+
let contentText;
|
|
3258
|
+
if (typeof rawContent === "string") {
|
|
3259
|
+
try {
|
|
3260
|
+
const parsed = JSON.parse(rawContent);
|
|
3261
|
+
if (typeof parsed === "string") {
|
|
3262
|
+
contentText = parsed;
|
|
3263
|
+
} else if (parsed && typeof parsed === "object" && "text" in parsed) {
|
|
3264
|
+
const t = parsed.text;
|
|
3265
|
+
contentText = typeof t === "string" ? t : undefined;
|
|
3266
|
+
}
|
|
3267
|
+
} catch {
|
|
3268
|
+
contentText = rawContent;
|
|
3269
|
+
}
|
|
3270
|
+
} else if (typeof rawContent === "object" && "text" in rawContent) {
|
|
3271
|
+
const t = rawContent.text;
|
|
3272
|
+
contentText = typeof t === "string" ? t : undefined;
|
|
3273
|
+
}
|
|
3274
|
+
if (!contentText)
|
|
3275
|
+
return;
|
|
3276
|
+
return isCommandMessage(contentText) ? contentText : undefined;
|
|
3277
|
+
}
|
|
2241
3278
|
var larkCliHandler = {
|
|
2242
3279
|
type: "lark-cli",
|
|
2243
3280
|
validateConfig(config) {
|
|
@@ -2249,7 +3286,7 @@ var larkCliHandler = {
|
|
|
2249
3286
|
},
|
|
2250
3287
|
async emitTriggerEvent(payload, store) {
|
|
2251
3288
|
const eventId = await store.insertTriggerEvent(payload, "lark-cli");
|
|
2252
|
-
|
|
3289
|
+
logger4.info(`[larkCliHandler] emitTriggerEvent: id=${eventId}`, {
|
|
2253
3290
|
chatId: payload.metadata?.chatId,
|
|
2254
3291
|
senderId: payload.senderId
|
|
2255
3292
|
});
|
|
@@ -2264,7 +3301,7 @@ var larkCliHandler = {
|
|
|
2264
3301
|
try {
|
|
2265
3302
|
const result = await runner.run(command);
|
|
2266
3303
|
if (result.code !== 0) {
|
|
2267
|
-
|
|
3304
|
+
logger4.error(`[larkCliHandler] replyMessage non-zero exit`, {
|
|
2268
3305
|
code: result.code,
|
|
2269
3306
|
stderr: result.stderr
|
|
2270
3307
|
});
|
|
@@ -2278,7 +3315,7 @@ var larkCliHandler = {
|
|
|
2278
3315
|
return { success: true, messageId: replyMessageId };
|
|
2279
3316
|
} catch (err) {
|
|
2280
3317
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2281
|
-
|
|
3318
|
+
logger4.error(`[larkCliHandler] replyMessage spawn error`, { error: errMsg });
|
|
2282
3319
|
return { success: false, error: errMsg };
|
|
2283
3320
|
}
|
|
2284
3321
|
},
|
|
@@ -2385,9 +3422,11 @@ async function patchCardByLarkCli(token, card, profile) {
|
|
|
2385
3422
|
const cmd = `lark-cli ${profileFlag}api POST /open-apis/interactive/v1/card/update ` + `--data '${JSON.stringify({ token, card }).replace(/'/g, "'\\''")}' --as bot`;
|
|
2386
3423
|
try {
|
|
2387
3424
|
const { stdout } = await execAsync2(cmd, { timeout: 30000 });
|
|
2388
|
-
|
|
3425
|
+
logger4.debug(`patchCardByLarkCli ok: ${stdout?.slice(0, 200)}`);
|
|
2389
3426
|
} catch (err) {
|
|
2390
|
-
|
|
3427
|
+
const msg = err.message;
|
|
3428
|
+
logger4.error(`patchCardByLarkCli failed: ${msg}`);
|
|
3429
|
+
throw new Error(`卡片刷新失败:${msg}`);
|
|
2391
3430
|
}
|
|
2392
3431
|
}
|
|
2393
3432
|
var builtInHandlers = [
|
|
@@ -2401,4 +3440,4 @@ function getBuiltInHandler(type) {
|
|
|
2401
3440
|
return builtInHandlers.find((h) => h.type === type);
|
|
2402
3441
|
}
|
|
2403
3442
|
|
|
2404
|
-
export { TimerInstance, timerHandler, buildDefaultBountyIMSystemPrompt, resolveBountyIMSystemPrompt, BountyIMInstance, bountyIMHandler, bountyCliHandler, buildLarkRecommendedAction, extractMetadata, LarkCliInstance, renderRuleNoticeSection, larkCliHandler, patchCardByLarkCli, builtInHandlers, getBuiltInHandler };
|
|
3443
|
+
export { TimerInstance, timerHandler, buildDefaultBountyIMSystemPrompt, resolveBountyIMSystemPrompt, BountyIMInstance, bountyIMHandler, bountyCliHandler, FALLBACK_MODEL_ACTION_NS, FALLBACK_MODEL_ACTIONS, isFallbackModelCard, renderTextCard, renderFallbackModelCard, DEFAULT_CONFIG_PATH, readFallbackModelList, readRawFallbackModelArray, writeFallbackModelList, fallbackModelCommand, listCommand, COMMAND_PREFIX, isCommandMessage, extractCommandInvocation, CommandRegistry, getGlobalCommandRegistry, handleCommandMessage, isCommandCardAction, translateCardActionToCommand, handleCommandCardAction, buildLarkRecommendedAction, extractMetadata, LarkCliInstance, renderRuleNoticeSection, extractCommandText, larkCliHandler, patchCardByLarkCli, builtInHandlers, getBuiltInHandler };
|