@oyasmi/pipiclaw 0.6.4 → 0.6.6-beta.1

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.md CHANGED
@@ -208,7 +208,9 @@ $env:PIPICLAW_SHELL = "C:\Program Files\Git\bin\bash.exe"
208
208
  "robotCode": "",
209
209
  "cardTemplateId": "",
210
210
  "cardTemplateKey": "content",
211
- "allowFrom": []
211
+ "allowFrom": [],
212
+ "busyMessageDefault": "steer",
213
+ "progressDisplay": "full"
212
214
  }
213
215
  ```
214
216
 
@@ -227,6 +229,10 @@ $env:PIPICLAW_SHELL = "C:\Program Files\Git\bin\bash.exe"
227
229
  建议配置;留空时表示暂不启用 AI Card
228
230
  - `allowFrom`
229
231
  设为 `[]` 或删除时表示允许所有人
232
+ - `busyMessageDefault`
233
+ 设为 `"steer"`(默认)或 `"followUp"` / `"followup"`。控制 Agent 忙碌时普通消息的默认处理方式;答疑机器人场景建议设为 `"followUp"`。
234
+ - `progressDisplay`
235
+ 设为 `"full"`(默认)或 `"rolling"`。控制 AI Card 进度展示方式;`"rolling"` 模式下执行中只显示最近 3 条进展,完成后收起为一行摘要。
230
236
 
231
237
  推荐把 AI Card 一起配上,这样在钉钉里能直接看到过程更新。只有在排查接入链路时,才建议临时把 `cardTemplateId` 留空。
232
238
 
@@ -24,6 +24,7 @@ export declare class ChannelRunner implements AgentRunner {
24
24
  private activeModel;
25
25
  private currentSkills;
26
26
  private firstTurnMemoryBootstrapPending;
27
+ private acceptingBusyMessages;
27
28
  private runState;
28
29
  constructor(sandboxConfig: SandboxConfig, channelId: string, channelDir: string);
29
30
  run(ctx: DingTalkContext, store: ChannelStore): Promise<{
@@ -53,6 +53,7 @@ function asSdkSettingsManager(manager) {
53
53
  export class ChannelRunner {
54
54
  constructor(sandboxConfig, channelId, channelDir) {
55
55
  this.firstTurnMemoryBootstrapPending = true;
56
+ this.acceptingBusyMessages = false;
56
57
  // --- Per run ---
57
58
  this.runState = createEmptyRunState();
58
59
  this.sandboxConfig = sandboxConfig;
@@ -169,8 +170,10 @@ export class ChannelRunner {
169
170
  // === Public API ===
170
171
  async run(ctx, store) {
171
172
  this.resetRunState(ctx, store);
173
+ this.acceptingBusyMessages = true;
172
174
  const runQueue = createRunQueue(ctx);
173
175
  this.runState.queue = runQueue.queue;
176
+ let promptSubmitted = false;
174
177
  try {
175
178
  await this.ensureSessionReady();
176
179
  this.memoryLifecycle.noteUserTurnStarted();
@@ -225,6 +228,7 @@ export class ChannelRunner {
225
228
  }
226
229
  await this.sessionResourceGate.runPrompt(async () => {
227
230
  await this.session.prompt(promptText);
231
+ promptSubmitted = true;
228
232
  });
229
233
  }
230
234
  catch (err) {
@@ -233,6 +237,14 @@ export class ChannelRunner {
233
237
  log.logWarning(`[${this.channelId}] Runner failed`, this.runState.errorMessage);
234
238
  }
235
239
  finally {
240
+ this.acceptingBusyMessages = false;
241
+ if (!promptSubmitted) {
242
+ const discarded = this.session.clearQueue();
243
+ const discardedCount = discarded.steering.length + discarded.followUp.length;
244
+ if (discardedCount > 0) {
245
+ log.logWarning(`[${this.channelId}] Discarded ${discardedCount} queued busy message(s) after run setup failed`);
246
+ }
247
+ }
236
248
  await runQueue.drain();
237
249
  const finalOutcome = this.runState.finalOutcome;
238
250
  const finalOutcomeText = getFinalOutcomeText(finalOutcome);
@@ -423,20 +435,28 @@ export class ChannelRunner {
423
435
  return `[${timestamp}] [${userName || "unknown"}]: ${text}`;
424
436
  }
425
437
  async queueBusyMessage(delivery, text, userName) {
426
- if (!this.session.isStreaming) {
438
+ if (!this.acceptingBusyMessages) {
427
439
  throw new Error("No task is currently running.");
428
440
  }
441
+ await this.ensureSessionReady();
429
442
  const clippedText = clipUserInput(text, MAX_USER_MESSAGE_CHARS);
430
443
  if (clippedText !== text.trim()) {
431
444
  log.logWarning(`[${this.channelId}] Queued message exceeded ${MAX_USER_MESSAGE_CHARS} chars and was clipped`);
432
445
  }
433
446
  const queuedMessage = this.formatUserMessage(clippedText, userName);
434
447
  await this.maybeRunPreventiveCompactionForIncomingText(queuedMessage);
435
- await this.sessionResourceGate.runPrompt(async () => {
436
- await this.session.prompt(queuedMessage, {
437
- streamingBehavior: delivery,
438
- });
439
- });
448
+ if (!this.acceptingBusyMessages) {
449
+ throw new Error("No task is currently running.");
450
+ }
451
+ const queueMessage = async () => {
452
+ if (delivery === "followUp") {
453
+ await this.session.followUp(queuedMessage);
454
+ }
455
+ else {
456
+ await this.session.steer(queuedMessage);
457
+ }
458
+ };
459
+ await this.sessionResourceGate.runPrompt(queueMessage);
440
460
  }
441
461
  resetRunState(ctx, store) {
442
462
  this.runState = createEmptyRunState();
@@ -19,7 +19,9 @@ These are handled directly by the DingTalk transport/runtime layer.
19
19
  Queue another request to run after the current task completes
20
20
  Example: \`/followup After that, draft a short executive summary\`
21
21
 
22
- While a task is running, a plain message is treated as \`steer\` by default.
22
+ While a task is running, plain messages use the configured busy-message default. The default is \`steer\`; set \`busyMessageDefault\` in channel.json to \`followUp\` or \`followup\` to queue plain messages after the current task.
23
+
24
+ Set \`progressDisplay\` in channel.json to \`rolling\` for compact AI Card progress: recent entries while running, then a short summary after completion.
23
25
 
24
26
  ## Session Commands
25
27
 
package/dist/index.d.ts CHANGED
@@ -15,7 +15,7 @@ export { findExactModelReferenceMatch, findModelReferenceMatch, formatModelList,
15
15
  export { APP_HOME_DIR, APP_NAME, AUTH_CONFIG_PATH, CHANNEL_CONFIG_PATH, MODELS_CONFIG_PATH, SECURITY_CONFIG_PATH, SETTINGS_CONFIG_PATH, SUB_AGENTS_DIR, SUB_AGENTS_DIR_NAME, TOOLS_CONFIG_PATH, WORKSPACE_DIR, } from "./paths.js";
16
16
  export { ensureChannelDir, getChannelDir, getChannelDirName, } from "./runtime/channel-paths.js";
17
17
  export { createDingTalkContext } from "./runtime/delivery.js";
18
- export { type BusyMessageMode, DingTalkBot, type DingTalkConfig, type DingTalkContext, type DingTalkEvent, type DingTalkHandler, } from "./runtime/dingtalk.js";
18
+ export { type BusyMessageDefaultConfig, type BusyMessageMode, DingTalkBot, type DingTalkConfig, type DingTalkContext, type DingTalkEvent, type DingTalkHandler, isBusyMessageDefaultConfig, isProgressDisplayConfig, normalizeBusyMessageDefault, normalizeProgressDisplay, type ProgressDisplayMode, } from "./runtime/dingtalk.js";
19
19
  export { createEventsWatcher, type EventAction, EventsWatcher, type ImmediateEvent, type OneShotEvent, type PeriodicEvent, type ScheduledEvent, } from "./runtime/events.js";
20
20
  export { ChannelStore, type LoggedMessage, type LoggedSubAgentRun } from "./runtime/store.js";
21
21
  export { createExecutor, type ExecOptions, type ExecResult, type Executor, parseSandboxArg, type SandboxConfig, validateSandbox, } from "./sandbox.js";
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ export { findExactModelReferenceMatch, findModelReferenceMatch, formatModelList,
15
15
  export { APP_HOME_DIR, APP_NAME, AUTH_CONFIG_PATH, CHANNEL_CONFIG_PATH, MODELS_CONFIG_PATH, SECURITY_CONFIG_PATH, SETTINGS_CONFIG_PATH, SUB_AGENTS_DIR, SUB_AGENTS_DIR_NAME, TOOLS_CONFIG_PATH, WORKSPACE_DIR, } from "./paths.js";
16
16
  export { ensureChannelDir, getChannelDir, getChannelDirName, } from "./runtime/channel-paths.js";
17
17
  export { createDingTalkContext } from "./runtime/delivery.js";
18
- export { DingTalkBot, } from "./runtime/dingtalk.js";
18
+ export { DingTalkBot, isBusyMessageDefaultConfig, isProgressDisplayConfig, normalizeBusyMessageDefault, normalizeProgressDisplay, } from "./runtime/dingtalk.js";
19
19
  export { createEventsWatcher, EventsWatcher, } from "./runtime/events.js";
20
20
  export { ChannelStore } from "./runtime/store.js";
21
21
  export { createExecutor, parseSandboxArg, validateSandbox, } from "./sandbox.js";
@@ -14,7 +14,7 @@ import { formatConfigDiagnostic } from "../shared/config-diagnostics.js";
14
14
  import { loadToolsConfigWithDiagnostics } from "../tools/config.js";
15
15
  import { ensureChannelDir } from "./channel-paths.js";
16
16
  import { createDingTalkContext } from "./delivery.js";
17
- import { DingTalkBot, } from "./dingtalk.js";
17
+ import { DingTalkBot, isBusyMessageDefaultConfig, isProgressDisplayConfig, normalizeBusyMessageDefault, normalizeProgressDisplay, } from "./dingtalk.js";
18
18
  import { createEventsWatcher } from "./events.js";
19
19
  import { ChannelStore } from "./store.js";
20
20
  const DEFAULT_SOUL = `# SOUL.md
