@tea-agent/loop-agent 0.32.1 → 0.33.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/dist/executors/model-routing.js +14 -4
  3. package/dist/governance/manifest-types.js +34 -7
  4. package/dist/worker/console/chat/model-resolver.js +114 -34
  5. package/dist/worker/console/chat/workspace-landing.js +58 -22
  6. package/dist/worker/console/doctor.js +1 -0
  7. package/dist/worker/console/night-aux-ticker.js +5 -0
  8. package/dist/worker/console/operator-surface-health.js +1 -0
  9. package/dist/worker/console/pi-readiness.js +26 -17
  10. package/dist/worker/console/server.js +2 -0
  11. package/dist/worker/console/static/assets/index-CnUXAqxG.css +1 -0
  12. package/dist/worker/console/static/assets/index-PzYzcuFG.js +29 -0
  13. package/dist/worker/console/static/index.html +3 -2
  14. package/dist/worker/console/static-src/app/useRecoveryConsole.js +3 -2
  15. package/dist/worker/console/static-src/night/useNightBoard.js +0 -31
  16. package/dist/worker/observability/read-model.js +106 -0
  17. package/dist/worker/observe/routes.js +22 -9
  18. package/dist/worker/observe/static/api.js +42 -3
  19. package/dist/worker/observe/static/app.js +15 -0
  20. package/dist/worker/observe/static/constants.js +10 -0
  21. package/dist/worker/observe/static/custom-select.js +567 -0
  22. package/dist/worker/observe/static/index.html +56 -47
  23. package/dist/worker/observe/static/kpi.js +2 -24
  24. package/dist/worker/observe/static/operator-chrome.css +480 -0
  25. package/dist/worker/observe/static/operator-chrome.d.ts +82 -0
  26. package/dist/worker/observe/static/operator-chrome.js +554 -0
  27. package/dist/worker/observe/static/router.js +54 -1
  28. package/dist/worker/observe/static/shell-chrome.js +1 -11
  29. package/dist/worker/observe/static/state.js +20 -0
  30. package/dist/worker/observe/static/styles.css +680 -299
  31. package/dist/worker/observe/static/views/dag-inspector.js +20 -17
  32. package/dist/worker/observe/static/views/dag.js +136 -59
  33. package/dist/worker/observe/static/views/dags.js +877 -0
  34. package/dist/worker/observe/static/views/dashboard.js +67 -8
  35. package/dist/workflows/dag/init-hybrid.js +58 -29
  36. package/docs/templates/harness.schema.json +5 -0
  37. package/harness.json +4 -4
  38. package/package.json +1 -1
  39. package/dist/worker/console/static/assets/index-Bpa2qrc-.js +0 -29
  40. package/dist/worker/console/static/assets/index-BqfFDdnG.css +0 -1
@@ -4,9 +4,10 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
+ <link rel="stylesheet" href="/inspect/operator-chrome.css" />
7
8
  <title>Loop 操作台 · Operator Console</title>
8
- <script type="module" crossorigin src="/assets/index-Bpa2qrc-.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-BqfFDdnG.css">
9
+ <script type="module" crossorigin src="/assets/index-PzYzcuFG.js"></script>
10
+ <link rel="stylesheet" crossorigin href="/assets/index-CnUXAqxG.css">
10
11
  </head>
11
12
  <body>
12
13
  <div id="root"></div>
@@ -30,7 +30,8 @@ export function useRecoveryConsole(params) {
30
30
  const [inspectSplitOpen, setInspectSplitOpen] = useState(false);
31
31
  const inspectPath = useCallback(() => {
32
32
  if (dagRunId.trim()) {
33
- return `/inspect/#/dag/${encodeURIComponent(dagRunId.trim())}`;
33
+ // from=recovery 激活统一壳的对象上下文 chip 与显式返回路径
34
+ return `/inspect/#/dag/${encodeURIComponent(dagRunId.trim())}?from=recovery`;
34
35
  }
35
36
  return "/inspect/#/";
36
37
  }, [dagRunId]);
@@ -91,7 +92,7 @@ export function useRecoveryConsole(params) {
91
92
  const resolved = await resolveDagRunForInspect();
92
93
  if (!resolved)
93
94
  return;
94
- window.location.assign(`/inspect/#/dag/${encodeURIComponent(resolved)}`);
95
+ window.location.assign(`/inspect/#/dag/${encodeURIComponent(resolved)}?from=recovery`);
95
96
  }, [resolveDagRunForInspect]);
