@soimy/dingtalk 3.6.11 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/index.js +11684 -10583
- package/dist/src/{ack-reaction-service.d.ts → ack-reaction/ack-reaction-service.d.ts} +1 -1
- package/dist/src/ack-reaction/dynamic-ack-reaction-controller.d.ts +3 -3
- package/dist/src/card/ask-user-question-context.d.ts +1 -1
- package/dist/src/card/ask-user-question-store.d.ts +1 -1
- package/dist/src/card/ask-user-question.d.ts +1 -1
- package/dist/src/card/card-action-handler.d.ts +2 -2
- package/dist/src/{card-draft-controller.d.ts → card/card-draft-controller.d.ts} +5 -1
- package/dist/src/card/card-run-registry.d.ts +2 -2
- package/dist/src/{card-service.d.ts → card/card-service.d.ts} +1 -1
- package/dist/src/card/card-stop-handler.d.ts +1 -1
- package/dist/src/card/card-streaming-mode.d.ts +1 -1
- package/dist/src/card/card-task-progress.d.ts +60 -0
- package/dist/src/channel.d.ts +6 -6
- package/dist/src/command/card-stop-command.d.ts +1 -1
- package/dist/src/{feedback-learning-service.d.ts → command/feedback-learning-service.d.ts} +1 -1
- package/dist/src/command/inbound-command-dispatch-service.d.ts +2 -2
- package/dist/src/{learning-command-service.d.ts → command/learning-command-service.d.ts} +1 -1
- package/dist/src/gateway/channel-gateway.d.ts +1 -1
- package/dist/src/{connection-manager.d.ts → gateway/connection-manager.d.ts} +1 -1
- package/dist/src/{docs-service.d.ts → gateway/docs-service.d.ts} +1 -1
- package/dist/src/{inbound-handler.d.ts → gateway/inbound-handler.d.ts} +1 -1
- package/dist/src/gateway/inbound-session-queue-dispatcher.d.ts +1 -1
- package/dist/src/messaging/attachment-text-extractor.d.ts +1 -1
- package/dist/src/messaging/btw-deliver.d.ts +1 -1
- package/dist/src/messaging/channel-outbound.d.ts +1 -1
- package/dist/src/{media-utils.d.ts → messaging/media-utils.d.ts} +1 -1
- package/dist/src/{message-context-store.d.ts → messaging/message-context-store.d.ts} +7 -1
- package/dist/src/{message-utils.d.ts → messaging/message-utils.d.ts} +1 -1
- package/dist/src/messaging/quoted-context.d.ts +2 -2
- package/dist/src/messaging/quoted-file-service.d.ts +1 -1
- package/dist/src/messaging/quoted-ref.d.ts +2 -2
- package/dist/src/{reply-strategy-card.d.ts → messaging/reply-strategy-card.d.ts} +1 -1
- package/dist/src/{reply-strategy-types.d.ts → messaging/reply-strategy-types.d.ts} +10 -1
- package/dist/src/{reply-strategy-with-reaction.d.ts → messaging/reply-strategy-with-reaction.d.ts} +1 -1
- package/dist/src/{reply-strategy.d.ts → messaging/reply-strategy.d.ts} +1 -1
- package/dist/src/{send-service.d.ts → messaging/send-service.d.ts} +1 -1
- package/dist/src/platform/channel-status.d.ts +1 -1
- package/dist/src/platform/config.d.ts +110 -0
- package/dist/src/platform/runtime-events.d.ts +67 -0
- package/dist/src/{types.d.ts → platform/types.d.ts} +74 -1
- package/dist/src/{persistence-store.d.ts → shared/persistence-store.d.ts} +1 -1
- package/dist/src/{utils.d.ts → shared/utils.d.ts} +1 -1
- package/dist/src/targeting/agent-name-matcher.d.ts +1 -1
- package/dist/src/targeting/agent-routing.d.ts +3 -3
- package/dist/src/{session-routing.d.ts → targeting/session-routing.d.ts} +1 -1
- package/index.ts +193 -69
- package/openclaw.plugin.json +158 -2
- package/package.json +16 -16
- package/src/{ack-reaction-classifier.ts → ack-reaction/ack-reaction-classifier.ts} +85 -13
- package/src/{ack-reaction-service.ts → ack-reaction/ack-reaction-service.ts} +13 -7
- package/src/ack-reaction/dynamic-ack-reaction-controller.ts +46 -31
- package/src/ack-reaction/dynamic-ack-reaction-progress.ts +4 -1
- package/src/card/ask-user-question-context.ts +1 -1
- package/src/card/ask-user-question-store.ts +2 -2
- package/src/card/ask-user-question.ts +11 -7
- package/src/card/card-action-handler.ts +2 -2
- package/src/{card-callback-service.ts → card/card-callback-service.ts} +7 -9
- package/src/card/card-draft-controller.ts +770 -0
- package/src/card/card-markdown-image-reroute.ts +8 -2
- package/src/card/card-run-registry.ts +17 -7
- package/src/{card-service.ts → card/card-service.ts} +91 -61
- package/src/card/card-stop-handler.ts +12 -8
- package/src/card/card-streaming-mode.ts +1 -1
- package/src/card/card-task-progress.ts +330 -0
- package/src/card/draft-stream-loop.ts +119 -0
- package/src/card/reasoning-answer-split.ts +125 -121
- package/src/card/reasoning-block-assembler.ts +122 -123
- package/src/{run-usage-store.ts → card/run-usage-store.ts} +18 -6
- package/src/card/statusline-renderer.ts +14 -5
- package/src/card/task-model-metadata.ts +8 -3
- package/src/channel.ts +14 -16
- package/src/command/card-stop-command.ts +2 -2
- package/src/{feedback-learning-service.ts → command/feedback-learning-service.ts} +36 -16
- package/src/{feedback-learning-store.ts → command/feedback-learning-store.ts} +133 -81
- package/src/command/inbound-command-dispatch-service.ts +12 -6
- package/src/{learning-command-service.ts → command/learning-command-service.ts} +36 -29
- package/src/{session-command-service.ts → command/session-command-service.ts} +3 -1
- package/src/gateway/channel-gateway.ts +106 -28
- package/src/{connection-manager.ts → gateway/connection-manager.ts} +45 -30
- package/src/{docs-service.ts → gateway/docs-service.ts} +5 -5
- package/src/{inbound-handler.ts → gateway/inbound-handler.ts} +139 -84
- package/src/gateway/inbound-session-queue-dispatcher.ts +18 -14
- package/src/{session-lock.ts → gateway/session-lock.ts} +14 -14
- package/src/messaging/attachment-text-extractor.ts +21 -7
- package/src/messaging/btw-deliver.ts +2 -2
- package/src/messaging/channel-actions.ts +15 -7
- package/src/messaging/channel-outbound.ts +16 -11
- package/src/{media-utils.ts → messaging/media-utils.ts} +276 -72
- package/src/{message-context-store.ts → messaging/message-context-store.ts} +11 -2
- package/src/{message-utils.ts → messaging/message-utils.ts} +46 -11
- package/src/messaging/quoted-context.ts +2 -2
- package/src/messaging/quoted-file-service.ts +311 -294
- package/src/messaging/quoted-ref.ts +11 -7
- package/src/{reply-strategy-card.ts → messaging/reply-strategy-card.ts} +178 -101
- package/src/{reply-strategy-markdown.ts → messaging/reply-strategy-markdown.ts} +8 -2
- package/src/{reply-strategy-types.ts → messaging/reply-strategy-types.ts} +11 -1
- package/src/{reply-strategy-with-reaction.ts → messaging/reply-strategy-with-reaction.ts} +6 -1
- package/src/{reply-strategy.ts → messaging/reply-strategy.ts} +2 -2
- package/src/{send-service.ts → messaging/send-service.ts} +100 -65
- package/src/{auth.ts → platform/auth.ts} +2 -2
- package/src/platform/channel-status.ts +3 -3
- package/src/{config-schema.ts → platform/config-schema.ts} +81 -4
- package/src/{config.ts → platform/config.ts} +167 -19
- package/src/{device-registration.ts → platform/device-registration.ts} +5 -4
- package/src/{onboarding.ts → platform/onboarding.ts} +1 -4
- package/src/{plugin-sdk-channel-actions-augment.ts → platform/plugin-sdk-channel-actions-augment.ts} +6 -5
- package/src/platform/runtime-events.ts +202 -0
- package/src/{secret-input.ts → platform/secret-input.ts} +117 -11
- package/src/{types.ts → platform/types.ts} +76 -1
- package/src/{http-client.ts → shared/http-client.ts} +1 -3
- package/src/{persistence-store.ts → shared/persistence-store.ts} +6 -6
- package/src/{utils.ts → shared/utils.ts} +30 -13
- package/src/targeting/agent-name-matcher.ts +1 -1
- package/src/targeting/agent-routing.ts +7 -7
- package/src/targeting/group-members-store.ts +1 -1
- package/src/{session-peer-store.ts → targeting/session-peer-store.ts} +9 -4
- package/src/{session-routing.ts → targeting/session-routing.ts} +1 -1
- package/src/targeting/target-directory-adapter.ts +3 -3
- package/src/targeting/target-directory-store.ts +1 -1
- package/src/targeting/target-input.ts +1 -1
- package/dist/src/ack-reaction/dynamic-ack-reaction-events.d.ts +0 -35
- package/dist/src/config.d.ts +0 -59
- package/src/ack-reaction/dynamic-ack-reaction-events.ts +0 -123
- package/src/card-draft-controller.ts +0 -637
- package/src/draft-stream-loop.ts +0 -119
- /package/dist/src/{ack-reaction-classifier.d.ts → ack-reaction/ack-reaction-classifier.d.ts} +0 -0
- /package/dist/src/{card-callback-service.d.ts → card/card-callback-service.d.ts} +0 -0
- /package/dist/src/{draft-stream-loop.d.ts → card/draft-stream-loop.d.ts} +0 -0
- /package/dist/src/{run-usage-store.d.ts → card/run-usage-store.d.ts} +0 -0
- /package/dist/src/{feedback-learning-store.d.ts → command/feedback-learning-store.d.ts} +0 -0
- /package/dist/src/{session-command-service.d.ts → command/session-command-service.d.ts} +0 -0
- /package/dist/src/{session-lock.d.ts → gateway/session-lock.d.ts} +0 -0
- /package/dist/src/{proactive-risk-registry.d.ts → messaging/proactive-risk-registry.d.ts} +0 -0
- /package/dist/src/{reply-strategy-markdown.d.ts → messaging/reply-strategy-markdown.d.ts} +0 -0
- /package/dist/src/{access-control.d.ts → platform/access-control.d.ts} +0 -0
- /package/dist/src/{auth.d.ts → platform/auth.d.ts} +0 -0
- /package/dist/src/{config-schema.d.ts → platform/config-schema.d.ts} +0 -0
- /package/dist/src/{device-registration.d.ts → platform/device-registration.d.ts} +0 -0
- /package/dist/src/{logger-context.d.ts → platform/logger-context.d.ts} +0 -0
- /package/dist/src/{onboarding.d.ts → platform/onboarding.d.ts} +0 -0
- /package/dist/src/{plugin-sdk-channel-actions-augment.d.ts → platform/plugin-sdk-channel-actions-augment.d.ts} +0 -0
- /package/dist/src/{runtime.d.ts → platform/runtime.d.ts} +0 -0
- /package/dist/src/{secret-input.d.ts → platform/secret-input.d.ts} +0 -0
- /package/dist/src/{session-state.d.ts → platform/session-state.d.ts} +0 -0
- /package/dist/src/{signature.d.ts → platform/signature.d.ts} +0 -0
- /package/dist/src/{dedup.d.ts → shared/dedup.d.ts} +0 -0
- /package/dist/src/{http-client.d.ts → shared/http-client.d.ts} +0 -0
- /package/dist/src/{path-utils.d.ts → shared/path-utils.d.ts} +0 -0
- /package/dist/src/{peer-id-registry.d.ts → targeting/peer-id-registry.d.ts} +0 -0
- /package/dist/src/{session-peer-store.d.ts → targeting/session-peer-store.d.ts} +0 -0
- /package/src/{proactive-risk-registry.ts → messaging/proactive-risk-registry.ts} +0 -0
- /package/src/{access-control.ts → platform/access-control.ts} +0 -0
- /package/src/{logger-context.ts → platform/logger-context.ts} +0 -0
- /package/src/{runtime.ts → platform/runtime.ts} +0 -0
- /package/src/{session-state.ts → platform/session-state.ts} +0 -0
- /package/src/{signature.ts → platform/signature.ts} +0 -0
- /package/src/{dedup.ts → shared/dedup.ts} +0 -0
- /package/src/{path-utils.ts → shared/path-utils.ts} +0 -0
- /package/src/{peer-id-registry.ts → targeting/peer-id-registry.ts} +0 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAgentEventCorrelator,
|
|
3
|
+
type RuntimeAgentEvent,
|
|
4
|
+
type RuntimeEventsLogger,
|
|
5
|
+
type RuntimeEventsSurface,
|
|
6
|
+
} from "../platform/runtime-events";
|
|
7
|
+
import type { CardTaskProgressRefresh, DingTalkConfig } from "../platform/types";
|
|
8
|
+
import { getErrorMessage } from "../shared/utils";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_START_DELAY_MS = 10_000;
|
|
11
|
+
const DEFAULT_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
12
|
+
const OPTIMISTIC_RUN_ID_CAPTURE_WINDOW_MS = 5_000;
|
|
13
|
+
const CORRELATION_CONSUMER = "card-task-progress";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Tool events use `start` / `update` / `result` (`update` is a mid-call
|
|
17
|
+
* progress tick and must not count). Only the terminal phase completes a step.
|
|
18
|
+
*
|
|
19
|
+
* A real-device run caught this: the counter previously matched `"end"`, which
|
|
20
|
+
* the runtime never emits, so "已完成 N 步" stayed at 0 forever.
|
|
21
|
+
*/
|
|
22
|
+
function isToolCompletedPhase(phase: unknown): boolean {
|
|
23
|
+
return phase === "result";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Decide whether the live task-progress block is allowed for this card.
|
|
28
|
+
*
|
|
29
|
+
* `cardTaskProgress` is tri-state on purpose: explicit `true`/`false` always
|
|
30
|
+
* wins. When unset the block is on by default, except for an explicit
|
|
31
|
+
* `cardStreamingMode: "off"` — that is a deliberate "keep card traffic
|
|
32
|
+
* minimal" request, so progress stays quiet until the user opts back in.
|
|
33
|
+
*
|
|
34
|
+
* The runtime normalizes an omitted `cardStreamingMode` to `"off"` before the
|
|
35
|
+
* config reaches this point, so the effective value alone cannot tell "omitted"
|
|
36
|
+
* apart from "explicitly off". `cardStreamingModeConfigured` carries that
|
|
37
|
+
* distinction; configs that never went through normalization (hand-built test
|
|
38
|
+
* or embedded configs) fall back to "was a mode present at all".
|
|
39
|
+
*/
|
|
40
|
+
export function resolveCardTaskProgressEnabled(
|
|
41
|
+
config: Pick<
|
|
42
|
+
DingTalkConfig,
|
|
43
|
+
"cardTaskProgress" | "cardStreamingMode" | "cardStreamingModeConfigured"
|
|
44
|
+
>,
|
|
45
|
+
): boolean {
|
|
46
|
+
if (config.cardTaskProgress === false) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (config.cardTaskProgress === true) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
const explicitlyConfigured =
|
|
53
|
+
config.cardStreamingModeConfigured ?? config.cardStreamingMode !== undefined;
|
|
54
|
+
if (!explicitlyConfigured) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return config.cardStreamingMode !== "off";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Map a tool name to a short, sanitized task-kind label.
|
|
62
|
+
*
|
|
63
|
+
* Labels are bare kinds (no leading 正在) because the progress line renders them
|
|
64
|
+
* as `{kind}中,已完成 N 步,耗时 …`. Only the normalized tool name is used —
|
|
65
|
+
* raw arguments, commands, URLs and outputs never reach the card.
|
|
66
|
+
*/
|
|
67
|
+
function resolveTaskKind(toolName: unknown): string {
|
|
68
|
+
const name = typeof toolName === "string" ? toolName.trim().toLowerCase() : "";
|
|
69
|
+
if (["read", "view", "find", "list", "glob"].includes(name)) {
|
|
70
|
+
return "检查文件";
|
|
71
|
+
}
|
|
72
|
+
if (["write", "edit", "patch", "apply_patch"].includes(name)) {
|
|
73
|
+
return "应用修改";
|
|
74
|
+
}
|
|
75
|
+
if (["web_search", "search", "fetch", "open", "open_url"].includes(name)) {
|
|
76
|
+
return "查询资料";
|
|
77
|
+
}
|
|
78
|
+
if (name.includes("browser")) {
|
|
79
|
+
return "验证页面";
|
|
80
|
+
}
|
|
81
|
+
if (["bash", "exec", "process", "exec_command"].includes(name)) {
|
|
82
|
+
return "执行检查";
|
|
83
|
+
}
|
|
84
|
+
if (name.includes("database") || name.includes("sql") || name.includes("query")) {
|
|
85
|
+
return "查询数据";
|
|
86
|
+
}
|
|
87
|
+
return "处理任务";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function formatElapsed(elapsedMs: number): string {
|
|
91
|
+
const totalSeconds = Math.max(0, Math.floor(elapsedMs / 1000));
|
|
92
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
93
|
+
const seconds = totalSeconds % 60;
|
|
94
|
+
return minutes > 0 ? `${minutes} 分 ${seconds} 秒` : `${seconds} 秒`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface CardTaskProgressController {
|
|
98
|
+
/** Wait for queued card updates to settle (test/observability helper). */
|
|
99
|
+
awaitDrain(): Promise<void>;
|
|
100
|
+
/**
|
|
101
|
+
* Release timers and the runtime-event subscription, then remove the
|
|
102
|
+
* progress block. Idempotent, and safe to call when nothing was rendered.
|
|
103
|
+
*
|
|
104
|
+
* `clearRemoteWhenEmpty` is for teardown paths that will never commit the
|
|
105
|
+
* card (for example ask-user question-card takeover): when the progress block
|
|
106
|
+
* was the only visible block, the local removal must also clear the remote
|
|
107
|
+
* card, otherwise the reader keeps seeing a live-looking "任务处理中" card.
|
|
108
|
+
*/
|
|
109
|
+
dispose(options?: { clearRemoteWhenEmpty?: boolean }): Promise<void>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface ClearProgressOptions {
|
|
113
|
+
/** Clear the remote card when removing progress leaves no visible block. */
|
|
114
|
+
clearRemoteWhenEmpty?: boolean;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Keep one replaceable "task in progress" block on an AI card.
|
|
119
|
+
*
|
|
120
|
+
* Returns a no-op controller when progress is disabled by config or when the
|
|
121
|
+
* session key is unknown: without a session key the correlator could only rely
|
|
122
|
+
* on its optimistic window, which surfaces as an intermittently missing block.
|
|
123
|
+
*/
|
|
124
|
+
export function createCardTaskProgressController(params: {
|
|
125
|
+
sessionKey: string;
|
|
126
|
+
enabled?: boolean;
|
|
127
|
+
/**
|
|
128
|
+
* `heartbeat` (default) coalesces stage/step changes into the 30s refresh;
|
|
129
|
+
* `interval` pushes them on every correlated tool event, throttled further
|
|
130
|
+
* down by the card draft loop's `cardStreamInterval`.
|
|
131
|
+
*/
|
|
132
|
+
refresh?: CardTaskProgressRefresh;
|
|
133
|
+
runtimeEvents?: RuntimeEventsSurface;
|
|
134
|
+
updateProgress: (text: string) => Promise<void>;
|
|
135
|
+
clearProgress: (options?: ClearProgressOptions) => Promise<void>;
|
|
136
|
+
startDelayMs?: number;
|
|
137
|
+
heartbeatIntervalMs?: number;
|
|
138
|
+
log?: RuntimeEventsLogger;
|
|
139
|
+
}): CardTaskProgressController {
|
|
140
|
+
const sessionKey = params.sessionKey?.trim() ?? "";
|
|
141
|
+
if (params.enabled === false || !sessionKey) {
|
|
142
|
+
params.log?.debug?.(
|
|
143
|
+
`[DingTalk][TaskProgress] Disabled — enabled=${params.enabled !== false} hasSessionKey=${Boolean(sessionKey)}`,
|
|
144
|
+
);
|
|
145
|
+
return {
|
|
146
|
+
async awaitDrain(): Promise<void> {},
|
|
147
|
+
async dispose(): Promise<void> {},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Unknown values (hand-built config) fall back to the safe default.
|
|
152
|
+
const refreshMode: CardTaskProgressRefresh =
|
|
153
|
+
params.refresh === "interval" ? "interval" : "heartbeat";
|
|
154
|
+
|
|
155
|
+
const startedAt = Date.now();
|
|
156
|
+
let currentStage = "处理任务";
|
|
157
|
+
let completedSteps = 0;
|
|
158
|
+
let visible = false;
|
|
159
|
+
let disposed = false;
|
|
160
|
+
let updatePromise: Promise<void> = Promise.resolve();
|
|
161
|
+
let heartbeatTimer: NodeJS.Timeout | undefined;
|
|
162
|
+
let startTimer: NodeJS.Timeout | undefined;
|
|
163
|
+
const completedToolCalls = new Set<string>();
|
|
164
|
+
const isCorrelatedEvent = createAgentEventCorrelator({
|
|
165
|
+
consumer: CORRELATION_CONSUMER,
|
|
166
|
+
sessionKey,
|
|
167
|
+
enabled: true,
|
|
168
|
+
createdAt: startedAt,
|
|
169
|
+
optimisticCaptureWindowMs: OPTIMISTIC_RUN_ID_CAPTURE_WINDOW_MS,
|
|
170
|
+
log: params.log,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* One compact line, rendered as a single card block.
|
|
175
|
+
*
|
|
176
|
+
* Real-device constraints: inside one multi-line markdown block the DingTalk
|
|
177
|
+
* client only re-renders the trailing line (which froze the step count), so a
|
|
178
|
+
* single line is both the most compact shape and the only one that updates
|
|
179
|
+
* reliably at every refresh cadence.
|
|
180
|
+
*
|
|
181
|
+
* The leading task kind comes from the normalized tool name (never raw
|
|
182
|
+
* arguments), and the elapsed value renders as `n 秒` or `n 分 n 秒`.
|
|
183
|
+
*/
|
|
184
|
+
const render = (): string =>
|
|
185
|
+
`${currentStage}中,已完成 ${completedSteps} 步,耗时 ${formatElapsed(Date.now() - startedAt)}`;
|
|
186
|
+
|
|
187
|
+
const awaitDrain = async (): Promise<void> => {
|
|
188
|
+
await updatePromise.catch(() => undefined);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const cancelStartTimer = () => {
|
|
192
|
+
if (startTimer) {
|
|
193
|
+
clearTimeout(startTimer);
|
|
194
|
+
startTimer = undefined;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const ensureHeartbeat = () => {
|
|
199
|
+
if (heartbeatTimer || disposed) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
heartbeatTimer = setInterval(() => {
|
|
203
|
+
if (visible) {
|
|
204
|
+
void enqueueUpdate("heartbeat");
|
|
205
|
+
}
|
|
206
|
+
}, params.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS);
|
|
207
|
+
// Never keep the gateway process alive for a cosmetic heartbeat.
|
|
208
|
+
heartbeatTimer.unref?.();
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Queue one card refresh.
|
|
213
|
+
*
|
|
214
|
+
* The debug line is the only observable trace of what the reader actually
|
|
215
|
+
* sees (the rendered text never touches the API logs), and a real-device run
|
|
216
|
+
* needed exactly this to prove the step counter advances.
|
|
217
|
+
*/
|
|
218
|
+
const enqueueUpdate = (reason: "appearance" | "heartbeat" | "tool-event") => {
|
|
219
|
+
if (disposed) {
|
|
220
|
+
return updatePromise;
|
|
221
|
+
}
|
|
222
|
+
params.log?.debug?.(
|
|
223
|
+
`[DingTalk][TaskProgress] Push reason=${reason} steps=${completedSteps} ` +
|
|
224
|
+
`stage="${currentStage}" elapsed=${Math.round((Date.now() - startedAt) / 1000)}s`,
|
|
225
|
+
);
|
|
226
|
+
updatePromise = updatePromise
|
|
227
|
+
.then(() => params.updateProgress(render()))
|
|
228
|
+
.catch((error: unknown) => {
|
|
229
|
+
params.log?.warn?.(
|
|
230
|
+
`[DingTalk][TaskProgress] Card update failed: ${getErrorMessage(error)}`,
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
return updatePromise;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Make the block visible for the first time and start the heartbeat.
|
|
238
|
+
*
|
|
239
|
+
* The first appearance is always immediate. Later stage/step changes follow
|
|
240
|
+
* `refreshMode`: `heartbeat` (default) leaves them to the next 30s refresh, so
|
|
241
|
+
* a tool-heavy task cannot spend one DingTalk card update per tool event;
|
|
242
|
+
* `interval` pushes them right away and lets `cardStreamInterval` throttle.
|
|
243
|
+
*/
|
|
244
|
+
const showProgress = () => {
|
|
245
|
+
if (disposed || visible) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
visible = true;
|
|
249
|
+
// An early tool event already satisfied the startup delay.
|
|
250
|
+
cancelStartTimer();
|
|
251
|
+
ensureHeartbeat();
|
|
252
|
+
void enqueueUpdate("appearance");
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const refreshProgress = () => {
|
|
256
|
+
if (disposed) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (!visible) {
|
|
260
|
+
showProgress();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (refreshMode === "interval") {
|
|
264
|
+
void enqueueUpdate("tool-event");
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const handleAgentEvent = (event: unknown) => {
|
|
269
|
+
if (disposed) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const agentEvent = event as RuntimeAgentEvent | undefined;
|
|
273
|
+
if (agentEvent?.stream === "lifecycle" && agentEvent.data?.phase === "start") {
|
|
274
|
+
void isCorrelatedEvent(agentEvent);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (agentEvent?.stream !== "tool" || !isCorrelatedEvent(agentEvent)) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (agentEvent.data?.phase === "start") {
|
|
282
|
+
currentStage = resolveTaskKind(agentEvent.data?.name);
|
|
283
|
+
refreshProgress();
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (isToolCompletedPhase(agentEvent.data?.phase)) {
|
|
287
|
+
const toolCallId = agentEvent.data?.toolCallId?.trim();
|
|
288
|
+
if (!toolCallId || !completedToolCalls.has(toolCallId)) {
|
|
289
|
+
if (toolCallId) {
|
|
290
|
+
completedToolCalls.add(toolCallId);
|
|
291
|
+
}
|
|
292
|
+
completedSteps += 1;
|
|
293
|
+
}
|
|
294
|
+
refreshProgress();
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const unsubscribe = params.runtimeEvents?.onAgentEvent?.(handleAgentEvent) ?? (() => {});
|
|
299
|
+
startTimer = setTimeout(() => {
|
|
300
|
+
showProgress();
|
|
301
|
+
}, params.startDelayMs ?? DEFAULT_START_DELAY_MS);
|
|
302
|
+
startTimer.unref?.();
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
awaitDrain,
|
|
306
|
+
async dispose(options: { clearRemoteWhenEmpty?: boolean } = {}): Promise<void> {
|
|
307
|
+
if (disposed) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
disposed = true;
|
|
311
|
+
cancelStartTimer();
|
|
312
|
+
if (heartbeatTimer) {
|
|
313
|
+
clearInterval(heartbeatTimer);
|
|
314
|
+
heartbeatTimer = undefined;
|
|
315
|
+
}
|
|
316
|
+
// A host-provided unsubscribe must not skip the card cleanup below.
|
|
317
|
+
try {
|
|
318
|
+
unsubscribe();
|
|
319
|
+
} catch (error: unknown) {
|
|
320
|
+
params.log?.warn?.(
|
|
321
|
+
`[DingTalk][TaskProgress] Unsubscribe failed: ${getErrorMessage(error)}`,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
await awaitDrain();
|
|
325
|
+
if (visible) {
|
|
326
|
+
await params.clearProgress(options);
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Throttled stream loop for fire-and-forget draft updates.
|
|
3
|
+
*
|
|
4
|
+
* Ported from OpenClaw core (channels/draft-stream-loop.ts) because the module
|
|
5
|
+
* is not exported via the plugin-sdk. Keeps the same interface so that future
|
|
6
|
+
* sync with upstream is straightforward.
|
|
7
|
+
*
|
|
8
|
+
* Key safety properties:
|
|
9
|
+
* - Single-flight: at most one `sendOrEditStreamMessage` is in-flight at any time.
|
|
10
|
+
* - Latest-wins: multiple `update()` calls during in-flight only keep the last text.
|
|
11
|
+
* - Throttle: respects `throttleMs` between consecutive sends.
|
|
12
|
+
* - `flush()` drains all pending + waits for in-flight before returning.
|
|
13
|
+
* - `stop()` clears pending and timers; subsequent `update()` calls are ignored.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export type DraftStreamLoop = {
|
|
17
|
+
update: (text: string) => void;
|
|
18
|
+
flush: () => Promise<void>;
|
|
19
|
+
stop: () => void;
|
|
20
|
+
resetPending: () => void;
|
|
21
|
+
resetThrottleWindow: () => void;
|
|
22
|
+
waitForInFlight: () => Promise<void>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function createDraftStreamLoop(params: {
|
|
26
|
+
throttleMs: number;
|
|
27
|
+
isStopped: () => boolean;
|
|
28
|
+
sendOrEditStreamMessage: (text: string) => Promise<void | boolean>;
|
|
29
|
+
}): DraftStreamLoop {
|
|
30
|
+
let lastSentAt = 0;
|
|
31
|
+
let pendingText = "";
|
|
32
|
+
let inFlightPromise: Promise<void | boolean> | undefined;
|
|
33
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
34
|
+
|
|
35
|
+
const flush = async () => {
|
|
36
|
+
if (timer) {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
timer = undefined;
|
|
39
|
+
}
|
|
40
|
+
while (!params.isStopped()) {
|
|
41
|
+
if (inFlightPromise) {
|
|
42
|
+
await inFlightPromise;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const text = pendingText;
|
|
46
|
+
if (!text.trim()) {
|
|
47
|
+
pendingText = "";
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
pendingText = "";
|
|
51
|
+
const current = params.sendOrEditStreamMessage(text).finally(() => {
|
|
52
|
+
if (inFlightPromise === current) {
|
|
53
|
+
inFlightPromise = undefined;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
inFlightPromise = current;
|
|
57
|
+
const sent = await current;
|
|
58
|
+
if (sent === false) {
|
|
59
|
+
pendingText = text;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
lastSentAt = Date.now();
|
|
63
|
+
if (!pendingText) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const schedule = () => {
|
|
70
|
+
if (timer) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const delay = Math.max(0, params.throttleMs - (Date.now() - lastSentAt));
|
|
74
|
+
timer = setTimeout(() => {
|
|
75
|
+
void flush();
|
|
76
|
+
}, delay);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
update: (text: string) => {
|
|
81
|
+
if (params.isStopped()) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
pendingText = text;
|
|
85
|
+
if (inFlightPromise) {
|
|
86
|
+
schedule();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (!timer && Date.now() - lastSentAt >= params.throttleMs) {
|
|
90
|
+
void flush();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
schedule();
|
|
94
|
+
},
|
|
95
|
+
flush,
|
|
96
|
+
stop: () => {
|
|
97
|
+
pendingText = "";
|
|
98
|
+
if (timer) {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
timer = undefined;
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
resetPending: () => {
|
|
104
|
+
pendingText = "";
|
|
105
|
+
},
|
|
106
|
+
resetThrottleWindow: () => {
|
|
107
|
+
lastSentAt = 0;
|
|
108
|
+
if (timer) {
|
|
109
|
+
clearTimeout(timer);
|
|
110
|
+
timer = undefined;
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
waitForInFlight: async () => {
|
|
114
|
+
if (inFlightPromise) {
|
|
115
|
+
await inFlightPromise;
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|