@@ -102,6 +102,8 @@ const CHANNEL_CONFIG_TEMPLATE = {
102
102
  cardTemplateId: "your-card-template-id",
103
103
  cardTemplateKey: "content",
104
104
  allowFrom: ["your-staff-id"],
105
+ busyMessageDefault: "steer",
106
+ progressDisplay: "full",
105
107
  };
106
108
  const MODELS_CONFIG_TEMPLATE = { providers: {} };
107
109
  const TOOLS_CONFIG_TEMPLATE = {
@@ -236,6 +238,14 @@ function listChannelConfigIssues(config) {
236
238
  if (Array.isArray(config.allowFrom) && config.allowFrom.some((value) => isPlaceholderString(value))) {
237
239
  issues.push("Replace placeholder values in `allowFrom`, or set it to an empty array to allow all users.");
238
240
  }
241
+ const busyMessageDefault = config.busyMessageDefault;
242
+ if (busyMessageDefault !== undefined && !isBusyMessageDefaultConfig(busyMessageDefault)) {
243
+ issues.push('Invalid `busyMessageDefault`: expected "steer", "followUp", or "followup".');
244
+ }
245
+ const progressDisplay = config.progressDisplay;
246
+ if (progressDisplay !== undefined && !isProgressDisplayConfig(progressDisplay)) {
247
+ issues.push('Invalid `progressDisplay`: expected "full" or "rolling".');
248
+ }
239
249
  return issues;
240
250
  }
241
251
  export function printBootstrapSummary(result, io = console, paths = DEFAULT_BOOTSTRAP_PATHS) {
@@ -270,6 +280,8 @@ export function loadConfig(paths = DEFAULT_BOOTSTRAP_PATHS, io = console) {
270
280
  }
271
281
  parsed.cardTemplateKey = parsed.cardTemplateKey || "content";
272
282
  parsed.robotCode = parsed.robotCode?.trim() ? parsed.robotCode : parsed.clientId;
283
+ parsed.busyMessageDefault = normalizeBusyMessageDefault(parsed.busyMessageDefault);
284
+ parsed.progressDisplay = normalizeProgressDisplay(parsed.progressDisplay);
273
285
  if (Array.isArray(parsed.allowFrom)) {
274
286
  parsed.allowFrom = parsed.allowFrom.filter((value) => value.trim().length > 0);
275
287
  }
@@ -329,6 +341,9 @@ function flushInactiveChannelMemory(channelStates) {
329
341
  }
330
342
  return flushes;
331
343
  }
344
+ function isNoRunningTaskQueueError(err) {
345
+ return err instanceof Error && err.message === "No task is currently running.";
346
+ }
332
347
  export function createRuntimeContext(options) {
333
348
  const startServices = options.startServices ?? true;
334
349
  const registerSignalHandlers = options.registerSignalHandlers ?? true;
@@ -378,20 +393,10 @@ export function createRuntimeContext(options) {
378
393
  },
379
394
  async handleBusyMessage(event, bot, mode, queueText) {
380
395
  if (shuttingDown) {
381
- return;
396
+ return true;
382
397
  }
383
398
  const state = getState(event.channelId);
384
399
  const trimmedQueueText = queueText.trim();
385
- await archiveIncomingMessage(event.channelId, {
386
- date: new Date().toISOString(),
387
- ts: event.ts,
388
- user: event.user,
389
- userName: event.userName,
390
- text: event.text,
391
- isBot: false,
392
- deliveryMode: mode,
393
- skipContextSync: true,
394
- }, `${mode} message`);
395
400
  try {
396
401
  if (mode === "followUp") {
397
402
  await state.runner.queueFollowUp(trimmedQueueText, event.userName);
@@ -399,18 +404,36 @@ export function createRuntimeContext(options) {
399
404
  else {
400
405
  await state.runner.queueSteer(trimmedQueueText, event.userName);
401
406
  }
407
+ await archiveIncomingMessage(event.channelId, {
408
+ date: new Date().toISOString(),
409
+ ts: event.ts,
410
+ user: event.user,
411
+ userName: event.userName,
412
+ text: event.text,
413
+ isBot: false,
414
+ deliveryMode: mode,
415
+ skipContextSync: true,
416
+ }, `${mode} message`);
402
417
  const confirmation = mode === "followUp"
403
- ? "Queued as follow-up. I’ll handle it after the current task completes."
418
+ ? event.text.trim().startsWith("/")
419
+ ? "Queued as follow-up. I’ll handle it after the current task completes."
420
+ : "Queued as follow-up. I’ll handle it after the current task completes. Use `/steer <message>` to apply it after the current tool step finishes."
404
421
  : event.text.trim().startsWith("/")
405
422
  ? "Queued as steer. I’ll apply it after the current tool step finishes."
406
423
  : "Queued as steer. I’ll apply this after the current tool step finishes. Use `/followup <message>` to queue it after completion.";
407
424
  await bot.sendPlain(event.channelId, confirmation);
408
425
  log.logInfo(`[${event.channelId}] Queued ${mode}: ${trimmedQueueText.substring(0, 80)}`);
426
+ return true;
409
427
  }
410
428
  catch (err) {
411
429
  const errMsg = err instanceof Error ? err.message : String(err);
430
+ if (isNoRunningTaskQueueError(err)) {
431
+ log.logInfo(`[${event.channelId}] Busy ${mode} window closed; requeueing as a normal message`);
432
+ return false;
433
+ }
412
434
  log.logWarning(`[${event.channelId}] Failed to queue ${mode}`, errMsg);
413
435
  await bot.sendPlain(event.channelId, `Could not queue this message: ${errMsg}`);
436
+ return true;
414
437
  }
415
438
  },
416
439
  async handleEvent(event, bot, _isEvent) {
@@ -1,5 +1,6 @@
1
1
  import * as log from "../log.js";
2
2
  const MIN_UPDATE_INTERVAL_MS = 800;
3
+ const ROLLING_WINDOW_SIZE = 3;
3
4
  const NO_CONTENT = "";
4
5
  class ChannelDeliveryController {
5
6
  constructor(event, bot, store) {
@@ -17,7 +18,9 @@ class ChannelDeliveryController {
17
18
  this.finalResponseDelivered = false;
18
19
  this.cardWarmupScheduled = false;
19
20
  this.cardWarmupTriggered = false;
21
+ this.progressStartedAt = 0;
20
22
  this.progressWindowStartedAt = 0;
23
+ this.toolCallCount = 0;
21
24
  this.lastDeliveredAt = 0;
22
25
  this.timer = null;
23
26
  this.cardWarmupTimer = null;
@@ -97,11 +100,20 @@ class ChannelDeliveryController {
97
100
  if (this.closed || this.finalResponseDelivered || !text.trim())
98
101
  return;
99
102
  this.clearCardWarmup();
103
+ if (this.progressStartedAt === 0) {
104
+ this.progressStartedAt = Date.now();
105
+ }
106
+ if (text.startsWith("Running:")) {
107
+ this.toolCallCount++;
108
+ }
100
109
  if (this.progressSegments.length > 0) {
101
110
  this.progressSegments.push("\n\n");
102
111
  }
103
112
  this.progressSegments.push(text);
104
113
  this.progressTextDirty = true;
114
+ if (this.bot.progressDisplay === "rolling") {
115
+ this.trimToRecentEntries(ROLLING_WINDOW_SIZE);
116
+ }
105
117
  if (this.progressWindowStartedAt === 0) {
106
118
  this.progressWindowStartedAt = Date.now();
107
119
  }
@@ -213,12 +225,13 @@ class ChannelDeliveryController {
213
225
  }
214
226
  else if (mode === "finalize-existing") {
215
227
  if (content || this.cardWarmupTriggered) {
216
- touchedRemote = await this.bot.replaceCard(this.event.channelId, content ? progressText : NO_CONTENT, true);
228
+ const finalProgressText = this.bot.progressDisplay === "rolling" ? this.buildSummaryText("Done") : progressText;
229
+ touchedRemote = await this.bot.replaceCard(this.event.channelId, content || this.bot.progressDisplay === "rolling" ? finalProgressText : NO_CONTENT, true);
217
230
  if (!touchedRemote) {
218
231
  this.bot.discardCard(this.event.channelId);
219
232
  }
220
233
  else {
221
- this.sentProgressChars = progressText.length;
234
+ this.sentProgressChars = finalProgressText.length;
222
235
  this.replayRequired = false;
223
236
  }
224
237
  }
@@ -306,6 +319,36 @@ class ChannelDeliveryController {
306
319
  this.progressTextDirty = false;
307
320
  return this.cachedProgressText;
308
321
  }
322
+ trimToRecentEntries(maxEntries) {
323
+ let entryCount = 0;
324
+ for (const segment of this.progressSegments) {
325
+ if (segment !== "\n\n") {
326
+ entryCount++;
327
+ }
328
+ }
329
+ if (entryCount <= maxEntries) {
330
+ return;
331
+ }
332
+ const entriesToRemove = entryCount - maxEntries;
333
+ let removedEntries = 0;
334
+ while (removedEntries < entriesToRemove && this.progressSegments.length > 0) {
335
+ const segment = this.progressSegments.shift();
336
+ if (segment !== "\n\n") {
337
+ removedEntries++;
338
+ }
339
+ }
340
+ while (this.progressSegments[0] === "\n\n") {
341
+ this.progressSegments.shift();
342
+ }
343
+ this.progressTextDirty = true;
344
+ this.replayRequired = true;
345
+ this.sentProgressChars = 0;
346
+ }
347
+ buildSummaryText(status) {
348
+ const elapsedSeconds = this.progressStartedAt > 0 ? Math.round((Date.now() - this.progressStartedAt) / 1000) : 0;
349
+ const toolLabel = this.toolCallCount === 1 ? "1 tool call" : `${this.toolCallCount} tool calls`;
350
+ return `${status} · ${toolLabel} · ${elapsedSeconds}s`;
351
+ }
309
352
  }
310
353
  export function createDingTalkContext(event, bot, store) {
311
354
  return new ChannelDeliveryController(event, bot, store).buildContext();
@@ -1,3 +1,10 @@
1
+ export type BusyMessageMode = "steer" | "followUp";
2
+ export type BusyMessageDefaultConfig = BusyMessageMode | "followup";
3
+ export type ProgressDisplayMode = "full" | "rolling";
4
+ export declare function isBusyMessageDefaultConfig(value: unknown): value is BusyMessageDefaultConfig;
5
+ export declare function isProgressDisplayConfig(value: unknown): value is ProgressDisplayMode;
6
+ export declare function normalizeBusyMessageDefault(value: unknown): BusyMessageMode;
7
+ export declare function normalizeProgressDisplay(value: unknown): ProgressDisplayMode;
1
8
  export interface DingTalkConfig {
2
9
  clientId: string;
3
10
  clientSecret: string;
@@ -6,6 +13,8 @@ export interface DingTalkConfig {
6
13
  cardTemplateKey?: string;
7
14
  allowFrom?: string[];
8
15
  stateDir?: string;
16
+ busyMessageDefault?: BusyMessageDefaultConfig;
17
+ progressDisplay?: ProgressDisplayMode;
9
18
  }
10
19
  export interface DingTalkEvent {
11
20
  type: "dm" | "group";
@@ -38,12 +47,11 @@ export interface DingTalkContext {
38
47
  flush: () => Promise<void>;
39
48
  close: () => Promise<void>;
40
49
  }
41
- export type BusyMessageMode = "steer" | "followUp";
42
50
  export interface DingTalkHandler {
43
51
  isRunning(channelId: string): boolean;
44
52
  handleEvent(event: DingTalkEvent, bot: DingTalkBot, isEvent?: boolean): Promise<void>;
45
53
  handleStop(channelId: string, bot: DingTalkBot): Promise<void>;
46
- handleBusyMessage(event: DingTalkEvent, bot: DingTalkBot, mode: BusyMessageMode, queueText: string): Promise<void>;
54
+ handleBusyMessage(event: DingTalkEvent, bot: DingTalkBot, mode: BusyMessageMode, queueText: string): Promise<boolean>;
47
55
  }
48
56
  export declare class DingTalkBot {
49
57
  private handler;
@@ -66,6 +74,8 @@ export declare class DingTalkBot {
66
74
  private processedIds;
67
75
  private processedIdsOrder;
68
76
  constructor(handler: DingTalkHandler, config: DingTalkConfig);
77
+ get busyMessageDefault(): BusyMessageMode;
78
+ get progressDisplay(): ProgressDisplayMode;
69
79
  /**
70
80
  * Mark an ID as processed. Returns true if this is a new ID, false if already seen.
71
81
  * Maintains a FIFO buffer of at most 200 entries.
@@ -95,6 +105,7 @@ export declare class DingTalkBot {
95
105
  * Returns true if enqueued, false if queue is full (max 5).
96
106
  */
97
107
  enqueueEvent(event: DingTalkEvent): boolean;
108
+ private enqueueStreamMessage;
98
109
  /**
99
110
  * Get or create an AI Card for a channel.
100
111
  */
@@ -15,6 +15,33 @@ import { parseBuiltInCommand, renderBuiltInHelp } from "../agent/commands.js";
15
15
  import * as log from "../log.js";
16
16
  import { isRecord } from "../shared/type-guards.js";
17
17
  import { getChannelDir } from "./channel-paths.js";
18
+ export function isBusyMessageDefaultConfig(value) {
19
+ return value === "steer" || value === "followUp" || value === "followup";
20
+ }
21
+ export function isProgressDisplayConfig(value) {
22
+ return value === "full" || value === "rolling";
23
+ }
24
+ export function normalizeBusyMessageDefault(value) {
25
+ if (value === undefined) {
26
+ return "steer";
27
+ }
28
+ if (value === "steer") {
29
+ return "steer";
30
+ }
31
+ if (value === "followUp" || value === "followup") {
32
+ return "followUp";
33
+ }
34
+ throw new Error('Invalid `busyMessageDefault`: expected "steer", "followUp", or "followup".');
35
+ }
36
+ export function normalizeProgressDisplay(value) {
37
+ if (value === undefined) {
38
+ return "full";
39
+ }
40
+ if (isProgressDisplayConfig(value)) {
41
+ return value;
42
+ }
43
+ throw new Error('Invalid `progressDisplay`: expected "full" or "rolling".');
44
+ }
18
45
  class ChannelQueue {
19
46
  constructor() {
20
47
  this.queue = [];
@@ -90,7 +117,17 @@ export class DingTalkBot {
90
117
  this.processedIds = new Set();
91
118
  this.processedIdsOrder = [];
92
119
  this.handler = handler;
93
- this.config = config;
120
+ this.config = {
121
+ ...config,
122
+ busyMessageDefault: normalizeBusyMessageDefault(config.busyMessageDefault),
123
+ progressDisplay: normalizeProgressDisplay(config.progressDisplay),
124
+ };
125
+ }
126
+ get busyMessageDefault() {
127
+ return normalizeBusyMessageDefault(this.config.busyMessageDefault);
128
+ }
129
+ get progressDisplay() {
130
+ return normalizeProgressDisplay(this.config.progressDisplay);
94
131
  }
95
132
  /**
96
133
  * Mark an ID as processed. Returns true if this is a new ID, false if already seen.
@@ -444,6 +481,18 @@ export class DingTalkBot {
444
481
  });
445
482
  return true;
446
483
  }
484
+ enqueueStreamMessage(event) {
485
+ this.getQueue(event.channelId).enqueue(async () => {
486
+ this.activeMessageProcessing = true;
487
+ try {
488
+ await this.handler.handleEvent(event, this);
489
+ }
490
+ finally {
491
+ this.activeMessageProcessing = false;
492
+ this.lastSocketAvailableTime = Date.now();
493
+ }
494
+ });
495
+ }
447
496
  // ==========================================================================
448
497
  // AI Card operations
449
498
  // ==========================================================================
@@ -821,35 +870,35 @@ export class DingTalkBot {
821
870
  return;
822
871
  }
823
872
  if (builtInCommand?.name === "steer") {
824
- await this.handler.handleBusyMessage(event, this, "steer", builtInCommand.args);
873
+ const handled = await this.handler.handleBusyMessage(event, this, "steer", builtInCommand.args);
874
+ if (!handled) {
875
+ this.enqueueStreamMessage(event);
876
+ }
825
877
  return;
826
878
  }
827
879
  if (builtInCommand?.name === "followup") {
828
- await this.handler.handleBusyMessage(event, this, "followUp", builtInCommand.args);
880
+ const handled = await this.handler.handleBusyMessage(event, this, "followUp", builtInCommand.args);
881
+ if (!handled) {
882
+ this.enqueueStreamMessage(event);
883
+ }
829
884
  return;
830
885
  }
831
886
  if (builtInCommand) {
832
- await this.sendPlain(channelId, "A task is already running. Use `/stop`, `/steer <message>`, or `/followup <message>`. Plain messages default to steer.");
887
+ await this.sendPlain(channelId, `A task is already running. Use \`/stop\`, \`/steer <message>\`, or \`/followup <message>\`. Plain messages default to ${this.busyMessageDefault}.`);
833
888
  return;
834
889
  }
835
890
  if (isSlashCommand) {
836
891
  await this.sendPlain(channelId, "A task is already running. Only `/stop`, `/steer <message>`, and `/followup <message>` are available while streaming.");
837
892
  return;
838
893
  }
839
- await this.handler.handleBusyMessage(event, this, "steer", content);
894
+ const handled = await this.handler.handleBusyMessage(event, this, this.busyMessageDefault, content);
895
+ if (!handled) {
896
+ this.enqueueStreamMessage(event);
897
+ }
840
898
  return;
841
899
  }
842
900
  // Enqueue for processing
843
- this.getQueue(channelId).enqueue(async () => {
844
- this.activeMessageProcessing = true;
845
- try {
846
- await this.handler.handleEvent(event, this);
847
- }
848
- finally {
849
- this.activeMessageProcessing = false;
850
- this.lastSocketAvailableTime = Date.now();
851
- }
852
- });
901
+ this.enqueueStreamMessage(event);
853
902
  }
854
903
  getQueue(channelId) {
855
904
  let queue = this.queues.get(channelId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oyasmi/pipiclaw",
3
- "version": "0.6.4",
3
+ "version": "0.6.6-beta.1",
4
4
  "description": "An AI assistant runtime for coding and team workflows, with DingTalk AI Cards, sub-agents, memory, and scheduled events.",
5
5
  "type": "module",
6
6
  "bin": {