@fieldwangai/agentflow 0.1.113 → 0.1.115
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/bin/lib/admin-run-detail.mjs +263 -0
- package/bin/lib/ui-server.mjs +351 -37
- package/builtin/web-ui/dist/assets/index-CuRtjhm3.css +1 -0
- package/builtin/web-ui/dist/assets/{index-B86D6SK3.js → index-sFdIyhv_.js} +45 -44
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/builtin/web-ui/dist/assets/index-Brqy4uhJ.css +0 -1
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
import { listAllRunDirs } from "./workspace.mjs";
|
|
5
|
+
import {
|
|
6
|
+
listWorkspaceRunLogs,
|
|
7
|
+
readWorkspaceRunLogEvents,
|
|
8
|
+
} from "./workspace-run-logs.mjs";
|
|
9
|
+
|
|
10
|
+
const MAX_LOG_BYTES = 512 * 1024;
|
|
11
|
+
const MAX_EVENTS = 2_000;
|
|
12
|
+
const MAX_EVENT_TEXT_CHARS = 20_000;
|
|
13
|
+
const SECRET_KEY_RE = /(token|password|passwd|secret|webhook|authorization|api[_-]?key|access[_-]?key)/i;
|
|
14
|
+
const SECRET_TEXT_PATTERNS = [
|
|
15
|
+
[/\b(Bearer)\s+[A-Za-z0-9._~+/=-]+/gi, "$1 ***"],
|
|
16
|
+
[/((?:token|password|passwd|secret|authorization|api[_-]?key|access[_-]?key)\s*[=:]\s*)("[^"]*"|'[^']*'|[^\s,;]+)/gi, "$1***"],
|
|
17
|
+
[/(["'](?:token|password|passwd|secret|authorization|api[_-]?key|access[_-]?key)["']\s*:\s*)("[^"]*"|'[^']*')/gi, "$1\"***\""],
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function truncateText(value, max = MAX_EVENT_TEXT_CHARS) {
|
|
21
|
+
const text = String(value ?? "");
|
|
22
|
+
if (text.length <= max) return text;
|
|
23
|
+
return `${text.slice(0, max)}\n... [truncated ${text.length - max} chars]`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function redactAdminRunText(value) {
|
|
27
|
+
let text = truncateText(value);
|
|
28
|
+
for (const [pattern, replacement] of SECRET_TEXT_PATTERNS) {
|
|
29
|
+
text = text.replace(pattern, replacement);
|
|
30
|
+
}
|
|
31
|
+
return text;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function sanitizeValue(value, depth = 0) {
|
|
35
|
+
if (depth > 8) return "[MaxDepth]";
|
|
36
|
+
if (typeof value === "string") return redactAdminRunText(value);
|
|
37
|
+
if (value == null || typeof value === "number" || typeof value === "boolean") return value;
|
|
38
|
+
if (Array.isArray(value)) return value.slice(0, 200).map((item) => sanitizeValue(item, depth + 1));
|
|
39
|
+
if (typeof value === "object") {
|
|
40
|
+
const out = {};
|
|
41
|
+
for (const [key, raw] of Object.entries(value).slice(0, 200)) {
|
|
42
|
+
if (key === "graph") out[key] = "[omitted]";
|
|
43
|
+
else out[key] = SECRET_KEY_RE.test(key) ? "***" : sanitizeValue(raw, depth + 1);
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
return redactAdminRunText(value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function safeJson(value) {
|
|
51
|
+
try {
|
|
52
|
+
return truncateText(JSON.stringify(sanitizeValue(value)));
|
|
53
|
+
} catch {
|
|
54
|
+
return redactAdminRunText(value);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function numberTime(value, fallback = 0) {
|
|
59
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
60
|
+
const parsed = Date.parse(String(value || ""));
|
|
61
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function structuredText(value) {
|
|
65
|
+
if (!value || typeof value !== "object") return "";
|
|
66
|
+
const direct = value.text ?? value.line ?? value.error ?? value.message ?? value.delta ?? value.thinking;
|
|
67
|
+
if (typeof direct === "string" || typeof direct === "number") return redactAdminRunText(direct);
|
|
68
|
+
if (typeof value.content === "string") return redactAdminRunText(value.content);
|
|
69
|
+
if (Array.isArray(value.content)) {
|
|
70
|
+
const texts = value.content
|
|
71
|
+
.map((block) => block?.thinking ?? block?.text ?? block?.content ?? "")
|
|
72
|
+
.filter((text) => typeof text === "string" && text.trim());
|
|
73
|
+
if (texts.length > 0) return redactAdminRunText(texts.join("\n"));
|
|
74
|
+
}
|
|
75
|
+
return "";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function eventKind(value, tag = "") {
|
|
79
|
+
const candidates = [
|
|
80
|
+
tag,
|
|
81
|
+
value?.kind,
|
|
82
|
+
value?.type,
|
|
83
|
+
value?.subtype,
|
|
84
|
+
value?.event,
|
|
85
|
+
value?.item?.type,
|
|
86
|
+
].map((item) => String(item || "").toLowerCase());
|
|
87
|
+
if (candidates.some((item) => item.includes("thinking") || item.includes("reasoning"))) return "thinking";
|
|
88
|
+
if (candidates.some((item) => item.includes("tool"))) return "tool";
|
|
89
|
+
if (candidates.some((item) => item.includes("error") || item.includes("fail"))) return "error";
|
|
90
|
+
if (candidates.some((item) => item.includes("result") || item.includes("assistant"))) return "result";
|
|
91
|
+
return "process";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizeEvent(value, index = 0, raw = "") {
|
|
95
|
+
const event = value && typeof value === "object" ? sanitizeValue(value) : {};
|
|
96
|
+
const type = String(event.type || event.event || event.kind || "event");
|
|
97
|
+
const kind = eventKind(event);
|
|
98
|
+
const nodeId = String(event.nodeId || event.runNodeId || event.instanceId || "");
|
|
99
|
+
const text = structuredText(event) || safeJson(event);
|
|
100
|
+
return {
|
|
101
|
+
id: `${numberTime(event.ts || event.at, index)}-${index}`,
|
|
102
|
+
ts: numberTime(event.ts || event.at, 0),
|
|
103
|
+
type,
|
|
104
|
+
kind,
|
|
105
|
+
nodeId,
|
|
106
|
+
text,
|
|
107
|
+
raw: redactAdminRunText(raw || safeJson(event)),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function parsePipelineLine(line, index) {
|
|
112
|
+
const match = String(line || "").match(/^\[([^\]]+)\]\s+\[([^\]]+)\]\s+([\s\S]*)$/);
|
|
113
|
+
if (!match) {
|
|
114
|
+
return {
|
|
115
|
+
id: `0-${index}`,
|
|
116
|
+
ts: 0,
|
|
117
|
+
type: "log",
|
|
118
|
+
kind: eventKind({}, "log"),
|
|
119
|
+
nodeId: "",
|
|
120
|
+
text: redactAdminRunText(line),
|
|
121
|
+
raw: redactAdminRunText(line),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const [, timestamp, tag, body] = match;
|
|
125
|
+
let parsed = null;
|
|
126
|
+
try {
|
|
127
|
+
parsed = JSON.parse(body);
|
|
128
|
+
} catch {
|
|
129
|
+
parsed = null;
|
|
130
|
+
}
|
|
131
|
+
const sanitized = parsed && typeof parsed === "object" ? sanitizeValue(parsed) : null;
|
|
132
|
+
const type = String(sanitized?.event || sanitized?.type || sanitized?.kind || tag || "log");
|
|
133
|
+
const kind = eventKind(sanitized || {}, tag);
|
|
134
|
+
const nodeId = String(sanitized?.nodeId || sanitized?.runNodeId || sanitized?.instanceId || "");
|
|
135
|
+
const text = structuredText(sanitized) || redactAdminRunText(body);
|
|
136
|
+
return {
|
|
137
|
+
id: `${numberTime(timestamp, index)}-${index}`,
|
|
138
|
+
ts: numberTime(timestamp, 0),
|
|
139
|
+
type,
|
|
140
|
+
kind,
|
|
141
|
+
nodeId,
|
|
142
|
+
text,
|
|
143
|
+
raw: redactAdminRunText(line),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function compactThinkingEvents(events) {
|
|
148
|
+
const out = [];
|
|
149
|
+
for (const event of events) {
|
|
150
|
+
const previous = out[out.length - 1];
|
|
151
|
+
if (
|
|
152
|
+
event.kind === "thinking" &&
|
|
153
|
+
previous?.kind === "thinking" &&
|
|
154
|
+
previous.nodeId === event.nodeId &&
|
|
155
|
+
previous.type === event.type
|
|
156
|
+
) {
|
|
157
|
+
previous.text = truncateText(`${previous.text}${event.text}`);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
out.push({ ...event });
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function parseAdminPipelineRunLog(text) {
|
|
166
|
+
const lines = String(text || "").split(/\r?\n/).filter((line) => line.trim());
|
|
167
|
+
const sliced = lines.slice(-MAX_EVENTS);
|
|
168
|
+
return {
|
|
169
|
+
events: compactThinkingEvents(sliced.map((line, index) => parsePipelineLine(line, index))),
|
|
170
|
+
rawLines: sliced.map((line) => redactAdminRunText(line)),
|
|
171
|
+
truncated: lines.length > sliced.length,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function readLogTail(filePath) {
|
|
176
|
+
if (!fs.existsSync(filePath)) return { text: "", bytes: 0, truncated: false };
|
|
177
|
+
const stat = fs.statSync(filePath);
|
|
178
|
+
const bytes = stat.size;
|
|
179
|
+
const start = Math.max(0, bytes - MAX_LOG_BYTES);
|
|
180
|
+
const fd = fs.openSync(filePath, "r");
|
|
181
|
+
try {
|
|
182
|
+
const buffer = Buffer.alloc(bytes - start);
|
|
183
|
+
fs.readSync(fd, buffer, 0, buffer.length, start);
|
|
184
|
+
let text = buffer.toString("utf-8");
|
|
185
|
+
if (start > 0) {
|
|
186
|
+
const newline = text.indexOf("\n");
|
|
187
|
+
if (newline >= 0) text = text.slice(newline + 1);
|
|
188
|
+
}
|
|
189
|
+
return { text, bytes, truncated: start > 0 };
|
|
190
|
+
} finally {
|
|
191
|
+
fs.closeSync(fd);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function sourceMatches(flowSource, source) {
|
|
196
|
+
if (flowSource === "user") return source === "user" || source === "legacyUserRoot";
|
|
197
|
+
if (flowSource === "workspace") return source === "workspace" || source === "legacyWorkspaceRoot";
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function findPipelineRun(workspaceRoot, input) {
|
|
202
|
+
return listAllRunDirs(workspaceRoot, {
|
|
203
|
+
userId: input.userId,
|
|
204
|
+
includeWorkspaceRuns: true,
|
|
205
|
+
includeLegacyUserRuns: true,
|
|
206
|
+
}).find((entry) => (
|
|
207
|
+
entry.flowName === input.flowId &&
|
|
208
|
+
entry.uuid === input.runId &&
|
|
209
|
+
sourceMatches(input.flowSource, entry.source)
|
|
210
|
+
)) || null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function workspaceRunDetail(input) {
|
|
214
|
+
const run = listWorkspaceRunLogs({
|
|
215
|
+
userId: input.userId,
|
|
216
|
+
flowId: input.flowId,
|
|
217
|
+
flowSource: input.flowSource,
|
|
218
|
+
limit: 200,
|
|
219
|
+
}).find((item) => String(item.runId || "") === input.runId);
|
|
220
|
+
if (!run) return null;
|
|
221
|
+
const allEvents = readWorkspaceRunLogEvents(input.runId);
|
|
222
|
+
const rawEvents = allEvents.slice(-MAX_EVENTS);
|
|
223
|
+
const events = compactThinkingEvents(rawEvents.map((event, index) => normalizeEvent(event, index)));
|
|
224
|
+
return {
|
|
225
|
+
run: { ...sanitizeValue(run), runType: "workspace" },
|
|
226
|
+
events,
|
|
227
|
+
rawLines: rawEvents.map((event) => safeJson(event)),
|
|
228
|
+
truncated: allEvents.length > rawEvents.length,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function pipelineRunDetail(workspaceRoot, input) {
|
|
233
|
+
const entry = findPipelineRun(workspaceRoot, input);
|
|
234
|
+
if (!entry) return null;
|
|
235
|
+
const log = readLogTail(path.join(entry.runDir, "logs", "log.txt"));
|
|
236
|
+
const parsed = parseAdminPipelineRunLog(log.text);
|
|
237
|
+
return {
|
|
238
|
+
run: {
|
|
239
|
+
userId: input.userId,
|
|
240
|
+
flowId: input.flowId,
|
|
241
|
+
flowSource: input.flowSource,
|
|
242
|
+
runId: input.runId,
|
|
243
|
+
runType: "pipeline",
|
|
244
|
+
},
|
|
245
|
+
events: parsed.events,
|
|
246
|
+
rawLines: parsed.rawLines,
|
|
247
|
+
bytes: log.bytes,
|
|
248
|
+
truncated: log.truncated || parsed.truncated,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function readAdminRunDetail(workspaceRoot, input = {}) {
|
|
253
|
+
const normalized = {
|
|
254
|
+
runType: String(input.runType || "pipeline").trim().toLowerCase(),
|
|
255
|
+
userId: String(input.userId || "").trim(),
|
|
256
|
+
flowId: String(input.flowId || "").trim(),
|
|
257
|
+
flowSource: String(input.flowSource || "user").trim().toLowerCase(),
|
|
258
|
+
runId: String(input.runId || "").trim(),
|
|
259
|
+
};
|
|
260
|
+
if (!normalized.userId || !normalized.flowId || !normalized.runId) return null;
|
|
261
|
+
if (normalized.runType === "workspace") return workspaceRunDetail(normalized);
|
|
262
|
+
return pipelineRunDetail(workspaceRoot, normalized);
|
|
263
|
+
}
|