@saltcorn/copilot 0.8.2 → 0.8.3
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/actions/generate-page.js +6 -2
- package/actions/generate-tables.js +56 -6
- package/actions/generate-workflow.js +54 -3
- package/agent-skills/pagegen.js +155 -57
- package/agent-skills/registry-editor.js +15 -2
- package/app-constructor/common.js +7 -1
- package/app-constructor/errors.js +749 -61
- package/app-constructor/feedback-action.js +62 -60
- package/app-constructor/feedback.js +1294 -67
- package/app-constructor/fixed-prompts.js +829 -0
- package/app-constructor/phases.js +1485 -0
- package/app-constructor/prompt-generator.js +587 -0
- package/app-constructor/requirements.js +113 -54
- package/app-constructor/research.js +350 -0
- package/app-constructor/run_task.js +195 -64
- package/app-constructor/schema.js +163 -324
- package/app-constructor/tasks.js +90 -886
- package/app-constructor/tools.js +13 -1
- package/app-constructor/view.js +307 -54
- package/builder-gen.js +11 -8
- package/builder-schema.js +6 -0
- package/package.json +1 -1
- package/app-constructor/prompts.js +0 -120
|
@@ -1,102 +1,790 @@
|
|
|
1
|
-
const Field = require("@saltcorn/data/models/field");
|
|
2
1
|
const Table = require("@saltcorn/data/models/table");
|
|
3
|
-
const Form = require("@saltcorn/data/models/form");
|
|
4
|
-
const MetaData = require("@saltcorn/data/models/metadata");
|
|
5
2
|
const View = require("@saltcorn/data/models/view");
|
|
6
3
|
const Trigger = require("@saltcorn/data/models/trigger");
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const {
|
|
12
|
-
localeDateTime,
|
|
13
|
-
renderForm,
|
|
14
|
-
mkTable,
|
|
15
|
-
post_delete_btn,
|
|
16
|
-
} = require("@saltcorn/markup");
|
|
4
|
+
const WorkflowStep = require("@saltcorn/data/models/workflow_step");
|
|
5
|
+
const Page = require("@saltcorn/data/models/page");
|
|
6
|
+
const MetaData = require("@saltcorn/data/models/metadata");
|
|
7
|
+
const { mkTable } = require("@saltcorn/markup");
|
|
17
8
|
const {
|
|
18
9
|
div,
|
|
19
10
|
script,
|
|
20
11
|
domReady,
|
|
21
12
|
pre,
|
|
22
|
-
code,
|
|
23
|
-
input,
|
|
24
|
-
h4,
|
|
25
|
-
style,
|
|
26
|
-
h5,
|
|
27
13
|
button,
|
|
28
|
-
text_attr,
|
|
29
14
|
i,
|
|
30
15
|
p,
|
|
16
|
+
a,
|
|
31
17
|
span,
|
|
32
18
|
small,
|
|
33
|
-
|
|
34
|
-
textarea,
|
|
19
|
+
text,
|
|
35
20
|
} = require("@saltcorn/markup/tags");
|
|
36
21
|
const { getState } = require("@saltcorn/data/db/state");
|
|
37
|
-
const
|
|
22
|
+
const db = require("@saltcorn/data/db");
|
|
38
23
|
const { viewname } = require("./common");
|
|
24
|
+
const { task_tool } = require("./tools");
|
|
25
|
+
const { PromptGenerator } = require("./prompt-generator");
|
|
39
26
|
|
|
40
|
-
const
|
|
41
|
-
const
|
|
27
|
+
const doCreateErrorFixTask = async (errorMd, userId) => {
|
|
28
|
+
const currentMd = await MetaData.findOne({
|
|
29
|
+
id: errorMd.id,
|
|
42
30
|
type: "CopilotConstructMgr",
|
|
43
31
|
name: "error",
|
|
44
32
|
});
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
33
|
+
if (!currentMd || currentMd.body.fixing) return;
|
|
34
|
+
await currentMd.update({ body: { ...currentMd.body, fixing: true } });
|
|
35
|
+
try {
|
|
36
|
+
const tables = await Table.find({});
|
|
37
|
+
const views = await View.find({});
|
|
38
|
+
const triggers = await Trigger.find({});
|
|
39
|
+
const pages = await Page.find({});
|
|
40
|
+
const errorText = JSON.stringify(errorMd.body.error, null, 2);
|
|
41
|
+
|
|
42
|
+
// Include the affected entity's config so the planning LLM can name exact broken values.
|
|
43
|
+
let entityConfigSection = "";
|
|
44
|
+
const errorUrl = errorMd.body.error?.url || "";
|
|
45
|
+
const mView = errorUrl.match(/\/view\/([^/?#]+)/);
|
|
46
|
+
const mPage = errorUrl.match(/\/page\/([^/?#]+)/);
|
|
47
|
+
if (mView) {
|
|
48
|
+
const viewName = decodeURIComponent(mView[1]);
|
|
49
|
+
const view = views.find((v) => v.name === viewName);
|
|
50
|
+
if (view)
|
|
51
|
+
entityConfigSection =
|
|
52
|
+
`\nThe error occurred while rendering view "${viewName}". ` +
|
|
53
|
+
`Current configuration:\n\`\`\`json\n` +
|
|
54
|
+
`${JSON.stringify(view.configuration, null, 2)}\n\`\`\`\n`;
|
|
55
|
+
} else if (mPage) {
|
|
56
|
+
const pageName = decodeURIComponent(mPage[1]);
|
|
57
|
+
const page = pages.find((p) => p.name === pageName);
|
|
58
|
+
if (page)
|
|
59
|
+
entityConfigSection =
|
|
60
|
+
`\nThe error occurred while rendering page "${pageName}". ` +
|
|
61
|
+
`Current configuration:\n\`\`\`json\n` +
|
|
62
|
+
`${JSON.stringify(page.layout, null, 2)}\n\`\`\`\n`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Workflow step errors: inject the trigger config and all its steps
|
|
66
|
+
const stack = errorMd.body.error?.stack || "";
|
|
67
|
+
if (!entityConfigSection && stack.includes("workflow_step")) {
|
|
68
|
+
// Try to identify the trigger from a WorkflowRun referenced in the error context
|
|
69
|
+
const runIdMatch =
|
|
70
|
+
stack.match(/workflow_run[^0-9]*(\d+)/i) ||
|
|
71
|
+
JSON.stringify(errorMd.body.error).match(/"run_id"\s*:\s*(\d+)/);
|
|
72
|
+
let matchedTrigger = null;
|
|
73
|
+
if (runIdMatch) {
|
|
74
|
+
const WorkflowRun = require("@saltcorn/data/models/workflow_run");
|
|
75
|
+
const run = await WorkflowRun.findOne({
|
|
76
|
+
id: parseInt(runIdMatch[1]),
|
|
77
|
+
}).catch(() => null);
|
|
78
|
+
if (run?.trigger_id) {
|
|
79
|
+
matchedTrigger = triggers.find((t) => t.id === run.trigger_id);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Fall back: include all workflow triggers with steps
|
|
83
|
+
const workflowTriggers = matchedTrigger
|
|
84
|
+
? [matchedTrigger]
|
|
85
|
+
: triggers.filter(
|
|
86
|
+
(t) => t.action === "Workflow" || t.action === "workflow"
|
|
87
|
+
);
|
|
88
|
+
if (workflowTriggers.length) {
|
|
89
|
+
const sections = await Promise.all(
|
|
90
|
+
workflowTriggers.map(async (t) => {
|
|
91
|
+
const steps = await WorkflowStep.find({ trigger_id: t.id });
|
|
92
|
+
return (
|
|
93
|
+
`Trigger "${t.name}" (table: ${t.table_id}, action: ${t.action}):\n` +
|
|
94
|
+
`Configuration: ${JSON.stringify(t.configuration, null, 2)}\n` +
|
|
95
|
+
`Steps (${steps.length}):\n` +
|
|
96
|
+
steps
|
|
97
|
+
.map(
|
|
98
|
+
(s) =>
|
|
99
|
+
` - name: ${s.name}, action: ${s.action_name}, initial: ${s.initial_step}\n` +
|
|
100
|
+
` configuration: ${JSON.stringify(
|
|
101
|
+
s.configuration,
|
|
102
|
+
null,
|
|
103
|
+
2
|
|
104
|
+
)}`
|
|
105
|
+
)
|
|
106
|
+
.join("\n")
|
|
107
|
+
);
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
entityConfigSection =
|
|
111
|
+
`\nThe error occurred in a workflow step. Relevant trigger(s) and steps:\n\n` +
|
|
112
|
+
sections.join("\n\n") +
|
|
113
|
+
"\n";
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// SQL column-not-found: likely a modify_row trigger with a table-qualified key
|
|
118
|
+
if (!entityConfigSection) {
|
|
119
|
+
const colErr = (errorMd.body.error?.message || "").match(
|
|
120
|
+
/column "([^"]+)" of relation "([^"]+)" does not exist/
|
|
121
|
+
);
|
|
122
|
+
if (colErr) {
|
|
123
|
+
const tableName = colErr[2];
|
|
124
|
+
const matchedTable = tables.find((t) => t.name === tableName);
|
|
125
|
+
const modifyTriggers = triggers.filter(
|
|
126
|
+
(t) => t.table_id === matchedTable?.id && t.action === "modify_row"
|
|
127
|
+
);
|
|
128
|
+
if (modifyTriggers.length) {
|
|
129
|
+
entityConfigSection =
|
|
130
|
+
`\nThe error is a SQL column-not-found on table "${tableName}". ` +
|
|
131
|
+
`This is typically caused by a modify_row trigger whose row_expr returns a table-qualified key ` +
|
|
132
|
+
`(e.g. {"${tableName}.some_field": value}) — the dot is stripped by SQL sanitization, ` +
|
|
133
|
+
`producing an invalid column name. Relevant modify_row triggers:\n\n` +
|
|
134
|
+
modifyTriggers
|
|
135
|
+
.map(
|
|
136
|
+
(t) =>
|
|
137
|
+
`Trigger "${t.name}" (when: ${t.when_trigger}):\n` +
|
|
138
|
+
`Configuration: ${JSON.stringify(t.configuration, null, 2)}`
|
|
139
|
+
)
|
|
140
|
+
.join("\n\n") +
|
|
141
|
+
"\n";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const cannot_fix_tool = {
|
|
147
|
+
type: "function",
|
|
148
|
+
function: {
|
|
149
|
+
name: "cannot_fix",
|
|
150
|
+
description:
|
|
151
|
+
"Use this when the error cannot be diagnosed or fixed from the available " +
|
|
152
|
+
"information — e.g. the error is too vague, the stack trace points to platform " +
|
|
153
|
+
"internals with no clear application-level fix, or no relevant entity configuration " +
|
|
154
|
+
"is available. Do NOT invent a task just to produce output.",
|
|
155
|
+
parameters: {
|
|
156
|
+
type: "object",
|
|
157
|
+
required: ["reason"],
|
|
158
|
+
properties: {
|
|
159
|
+
reason: {
|
|
160
|
+
type: "string",
|
|
161
|
+
description:
|
|
162
|
+
"One sentence explaining why a fix task cannot be created.",
|
|
163
|
+
},
|
|
54
164
|
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const generator = await PromptGenerator.createInstance();
|
|
170
|
+
if (!generator.spec) return;
|
|
171
|
+
|
|
172
|
+
const answer = await getState().functions.llm_generate.run(
|
|
173
|
+
generator.errorPrompt(errorText, entityConfigSection),
|
|
174
|
+
{
|
|
175
|
+
tools: [task_tool, cannot_fix_tool],
|
|
176
|
+
systemPrompt:
|
|
177
|
+
"You are a Saltcorn developer. Analyse the error and decide: can you produce a\n" +
|
|
178
|
+
"concrete, actionable fix? If yes, call plan_tasks. If not, call cannot_fix.",
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const tc =
|
|
183
|
+
typeof answer.getToolCalls === "function"
|
|
184
|
+
? answer.getToolCalls()?.[0]
|
|
185
|
+
: undefined;
|
|
186
|
+
if (!tc) return;
|
|
187
|
+
|
|
188
|
+
if (tc.tool_name === "cannot_fix") {
|
|
189
|
+
await currentMd.update({
|
|
190
|
+
body: { ...currentMd.body, cannot_fix_reason: tc.input.reason },
|
|
191
|
+
});
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (tc.tool_name !== "plan_tasks" || !Array.isArray(tc.input.tasks)) return;
|
|
196
|
+
|
|
197
|
+
for (const task of tc.input.tasks)
|
|
198
|
+
await MetaData.create({
|
|
199
|
+
type: "CopilotConstructMgr",
|
|
200
|
+
name: "task",
|
|
201
|
+
body: { ...task, source: "error_fix", error_id: currentMd.id },
|
|
202
|
+
user_id: userId,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
await currentMd.update({
|
|
206
|
+
body: { ...currentMd.body, fix_task_created: true },
|
|
207
|
+
});
|
|
208
|
+
} finally {
|
|
209
|
+
try {
|
|
210
|
+
const current = await MetaData.findOne({
|
|
211
|
+
id: currentMd.id,
|
|
212
|
+
type: "CopilotConstructMgr",
|
|
213
|
+
name: "error",
|
|
214
|
+
});
|
|
215
|
+
if (current) {
|
|
216
|
+
const { fixing, ...rest } = current.body;
|
|
217
|
+
await current.update({ body: rest });
|
|
218
|
+
}
|
|
219
|
+
} catch (_) {}
|
|
220
|
+
try {
|
|
221
|
+
getState().emitDynamicUpdate(db.getTenantSchema(), {
|
|
222
|
+
eval_js: [
|
|
223
|
+
"if(typeof copilotRefreshErrs==='function')copilotRefreshErrs();",
|
|
224
|
+
"if(typeof copilotRefreshTasks==='function')copilotRefreshTasks();",
|
|
225
|
+
],
|
|
226
|
+
});
|
|
227
|
+
} catch (_) {}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Renders the self-healing toggle section and error table.
|
|
233
|
+
*/
|
|
234
|
+
const errorList = async (req) => {
|
|
235
|
+
const settings = await MetaData.findOne({
|
|
236
|
+
type: "CopilotConstructMgr",
|
|
237
|
+
name: "settings",
|
|
238
|
+
});
|
|
239
|
+
const healEnabled = !!settings?.body?.error_heal;
|
|
240
|
+
|
|
241
|
+
const healSection = healEnabled
|
|
242
|
+
? div(
|
|
243
|
+
{
|
|
244
|
+
class:
|
|
245
|
+
"alert alert-success d-flex align-items-center gap-3 py-2 mb-3",
|
|
246
|
+
},
|
|
247
|
+
i({ class: "fas fa-heartbeat" }),
|
|
248
|
+
span(
|
|
249
|
+
"Self-healing enabled — new errors will automatically generate fix tasks."
|
|
250
|
+
),
|
|
251
|
+
button(
|
|
55
252
|
{
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
button(
|
|
59
|
-
{
|
|
60
|
-
class: "btn btn-outline-danger btn-sm",
|
|
61
|
-
onclick: `view_post("${viewname}", "del_err", {id:${r.id}})`,
|
|
62
|
-
},
|
|
63
|
-
i({ class: "fas fa-trash-alt" }),
|
|
64
|
-
),
|
|
253
|
+
class: "btn btn-sm btn-outline-secondary ms-auto",
|
|
254
|
+
onclick: "copilotToggleErrorHealing()",
|
|
65
255
|
},
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
)
|
|
69
|
-
|
|
256
|
+
"Disable"
|
|
257
|
+
)
|
|
258
|
+
)
|
|
259
|
+
: div(
|
|
70
260
|
{
|
|
71
|
-
class:
|
|
72
|
-
|
|
261
|
+
class:
|
|
262
|
+
"alert alert-secondary d-flex align-items-center gap-3 py-2 mb-3",
|
|
73
263
|
},
|
|
74
|
-
"
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
264
|
+
i({ class: "fas fa-heartbeat" }),
|
|
265
|
+
div(
|
|
266
|
+
span({ class: "d-block" }, "Self-healing is disabled."),
|
|
267
|
+
small(
|
|
268
|
+
{ class: "text-muted" },
|
|
269
|
+
"When enabled, new errors automatically generate a fix task."
|
|
270
|
+
)
|
|
271
|
+
),
|
|
272
|
+
button(
|
|
273
|
+
{
|
|
274
|
+
class: "btn btn-sm btn-primary ms-auto",
|
|
275
|
+
onclick: "copilotToggleErrorHealing()",
|
|
276
|
+
},
|
|
277
|
+
"Enable self-healing"
|
|
278
|
+
)
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
const errs = (
|
|
282
|
+
await MetaData.find({
|
|
283
|
+
type: "CopilotConstructMgr",
|
|
284
|
+
name: "error",
|
|
285
|
+
})
|
|
286
|
+
).sort((a, b) => new Date(b.written_at) - new Date(a.written_at));
|
|
287
|
+
|
|
288
|
+
// Build error_id → most recent fix task map for run links
|
|
289
|
+
const allTasks = await MetaData.find({
|
|
290
|
+
type: "CopilotConstructMgr",
|
|
291
|
+
name: "task",
|
|
292
|
+
});
|
|
293
|
+
const fixTaskByErrorId = {};
|
|
294
|
+
for (const t of allTasks) {
|
|
295
|
+
if (t.body.source === "error_fix" && t.body.error_id != null) {
|
|
296
|
+
const eid = parseInt(t.body.error_id);
|
|
297
|
+
if (!fixTaskByErrorId[eid] || t.id > fixTaskByErrorId[eid].id)
|
|
298
|
+
fixTaskByErrorId[eid] = t;
|
|
299
|
+
}
|
|
79
300
|
}
|
|
301
|
+
|
|
302
|
+
const errTable = errs.length
|
|
303
|
+
? div(
|
|
304
|
+
mkTable(
|
|
305
|
+
[
|
|
306
|
+
{
|
|
307
|
+
label: "Source",
|
|
308
|
+
key: (m) =>
|
|
309
|
+
m.body.source === "constructor"
|
|
310
|
+
? span(
|
|
311
|
+
{
|
|
312
|
+
class: "badge bg-secondary",
|
|
313
|
+
title: "Error in the app constructor itself",
|
|
314
|
+
},
|
|
315
|
+
"constructor"
|
|
316
|
+
)
|
|
317
|
+
: span(
|
|
318
|
+
{
|
|
319
|
+
class: "badge bg-primary",
|
|
320
|
+
title: "Error in the application being built",
|
|
321
|
+
},
|
|
322
|
+
"application"
|
|
323
|
+
),
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
label: "When",
|
|
327
|
+
key: (m) => {
|
|
328
|
+
const d = m.written_at ? new Date(m.written_at) : null;
|
|
329
|
+
if (!d) return "";
|
|
330
|
+
return small(
|
|
331
|
+
{ class: "text-muted", style: "white-space:nowrap" },
|
|
332
|
+
d.toLocaleDateString([], { month: "short", day: "numeric" }),
|
|
333
|
+
" ",
|
|
334
|
+
d.toLocaleTimeString([], {
|
|
335
|
+
hour: "2-digit",
|
|
336
|
+
minute: "2-digit",
|
|
337
|
+
})
|
|
338
|
+
);
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
label: "Error",
|
|
343
|
+
key: (m) => {
|
|
344
|
+
const err = m.body.error || {};
|
|
345
|
+
const msg = err.message || String(err) || "(no message)";
|
|
346
|
+
const urlLine = err.url
|
|
347
|
+
? div(
|
|
348
|
+
{ class: "text-muted", style: "font-size:0.75rem" },
|
|
349
|
+
text(String(err.url))
|
|
350
|
+
)
|
|
351
|
+
: "";
|
|
352
|
+
const stackLines = (err.stack || "").split("\n").slice(0, 5);
|
|
353
|
+
const stackPreview = stackLines.length
|
|
354
|
+
? pre(
|
|
355
|
+
{
|
|
356
|
+
style:
|
|
357
|
+
"font-size:0.72rem;white-space:pre-wrap;overflow-wrap:break-word;margin:4px 0 0;",
|
|
358
|
+
},
|
|
359
|
+
text(stackLines.join("\n"))
|
|
360
|
+
)
|
|
361
|
+
: "";
|
|
362
|
+
return div(
|
|
363
|
+
{ style: "word-break:break-word;min-width:0;" },
|
|
364
|
+
div({ class: "fw-semibold small" }, text(msg)),
|
|
365
|
+
urlLine,
|
|
366
|
+
stackPreview
|
|
367
|
+
);
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
label: "Status",
|
|
372
|
+
key: (r) => {
|
|
373
|
+
if (r.body.source === "constructor") return "";
|
|
374
|
+
if (r.body.fixing)
|
|
375
|
+
return span(
|
|
376
|
+
{
|
|
377
|
+
class: "badge bg-info text-dark",
|
|
378
|
+
"data-fixing-id": r.id,
|
|
379
|
+
},
|
|
380
|
+
i({ class: "fas fa-spinner fa-spin me-1" }),
|
|
381
|
+
"Creating fix task..."
|
|
382
|
+
);
|
|
383
|
+
if (r.body.cannot_fix_reason)
|
|
384
|
+
return div(
|
|
385
|
+
span(
|
|
386
|
+
{ class: "badge bg-warning text-dark" },
|
|
387
|
+
"No fix found"
|
|
388
|
+
),
|
|
389
|
+
div(
|
|
390
|
+
{
|
|
391
|
+
class: "text-muted mt-1",
|
|
392
|
+
style:
|
|
393
|
+
"font-size:0.75rem;max-width:220px;white-space:normal;",
|
|
394
|
+
},
|
|
395
|
+
r.body.cannot_fix_reason
|
|
396
|
+
)
|
|
397
|
+
);
|
|
398
|
+
if (r.body.fix_task_created)
|
|
399
|
+
return div(
|
|
400
|
+
{ class: "d-flex align-items-center gap-1" },
|
|
401
|
+
span({ class: "badge bg-success" }, "Fix task created"),
|
|
402
|
+
fixTaskByErrorId[r.id]?.body?.run_id
|
|
403
|
+
? a(
|
|
404
|
+
{
|
|
405
|
+
href: `/view/Saltcorn%20Agent%20copilot?run_id=${
|
|
406
|
+
fixTaskByErrorId[r.id].body.run_id
|
|
407
|
+
}`,
|
|
408
|
+
target: "_blank",
|
|
409
|
+
title: "View fix task run",
|
|
410
|
+
class: "text-muted",
|
|
411
|
+
},
|
|
412
|
+
i({ class: "fas fa-external-link-alt" })
|
|
413
|
+
)
|
|
414
|
+
: ""
|
|
415
|
+
);
|
|
416
|
+
return span(
|
|
417
|
+
{ class: "badge bg-light text-dark border" },
|
|
418
|
+
"New"
|
|
419
|
+
);
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
label: "",
|
|
424
|
+
key: (r) => {
|
|
425
|
+
const iconRow = div(
|
|
426
|
+
{ class: "d-flex align-items-center gap-1" },
|
|
427
|
+
button(
|
|
428
|
+
{
|
|
429
|
+
class: "btn btn-sm btn-outline-secondary",
|
|
430
|
+
onclick: "copilotShowErrDetail(this)",
|
|
431
|
+
title: "View full error",
|
|
432
|
+
"data-err": JSON.stringify(r.body.error),
|
|
433
|
+
},
|
|
434
|
+
i({ class: "fas fa-eye" })
|
|
435
|
+
),
|
|
436
|
+
button(
|
|
437
|
+
{
|
|
438
|
+
class: "btn btn-sm btn-outline-danger",
|
|
439
|
+
onclick: `copilotDelErr(${r.id})`,
|
|
440
|
+
title: "Delete",
|
|
441
|
+
},
|
|
442
|
+
i({ class: "fas fa-trash-alt" })
|
|
443
|
+
)
|
|
444
|
+
);
|
|
445
|
+
let fixRow = "";
|
|
446
|
+
if (r.body.source !== "constructor" && !r.body.fixing) {
|
|
447
|
+
const fixTask = fixTaskByErrorId[r.id];
|
|
448
|
+
if (r.body.fix_task_created && fixTask) {
|
|
449
|
+
const taskStatus = fixTask.body.status || null;
|
|
450
|
+
const canRun =
|
|
451
|
+
taskStatus !== "Done" && taskStatus !== "Running";
|
|
452
|
+
fixRow = div(
|
|
453
|
+
{ class: "d-flex flex-column gap-1 mt-1" },
|
|
454
|
+
button(
|
|
455
|
+
{
|
|
456
|
+
class: "btn btn-sm btn-outline-secondary",
|
|
457
|
+
onclick: `copilotViewFixTask(${r.id})`,
|
|
458
|
+
title: "View fix task",
|
|
459
|
+
},
|
|
460
|
+
i({ class: "fas fa-search me-1" }),
|
|
461
|
+
"View task"
|
|
462
|
+
),
|
|
463
|
+
canRun
|
|
464
|
+
? button(
|
|
465
|
+
{
|
|
466
|
+
id: `run-fix-btn-${fixTask.id}`,
|
|
467
|
+
class: "btn btn-sm btn-success",
|
|
468
|
+
onclick: `copilotRunFixTask(${fixTask.id}, ${r.id})`,
|
|
469
|
+
title: "Run fix task",
|
|
470
|
+
},
|
|
471
|
+
i({ class: "fas fa-play me-1" }),
|
|
472
|
+
"Run"
|
|
473
|
+
)
|
|
474
|
+
: ""
|
|
475
|
+
);
|
|
476
|
+
} else if (
|
|
477
|
+
!r.body.fix_task_created &&
|
|
478
|
+
!r.body.cannot_fix_reason
|
|
479
|
+
) {
|
|
480
|
+
fixRow = div(
|
|
481
|
+
{ class: "mt-1" },
|
|
482
|
+
button(
|
|
483
|
+
{
|
|
484
|
+
id: `fix-err-btn-${r.id}`,
|
|
485
|
+
class: "btn btn-sm btn-outline-primary",
|
|
486
|
+
onclick: `copilotFixError(${r.id})`,
|
|
487
|
+
},
|
|
488
|
+
"Create fix task"
|
|
489
|
+
)
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return div({ style: "white-space:nowrap;" }, iconRow, fixRow);
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
],
|
|
497
|
+
errs
|
|
498
|
+
),
|
|
499
|
+
button(
|
|
500
|
+
{
|
|
501
|
+
class: "btn btn-outline-danger",
|
|
502
|
+
onclick: "copilotDelAllErrs()",
|
|
503
|
+
},
|
|
504
|
+
"Delete all"
|
|
505
|
+
)
|
|
506
|
+
)
|
|
507
|
+
: p("No errors");
|
|
508
|
+
|
|
509
|
+
return div({ class: "mt-2" }, healSection, errTable);
|
|
80
510
|
};
|
|
81
511
|
|
|
82
|
-
const del_err = async (table_id,
|
|
512
|
+
const del_err = async (table_id, vn, config, body, { req, res }) => {
|
|
83
513
|
const r = await MetaData.findOne({
|
|
84
|
-
id: body.id,
|
|
514
|
+
id: parseInt(body.id),
|
|
515
|
+
type: "CopilotConstructMgr",
|
|
516
|
+
name: "error",
|
|
85
517
|
});
|
|
86
|
-
|
|
87
518
|
if (!r) throw new Error("Error not found");
|
|
88
519
|
await r.delete();
|
|
89
|
-
return { json: {
|
|
520
|
+
return { json: { success: true } };
|
|
90
521
|
};
|
|
91
|
-
|
|
522
|
+
|
|
523
|
+
const del_all_errs = async (table_id, vn, config, body, { req, res }) => {
|
|
92
524
|
const rs = await MetaData.find({
|
|
93
525
|
type: "CopilotConstructMgr",
|
|
94
526
|
name: "error",
|
|
95
527
|
});
|
|
96
528
|
for (const r of rs) await r.delete();
|
|
97
|
-
return { json: {
|
|
529
|
+
return { json: { success: true } };
|
|
98
530
|
};
|
|
99
531
|
|
|
100
|
-
|
|
532
|
+
/** Route: toggles the error_heal setting. */
|
|
533
|
+
const toggle_error_healing = async (
|
|
534
|
+
table_id,
|
|
535
|
+
vn,
|
|
536
|
+
config,
|
|
537
|
+
body,
|
|
538
|
+
{ req, res }
|
|
539
|
+
) => {
|
|
540
|
+
const settings = await MetaData.findOne({
|
|
541
|
+
type: "CopilotConstructMgr",
|
|
542
|
+
name: "settings",
|
|
543
|
+
});
|
|
544
|
+
if (settings) {
|
|
545
|
+
await settings.update({
|
|
546
|
+
body: { ...settings.body, error_heal: !settings.body.error_heal },
|
|
547
|
+
});
|
|
548
|
+
} else {
|
|
549
|
+
await MetaData.create({
|
|
550
|
+
type: "CopilotConstructMgr",
|
|
551
|
+
name: "settings",
|
|
552
|
+
body: { error_heal: true },
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
return { json: { success: true } };
|
|
556
|
+
};
|
|
101
557
|
|
|
102
|
-
|
|
558
|
+
/** Route: fires doCreateErrorFixTask for a single error record. */
|
|
559
|
+
const fix_error_task = async (table_id, vn, config, body, { req, res }) => {
|
|
560
|
+
const id = parseInt(body.id);
|
|
561
|
+
const errorMd = await MetaData.findOne({
|
|
562
|
+
id,
|
|
563
|
+
type: "CopilotConstructMgr",
|
|
564
|
+
name: "error",
|
|
565
|
+
});
|
|
566
|
+
if (!errorMd) return { json: { error: "Error record not found" } };
|
|
567
|
+
doCreateErrorFixTask(errorMd, req.user?.id).catch((e) =>
|
|
568
|
+
console.error("fix_error_task error", e)
|
|
569
|
+
);
|
|
570
|
+
return { json: { success: true } };
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
/** Route: returns whether a fix task is still being generated for the given error id. */
|
|
574
|
+
const fix_error_status = async (table_id, vn, config, body, { req, res }) => {
|
|
575
|
+
const id = parseInt(body.id);
|
|
576
|
+
const errorMd = await MetaData.findOne({
|
|
577
|
+
id,
|
|
578
|
+
type: "CopilotConstructMgr",
|
|
579
|
+
name: "error",
|
|
580
|
+
});
|
|
581
|
+
return { json: { fixing: !!errorMd?.body?.fixing } };
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
/** Route: returns task details for a fix task linked to an error. */
|
|
585
|
+
const get_fix_task = async (table_id, vn, config, body, { req, res }) => {
|
|
586
|
+
const errorId = parseInt(body.error_id);
|
|
587
|
+
const allTasks = await MetaData.find({
|
|
588
|
+
type: "CopilotConstructMgr",
|
|
589
|
+
name: "task",
|
|
590
|
+
});
|
|
591
|
+
const fixTask = allTasks
|
|
592
|
+
.filter(
|
|
593
|
+
(t) =>
|
|
594
|
+
t.body.source === "error_fix" && parseInt(t.body.error_id) === errorId
|
|
595
|
+
)
|
|
596
|
+
.sort((a, b) => b.id - a.id)[0];
|
|
597
|
+
if (!fixTask) return { json: { error: "Fix task not found" } };
|
|
598
|
+
return {
|
|
599
|
+
json: {
|
|
600
|
+
task: {
|
|
601
|
+
id: fixTask.id,
|
|
602
|
+
name: fixTask.body.name,
|
|
603
|
+
description: fixTask.body.description,
|
|
604
|
+
status: fixTask.body.status || null,
|
|
605
|
+
},
|
|
606
|
+
},
|
|
607
|
+
};
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
/** Route: returns the rendered error list HTML for AJAX refresh. */
|
|
611
|
+
const err_list_html = async (table_id, vn, config, body, { req, res }) => {
|
|
612
|
+
const html = await errorList(req);
|
|
613
|
+
return { json: { html } };
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
const error_routes = {
|
|
617
|
+
del_err,
|
|
618
|
+
del_all_errs,
|
|
619
|
+
toggle_error_healing,
|
|
620
|
+
fix_error_task,
|
|
621
|
+
fix_error_status,
|
|
622
|
+
get_fix_task,
|
|
623
|
+
err_list_html,
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
const errTableStaticHtml = `
|
|
627
|
+
<style>
|
|
628
|
+
#err-list-area table { table-layout: auto; width: 100%; }
|
|
629
|
+
#err-list-area table th:nth-child(1),
|
|
630
|
+
#err-list-area table td:nth-child(1),
|
|
631
|
+
#err-list-area table th:nth-child(2),
|
|
632
|
+
#err-list-area table td:nth-child(2),
|
|
633
|
+
#err-list-area table th:nth-child(5),
|
|
634
|
+
#err-list-area table td:nth-child(5) { width: 1px; white-space: nowrap; }
|
|
635
|
+
#err-list-area table th:nth-child(3),
|
|
636
|
+
#err-list-area table td:nth-child(3) { width: 100%; }
|
|
637
|
+
#err-list-area table th:nth-child(4),
|
|
638
|
+
#err-list-area table td:nth-child(4) { padding-right: 2rem; }
|
|
639
|
+
</style>
|
|
640
|
+
<div class="modal fade" id="err-detail-modal" tabindex="-1" aria-hidden="true">
|
|
641
|
+
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
|
642
|
+
<div class="modal-content">
|
|
643
|
+
<div class="modal-header">
|
|
644
|
+
<h5 class="modal-title">Error details</h5>
|
|
645
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
646
|
+
</div>
|
|
647
|
+
<div class="modal-body">
|
|
648
|
+
<pre id="err-detail-body" style="white-space:pre-wrap;overflow-wrap:break-word;font-size:0.8rem;"></pre>
|
|
649
|
+
</div>
|
|
650
|
+
</div>
|
|
651
|
+
</div>
|
|
652
|
+
</div>
|
|
653
|
+
<div class="modal fade" id="fix-task-modal" tabindex="-1" aria-hidden="true">
|
|
654
|
+
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
|
655
|
+
<div class="modal-content">
|
|
656
|
+
<div class="modal-header">
|
|
657
|
+
<h5 class="modal-title">Fix task: <span id="fix-task-name" class="text-monospace fw-normal"></span></h5>
|
|
658
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
659
|
+
</div>
|
|
660
|
+
<div class="modal-body">
|
|
661
|
+
<div class="mb-2" id="fix-task-status-row"></div>
|
|
662
|
+
<p id="fix-task-desc" style="white-space:pre-wrap;font-size:0.9rem;"></p>
|
|
663
|
+
</div>
|
|
664
|
+
<div class="modal-footer">
|
|
665
|
+
<button type="button" id="fix-task-run-btn" class="btn btn-success" style="display:none">
|
|
666
|
+
<i class="fas fa-play me-1"></i>Run fix task
|
|
667
|
+
</button>
|
|
668
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
669
|
+
</div>
|
|
670
|
+
</div>
|
|
671
|
+
</div>
|
|
672
|
+
</div>
|
|
673
|
+
<script>
|
|
674
|
+
(function () {
|
|
675
|
+
const _vn = ${JSON.stringify(viewname)};
|
|
676
|
+
function refreshErrArea() {
|
|
677
|
+
view_post(_vn, 'err_list_html', {}, (r) => {
|
|
678
|
+
const el = document.getElementById('err-list-area');
|
|
679
|
+
if (r && r.html && el) el.innerHTML = r.html;
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
function startFixPolling() {
|
|
683
|
+
if (window.dynamic_updates_cfg?.enabled) return;
|
|
684
|
+
document.querySelectorAll('[data-fixing-id]').forEach((el) => {
|
|
685
|
+
const id = parseInt(el.dataset.fixingId);
|
|
686
|
+
const poll = () => {
|
|
687
|
+
view_post(_vn, 'fix_error_status', { id }, (resp) => {
|
|
688
|
+
if (resp && !resp.fixing) {
|
|
689
|
+
refreshErrArea();
|
|
690
|
+
} else {
|
|
691
|
+
setTimeout(poll, 3000);
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
};
|
|
695
|
+
setTimeout(poll, 3000);
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
window.copilotRefreshErrs = refreshErrArea;
|
|
699
|
+
window.copilotToggleErrorHealing = () => {
|
|
700
|
+
view_post(_vn, 'toggle_error_healing', {}, () => refreshErrArea());
|
|
701
|
+
};
|
|
702
|
+
window.copilotFixError = (id) => {
|
|
703
|
+
const btn = document.getElementById('fix-err-btn-' + id);
|
|
704
|
+
if (btn) { btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>'; }
|
|
705
|
+
view_post(_vn, 'fix_error_task', { id }, () => {
|
|
706
|
+
if (window.dynamic_updates_cfg?.enabled) return;
|
|
707
|
+
const poll = () => {
|
|
708
|
+
view_post(_vn, 'fix_error_status', { id }, (resp) => {
|
|
709
|
+
if (resp && !resp.fixing) {
|
|
710
|
+
refreshErrArea();
|
|
711
|
+
} else {
|
|
712
|
+
setTimeout(poll, 3000);
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
};
|
|
716
|
+
setTimeout(poll, 3000);
|
|
717
|
+
});
|
|
718
|
+
};
|
|
719
|
+
window.copilotDelErr = (id) => {
|
|
720
|
+
view_post(_vn, 'del_err', { id }, () => refreshErrArea());
|
|
721
|
+
};
|
|
722
|
+
window.copilotDelAllErrs = () => {
|
|
723
|
+
view_post(_vn, 'del_all_errs', {}, () => refreshErrArea());
|
|
724
|
+
};
|
|
725
|
+
window.copilotShowErrDetail = (btn) => {
|
|
726
|
+
let err = {};
|
|
727
|
+
try { err = JSON.parse(btn.dataset.err || '{}'); } catch (e) {}
|
|
728
|
+
document.getElementById('err-detail-body').textContent = JSON.stringify(err, null, 2);
|
|
729
|
+
new bootstrap.Modal(document.getElementById('err-detail-modal')).show();
|
|
730
|
+
};
|
|
731
|
+
window.copilotViewFixTask = (errorId) => {
|
|
732
|
+
view_post(_vn, 'get_fix_task', { error_id: errorId }, (r) => {
|
|
733
|
+
if (!r || !r.task) return;
|
|
734
|
+
const t = r.task;
|
|
735
|
+
document.getElementById('fix-task-name').textContent = t.name || '';
|
|
736
|
+
document.getElementById('fix-task-desc').textContent = t.description || '';
|
|
737
|
+
const statusBadge =
|
|
738
|
+
t.status === 'Done' ? '<span class="badge bg-success">Done</span>' :
|
|
739
|
+
t.status === 'Running' ? '<span class="badge bg-info text-dark"><i class="fas fa-spinner fa-spin me-1"></i>Running</span>' :
|
|
740
|
+
t.status === 'Error' ? '<span class="badge bg-danger">Error</span>' :
|
|
741
|
+
'<span class="badge bg-secondary">Pending</span>';
|
|
742
|
+
document.getElementById('fix-task-status-row').innerHTML = statusBadge;
|
|
743
|
+
const runBtn = document.getElementById('fix-task-run-btn');
|
|
744
|
+
if (t.status === 'Done' || t.status === 'Running') {
|
|
745
|
+
runBtn.style.display = 'none';
|
|
746
|
+
} else {
|
|
747
|
+
runBtn.style.display = '';
|
|
748
|
+
runBtn.disabled = false;
|
|
749
|
+
runBtn.innerHTML = '<i class="fas fa-play me-1"></i>Run fix task';
|
|
750
|
+
runBtn.onclick = () => copilotRunFixTask(t.id, errorId);
|
|
751
|
+
}
|
|
752
|
+
new bootstrap.Modal(document.getElementById('fix-task-modal')).show();
|
|
753
|
+
});
|
|
754
|
+
};
|
|
755
|
+
window.copilotRunFixTask = (taskId, errorId) => {
|
|
756
|
+
const modalRunBtn = document.getElementById('fix-task-run-btn');
|
|
757
|
+
const rowRunBtn = document.getElementById('run-fix-btn-' + taskId);
|
|
758
|
+
[modalRunBtn, rowRunBtn].forEach((btn) => {
|
|
759
|
+
if (!btn) return;
|
|
760
|
+
btn.disabled = true;
|
|
761
|
+
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Running...';
|
|
762
|
+
});
|
|
763
|
+
const modal = bootstrap.Modal.getInstance(document.getElementById('fix-task-modal'));
|
|
764
|
+
if (modal) modal.hide();
|
|
765
|
+
view_post(_vn, 'run_task', { id: taskId }, () => {
|
|
766
|
+
const poll = () => {
|
|
767
|
+
view_post(_vn, 'get_fix_task', { error_id: errorId }, (r) => {
|
|
768
|
+
const status = r && r.task && r.task.status;
|
|
769
|
+
if (status === 'Done' || status === 'Error') {
|
|
770
|
+
refreshErrArea();
|
|
771
|
+
if (typeof copilotRefreshTasks === 'function') copilotRefreshTasks();
|
|
772
|
+
} else {
|
|
773
|
+
setTimeout(poll, 3000);
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
};
|
|
777
|
+
setTimeout(poll, 3000);
|
|
778
|
+
});
|
|
779
|
+
};
|
|
780
|
+
if (document.readyState !== 'loading') startFixPolling();
|
|
781
|
+
else document.addEventListener('DOMContentLoaded', () => startFixPolling());
|
|
782
|
+
})();
|
|
783
|
+
</script>`;
|
|
784
|
+
|
|
785
|
+
module.exports = {
|
|
786
|
+
errorList,
|
|
787
|
+
doCreateErrorFixTask,
|
|
788
|
+
error_routes,
|
|
789
|
+
errTableStaticHtml,
|
|
790
|
+
};
|