96
97
  const loadRecoveryRuns = useCallback(async () => {
97
98
  setRecoveryRunsLoading(true);
@@ -1,5 +1,4 @@
1
1
  import { useCallback, useEffect, useState } from "react";
2
- import { confirmationToken, } from "./night-types.js";
3
2
  /** Night board data plane: schedules list + doctor report + task pool +
4
3
  * foreground aux-ticker projection, loaded together on mount/refresh. */
5
4
  export function useNightBoard(params) {
@@ -12,7 +11,6 @@ export function useNightBoard(params) {
12
11
  const [advancedOpen, setAdvancedOpen] = useState(false);
13
12
  const [morningPreview, setMorningPreview] = useState(null);
14
13
  const [auxTicker, setAuxTicker] = useState(null);
15
- const [auxTickerBusy, setAuxTickerBusy] = useState(false);
16
14
  const refreshBoard = useCallback(async () => {
17
15
  setLoadingBoard(true);
18
16
  setLoadError(null);
@@ -59,33 +57,6 @@ export function useNightBoard(params) {
59
57
  useEffect(() => {
60
58
  void refreshBoard();
61
59
  }, [refreshBoard]);
62
- const onToggleAuxTicker = useCallback(async (enabled) => {
63
- setAuxTickerBusy(true);
64
- try {
65
- const res = await fetch("/api/operator/v1/night-aux-ticker", {
66
- method: "POST",
67
- credentials: "same-origin",
68
- headers: {
69
- "Content-Type": "application/json",
70
- "X-Loop-Console-Confirmation": confirmationToken(),
71
- },
72
- body: JSON.stringify({ enabled }),
73
- });
74
- const body = (await res.json());
75
- if (!res.ok || body.ok === false || !body.ticker) {
76
- setOpError(body.error?.message ??
77
- `切换前台辅助 ticker 失败(HTTP ${res.status})`);
78
- return;
79
- }
80
- setAuxTicker(body.ticker);
81
- }
82
- catch (error) {
83
- setOpError(error instanceof Error ? error.message : String(error));
84
- }
85
- finally {
86
- setAuxTickerBusy(false);
87
- }
88
- }, [setOpError]);
89
60
  const onMorningReport = useCallback(async () => {
90
61
  try {
91
62
  const res = await fetch("/api/night-jobs/morning", {
@@ -115,9 +86,7 @@ export function useNightBoard(params) {
115
86
  setAdvancedOpen,
116
87
  morningPreview,
117
88
  auxTicker,
118
- auxTickerBusy,
119
89
  refreshBoard,
120
- onToggleAuxTicker,
121
90
  onMorningReport,
122
91
  doctorIssues,
123
92
  clockHint,
@@ -43,6 +43,11 @@ export function resolveLegacyTask(tasks, taskId) {
43
43
  }
44
44
  export const TASK_RUN_HISTORY_DEFAULT_LIMIT = 20;
45
45
  export const TASK_RUN_HISTORY_MAX_LIMIT = 100;
46
+ /** Default / allowed page sizes for GET /api/dag-runs history list. */
47
+ export const DAG_RUN_HISTORY_DEFAULT_PAGE = 1;
48
+ export const DAG_RUN_HISTORY_DEFAULT_PAGE_SIZE = 20;
49
+ export const DAG_RUN_HISTORY_MAX_PAGE_SIZE = 100;
50
+ export const DAG_RUN_HISTORY_ALLOWED_PAGE_SIZES = [20, 50, 100];
46
51
  const ACTIVE_TASK_STATUSES = new Set(["running", "pending"]);
47
52
  const ACTIVE_BATCH_STATUSES = new Set(["running", "pending"]);
48
53
  export async function buildGlobalSnapshot(options) {
@@ -227,6 +232,107 @@ export function clampTaskRunHistoryLimit(limit) {
227
232
  }
228
233
  return Math.min(Math.floor(limit), TASK_RUN_HISTORY_MAX_LIMIT);
229
234
  }
235
+ export function clampDagRunHistoryPageSize(pageSize) {
236
+ if (pageSize === undefined || !Number.isFinite(pageSize) || pageSize <= 0) {
237
+ return DAG_RUN_HISTORY_DEFAULT_PAGE_SIZE;
238
+ }
239
+ const floored = Math.floor(pageSize);
240
+ if (DAG_RUN_HISTORY_ALLOWED_PAGE_SIZES.includes(floored)) {
241
+ return floored;
242
+ }
243
+ // Nearest allowed size, prefer smaller when equidistant, then clamp to max.
244
+ let best = DAG_RUN_HISTORY_DEFAULT_PAGE_SIZE;
245
+ let bestDist = Number.POSITIVE_INFINITY;
246
+ for (const allowed of DAG_RUN_HISTORY_ALLOWED_PAGE_SIZES) {
247
+ const dist = Math.abs(allowed - floored);
248
+ if (dist < bestDist || (dist === bestDist && allowed < best)) {
249
+ best = allowed;
250
+ bestDist = dist;
251
+ }
252
+ }
253
+ return Math.min(best, DAG_RUN_HISTORY_MAX_PAGE_SIZE);
254
+ }
255
+ export function clampDagRunHistoryPage(page, totalPages) {
256
+ const safeTotal = Math.max(1, Math.floor(totalPages) || 1);
257
+ if (page === undefined || !Number.isFinite(page) || page <= 0) {
258
+ return DAG_RUN_HISTORY_DEFAULT_PAGE;
259
+ }
260
+ return Math.min(Math.floor(page), safeTotal);
261
+ }
262
+ /**
263
+ * Formal DAG history membership (AC-004):
264
+ * exclude executionMode or lifecycle dry-run/init-only;
265
+ * keep missing executionMode when lifecycle is not a non-execution mode.
266
+ */
267
+ export function isFormalDagRunHistoryCandidate(dag) {
268
+ const executionMode = (dag.executionMode ?? "").toLowerCase();
269
+ if (NON_EXECUTION_MODES.has(executionMode))
270
+ return false;
271
+ const lifecycle = (dag.lifecycle ?? "").toLowerCase();
272
+ if (NON_EXECUTION_MODES.has(lifecycle))
273
+ return false;
274
+ return true;
275
+ }
276
+ function dagRunHistorySortKey(dag) {
277
+ return `${dag.startedAt ?? dag.finishedAt ?? ""}\0${dag.dagRunId}`;
278
+ }
279
+ function projectDagRunHistoryProgress(dag) {
280
+ const nodes = dag.nodes ?? [];
281
+ let finished = 0;
282
+ for (const node of nodes) {
283
+ const status = (node.status ?? "").toLowerCase();
284
+ if (status === "finished" ||
285
+ status === "completed" ||
286
+ status === "succeeded" ||
287
+ status === "done" ||
288
+ status === "error" ||
289
+ status === "failed" ||
290
+ status === "skipped" ||
291
+ status === "partial_failed") {
292
+ finished += 1;
293
+ }
294
+ }
295
+ return { finished, total: nodes.length };
296
+ }
297
+ function toDagRunHistoryItem(dag) {
298
+ return {
299
+ dagRunId: dag.dagRunId,
300
+ ...(dag.title ? { title: dag.title } : {}),
301
+ ...(dag.status ? { status: dag.status } : {}),
302
+ ...(dag.effectiveStatus ? { effectiveStatus: dag.effectiveStatus } : {}),
303
+ ...(dag.lifecycle ? { lifecycle: dag.lifecycle } : {}),
304
+ ...(dag.executionMode ? { executionMode: dag.executionMode } : {}),
305
+ ...(dag.startedAt ? { startedAt: dag.startedAt } : {}),
306
+ ...(dag.finishedAt ? { finishedAt: dag.finishedAt } : {}),
307
+ ...(dag.durationMs !== undefined ? { durationMs: dag.durationMs } : {}),
308
+ progress: projectDagRunHistoryProgress(dag),
309
+ };
310
+ }
311
+ /**
312
+ * Paginated lightweight formal DAG run history from an in-memory GlobalSnapshot.
313
+ * Filters dry-run/init-only; sorts newest first; never embeds nodes/edges/previews.
314
+ */
315
+ export function listDagRunHistoryFromSnapshot(snapshot, options = {}) {
316
+ const pageSize = clampDagRunHistoryPageSize(options.pageSize);
317
+ const formal = (snapshot.dagRuns ?? [])
318
+ .filter(isFormalDagRunHistoryCandidate)
319
+ .slice()
320
+ .sort((a, b) => dagRunHistorySortKey(b).localeCompare(dagRunHistorySortKey(a)));
321
+ const total = formal.length;
322
+ const totalPages = Math.max(1, Math.ceil(total / pageSize) || 1);
323
+ const page = clampDagRunHistoryPage(options.page, totalPages);
324
+ const start = (page - 1) * pageSize;
325
+ const runs = formal.slice(start, start + pageSize).map(toDagRunHistoryItem);
326
+ return {
327
+ schemaVersion: 1,
328
+ generatedAt: snapshot.generatedAt,
329
+ page,
330
+ pageSize,
331
+ total,
332
+ totalPages,
333
+ runs,
334
+ };
335
+ }
230
336
  function emptySnapshot(repoRoot, now) {
231
337
  let taskPoolPresent = false;
232
338
  try {
@@ -7,7 +7,7 @@ import { redactSecrets, truncateUtf8Preview } from "../../shared/preview.js";
7
7
  import { parseWorkerEventLine } from "../observability/events.js";
8
8
  import { isSafeObservabilityIdentifier } from "../observability/event-store.js";
9
9
  import { clampEventHistoryLimit, listBatchEventHistory, listPoolEventHistory, } from "../observability/event-history.js";
10
- import { buildGlobalSnapshot, clampTaskRunHistoryLimit, listTaskRunHistory, resolveLegacyTask, } from "../observability/read-model.js";
10
+ import { buildGlobalSnapshot, clampDagRunHistoryPageSize, clampTaskRunHistoryLimit, listDagRunHistoryFromSnapshot, listTaskRunHistory, resolveLegacyTask, } from "../observability/read-model.js";
11
11
  import { loadDagRunExecutionTrajectory } from "../observability/dag-execution-trajectory.js";
12
12
  import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
13
13
  import { dagSourceBindingSchema } from "../../workflows/dag/types.js";
@@ -115,6 +115,13 @@ export const ROUTES = [
115
115
  pattern: /^\/api\/dag-runs\/([^/]+)$/,
116
116
  handler: handleDagRunById,
117
117
  },
118
+ // List route must be registered after /api/dag-runs/:id so detail keeps priority;
119
+ // matchObserveGetRoute scans in order and this path has no capture group.
120
+ {
121
+ method: "GET",
122
+ pattern: /^\/api\/dag-runs$/,
123
+ handler: handleDagRuns,
124
+ },
118
125
  { method: "GET", pattern: /^\/api\/artifacts$/, handler: handleArtifactRead },
119
126
  {
120
127
  method: "GET",
@@ -194,9 +201,9 @@ export async function serveObserveStatic(req, res, staticDir, options = {}) {
194
201
  const url = new URL(req.url ?? "/", "http://localhost");
195
202
  let pathname = decodeURIComponent(url.pathname);
196
203
  const urlPrefix = options.urlPrefix?.replace(/\/$/, "");
197
- const embeddedShell = Boolean(urlPrefix) &&
204
+ const withinPrefix = Boolean(urlPrefix) &&
198
205
  (pathname === urlPrefix || pathname.startsWith(`${urlPrefix}/`));
199
- if (urlPrefix && embeddedShell) {
206
+ if (urlPrefix && withinPrefix) {
200
207
  pathname = pathname.slice(urlPrefix.length) || "/";
201
208
  }
202
209
  if (pathname === "/") {
@@ -214,13 +221,8 @@ export async function serveObserveStatic(req, res, staticDir, options = {}) {
214
221
  sendJson(res, 404, { error: "Not found" });
215
222
  return;
216
223
  }
217
- let content = await readFile(resolvedFile);
224
+ const content = await readFile(resolvedFile);
218
225
  const contentType = contentTypeFor(pathname);
219
- if (embeddedShell && contentType.startsWith("text/html")) {
220
- content = Buffer.from(content
221
- .toString("utf8")
222
- .replace(/<body\b/i, '<body data-embedded-shell="true"'));
223
- }
224
226
  res.writeHead(200, {
225
227
  "Content-Type": contentType,
226
228
  "Content-Length": content.byteLength,
@@ -507,6 +509,17 @@ async function handleRunArtifacts(_req, res, match, ctx) {
507
509
  }
508
510
  sendJson(res, 200, { artifacts });
509
511
  }
512
+ /**
513
+ * GET /api/dag-runs?page=&pageSize=
514
+ * Lightweight formal DAG history page (schemaVersion 1).
515
+ */
516
+ async function handleDagRuns(_req, res, match, ctx) {
517
+ const snapshot = await getSnapshot(ctx);
518
+ const page = parsePositiveInt(match.query.get("page"), 1);
519
+ const pageSize = clampDagRunHistoryPageSize(parsePositiveInt(match.query.get("pageSize"), 0) || undefined);
520
+ const body = listDagRunHistoryFromSnapshot(snapshot, { page, pageSize });
521
+ sendJson(res, 200, body);
522
+ }
510
523
  export async function handleDagRunById(_req, res, match, ctx) {
511
524
  const snapshot = await getSnapshot(ctx);
512
525
  const dagRun = snapshot.dagRuns.find((d) => d.dagRunId === match.params.id);
@@ -13,10 +13,12 @@ export async function fetchJson(url) {
13
13
  /**
14
14
  * Read-only JSON fetch that preserves HTTP status so callers can distinguish
15
15
  * 404 (missing) from 409 (ambiguous task identity). Body is parsed best-effort.
16
+ * Optional `signal` enables AbortController cancellation for formal navigation.
16
17
  */
17
- export async function fetchJsonResult(url) {
18
+ export async function fetchJsonResult(url, options = {}) {
19
+ const signal = options?.signal;
18
20
  try {
19
- const res = await fetch(url);
21
+ const res = await fetch(url, signal ? { signal } : undefined);
20
22
  let body = null;
21
23
  try {
22
24
  body = await res.json();
@@ -24,11 +26,48 @@ export async function fetchJsonResult(url) {
24
26
  body = null;
25
27
  }
26
28
  return { ok: res.ok, status: res.status, body };
27
- } catch {
29
+ } catch (err) {
30
+ if (
31
+ (err && (err.name === "AbortError" || err.code === 20)) ||
32
+ (signal && signal.aborted)
33
+ ) {
34
+ return { ok: false, status: 0, body: null, aborted: true };
35
+ }
28
36
  return { ok: false, status: 0, body: null };
29
37
  }
30
38
  }
31
39
 
40
+ /**
41
+ * Formal DAG history page (real request path). page/pageSize are query params.
42
+ * Callers should discard late responses via request generation tokens and may
43
+ * pass AbortSignal to cancel superseded formal navigation requests.
44
+ */
45
+ export async function fetchDagRunHistory(page, pageSize, options = {}) {
46
+ const params = new URLSearchParams();
47
+ if (page != null) params.set("page", String(page));
48
+ if (pageSize != null) params.set("pageSize", String(pageSize));
49
+ const qs = params.toString();
50
+ const url = qs ? `/api/dag-runs?${qs}` : "/api/dag-runs";
51
+ return fetchJsonResult(url, options);
52
+ }
53
+
54
+ /**
55
+ * Safe user-facing error summary for DAG history (AC-015).
56
+ * Only local trusted fixed copy — never renders server error/message/detail
57
+ * or absolute local paths (Windows or POSIX).
58
+ */
59
+ export function summarizeFetchError(result) {
60
+ if (!result) return "加载失败。";
61
+ if (result.status === 0) return "网络错误,请检查 Observe 服务是否可用。";
62
+ if (typeof result.status === "number" && result.status >= 500) {
63
+ return "服务暂时不可用,请稍后重试。";
64
+ }
65
+ if (typeof result.status === "number" && result.status >= 400) {
66
+ return "请求失败,请稍后重试。";
67
+ }
68
+ return "加载失败。";
69
+ }
70
+
32
71
  /**
33
72
  * Fetch a dag node input projection. Preserves HTTP status so callers can
34
73
  * distinguish missing runs, unavailable nodes, and transport errors.
@@ -49,6 +49,7 @@ import {
49
49
  import { renderFailures } from "./views/failures.js";
50
50
  import { startPoolView } from "./views/pool.js";
51
51
  import { startDashboardPolling } from "./views/dashboard.js";
52
+ import { startDagsView } from "./views/dags.js";
52
53
  import { renderFeatureDetail } from "./views/feature.js";
53
54
  import { renderBatch } from "./views/batch.js";
54
55
  import { startTaskPolling } from "./views/task.js";
@@ -66,6 +67,7 @@ import {
66
67
  mergeSessionEvents,
67
68
  renderSessionTimeline,
68
69
  } from "./views/session-timeline.js";
70
+ import { mountOperatorChrome } from "./operator-chrome.js";
69
71
 
70
72
  // Compatibility re-exports for poll-stable UI helpers (owned by state.js / views):
71
73
  // reuse-dag-detail-shell, reuse-dag-graph-wrap, existingGraphWrap?.parentNode === ranksEl,
@@ -119,6 +121,8 @@ function route() {
119
121
  const r = parseRoute();
120
122
  if (r.view === "dashboard") {
121
123
  startDashboardPolling(r.scrollTo);
124
+ } else if (r.view === "dags") {
125
+ void startDagsView();
122
126
  } else if (r.view === "batch") {
123
127
  void renderBatch(r.batchRunId);
124
128
  } else if (r.view === "run") {
@@ -139,6 +143,16 @@ function route() {
139
143
  }
140
144
 
141
145
  if (typeof window !== "undefined") {
146
+ // 先同步挂载共享壳,再启动路由;首个 snapshot 的新鲜度不会因异步挂载丢失。
147
+ const chromeHost = document.getElementById("operator-chrome");
148
+ if (chromeHost) {
149
+ mountOperatorChrome(chromeHost, {
150
+ mode: "inspect",
151
+ liveLocation: true,
152
+ healthUrl: "/api/health",
153
+ readinessTitle: "Pi 状态(只读展示)。模型与认证管理在操作侧,点击进入。",
154
+ });
155
+ }
142
156
  window.addEventListener("hashchange", route);
143
157
  window.addEventListener("load", route);
144
158
  document.addEventListener("pointerdown", closeDagInspectorOnOutsideClick);
@@ -146,6 +160,7 @@ if (typeof window !== "undefined") {
146
160
  if (!isPageVisible()) return;
147
161
  const r = parseRoute();
148
162
  if (r.view === "dashboard") startDashboardPolling();
163
+ // History page is manual-refresh only (no 5s dashboard polling).
149
164
  if (r.view === "run") startRunPolling(r.workerRunId, false);
150
165
  if (r.view === "dag") startDagPolling(r.dagRunId, false);
151
166
  if (r.view === "pool" && uiState.poolAutoRefresh) startPoolView();
@@ -21,6 +21,8 @@ export const OUTPUT_FOLLOW_LATEST_PX = 48;
21
21
  export const UI_TEXT = {
22
22
  dashboard: "总览",
23
23
  dagRuns: "DAG 运行",
24
+ dagHistory: "DAG 运行历史",
25
+ viewAllDags: "查看全部 →",
24
26
  failures: "异常 Task",
25
27
  nightJobs: "夜间任务",
26
28
  batchDetail: "批次详情",
@@ -31,6 +33,14 @@ export const UI_TEXT = {
31
33
  batches: "Worker 批次",
32
34
  noActiveDags: "当前没有进行中的 DAG,启动后将出现在这里。",
33
35
  noRecentDags: "暂无 DAG 运行记录。",
36
+ noFormalDagHistory: "暂无正式 DAG 运行记录",
37
+ noFormalDagHistoryHint:
38
+ "dry-run / init-only 预检记录不会出现在此列表中。",
39
+ dagHistoryRefreshFailed: "刷新失败,当前展示上一次结果",
40
+ dagHistoryCopied: "已复制",
41
+ dagHistoryCopiedAnnounce: "已复制运行 ID",
42
+ dagHistoryCopyFailed: "复制失败,请手动选择运行 ID",
43
+ dagHistoryCopyTooltip: "复制运行 ID",
34
44
  noBatches: "暂无 Worker 批次,运行后将出现在这里。",
35
45
  noNodes: "暂无节点。",
36
46
  noOutput: "暂无节点输出。",