@oyasmi/pipiclaw 0.6.4 → 0.6.5

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
 
@@ -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
  }
@@ -400,7 +412,9 @@ export function createRuntimeContext(options) {
400
412
  await state.runner.queueSteer(trimmedQueueText, event.userName);
401
413
  }
402
414
  const confirmation = mode === "followUp"
403
- ? "Queued as follow-up. I’ll handle it after the current task completes."
415
+ ? event.text.trim().startsWith("/")
416
+ ? "Queued as follow-up. I’ll handle it after the current task completes."
417
+ : "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
418
  : event.text.trim().startsWith("/")
405
419
  ? "Queued as steer. I’ll apply it after the current tool step finishes."
406
420
  : "Queued as steer. I’ll apply this after the current tool step finishes. Use `/followup <message>` to queue it after completion.";
@@ -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,7 +47,6 @@ 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>;
@@ -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.
@@ -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.
@@ -829,14 +866,14 @@ export class DingTalkBot {
829
866
  return;
830
867
  }
831
868
  if (builtInCommand) {
832
- await this.sendPlain(channelId, "A task is already running. Use `/stop`, `/steer <message>`, or `/followup <message>`. Plain messages default to steer.");
869
+ await this.sendPlain(channelId, `A task is already running. Use \`/stop\`, \`/steer <message>\`, or \`/followup <message>\`. Plain messages default to ${this.busyMessageDefault}.`);
833
870
  return;
834
871
  }
835
872
  if (isSlashCommand) {
836
873
  await this.sendPlain(channelId, "A task is already running. Only `/stop`, `/steer <message>`, and `/followup <message>` are available while streaming.");
837
874
  return;
838
875
  }
839
- await this.handler.handleBusyMessage(event, this, "steer", content);
876
+ await this.handler.handleBusyMessage(event, this, this.busyMessageDefault, content);
840
877
  return;
841
878
  }
842
879
  // Enqueue for processing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oyasmi/pipiclaw",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
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": {