strapi-plugin-mcp-chat 0.3.1 → 0.6.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/README.md +31 -5
- package/admin/src/components/AdminOverlays.tsx +8 -18
- package/admin/src/components/ErrorBoundary.tsx +29 -0
- package/admin/src/components/FloatingChat.tsx +78 -3
- package/admin/src/index.tsx +39 -5
- package/dist/server/index.js +734 -397
- package/package.json +1 -1
- package/server/src/content-tools.ts +20 -6
- package/server/src/controllers/chat.ts +2 -2
- package/server/src/controllers/frontend.ts +7 -2
- package/server/src/index.ts +14 -2
- package/server/src/mcp/define.ts +72 -0
- package/server/src/mcp/index.ts +19 -6
- package/server/src/mcp/tools/buscar-texto.ts +18 -24
- package/server/src/mcp/tools/criar-locale.ts +20 -26
- package/server/src/mcp/tools/editar-campo.ts +29 -35
- package/server/src/mcp/tools/habilitar-i18n.ts +23 -29
- package/server/src/mcp/tools/listar-locales.ts +17 -23
- package/server/src/mcp/tools/publicar.ts +21 -27
- package/server/src/mcp/tools/traduzir.ts +26 -32
- package/server/src/mcp/types.ts +12 -9
- package/server/src/mcp-client.ts +15 -3
- package/server/src/provision/infer.ts +92 -20
- package/server/src/provision/link.ts +232 -35
- package/server/src/provision/orchestrate.ts +4 -3
- package/server/src/provision/runner.ts +44 -1
- package/server/src/provision/write.ts +92 -0
- package/server/src/services/chat.ts +36 -8
package/dist/server/index.js
CHANGED
|
@@ -36,12 +36,12 @@ module.exports = __toCommonJS(index_exports);
|
|
|
36
36
|
// server/src/controllers/chat.ts
|
|
37
37
|
var chat_default = ({ strapi }) => ({
|
|
38
38
|
async message(ctx) {
|
|
39
|
-
const { messages, image, lang, previewUrl, autoPublish } = ctx.request.body || {};
|
|
39
|
+
const { messages, image, lang, previewUrl, previewStatus, autoPublish } = ctx.request.body || {};
|
|
40
40
|
if (!Array.isArray(messages) || messages.length === 0) {
|
|
41
41
|
return ctx.badRequest('Campo "messages" (array) \xE9 obrigat\xF3rio.');
|
|
42
42
|
}
|
|
43
43
|
try {
|
|
44
|
-
const result = await strapi.plugin("mcp-chat").service("chat").chat({ messages, image, lang, previewUrl, autoPublish });
|
|
44
|
+
const result = await strapi.plugin("mcp-chat").service("chat").chat({ messages, image, lang, previewUrl, previewStatus, autoPublish });
|
|
45
45
|
ctx.body = result;
|
|
46
46
|
} catch (e) {
|
|
47
47
|
strapi.log.error(`[mcp-chat] ${e?.message || e}`);
|
|
@@ -88,8 +88,8 @@ var import_node_path7 = __toESM(require("node:path"));
|
|
|
88
88
|
var import_jszip = __toESM(require("jszip"));
|
|
89
89
|
|
|
90
90
|
// server/src/provision/orchestrate.ts
|
|
91
|
-
var
|
|
92
|
-
var
|
|
91
|
+
var import_node_fs4 = __toESM(require("node:fs"));
|
|
92
|
+
var import_node_path4 = __toESM(require("node:path"));
|
|
93
93
|
|
|
94
94
|
// server/src/provision/manifest.ts
|
|
95
95
|
var import_utils = require("@strapi/utils");
|
|
@@ -412,6 +412,68 @@ var import_node_path = __toESM(require("node:path"));
|
|
|
412
412
|
function isDev() {
|
|
413
413
|
return process.env.NODE_ENV === "development";
|
|
414
414
|
}
|
|
415
|
+
var KNOWN_ATTR_TYPES = /* @__PURE__ */ new Set([
|
|
416
|
+
"string",
|
|
417
|
+
"text",
|
|
418
|
+
"richtext",
|
|
419
|
+
"blocks",
|
|
420
|
+
"email",
|
|
421
|
+
"password",
|
|
422
|
+
"uid",
|
|
423
|
+
"enumeration",
|
|
424
|
+
"json",
|
|
425
|
+
"integer",
|
|
426
|
+
"biginteger",
|
|
427
|
+
"decimal",
|
|
428
|
+
"float",
|
|
429
|
+
"date",
|
|
430
|
+
"time",
|
|
431
|
+
"datetime",
|
|
432
|
+
"timestamp",
|
|
433
|
+
"boolean",
|
|
434
|
+
"media",
|
|
435
|
+
"relation",
|
|
436
|
+
"component",
|
|
437
|
+
"dynamiczone"
|
|
438
|
+
]);
|
|
439
|
+
function validateApi(api) {
|
|
440
|
+
const errs = [];
|
|
441
|
+
const rel = Object.keys(api.files).find((r) => r.endsWith("schema.json"));
|
|
442
|
+
if (!rel) {
|
|
443
|
+
errs.push(`${api.singularName}: schema.json ausente nos arquivos gerados`);
|
|
444
|
+
return errs;
|
|
445
|
+
}
|
|
446
|
+
let schema;
|
|
447
|
+
try {
|
|
448
|
+
schema = JSON.parse(api.files[rel]);
|
|
449
|
+
} catch (e) {
|
|
450
|
+
errs.push(`${api.singularName}: schema.json n\xE3o \xE9 JSON v\xE1lido (${e?.message ?? e})`);
|
|
451
|
+
return errs;
|
|
452
|
+
}
|
|
453
|
+
if (schema?.kind !== "collectionType" && schema?.kind !== "singleType") {
|
|
454
|
+
errs.push(`${api.singularName}: "kind" inv\xE1lido (${schema?.kind})`);
|
|
455
|
+
}
|
|
456
|
+
if (!schema?.info?.singularName || !schema?.info?.pluralName) {
|
|
457
|
+
errs.push(`${api.singularName}: info.singularName/pluralName obrigat\xF3rios`);
|
|
458
|
+
}
|
|
459
|
+
const attrs = schema?.attributes;
|
|
460
|
+
if (!attrs || typeof attrs !== "object") {
|
|
461
|
+
errs.push(`${api.singularName}: "attributes" ausente ou inv\xE1lido`);
|
|
462
|
+
} else {
|
|
463
|
+
for (const [name, a] of Object.entries(attrs)) {
|
|
464
|
+
if (!a || typeof a !== "object" || !a.type) {
|
|
465
|
+
errs.push(`${api.singularName}.${name}: atributo sem "type"`);
|
|
466
|
+
} else if (!KNOWN_ATTR_TYPES.has(a.type)) {
|
|
467
|
+
errs.push(`${api.singularName}.${name}: type desconhecido "${a.type}"`);
|
|
468
|
+
} else if (a.type === "relation" && !a.target) {
|
|
469
|
+
errs.push(`${api.singularName}.${name}: relation sem "target"`);
|
|
470
|
+
} else if (a.type === "component" && !a.component) {
|
|
471
|
+
errs.push(`${api.singularName}.${name}: component sem "component"`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return errs;
|
|
476
|
+
}
|
|
415
477
|
function writeApis(apis, opts) {
|
|
416
478
|
const result = {
|
|
417
479
|
ok: false,
|
|
@@ -431,6 +493,37 @@ function writeApis(apis, opts) {
|
|
|
431
493
|
result.errors.push(`apiRoot deve ser um caminho absoluto: ${opts.apiRoot}`);
|
|
432
494
|
return result;
|
|
433
495
|
}
|
|
496
|
+
const toWrite = apis.filter((api) => !import_node_fs.default.existsSync(import_node_path.default.join(opts.apiRoot, api.singularName)));
|
|
497
|
+
const knownSingulars = /* @__PURE__ */ new Set([
|
|
498
|
+
...apis.map((a) => a.singularName),
|
|
499
|
+
...import_node_fs.default.existsSync(opts.apiRoot) ? import_node_fs.default.readdirSync(opts.apiRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name) : []
|
|
500
|
+
]);
|
|
501
|
+
const validationErrors = [];
|
|
502
|
+
for (const api of toWrite) {
|
|
503
|
+
validationErrors.push(...validateApi(api));
|
|
504
|
+
const rel = Object.keys(api.files).find((r) => r.endsWith("schema.json"));
|
|
505
|
+
if (rel) {
|
|
506
|
+
try {
|
|
507
|
+
const attrs = JSON.parse(api.files[rel])?.attributes || {};
|
|
508
|
+
for (const [name, a] of Object.entries(attrs)) {
|
|
509
|
+
if (a?.type === "relation" && typeof a.target === "string") {
|
|
510
|
+
const tgt = a.target.split("::")[1]?.split(".")[0];
|
|
511
|
+
if (tgt && !knownSingulars.has(tgt)) {
|
|
512
|
+
validationErrors.push(`${api.singularName}.${name}: relation aponta para "${a.target}" inexistente`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
} catch {
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (validationErrors.length) {
|
|
521
|
+
result.errors.push(
|
|
522
|
+
"Schema gerado inv\xE1lido \u2014 nada foi escrito (provis\xE3o abortada com seguran\xE7a):",
|
|
523
|
+
...validationErrors
|
|
524
|
+
);
|
|
525
|
+
return result;
|
|
526
|
+
}
|
|
434
527
|
for (const api of apis) {
|
|
435
528
|
const apiDir = import_node_path.default.join(opts.apiRoot, api.singularName);
|
|
436
529
|
if (import_node_fs.default.existsSync(apiDir)) {
|
|
@@ -540,8 +633,8 @@ async function seedContent(strapi, manifest) {
|
|
|
540
633
|
}
|
|
541
634
|
|
|
542
635
|
// server/src/provision/link.ts
|
|
543
|
-
var
|
|
544
|
-
var
|
|
636
|
+
var import_node_fs3 = __toESM(require("node:fs"));
|
|
637
|
+
var import_node_path3 = __toESM(require("node:path"));
|
|
545
638
|
|
|
546
639
|
// server/src/provision/adapters.ts
|
|
547
640
|
var nextAdapter = {
|
|
@@ -677,6 +770,179 @@ ${interfaces}
|
|
|
677
770
|
`;
|
|
678
771
|
}
|
|
679
772
|
|
|
773
|
+
// server/src/provision/runner.ts
|
|
774
|
+
var import_node_child_process = require("node:child_process");
|
|
775
|
+
var import_node_net = __toESM(require("node:net"));
|
|
776
|
+
var import_node_fs2 = __toESM(require("node:fs"));
|
|
777
|
+
var import_node_path2 = __toESM(require("node:path"));
|
|
778
|
+
var info = { state: "idle", dir: null, url: null, pm: null, error: null, log: [] };
|
|
779
|
+
var child = null;
|
|
780
|
+
var pollTimer = null;
|
|
781
|
+
var appRootForPid = null;
|
|
782
|
+
function pidFilePath(appRoot) {
|
|
783
|
+
return import_node_path2.default.join(appRoot, ".mcp-chat", "frontend.pid");
|
|
784
|
+
}
|
|
785
|
+
function writePid(pid) {
|
|
786
|
+
try {
|
|
787
|
+
if (!appRootForPid) return;
|
|
788
|
+
const pf = pidFilePath(appRootForPid);
|
|
789
|
+
import_node_fs2.default.mkdirSync(import_node_path2.default.dirname(pf), { recursive: true });
|
|
790
|
+
import_node_fs2.default.writeFileSync(pf, String(pid), "utf8");
|
|
791
|
+
} catch {
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
function clearPid() {
|
|
795
|
+
try {
|
|
796
|
+
if (appRootForPid) import_node_fs2.default.unlinkSync(pidFilePath(appRootForPid));
|
|
797
|
+
} catch {
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
function isAlive(pid) {
|
|
801
|
+
try {
|
|
802
|
+
process.kill(pid, 0);
|
|
803
|
+
return true;
|
|
804
|
+
} catch {
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
function cleanupStaleFrontend(appRoot) {
|
|
809
|
+
appRootForPid = appRoot;
|
|
810
|
+
try {
|
|
811
|
+
const pf = pidFilePath(appRoot);
|
|
812
|
+
if (!import_node_fs2.default.existsSync(pf)) return;
|
|
813
|
+
const pid = parseInt(import_node_fs2.default.readFileSync(pf, "utf8").trim(), 10);
|
|
814
|
+
if (pid && isAlive(pid)) {
|
|
815
|
+
try {
|
|
816
|
+
process.kill(pid, "SIGTERM");
|
|
817
|
+
} catch {
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
import_node_fs2.default.unlinkSync(pf);
|
|
821
|
+
} catch {
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
function detectPM(dir) {
|
|
825
|
+
if (import_node_fs2.default.existsSync(import_node_path2.default.join(dir, "bun.lockb")) || import_node_fs2.default.existsSync(import_node_path2.default.join(dir, "bun.lock"))) return "bun";
|
|
826
|
+
if (import_node_fs2.default.existsSync(import_node_path2.default.join(dir, "pnpm-lock.yaml"))) return "pnpm";
|
|
827
|
+
if (import_node_fs2.default.existsSync(import_node_path2.default.join(dir, "yarn.lock"))) return "yarn";
|
|
828
|
+
return "npm";
|
|
829
|
+
}
|
|
830
|
+
var has = (dir, ...names) => names.some((n) => import_node_fs2.default.existsSync(import_node_path2.default.join(dir, n)));
|
|
831
|
+
function detectFramework(dir) {
|
|
832
|
+
if (has(dir, "next.config.js", "next.config.ts", "next.config.mjs")) return "next";
|
|
833
|
+
if (has(dir, "vite.config.js", "vite.config.ts", "vite.config.mjs")) return "vite";
|
|
834
|
+
return "other";
|
|
835
|
+
}
|
|
836
|
+
var FRONTEND_BASE_PORT = 4321;
|
|
837
|
+
function findFreePort(start) {
|
|
838
|
+
return new Promise((resolve) => {
|
|
839
|
+
const tryPort = (p) => {
|
|
840
|
+
if (p > start + 200) return resolve(start);
|
|
841
|
+
const srv = import_node_net.default.createServer();
|
|
842
|
+
srv.once("error", () => tryPort(p + 1));
|
|
843
|
+
srv.once("listening", () => srv.close(() => resolve(p)));
|
|
844
|
+
srv.listen(p, "0.0.0.0");
|
|
845
|
+
};
|
|
846
|
+
tryPort(start);
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
function pushLog(s) {
|
|
850
|
+
for (const line of String(s).split("\n")) {
|
|
851
|
+
const t = line.trim();
|
|
852
|
+
if (t) info.log.push(t);
|
|
853
|
+
}
|
|
854
|
+
if (info.log.length > 60) info.log = info.log.slice(-60);
|
|
855
|
+
}
|
|
856
|
+
async function urlUp(url) {
|
|
857
|
+
try {
|
|
858
|
+
const res = await fetch(url, { method: "GET" });
|
|
859
|
+
return res.status >= 200 && res.status < 400;
|
|
860
|
+
} catch {
|
|
861
|
+
return false;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
function getRunStatus() {
|
|
865
|
+
return { ...info, log: info.log.slice(-15) };
|
|
866
|
+
}
|
|
867
|
+
function stopFrontend() {
|
|
868
|
+
if (pollTimer) {
|
|
869
|
+
clearInterval(pollTimer);
|
|
870
|
+
pollTimer = null;
|
|
871
|
+
}
|
|
872
|
+
if (child) {
|
|
873
|
+
try {
|
|
874
|
+
child.kill("SIGTERM");
|
|
875
|
+
} catch {
|
|
876
|
+
}
|
|
877
|
+
child = null;
|
|
878
|
+
}
|
|
879
|
+
clearPid();
|
|
880
|
+
if (info.state !== "error") info.state = "idle";
|
|
881
|
+
}
|
|
882
|
+
async function startFrontend(_strapi, opts) {
|
|
883
|
+
const { dir } = opts;
|
|
884
|
+
if (_strapi?.dirs?.app?.root) appRootForPid = _strapi.dirs.app.root;
|
|
885
|
+
if (child && info.dir === dir && ["installing", "starting", "running"].includes(info.state)) {
|
|
886
|
+
return getRunStatus();
|
|
887
|
+
}
|
|
888
|
+
stopFrontend();
|
|
889
|
+
const pm = detectPM(dir);
|
|
890
|
+
const framework = detectFramework(dir);
|
|
891
|
+
const port = await findFreePort(FRONTEND_BASE_PORT);
|
|
892
|
+
const url = `http://127.0.0.1:${port}`;
|
|
893
|
+
info = { state: "installing", dir, url, pm, error: null, log: [] };
|
|
894
|
+
const spawnIn = (cmd, args) => (0, import_node_child_process.spawn)(cmd, args, { cwd: dir, env: { ...process.env }, stdio: ["ignore", "pipe", "pipe"] });
|
|
895
|
+
const fwArgs = framework === "next" ? ["-H", "127.0.0.1", "-p", String(port)] : framework === "vite" ? ["--host", "127.0.0.1", "--port", String(port), "--strictPort"] : ["--port", String(port)];
|
|
896
|
+
const devArgs = pm === "yarn" ? ["dev", ...fwArgs] : ["run", "dev", "--", ...fwArgs];
|
|
897
|
+
const startDev = () => {
|
|
898
|
+
info.state = "starting";
|
|
899
|
+
child = spawnIn(pm, devArgs);
|
|
900
|
+
if (child.pid) writePid(child.pid);
|
|
901
|
+
child.stdout?.on("data", (d) => pushLog(d));
|
|
902
|
+
child.stderr?.on("data", (d) => pushLog(d));
|
|
903
|
+
child.on("exit", (code) => {
|
|
904
|
+
child = null;
|
|
905
|
+
clearPid();
|
|
906
|
+
if (pollTimer) {
|
|
907
|
+
clearInterval(pollTimer);
|
|
908
|
+
pollTimer = null;
|
|
909
|
+
}
|
|
910
|
+
if (info.state === "running") info.state = "idle";
|
|
911
|
+
else {
|
|
912
|
+
info.state = "error";
|
|
913
|
+
info.error = `dev encerrou (c\xF3digo ${code}). Veja o log.`;
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
pollTimer = setInterval(async () => {
|
|
917
|
+
if (await urlUp(url)) {
|
|
918
|
+
info.state = "running";
|
|
919
|
+
if (pollTimer) {
|
|
920
|
+
clearInterval(pollTimer);
|
|
921
|
+
pollTimer = null;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}, 1500);
|
|
925
|
+
};
|
|
926
|
+
const needInstall = !import_node_fs2.default.existsSync(import_node_path2.default.join(dir, "node_modules"));
|
|
927
|
+
if (needInstall) {
|
|
928
|
+
pushLog(`Instalando depend\xEAncias com ${pm}\u2026`);
|
|
929
|
+
const installArgs = pm === "npm" ? ["install", "--no-audit", "--no-fund"] : ["install"];
|
|
930
|
+
const inst = spawnIn(pm, installArgs);
|
|
931
|
+
inst.stdout?.on("data", (d) => pushLog(d));
|
|
932
|
+
inst.stderr?.on("data", (d) => pushLog(d));
|
|
933
|
+
inst.on("exit", (code) => {
|
|
934
|
+
if (code === 0) startDev();
|
|
935
|
+
else {
|
|
936
|
+
info.state = "error";
|
|
937
|
+
info.error = `instala\xE7\xE3o falhou (c\xF3digo ${code}). Veja o log.`;
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
} else {
|
|
941
|
+
startDev();
|
|
942
|
+
}
|
|
943
|
+
return getRunStatus();
|
|
944
|
+
}
|
|
945
|
+
|
|
680
946
|
// server/src/provision/link.ts
|
|
681
947
|
function parseEnv(content) {
|
|
682
948
|
const out = {};
|
|
@@ -706,48 +972,120 @@ function mergeEnv(existing, next) {
|
|
|
706
972
|
}
|
|
707
973
|
return { content, added, preserved };
|
|
708
974
|
}
|
|
709
|
-
|
|
975
|
+
var PREVIEW_MARKER = "mcp-chat:preview-merged";
|
|
976
|
+
var PREVIEW_MODULE = "mcp-chat-preview";
|
|
977
|
+
function buildPreviewConfig(manifest, framework = manifest.framework) {
|
|
710
978
|
const routes = {};
|
|
711
979
|
for (const ct of manifest.contentTypes) {
|
|
712
|
-
|
|
980
|
+
routes[apiUid(ct.singularName)] = ct.preview?.route ?? "/";
|
|
713
981
|
}
|
|
714
982
|
const routesJson = JSON.stringify(routes, null, 2);
|
|
715
|
-
|
|
983
|
+
const isNext = framework === "next";
|
|
984
|
+
const urlBranch = isNext ? ` // Next.js: rota de draft mode que seta o cookie e redireciona p/ \`path\`.
|
|
985
|
+
const qs = new URLSearchParams({ secret, status: status ?? 'draft', path: pathname });
|
|
986
|
+
return \`\${clientUrl}/api/preview?\${qs.toString()}\`;` : ` // SPA (Vite/TanStack): abre a p\xE1gina direta; o front l\xEA ?preview/status.
|
|
987
|
+
const qs = new URLSearchParams({ preview: '1', status: status ?? 'draft', secret });
|
|
988
|
+
return \`\${clientUrl}\${pathname}?\${qs.toString()}\`;`;
|
|
989
|
+
return `// Preview gerado pelo mcp-chat a partir do strapi.manifest.json (framework: ${framework}).
|
|
716
990
|
// Mapa uid -> rota do frontend (placeholders :campo s\xE3o preenchidos pelo doc).
|
|
991
|
+
// Este arquivo \xE9 mesclado em config/admin.ts \u2014 n\xE3o precisa edit\xE1-lo \xE0 m\xE3o.
|
|
717
992
|
const PREVIEW_ROUTES: Record<string, string> = ${routesJson};
|
|
718
993
|
|
|
719
|
-
export default ({ env }) => ({
|
|
720
|
-
auth: {
|
|
721
|
-
secret: env('ADMIN_JWT_SECRET'),
|
|
722
|
-
},
|
|
723
|
-
apiToken: { salt: env('API_TOKEN_SALT') },
|
|
724
|
-
transfer: { token: { salt: env('TRANSFER_TOKEN_SALT') } },
|
|
994
|
+
export default ({ env }: { env: any }) => ({
|
|
725
995
|
preview: {
|
|
726
996
|
enabled: true,
|
|
727
997
|
config: {
|
|
728
998
|
allowedOrigins: [env('CLIENT_URL', 'http://localhost:3000')],
|
|
729
999
|
async handler(uid: string, { documentId, locale, status }: any) {
|
|
730
|
-
const route = PREVIEW_ROUTES[uid];
|
|
731
|
-
if (!route) return null;
|
|
732
|
-
const doc = await strapi.documents(uid as any).findOne({ documentId, locale });
|
|
733
|
-
if (!doc) return null;
|
|
734
|
-
// substitui :campo pelos valores do documento (ex.: :slug)
|
|
735
|
-
const pathname = route.replace(/:([a-zA-Z0-9_]+)/g, (_m, f) =>
|
|
736
|
-
encodeURIComponent(String((doc as any)[f] ?? ''))
|
|
737
|
-
);
|
|
1000
|
+
const route = PREVIEW_ROUTES[uid] ?? '/';
|
|
738
1001
|
const clientUrl = env('CLIENT_URL', 'http://localhost:3000');
|
|
739
1002
|
const secret = env('PREVIEW_SECRET', '');
|
|
740
|
-
|
|
741
|
-
|
|
1003
|
+
|
|
1004
|
+
// s\xF3 busca o doc se a rota tiver placeholders (ex.: :slug) a preencher.
|
|
1005
|
+
let pathname = route;
|
|
1006
|
+
if (pathname.includes(':')) {
|
|
1007
|
+
const doc = await strapi.documents(uid as any).findOne({ documentId, locale });
|
|
1008
|
+
if (!doc) return null;
|
|
1009
|
+
pathname = pathname.replace(/:([a-zA-Z0-9_]+)/g, (_m, f) =>
|
|
1010
|
+
encodeURIComponent(String((doc as any)[f] ?? ''))
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
${urlBranch}
|
|
742
1015
|
},
|
|
743
1016
|
},
|
|
744
1017
|
},
|
|
745
1018
|
});
|
|
746
1019
|
`;
|
|
747
1020
|
}
|
|
1021
|
+
function buildStandaloneAdmin() {
|
|
1022
|
+
return `// ${PREVIEW_MARKER} \u2014 admin.ts gerado pelo mcp-chat (preview inclu\xEDdo).
|
|
1023
|
+
import previewConfig from './${PREVIEW_MODULE}';
|
|
1024
|
+
|
|
1025
|
+
export default ({ env }: { env: any }) => ({
|
|
1026
|
+
auth: { secret: env('ADMIN_JWT_SECRET') },
|
|
1027
|
+
apiToken: { salt: env('API_TOKEN_SALT') },
|
|
1028
|
+
transfer: { token: { salt: env('TRANSFER_TOKEN_SALT') } },
|
|
1029
|
+
secrets: { encryptionKey: env('ENCRYPTION_KEY') },
|
|
1030
|
+
flags: {
|
|
1031
|
+
nps: env.bool('FLAG_NPS', true),
|
|
1032
|
+
promoteEE: env.bool('FLAG_PROMOTE_EE', true),
|
|
1033
|
+
},
|
|
1034
|
+
...previewConfig({ env }),
|
|
1035
|
+
});
|
|
1036
|
+
`;
|
|
1037
|
+
}
|
|
1038
|
+
function buildAdminWrapper() {
|
|
1039
|
+
return `// ${PREVIEW_MARKER} \u2014 preview do mcp-chat mesclado sobre o admin original.
|
|
1040
|
+
// Sua config original est\xE1 preservada em ./admin.base \u2014 edite l\xE1, n\xE3o aqui.
|
|
1041
|
+
import base from './admin.base';
|
|
1042
|
+
import previewConfig from './${PREVIEW_MODULE}';
|
|
1043
|
+
|
|
1044
|
+
export default (ctx: any) => {
|
|
1045
|
+
const b = typeof base === 'function' ? (base as any)(ctx) : (base ?? {});
|
|
1046
|
+
return { ...b, ...previewConfig(ctx) };
|
|
1047
|
+
};
|
|
1048
|
+
`;
|
|
1049
|
+
}
|
|
1050
|
+
var CSP_MARKER = "mcp-chat:csp-frame";
|
|
1051
|
+
function securityBlock() {
|
|
1052
|
+
return ` // ${CSP_MARKER} \u2014 libera o frame-src p/ o admin embutir o preview do frontend
|
|
1053
|
+
// (dev server local em qualquer porta). Sem isto a CSP padr\xE3o (default-src 'self')
|
|
1054
|
+
// bloqueia o iframe e o preview fica em branco.
|
|
1055
|
+
{
|
|
1056
|
+
name: 'strapi::security',
|
|
1057
|
+
config: {
|
|
1058
|
+
contentSecurityPolicy: {
|
|
1059
|
+
useDefaults: true,
|
|
1060
|
+
directives: {
|
|
1061
|
+
'connect-src': ["'self'", 'https:', 'http:'],
|
|
1062
|
+
'frame-src': ["'self'", 'http://localhost:*', 'http://127.0.0.1:*'],
|
|
1063
|
+
'img-src': ["'self'", 'data:', 'blob:', 'market-assets.strapi.io'],
|
|
1064
|
+
'media-src': ["'self'", 'data:', 'blob:'],
|
|
1065
|
+
upgradeInsecureRequests: null,
|
|
1066
|
+
},
|
|
1067
|
+
},
|
|
1068
|
+
},
|
|
1069
|
+
},`;
|
|
1070
|
+
}
|
|
1071
|
+
function patchSecurityMiddleware(strapiAppDir, dryRun) {
|
|
1072
|
+
const configDir = import_node_path3.default.join(strapiAppDir, "config");
|
|
1073
|
+
const file = ["middlewares.ts", "middlewares.js"].find(
|
|
1074
|
+
(f) => import_node_fs3.default.existsSync(import_node_path3.default.join(configDir, f))
|
|
1075
|
+
);
|
|
1076
|
+
if (!file) return "skipped";
|
|
1077
|
+
const p = import_node_path3.default.join(configDir, file);
|
|
1078
|
+
const content = import_node_fs3.default.readFileSync(p, "utf8");
|
|
1079
|
+
if (content.includes(CSP_MARKER) || content.includes("'frame-src'")) return "already";
|
|
1080
|
+
const m = content.match(/^[ \t]*['"]strapi::security['"]\s*,/m);
|
|
1081
|
+
if (!m) return "manual";
|
|
1082
|
+
const next = content.replace(m[0], securityBlock());
|
|
1083
|
+
if (!dryRun) import_node_fs3.default.writeFileSync(p, next, "utf8");
|
|
1084
|
+
return "patched";
|
|
1085
|
+
}
|
|
748
1086
|
function ensureInside(base, target) {
|
|
749
|
-
const n =
|
|
750
|
-
return n === base || n.startsWith(base +
|
|
1087
|
+
const n = import_node_path3.default.normalize(target);
|
|
1088
|
+
return n === base || n.startsWith(base + import_node_path3.default.sep);
|
|
751
1089
|
}
|
|
752
1090
|
function linkFrontend(manifest, opts) {
|
|
753
1091
|
const adapter = adapterForManifest(manifest);
|
|
@@ -759,49 +1097,90 @@ function linkFrontend(manifest, opts) {
|
|
|
759
1097
|
typesFile: "strapi-types.ts",
|
|
760
1098
|
previewFile: "config/admin.ts",
|
|
761
1099
|
previewAction: "skipped",
|
|
1100
|
+
backendEnvAdded: [],
|
|
1101
|
+
cspAction: "skipped",
|
|
762
1102
|
errors: []
|
|
763
1103
|
};
|
|
764
|
-
if (!
|
|
1104
|
+
if (!import_node_path3.default.isAbsolute(opts.frontendDir) || !import_node_path3.default.isAbsolute(opts.strapiAppDir)) {
|
|
765
1105
|
result.errors.push("frontendDir e strapiAppDir devem ser absolutos");
|
|
766
1106
|
return result;
|
|
767
1107
|
}
|
|
768
1108
|
try {
|
|
769
|
-
const envPath =
|
|
1109
|
+
const envPath = import_node_path3.default.join(opts.frontendDir, adapter.envFileName);
|
|
770
1110
|
if (!ensureInside(opts.frontendDir, envPath)) throw new Error("env fora do frontendDir");
|
|
771
|
-
const existing =
|
|
1111
|
+
const existing = import_node_fs3.default.existsSync(envPath) ? import_node_fs3.default.readFileSync(envPath, "utf8") : "";
|
|
772
1112
|
const vars = adapter.buildEnv(opts.context);
|
|
773
1113
|
const { content, added, preserved } = mergeEnv(existing, vars);
|
|
774
1114
|
result.envAdded = added;
|
|
775
1115
|
result.envPreserved = preserved;
|
|
776
|
-
if (!opts.dryRun && added.length)
|
|
1116
|
+
if (!opts.dryRun && added.length) import_node_fs3.default.writeFileSync(envPath, content, "utf8");
|
|
777
1117
|
} catch (e) {
|
|
778
1118
|
result.errors.push(`env: ${e?.message ?? e}`);
|
|
779
1119
|
}
|
|
780
1120
|
try {
|
|
781
|
-
const typesPath =
|
|
1121
|
+
const typesPath = import_node_path3.default.join(opts.frontendDir, result.typesFile);
|
|
782
1122
|
if (!ensureInside(opts.frontendDir, typesPath)) throw new Error("types fora do frontendDir");
|
|
783
|
-
if (!opts.dryRun)
|
|
1123
|
+
if (!opts.dryRun) import_node_fs3.default.writeFileSync(typesPath, generateTypes(manifest), "utf8");
|
|
784
1124
|
} catch (e) {
|
|
785
1125
|
result.errors.push(`types: ${e?.message ?? e}`);
|
|
786
1126
|
}
|
|
787
1127
|
try {
|
|
788
|
-
const
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
1128
|
+
const configDir = import_node_path3.default.join(opts.strapiAppDir, "config");
|
|
1129
|
+
if (!ensureInside(opts.strapiAppDir, configDir)) throw new Error("config fora do strapiAppDir");
|
|
1130
|
+
const adminPath = import_node_path3.default.join(configDir, "admin.ts");
|
|
1131
|
+
const adminBasePath = import_node_path3.default.join(configDir, "admin.base.ts");
|
|
1132
|
+
const modulePath = import_node_path3.default.join(configDir, `${PREVIEW_MODULE}.ts`);
|
|
1133
|
+
if (!opts.dryRun) {
|
|
1134
|
+
import_node_fs3.default.mkdirSync(configDir, { recursive: true });
|
|
1135
|
+
import_node_fs3.default.writeFileSync(modulePath, buildPreviewConfig(manifest, adapter.framework), "utf8");
|
|
1136
|
+
try {
|
|
1137
|
+
import_node_fs3.default.unlinkSync(import_node_path3.default.join(configDir, "admin.mcp-chat-preview.ts"));
|
|
1138
|
+
} catch {
|
|
799
1139
|
}
|
|
1140
|
+
}
|
|
1141
|
+
result.previewFile = `config/${PREVIEW_MODULE}.ts`;
|
|
1142
|
+
if (!import_node_fs3.default.existsSync(adminPath)) {
|
|
1143
|
+
if (!opts.dryRun) import_node_fs3.default.writeFileSync(adminPath, buildStandaloneAdmin(), "utf8");
|
|
800
1144
|
result.previewAction = "created";
|
|
1145
|
+
} else {
|
|
1146
|
+
const adminContent = import_node_fs3.default.readFileSync(adminPath, "utf8");
|
|
1147
|
+
if (!adminContent.includes(PREVIEW_MARKER)) {
|
|
1148
|
+
if (!opts.dryRun) {
|
|
1149
|
+
if (!import_node_fs3.default.existsSync(adminBasePath)) {
|
|
1150
|
+
import_node_fs3.default.writeFileSync(adminBasePath, adminContent, "utf8");
|
|
1151
|
+
}
|
|
1152
|
+
import_node_fs3.default.writeFileSync(adminPath, buildAdminWrapper(), "utf8");
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
result.previewAction = "merged";
|
|
801
1156
|
}
|
|
802
1157
|
} catch (e) {
|
|
803
1158
|
result.errors.push(`preview: ${e?.message ?? e}`);
|
|
804
1159
|
}
|
|
1160
|
+
try {
|
|
1161
|
+
const clientUrl = opts.context.frontendUrl || `http://localhost:${FRONTEND_BASE_PORT}`;
|
|
1162
|
+
const backendVars = { CLIENT_URL: clientUrl };
|
|
1163
|
+
if (opts.context.previewSecret) backendVars.PREVIEW_SECRET = opts.context.previewSecret;
|
|
1164
|
+
const backendEnvPath = import_node_path3.default.join(opts.strapiAppDir, ".env");
|
|
1165
|
+
if (ensureInside(opts.strapiAppDir, backendEnvPath)) {
|
|
1166
|
+
const existing = import_node_fs3.default.existsSync(backendEnvPath) ? import_node_fs3.default.readFileSync(backendEnvPath, "utf8") : "";
|
|
1167
|
+
const { content, added } = mergeEnv(existing, backendVars);
|
|
1168
|
+
result.backendEnvAdded = added;
|
|
1169
|
+
if (!opts.dryRun && added.length) import_node_fs3.default.writeFileSync(backendEnvPath, content, "utf8");
|
|
1170
|
+
}
|
|
1171
|
+
} catch (e) {
|
|
1172
|
+
result.errors.push(`backend env: ${e?.message ?? e}`);
|
|
1173
|
+
}
|
|
1174
|
+
try {
|
|
1175
|
+
result.cspAction = patchSecurityMiddleware(opts.strapiAppDir, opts.dryRun);
|
|
1176
|
+
if (result.cspAction === "manual") {
|
|
1177
|
+
result.errors.push(
|
|
1178
|
+
"CSP: config/middlewares j\xE1 tem strapi::security customizado \u2014 adicione manualmente frame-src 'self' http://localhost:* http://127.0.0.1:* para o preview embutir o frontend."
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
} catch (e) {
|
|
1182
|
+
result.errors.push(`csp: ${e?.message ?? e}`);
|
|
1183
|
+
}
|
|
805
1184
|
result.ok = result.errors.length === 0;
|
|
806
1185
|
return result;
|
|
807
1186
|
}
|
|
@@ -844,17 +1223,17 @@ var MARKER_DIR = ".mcp-chat";
|
|
|
844
1223
|
var MARKER_FILE = "pending-provision.json";
|
|
845
1224
|
var DONE_FILE = "last-provision.json";
|
|
846
1225
|
function markerPath(strapiAppDir) {
|
|
847
|
-
return
|
|
1226
|
+
return import_node_path4.default.join(strapiAppDir, MARKER_DIR, MARKER_FILE);
|
|
848
1227
|
}
|
|
849
1228
|
function donePath(strapiAppDir) {
|
|
850
|
-
return
|
|
1229
|
+
return import_node_path4.default.join(strapiAppDir, MARKER_DIR, DONE_FILE);
|
|
851
1230
|
}
|
|
852
1231
|
function getProvisionStatus(strapiAppDir) {
|
|
853
|
-
const pending =
|
|
1232
|
+
const pending = import_node_fs4.default.existsSync(markerPath(strapiAppDir));
|
|
854
1233
|
let done = null;
|
|
855
1234
|
try {
|
|
856
1235
|
const dp = donePath(strapiAppDir);
|
|
857
|
-
if (
|
|
1236
|
+
if (import_node_fs4.default.existsSync(dp)) done = JSON.parse(import_node_fs4.default.readFileSync(dp, "utf8"));
|
|
858
1237
|
} catch {
|
|
859
1238
|
}
|
|
860
1239
|
return { pending, done };
|
|
@@ -890,10 +1269,10 @@ function stageProvision(strapi, input) {
|
|
|
890
1269
|
if (!input.dryRun) {
|
|
891
1270
|
try {
|
|
892
1271
|
const mp = markerPath(input.strapiAppDir);
|
|
893
|
-
|
|
894
|
-
|
|
1272
|
+
import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(mp), { recursive: true });
|
|
1273
|
+
import_node_fs4.default.writeFileSync(mp, JSON.stringify(marker, null, 2), "utf8");
|
|
895
1274
|
try {
|
|
896
|
-
|
|
1275
|
+
import_node_fs4.default.unlinkSync(donePath(input.strapiAppDir));
|
|
897
1276
|
} catch {
|
|
898
1277
|
}
|
|
899
1278
|
result.staged = true;
|
|
@@ -909,10 +1288,10 @@ function stageProvision(strapi, input) {
|
|
|
909
1288
|
async function runPendingProvision(strapi, strapiAppDir) {
|
|
910
1289
|
const result = { ran: false, errors: [] };
|
|
911
1290
|
const mp = markerPath(strapiAppDir);
|
|
912
|
-
if (!
|
|
1291
|
+
if (!import_node_fs4.default.existsSync(mp)) return result;
|
|
913
1292
|
let marker;
|
|
914
1293
|
try {
|
|
915
|
-
marker = JSON.parse(
|
|
1294
|
+
marker = JSON.parse(import_node_fs4.default.readFileSync(mp, "utf8"));
|
|
916
1295
|
} catch (e) {
|
|
917
1296
|
result.errors.push(`marcador ileg\xEDvel: ${e?.message ?? e}`);
|
|
918
1297
|
return result;
|
|
@@ -941,8 +1320,7 @@ async function runPendingProvision(strapi, strapiAppDir) {
|
|
|
941
1320
|
result.errors.push(`link: ${e?.message ?? e}`);
|
|
942
1321
|
}
|
|
943
1322
|
try {
|
|
944
|
-
const
|
|
945
|
-
const previewUrl = marker.context.frontendUrl || `http://localhost:${adapter.defaultPort}`;
|
|
1323
|
+
const previewUrl = marker.context.frontendUrl || `http://localhost:${FRONTEND_BASE_PORT}`;
|
|
946
1324
|
const done = {
|
|
947
1325
|
name: marker.manifest.name,
|
|
948
1326
|
framework: marker.manifest.framework,
|
|
@@ -954,21 +1332,21 @@ async function runPendingProvision(strapi, strapiAppDir) {
|
|
|
954
1332
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
955
1333
|
};
|
|
956
1334
|
const dp = donePath(strapiAppDir);
|
|
957
|
-
|
|
958
|
-
|
|
1335
|
+
import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(dp), { recursive: true });
|
|
1336
|
+
import_node_fs4.default.writeFileSync(dp, JSON.stringify(done, null, 2), "utf8");
|
|
959
1337
|
} catch (e) {
|
|
960
1338
|
result.errors.push(`resumo: ${e?.message ?? e}`);
|
|
961
1339
|
}
|
|
962
1340
|
try {
|
|
963
|
-
|
|
1341
|
+
import_node_fs4.default.unlinkSync(mp);
|
|
964
1342
|
} catch {
|
|
965
1343
|
}
|
|
966
1344
|
return result;
|
|
967
1345
|
}
|
|
968
1346
|
|
|
969
1347
|
// server/src/provision/infer.ts
|
|
970
|
-
var
|
|
971
|
-
var
|
|
1348
|
+
var import_node_fs5 = __toESM(require("node:fs"));
|
|
1349
|
+
var import_node_path5 = __toESM(require("node:path"));
|
|
972
1350
|
var OPENAI_URL = "https://api.openai.com/v1/chat/completions";
|
|
973
1351
|
var MODEL = process.env.OPENAI_CHAT_MODEL || "gpt-4o";
|
|
974
1352
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
@@ -1001,38 +1379,53 @@ function score(rel) {
|
|
|
1001
1379
|
function walk(dir, base, out) {
|
|
1002
1380
|
let entries;
|
|
1003
1381
|
try {
|
|
1004
|
-
entries =
|
|
1382
|
+
entries = import_node_fs5.default.readdirSync(dir, { withFileTypes: true });
|
|
1005
1383
|
} catch {
|
|
1006
1384
|
return;
|
|
1007
1385
|
}
|
|
1008
1386
|
for (const e of entries) {
|
|
1009
1387
|
if (e.name.startsWith(".") && e.name !== ".") continue;
|
|
1010
|
-
const full =
|
|
1388
|
+
const full = import_node_path5.default.join(dir, e.name);
|
|
1011
1389
|
if (e.isDirectory()) {
|
|
1012
1390
|
if (SKIP_DIRS.has(e.name)) continue;
|
|
1013
1391
|
walk(full, base, out);
|
|
1014
|
-
} else if (CODE_EXT.has(
|
|
1015
|
-
out.push(
|
|
1392
|
+
} else if (CODE_EXT.has(import_node_path5.default.extname(e.name))) {
|
|
1393
|
+
out.push(import_node_path5.default.relative(base, full));
|
|
1016
1394
|
}
|
|
1017
1395
|
}
|
|
1018
1396
|
}
|
|
1397
|
+
function hasInlineDataArray(content) {
|
|
1398
|
+
return /(?:export\s+)?const\s+\w+\s*(?::[^=\n]+)?=\s*\[\s*\{/.test(content);
|
|
1399
|
+
}
|
|
1019
1400
|
function collectFiles(frontendDir) {
|
|
1020
1401
|
const all = [];
|
|
1021
1402
|
walk(frontendDir, frontendDir, all);
|
|
1022
1403
|
const tree = all.slice().sort();
|
|
1023
|
-
const
|
|
1024
|
-
const
|
|
1025
|
-
|
|
1026
|
-
for (const { rel } of ranked) {
|
|
1027
|
-
if (files.length >= MAX_FILES || total >= MAX_TOTAL_CHARS) break;
|
|
1404
|
+
const scored = [];
|
|
1405
|
+
for (const rel of all) {
|
|
1406
|
+
let content;
|
|
1028
1407
|
try {
|
|
1029
|
-
|
|
1030
|
-
if (!/export\s+(const|default|type|interface)/.test(content)) continue;
|
|
1031
|
-
if (content.length > MAX_FILE_CHARS) content = content.slice(0, MAX_FILE_CHARS) + "\n/* \u2026truncado\u2026 */";
|
|
1032
|
-
files.push({ rel, content });
|
|
1033
|
-
total += content.length;
|
|
1408
|
+
content = import_node_fs5.default.readFileSync(import_node_path5.default.join(frontendDir, rel), "utf8");
|
|
1034
1409
|
} catch {
|
|
1410
|
+
continue;
|
|
1035
1411
|
}
|
|
1412
|
+
const dataArray = hasInlineDataArray(content);
|
|
1413
|
+
const hasExport = /export\s+(const|default|type|interface)/.test(content);
|
|
1414
|
+
if (!hasExport && !dataArray) continue;
|
|
1415
|
+
let s = score(rel);
|
|
1416
|
+
if (dataArray) s += 8;
|
|
1417
|
+
if (s <= 0) continue;
|
|
1418
|
+
scored.push({ rel, content, s });
|
|
1419
|
+
}
|
|
1420
|
+
scored.sort((a, b) => b.s - a.s);
|
|
1421
|
+
const files = [];
|
|
1422
|
+
let total = 0;
|
|
1423
|
+
for (const it of scored) {
|
|
1424
|
+
if (files.length >= MAX_FILES || total >= MAX_TOTAL_CHARS) break;
|
|
1425
|
+
let content = it.content;
|
|
1426
|
+
if (content.length > MAX_FILE_CHARS) content = content.slice(0, MAX_FILE_CHARS) + "\n/* \u2026truncado\u2026 */";
|
|
1427
|
+
files.push({ rel: it.rel, content });
|
|
1428
|
+
total += content.length;
|
|
1036
1429
|
}
|
|
1037
1430
|
return { files, tree };
|
|
1038
1431
|
}
|
|
@@ -1067,7 +1460,7 @@ function collectPageTexts(frontendDir) {
|
|
|
1067
1460
|
for (const rel of ranked) {
|
|
1068
1461
|
if (out.length >= MAX_TEXT_FILES) break;
|
|
1069
1462
|
try {
|
|
1070
|
-
const texts = extractTexts(
|
|
1463
|
+
const texts = extractTexts(import_node_fs5.default.readFileSync(import_node_path5.default.join(frontendDir, rel), "utf8"));
|
|
1071
1464
|
if (texts.length) out.push({ rel, texts });
|
|
1072
1465
|
} catch {
|
|
1073
1466
|
}
|
|
@@ -1155,9 +1548,9 @@ function buildPageContentTypes(pageTexts, budget) {
|
|
|
1155
1548
|
}
|
|
1156
1549
|
return { contentTypes, seed };
|
|
1157
1550
|
}
|
|
1158
|
-
function
|
|
1551
|
+
function detectFramework2(frontendDir) {
|
|
1159
1552
|
try {
|
|
1160
|
-
const pkg = JSON.parse(
|
|
1553
|
+
const pkg = JSON.parse(import_node_fs5.default.readFileSync(import_node_path5.default.join(frontendDir, "package.json"), "utf8"));
|
|
1161
1554
|
const deps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
|
|
1162
1555
|
if (deps.next) return "next";
|
|
1163
1556
|
if (deps["@tanstack/react-start"]) return "tanstack";
|
|
@@ -1201,13 +1594,15 @@ Gere um JSON "strapi.manifest.json" com ESTE formato:
|
|
|
1201
1594
|
|
|
1202
1595
|
REGRAS:
|
|
1203
1596
|
- Crie uma content-type para cada COLE\xC7\xC3O de dados (arrays de objetos). Use os MESMOS nomes de campo do c\xF3digo.
|
|
1597
|
+
- IMPORTANTE: muitos frontends guardam os dados em arrays declarados INLINE dentro de componentes (.tsx/.jsx), ex.: \`const services = [{ name, price, desc }]\`, \`const reviews = [...]\`, \`const hours = [...]\`. TRATE esses arrays como cole\xE7\xF5es e modele cada um como um collectionType, mesmo que estejam dentro de um componente de UI.
|
|
1598
|
+
- Ao modelar um array desses, inclua SOMENTE os campos que s\xE3o dados/texto (ex.: name, price, desc, label, href, value). IGNORE props que s\xE3o c\xF3digo/apresenta\xE7\xE3o: componentes de \xEDcone (ex.: \`icon: Scissors\`), elementos React, fun\xE7\xF5es, classes CSS, imports de imagem.
|
|
1204
1599
|
- Dados de "configura\xE7\xE3o do site" (objeto \xFAnico: nome, telefone, etc.) \u2192 singleType.
|
|
1205
1600
|
- Campos string longos/descri\xE7\xF5es \u2192 "text" ou "richtext". Listas de strings \u2192 "json".
|
|
1206
1601
|
- Use "date"/"datetime" SOMENTE para datas ISO completas (YYYY-MM-DD). Datas parciais como "2025-04" ou textos livres \u2192 use "string" (sen\xE3o o seed falha).
|
|
1207
1602
|
- Imagens (imports de assets ou caminhos) \u2192 "media" (N\xC3O coloque o valor da imagem no seed; omita o campo no seed).
|
|
1208
|
-
- Em "seed",
|
|
1603
|
+
- Em "seed", copie o conte\xFAdo REAL hardcoded no c\xF3digo, VERBATIM (exatamente como est\xE1, sem reescrever, traduzir ou inventar), omitindo campos de m\xEDdia e rela\xE7\xF5es. Todo valor de seed TEM que existir literalmente no c\xF3digo fornecido.
|
|
1209
1604
|
- Foque APENAS em cole\xE7\xF5es/objetos de dados \u2014 N\xC3O precisa modelar textos soltos de UI (isso \xE9 tratado \xE0 parte).
|
|
1210
|
-
- N\xC3O invente. singularName kebab-case, sem repetir. Rela\xE7\xF5es s\xF3 apontam para types definidos por voc\xEA.
|
|
1605
|
+
- N\xC3O invente NADA. Se n\xE3o tiver certeza de um valor, omita-o. singularName kebab-case, sem repetir. Rela\xE7\xF5es s\xF3 apontam para types definidos por voc\xEA.
|
|
1211
1606
|
- Se n\xE3o houver cole\xE7\xF5es de dados, devolva contentTypes: [] e seed: [].
|
|
1212
1607
|
- Responda APENAS com o JSON, nada de markdown.
|
|
1213
1608
|
|
|
@@ -1218,7 +1613,7 @@ Arquivos de dados (cole\xE7\xF5es):
|
|
|
1218
1613
|
${filesBlock}`;
|
|
1219
1614
|
}
|
|
1220
1615
|
async function inferManifest(strapi, frontendDir, opts) {
|
|
1221
|
-
const framework =
|
|
1616
|
+
const framework = detectFramework2(frontendDir);
|
|
1222
1617
|
const result = {
|
|
1223
1618
|
ok: false,
|
|
1224
1619
|
inferred: true,
|
|
@@ -1227,10 +1622,10 @@ async function inferManifest(strapi, frontendDir, opts) {
|
|
|
1227
1622
|
warnings: [],
|
|
1228
1623
|
errors: []
|
|
1229
1624
|
};
|
|
1230
|
-
const existing =
|
|
1231
|
-
if (
|
|
1625
|
+
const existing = import_node_path5.default.join(frontendDir, "strapi.manifest.json");
|
|
1626
|
+
if (import_node_fs5.default.existsSync(existing)) {
|
|
1232
1627
|
try {
|
|
1233
|
-
const raw = JSON.parse(
|
|
1628
|
+
const raw = JSON.parse(import_node_fs5.default.readFileSync(existing, "utf8"));
|
|
1234
1629
|
const v2 = validateManifest(raw);
|
|
1235
1630
|
result.inferred = false;
|
|
1236
1631
|
result.rawManifest = raw;
|
|
@@ -1313,6 +1708,48 @@ async function inferManifest(strapi, frontendDir, opts) {
|
|
|
1313
1708
|
} else if (!apiKey) {
|
|
1314
1709
|
result.warnings.push("Sem OPENAI_API_KEY: modelando os TEXTOS (determin\xEDstico); cole\xE7\xF5es de dados n\xE3o inferidas.");
|
|
1315
1710
|
}
|
|
1711
|
+
if (dataCts.length && dataSeed.length) {
|
|
1712
|
+
const parts = [];
|
|
1713
|
+
for (const f of collected.files) {
|
|
1714
|
+
try {
|
|
1715
|
+
parts.push(import_node_fs5.default.readFileSync(import_node_path5.default.join(frontendDir, f.rel), "utf8"));
|
|
1716
|
+
} catch {
|
|
1717
|
+
parts.push(f.content);
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
const norm = (x) => String(x).toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
1721
|
+
const hay = norm(parts.join("\n"));
|
|
1722
|
+
const present = (v2) => {
|
|
1723
|
+
if (typeof v2 !== "string") return false;
|
|
1724
|
+
const n = norm(v2);
|
|
1725
|
+
return n.length >= 4 && hay.includes(n);
|
|
1726
|
+
};
|
|
1727
|
+
const keep = /* @__PURE__ */ new Set();
|
|
1728
|
+
const verifiedSeed = [];
|
|
1729
|
+
let droppedEntries = 0;
|
|
1730
|
+
for (const grp of dataSeed) {
|
|
1731
|
+
const entries = (grp.entries ?? []).filter((e) => {
|
|
1732
|
+
const ok = Object.values(e).some(present);
|
|
1733
|
+
if (!ok) droppedEntries++;
|
|
1734
|
+
return ok;
|
|
1735
|
+
});
|
|
1736
|
+
if (entries.length) {
|
|
1737
|
+
verifiedSeed.push({ ...grp, entries });
|
|
1738
|
+
keep.add(grp.singularName);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
const verifiedCts = dataCts.filter(
|
|
1742
|
+
(ct) => ct.kind === "singleType" || keep.has(ct.singularName)
|
|
1743
|
+
);
|
|
1744
|
+
const droppedCts = dataCts.length - verifiedCts.length;
|
|
1745
|
+
if (droppedEntries || droppedCts) {
|
|
1746
|
+
result.warnings.push(
|
|
1747
|
+
`Anti-alucina\xE7\xE3o: descartei ${droppedEntries} entrada(s) e ${droppedCts} content-type(s) cujos valores n\xE3o batiam com o c\xF3digo.`
|
|
1748
|
+
);
|
|
1749
|
+
}
|
|
1750
|
+
dataCts = verifiedCts;
|
|
1751
|
+
dataSeed = verifiedSeed;
|
|
1752
|
+
}
|
|
1316
1753
|
const budget = 60 - dataCts.length - 1;
|
|
1317
1754
|
const page = buildPageContentTypes(pageTexts, budget);
|
|
1318
1755
|
const finalManifest = {
|
|
@@ -1341,132 +1778,6 @@ async function inferManifest(strapi, frontendDir, opts) {
|
|
|
1341
1778
|
return result;
|
|
1342
1779
|
}
|
|
1343
1780
|
|
|
1344
|
-
// server/src/provision/runner.ts
|
|
1345
|
-
var import_node_child_process = require("node:child_process");
|
|
1346
|
-
var import_node_net = __toESM(require("node:net"));
|
|
1347
|
-
var import_node_fs5 = __toESM(require("node:fs"));
|
|
1348
|
-
var import_node_path5 = __toESM(require("node:path"));
|
|
1349
|
-
var info = { state: "idle", dir: null, url: null, pm: null, error: null, log: [] };
|
|
1350
|
-
var child = null;
|
|
1351
|
-
var pollTimer = null;
|
|
1352
|
-
function detectPM(dir) {
|
|
1353
|
-
if (import_node_fs5.default.existsSync(import_node_path5.default.join(dir, "bun.lockb")) || import_node_fs5.default.existsSync(import_node_path5.default.join(dir, "bun.lock"))) return "bun";
|
|
1354
|
-
if (import_node_fs5.default.existsSync(import_node_path5.default.join(dir, "pnpm-lock.yaml"))) return "pnpm";
|
|
1355
|
-
if (import_node_fs5.default.existsSync(import_node_path5.default.join(dir, "yarn.lock"))) return "yarn";
|
|
1356
|
-
return "npm";
|
|
1357
|
-
}
|
|
1358
|
-
var has = (dir, ...names) => names.some((n) => import_node_fs5.default.existsSync(import_node_path5.default.join(dir, n)));
|
|
1359
|
-
function detectFramework2(dir) {
|
|
1360
|
-
if (has(dir, "next.config.js", "next.config.ts", "next.config.mjs")) return "next";
|
|
1361
|
-
if (has(dir, "vite.config.js", "vite.config.ts", "vite.config.mjs")) return "vite";
|
|
1362
|
-
return "other";
|
|
1363
|
-
}
|
|
1364
|
-
var FRONTEND_BASE_PORT = 4321;
|
|
1365
|
-
function findFreePort(start) {
|
|
1366
|
-
return new Promise((resolve) => {
|
|
1367
|
-
const tryPort = (p) => {
|
|
1368
|
-
if (p > start + 200) return resolve(start);
|
|
1369
|
-
const srv = import_node_net.default.createServer();
|
|
1370
|
-
srv.once("error", () => tryPort(p + 1));
|
|
1371
|
-
srv.once("listening", () => srv.close(() => resolve(p)));
|
|
1372
|
-
srv.listen(p, "0.0.0.0");
|
|
1373
|
-
};
|
|
1374
|
-
tryPort(start);
|
|
1375
|
-
});
|
|
1376
|
-
}
|
|
1377
|
-
function pushLog(s) {
|
|
1378
|
-
for (const line of String(s).split("\n")) {
|
|
1379
|
-
const t = line.trim();
|
|
1380
|
-
if (t) info.log.push(t);
|
|
1381
|
-
}
|
|
1382
|
-
if (info.log.length > 60) info.log = info.log.slice(-60);
|
|
1383
|
-
}
|
|
1384
|
-
async function urlUp(url) {
|
|
1385
|
-
try {
|
|
1386
|
-
const res = await fetch(url, { method: "GET" });
|
|
1387
|
-
return res.status >= 200 && res.status < 400;
|
|
1388
|
-
} catch {
|
|
1389
|
-
return false;
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
function getRunStatus() {
|
|
1393
|
-
return { ...info, log: info.log.slice(-15) };
|
|
1394
|
-
}
|
|
1395
|
-
function stopFrontend() {
|
|
1396
|
-
if (pollTimer) {
|
|
1397
|
-
clearInterval(pollTimer);
|
|
1398
|
-
pollTimer = null;
|
|
1399
|
-
}
|
|
1400
|
-
if (child) {
|
|
1401
|
-
try {
|
|
1402
|
-
child.kill("SIGTERM");
|
|
1403
|
-
} catch {
|
|
1404
|
-
}
|
|
1405
|
-
child = null;
|
|
1406
|
-
}
|
|
1407
|
-
if (info.state !== "error") info.state = "idle";
|
|
1408
|
-
}
|
|
1409
|
-
async function startFrontend(_strapi, opts) {
|
|
1410
|
-
const { dir } = opts;
|
|
1411
|
-
if (child && info.dir === dir && ["installing", "starting", "running"].includes(info.state)) {
|
|
1412
|
-
return getRunStatus();
|
|
1413
|
-
}
|
|
1414
|
-
stopFrontend();
|
|
1415
|
-
const pm = detectPM(dir);
|
|
1416
|
-
const framework = detectFramework2(dir);
|
|
1417
|
-
const port = await findFreePort(FRONTEND_BASE_PORT);
|
|
1418
|
-
const url = `http://127.0.0.1:${port}`;
|
|
1419
|
-
info = { state: "installing", dir, url, pm, error: null, log: [] };
|
|
1420
|
-
const spawnIn = (cmd, args) => (0, import_node_child_process.spawn)(cmd, args, { cwd: dir, env: { ...process.env }, stdio: ["ignore", "pipe", "pipe"] });
|
|
1421
|
-
const fwArgs = framework === "next" ? ["-H", "127.0.0.1", "-p", String(port)] : framework === "vite" ? ["--host", "127.0.0.1", "--port", String(port), "--strictPort"] : ["--port", String(port)];
|
|
1422
|
-
const devArgs = pm === "yarn" ? ["dev", ...fwArgs] : ["run", "dev", "--", ...fwArgs];
|
|
1423
|
-
const startDev = () => {
|
|
1424
|
-
info.state = "starting";
|
|
1425
|
-
child = spawnIn(pm, devArgs);
|
|
1426
|
-
child.stdout?.on("data", (d) => pushLog(d));
|
|
1427
|
-
child.stderr?.on("data", (d) => pushLog(d));
|
|
1428
|
-
child.on("exit", (code) => {
|
|
1429
|
-
child = null;
|
|
1430
|
-
if (pollTimer) {
|
|
1431
|
-
clearInterval(pollTimer);
|
|
1432
|
-
pollTimer = null;
|
|
1433
|
-
}
|
|
1434
|
-
if (info.state === "running") info.state = "idle";
|
|
1435
|
-
else {
|
|
1436
|
-
info.state = "error";
|
|
1437
|
-
info.error = `dev encerrou (c\xF3digo ${code}). Veja o log.`;
|
|
1438
|
-
}
|
|
1439
|
-
});
|
|
1440
|
-
pollTimer = setInterval(async () => {
|
|
1441
|
-
if (await urlUp(url)) {
|
|
1442
|
-
info.state = "running";
|
|
1443
|
-
if (pollTimer) {
|
|
1444
|
-
clearInterval(pollTimer);
|
|
1445
|
-
pollTimer = null;
|
|
1446
|
-
}
|
|
1447
|
-
}
|
|
1448
|
-
}, 1500);
|
|
1449
|
-
};
|
|
1450
|
-
const needInstall = !import_node_fs5.default.existsSync(import_node_path5.default.join(dir, "node_modules"));
|
|
1451
|
-
if (needInstall) {
|
|
1452
|
-
pushLog(`Instalando depend\xEAncias com ${pm}\u2026`);
|
|
1453
|
-
const installArgs = pm === "npm" ? ["install", "--no-audit", "--no-fund"] : ["install"];
|
|
1454
|
-
const inst = spawnIn(pm, installArgs);
|
|
1455
|
-
inst.stdout?.on("data", (d) => pushLog(d));
|
|
1456
|
-
inst.stderr?.on("data", (d) => pushLog(d));
|
|
1457
|
-
inst.on("exit", (code) => {
|
|
1458
|
-
if (code === 0) startDev();
|
|
1459
|
-
else {
|
|
1460
|
-
info.state = "error";
|
|
1461
|
-
info.error = `instala\xE7\xE3o falhou (c\xF3digo ${code}). Veja o log.`;
|
|
1462
|
-
}
|
|
1463
|
-
});
|
|
1464
|
-
} else {
|
|
1465
|
-
startDev();
|
|
1466
|
-
}
|
|
1467
|
-
return getRunStatus();
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
1781
|
// server/src/provision/integrate.ts
|
|
1471
1782
|
var import_node_fs6 = __toESM(require("node:fs"));
|
|
1472
1783
|
var import_node_path6 = __toESM(require("node:path"));
|
|
@@ -2088,8 +2399,11 @@ async function integrateFrontend(strapi, opts) {
|
|
|
2088
2399
|
// server/src/controllers/frontend.ts
|
|
2089
2400
|
var MANIFEST_NAME = "strapi.manifest.json";
|
|
2090
2401
|
function ensureInside2(base, target) {
|
|
2091
|
-
const
|
|
2092
|
-
|
|
2402
|
+
const b = import_node_path7.default.resolve(base);
|
|
2403
|
+
const t = import_node_path7.default.resolve(target);
|
|
2404
|
+
if (t === b) return true;
|
|
2405
|
+
const rel = import_node_path7.default.relative(b, t);
|
|
2406
|
+
return !!rel && !rel.startsWith("..") && !import_node_path7.default.isAbsolute(rel);
|
|
2093
2407
|
}
|
|
2094
2408
|
function toKebab(input) {
|
|
2095
2409
|
const s = (input || "frontend").toLowerCase().replace(/\.zip$/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/^[^a-z]+/, "");
|
|
@@ -2311,6 +2625,15 @@ var baseHeaders = {
|
|
|
2311
2625
|
"Content-Type": "application/json",
|
|
2312
2626
|
Accept: "application/json, text/event-stream"
|
|
2313
2627
|
};
|
|
2628
|
+
var fetchT = async (url, opts, timeoutMs = 8e3) => {
|
|
2629
|
+
const ctrl = new AbortController();
|
|
2630
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
2631
|
+
try {
|
|
2632
|
+
return await fetch(url, { ...opts, signal: ctrl.signal });
|
|
2633
|
+
} finally {
|
|
2634
|
+
clearTimeout(t);
|
|
2635
|
+
}
|
|
2636
|
+
};
|
|
2314
2637
|
var parseSse = (text) => {
|
|
2315
2638
|
const dataLines = text.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trim()).filter(Boolean);
|
|
2316
2639
|
const last = dataLines[dataLines.length - 1];
|
|
@@ -2334,7 +2657,7 @@ var McpClient = class {
|
|
|
2334
2657
|
return h;
|
|
2335
2658
|
}
|
|
2336
2659
|
async init() {
|
|
2337
|
-
const res = await
|
|
2660
|
+
const res = await fetchT(this.url, {
|
|
2338
2661
|
method: "POST",
|
|
2339
2662
|
headers: this.headers(),
|
|
2340
2663
|
body: JSON.stringify({
|
|
@@ -2350,14 +2673,14 @@ var McpClient = class {
|
|
|
2350
2673
|
});
|
|
2351
2674
|
this.sessionId = res.headers.get("mcp-session-id") || void 0;
|
|
2352
2675
|
await res.text();
|
|
2353
|
-
await
|
|
2676
|
+
await fetchT(this.url, {
|
|
2354
2677
|
method: "POST",
|
|
2355
2678
|
headers: this.headers(),
|
|
2356
2679
|
body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
|
|
2357
2680
|
});
|
|
2358
2681
|
}
|
|
2359
2682
|
async rpc(method, params, id) {
|
|
2360
|
-
const res = await
|
|
2683
|
+
const res = await fetchT(this.url, {
|
|
2361
2684
|
method: "POST",
|
|
2362
2685
|
headers: this.headers(),
|
|
2363
2686
|
body: JSON.stringify({ jsonrpc: "2.0", id, method, params })
|
|
@@ -2473,16 +2796,18 @@ function createContentTools(strapi) {
|
|
|
2473
2796
|
);
|
|
2474
2797
|
const attrsOf = (uid) => strapi.contentTypes?.[uid]?.attributes || strapi.components?.[uid]?.attributes || {};
|
|
2475
2798
|
const hasDraftAndPublish = (uid) => strapi.contentTypes?.[uid]?.options?.draftAndPublish === true;
|
|
2476
|
-
const
|
|
2799
|
+
const MAX_DEPTH = 8;
|
|
2800
|
+
const buildPopulate = (attributes, seen = /* @__PURE__ */ new Set(), depth = 0) => {
|
|
2801
|
+
if (depth >= MAX_DEPTH) return {};
|
|
2477
2802
|
const populate = {};
|
|
2478
2803
|
for (const [name, a] of Object.entries(attributes)) {
|
|
2479
2804
|
if (a.type === "component" && a.component) {
|
|
2480
|
-
const sub = seen.has(a.component) ? {} : buildPopulate(attrsOf(a.component), new Set(seen).add(a.component));
|
|
2805
|
+
const sub = seen.has(a.component) ? {} : buildPopulate(attrsOf(a.component), new Set(seen).add(a.component), depth + 1);
|
|
2481
2806
|
populate[name] = Object.keys(sub).length ? { populate: sub } : true;
|
|
2482
2807
|
} else if (a.type === "dynamiczone") {
|
|
2483
2808
|
const on = {};
|
|
2484
2809
|
for (const comp of a.components || []) {
|
|
2485
|
-
const sub = seen.has(comp) ? {} : buildPopulate(attrsOf(comp), new Set(seen).add(comp));
|
|
2810
|
+
const sub = seen.has(comp) ? {} : buildPopulate(attrsOf(comp), new Set(seen).add(comp), depth + 1);
|
|
2486
2811
|
on[comp] = Object.keys(sub).length ? { populate: sub } : true;
|
|
2487
2812
|
}
|
|
2488
2813
|
populate[name] = { on };
|
|
@@ -2494,6 +2819,7 @@ function createContentTools(strapi) {
|
|
|
2494
2819
|
};
|
|
2495
2820
|
const walkFind = (node, attributes, basePath, needle, collect) => {
|
|
2496
2821
|
if (!node || typeof node !== "object") return;
|
|
2822
|
+
if (basePath.length > 24) return;
|
|
2497
2823
|
for (const [name, a] of Object.entries(attributes)) {
|
|
2498
2824
|
const v = node[name];
|
|
2499
2825
|
if (v == null) continue;
|
|
@@ -2518,16 +2844,19 @@ function createContentTools(strapi) {
|
|
|
2518
2844
|
}
|
|
2519
2845
|
}
|
|
2520
2846
|
};
|
|
2521
|
-
const
|
|
2847
|
+
const MAX_MATCHES = 100;
|
|
2848
|
+
const buscarTexto = async (termo, status = "draft") => {
|
|
2522
2849
|
const needle = String(termo || "").toLowerCase().trim();
|
|
2523
2850
|
if (!needle) return { erro: "termo vazio" };
|
|
2851
|
+
const st = status === "published" ? "published" : "draft";
|
|
2524
2852
|
const matches = [];
|
|
2525
2853
|
for (const ct of apiContentTypes()) {
|
|
2854
|
+
if (matches.length >= MAX_MATCHES) break;
|
|
2526
2855
|
const attributes = ct.attributes || {};
|
|
2527
2856
|
const populate = buildPopulate(attributes);
|
|
2528
2857
|
let entries = [];
|
|
2529
2858
|
try {
|
|
2530
|
-
const res = await strapi.documents(ct.uid).findMany({ status:
|
|
2859
|
+
const res = await strapi.documents(ct.uid).findMany({ status: st, populate, limit: 200 });
|
|
2531
2860
|
entries = Array.isArray(res) ? res : res ? [res] : [];
|
|
2532
2861
|
} catch {
|
|
2533
2862
|
continue;
|
|
@@ -2549,7 +2878,12 @@ function createContentTools(strapi) {
|
|
|
2549
2878
|
});
|
|
2550
2879
|
}
|
|
2551
2880
|
}
|
|
2552
|
-
|
|
2881
|
+
const truncated = matches.length > MAX_MATCHES;
|
|
2882
|
+
return {
|
|
2883
|
+
total: matches.length,
|
|
2884
|
+
resultados: matches.slice(0, MAX_MATCHES),
|
|
2885
|
+
...truncated ? { truncado: true, nota: `mostrando ${MAX_MATCHES} de ${matches.length} resultados; refine o termo` } : {}
|
|
2886
|
+
};
|
|
2553
2887
|
};
|
|
2554
2888
|
const sanitizeNode = (node, attributes) => {
|
|
2555
2889
|
if (node == null) return node;
|
|
@@ -3037,7 +3371,7 @@ If the user shares their screen, an image is attached to the last message \u2014
|
|
|
3037
3371
|
Be concise and actionable. ALWAYS answer in English.`
|
|
3038
3372
|
};
|
|
3039
3373
|
var chat_default2 = ({ strapi }) => ({
|
|
3040
|
-
async chat({ messages, image, lang = "pt", previewUrl, autoPublish = false }) {
|
|
3374
|
+
async chat({ messages, image, lang = "pt", previewUrl, previewStatus = "draft", autoPublish = false }) {
|
|
3041
3375
|
const apiKey = process.env.OPENAI_API_KEY;
|
|
3042
3376
|
if (!apiKey) {
|
|
3043
3377
|
throw new Error(
|
|
@@ -3047,7 +3381,7 @@ var chat_default2 = ({ strapi }) => ({
|
|
|
3047
3381
|
const language = lang === "en" ? "en" : "pt";
|
|
3048
3382
|
const { buscarTexto, editarCampo, publicar, listarLocales, criarLocale, traduzir } = createContentTools(strapi);
|
|
3049
3383
|
const LOCAL_TOOLS = {
|
|
3050
|
-
buscar_texto: (a) => buscarTexto(a?.termo),
|
|
3384
|
+
buscar_texto: (a) => buscarTexto(a?.termo, previewStatus),
|
|
3051
3385
|
editar_campo: (a) => editarCampo(a),
|
|
3052
3386
|
publicar: (a) => publicar(a),
|
|
3053
3387
|
listar_locales: () => listarLocales(),
|
|
@@ -3061,7 +3395,10 @@ var chat_default2 = ({ strapi }) => ({
|
|
|
3061
3395
|
if (process.env.PLAYWRIGHT_MCP_URL) {
|
|
3062
3396
|
try {
|
|
3063
3397
|
const client = new McpClient(process.env.PLAYWRIGHT_MCP_URL, "playwright");
|
|
3064
|
-
await
|
|
3398
|
+
await Promise.race([
|
|
3399
|
+
client.init(),
|
|
3400
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), 4e3))
|
|
3401
|
+
]);
|
|
3065
3402
|
const list = await client.listTools();
|
|
3066
3403
|
for (const t of list) {
|
|
3067
3404
|
if (mcpByTool[t.name]) continue;
|
|
@@ -3133,11 +3470,22 @@ PUBLISH POLICY: DRAFT MODE (auto-publish OFF). Do NOT call publicar unless the u
|
|
|
3133
3470
|
}
|
|
3134
3471
|
});
|
|
3135
3472
|
const callOpenAI = async (body) => {
|
|
3136
|
-
const
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3473
|
+
const ctrl = new AbortController();
|
|
3474
|
+
const timer = setTimeout(() => ctrl.abort(), 6e4);
|
|
3475
|
+
let res;
|
|
3476
|
+
try {
|
|
3477
|
+
res = await fetch(OPENAI_URL4, {
|
|
3478
|
+
method: "POST",
|
|
3479
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
3480
|
+
body: JSON.stringify(body),
|
|
3481
|
+
signal: ctrl.signal
|
|
3482
|
+
});
|
|
3483
|
+
} catch (e) {
|
|
3484
|
+
if (e?.name === "AbortError") throw new Error("OpenAI chat: tempo limite excedido (60s).");
|
|
3485
|
+
throw e;
|
|
3486
|
+
} finally {
|
|
3487
|
+
clearTimeout(timer);
|
|
3488
|
+
}
|
|
3141
3489
|
if (!res.ok) throw new Error(`OpenAI chat: ${await res.text()}`);
|
|
3142
3490
|
return res.json();
|
|
3143
3491
|
};
|
|
@@ -3176,6 +3524,11 @@ PUBLISH POLICY: DRAFT MODE (auto-publish OFF). Do NOT call publicar unless the u
|
|
|
3176
3524
|
} catch (e) {
|
|
3177
3525
|
content = `Erro ao chamar a tool ${name}: ${e?.message || e}`;
|
|
3178
3526
|
}
|
|
3527
|
+
const MAX_TOOL_CHARS = 12e3;
|
|
3528
|
+
if (content.length > MAX_TOOL_CHARS) {
|
|
3529
|
+
content = content.slice(0, MAX_TOOL_CHARS) + `
|
|
3530
|
+
\u2026[resultado truncado: ${content.length} chars]`;
|
|
3531
|
+
}
|
|
3179
3532
|
convo.push({ role: "tool", tool_call_id: call.id, content });
|
|
3180
3533
|
}
|
|
3181
3534
|
continue;
|
|
@@ -3299,196 +3652,166 @@ var routes_default = {
|
|
|
3299
3652
|
|
|
3300
3653
|
// server/src/mcp/tools/buscar-texto.ts
|
|
3301
3654
|
var import_utils2 = require("@strapi/utils");
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3655
|
+
|
|
3656
|
+
// server/src/mcp/define.ts
|
|
3657
|
+
var defineTool = (def) => def;
|
|
3658
|
+
|
|
3659
|
+
// server/src/mcp/tools/buscar-texto.ts
|
|
3660
|
+
var buscar_texto_default = defineTool({
|
|
3661
|
+
name: "mcp_chat_buscar_texto",
|
|
3662
|
+
title: "Search text across content (deep)",
|
|
3663
|
+
description: 'Search a phrase across ALL content-types, single types, components and dynamic zones (recursive, substring). Returns matches with a `path` (e.g. ["dynamic_zone",2,"heading"]) to pass to mcp_chat_editar_campo.',
|
|
3664
|
+
resolveInputSchema: () => import_utils2.z.object({ termo: import_utils2.z.string() }),
|
|
3665
|
+
resolveOutputSchema: () => import_utils2.z.object({
|
|
3666
|
+
total: import_utils2.z.number().optional(),
|
|
3667
|
+
resultados: import_utils2.z.array(import_utils2.z.any()).optional(),
|
|
3668
|
+
erro: import_utils2.z.string().optional()
|
|
3669
|
+
}),
|
|
3670
|
+
auth: { policies: [{ action: "plugin::content-manager.explorer.read" }] },
|
|
3671
|
+
createHandler: (strapi) => async ({ args }) => {
|
|
3672
|
+
const r = await createContentTools(strapi).buscarTexto(args.termo);
|
|
3673
|
+
return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
|
|
3320
3674
|
}
|
|
3321
|
-
};
|
|
3322
|
-
var buscar_texto_default = tool;
|
|
3675
|
+
});
|
|
3323
3676
|
|
|
3324
3677
|
// server/src/mcp/tools/editar-campo.ts
|
|
3325
3678
|
var import_utils3 = require("@strapi/utils");
|
|
3326
|
-
var
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
const r = await createContentTools(strapi).editarCampo(args);
|
|
3351
|
-
return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
|
|
3352
|
-
}
|
|
3353
|
-
});
|
|
3679
|
+
var editar_campo_default = defineTool({
|
|
3680
|
+
name: "mcp_chat_editar_campo",
|
|
3681
|
+
title: "Edit a (possibly nested) field",
|
|
3682
|
+
description: "Edit a field value (saved as draft), including text nested in components/dynamic zones. Pass the `path` exactly as returned by mcp_chat_buscar_texto; for a simple top-level field you may use `campo`.",
|
|
3683
|
+
resolveInputSchema: () => import_utils3.z.object({
|
|
3684
|
+
uid: import_utils3.z.string(),
|
|
3685
|
+
documentId: import_utils3.z.string(),
|
|
3686
|
+
path: import_utils3.z.array(import_utils3.z.union([import_utils3.z.string(), import_utils3.z.number()])).optional(),
|
|
3687
|
+
campo: import_utils3.z.string().optional(),
|
|
3688
|
+
novo_valor: import_utils3.z.string(),
|
|
3689
|
+
locale: import_utils3.z.string().optional()
|
|
3690
|
+
}),
|
|
3691
|
+
resolveOutputSchema: () => import_utils3.z.object({
|
|
3692
|
+
ok: import_utils3.z.boolean().optional(),
|
|
3693
|
+
uid: import_utils3.z.string().optional(),
|
|
3694
|
+
documentId: import_utils3.z.string().optional(),
|
|
3695
|
+
path: import_utils3.z.array(import_utils3.z.any()).optional(),
|
|
3696
|
+
novo_valor: import_utils3.z.string().optional(),
|
|
3697
|
+
erro: import_utils3.z.string().optional()
|
|
3698
|
+
}),
|
|
3699
|
+
auth: { policies: [{ action: "plugin::content-manager.explorer.update" }] },
|
|
3700
|
+
createHandler: (strapi) => async ({ args }) => {
|
|
3701
|
+
const r = await createContentTools(strapi).editarCampo(args);
|
|
3702
|
+
return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
|
|
3354
3703
|
}
|
|
3355
|
-
};
|
|
3356
|
-
var editar_campo_default = tool2;
|
|
3704
|
+
});
|
|
3357
3705
|
|
|
3358
3706
|
// server/src/mcp/tools/publicar.ts
|
|
3359
3707
|
var import_utils4 = require("@strapi/utils");
|
|
3360
|
-
var
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
const r = await createContentTools(strapi).publicar(args);
|
|
3377
|
-
return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
|
|
3378
|
-
}
|
|
3379
|
-
});
|
|
3708
|
+
var publicar_default = defineTool({
|
|
3709
|
+
name: "mcp_chat_publicar",
|
|
3710
|
+
title: "Publish an entry",
|
|
3711
|
+
description: 'Publish an entry by uid + documentId, making the change visible on the site. Pass `locale` to publish a specific language, or "*" for all. For content-types without Draft & Publish there is nothing to publish (returns status "no-draft-publish") \u2014 the edit is already live.',
|
|
3712
|
+
resolveInputSchema: () => import_utils4.z.object({ uid: import_utils4.z.string(), documentId: import_utils4.z.string(), locale: import_utils4.z.string().optional() }),
|
|
3713
|
+
resolveOutputSchema: () => import_utils4.z.object({
|
|
3714
|
+
ok: import_utils4.z.boolean().optional(),
|
|
3715
|
+
uid: import_utils4.z.string().optional(),
|
|
3716
|
+
documentId: import_utils4.z.string().optional(),
|
|
3717
|
+
status: import_utils4.z.string().optional(),
|
|
3718
|
+
locale: import_utils4.z.string().optional()
|
|
3719
|
+
}),
|
|
3720
|
+
auth: { policies: [{ action: "plugin::content-manager.explorer.publish" }] },
|
|
3721
|
+
createHandler: (strapi) => async ({ args }) => {
|
|
3722
|
+
const r = await createContentTools(strapi).publicar(args);
|
|
3723
|
+
return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
|
|
3380
3724
|
}
|
|
3381
|
-
};
|
|
3382
|
-
var publicar_default = tool3;
|
|
3725
|
+
});
|
|
3383
3726
|
|
|
3384
3727
|
// server/src/mcp/tools/listar-locales.ts
|
|
3385
3728
|
var import_utils5 = require("@strapi/utils");
|
|
3386
|
-
var
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
const r = await createContentTools(strapi).listarLocales();
|
|
3401
|
-
return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
|
|
3402
|
-
}
|
|
3403
|
-
});
|
|
3729
|
+
var listar_locales_default = defineTool({
|
|
3730
|
+
name: "mcp_chat_listar_locales",
|
|
3731
|
+
title: "List i18n locales",
|
|
3732
|
+
description: "List the configured locales (languages) and which one is the default.",
|
|
3733
|
+
resolveInputSchema: () => import_utils5.z.object({}),
|
|
3734
|
+
resolveOutputSchema: () => import_utils5.z.object({
|
|
3735
|
+
default: import_utils5.z.string().optional(),
|
|
3736
|
+
locales: import_utils5.z.array(import_utils5.z.any()).optional(),
|
|
3737
|
+
erro: import_utils5.z.string().optional()
|
|
3738
|
+
}),
|
|
3739
|
+
auth: { policies: [{ action: "plugin::content-manager.explorer.read" }] },
|
|
3740
|
+
createHandler: (strapi) => async () => {
|
|
3741
|
+
const r = await createContentTools(strapi).listarLocales();
|
|
3742
|
+
return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
|
|
3404
3743
|
}
|
|
3405
|
-
};
|
|
3406
|
-
var listar_locales_default = tool4;
|
|
3744
|
+
});
|
|
3407
3745
|
|
|
3408
3746
|
// server/src/mcp/tools/criar-locale.ts
|
|
3409
3747
|
var import_utils6 = require("@strapi/utils");
|
|
3410
|
-
var
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
const r = await createContentTools(strapi).criarLocale(args);
|
|
3427
|
-
return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
|
|
3428
|
-
}
|
|
3429
|
-
});
|
|
3748
|
+
var criar_locale_default = defineTool({
|
|
3749
|
+
name: "mcp_chat_criar_locale",
|
|
3750
|
+
title: "Create an i18n locale",
|
|
3751
|
+
description: 'Create a locale (language). `code` must be a valid ISO code (e.g. "pt-BR", "es"). Idempotent: returns ok if it already exists.',
|
|
3752
|
+
resolveInputSchema: () => import_utils6.z.object({ code: import_utils6.z.string(), name: import_utils6.z.string().optional() }),
|
|
3753
|
+
resolveOutputSchema: () => import_utils6.z.object({
|
|
3754
|
+
ok: import_utils6.z.boolean().optional(),
|
|
3755
|
+
code: import_utils6.z.string().optional(),
|
|
3756
|
+
name: import_utils6.z.string().optional(),
|
|
3757
|
+
existed: import_utils6.z.boolean().optional(),
|
|
3758
|
+
erro: import_utils6.z.string().optional()
|
|
3759
|
+
}),
|
|
3760
|
+
auth: { policies: [{ action: "plugin::i18n.locale.create" }] },
|
|
3761
|
+
createHandler: (strapi) => async ({ args }) => {
|
|
3762
|
+
const r = await createContentTools(strapi).criarLocale(args);
|
|
3763
|
+
return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
|
|
3430
3764
|
}
|
|
3431
|
-
};
|
|
3432
|
-
var criar_locale_default = tool5;
|
|
3765
|
+
});
|
|
3433
3766
|
|
|
3434
3767
|
// server/src/mcp/tools/traduzir.ts
|
|
3435
3768
|
var import_utils7 = require("@strapi/utils");
|
|
3436
|
-
var
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
const r = await createContentTools(strapi).traduzir(args);
|
|
3458
|
-
return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
|
|
3459
|
-
}
|
|
3460
|
-
});
|
|
3769
|
+
var traduzir_default = defineTool({
|
|
3770
|
+
name: "mcp_chat_traduzir",
|
|
3771
|
+
title: "Translate localized content",
|
|
3772
|
+
description: "Translate localized content into one or more languages. Creates missing locales, translates field by field (long text is split and reassembled, never overflows) and publishes (only on content-types with Draft & Publish). Without uid/documentId, translates ALL localized content-types. Handles many locales at once.",
|
|
3773
|
+
resolveInputSchema: () => import_utils7.z.object({
|
|
3774
|
+
target_locales: import_utils7.z.array(import_utils7.z.string()).min(1),
|
|
3775
|
+
source_locale: import_utils7.z.string().optional(),
|
|
3776
|
+
uid: import_utils7.z.string().optional(),
|
|
3777
|
+
documentId: import_utils7.z.string().optional(),
|
|
3778
|
+
publish: import_utils7.z.boolean().optional()
|
|
3779
|
+
}),
|
|
3780
|
+
resolveOutputSchema: () => import_utils7.z.object({
|
|
3781
|
+
ok: import_utils7.z.boolean().optional(),
|
|
3782
|
+
source: import_utils7.z.string().optional(),
|
|
3783
|
+
por_locale: import_utils7.z.array(import_utils7.z.any()).optional(),
|
|
3784
|
+
erro: import_utils7.z.string().optional()
|
|
3785
|
+
}),
|
|
3786
|
+
auth: { policies: [{ action: "plugin::content-manager.explorer.update" }] },
|
|
3787
|
+
createHandler: (strapi) => async ({ args }) => {
|
|
3788
|
+
const r = await createContentTools(strapi).traduzir(args);
|
|
3789
|
+
return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
|
|
3461
3790
|
}
|
|
3462
|
-
};
|
|
3463
|
-
var traduzir_default = tool6;
|
|
3791
|
+
});
|
|
3464
3792
|
|
|
3465
3793
|
// server/src/mcp/tools/habilitar-i18n.ts
|
|
3466
3794
|
var import_utils8 = require("@strapi/utils");
|
|
3467
|
-
var
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
const r = enableI18n({ strapi, uid: args?.uid, campos: args?.campos });
|
|
3486
|
-
return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
|
|
3487
|
-
}
|
|
3488
|
-
});
|
|
3795
|
+
var habilitar_i18n_default = defineTool({
|
|
3796
|
+
name: "mcp_chat_habilitar_i18n",
|
|
3797
|
+
title: "Enable i18n on a content-type",
|
|
3798
|
+
description: 'Enable translation on content-types not localized yet: marks the content-type and its textual fields/components as localized. Required before translating content provisioned without i18n. Omit `uid` (or pass "*") to enable ALL content-types at once. Edits the schema (dev-only); Strapi restarts.',
|
|
3799
|
+
resolveInputSchema: () => import_utils8.z.object({ uid: import_utils8.z.string().optional(), campos: import_utils8.z.array(import_utils8.z.string()).optional() }),
|
|
3800
|
+
resolveOutputSchema: () => import_utils8.z.object({
|
|
3801
|
+
ok: import_utils8.z.boolean().optional(),
|
|
3802
|
+
uid: import_utils8.z.string().optional(),
|
|
3803
|
+
campos: import_utils8.z.array(import_utils8.z.string()).optional(),
|
|
3804
|
+
contentTypes: import_utils8.z.array(import_utils8.z.any()).optional(),
|
|
3805
|
+
total: import_utils8.z.number().optional(),
|
|
3806
|
+
restart: import_utils8.z.boolean().optional(),
|
|
3807
|
+
erro: import_utils8.z.string().optional()
|
|
3808
|
+
}),
|
|
3809
|
+
auth: { policies: [{ action: "plugin::content-type-builder.read" }] },
|
|
3810
|
+
createHandler: (strapi) => async ({ args }) => {
|
|
3811
|
+
const r = enableI18n({ strapi, uid: args.uid, campos: args.campos });
|
|
3812
|
+
return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
|
|
3489
3813
|
}
|
|
3490
|
-
};
|
|
3491
|
-
var habilitar_i18n_default = tool7;
|
|
3814
|
+
});
|
|
3492
3815
|
|
|
3493
3816
|
// server/src/mcp/tools/index.ts
|
|
3494
3817
|
var tools = [
|
|
@@ -3511,9 +3834,16 @@ var registerMcpTools = (strapi) => {
|
|
|
3511
3834
|
);
|
|
3512
3835
|
return;
|
|
3513
3836
|
}
|
|
3514
|
-
|
|
3515
|
-
for (const
|
|
3516
|
-
|
|
3837
|
+
let registered = 0;
|
|
3838
|
+
for (const tool of tools) {
|
|
3839
|
+
try {
|
|
3840
|
+
mcp.registerTool(tool);
|
|
3841
|
+
registered += 1;
|
|
3842
|
+
} catch (e) {
|
|
3843
|
+
strapi.log.warn(`[mcp-chat] tool "${tool?.name}" falhou ao registrar: ${e?.message ?? e}`);
|
|
3844
|
+
}
|
|
3845
|
+
}
|
|
3846
|
+
strapi.log.info(`[mcp-chat] ${registered}/${tools.length} tools registradas no MCP nativo (mcp_chat_*).`);
|
|
3517
3847
|
};
|
|
3518
3848
|
|
|
3519
3849
|
// server/src/register.ts
|
|
@@ -3529,6 +3859,10 @@ var register_default = ({ strapi }) => {
|
|
|
3529
3859
|
var index_default = {
|
|
3530
3860
|
register: register_default,
|
|
3531
3861
|
async bootstrap({ strapi }) {
|
|
3862
|
+
try {
|
|
3863
|
+
cleanupStaleFrontend(strapi.dirs.app.root);
|
|
3864
|
+
} catch {
|
|
3865
|
+
}
|
|
3532
3866
|
try {
|
|
3533
3867
|
const r = await runPendingProvision(strapi, strapi.dirs.app.root);
|
|
3534
3868
|
if (r.ran) {
|
|
@@ -3542,7 +3876,10 @@ var index_default = {
|
|
|
3542
3876
|
}
|
|
3543
3877
|
},
|
|
3544
3878
|
destroy() {
|
|
3545
|
-
|
|
3879
|
+
try {
|
|
3880
|
+
stopFrontend();
|
|
3881
|
+
} catch {
|
|
3882
|
+
}
|
|
3546
3883
|
},
|
|
3547
3884
|
config: {
|
|
3548
3885
|
default: {},
|