@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 +7 -1
- package/dist/agent/channel-runner.d.ts +1 -0
- package/dist/agent/channel-runner.js +26 -6
- package/dist/agent/commands.js +3 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/runtime/bootstrap.js +36 -13
- package/dist/runtime/delivery.js +45 -2
- package/dist/runtime/dingtalk.d.ts +13 -2
- package/dist/runtime/dingtalk.js +64 -15
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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();
|
package/dist/agent/commands.js
CHANGED
|
@@ -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,
|
|
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
|
-
?
|
|
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) {
|
package/dist/runtime/delivery.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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<
|
|
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
|
*/
|
package/dist/runtime/dingtalk.js
CHANGED
|
@@ -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 =
|
|
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,
|
|
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,
|
|
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.
|
|
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