@cecwxf/wtt 0.1.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/LICENSE +21 -0
- package/README.md +147 -0
- package/bin/openclaw-wtt-bootstrap.mjs +181 -0
- package/dist/channel.d.ts +275 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +2088 -0
- package/dist/channel.js.map +1 -0
- package/dist/commands/account.d.ts +16 -0
- package/dist/commands/account.d.ts.map +1 -0
- package/dist/commands/account.js +37 -0
- package/dist/commands/account.js.map +1 -0
- package/dist/commands/bind.d.ts +3 -0
- package/dist/commands/bind.d.ts.map +1 -0
- package/dist/commands/bind.js +102 -0
- package/dist/commands/bind.js.map +1 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +38 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/delegate.d.ts +7 -0
- package/dist/commands/delegate.d.ts.map +1 -0
- package/dist/commands/delegate.js +99 -0
- package/dist/commands/delegate.js.map +1 -0
- package/dist/commands/formatter.d.ts +8 -0
- package/dist/commands/formatter.d.ts.map +1 -0
- package/dist/commands/formatter.js +198 -0
- package/dist/commands/formatter.js.map +1 -0
- package/dist/commands/handlers.d.ts +3 -0
- package/dist/commands/handlers.d.ts.map +1 -0
- package/dist/commands/handlers.js +79 -0
- package/dist/commands/handlers.js.map +1 -0
- package/dist/commands/http.d.ts +26 -0
- package/dist/commands/http.d.ts.map +1 -0
- package/dist/commands/http.js +190 -0
- package/dist/commands/http.js.map +1 -0
- package/dist/commands/index.d.ts +3 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +2 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/parser.d.ts +5 -0
- package/dist/commands/parser.d.ts.map +1 -0
- package/dist/commands/parser.js +325 -0
- package/dist/commands/parser.js.map +1 -0
- package/dist/commands/pipeline.d.ts +7 -0
- package/dist/commands/pipeline.d.ts.map +1 -0
- package/dist/commands/pipeline.js +99 -0
- package/dist/commands/pipeline.js.map +1 -0
- package/dist/commands/router.d.ts +18 -0
- package/dist/commands/router.d.ts.map +1 -0
- package/dist/commands/router.js +74 -0
- package/dist/commands/router.js.map +1 -0
- package/dist/commands/setup.d.ts +7 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +89 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/task.d.ts +26 -0
- package/dist/commands/task.d.ts.map +1 -0
- package/dist/commands/task.js +438 -0
- package/dist/commands/task.js.map +1 -0
- package/dist/commands/types.d.ts +173 -0
- package/dist/commands/types.d.ts.map +1 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/types.js.map +1 -0
- package/dist/e2e-crypto.d.ts +36 -0
- package/dist/e2e-crypto.d.ts.map +1 -0
- package/dist/e2e-crypto.js +166 -0
- package/dist/e2e-crypto.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin-config.d.ts +32 -0
- package/dist/plugin-config.d.ts.map +1 -0
- package/dist/plugin-config.js +268 -0
- package/dist/plugin-config.js.map +1 -0
- package/dist/runtime/index.d.ts +13 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +7 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/progress-ticker.d.ts +65 -0
- package/dist/runtime/progress-ticker.d.ts.map +1 -0
- package/dist/runtime/progress-ticker.js +116 -0
- package/dist/runtime/progress-ticker.js.map +1 -0
- package/dist/runtime/session-binding.d.ts +16 -0
- package/dist/runtime/session-binding.d.ts.map +1 -0
- package/dist/runtime/session-binding.js +20 -0
- package/dist/runtime/session-binding.js.map +1 -0
- package/dist/runtime/status-transition.d.ts +19 -0
- package/dist/runtime/status-transition.d.ts.map +1 -0
- package/dist/runtime/status-transition.js +95 -0
- package/dist/runtime/status-transition.js.map +1 -0
- package/dist/runtime/task-executor-persistence.d.ts +63 -0
- package/dist/runtime/task-executor-persistence.d.ts.map +1 -0
- package/dist/runtime/task-executor-persistence.js +201 -0
- package/dist/runtime/task-executor-persistence.js.map +1 -0
- package/dist/runtime/task-executor.d.ts +169 -0
- package/dist/runtime/task-executor.d.ts.map +1 -0
- package/dist/runtime/task-executor.js +1230 -0
- package/dist/runtime/task-executor.js.map +1 -0
- package/dist/runtime/task-status-handler.d.ts +28 -0
- package/dist/runtime/task-status-handler.d.ts.map +1 -0
- package/dist/runtime/task-status-handler.js +102 -0
- package/dist/runtime/task-status-handler.js.map +1 -0
- package/dist/types.d.ts +159 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/ws-client.d.ts +90 -0
- package/dist/ws-client.d.ts.map +1 -0
- package/dist/ws-client.js +385 -0
- package/dist/ws-client.js.map +1 -0
- package/index.ts +19 -0
- package/openclaw.plugin.json +49 -0
- package/package.json +62 -0
- package/scripts/install-bootstrap-cli.sh +54 -0
|
@@ -0,0 +1,1230 @@
|
|
|
1
|
+
import { createProgressHeartbeatScheduler, } from "./progress-ticker.js";
|
|
2
|
+
import { validateTaskTransition, } from "./status-transition.js";
|
|
3
|
+
import { TaskRunExecutorQueueStore, resolveTaskRunExecutorPersistenceOptions, } from "./task-executor-persistence.js";
|
|
4
|
+
const DEFAULT_API_TIMEOUT_MS = 12_000;
|
|
5
|
+
const DEFAULT_MAX_RECOVERY_RETRY_COUNT = 2;
|
|
6
|
+
const DEFAULT_REVIEW_PATCH_RETRY_DELAYS_MS = [250, 750];
|
|
7
|
+
const DEFAULT_COMPENSATING_REVIEW_DELAY_MS = 15_000;
|
|
8
|
+
const DEFAULT_COMPENSATING_REVIEW_RETRY_DELAYS_MS = [500, 1_500];
|
|
9
|
+
const TERMINAL_OR_REVIEW_STATUSES = new Set(["review", "done", "approved", "rejected", "cancelled"]);
|
|
10
|
+
function toErrorMessage(error) {
|
|
11
|
+
if (error instanceof Error)
|
|
12
|
+
return error.message;
|
|
13
|
+
return String(error);
|
|
14
|
+
}
|
|
15
|
+
function isEndpointUnavailableError(error) {
|
|
16
|
+
if (!error || typeof error !== "object")
|
|
17
|
+
return false;
|
|
18
|
+
const code = error.code;
|
|
19
|
+
return code === "ENDPOINT_UNAVAILABLE";
|
|
20
|
+
}
|
|
21
|
+
function normalizeAgentId(raw) {
|
|
22
|
+
if (!raw)
|
|
23
|
+
return undefined;
|
|
24
|
+
const value = raw.trim();
|
|
25
|
+
return value || undefined;
|
|
26
|
+
}
|
|
27
|
+
function normalizeContextToken(raw, fallback = "-") {
|
|
28
|
+
const normalized = raw?.trim();
|
|
29
|
+
return normalized || fallback;
|
|
30
|
+
}
|
|
31
|
+
function normalizeRetryDelaysMs(raw, fallback) {
|
|
32
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
33
|
+
return [...fallback];
|
|
34
|
+
const normalized = raw
|
|
35
|
+
.map((value) => (Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0));
|
|
36
|
+
return normalized.length > 0 ? normalized : [...fallback];
|
|
37
|
+
}
|
|
38
|
+
async function sleepMs(timeoutMs) {
|
|
39
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
40
|
+
return;
|
|
41
|
+
await new Promise((resolve) => {
|
|
42
|
+
setTimeout(resolve, timeoutMs);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function asRecord(value) {
|
|
46
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
47
|
+
return undefined;
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
function pickString(input, keys, fallback = "-") {
|
|
51
|
+
if (!input)
|
|
52
|
+
return fallback;
|
|
53
|
+
for (const key of keys) {
|
|
54
|
+
const value = input[key];
|
|
55
|
+
if (typeof value === "string" && value.trim()) {
|
|
56
|
+
return value.trim();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return fallback;
|
|
60
|
+
}
|
|
61
|
+
function pickNumber(input, keys, fallback = 0) {
|
|
62
|
+
if (!input)
|
|
63
|
+
return fallback;
|
|
64
|
+
for (const key of keys) {
|
|
65
|
+
const raw = input[key];
|
|
66
|
+
const n = Number(raw);
|
|
67
|
+
if (Number.isFinite(n)) {
|
|
68
|
+
return Math.max(0, Math.floor(n));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return fallback;
|
|
72
|
+
}
|
|
73
|
+
function readTaskUsageTotals(raw) {
|
|
74
|
+
const payload = asRecord(raw);
|
|
75
|
+
return {
|
|
76
|
+
promptTokens: pickNumber(payload, ["usage_prompt_tokens", "usagePromptTokens"], 0),
|
|
77
|
+
completionTokens: pickNumber(payload, ["usage_completion_tokens", "usageCompletionTokens"], 0),
|
|
78
|
+
cacheReadTokens: pickNumber(payload, ["usage_cache_read_tokens", "usageCacheReadTokens"], 0),
|
|
79
|
+
cacheWriteTokens: pickNumber(payload, ["usage_cache_write_tokens", "usageCacheWriteTokens"], 0),
|
|
80
|
+
totalTokens: pickNumber(payload, ["usage_total_tokens", "usageTotalTokens"], 0),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function normalizeInferenceUsage(raw) {
|
|
84
|
+
if (!raw)
|
|
85
|
+
return undefined;
|
|
86
|
+
const promptTokens = Math.max(0, Math.floor(Number(raw.promptTokens) || 0));
|
|
87
|
+
const completionTokens = Math.max(0, Math.floor(Number(raw.completionTokens) || 0));
|
|
88
|
+
const cacheReadTokens = Math.max(0, Math.floor(Number(raw.cacheReadTokens) || 0));
|
|
89
|
+
const cacheWriteTokens = Math.max(0, Math.floor(Number(raw.cacheWriteTokens) || 0));
|
|
90
|
+
const providedTotal = Math.max(0, Math.floor(Number(raw.totalTokens) || 0));
|
|
91
|
+
const totalTokens = providedTotal > 0
|
|
92
|
+
? providedTotal
|
|
93
|
+
: (promptTokens + completionTokens + cacheReadTokens + cacheWriteTokens);
|
|
94
|
+
if (totalTokens <= 0 && promptTokens <= 0 && completionTokens <= 0 && cacheReadTokens <= 0 && cacheWriteTokens <= 0) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
promptTokens,
|
|
99
|
+
completionTokens,
|
|
100
|
+
cacheReadTokens,
|
|
101
|
+
cacheWriteTokens,
|
|
102
|
+
totalTokens,
|
|
103
|
+
source: raw.source,
|
|
104
|
+
provider: raw.provider,
|
|
105
|
+
model: raw.model,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function buildUsagePatchFields(base, delta) {
|
|
109
|
+
const normalized = normalizeInferenceUsage(delta);
|
|
110
|
+
if (!normalized)
|
|
111
|
+
return undefined;
|
|
112
|
+
const promptTokens = Math.max(0, base.promptTokens + (normalized.promptTokens ?? 0));
|
|
113
|
+
const completionTokens = Math.max(0, base.completionTokens + (normalized.completionTokens ?? 0));
|
|
114
|
+
const cacheReadTokens = Math.max(0, base.cacheReadTokens + (normalized.cacheReadTokens ?? 0));
|
|
115
|
+
const cacheWriteTokens = Math.max(0, base.cacheWriteTokens + (normalized.cacheWriteTokens ?? 0));
|
|
116
|
+
const totalTokens = Math.max(0, base.totalTokens + (normalized.totalTokens ?? 0));
|
|
117
|
+
return {
|
|
118
|
+
usage_prompt_tokens: promptTokens,
|
|
119
|
+
usage_completion_tokens: completionTokens,
|
|
120
|
+
usage_cache_read_tokens: cacheReadTokens,
|
|
121
|
+
usage_cache_write_tokens: cacheWriteTokens,
|
|
122
|
+
usage_total_tokens: totalTokens,
|
|
123
|
+
usage_source: normalized.source || "runtime_inference",
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function mergeInferenceUsage(base, delta) {
|
|
127
|
+
const left = normalizeInferenceUsage(base);
|
|
128
|
+
const right = normalizeInferenceUsage(delta);
|
|
129
|
+
if (!left)
|
|
130
|
+
return right;
|
|
131
|
+
if (!right)
|
|
132
|
+
return left;
|
|
133
|
+
return {
|
|
134
|
+
promptTokens: (left.promptTokens ?? 0) + (right.promptTokens ?? 0),
|
|
135
|
+
completionTokens: (left.completionTokens ?? 0) + (right.completionTokens ?? 0),
|
|
136
|
+
cacheReadTokens: (left.cacheReadTokens ?? 0) + (right.cacheReadTokens ?? 0),
|
|
137
|
+
cacheWriteTokens: (left.cacheWriteTokens ?? 0) + (right.cacheWriteTokens ?? 0),
|
|
138
|
+
totalTokens: (left.totalTokens ?? 0) + (right.totalTokens ?? 0),
|
|
139
|
+
source: right.source || left.source,
|
|
140
|
+
provider: right.provider || left.provider,
|
|
141
|
+
model: right.model || left.model,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function normalizeTaskDetail(taskId, metadata, raw) {
|
|
145
|
+
const payload = asRecord(raw);
|
|
146
|
+
return {
|
|
147
|
+
id: pickString(payload, ["id"], metadata.id || taskId),
|
|
148
|
+
title: pickString(payload, ["title"], metadata.title || "(未命名任务)"),
|
|
149
|
+
description: pickString(payload, ["description", "desc"], metadata.description ?? ""),
|
|
150
|
+
topicId: pickString(payload, ["topic_id", "topicId"], metadata.topicId || "-"),
|
|
151
|
+
taskType: pickString(payload, ["task_type", "taskType"], metadata.taskType ?? "generic"),
|
|
152
|
+
execMode: pickString(payload, ["exec_mode", "execMode"], metadata.execMode ?? "default"),
|
|
153
|
+
status: pickString(payload, ["status"], metadata.status || "unknown"),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function buildTaskPrompt(task) {
|
|
157
|
+
const description = task.description && task.description !== "-" ? task.description : "(无描述)";
|
|
158
|
+
return [
|
|
159
|
+
"你正在执行 WTT Task。",
|
|
160
|
+
`task_id: ${task.id}`,
|
|
161
|
+
`title: ${task.title}`,
|
|
162
|
+
`description: ${description}`,
|
|
163
|
+
`topic_id: ${task.topicId}`,
|
|
164
|
+
`task_type: ${task.taskType}`,
|
|
165
|
+
`exec_mode: ${task.execMode}`,
|
|
166
|
+
"输出规则(严格遵守):",
|
|
167
|
+
"1) 仅输出最终推理结果正文;不要加标题、前言、结语。",
|
|
168
|
+
"2) 禁止输出这些模板词:STEP、MID、CHANGE、任务结果(可审查)、关键依据、可直接用于 review。",
|
|
169
|
+
"3) 禁止输出系统日志/心跳样式文本。",
|
|
170
|
+
"4) 信息不完整时,直接给出简洁可执行结果;不要解释假设过程,不要出现“最小可执行假设”字样。",
|
|
171
|
+
].join("\n");
|
|
172
|
+
}
|
|
173
|
+
function deriveTriggerContextKey(request) {
|
|
174
|
+
const resolvedRunner = normalizeAgentId(request.runnerAgentId)
|
|
175
|
+
?? normalizeAgentId(request.metadata.runnerAgentId)
|
|
176
|
+
?? "-";
|
|
177
|
+
const parts = [
|
|
178
|
+
`account:${normalizeContextToken(request.accountId)}`,
|
|
179
|
+
`trigger:${normalizeContextToken(request.triggerAgentId)}`,
|
|
180
|
+
`runner:${normalizeContextToken(resolvedRunner)}`,
|
|
181
|
+
`pipeline:${normalizeContextToken(request.metadata.pipelineId)}`,
|
|
182
|
+
`topic:${normalizeContextToken(request.metadata.topicId)}`,
|
|
183
|
+
];
|
|
184
|
+
return parts.join("|");
|
|
185
|
+
}
|
|
186
|
+
function deriveIdempotencyKey(taskId, triggerContextKey) {
|
|
187
|
+
return `${taskId}::${triggerContextKey}`;
|
|
188
|
+
}
|
|
189
|
+
function normalizeRecoveryMetadata(recovery, maxDefault) {
|
|
190
|
+
const maxRetryCount = Math.max(0, Math.floor(recovery?.maxRetryCount ?? maxDefault));
|
|
191
|
+
const retryCount = Math.max(0, Math.floor(recovery?.retryCount ?? 0));
|
|
192
|
+
return {
|
|
193
|
+
retryCount,
|
|
194
|
+
maxRetryCount,
|
|
195
|
+
lastRecoveredAt: recovery?.lastRecoveredAt,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function isTerminalOrReviewStatus(status) {
|
|
199
|
+
if (!status)
|
|
200
|
+
return false;
|
|
201
|
+
return TERMINAL_OR_REVIEW_STATUSES.has(status.trim().toLowerCase());
|
|
202
|
+
}
|
|
203
|
+
function truncatePreview(text, maxLength = 280) {
|
|
204
|
+
if (text.length <= maxLength)
|
|
205
|
+
return text;
|
|
206
|
+
return `${text.slice(0, maxLength)}...`;
|
|
207
|
+
}
|
|
208
|
+
function buildSummary(params) {
|
|
209
|
+
const output = params.outputText?.trim() ?? "";
|
|
210
|
+
return {
|
|
211
|
+
kind: "task_run_summary",
|
|
212
|
+
taskId: params.taskId,
|
|
213
|
+
at: params.now.toISOString(),
|
|
214
|
+
status: params.status,
|
|
215
|
+
action: params.action,
|
|
216
|
+
transitionApplied: params.transitionApplied,
|
|
217
|
+
notes: params.notes,
|
|
218
|
+
outputPreview: output ? truncatePreview(output) : undefined,
|
|
219
|
+
nextSteps: [
|
|
220
|
+
"每 60 秒汇报一次:时间 / 状态 / 当前动作。",
|
|
221
|
+
"执行结束后复核输出,并按需执行 @wtt task review approve/reject/block。",
|
|
222
|
+
"若结果通过评审,任务应从 review 进入 done。",
|
|
223
|
+
],
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function withApiPrefixCandidates(pathname) {
|
|
227
|
+
const normalized = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
228
|
+
if (normalized.startsWith("/api/"))
|
|
229
|
+
return [normalized];
|
|
230
|
+
return [normalized, `/api${normalized}`, `/api/v1${normalized}`];
|
|
231
|
+
}
|
|
232
|
+
function hasJsonContentType(response) {
|
|
233
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
234
|
+
return contentType.toLowerCase().includes("application/json");
|
|
235
|
+
}
|
|
236
|
+
async function parseResponseBody(response) {
|
|
237
|
+
if (response.status === 204)
|
|
238
|
+
return {};
|
|
239
|
+
if (hasJsonContentType(response)) {
|
|
240
|
+
try {
|
|
241
|
+
return await response.json();
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// fallthrough
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const text = await response.text();
|
|
248
|
+
if (!text)
|
|
249
|
+
return {};
|
|
250
|
+
try {
|
|
251
|
+
return JSON.parse(text);
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return { message: text };
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function extractErrorMessage(payload) {
|
|
258
|
+
if (typeof payload === "string" && payload.trim())
|
|
259
|
+
return payload.trim();
|
|
260
|
+
if (typeof payload === "object" && payload !== null) {
|
|
261
|
+
const data = payload;
|
|
262
|
+
for (const key of ["detail", "error", "message", "msg", "reason"]) {
|
|
263
|
+
const value = data[key];
|
|
264
|
+
if (typeof value === "string" && value.trim())
|
|
265
|
+
return value.trim();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return "服务端返回异常";
|
|
269
|
+
}
|
|
270
|
+
function createErrorWithCode(message, code) {
|
|
271
|
+
const err = new Error(message);
|
|
272
|
+
err.code = code;
|
|
273
|
+
return err;
|
|
274
|
+
}
|
|
275
|
+
function buildApiRequestFromContext(context) {
|
|
276
|
+
const cloudUrl = context.cloudUrl.replace(/\/$/, "");
|
|
277
|
+
const token = context.token?.trim();
|
|
278
|
+
const timeoutMs = context.timeoutMs ?? DEFAULT_API_TIMEOUT_MS;
|
|
279
|
+
return async (request) => {
|
|
280
|
+
const headers = {
|
|
281
|
+
Accept: "application/json",
|
|
282
|
+
};
|
|
283
|
+
if (request.body !== undefined)
|
|
284
|
+
headers["Content-Type"] = "application/json";
|
|
285
|
+
if (token) {
|
|
286
|
+
headers.Authorization = `Bearer ${token}`;
|
|
287
|
+
headers["X-Agent-Token"] = token;
|
|
288
|
+
}
|
|
289
|
+
const candidates = withApiPrefixCandidates(request.path);
|
|
290
|
+
for (const candidate of candidates) {
|
|
291
|
+
const endpoint = `${cloudUrl}${candidate}`;
|
|
292
|
+
const controller = new AbortController();
|
|
293
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
294
|
+
try {
|
|
295
|
+
const response = await fetch(endpoint, {
|
|
296
|
+
method: request.method,
|
|
297
|
+
headers,
|
|
298
|
+
body: request.body === undefined ? undefined : JSON.stringify(request.body),
|
|
299
|
+
signal: controller.signal,
|
|
300
|
+
});
|
|
301
|
+
const payload = await parseResponseBody(response);
|
|
302
|
+
if (response.ok)
|
|
303
|
+
return payload;
|
|
304
|
+
if (response.status === 404 || response.status === 405) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (response.status === 401 || response.status === 403) {
|
|
308
|
+
throw createErrorWithCode("鉴权失败", "UNAUTHORIZED");
|
|
309
|
+
}
|
|
310
|
+
throw new Error(extractErrorMessage(payload));
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
const err = error;
|
|
314
|
+
const isAbort = error instanceof Error
|
|
315
|
+
&& (error.name === "AbortError" || /aborted/i.test(error.message));
|
|
316
|
+
if (isAbort) {
|
|
317
|
+
throw createErrorWithCode("请求超时", "TIMEOUT");
|
|
318
|
+
}
|
|
319
|
+
if (err?.code === "UNAUTHORIZED") {
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
if (error instanceof Error) {
|
|
323
|
+
throw createErrorWithCode(error.message, "NETWORK_ERROR");
|
|
324
|
+
}
|
|
325
|
+
throw createErrorWithCode(String(error), "NETWORK_ERROR");
|
|
326
|
+
}
|
|
327
|
+
finally {
|
|
328
|
+
clearTimeout(timer);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
throw createErrorWithCode("服务端暂未暴露该接口", "ENDPOINT_UNAVAILABLE");
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function toPersistedIntent(request) {
|
|
335
|
+
return {
|
|
336
|
+
taskId: request.taskId,
|
|
337
|
+
metadata: request.metadata,
|
|
338
|
+
accountId: request.accountId,
|
|
339
|
+
triggerAgentId: request.triggerAgentId,
|
|
340
|
+
runnerAgentId: request.runnerAgentId,
|
|
341
|
+
note: request.note,
|
|
342
|
+
heartbeatSeconds: request.heartbeatSeconds,
|
|
343
|
+
apiContext: request.apiContext,
|
|
344
|
+
triggerContextKey: request.triggerContextKey,
|
|
345
|
+
idempotencyKey: request.idempotencyKey,
|
|
346
|
+
recovery: request.recovery,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
function toIsoString(input, fallback) {
|
|
350
|
+
const parsed = new Date(input);
|
|
351
|
+
if (Number.isNaN(parsed.getTime()))
|
|
352
|
+
return fallback.toISOString();
|
|
353
|
+
return parsed.toISOString();
|
|
354
|
+
}
|
|
355
|
+
class InMemoryTaskRunExecutor {
|
|
356
|
+
queue = [];
|
|
357
|
+
processing = false;
|
|
358
|
+
runningItem;
|
|
359
|
+
stopping = false;
|
|
360
|
+
lastError = null;
|
|
361
|
+
persistenceEnabled;
|
|
362
|
+
persistenceFilePath;
|
|
363
|
+
queueStore;
|
|
364
|
+
createRecoveredApiRequest;
|
|
365
|
+
onRecoveredExecutionResult;
|
|
366
|
+
onRecoveredExecutionError;
|
|
367
|
+
startupReady;
|
|
368
|
+
maxRecoveryRetryCount;
|
|
369
|
+
reviewPatchRetryDelaysMs;
|
|
370
|
+
compensatingReviewDelayMs;
|
|
371
|
+
compensatingReviewRetryDelaysMs;
|
|
372
|
+
compensatingTimers = new Set();
|
|
373
|
+
drainInFlight = false;
|
|
374
|
+
drainRequested = false;
|
|
375
|
+
drainPromise = null;
|
|
376
|
+
drainLockContention = 0;
|
|
377
|
+
enqueueAccepted = 0;
|
|
378
|
+
dedupHits = 0;
|
|
379
|
+
recoveryStats = {
|
|
380
|
+
loadedQueued: 0,
|
|
381
|
+
loadedRunning: 0,
|
|
382
|
+
requeued: 0,
|
|
383
|
+
skippedTerminalOrReview: 0,
|
|
384
|
+
skippedByRetryCap: 0,
|
|
385
|
+
retryAttempts: 0,
|
|
386
|
+
};
|
|
387
|
+
constructor(options) {
|
|
388
|
+
this.createRecoveredApiRequest = options?.createRecoveredApiRequest;
|
|
389
|
+
this.onRecoveredExecutionResult = options?.onRecoveredExecutionResult;
|
|
390
|
+
this.onRecoveredExecutionError = options?.onRecoveredExecutionError;
|
|
391
|
+
this.maxRecoveryRetryCount = Math.max(0, Math.floor(options?.maxRecoveryRetryCount ?? DEFAULT_MAX_RECOVERY_RETRY_COUNT));
|
|
392
|
+
this.reviewPatchRetryDelaysMs = normalizeRetryDelaysMs(options?.reviewPatchRetryDelaysMs, DEFAULT_REVIEW_PATCH_RETRY_DELAYS_MS);
|
|
393
|
+
this.compensatingReviewDelayMs = Math.max(0, Math.floor(options?.compensatingReviewDelayMs ?? DEFAULT_COMPENSATING_REVIEW_DELAY_MS));
|
|
394
|
+
this.compensatingReviewRetryDelaysMs = normalizeRetryDelaysMs(options?.compensatingReviewRetryDelaysMs, DEFAULT_COMPENSATING_REVIEW_RETRY_DELAYS_MS);
|
|
395
|
+
const persistence = resolveTaskRunExecutorPersistenceOptions(options?.persistence);
|
|
396
|
+
this.persistenceEnabled = persistence.enabled;
|
|
397
|
+
this.persistenceFilePath = persistence.filePath;
|
|
398
|
+
if (this.persistenceEnabled) {
|
|
399
|
+
this.queueStore = new TaskRunExecutorQueueStore(persistence.filePath);
|
|
400
|
+
this.startupReady = this.loadPersistedQueueOnce();
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
this.startupReady = Promise.resolve();
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async enqueueRun(request) {
|
|
407
|
+
await this.startupReady;
|
|
408
|
+
if (this.stopping) {
|
|
409
|
+
throw new Error("task executor is stopping; reject enqueue");
|
|
410
|
+
}
|
|
411
|
+
const now = request.now ?? (() => new Date());
|
|
412
|
+
const triggerContextKey = request.triggerContextKey ?? deriveTriggerContextKey(request);
|
|
413
|
+
const idempotencyKey = request.idempotencyKey ?? deriveIdempotencyKey(request.taskId, triggerContextKey);
|
|
414
|
+
const duplicate = this.findDuplicate(request.taskId, idempotencyKey);
|
|
415
|
+
if (duplicate) {
|
|
416
|
+
this.dedupHits += 1;
|
|
417
|
+
return {
|
|
418
|
+
deduplicated: true,
|
|
419
|
+
taskId: request.taskId,
|
|
420
|
+
accountId: request.accountId,
|
|
421
|
+
queueLength: this.queue.length,
|
|
422
|
+
runningTaskId: this.runningItem?.request.taskId ?? null,
|
|
423
|
+
persistence: {
|
|
424
|
+
enabled: this.persistenceEnabled,
|
|
425
|
+
filePath: this.persistenceEnabled ? this.persistenceFilePath : undefined,
|
|
426
|
+
},
|
|
427
|
+
idempotency: {
|
|
428
|
+
decision: "deduplicated",
|
|
429
|
+
idempotencyKey,
|
|
430
|
+
triggerContextKey,
|
|
431
|
+
reason: duplicate.reason,
|
|
432
|
+
duplicateState: duplicate.state,
|
|
433
|
+
duplicateTaskId: duplicate.item.request.taskId,
|
|
434
|
+
duplicateEnqueuedAt: duplicate.item.enqueuedAt.toISOString(),
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
this.enqueueAccepted += 1;
|
|
439
|
+
return new Promise((resolve, reject) => {
|
|
440
|
+
this.queue.push({
|
|
441
|
+
request: {
|
|
442
|
+
...request,
|
|
443
|
+
triggerContextKey,
|
|
444
|
+
idempotencyKey,
|
|
445
|
+
recovery: normalizeRecoveryMetadata(request.recovery, this.maxRecoveryRetryCount),
|
|
446
|
+
},
|
|
447
|
+
enqueuedAt: now(),
|
|
448
|
+
resolve,
|
|
449
|
+
reject,
|
|
450
|
+
idempotencyKey,
|
|
451
|
+
triggerContextKey,
|
|
452
|
+
});
|
|
453
|
+
void this.persistQueueState();
|
|
454
|
+
this.requestDrain();
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
getQueueDepth() {
|
|
458
|
+
return this.queue.length;
|
|
459
|
+
}
|
|
460
|
+
isProcessing() {
|
|
461
|
+
return this.processing;
|
|
462
|
+
}
|
|
463
|
+
getSnapshot() {
|
|
464
|
+
return {
|
|
465
|
+
queueLength: this.queue.length,
|
|
466
|
+
runningTaskId: this.runningItem?.request.taskId ?? null,
|
|
467
|
+
processing: this.processing,
|
|
468
|
+
stopping: this.stopping,
|
|
469
|
+
lastError: this.lastError,
|
|
470
|
+
persistenceEnabled: this.persistenceEnabled,
|
|
471
|
+
persistenceFilePath: this.persistenceEnabled ? this.persistenceFilePath : undefined,
|
|
472
|
+
enqueueAccepted: this.enqueueAccepted,
|
|
473
|
+
dedupHits: this.dedupHits,
|
|
474
|
+
recovery: {
|
|
475
|
+
loadedQueued: this.recoveryStats.loadedQueued,
|
|
476
|
+
loadedRunning: this.recoveryStats.loadedRunning,
|
|
477
|
+
requeued: this.recoveryStats.requeued,
|
|
478
|
+
skippedTerminalOrReview: this.recoveryStats.skippedTerminalOrReview,
|
|
479
|
+
skippedByRetryCap: this.recoveryStats.skippedByRetryCap,
|
|
480
|
+
retryAttempts: this.recoveryStats.retryAttempts,
|
|
481
|
+
},
|
|
482
|
+
drainLockContention: this.drainLockContention,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
getStatus() {
|
|
486
|
+
return this.getSnapshot();
|
|
487
|
+
}
|
|
488
|
+
async stopGracefully() {
|
|
489
|
+
this.stopping = true;
|
|
490
|
+
await this.startupReady;
|
|
491
|
+
if (this.drainPromise) {
|
|
492
|
+
await this.drainPromise;
|
|
493
|
+
}
|
|
494
|
+
for (const timer of this.compensatingTimers) {
|
|
495
|
+
clearTimeout(timer);
|
|
496
|
+
}
|
|
497
|
+
this.compensatingTimers.clear();
|
|
498
|
+
await this.persistQueueState();
|
|
499
|
+
}
|
|
500
|
+
findDuplicate(taskId, idempotencyKey) {
|
|
501
|
+
if (this.runningItem) {
|
|
502
|
+
if (this.runningItem.request.taskId === taskId || this.runningItem.idempotencyKey === idempotencyKey) {
|
|
503
|
+
const sameKey = this.runningItem.idempotencyKey === idempotencyKey;
|
|
504
|
+
return {
|
|
505
|
+
state: "running",
|
|
506
|
+
item: this.runningItem,
|
|
507
|
+
reason: sameKey
|
|
508
|
+
? "同一 task + trigger context 已在运行中,已触发幂等去重。"
|
|
509
|
+
: "同一 task 已在运行中(触发上下文不同),为避免重复执行已阻止重复入队。",
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const queued = this.queue.find((item) => item.request.taskId === taskId || item.idempotencyKey === idempotencyKey);
|
|
514
|
+
if (queued) {
|
|
515
|
+
const sameKey = queued.idempotencyKey === idempotencyKey;
|
|
516
|
+
return {
|
|
517
|
+
state: "queued",
|
|
518
|
+
item: queued,
|
|
519
|
+
reason: sameKey
|
|
520
|
+
? "同一 task + trigger context 已在队列中,已触发幂等去重。"
|
|
521
|
+
: "同一 task 已在队列中(触发上下文不同),为避免重复执行已阻止重复入队。",
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
requestDrain() {
|
|
527
|
+
if (this.stopping)
|
|
528
|
+
return;
|
|
529
|
+
this.drainRequested = true;
|
|
530
|
+
if (this.drainInFlight) {
|
|
531
|
+
this.drainLockContention += 1;
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
this.drainInFlight = true;
|
|
535
|
+
this.processing = true;
|
|
536
|
+
this.drainPromise = this.runDrainSingleFlight().finally(() => {
|
|
537
|
+
this.processing = false;
|
|
538
|
+
this.drainInFlight = false;
|
|
539
|
+
this.drainPromise = null;
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
async runDrainSingleFlight() {
|
|
543
|
+
try {
|
|
544
|
+
while (!this.stopping && this.drainRequested) {
|
|
545
|
+
this.drainRequested = false;
|
|
546
|
+
while (!this.stopping && this.queue.length > 0) {
|
|
547
|
+
const item = this.queue.shift();
|
|
548
|
+
if (!item)
|
|
549
|
+
continue;
|
|
550
|
+
this.runningItem = item;
|
|
551
|
+
await this.persistQueueState();
|
|
552
|
+
try {
|
|
553
|
+
const result = await this.execute(item, this.queue.length);
|
|
554
|
+
item.resolve(result);
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
this.lastError = toErrorMessage(error);
|
|
558
|
+
item.reject(error);
|
|
559
|
+
}
|
|
560
|
+
finally {
|
|
561
|
+
this.runningItem = undefined;
|
|
562
|
+
await this.persistQueueState();
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
finally {
|
|
568
|
+
// noop
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
async loadPersistedQueueOnce() {
|
|
572
|
+
if (!this.queueStore)
|
|
573
|
+
return;
|
|
574
|
+
try {
|
|
575
|
+
const state = await this.queueStore.load();
|
|
576
|
+
const now = new Date();
|
|
577
|
+
const recovered = [];
|
|
578
|
+
let touchedPersistedState = false;
|
|
579
|
+
const appendRecovered = (item, source) => {
|
|
580
|
+
touchedPersistedState = true;
|
|
581
|
+
if (source === "running")
|
|
582
|
+
this.recoveryStats.loadedRunning += 1;
|
|
583
|
+
else
|
|
584
|
+
this.recoveryStats.loadedQueued += 1;
|
|
585
|
+
const status = item.intent.metadata.status;
|
|
586
|
+
if (isTerminalOrReviewStatus(status)) {
|
|
587
|
+
this.recoveryStats.skippedTerminalOrReview += 1;
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
const recovery = normalizeRecoveryMetadata(item.intent.recovery, this.maxRecoveryRetryCount);
|
|
591
|
+
const nextRecovery = {
|
|
592
|
+
...recovery,
|
|
593
|
+
};
|
|
594
|
+
if (source === "running") {
|
|
595
|
+
if (recovery.retryCount >= recovery.maxRetryCount) {
|
|
596
|
+
this.recoveryStats.skippedByRetryCap += 1;
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
nextRecovery.retryCount = recovery.retryCount + 1;
|
|
600
|
+
nextRecovery.lastRecoveredAt = now.toISOString();
|
|
601
|
+
this.recoveryStats.retryAttempts += 1;
|
|
602
|
+
}
|
|
603
|
+
const request = this.buildRecoveredRequest({
|
|
604
|
+
...item.intent,
|
|
605
|
+
recovery: nextRecovery,
|
|
606
|
+
}, source);
|
|
607
|
+
const triggerContextKey = request.triggerContextKey ?? deriveTriggerContextKey(request);
|
|
608
|
+
const idempotencyKey = request.idempotencyKey ?? deriveIdempotencyKey(request.taskId, triggerContextKey);
|
|
609
|
+
const enqueuedAtRaw = toIsoString(item.enqueuedAt, now);
|
|
610
|
+
const recoveryNote = source === "running"
|
|
611
|
+
? `检测到任务上次处于 running 未完成,已按恢复模式重新入队(retry ${nextRecovery.retryCount}/${nextRecovery.maxRetryCount})。`
|
|
612
|
+
: "任务从持久化队列恢复。";
|
|
613
|
+
recovered.push({
|
|
614
|
+
request: {
|
|
615
|
+
...request,
|
|
616
|
+
triggerContextKey,
|
|
617
|
+
idempotencyKey,
|
|
618
|
+
recovery: nextRecovery,
|
|
619
|
+
},
|
|
620
|
+
enqueuedAt: new Date(enqueuedAtRaw),
|
|
621
|
+
resolve: (result) => {
|
|
622
|
+
this.onRecoveredExecutionResult?.(result);
|
|
623
|
+
},
|
|
624
|
+
reject: (error) => {
|
|
625
|
+
this.lastError = `recovered task ${request.taskId} failed: ${toErrorMessage(error)}`;
|
|
626
|
+
this.onRecoveredExecutionError?.(error, item.intent);
|
|
627
|
+
},
|
|
628
|
+
recovery: {
|
|
629
|
+
source,
|
|
630
|
+
note: recoveryNote,
|
|
631
|
+
},
|
|
632
|
+
idempotencyKey,
|
|
633
|
+
triggerContextKey,
|
|
634
|
+
});
|
|
635
|
+
};
|
|
636
|
+
if (state.running) {
|
|
637
|
+
appendRecovered(state.running, "running");
|
|
638
|
+
}
|
|
639
|
+
for (const item of state.queued) {
|
|
640
|
+
appendRecovered(item, "queued");
|
|
641
|
+
}
|
|
642
|
+
for (const item of recovered) {
|
|
643
|
+
const duplicate = this.findDuplicate(item.request.taskId, item.idempotencyKey);
|
|
644
|
+
if (duplicate) {
|
|
645
|
+
this.dedupHits += 1;
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
this.queue.push(item);
|
|
649
|
+
this.recoveryStats.requeued += 1;
|
|
650
|
+
}
|
|
651
|
+
if (touchedPersistedState) {
|
|
652
|
+
await this.persistQueueState();
|
|
653
|
+
}
|
|
654
|
+
if (this.queue.length > 0) {
|
|
655
|
+
this.requestDrain();
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
catch (error) {
|
|
659
|
+
this.lastError = `load persisted queue failed: ${toErrorMessage(error)}`;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
buildRecoveredRequest(intent, source) {
|
|
663
|
+
const apiRequest = this.createRecoveredApiRequest?.(intent, source)
|
|
664
|
+
?? (intent.apiContext ? buildApiRequestFromContext(intent.apiContext) : undefined)
|
|
665
|
+
?? (async () => {
|
|
666
|
+
throw createErrorWithCode("recovered task missing apiContext/createRecoveredApiRequest", "ENDPOINT_UNAVAILABLE");
|
|
667
|
+
});
|
|
668
|
+
return {
|
|
669
|
+
taskId: intent.taskId,
|
|
670
|
+
metadata: intent.metadata,
|
|
671
|
+
accountId: intent.accountId,
|
|
672
|
+
triggerAgentId: intent.triggerAgentId,
|
|
673
|
+
runnerAgentId: intent.runnerAgentId,
|
|
674
|
+
note: intent.note,
|
|
675
|
+
heartbeatSeconds: intent.heartbeatSeconds,
|
|
676
|
+
apiContext: intent.apiContext,
|
|
677
|
+
triggerContextKey: intent.triggerContextKey,
|
|
678
|
+
idempotencyKey: intent.idempotencyKey,
|
|
679
|
+
recovery: normalizeRecoveryMetadata(intent.recovery, this.maxRecoveryRetryCount),
|
|
680
|
+
apiRequest,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
async persistQueueState() {
|
|
684
|
+
if (!this.queueStore)
|
|
685
|
+
return;
|
|
686
|
+
try {
|
|
687
|
+
await this.queueStore.save({
|
|
688
|
+
queued: this.queue.map((item) => ({
|
|
689
|
+
intent: toPersistedIntent(item.request),
|
|
690
|
+
enqueuedAt: item.enqueuedAt.toISOString(),
|
|
691
|
+
})),
|
|
692
|
+
running: this.runningItem
|
|
693
|
+
? {
|
|
694
|
+
intent: toPersistedIntent(this.runningItem.request),
|
|
695
|
+
enqueuedAt: this.runningItem.enqueuedAt.toISOString(),
|
|
696
|
+
}
|
|
697
|
+
: undefined,
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
catch (error) {
|
|
701
|
+
this.lastError = `persist queue failed: ${toErrorMessage(error)}`;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
logReviewTransition(level, message) {
|
|
705
|
+
const line = `[task-executor] ${message}`;
|
|
706
|
+
if (level === "error") {
|
|
707
|
+
console.error(line);
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (level === "warn") {
|
|
711
|
+
console.warn(line);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
console.info(line);
|
|
715
|
+
}
|
|
716
|
+
async patchTaskStatusWithRetry(params) {
|
|
717
|
+
const retryDelaysMs = params.retryDelaysMs.length > 0 ? params.retryDelaysMs : [0];
|
|
718
|
+
const totalAttempts = retryDelaysMs.length + 1;
|
|
719
|
+
const notes = [];
|
|
720
|
+
let lastError;
|
|
721
|
+
for (let index = 0; index < totalAttempts; index += 1) {
|
|
722
|
+
const attempt = index + 1;
|
|
723
|
+
const attemptPrefix = `${params.notePrefix} attempt ${attempt}/${totalAttempts}`;
|
|
724
|
+
const patchBody = {
|
|
725
|
+
status: params.status,
|
|
726
|
+
notes: params.notes,
|
|
727
|
+
...(params.patchFields ?? {}),
|
|
728
|
+
};
|
|
729
|
+
if (params.runnerAgentId)
|
|
730
|
+
patchBody.runner_agent_id = params.runnerAgentId;
|
|
731
|
+
try {
|
|
732
|
+
await params.request.apiRequest({
|
|
733
|
+
method: "PATCH",
|
|
734
|
+
path: `/tasks/${encodeURIComponent(params.taskId)}`,
|
|
735
|
+
body: patchBody,
|
|
736
|
+
});
|
|
737
|
+
const okNote = `${attemptPrefix} succeeded`;
|
|
738
|
+
notes.push(okNote);
|
|
739
|
+
this.logReviewTransition("info", `${params.taskId} ${okNote}`);
|
|
740
|
+
return {
|
|
741
|
+
succeeded: true,
|
|
742
|
+
attempts: attempt,
|
|
743
|
+
notes,
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
catch (error) {
|
|
747
|
+
lastError = toErrorMessage(error);
|
|
748
|
+
const failedNote = `${attemptPrefix} failed: ${lastError}`;
|
|
749
|
+
notes.push(failedNote);
|
|
750
|
+
this.logReviewTransition("warn", `${params.taskId} ${failedNote}`);
|
|
751
|
+
if (attempt >= totalAttempts) {
|
|
752
|
+
const finalNote = `${params.notePrefix} failed after ${totalAttempts} attempts`;
|
|
753
|
+
notes.push(finalNote);
|
|
754
|
+
this.logReviewTransition("error", `${params.taskId} ${finalNote}`);
|
|
755
|
+
return {
|
|
756
|
+
succeeded: false,
|
|
757
|
+
attempts: attempt,
|
|
758
|
+
lastError,
|
|
759
|
+
notes,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
const delayMs = retryDelaysMs[index] ?? retryDelaysMs[retryDelaysMs.length - 1] ?? 0;
|
|
763
|
+
if (delayMs > 0) {
|
|
764
|
+
const waitNote = `${params.notePrefix} retry backoff ${delayMs}ms before attempt ${attempt + 1}`;
|
|
765
|
+
notes.push(waitNote);
|
|
766
|
+
this.logReviewTransition("info", `${params.taskId} ${waitNote}`);
|
|
767
|
+
await sleepMs(delayMs);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return {
|
|
772
|
+
succeeded: false,
|
|
773
|
+
attempts: totalAttempts,
|
|
774
|
+
lastError,
|
|
775
|
+
notes,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
scheduleCompensatingReviewPatch(params) {
|
|
779
|
+
if (this.stopping)
|
|
780
|
+
return;
|
|
781
|
+
const delayMs = this.compensatingReviewDelayMs;
|
|
782
|
+
const timer = setTimeout(() => {
|
|
783
|
+
this.compensatingTimers.delete(timer);
|
|
784
|
+
void this.runCompensatingReviewPatch(params);
|
|
785
|
+
}, delayMs);
|
|
786
|
+
this.compensatingTimers.add(timer);
|
|
787
|
+
timer.unref?.();
|
|
788
|
+
this.logReviewTransition("warn", `${params.taskId} watchdog scheduled compensating review patch in ${delayMs}ms (reason=${params.failureReason})`);
|
|
789
|
+
}
|
|
790
|
+
async runCompensatingReviewPatch(params) {
|
|
791
|
+
if (this.stopping)
|
|
792
|
+
return;
|
|
793
|
+
const compensatingNotes = [
|
|
794
|
+
params.reviewNotes,
|
|
795
|
+
"",
|
|
796
|
+
`[watchdog] compensating_review_at=${new Date().toISOString()}`,
|
|
797
|
+
`[watchdog] initial_failure=${params.failureReason}`,
|
|
798
|
+
].join("\n");
|
|
799
|
+
const directReview = await this.patchTaskStatusWithRetry({
|
|
800
|
+
taskId: params.taskId,
|
|
801
|
+
request: params.request,
|
|
802
|
+
status: "review",
|
|
803
|
+
notes: compensatingNotes,
|
|
804
|
+
runnerAgentId: params.runnerAgentId,
|
|
805
|
+
patchFields: params.patchFields,
|
|
806
|
+
retryDelaysMs: this.compensatingReviewRetryDelaysMs,
|
|
807
|
+
notePrefix: "watchdog.review_patch",
|
|
808
|
+
});
|
|
809
|
+
if (directReview.succeeded) {
|
|
810
|
+
this.logReviewTransition("info", `${params.taskId} watchdog compensating review patch succeeded`);
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
const toDoing = await this.patchTaskStatusWithRetry({
|
|
814
|
+
taskId: params.taskId,
|
|
815
|
+
request: params.request,
|
|
816
|
+
status: "doing",
|
|
817
|
+
notes: [
|
|
818
|
+
"watchdog compensation: preparing retry path to review",
|
|
819
|
+
`reason=${directReview.lastError ?? params.failureReason}`,
|
|
820
|
+
].join("\n"),
|
|
821
|
+
runnerAgentId: params.runnerAgentId,
|
|
822
|
+
retryDelaysMs: [0],
|
|
823
|
+
notePrefix: "watchdog.doing_patch",
|
|
824
|
+
});
|
|
825
|
+
if (!toDoing.succeeded) {
|
|
826
|
+
this.logReviewTransition("error", `${params.taskId} watchdog failed to move task back to doing for compensating review patch`);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
const reviewAfterDoing = await this.patchTaskStatusWithRetry({
|
|
830
|
+
taskId: params.taskId,
|
|
831
|
+
request: params.request,
|
|
832
|
+
status: "review",
|
|
833
|
+
notes: compensatingNotes,
|
|
834
|
+
runnerAgentId: params.runnerAgentId,
|
|
835
|
+
patchFields: params.patchFields,
|
|
836
|
+
retryDelaysMs: this.compensatingReviewRetryDelaysMs,
|
|
837
|
+
notePrefix: "watchdog.review_patch_after_doing",
|
|
838
|
+
});
|
|
839
|
+
if (reviewAfterDoing.succeeded) {
|
|
840
|
+
this.logReviewTransition("info", `${params.taskId} watchdog compensating review patch succeeded after doing bridge`);
|
|
841
|
+
}
|
|
842
|
+
else {
|
|
843
|
+
this.logReviewTransition("error", `${params.taskId} watchdog compensating review patch still failed: ${reviewAfterDoing.lastError ?? "unknown"}`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
async execute(item, pendingAfterDequeue) {
|
|
847
|
+
const request = item.request;
|
|
848
|
+
const nowFn = request.now ?? (() => new Date());
|
|
849
|
+
const dequeuedAt = nowFn();
|
|
850
|
+
const transition = validateTaskTransition({
|
|
851
|
+
currentStatus: request.metadata.status,
|
|
852
|
+
intent: { kind: "run" },
|
|
853
|
+
});
|
|
854
|
+
let heartbeatPublished = 0;
|
|
855
|
+
const heartbeatPublishErrors = [];
|
|
856
|
+
let task = normalizeTaskDetail(request.taskId, request.metadata);
|
|
857
|
+
let taskUsageTotals = readTaskUsageTotals();
|
|
858
|
+
const runnerAgentId = normalizeAgentId(request.runnerAgentId) ?? normalizeAgentId(request.metadata.runnerAgentId);
|
|
859
|
+
const inferenceState = {
|
|
860
|
+
attempted: false,
|
|
861
|
+
succeeded: false,
|
|
862
|
+
provider: "none",
|
|
863
|
+
outputText: "",
|
|
864
|
+
prompt: "",
|
|
865
|
+
};
|
|
866
|
+
const scheduler = createProgressHeartbeatScheduler({
|
|
867
|
+
taskId: request.taskId,
|
|
868
|
+
status: transition.allowed ? transition.toStatus : transition.fromStatus,
|
|
869
|
+
action: "任务已进入本地执行器",
|
|
870
|
+
heartbeatSeconds: request.heartbeatSeconds,
|
|
871
|
+
now: nowFn,
|
|
872
|
+
getMetrics: async () => {
|
|
873
|
+
const elapsedSeconds = Math.max(0, Math.floor((nowFn().getTime() - dequeuedAt.getTime()) / 1000));
|
|
874
|
+
let runtimeMetrics;
|
|
875
|
+
if (request.getSessionRuntimeMetrics) {
|
|
876
|
+
try {
|
|
877
|
+
runtimeMetrics = await request.getSessionRuntimeMetrics();
|
|
878
|
+
}
|
|
879
|
+
catch {
|
|
880
|
+
runtimeMetrics = undefined;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
return {
|
|
884
|
+
elapsedSeconds,
|
|
885
|
+
queueDepth: Number.isFinite(runtimeMetrics?.queueDepth)
|
|
886
|
+
? Math.max(0, Math.floor(Number(runtimeMetrics?.queueDepth)))
|
|
887
|
+
: Math.max(0, pendingAfterDequeue),
|
|
888
|
+
queueMode: runtimeMetrics?.queueMode,
|
|
889
|
+
source: runtimeMetrics?.source,
|
|
890
|
+
sessionKey: runtimeMetrics?.sessionKey,
|
|
891
|
+
inflight: typeof runtimeMetrics?.inflight === "boolean" ? runtimeMetrics.inflight : true,
|
|
892
|
+
};
|
|
893
|
+
},
|
|
894
|
+
publish: async (payload) => {
|
|
895
|
+
if (!request.publishHeartbeat)
|
|
896
|
+
return;
|
|
897
|
+
try {
|
|
898
|
+
await request.publishHeartbeat(payload);
|
|
899
|
+
heartbeatPublished += 1;
|
|
900
|
+
}
|
|
901
|
+
catch (error) {
|
|
902
|
+
heartbeatPublishErrors.push(toErrorMessage(error));
|
|
903
|
+
}
|
|
904
|
+
},
|
|
905
|
+
});
|
|
906
|
+
scheduler.start();
|
|
907
|
+
let transitionApplied = "none";
|
|
908
|
+
let endpointFallbackUsed = false;
|
|
909
|
+
let fallbackMessage;
|
|
910
|
+
const summaryNotes = [];
|
|
911
|
+
const buildResult = (params) => ({
|
|
912
|
+
deduplicated: false,
|
|
913
|
+
taskId: request.taskId,
|
|
914
|
+
accountId: request.accountId,
|
|
915
|
+
queue: {
|
|
916
|
+
enqueuedAt: item.enqueuedAt.toISOString(),
|
|
917
|
+
dequeuedAt: dequeuedAt.toISOString(),
|
|
918
|
+
finishedAt: params.finishedAt.toISOString(),
|
|
919
|
+
pendingAfterDequeue,
|
|
920
|
+
},
|
|
921
|
+
transition,
|
|
922
|
+
transitionApplied,
|
|
923
|
+
endpointFallbackUsed,
|
|
924
|
+
fallbackMessage,
|
|
925
|
+
finalStatus: params.finalStatus,
|
|
926
|
+
task,
|
|
927
|
+
inference: inferenceState,
|
|
928
|
+
heartbeatPayloads: scheduler.getGeneratedPayloads(),
|
|
929
|
+
heartbeatPublished,
|
|
930
|
+
heartbeatPublishErrors,
|
|
931
|
+
summary: buildSummary({
|
|
932
|
+
taskId: request.taskId,
|
|
933
|
+
now: params.finishedAt,
|
|
934
|
+
status: params.finalStatus,
|
|
935
|
+
transitionApplied,
|
|
936
|
+
notes: summaryNotes,
|
|
937
|
+
action: params.action,
|
|
938
|
+
outputText: params.outputText,
|
|
939
|
+
}),
|
|
940
|
+
persistence: {
|
|
941
|
+
enabled: this.persistenceEnabled,
|
|
942
|
+
filePath: this.persistenceEnabled ? this.persistenceFilePath : undefined,
|
|
943
|
+
recoveredFrom: item.recovery?.source,
|
|
944
|
+
},
|
|
945
|
+
idempotency: {
|
|
946
|
+
decision: "enqueued",
|
|
947
|
+
idempotencyKey: item.idempotencyKey,
|
|
948
|
+
triggerContextKey: item.triggerContextKey,
|
|
949
|
+
reason: "任务已通过幂等校验并成功入队。",
|
|
950
|
+
},
|
|
951
|
+
recovery: request.recovery,
|
|
952
|
+
});
|
|
953
|
+
try {
|
|
954
|
+
const recoveryNote = item.recovery?.source === "running"
|
|
955
|
+
? item.recovery.note
|
|
956
|
+
: undefined;
|
|
957
|
+
await scheduler.emitNow({
|
|
958
|
+
action: recoveryNote
|
|
959
|
+
? `${recoveryNote} 已开始执行。`
|
|
960
|
+
: "任务已从队列出队,开始执行",
|
|
961
|
+
});
|
|
962
|
+
if (recoveryNote) {
|
|
963
|
+
summaryNotes.push(recoveryNote);
|
|
964
|
+
}
|
|
965
|
+
if (!transition.allowed) {
|
|
966
|
+
summaryNotes.push(`状态校验未通过:${transition.reason}`);
|
|
967
|
+
summaryNotes.push(`建议动作:${transition.nextAction}`);
|
|
968
|
+
await scheduler.emitNow({
|
|
969
|
+
status: transition.fromStatus,
|
|
970
|
+
action: "状态校验未通过,终止执行",
|
|
971
|
+
});
|
|
972
|
+
const finishedAt = nowFn();
|
|
973
|
+
return buildResult({
|
|
974
|
+
finishedAt,
|
|
975
|
+
finalStatus: transition.fromStatus,
|
|
976
|
+
action: "状态校验失败,未执行推理",
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
const currentStatus = String(task.status || "").trim().toLowerCase();
|
|
980
|
+
const alreadyDoing = currentStatus === "doing";
|
|
981
|
+
if (alreadyDoing) {
|
|
982
|
+
transitionApplied = "none";
|
|
983
|
+
summaryNotes.push("任务当前已是 doing,跳过 /run 调用,直接进入执行阶段。");
|
|
984
|
+
}
|
|
985
|
+
else {
|
|
986
|
+
const runBody = {
|
|
987
|
+
trigger_agent_id: normalizeAgentId(request.triggerAgentId),
|
|
988
|
+
note: request.note ?? "triggered by wtt_plugin internal executor",
|
|
989
|
+
};
|
|
990
|
+
if (runnerAgentId)
|
|
991
|
+
runBody.runner_agent_id = runnerAgentId;
|
|
992
|
+
if (recoveryNote) {
|
|
993
|
+
runBody.note = `${runBody.note} | [recovery] ${recoveryNote}`;
|
|
994
|
+
}
|
|
995
|
+
try {
|
|
996
|
+
await request.apiRequest({
|
|
997
|
+
method: "POST",
|
|
998
|
+
path: `/tasks/${encodeURIComponent(request.taskId)}/run`,
|
|
999
|
+
body: runBody,
|
|
1000
|
+
});
|
|
1001
|
+
transitionApplied = "run_endpoint";
|
|
1002
|
+
summaryNotes.push("已调用 POST /tasks/{task_id}/run 推进状态到 doing。");
|
|
1003
|
+
}
|
|
1004
|
+
catch (error) {
|
|
1005
|
+
if (!isEndpointUnavailableError(error)) {
|
|
1006
|
+
throw error;
|
|
1007
|
+
}
|
|
1008
|
+
endpointFallbackUsed = true;
|
|
1009
|
+
const runUnavailable = toErrorMessage(error);
|
|
1010
|
+
try {
|
|
1011
|
+
const patchBody = {
|
|
1012
|
+
status: "doing",
|
|
1013
|
+
notes: "internal executor fallback: /run endpoint unavailable",
|
|
1014
|
+
};
|
|
1015
|
+
if (runnerAgentId)
|
|
1016
|
+
patchBody.runner_agent_id = runnerAgentId;
|
|
1017
|
+
await request.apiRequest({
|
|
1018
|
+
method: "PATCH",
|
|
1019
|
+
path: `/tasks/${encodeURIComponent(request.taskId)}`,
|
|
1020
|
+
body: patchBody,
|
|
1021
|
+
});
|
|
1022
|
+
transitionApplied = "patch_status";
|
|
1023
|
+
fallbackMessage = "run API 不可用,已降级为 PATCH /tasks/{task_id} 推进到 doing。";
|
|
1024
|
+
summaryNotes.push(`run API 不可用:${runUnavailable}`);
|
|
1025
|
+
summaryNotes.push("已通过 PATCH /tasks/{task_id} 进行状态推进(fallback)。");
|
|
1026
|
+
}
|
|
1027
|
+
catch (patchError) {
|
|
1028
|
+
if (!isEndpointUnavailableError(patchError)) {
|
|
1029
|
+
throw patchError;
|
|
1030
|
+
}
|
|
1031
|
+
transitionApplied = "none";
|
|
1032
|
+
const patchUnavailable = toErrorMessage(patchError);
|
|
1033
|
+
fallbackMessage = "run 与 patch 状态接口均不可用,无法推进到 doing。";
|
|
1034
|
+
summaryNotes.push(`run API 不可用:${runUnavailable}`);
|
|
1035
|
+
summaryNotes.push(`patch API 不可用:${patchUnavailable}`);
|
|
1036
|
+
summaryNotes.push("请确认后端暴露 /tasks/{id}/run 或 PATCH /tasks/{id}。");
|
|
1037
|
+
throw createErrorWithCode(fallbackMessage, "ENDPOINT_UNAVAILABLE");
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
await scheduler.emitNow({
|
|
1042
|
+
status: "doing",
|
|
1043
|
+
action: alreadyDoing
|
|
1044
|
+
? "任务已处于 doing,直接进入执行阶段"
|
|
1045
|
+
: transitionApplied === "run_endpoint"
|
|
1046
|
+
? "任务状态已推进到 doing,准备拉取任务详情"
|
|
1047
|
+
: "任务状态已通过 fallback 推进到 doing,准备拉取任务详情",
|
|
1048
|
+
});
|
|
1049
|
+
let detailPayload;
|
|
1050
|
+
try {
|
|
1051
|
+
detailPayload = request.fetchTaskDetail
|
|
1052
|
+
? await request.fetchTaskDetail()
|
|
1053
|
+
: await request.apiRequest({
|
|
1054
|
+
method: "GET",
|
|
1055
|
+
path: `/tasks/${encodeURIComponent(request.taskId)}`,
|
|
1056
|
+
});
|
|
1057
|
+
task = normalizeTaskDetail(request.taskId, request.metadata, detailPayload);
|
|
1058
|
+
taskUsageTotals = readTaskUsageTotals(detailPayload);
|
|
1059
|
+
}
|
|
1060
|
+
catch (error) {
|
|
1061
|
+
summaryNotes.push(`拉取任务详情失败:${toErrorMessage(error)},改用入队元数据继续执行。`);
|
|
1062
|
+
}
|
|
1063
|
+
const prompt = buildTaskPrompt(task);
|
|
1064
|
+
inferenceState.prompt = prompt;
|
|
1065
|
+
inferenceState.attempted = true;
|
|
1066
|
+
await scheduler.emitNow({
|
|
1067
|
+
status: "doing",
|
|
1068
|
+
action: "调用 Agent 推理执行任务",
|
|
1069
|
+
});
|
|
1070
|
+
if (!request.invokeTaskInference) {
|
|
1071
|
+
throw createErrorWithCode("gateway runtime 未提供推理调度钩子(invokeTaskInference)", "RUNTIME_HOOK_UNAVAILABLE");
|
|
1072
|
+
}
|
|
1073
|
+
const inferenceResult = await request.invokeTaskInference({
|
|
1074
|
+
taskId: request.taskId,
|
|
1075
|
+
prompt,
|
|
1076
|
+
task,
|
|
1077
|
+
accountId: request.accountId,
|
|
1078
|
+
});
|
|
1079
|
+
let outputText = (inferenceResult.outputText ?? "").trim();
|
|
1080
|
+
inferenceState.provider = inferenceResult.provider?.trim() || "invokeTaskInference";
|
|
1081
|
+
inferenceState.usage = mergeInferenceUsage(inferenceState.usage, inferenceResult.usage);
|
|
1082
|
+
if (!outputText) {
|
|
1083
|
+
const retryPrompt = `${prompt}\n\n补充要求:请至少输出一段最终结论,不能留空。`;
|
|
1084
|
+
const retryResult = await request.invokeTaskInference({
|
|
1085
|
+
taskId: request.taskId,
|
|
1086
|
+
prompt: retryPrompt,
|
|
1087
|
+
task,
|
|
1088
|
+
accountId: request.accountId,
|
|
1089
|
+
});
|
|
1090
|
+
inferenceState.usage = mergeInferenceUsage(inferenceState.usage, retryResult.usage);
|
|
1091
|
+
const retryText = (retryResult.outputText ?? "").trim();
|
|
1092
|
+
if (retryText) {
|
|
1093
|
+
outputText = retryText;
|
|
1094
|
+
const retryProvider = retryResult.provider?.trim() || inferenceState.provider;
|
|
1095
|
+
inferenceState.provider = `${retryProvider}+retry`;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
inferenceState.outputText = outputText;
|
|
1099
|
+
inferenceState.succeeded = true;
|
|
1100
|
+
const reviewNote = outputText || "任务已执行完成。";
|
|
1101
|
+
const usagePatchFields = buildUsagePatchFields(taskUsageTotals, inferenceState.usage);
|
|
1102
|
+
summaryNotes.push(`executor_output_at=${nowFn().toISOString()}`, `provider=${inferenceState.provider}`);
|
|
1103
|
+
if (inferenceState.usage?.totalTokens && inferenceState.usage.totalTokens > 0) {
|
|
1104
|
+
summaryNotes.push(`tokens.total=${inferenceState.usage.totalTokens}`);
|
|
1105
|
+
}
|
|
1106
|
+
const reviewTransitionResult = await this.patchTaskStatusWithRetry({
|
|
1107
|
+
taskId: request.taskId,
|
|
1108
|
+
request,
|
|
1109
|
+
status: "review",
|
|
1110
|
+
notes: reviewNote,
|
|
1111
|
+
runnerAgentId,
|
|
1112
|
+
patchFields: usagePatchFields,
|
|
1113
|
+
retryDelaysMs: this.reviewPatchRetryDelaysMs,
|
|
1114
|
+
notePrefix: "review_status_patch",
|
|
1115
|
+
});
|
|
1116
|
+
summaryNotes.push(...reviewTransitionResult.notes);
|
|
1117
|
+
if (reviewTransitionResult.succeeded) {
|
|
1118
|
+
await scheduler.emitNow({
|
|
1119
|
+
status: "review",
|
|
1120
|
+
action: "推理完成,任务已推进到 review",
|
|
1121
|
+
});
|
|
1122
|
+
summaryNotes.push("任务已推进:todo -> doing -> review。");
|
|
1123
|
+
summaryNotes.push(`推理输出长度:${outputText.length} 字符。`);
|
|
1124
|
+
const finishedAt = nowFn();
|
|
1125
|
+
return buildResult({
|
|
1126
|
+
finishedAt,
|
|
1127
|
+
finalStatus: "review",
|
|
1128
|
+
action: "执行完成,等待 review 审批",
|
|
1129
|
+
outputText,
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
const reviewFailureReason = reviewTransitionResult.lastError ?? "review 状态回写失败";
|
|
1133
|
+
inferenceState.error = `inference succeeded but review patch failed: ${reviewFailureReason}`;
|
|
1134
|
+
summaryNotes.push(`watchdog: 推理成功但 review 回写失败(attempts=${reviewTransitionResult.attempts}):${reviewFailureReason}`);
|
|
1135
|
+
let finalStatus = "doing";
|
|
1136
|
+
const watchdogBlockedPatch = await this.patchTaskStatusWithRetry({
|
|
1137
|
+
taskId: request.taskId,
|
|
1138
|
+
request,
|
|
1139
|
+
status: "blocked",
|
|
1140
|
+
notes: [
|
|
1141
|
+
"internal executor watchdog triggered",
|
|
1142
|
+
"reason: inference succeeded but failed to patch review",
|
|
1143
|
+
`review_attempts=${reviewTransitionResult.attempts}`,
|
|
1144
|
+
`review_error=${reviewFailureReason}`,
|
|
1145
|
+
].join("\n"),
|
|
1146
|
+
runnerAgentId,
|
|
1147
|
+
retryDelaysMs: [0],
|
|
1148
|
+
notePrefix: "watchdog.blocked_patch",
|
|
1149
|
+
});
|
|
1150
|
+
summaryNotes.push(...watchdogBlockedPatch.notes);
|
|
1151
|
+
if (watchdogBlockedPatch.succeeded) {
|
|
1152
|
+
finalStatus = "blocked";
|
|
1153
|
+
summaryNotes.push("watchdog: 已将任务标记为 blocked,等待补偿更新或人工介入。");
|
|
1154
|
+
}
|
|
1155
|
+
else {
|
|
1156
|
+
summaryNotes.push(`watchdog: blocked 状态回写失败:${watchdogBlockedPatch.lastError ?? "unknown"}`);
|
|
1157
|
+
}
|
|
1158
|
+
this.scheduleCompensatingReviewPatch({
|
|
1159
|
+
taskId: request.taskId,
|
|
1160
|
+
request,
|
|
1161
|
+
reviewNotes: reviewNote,
|
|
1162
|
+
runnerAgentId,
|
|
1163
|
+
patchFields: usagePatchFields,
|
|
1164
|
+
failureReason: reviewFailureReason,
|
|
1165
|
+
});
|
|
1166
|
+
summaryNotes.push(`watchdog: 已排队延迟 ${this.compensatingReviewDelayMs}ms 的补偿 review 回写。`);
|
|
1167
|
+
await scheduler.emitNow({
|
|
1168
|
+
status: finalStatus,
|
|
1169
|
+
action: "推理成功但 review 回写失败,已触发 watchdog",
|
|
1170
|
+
});
|
|
1171
|
+
const finishedAt = nowFn();
|
|
1172
|
+
return buildResult({
|
|
1173
|
+
finishedAt,
|
|
1174
|
+
finalStatus,
|
|
1175
|
+
action: "推理成功但 review 回写失败,已触发补偿",
|
|
1176
|
+
outputText,
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
catch (error) {
|
|
1180
|
+
const errMsg = toErrorMessage(error);
|
|
1181
|
+
inferenceState.error = errMsg;
|
|
1182
|
+
if (!inferenceState.succeeded) {
|
|
1183
|
+
inferenceState.outputText = "";
|
|
1184
|
+
}
|
|
1185
|
+
summaryNotes.push(`执行失败:${errMsg}`);
|
|
1186
|
+
let finalStatus = transitionApplied === "none" ? transition.fromStatus : "doing";
|
|
1187
|
+
try {
|
|
1188
|
+
const blockedPatchBody = {
|
|
1189
|
+
status: "blocked",
|
|
1190
|
+
notes: `internal executor failed: ${errMsg}`,
|
|
1191
|
+
};
|
|
1192
|
+
if (runnerAgentId)
|
|
1193
|
+
blockedPatchBody.runner_agent_id = runnerAgentId;
|
|
1194
|
+
await request.apiRequest({
|
|
1195
|
+
method: "PATCH",
|
|
1196
|
+
path: `/tasks/${encodeURIComponent(request.taskId)}`,
|
|
1197
|
+
body: blockedPatchBody,
|
|
1198
|
+
});
|
|
1199
|
+
finalStatus = "blocked";
|
|
1200
|
+
summaryNotes.push("失败后已自动标记为 blocked。请人工介入。");
|
|
1201
|
+
}
|
|
1202
|
+
catch (patchError) {
|
|
1203
|
+
summaryNotes.push(`失败状态回写 blocked 失败:${toErrorMessage(patchError)}`);
|
|
1204
|
+
}
|
|
1205
|
+
await scheduler.emitNow({
|
|
1206
|
+
status: finalStatus,
|
|
1207
|
+
action: "执行失败,任务已停止",
|
|
1208
|
+
});
|
|
1209
|
+
const finishedAt = nowFn();
|
|
1210
|
+
return buildResult({
|
|
1211
|
+
finishedAt,
|
|
1212
|
+
finalStatus,
|
|
1213
|
+
action: "执行失败,已记录错误",
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
finally {
|
|
1217
|
+
scheduler.stop();
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
export function createTaskRunExecutorLoop(options) {
|
|
1222
|
+
return new InMemoryTaskRunExecutor(options);
|
|
1223
|
+
}
|
|
1224
|
+
const sharedTaskRunExecutor = createTaskRunExecutorLoop({
|
|
1225
|
+
persistence: { enabled: true },
|
|
1226
|
+
});
|
|
1227
|
+
export function getSharedTaskRunExecutor() {
|
|
1228
|
+
return sharedTaskRunExecutor;
|
|
1229
|
+
}
|
|
1230
|
+
//# sourceMappingURL=task-executor.js.map
|