strapi-plugin-mcp-chat 0.5.0 → 0.7.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/admin/src/components/AdminOverlays.tsx +8 -18
- package/admin/src/components/FloatingChat.tsx +70 -3
- package/dist/server/index.js +943 -335
- package/package.json +1 -1
- package/server/src/content-tools.ts +4 -2
- package/server/src/controllers/chat.ts +2 -2
- package/server/src/controllers/frontend.ts +35 -0
- package/server/src/index.ts +8 -1
- package/server/src/provision/infer.ts +92 -20
- package/server/src/provision/link.ts +232 -35
- package/server/src/provision/orchestrate.ts +17 -3
- package/server/src/provision/runner.ts +44 -1
- package/server/src/provision/wire.ts +393 -0
- package/server/src/routes/index.ts +6 -0
- package/server/src/services/chat.ts +8 -2
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}`);
|
|
@@ -83,13 +83,13 @@ var audio_default = ({ strapi }) => ({
|
|
|
83
83
|
});
|
|
84
84
|
|
|
85
85
|
// server/src/controllers/frontend.ts
|
|
86
|
-
var
|
|
87
|
-
var
|
|
86
|
+
var import_node_fs8 = __toESM(require("node:fs"));
|
|
87
|
+
var import_node_path8 = __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_fs5 = __toESM(require("node:fs"));
|
|
92
|
+
var import_node_path5 = __toESM(require("node:path"));
|
|
93
93
|
|
|
94
94
|
// server/src/provision/manifest.ts
|
|
95
95
|
var import_utils = require("@strapi/utils");
|
|
@@ -267,8 +267,8 @@ function validateManifest(raw) {
|
|
|
267
267
|
const parsed = manifestSchema.safeParse(raw);
|
|
268
268
|
if (parsed.success) return { ok: true, data: parsed.data };
|
|
269
269
|
const errors = parsed.error.issues.map((i) => {
|
|
270
|
-
const
|
|
271
|
-
return `${
|
|
270
|
+
const path10 = i.path.length ? `${i.path.join(".")}: ` : "";
|
|
271
|
+
return `${path10}${i.message}`;
|
|
272
272
|
});
|
|
273
273
|
return { ok: false, errors };
|
|
274
274
|
}
|
|
@@ -633,8 +633,8 @@ async function seedContent(strapi, manifest) {
|
|
|
633
633
|
}
|
|
634
634
|
|
|
635
635
|
// server/src/provision/link.ts
|
|
636
|
-
var
|
|
637
|
-
var
|
|
636
|
+
var import_node_fs3 = __toESM(require("node:fs"));
|
|
637
|
+
var import_node_path3 = __toESM(require("node:path"));
|
|
638
638
|
|
|
639
639
|
// server/src/provision/adapters.ts
|
|
640
640
|
var nextAdapter = {
|
|
@@ -770,6 +770,179 @@ ${interfaces}
|
|
|
770
770
|
`;
|
|
771
771
|
}
|
|
772
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
|
+
|
|
773
946
|
// server/src/provision/link.ts
|
|
774
947
|
function parseEnv(content) {
|
|
775
948
|
const out = {};
|
|
@@ -799,48 +972,120 @@ function mergeEnv(existing, next) {
|
|
|
799
972
|
}
|
|
800
973
|
return { content, added, preserved };
|
|
801
974
|
}
|
|
802
|
-
|
|
975
|
+
var PREVIEW_MARKER = "mcp-chat:preview-merged";
|
|
976
|
+
var PREVIEW_MODULE = "mcp-chat-preview";
|
|
977
|
+
function buildPreviewConfig(manifest, framework = manifest.framework) {
|
|
803
978
|
const routes = {};
|
|
804
979
|
for (const ct of manifest.contentTypes) {
|
|
805
|
-
|
|
980
|
+
routes[apiUid(ct.singularName)] = ct.preview?.route ?? "/";
|
|
806
981
|
}
|
|
807
982
|
const routesJson = JSON.stringify(routes, null, 2);
|
|
808
|
-
|
|
983
|
+
const isNext2 = framework === "next";
|
|
984
|
+
const urlBranch = isNext2 ? ` // 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}).
|
|
809
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.
|
|
810
992
|
const PREVIEW_ROUTES: Record<string, string> = ${routesJson};
|
|
811
993
|
|
|
812
|
-
export default ({ env }) => ({
|
|
813
|
-
auth: {
|
|
814
|
-
secret: env('ADMIN_JWT_SECRET'),
|
|
815
|
-
},
|
|
816
|
-
apiToken: { salt: env('API_TOKEN_SALT') },
|
|
817
|
-
transfer: { token: { salt: env('TRANSFER_TOKEN_SALT') } },
|
|
994
|
+
export default ({ env }: { env: any }) => ({
|
|
818
995
|
preview: {
|
|
819
996
|
enabled: true,
|
|
820
997
|
config: {
|
|
821
998
|
allowedOrigins: [env('CLIENT_URL', 'http://localhost:3000')],
|
|
822
999
|
async handler(uid: string, { documentId, locale, status }: any) {
|
|
823
|
-
const route = PREVIEW_ROUTES[uid];
|
|
824
|
-
if (!route) return null;
|
|
825
|
-
const doc = await strapi.documents(uid as any).findOne({ documentId, locale });
|
|
826
|
-
if (!doc) return null;
|
|
827
|
-
// substitui :campo pelos valores do documento (ex.: :slug)
|
|
828
|
-
const pathname = route.replace(/:([a-zA-Z0-9_]+)/g, (_m, f) =>
|
|
829
|
-
encodeURIComponent(String((doc as any)[f] ?? ''))
|
|
830
|
-
);
|
|
1000
|
+
const route = PREVIEW_ROUTES[uid] ?? '/';
|
|
831
1001
|
const clientUrl = env('CLIENT_URL', 'http://localhost:3000');
|
|
832
1002
|
const secret = env('PREVIEW_SECRET', '');
|
|
833
|
-
|
|
834
|
-
|
|
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}
|
|
835
1015
|
},
|
|
836
1016
|
},
|
|
837
1017
|
},
|
|
838
1018
|
});
|
|
839
1019
|
`;
|
|
840
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
|
+
}
|
|
841
1086
|
function ensureInside(base, target) {
|
|
842
|
-
const n =
|
|
843
|
-
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);
|
|
844
1089
|
}
|
|
845
1090
|
function linkFrontend(manifest, opts) {
|
|
846
1091
|
const adapter = adapterForManifest(manifest);
|
|
@@ -852,50 +1097,432 @@ function linkFrontend(manifest, opts) {
|
|
|
852
1097
|
typesFile: "strapi-types.ts",
|
|
853
1098
|
previewFile: "config/admin.ts",
|
|
854
1099
|
previewAction: "skipped",
|
|
1100
|
+
backendEnvAdded: [],
|
|
1101
|
+
cspAction: "skipped",
|
|
855
1102
|
errors: []
|
|
856
1103
|
};
|
|
857
|
-
if (!
|
|
1104
|
+
if (!import_node_path3.default.isAbsolute(opts.frontendDir) || !import_node_path3.default.isAbsolute(opts.strapiAppDir)) {
|
|
858
1105
|
result.errors.push("frontendDir e strapiAppDir devem ser absolutos");
|
|
859
1106
|
return result;
|
|
860
1107
|
}
|
|
861
1108
|
try {
|
|
862
|
-
const envPath =
|
|
1109
|
+
const envPath = import_node_path3.default.join(opts.frontendDir, adapter.envFileName);
|
|
863
1110
|
if (!ensureInside(opts.frontendDir, envPath)) throw new Error("env fora do frontendDir");
|
|
864
|
-
const existing =
|
|
1111
|
+
const existing = import_node_fs3.default.existsSync(envPath) ? import_node_fs3.default.readFileSync(envPath, "utf8") : "";
|
|
865
1112
|
const vars = adapter.buildEnv(opts.context);
|
|
866
1113
|
const { content, added, preserved } = mergeEnv(existing, vars);
|
|
867
1114
|
result.envAdded = added;
|
|
868
1115
|
result.envPreserved = preserved;
|
|
869
|
-
if (!opts.dryRun && added.length)
|
|
1116
|
+
if (!opts.dryRun && added.length) import_node_fs3.default.writeFileSync(envPath, content, "utf8");
|
|
870
1117
|
} catch (e) {
|
|
871
1118
|
result.errors.push(`env: ${e?.message ?? e}`);
|
|
872
1119
|
}
|
|
873
1120
|
try {
|
|
874
|
-
const typesPath =
|
|
875
|
-
if (!ensureInside(opts.frontendDir, typesPath)) throw new Error("types fora do frontendDir");
|
|
876
|
-
if (!opts.dryRun)
|
|
1121
|
+
const typesPath = import_node_path3.default.join(opts.frontendDir, result.typesFile);
|
|
1122
|
+
if (!ensureInside(opts.frontendDir, typesPath)) throw new Error("types fora do frontendDir");
|
|
1123
|
+
if (!opts.dryRun) import_node_fs3.default.writeFileSync(typesPath, generateTypes(manifest), "utf8");
|
|
1124
|
+
} catch (e) {
|
|
1125
|
+
result.errors.push(`types: ${e?.message ?? e}`);
|
|
1126
|
+
}
|
|
1127
|
+
try {
|
|
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 {
|
|
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");
|
|
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";
|
|
1156
|
+
}
|
|
1157
|
+
} catch (e) {
|
|
1158
|
+
result.errors.push(`preview: ${e?.message ?? e}`);
|
|
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
|
+
}
|
|
1184
|
+
result.ok = result.errors.length === 0;
|
|
1185
|
+
return result;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// server/src/provision/wire.ts
|
|
1189
|
+
var import_node_fs4 = __toESM(require("node:fs"));
|
|
1190
|
+
var import_node_path4 = __toESM(require("node:path"));
|
|
1191
|
+
var OPENAI_URL = "https://api.openai.com/v1/chat/completions";
|
|
1192
|
+
var MODEL = process.env.OPENAI_CHAT_MODEL || "gpt-4o";
|
|
1193
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
1194
|
+
"node_modules",
|
|
1195
|
+
".git",
|
|
1196
|
+
"dist",
|
|
1197
|
+
".next",
|
|
1198
|
+
".output",
|
|
1199
|
+
".vinxi",
|
|
1200
|
+
".tanstack",
|
|
1201
|
+
"build",
|
|
1202
|
+
"coverage",
|
|
1203
|
+
".turbo",
|
|
1204
|
+
".cache",
|
|
1205
|
+
"public"
|
|
1206
|
+
]);
|
|
1207
|
+
function ensureInside2(base, target) {
|
|
1208
|
+
const b = import_node_path4.default.resolve(base);
|
|
1209
|
+
const t = import_node_path4.default.resolve(target);
|
|
1210
|
+
return t === b || t.startsWith(b + import_node_path4.default.sep);
|
|
1211
|
+
}
|
|
1212
|
+
function isNext(manifest) {
|
|
1213
|
+
return manifest.framework === "next";
|
|
1214
|
+
}
|
|
1215
|
+
function usesAtAlias(frontendDir) {
|
|
1216
|
+
for (const f of ["tsconfig.json", "tsconfig.app.json", "jsconfig.json"]) {
|
|
1217
|
+
try {
|
|
1218
|
+
const c = import_node_fs4.default.readFileSync(import_node_path4.default.join(frontendDir, f), "utf8");
|
|
1219
|
+
if (/"@\/\*"\s*:/.test(c)) return true;
|
|
1220
|
+
} catch {
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
return false;
|
|
1224
|
+
}
|
|
1225
|
+
function strapiClientSrc(manifest) {
|
|
1226
|
+
const envExpr = isNext(manifest) ? "(typeof process !== 'undefined' ? process.env.NEXT_PUBLIC_STRAPI_URL : undefined)" : "(import.meta as any).env?.VITE_STRAPI_URL";
|
|
1227
|
+
return `// Gerado pelo mcp-chat \u2014 camada de acesso \xE0 Strapi (REST, flat, sem nesting).
|
|
1228
|
+
// N\xE3o edite \xE0 m\xE3o: \xE9 regenerado ao religar o frontend.
|
|
1229
|
+
export const STRAPI_URL =
|
|
1230
|
+
(${envExpr} || "http://localhost:1337").replace(/\\/$/, "");
|
|
1231
|
+
|
|
1232
|
+
export type PreviewMode = { isPreview: boolean; status: "draft" | "published" };
|
|
1233
|
+
|
|
1234
|
+
/** L\xEA o modo de preview da URL (?preview=1 / ?status=draft). */
|
|
1235
|
+
export function getPreviewMode(): PreviewMode {
|
|
1236
|
+
if (typeof window === "undefined") return { isPreview: false, status: "published" };
|
|
1237
|
+
const p = new URLSearchParams(window.location.search);
|
|
1238
|
+
const status = p.get("status");
|
|
1239
|
+
const isPreview = p.get("preview") === "1" || p.has("preview") || status === "draft";
|
|
1240
|
+
return { isPreview, status: status === "draft" || isPreview ? "draft" : "published" };
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/** Busca um singleType e devolve s\xF3 os atributos (objeto). null em erro. */
|
|
1244
|
+
export async function fetchSection<T = Record<string, any>>(
|
|
1245
|
+
name: string,
|
|
1246
|
+
status: "draft" | "published" = "published"
|
|
1247
|
+
): Promise<T | null> {
|
|
1248
|
+
const qs = status === "draft" ? "?status=draft" : "";
|
|
1249
|
+
try {
|
|
1250
|
+
const res = await fetch(\`\${STRAPI_URL}/api/\${name}\${qs}\`);
|
|
1251
|
+
if (!res.ok) return null;
|
|
1252
|
+
const json = await res.json();
|
|
1253
|
+
return (json?.data ?? null) as T | null;
|
|
1254
|
+
} catch {
|
|
1255
|
+
return null;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/** Busca uma collection (lista) pelo nome plural; ordena por sortOrder se existir. */
|
|
1260
|
+
export async function fetchCollection<T = Record<string, any>>(
|
|
1261
|
+
pluralName: string,
|
|
1262
|
+
status: "draft" | "published" = "published"
|
|
1263
|
+
): Promise<T[]> {
|
|
1264
|
+
const qs = new URLSearchParams({ "sort": "sortOrder:asc", "pagination[pageSize]": "100" });
|
|
1265
|
+
if (status === "draft") qs.set("status", "draft");
|
|
1266
|
+
try {
|
|
1267
|
+
const res = await fetch(\`\${STRAPI_URL}/api/\${pluralName}?\${qs.toString()}\`);
|
|
1268
|
+
if (!res.ok) return [];
|
|
1269
|
+
const json = await res.json();
|
|
1270
|
+
return (Array.isArray(json?.data) ? json.data : []) as T[];
|
|
1271
|
+
} catch {
|
|
1272
|
+
return [];
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
`;
|
|
1276
|
+
}
|
|
1277
|
+
function hooksSrc(manifest, libImport) {
|
|
1278
|
+
const clientDirective = isNext(manifest) ? `"use client";
|
|
1279
|
+
|
|
1280
|
+
` : "";
|
|
1281
|
+
return `${clientDirective}// Gerado pelo mcp-chat \u2014 hooks de leitura da Strapi (sem depend\xEAncias al\xE9m do React).
|
|
1282
|
+
// Em modo preview faz polling curto + refetch via postMessage (reflete edi\xE7\xF5es ao vivo).
|
|
1283
|
+
import { useEffect, useState } from "react";
|
|
1284
|
+
import { fetchSection, fetchCollection, getPreviewMode } from "${libImport}";
|
|
1285
|
+
|
|
1286
|
+
export function useSection<T = Record<string, any>>(name: string): Partial<T> {
|
|
1287
|
+
const [data, setData] = useState<Partial<T>>({});
|
|
1288
|
+
useEffect(() => {
|
|
1289
|
+
let alive = true;
|
|
1290
|
+
const { isPreview, status } = getPreviewMode();
|
|
1291
|
+
const load = () => fetchSection<T>(name, status).then((d) => { if (alive && d) setData(d as Partial<T>); });
|
|
1292
|
+
load();
|
|
1293
|
+
if (!isPreview) return () => { alive = false; };
|
|
1294
|
+
const id = window.setInterval(load, 2500);
|
|
1295
|
+
const onMsg = () => load();
|
|
1296
|
+
window.addEventListener("message", onMsg);
|
|
1297
|
+
return () => { alive = false; window.clearInterval(id); window.removeEventListener("message", onMsg); };
|
|
1298
|
+
}, [name]);
|
|
1299
|
+
return data;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
export function useCollection<T = Record<string, any>>(pluralName: string): T[] {
|
|
1303
|
+
const [data, setData] = useState<T[]>([]);
|
|
1304
|
+
useEffect(() => {
|
|
1305
|
+
let alive = true;
|
|
1306
|
+
const { isPreview, status } = getPreviewMode();
|
|
1307
|
+
const load = () => fetchCollection<T>(pluralName, status).then((d) => { if (alive) setData(d); });
|
|
1308
|
+
load();
|
|
1309
|
+
if (!isPreview) return () => { alive = false; };
|
|
1310
|
+
const id = window.setInterval(load, 2500);
|
|
1311
|
+
const onMsg = () => load();
|
|
1312
|
+
window.addEventListener("message", onMsg);
|
|
1313
|
+
return () => { alive = false; window.clearInterval(id); window.removeEventListener("message", onMsg); };
|
|
1314
|
+
}, [pluralName]);
|
|
1315
|
+
return data;
|
|
1316
|
+
}
|
|
1317
|
+
`;
|
|
1318
|
+
}
|
|
1319
|
+
function writeDataLayer(frontendDir, manifest, dryRun) {
|
|
1320
|
+
const written = [];
|
|
1321
|
+
const libDir = import_node_path4.default.join(frontendDir, "src", "lib");
|
|
1322
|
+
const hooksDir = import_node_path4.default.join(frontendDir, "src", "hooks");
|
|
1323
|
+
const libFile = import_node_path4.default.join(libDir, "strapi.ts");
|
|
1324
|
+
const hooksFile = import_node_path4.default.join(hooksDir, "useStrapi.ts");
|
|
1325
|
+
if (!ensureInside2(frontendDir, libFile) || !ensureInside2(frontendDir, hooksFile)) {
|
|
1326
|
+
throw new Error("camada de dados fora do frontendDir");
|
|
1327
|
+
}
|
|
1328
|
+
const libImport = usesAtAlias(frontendDir) ? "@/lib/strapi" : "../lib/strapi";
|
|
1329
|
+
if (!dryRun) {
|
|
1330
|
+
import_node_fs4.default.mkdirSync(libDir, { recursive: true });
|
|
1331
|
+
import_node_fs4.default.mkdirSync(hooksDir, { recursive: true });
|
|
1332
|
+
import_node_fs4.default.writeFileSync(libFile, strapiClientSrc(manifest), "utf8");
|
|
1333
|
+
import_node_fs4.default.writeFileSync(hooksFile, hooksSrc(manifest, libImport), "utf8");
|
|
1334
|
+
}
|
|
1335
|
+
written.push("src/lib/strapi.ts", "src/hooks/useStrapi.ts");
|
|
1336
|
+
return written;
|
|
1337
|
+
}
|
|
1338
|
+
var CODE_EXT = /* @__PURE__ */ new Set([".tsx", ".jsx"]);
|
|
1339
|
+
var MAX_COMPONENT_CHARS = 16e3;
|
|
1340
|
+
function walkComponents(dir, base, out) {
|
|
1341
|
+
let entries;
|
|
1342
|
+
try {
|
|
1343
|
+
entries = import_node_fs4.default.readdirSync(dir, { withFileTypes: true });
|
|
1344
|
+
} catch {
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
for (const e of entries) {
|
|
1348
|
+
if (e.name.startsWith(".")) continue;
|
|
1349
|
+
const full = import_node_path4.default.join(dir, e.name);
|
|
1350
|
+
if (e.isDirectory()) {
|
|
1351
|
+
if (SKIP_DIRS.has(e.name) || e.name === "ui") continue;
|
|
1352
|
+
walkComponents(full, base, out);
|
|
1353
|
+
} else if (CODE_EXT.has(import_node_path4.default.extname(e.name))) {
|
|
1354
|
+
out.push(import_node_path4.default.relative(base, full));
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
function resolvePlural(strapi, singularName) {
|
|
1359
|
+
const real = strapi?.contentTypes?.[apiUid(singularName)]?.info?.pluralName;
|
|
1360
|
+
return real || `${singularName}s`;
|
|
1361
|
+
}
|
|
1362
|
+
function contentModelSummary(strapi, manifest) {
|
|
1363
|
+
const lines = [];
|
|
1364
|
+
for (const ct of manifest.contentTypes) {
|
|
1365
|
+
const fields = Object.keys(ct.attributes || {}).join(", ");
|
|
1366
|
+
if (ct.kind === "singleType") {
|
|
1367
|
+
lines.push(`singleType "${ct.singularName}" (useSection("${ct.singularName}")) campos: ${fields}`);
|
|
1368
|
+
} else {
|
|
1369
|
+
const plural = resolvePlural(strapi, ct.singularName);
|
|
1370
|
+
lines.push(`collection "${ct.singularName}" (useCollection("${plural}")) campos: ${fields}`);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
return lines.join("\n");
|
|
1374
|
+
}
|
|
1375
|
+
function wirePrompt(rel, source, model, hooksImport, seedSnippet) {
|
|
1376
|
+
return `Voc\xEA religa um componente React para ler o conte\xFAdo da Strapi, SEM quebrar nada.
|
|
1377
|
+
|
|
1378
|
+
MODELO DE CONTE\xDADO (use EXATAMENTE estes nomes de hook/campo):
|
|
1379
|
+
${model}
|
|
1380
|
+
|
|
1381
|
+
DADOS SEMEADOS (para casar o texto hardcoded com o campo certo):
|
|
1382
|
+
${seedSnippet}
|
|
1383
|
+
|
|
1384
|
+
REGRAS (siga \xE0 risca):
|
|
1385
|
+
- Importe os hooks de "${hooksImport}" (ex.: import { useSection, useCollection } from "${hooksImport}";).
|
|
1386
|
+
- Dentro do componente, chame os hooks necess\xE1rios (ex.: const hero = useSection("hero-section-content");).
|
|
1387
|
+
- Troque CADA texto hardcoded que casa com um campo por { obj.campo ?? "TEXTO ORIGINAL" } \u2014 SEMPRE mantenha o texto original como fallback no ?? .
|
|
1388
|
+
- Para listas (arrays hardcoded de objetos), troque o array por useCollection(...) e itere sobre ele; mantenha \xEDcones/imagens/classes/animacoes/layout EXATAMENTE como est\xE3o (n\xE3o s\xE3o conte\xFAdo).
|
|
1389
|
+
- N\xC3O altere imports de \xEDcones/assets, JSX estrutural, classes Tailwind, hooks de anima\xE7\xE3o, nada que n\xE3o seja texto/dado.
|
|
1390
|
+
- N\xC3O invente campos nem textos. Se um trecho n\xE3o casa com nenhum campo, deixe como est\xE1.
|
|
1391
|
+
- Mantenha o arquivo V\xC1LIDO e COMPLETO (TypeScript/TSX que compila). Responda com JSON: {"code":"<arquivo .tsx completo>"} e NADA al\xE9m disso.
|
|
1392
|
+
|
|
1393
|
+
ARQUIVO: ${rel}
|
|
1394
|
+
\`\`\`tsx
|
|
1395
|
+
${source}
|
|
1396
|
+
\`\`\``;
|
|
1397
|
+
}
|
|
1398
|
+
function syntaxError(code) {
|
|
1399
|
+
let esbuild;
|
|
1400
|
+
try {
|
|
1401
|
+
esbuild = require("esbuild");
|
|
1402
|
+
} catch {
|
|
1403
|
+
return null;
|
|
1404
|
+
}
|
|
1405
|
+
try {
|
|
1406
|
+
esbuild.transformSync(code, { loader: "tsx", jsx: "automatic" });
|
|
1407
|
+
return "";
|
|
1408
|
+
} catch (e) {
|
|
1409
|
+
return (e?.message || "erro de sintaxe").split("\n")[0];
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
function looksSane(original, next, hooksImport) {
|
|
1413
|
+
if (!next || next.length < 40) return "sa\xEDda vazia/curta demais";
|
|
1414
|
+
if (next.length < original.length * 0.5) return "sa\xEDda muito menor que o original (poss\xEDvel truncamento)";
|
|
1415
|
+
if (!/export\s+default|export\s+function|export\s+const/.test(next)) return "sem export";
|
|
1416
|
+
if (!next.includes(hooksImport)) return "n\xE3o importou os hooks";
|
|
1417
|
+
const balanced = (s, a, b) => s.split(a).length - 1 === s.split(b).length - 1;
|
|
1418
|
+
if (!balanced(next, "{", "}")) return "chaves desbalanceadas";
|
|
1419
|
+
if (!balanced(next, "(", ")")) return "par\xEAnteses desbalanceados";
|
|
1420
|
+
if (!balanced(next, "[", "]")) return "colchetes desbalanceados";
|
|
1421
|
+
if (/```/.test(next)) return "markdown na sa\xEDda";
|
|
1422
|
+
return null;
|
|
1423
|
+
}
|
|
1424
|
+
async function callOpenAI(apiKey, prompt) {
|
|
1425
|
+
const res = await fetch(OPENAI_URL, {
|
|
1426
|
+
method: "POST",
|
|
1427
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
1428
|
+
body: JSON.stringify({
|
|
1429
|
+
model: MODEL,
|
|
1430
|
+
temperature: 0,
|
|
1431
|
+
response_format: { type: "json_object" },
|
|
1432
|
+
messages: [
|
|
1433
|
+
{ role: "system", content: 'Voc\xEA religa componentes React para a Strapi e responde s\xF3 com JSON {"code": "..."} v\xE1lido.' },
|
|
1434
|
+
{ role: "user", content: prompt }
|
|
1435
|
+
]
|
|
1436
|
+
})
|
|
1437
|
+
});
|
|
1438
|
+
if (!res.ok) throw new Error(`OpenAI: ${await res.text()}`);
|
|
1439
|
+
const data = await res.json();
|
|
1440
|
+
const raw = JSON.parse(data.choices?.[0]?.message?.content ?? "{}");
|
|
1441
|
+
return typeof raw.code === "string" ? raw.code : "";
|
|
1442
|
+
}
|
|
1443
|
+
async function wireFrontend(_strapi, opts) {
|
|
1444
|
+
const { frontendDir, manifest } = opts;
|
|
1445
|
+
const result = {
|
|
1446
|
+
ok: false,
|
|
1447
|
+
dataLayer: [],
|
|
1448
|
+
componentsWired: [],
|
|
1449
|
+
componentsSkipped: [],
|
|
1450
|
+
warnings: [],
|
|
1451
|
+
errors: []
|
|
1452
|
+
};
|
|
1453
|
+
if (!import_node_path4.default.isAbsolute(frontendDir)) {
|
|
1454
|
+
result.errors.push("frontendDir deve ser absoluto");
|
|
1455
|
+
return result;
|
|
1456
|
+
}
|
|
1457
|
+
try {
|
|
1458
|
+
result.dataLayer = writeDataLayer(frontendDir, manifest, opts.dryRun);
|
|
877
1459
|
} catch (e) {
|
|
878
|
-
result.errors.push(`
|
|
1460
|
+
result.errors.push(`camada de dados: ${e?.message ?? e}`);
|
|
1461
|
+
return result;
|
|
879
1462
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1463
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
1464
|
+
if (!apiKey) {
|
|
1465
|
+
result.warnings.push("Sem OPENAI_API_KEY: camada de dados criada, mas os componentes N\xC3O foram religados (precisa da chave).");
|
|
1466
|
+
result.ok = true;
|
|
1467
|
+
return result;
|
|
1468
|
+
}
|
|
1469
|
+
const srcDir = import_node_path4.default.join(frontendDir, "src");
|
|
1470
|
+
const all = [];
|
|
1471
|
+
walkComponents(srcDir, frontendDir, all);
|
|
1472
|
+
const components = all.filter((rel) => /(\/|^)(components|pages|app|routes)(\/|$)/.test(rel.replace(/\\/g, "/")));
|
|
1473
|
+
const model = contentModelSummary(_strapi, manifest);
|
|
1474
|
+
const hooksImport = usesAtAlias(frontendDir) ? "@/hooks/useStrapi" : "../hooks/useStrapi";
|
|
1475
|
+
const seedSnippet = JSON.stringify(manifest.seed ?? []).slice(0, 6e3);
|
|
1476
|
+
for (const rel of components) {
|
|
1477
|
+
const abs = import_node_path4.default.join(frontendDir, rel);
|
|
1478
|
+
if (!ensureInside2(frontendDir, abs)) continue;
|
|
1479
|
+
let source;
|
|
1480
|
+
try {
|
|
1481
|
+
source = import_node_fs4.default.readFileSync(abs, "utf8");
|
|
1482
|
+
} catch {
|
|
1483
|
+
continue;
|
|
1484
|
+
}
|
|
1485
|
+
if (source.length > MAX_COMPONENT_CHARS) {
|
|
1486
|
+
result.componentsSkipped.push({ rel, reason: "arquivo grande demais" });
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
if (source.includes("useStrapi")) {
|
|
1490
|
+
result.componentsSkipped.push({ rel, reason: "j\xE1 religado" });
|
|
1491
|
+
continue;
|
|
1492
|
+
}
|
|
1493
|
+
const check = (code) => {
|
|
1494
|
+
const h = looksSane(source, code, hooksImport);
|
|
1495
|
+
if (h) return h;
|
|
1496
|
+
const s = syntaxError(code);
|
|
1497
|
+
return s ? `sintaxe: ${s}` : null;
|
|
1498
|
+
};
|
|
1499
|
+
try {
|
|
1500
|
+
const prompt = wirePrompt(rel, source, model, hooksImport, seedSnippet);
|
|
1501
|
+
let next = await callOpenAI(apiKey, prompt);
|
|
1502
|
+
let bad = check(next);
|
|
1503
|
+
if (bad) {
|
|
1504
|
+
const fix = `${prompt}
|
|
1505
|
+
|
|
1506
|
+
A sua tentativa anterior foi REJEITADA por: ${bad}. Corrija e responda s\xF3 com o JSON {"code":"..."} do arquivo completo e v\xE1lido.`;
|
|
1507
|
+
const next2 = await callOpenAI(apiKey, fix);
|
|
1508
|
+
const bad2 = check(next2);
|
|
1509
|
+
if (bad2) {
|
|
1510
|
+
result.componentsSkipped.push({ rel, reason: bad2 });
|
|
1511
|
+
continue;
|
|
1512
|
+
}
|
|
1513
|
+
next = next2;
|
|
1514
|
+
}
|
|
889
1515
|
if (!opts.dryRun) {
|
|
890
|
-
|
|
891
|
-
|
|
1516
|
+
const bak = abs + ".bak";
|
|
1517
|
+
if (!import_node_fs4.default.existsSync(bak)) import_node_fs4.default.writeFileSync(bak, source, "utf8");
|
|
1518
|
+
import_node_fs4.default.writeFileSync(abs, next, "utf8");
|
|
892
1519
|
}
|
|
893
|
-
result.
|
|
1520
|
+
result.componentsWired.push(rel);
|
|
1521
|
+
} catch (e) {
|
|
1522
|
+
result.componentsSkipped.push({ rel, reason: `IA: ${e?.message ?? e}` });
|
|
894
1523
|
}
|
|
895
|
-
} catch (e) {
|
|
896
|
-
result.errors.push(`preview: ${e?.message ?? e}`);
|
|
897
1524
|
}
|
|
898
|
-
result.ok =
|
|
1525
|
+
result.ok = true;
|
|
899
1526
|
return result;
|
|
900
1527
|
}
|
|
901
1528
|
|
|
@@ -937,17 +1564,17 @@ var MARKER_DIR = ".mcp-chat";
|
|
|
937
1564
|
var MARKER_FILE = "pending-provision.json";
|
|
938
1565
|
var DONE_FILE = "last-provision.json";
|
|
939
1566
|
function markerPath(strapiAppDir) {
|
|
940
|
-
return
|
|
1567
|
+
return import_node_path5.default.join(strapiAppDir, MARKER_DIR, MARKER_FILE);
|
|
941
1568
|
}
|
|
942
1569
|
function donePath(strapiAppDir) {
|
|
943
|
-
return
|
|
1570
|
+
return import_node_path5.default.join(strapiAppDir, MARKER_DIR, DONE_FILE);
|
|
944
1571
|
}
|
|
945
1572
|
function getProvisionStatus(strapiAppDir) {
|
|
946
|
-
const pending =
|
|
1573
|
+
const pending = import_node_fs5.default.existsSync(markerPath(strapiAppDir));
|
|
947
1574
|
let done = null;
|
|
948
1575
|
try {
|
|
949
1576
|
const dp = donePath(strapiAppDir);
|
|
950
|
-
if (
|
|
1577
|
+
if (import_node_fs5.default.existsSync(dp)) done = JSON.parse(import_node_fs5.default.readFileSync(dp, "utf8"));
|
|
951
1578
|
} catch {
|
|
952
1579
|
}
|
|
953
1580
|
return { pending, done };
|
|
@@ -983,10 +1610,10 @@ function stageProvision(strapi, input) {
|
|
|
983
1610
|
if (!input.dryRun) {
|
|
984
1611
|
try {
|
|
985
1612
|
const mp = markerPath(input.strapiAppDir);
|
|
986
|
-
|
|
987
|
-
|
|
1613
|
+
import_node_fs5.default.mkdirSync(import_node_path5.default.dirname(mp), { recursive: true });
|
|
1614
|
+
import_node_fs5.default.writeFileSync(mp, JSON.stringify(marker, null, 2), "utf8");
|
|
988
1615
|
try {
|
|
989
|
-
|
|
1616
|
+
import_node_fs5.default.unlinkSync(donePath(input.strapiAppDir));
|
|
990
1617
|
} catch {
|
|
991
1618
|
}
|
|
992
1619
|
result.staged = true;
|
|
@@ -1002,10 +1629,10 @@ function stageProvision(strapi, input) {
|
|
|
1002
1629
|
async function runPendingProvision(strapi, strapiAppDir) {
|
|
1003
1630
|
const result = { ran: false, errors: [] };
|
|
1004
1631
|
const mp = markerPath(strapiAppDir);
|
|
1005
|
-
if (!
|
|
1632
|
+
if (!import_node_fs5.default.existsSync(mp)) return result;
|
|
1006
1633
|
let marker;
|
|
1007
1634
|
try {
|
|
1008
|
-
marker = JSON.parse(
|
|
1635
|
+
marker = JSON.parse(import_node_fs5.default.readFileSync(mp, "utf8"));
|
|
1009
1636
|
} catch (e) {
|
|
1010
1637
|
result.errors.push(`marcador ileg\xEDvel: ${e?.message ?? e}`);
|
|
1011
1638
|
return result;
|
|
@@ -1034,8 +1661,16 @@ async function runPendingProvision(strapi, strapiAppDir) {
|
|
|
1034
1661
|
result.errors.push(`link: ${e?.message ?? e}`);
|
|
1035
1662
|
}
|
|
1036
1663
|
try {
|
|
1037
|
-
|
|
1038
|
-
|
|
1664
|
+
result.wire = await wireFrontend(strapi, {
|
|
1665
|
+
frontendDir: marker.frontendDir,
|
|
1666
|
+
manifest: marker.manifest
|
|
1667
|
+
});
|
|
1668
|
+
if (result.wire.errors.length) result.errors.push(...result.wire.errors.map((e) => `wire: ${e}`));
|
|
1669
|
+
} catch (e) {
|
|
1670
|
+
result.errors.push(`wire: ${e?.message ?? e}`);
|
|
1671
|
+
}
|
|
1672
|
+
try {
|
|
1673
|
+
const previewUrl = marker.context.frontendUrl || `http://localhost:${FRONTEND_BASE_PORT}`;
|
|
1039
1674
|
const done = {
|
|
1040
1675
|
name: marker.manifest.name,
|
|
1041
1676
|
framework: marker.manifest.framework,
|
|
@@ -1047,24 +1682,24 @@ async function runPendingProvision(strapi, strapiAppDir) {
|
|
|
1047
1682
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1048
1683
|
};
|
|
1049
1684
|
const dp = donePath(strapiAppDir);
|
|
1050
|
-
|
|
1051
|
-
|
|
1685
|
+
import_node_fs5.default.mkdirSync(import_node_path5.default.dirname(dp), { recursive: true });
|
|
1686
|
+
import_node_fs5.default.writeFileSync(dp, JSON.stringify(done, null, 2), "utf8");
|
|
1052
1687
|
} catch (e) {
|
|
1053
1688
|
result.errors.push(`resumo: ${e?.message ?? e}`);
|
|
1054
1689
|
}
|
|
1055
1690
|
try {
|
|
1056
|
-
|
|
1691
|
+
import_node_fs5.default.unlinkSync(mp);
|
|
1057
1692
|
} catch {
|
|
1058
1693
|
}
|
|
1059
1694
|
return result;
|
|
1060
1695
|
}
|
|
1061
1696
|
|
|
1062
1697
|
// server/src/provision/infer.ts
|
|
1063
|
-
var
|
|
1064
|
-
var
|
|
1065
|
-
var
|
|
1066
|
-
var
|
|
1067
|
-
var
|
|
1698
|
+
var import_node_fs6 = __toESM(require("node:fs"));
|
|
1699
|
+
var import_node_path6 = __toESM(require("node:path"));
|
|
1700
|
+
var OPENAI_URL2 = "https://api.openai.com/v1/chat/completions";
|
|
1701
|
+
var MODEL2 = process.env.OPENAI_CHAT_MODEL || "gpt-4o";
|
|
1702
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
1068
1703
|
"node_modules",
|
|
1069
1704
|
".git",
|
|
1070
1705
|
"dist",
|
|
@@ -1078,7 +1713,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
1078
1713
|
".cache",
|
|
1079
1714
|
"public"
|
|
1080
1715
|
]);
|
|
1081
|
-
var
|
|
1716
|
+
var CODE_EXT2 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs"]);
|
|
1082
1717
|
var MAX_FILES = 18;
|
|
1083
1718
|
var MAX_TOTAL_CHARS = 6e4;
|
|
1084
1719
|
var MAX_FILE_CHARS = 12e3;
|
|
@@ -1094,38 +1729,53 @@ function score(rel) {
|
|
|
1094
1729
|
function walk(dir, base, out) {
|
|
1095
1730
|
let entries;
|
|
1096
1731
|
try {
|
|
1097
|
-
entries =
|
|
1732
|
+
entries = import_node_fs6.default.readdirSync(dir, { withFileTypes: true });
|
|
1098
1733
|
} catch {
|
|
1099
1734
|
return;
|
|
1100
1735
|
}
|
|
1101
1736
|
for (const e of entries) {
|
|
1102
1737
|
if (e.name.startsWith(".") && e.name !== ".") continue;
|
|
1103
|
-
const full =
|
|
1738
|
+
const full = import_node_path6.default.join(dir, e.name);
|
|
1104
1739
|
if (e.isDirectory()) {
|
|
1105
|
-
if (
|
|
1740
|
+
if (SKIP_DIRS2.has(e.name)) continue;
|
|
1106
1741
|
walk(full, base, out);
|
|
1107
|
-
} else if (
|
|
1108
|
-
out.push(
|
|
1742
|
+
} else if (CODE_EXT2.has(import_node_path6.default.extname(e.name))) {
|
|
1743
|
+
out.push(import_node_path6.default.relative(base, full));
|
|
1109
1744
|
}
|
|
1110
1745
|
}
|
|
1111
1746
|
}
|
|
1747
|
+
function hasInlineDataArray(content) {
|
|
1748
|
+
return /(?:export\s+)?const\s+\w+\s*(?::[^=\n]+)?=\s*\[\s*\{/.test(content);
|
|
1749
|
+
}
|
|
1112
1750
|
function collectFiles(frontendDir) {
|
|
1113
1751
|
const all = [];
|
|
1114
1752
|
walk(frontendDir, frontendDir, all);
|
|
1115
1753
|
const tree = all.slice().sort();
|
|
1116
|
-
const
|
|
1117
|
-
const
|
|
1118
|
-
|
|
1119
|
-
for (const { rel } of ranked) {
|
|
1120
|
-
if (files.length >= MAX_FILES || total >= MAX_TOTAL_CHARS) break;
|
|
1754
|
+
const scored = [];
|
|
1755
|
+
for (const rel of all) {
|
|
1756
|
+
let content;
|
|
1121
1757
|
try {
|
|
1122
|
-
|
|
1123
|
-
if (!/export\s+(const|default|type|interface)/.test(content)) continue;
|
|
1124
|
-
if (content.length > MAX_FILE_CHARS) content = content.slice(0, MAX_FILE_CHARS) + "\n/* \u2026truncado\u2026 */";
|
|
1125
|
-
files.push({ rel, content });
|
|
1126
|
-
total += content.length;
|
|
1758
|
+
content = import_node_fs6.default.readFileSync(import_node_path6.default.join(frontendDir, rel), "utf8");
|
|
1127
1759
|
} catch {
|
|
1760
|
+
continue;
|
|
1128
1761
|
}
|
|
1762
|
+
const dataArray = hasInlineDataArray(content);
|
|
1763
|
+
const hasExport = /export\s+(const|default|type|interface)/.test(content);
|
|
1764
|
+
if (!hasExport && !dataArray) continue;
|
|
1765
|
+
let s = score(rel);
|
|
1766
|
+
if (dataArray) s += 8;
|
|
1767
|
+
if (s <= 0) continue;
|
|
1768
|
+
scored.push({ rel, content, s });
|
|
1769
|
+
}
|
|
1770
|
+
scored.sort((a, b) => b.s - a.s);
|
|
1771
|
+
const files = [];
|
|
1772
|
+
let total = 0;
|
|
1773
|
+
for (const it of scored) {
|
|
1774
|
+
if (files.length >= MAX_FILES || total >= MAX_TOTAL_CHARS) break;
|
|
1775
|
+
let content = it.content;
|
|
1776
|
+
if (content.length > MAX_FILE_CHARS) content = content.slice(0, MAX_FILE_CHARS) + "\n/* \u2026truncado\u2026 */";
|
|
1777
|
+
files.push({ rel: it.rel, content });
|
|
1778
|
+
total += content.length;
|
|
1129
1779
|
}
|
|
1130
1780
|
return { files, tree };
|
|
1131
1781
|
}
|
|
@@ -1160,7 +1810,7 @@ function collectPageTexts(frontendDir) {
|
|
|
1160
1810
|
for (const rel of ranked) {
|
|
1161
1811
|
if (out.length >= MAX_TEXT_FILES) break;
|
|
1162
1812
|
try {
|
|
1163
|
-
const texts = extractTexts(
|
|
1813
|
+
const texts = extractTexts(import_node_fs6.default.readFileSync(import_node_path6.default.join(frontendDir, rel), "utf8"));
|
|
1164
1814
|
if (texts.length) out.push({ rel, texts });
|
|
1165
1815
|
} catch {
|
|
1166
1816
|
}
|
|
@@ -1248,9 +1898,9 @@ function buildPageContentTypes(pageTexts, budget) {
|
|
|
1248
1898
|
}
|
|
1249
1899
|
return { contentTypes, seed };
|
|
1250
1900
|
}
|
|
1251
|
-
function
|
|
1901
|
+
function detectFramework2(frontendDir) {
|
|
1252
1902
|
try {
|
|
1253
|
-
const pkg = JSON.parse(
|
|
1903
|
+
const pkg = JSON.parse(import_node_fs6.default.readFileSync(import_node_path6.default.join(frontendDir, "package.json"), "utf8"));
|
|
1254
1904
|
const deps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
|
|
1255
1905
|
if (deps.next) return "next";
|
|
1256
1906
|
if (deps["@tanstack/react-start"]) return "tanstack";
|
|
@@ -1294,13 +1944,15 @@ Gere um JSON "strapi.manifest.json" com ESTE formato:
|
|
|
1294
1944
|
|
|
1295
1945
|
REGRAS:
|
|
1296
1946
|
- Crie uma content-type para cada COLE\xC7\xC3O de dados (arrays de objetos). Use os MESMOS nomes de campo do c\xF3digo.
|
|
1947
|
+
- 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.
|
|
1948
|
+
- 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.
|
|
1297
1949
|
- Dados de "configura\xE7\xE3o do site" (objeto \xFAnico: nome, telefone, etc.) \u2192 singleType.
|
|
1298
1950
|
- Campos string longos/descri\xE7\xF5es \u2192 "text" ou "richtext". Listas de strings \u2192 "json".
|
|
1299
1951
|
- 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).
|
|
1300
1952
|
- Imagens (imports de assets ou caminhos) \u2192 "media" (N\xC3O coloque o valor da imagem no seed; omita o campo no seed).
|
|
1301
|
-
- Em "seed",
|
|
1953
|
+
- 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.
|
|
1302
1954
|
- Foque APENAS em cole\xE7\xF5es/objetos de dados \u2014 N\xC3O precisa modelar textos soltos de UI (isso \xE9 tratado \xE0 parte).
|
|
1303
|
-
- N\xC3O invente. singularName kebab-case, sem repetir. Rela\xE7\xF5es s\xF3 apontam para types definidos por voc\xEA.
|
|
1955
|
+
- 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.
|
|
1304
1956
|
- Se n\xE3o houver cole\xE7\xF5es de dados, devolva contentTypes: [] e seed: [].
|
|
1305
1957
|
- Responda APENAS com o JSON, nada de markdown.
|
|
1306
1958
|
|
|
@@ -1311,7 +1963,7 @@ Arquivos de dados (cole\xE7\xF5es):
|
|
|
1311
1963
|
${filesBlock}`;
|
|
1312
1964
|
}
|
|
1313
1965
|
async function inferManifest(strapi, frontendDir, opts) {
|
|
1314
|
-
const framework =
|
|
1966
|
+
const framework = detectFramework2(frontendDir);
|
|
1315
1967
|
const result = {
|
|
1316
1968
|
ok: false,
|
|
1317
1969
|
inferred: true,
|
|
@@ -1320,10 +1972,10 @@ async function inferManifest(strapi, frontendDir, opts) {
|
|
|
1320
1972
|
warnings: [],
|
|
1321
1973
|
errors: []
|
|
1322
1974
|
};
|
|
1323
|
-
const existing =
|
|
1324
|
-
if (
|
|
1975
|
+
const existing = import_node_path6.default.join(frontendDir, "strapi.manifest.json");
|
|
1976
|
+
if (import_node_fs6.default.existsSync(existing)) {
|
|
1325
1977
|
try {
|
|
1326
|
-
const raw = JSON.parse(
|
|
1978
|
+
const raw = JSON.parse(import_node_fs6.default.readFileSync(existing, "utf8"));
|
|
1327
1979
|
const v2 = validateManifest(raw);
|
|
1328
1980
|
result.inferred = false;
|
|
1329
1981
|
result.rawManifest = raw;
|
|
@@ -1360,11 +2012,11 @@ async function inferManifest(strapi, frontendDir, opts) {
|
|
|
1360
2012
|
let dataSeed = [];
|
|
1361
2013
|
const apiKey = process.env.OPENAI_API_KEY;
|
|
1362
2014
|
if (apiKey && collected.files.length) {
|
|
1363
|
-
const
|
|
1364
|
-
const res = await fetch(
|
|
2015
|
+
const callOpenAI2 = async (messages2) => {
|
|
2016
|
+
const res = await fetch(OPENAI_URL2, {
|
|
1365
2017
|
method: "POST",
|
|
1366
2018
|
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
1367
|
-
body: JSON.stringify({ model:
|
|
2019
|
+
body: JSON.stringify({ model: MODEL2, temperature: 0, response_format: { type: "json_object" }, messages: messages2 })
|
|
1368
2020
|
});
|
|
1369
2021
|
if (!res.ok) throw new Error(`OpenAI: ${await res.text()}`);
|
|
1370
2022
|
return res.json();
|
|
@@ -1375,7 +2027,7 @@ async function inferManifest(strapi, frontendDir, opts) {
|
|
|
1375
2027
|
];
|
|
1376
2028
|
try {
|
|
1377
2029
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1378
|
-
const data = await
|
|
2030
|
+
const data = await callOpenAI2(messages);
|
|
1379
2031
|
const raw = JSON.parse(data.choices?.[0]?.message?.content ?? "{}");
|
|
1380
2032
|
const candidate = {
|
|
1381
2033
|
manifestVersion: 1,
|
|
@@ -1406,6 +2058,48 @@ async function inferManifest(strapi, frontendDir, opts) {
|
|
|
1406
2058
|
} else if (!apiKey) {
|
|
1407
2059
|
result.warnings.push("Sem OPENAI_API_KEY: modelando os TEXTOS (determin\xEDstico); cole\xE7\xF5es de dados n\xE3o inferidas.");
|
|
1408
2060
|
}
|
|
2061
|
+
if (dataCts.length && dataSeed.length) {
|
|
2062
|
+
const parts = [];
|
|
2063
|
+
for (const f of collected.files) {
|
|
2064
|
+
try {
|
|
2065
|
+
parts.push(import_node_fs6.default.readFileSync(import_node_path6.default.join(frontendDir, f.rel), "utf8"));
|
|
2066
|
+
} catch {
|
|
2067
|
+
parts.push(f.content);
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
const norm = (x) => String(x).toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
2071
|
+
const hay = norm(parts.join("\n"));
|
|
2072
|
+
const present = (v2) => {
|
|
2073
|
+
if (typeof v2 !== "string") return false;
|
|
2074
|
+
const n = norm(v2);
|
|
2075
|
+
return n.length >= 4 && hay.includes(n);
|
|
2076
|
+
};
|
|
2077
|
+
const keep = /* @__PURE__ */ new Set();
|
|
2078
|
+
const verifiedSeed = [];
|
|
2079
|
+
let droppedEntries = 0;
|
|
2080
|
+
for (const grp of dataSeed) {
|
|
2081
|
+
const entries = (grp.entries ?? []).filter((e) => {
|
|
2082
|
+
const ok = Object.values(e).some(present);
|
|
2083
|
+
if (!ok) droppedEntries++;
|
|
2084
|
+
return ok;
|
|
2085
|
+
});
|
|
2086
|
+
if (entries.length) {
|
|
2087
|
+
verifiedSeed.push({ ...grp, entries });
|
|
2088
|
+
keep.add(grp.singularName);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
const verifiedCts = dataCts.filter(
|
|
2092
|
+
(ct) => ct.kind === "singleType" || keep.has(ct.singularName)
|
|
2093
|
+
);
|
|
2094
|
+
const droppedCts = dataCts.length - verifiedCts.length;
|
|
2095
|
+
if (droppedEntries || droppedCts) {
|
|
2096
|
+
result.warnings.push(
|
|
2097
|
+
`Anti-alucina\xE7\xE3o: descartei ${droppedEntries} entrada(s) e ${droppedCts} content-type(s) cujos valores n\xE3o batiam com o c\xF3digo.`
|
|
2098
|
+
);
|
|
2099
|
+
}
|
|
2100
|
+
dataCts = verifiedCts;
|
|
2101
|
+
dataSeed = verifiedSeed;
|
|
2102
|
+
}
|
|
1409
2103
|
const budget = 60 - dataCts.length - 1;
|
|
1410
2104
|
const page = buildPageContentTypes(pageTexts, budget);
|
|
1411
2105
|
const finalManifest = {
|
|
@@ -1434,139 +2128,13 @@ async function inferManifest(strapi, frontendDir, opts) {
|
|
|
1434
2128
|
return result;
|
|
1435
2129
|
}
|
|
1436
2130
|
|
|
1437
|
-
// server/src/provision/runner.ts
|
|
1438
|
-
var import_node_child_process = require("node:child_process");
|
|
1439
|
-
var import_node_net = __toESM(require("node:net"));
|
|
1440
|
-
var import_node_fs5 = __toESM(require("node:fs"));
|
|
1441
|
-
var import_node_path5 = __toESM(require("node:path"));
|
|
1442
|
-
var info = { state: "idle", dir: null, url: null, pm: null, error: null, log: [] };
|
|
1443
|
-
var child = null;
|
|
1444
|
-
var pollTimer = null;
|
|
1445
|
-
function detectPM(dir) {
|
|
1446
|
-
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";
|
|
1447
|
-
if (import_node_fs5.default.existsSync(import_node_path5.default.join(dir, "pnpm-lock.yaml"))) return "pnpm";
|
|
1448
|
-
if (import_node_fs5.default.existsSync(import_node_path5.default.join(dir, "yarn.lock"))) return "yarn";
|
|
1449
|
-
return "npm";
|
|
1450
|
-
}
|
|
1451
|
-
var has = (dir, ...names) => names.some((n) => import_node_fs5.default.existsSync(import_node_path5.default.join(dir, n)));
|
|
1452
|
-
function detectFramework2(dir) {
|
|
1453
|
-
if (has(dir, "next.config.js", "next.config.ts", "next.config.mjs")) return "next";
|
|
1454
|
-
if (has(dir, "vite.config.js", "vite.config.ts", "vite.config.mjs")) return "vite";
|
|
1455
|
-
return "other";
|
|
1456
|
-
}
|
|
1457
|
-
var FRONTEND_BASE_PORT = 4321;
|
|
1458
|
-
function findFreePort(start) {
|
|
1459
|
-
return new Promise((resolve) => {
|
|
1460
|
-
const tryPort = (p) => {
|
|
1461
|
-
if (p > start + 200) return resolve(start);
|
|
1462
|
-
const srv = import_node_net.default.createServer();
|
|
1463
|
-
srv.once("error", () => tryPort(p + 1));
|
|
1464
|
-
srv.once("listening", () => srv.close(() => resolve(p)));
|
|
1465
|
-
srv.listen(p, "0.0.0.0");
|
|
1466
|
-
};
|
|
1467
|
-
tryPort(start);
|
|
1468
|
-
});
|
|
1469
|
-
}
|
|
1470
|
-
function pushLog(s) {
|
|
1471
|
-
for (const line of String(s).split("\n")) {
|
|
1472
|
-
const t = line.trim();
|
|
1473
|
-
if (t) info.log.push(t);
|
|
1474
|
-
}
|
|
1475
|
-
if (info.log.length > 60) info.log = info.log.slice(-60);
|
|
1476
|
-
}
|
|
1477
|
-
async function urlUp(url) {
|
|
1478
|
-
try {
|
|
1479
|
-
const res = await fetch(url, { method: "GET" });
|
|
1480
|
-
return res.status >= 200 && res.status < 400;
|
|
1481
|
-
} catch {
|
|
1482
|
-
return false;
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
function getRunStatus() {
|
|
1486
|
-
return { ...info, log: info.log.slice(-15) };
|
|
1487
|
-
}
|
|
1488
|
-
function stopFrontend() {
|
|
1489
|
-
if (pollTimer) {
|
|
1490
|
-
clearInterval(pollTimer);
|
|
1491
|
-
pollTimer = null;
|
|
1492
|
-
}
|
|
1493
|
-
if (child) {
|
|
1494
|
-
try {
|
|
1495
|
-
child.kill("SIGTERM");
|
|
1496
|
-
} catch {
|
|
1497
|
-
}
|
|
1498
|
-
child = null;
|
|
1499
|
-
}
|
|
1500
|
-
if (info.state !== "error") info.state = "idle";
|
|
1501
|
-
}
|
|
1502
|
-
async function startFrontend(_strapi, opts) {
|
|
1503
|
-
const { dir } = opts;
|
|
1504
|
-
if (child && info.dir === dir && ["installing", "starting", "running"].includes(info.state)) {
|
|
1505
|
-
return getRunStatus();
|
|
1506
|
-
}
|
|
1507
|
-
stopFrontend();
|
|
1508
|
-
const pm = detectPM(dir);
|
|
1509
|
-
const framework = detectFramework2(dir);
|
|
1510
|
-
const port = await findFreePort(FRONTEND_BASE_PORT);
|
|
1511
|
-
const url = `http://127.0.0.1:${port}`;
|
|
1512
|
-
info = { state: "installing", dir, url, pm, error: null, log: [] };
|
|
1513
|
-
const spawnIn = (cmd, args) => (0, import_node_child_process.spawn)(cmd, args, { cwd: dir, env: { ...process.env }, stdio: ["ignore", "pipe", "pipe"] });
|
|
1514
|
-
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)];
|
|
1515
|
-
const devArgs = pm === "yarn" ? ["dev", ...fwArgs] : ["run", "dev", "--", ...fwArgs];
|
|
1516
|
-
const startDev = () => {
|
|
1517
|
-
info.state = "starting";
|
|
1518
|
-
child = spawnIn(pm, devArgs);
|
|
1519
|
-
child.stdout?.on("data", (d) => pushLog(d));
|
|
1520
|
-
child.stderr?.on("data", (d) => pushLog(d));
|
|
1521
|
-
child.on("exit", (code) => {
|
|
1522
|
-
child = null;
|
|
1523
|
-
if (pollTimer) {
|
|
1524
|
-
clearInterval(pollTimer);
|
|
1525
|
-
pollTimer = null;
|
|
1526
|
-
}
|
|
1527
|
-
if (info.state === "running") info.state = "idle";
|
|
1528
|
-
else {
|
|
1529
|
-
info.state = "error";
|
|
1530
|
-
info.error = `dev encerrou (c\xF3digo ${code}). Veja o log.`;
|
|
1531
|
-
}
|
|
1532
|
-
});
|
|
1533
|
-
pollTimer = setInterval(async () => {
|
|
1534
|
-
if (await urlUp(url)) {
|
|
1535
|
-
info.state = "running";
|
|
1536
|
-
if (pollTimer) {
|
|
1537
|
-
clearInterval(pollTimer);
|
|
1538
|
-
pollTimer = null;
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
}, 1500);
|
|
1542
|
-
};
|
|
1543
|
-
const needInstall = !import_node_fs5.default.existsSync(import_node_path5.default.join(dir, "node_modules"));
|
|
1544
|
-
if (needInstall) {
|
|
1545
|
-
pushLog(`Instalando depend\xEAncias com ${pm}\u2026`);
|
|
1546
|
-
const installArgs = pm === "npm" ? ["install", "--no-audit", "--no-fund"] : ["install"];
|
|
1547
|
-
const inst = spawnIn(pm, installArgs);
|
|
1548
|
-
inst.stdout?.on("data", (d) => pushLog(d));
|
|
1549
|
-
inst.stderr?.on("data", (d) => pushLog(d));
|
|
1550
|
-
inst.on("exit", (code) => {
|
|
1551
|
-
if (code === 0) startDev();
|
|
1552
|
-
else {
|
|
1553
|
-
info.state = "error";
|
|
1554
|
-
info.error = `instala\xE7\xE3o falhou (c\xF3digo ${code}). Veja o log.`;
|
|
1555
|
-
}
|
|
1556
|
-
});
|
|
1557
|
-
} else {
|
|
1558
|
-
startDev();
|
|
1559
|
-
}
|
|
1560
|
-
return getRunStatus();
|
|
1561
|
-
}
|
|
1562
|
-
|
|
1563
2131
|
// server/src/provision/integrate.ts
|
|
1564
|
-
var
|
|
1565
|
-
var
|
|
2132
|
+
var import_node_fs7 = __toESM(require("node:fs"));
|
|
2133
|
+
var import_node_path7 = __toESM(require("node:path"));
|
|
1566
2134
|
var import_node_child_process2 = require("node:child_process");
|
|
1567
|
-
var
|
|
1568
|
-
var
|
|
1569
|
-
var
|
|
2135
|
+
var OPENAI_URL3 = "https://api.openai.com/v1/chat/completions";
|
|
2136
|
+
var MODEL3 = process.env.OPENAI_CHAT_MODEL || "gpt-4o";
|
|
2137
|
+
var SKIP_DIRS3 = /* @__PURE__ */ new Set([
|
|
1570
2138
|
"node_modules",
|
|
1571
2139
|
".git",
|
|
1572
2140
|
"dist",
|
|
@@ -1580,7 +2148,7 @@ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
|
1580
2148
|
".cache",
|
|
1581
2149
|
"public"
|
|
1582
2150
|
]);
|
|
1583
|
-
var
|
|
2151
|
+
var CODE_EXT3 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs"]);
|
|
1584
2152
|
var MAX_DATA_FILES = 3;
|
|
1585
2153
|
var MAX_FILE_CHARS2 = 16e3;
|
|
1586
2154
|
function score2(rel) {
|
|
@@ -1595,18 +2163,18 @@ function score2(rel) {
|
|
|
1595
2163
|
function walk2(dir, base, out) {
|
|
1596
2164
|
let entries;
|
|
1597
2165
|
try {
|
|
1598
|
-
entries =
|
|
2166
|
+
entries = import_node_fs7.default.readdirSync(dir, { withFileTypes: true });
|
|
1599
2167
|
} catch {
|
|
1600
2168
|
return;
|
|
1601
2169
|
}
|
|
1602
2170
|
for (const e of entries) {
|
|
1603
2171
|
if (e.name.startsWith(".")) continue;
|
|
1604
|
-
const full =
|
|
2172
|
+
const full = import_node_path7.default.join(dir, e.name);
|
|
1605
2173
|
if (e.isDirectory()) {
|
|
1606
|
-
if (
|
|
2174
|
+
if (SKIP_DIRS3.has(e.name)) continue;
|
|
1607
2175
|
walk2(full, base, out);
|
|
1608
|
-
} else if (
|
|
1609
|
-
out.push(
|
|
2176
|
+
} else if (CODE_EXT3.has(import_node_path7.default.extname(e.name))) {
|
|
2177
|
+
out.push(import_node_path7.default.relative(base, full));
|
|
1610
2178
|
}
|
|
1611
2179
|
}
|
|
1612
2180
|
}
|
|
@@ -1615,7 +2183,7 @@ function findDataFiles(frontendDir) {
|
|
|
1615
2183
|
walk2(frontendDir, frontendDir, all);
|
|
1616
2184
|
return all.map((rel) => ({ rel, s: score2(rel) })).filter((x) => x.s > 0).filter(({ rel }) => {
|
|
1617
2185
|
try {
|
|
1618
|
-
const c =
|
|
2186
|
+
const c = import_node_fs7.default.readFileSync(import_node_path7.default.join(frontendDir, rel), "utf8");
|
|
1619
2187
|
return /export\s+const\s+\w+\s*[:=]\s*(\[|\{)/.test(c);
|
|
1620
2188
|
} catch {
|
|
1621
2189
|
return false;
|
|
@@ -1684,19 +2252,19 @@ export async function fetchSingle(singular: string, o: FetchOpts = {}): Promise<
|
|
|
1684
2252
|
}
|
|
1685
2253
|
`;
|
|
1686
2254
|
async function ensureClientDep(frontendDir, warnings) {
|
|
1687
|
-
if (
|
|
2255
|
+
if (import_node_fs7.default.existsSync(import_node_path7.default.join(frontendDir, "node_modules", "@strapi", "client"))) return;
|
|
1688
2256
|
try {
|
|
1689
|
-
const pkgPath =
|
|
1690
|
-
const pkg = JSON.parse(
|
|
2257
|
+
const pkgPath = import_node_path7.default.join(frontendDir, "package.json");
|
|
2258
|
+
const pkg = JSON.parse(import_node_fs7.default.readFileSync(pkgPath, "utf8"));
|
|
1691
2259
|
pkg.dependencies = pkg.dependencies || {};
|
|
1692
2260
|
if (!pkg.dependencies["@strapi/client"]) {
|
|
1693
2261
|
pkg.dependencies["@strapi/client"] = "^1.6.2";
|
|
1694
|
-
|
|
2262
|
+
import_node_fs7.default.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
1695
2263
|
}
|
|
1696
2264
|
} catch (e) {
|
|
1697
2265
|
warnings.push(`package.json do frontend: ${e?.message ?? e}`);
|
|
1698
2266
|
}
|
|
1699
|
-
const pm =
|
|
2267
|
+
const pm = import_node_fs7.default.existsSync(import_node_path7.default.join(frontendDir, "bun.lock")) || import_node_fs7.default.existsSync(import_node_path7.default.join(frontendDir, "bun.lockb")) ? "bun" : import_node_fs7.default.existsSync(import_node_path7.default.join(frontendDir, "pnpm-lock.yaml")) ? "pnpm" : import_node_fs7.default.existsSync(import_node_path7.default.join(frontendDir, "yarn.lock")) ? "yarn" : "npm";
|
|
1700
2268
|
const args = pm === "npm" ? ["install", "@strapi/client", "--no-audit", "--no-fund"] : ["add", "@strapi/client"];
|
|
1701
2269
|
await new Promise((resolve) => {
|
|
1702
2270
|
try {
|
|
@@ -1748,11 +2316,11 @@ ${baseSrc.length > MAX_FILE_CHARS2 ? baseSrc.slice(0, MAX_FILE_CHARS2) : baseSrc
|
|
|
1748
2316
|
|
|
1749
2317
|
AMOSTRA REAL DA RESPOSTA DO STRAPI (JSON, por singularName):
|
|
1750
2318
|
${JSON.stringify(sample, null, 1).slice(0, 18e3)}`;
|
|
1751
|
-
const res = await fetch(
|
|
2319
|
+
const res = await fetch(OPENAI_URL3, {
|
|
1752
2320
|
method: "POST",
|
|
1753
2321
|
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
1754
2322
|
body: JSON.stringify({
|
|
1755
|
-
model:
|
|
2323
|
+
model: MODEL3,
|
|
1756
2324
|
temperature: 0,
|
|
1757
2325
|
messages: [
|
|
1758
2326
|
{ role: "system", content: "Voc\xEA gera uma fun\xE7\xE3o pura de mapeamento Strapi\u2192shape do frontend. Responde s\xF3 com a fun\xE7\xE3o." },
|
|
@@ -1906,15 +2474,15 @@ export function LanguageSwitcher() {
|
|
|
1906
2474
|
export default LanguageSwitcher;
|
|
1907
2475
|
`;
|
|
1908
2476
|
function injectSwitcher(frontendDir, warnings, dataImport = "@/data/site") {
|
|
1909
|
-
const compDir =
|
|
1910
|
-
|
|
1911
|
-
|
|
2477
|
+
const compDir = import_node_path7.default.join(frontendDir, "src", "components");
|
|
2478
|
+
import_node_fs7.default.mkdirSync(compDir, { recursive: true });
|
|
2479
|
+
import_node_fs7.default.writeFileSync(import_node_path7.default.join(compDir, "LanguageSwitcher.tsx"), switcherTsx(dataImport), "utf8");
|
|
1912
2480
|
const rootRel = ["src/routes/__root.tsx", "src/routes/__root.jsx"].find(
|
|
1913
|
-
(r) =>
|
|
2481
|
+
(r) => import_node_fs7.default.existsSync(import_node_path7.default.join(frontendDir, r))
|
|
1914
2482
|
);
|
|
1915
2483
|
if (rootRel) {
|
|
1916
|
-
const abs =
|
|
1917
|
-
let src =
|
|
2484
|
+
const abs = import_node_path7.default.join(frontendDir, rootRel);
|
|
2485
|
+
let src = import_node_fs7.default.readFileSync(abs, "utf8");
|
|
1918
2486
|
if (!src.includes("loadAllData")) {
|
|
1919
2487
|
src = `import { loadAllData } from "${dataImport}";
|
|
1920
2488
|
` + src;
|
|
@@ -1928,17 +2496,17 @@ function injectSwitcher(frontendDir, warnings, dataImport = "@/data/site") {
|
|
|
1928
2496
|
} else {
|
|
1929
2497
|
warnings.push("n\xE3o consegui injetar o loader no __root (padr\xE3o n\xE3o encontrado).");
|
|
1930
2498
|
}
|
|
1931
|
-
|
|
2499
|
+
import_node_fs7.default.writeFileSync(abs, src, "utf8");
|
|
1932
2500
|
}
|
|
1933
2501
|
} else {
|
|
1934
2502
|
warnings.push("__root n\xE3o encontrado \u2014 dados ao vivo n\xE3o ligados ao SSR.");
|
|
1935
2503
|
}
|
|
1936
2504
|
const headerRel = ["src/components/Header.tsx", "src/components/Header.jsx"].find(
|
|
1937
|
-
(r) =>
|
|
2505
|
+
(r) => import_node_fs7.default.existsSync(import_node_path7.default.join(frontendDir, r))
|
|
1938
2506
|
);
|
|
1939
2507
|
if (headerRel) {
|
|
1940
|
-
const abs =
|
|
1941
|
-
let src =
|
|
2508
|
+
const abs = import_node_path7.default.join(frontendDir, headerRel);
|
|
2509
|
+
let src = import_node_fs7.default.readFileSync(abs, "utf8");
|
|
1942
2510
|
if (!src.includes("LanguageSwitcher")) {
|
|
1943
2511
|
src = `import { LanguageSwitcher } from "@/components/LanguageSwitcher";
|
|
1944
2512
|
` + src;
|
|
@@ -1948,7 +2516,7 @@ function injectSwitcher(frontendDir, warnings, dataImport = "@/data/site") {
|
|
|
1948
2516
|
src = src.replace(/<\/header>/, ` <LanguageSwitcher />
|
|
1949
2517
|
</header>`);
|
|
1950
2518
|
}
|
|
1951
|
-
|
|
2519
|
+
import_node_fs7.default.writeFileSync(abs, src, "utf8");
|
|
1952
2520
|
}
|
|
1953
2521
|
} else {
|
|
1954
2522
|
warnings.push("Header n\xE3o encontrado \u2014 adicione <LanguageSwitcher/> manualmente.");
|
|
@@ -1994,11 +2562,11 @@ REGRAS ESTRITAS:
|
|
|
1994
2562
|
|
|
1995
2563
|
ARQUIVO:
|
|
1996
2564
|
${src.length > MAX_FILE_CHARS2 ? src.slice(0, MAX_FILE_CHARS2) : src}`;
|
|
1997
|
-
const res = await fetch(
|
|
2565
|
+
const res = await fetch(OPENAI_URL3, {
|
|
1998
2566
|
method: "POST",
|
|
1999
2567
|
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
2000
2568
|
body: JSON.stringify({
|
|
2001
|
-
model:
|
|
2569
|
+
model: MODEL3,
|
|
2002
2570
|
temperature: 0,
|
|
2003
2571
|
messages: [
|
|
2004
2572
|
{ role: "system", content: "Voc\xEA religa componentes ao CMS trocando s\xF3 os textos do mapa por express\xF5es. Responde s\xF3 com o c\xF3digo." },
|
|
@@ -2072,10 +2640,10 @@ async function rewireComponents(strapi, opts, warnings) {
|
|
|
2072
2640
|
(rel) => /\.(tsx|jsx)$/.test(rel) && !/__root|routeTree\.gen|\/api\/|LanguageSwitcher|PreviewBridge|\/ui\//.test(rel)
|
|
2073
2641
|
);
|
|
2074
2642
|
for (const rel of targets) {
|
|
2075
|
-
const abs =
|
|
2643
|
+
const abs = import_node_path7.default.join(opts.frontendDir, rel);
|
|
2076
2644
|
let src;
|
|
2077
2645
|
try {
|
|
2078
|
-
src =
|
|
2646
|
+
src = import_node_fs7.default.readFileSync(abs, "utf8");
|
|
2079
2647
|
} catch {
|
|
2080
2648
|
continue;
|
|
2081
2649
|
}
|
|
@@ -2094,8 +2662,8 @@ async function rewireComponents(strapi, opts, warnings) {
|
|
|
2094
2662
|
continue;
|
|
2095
2663
|
}
|
|
2096
2664
|
const bak = abs + ".bak";
|
|
2097
|
-
if (!
|
|
2098
|
-
|
|
2665
|
+
if (!import_node_fs7.default.existsSync(bak)) import_node_fs7.default.writeFileSync(bak, src, "utf8");
|
|
2666
|
+
import_node_fs7.default.writeFileSync(abs, out, "utf8");
|
|
2099
2667
|
rewired.push(rel);
|
|
2100
2668
|
} catch (e) {
|
|
2101
2669
|
warnings.push(`${rel}: rewire falhou (${e?.message ?? e}).`);
|
|
@@ -2134,19 +2702,19 @@ async function integrateFrontend(strapi, opts) {
|
|
|
2134
2702
|
count: Array.isArray(sample[c.singularName]) ? sample[c.singularName].length : sample[c.singularName] ? 1 : 0
|
|
2135
2703
|
}));
|
|
2136
2704
|
for (const rel of dataFiles) {
|
|
2137
|
-
const abs =
|
|
2705
|
+
const abs = import_node_path7.default.join(opts.frontendDir, rel);
|
|
2138
2706
|
try {
|
|
2139
|
-
const original =
|
|
2707
|
+
const original = import_node_fs7.default.readFileSync(abs, "utf8");
|
|
2140
2708
|
const bak = abs + ".bak";
|
|
2141
|
-
if (!
|
|
2142
|
-
const baseSrc =
|
|
2709
|
+
if (!import_node_fs7.default.existsSync(bak)) import_node_fs7.default.writeFileSync(bak, original, "utf8");
|
|
2710
|
+
const baseSrc = import_node_fs7.default.existsSync(bak) ? import_node_fs7.default.readFileSync(bak, "utf8") : original;
|
|
2143
2711
|
const mapper = await generateMapper(apiKey, baseSrc, sample, assetImportIds(baseSrc));
|
|
2144
2712
|
if (!mapper || !/mapStrapiToData/.test(mapper)) {
|
|
2145
2713
|
result.warnings.push(`${rel}: mapeador inv\xE1lido, pulado.`);
|
|
2146
2714
|
continue;
|
|
2147
2715
|
}
|
|
2148
2716
|
const moduleSrc = buildLiveDataModule(baseSrc, mapper, ctMeta, locales, def);
|
|
2149
|
-
|
|
2717
|
+
import_node_fs7.default.writeFileSync(abs, moduleSrc, "utf8");
|
|
2150
2718
|
result.filesRewritten.push(rel);
|
|
2151
2719
|
} catch (e) {
|
|
2152
2720
|
result.errors.push(`${rel}: ${e?.message ?? e}`);
|
|
@@ -2155,8 +2723,8 @@ async function integrateFrontend(strapi, opts) {
|
|
|
2155
2723
|
if (result.filesRewritten.length > 0) {
|
|
2156
2724
|
try {
|
|
2157
2725
|
const rel0 = result.filesRewritten[0];
|
|
2158
|
-
const dataDir =
|
|
2159
|
-
|
|
2726
|
+
const dataDir = import_node_path7.default.dirname(import_node_path7.default.join(opts.frontendDir, rel0));
|
|
2727
|
+
import_node_fs7.default.writeFileSync(import_node_path7.default.join(dataDir, "strapi-client.ts"), STRAPI_CLIENT_TS, "utf8");
|
|
2160
2728
|
const dataImport = "@/" + rel0.replace(/^src\//, "").replace(/\.(tsx?|jsx?)$/, "");
|
|
2161
2729
|
injectSwitcher(opts.frontendDir, result.warnings, dataImport);
|
|
2162
2730
|
await ensureClientDep(opts.frontendDir, result.warnings);
|
|
@@ -2180,12 +2748,12 @@ async function integrateFrontend(strapi, opts) {
|
|
|
2180
2748
|
|
|
2181
2749
|
// server/src/controllers/frontend.ts
|
|
2182
2750
|
var MANIFEST_NAME = "strapi.manifest.json";
|
|
2183
|
-
function
|
|
2184
|
-
const b =
|
|
2185
|
-
const t =
|
|
2751
|
+
function ensureInside3(base, target) {
|
|
2752
|
+
const b = import_node_path8.default.resolve(base);
|
|
2753
|
+
const t = import_node_path8.default.resolve(target);
|
|
2186
2754
|
if (t === b) return true;
|
|
2187
|
-
const rel =
|
|
2188
|
-
return !!rel && !rel.startsWith("..") && !
|
|
2755
|
+
const rel = import_node_path8.default.relative(b, t);
|
|
2756
|
+
return !!rel && !rel.startsWith("..") && !import_node_path8.default.isAbsolute(rel);
|
|
2189
2757
|
}
|
|
2190
2758
|
function toKebab(input) {
|
|
2191
2759
|
const s = (input || "frontend").toLowerCase().replace(/\.zip$/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/^[^a-z]+/, "");
|
|
@@ -2213,13 +2781,13 @@ var frontend_default = {
|
|
|
2213
2781
|
const originalName = file.originalFilename || file.name || "frontend";
|
|
2214
2782
|
let zip;
|
|
2215
2783
|
try {
|
|
2216
|
-
zip = await import_jszip.default.loadAsync(
|
|
2784
|
+
zip = await import_jszip.default.loadAsync(import_node_fs8.default.readFileSync(filepath));
|
|
2217
2785
|
} catch (e) {
|
|
2218
2786
|
return ctx.badRequest(`Zip inv\xE1lido: ${e?.message ?? e}`);
|
|
2219
2787
|
}
|
|
2220
2788
|
const entryNames = Object.keys(zip.files);
|
|
2221
2789
|
const manifestEntry = entryNames.find(
|
|
2222
|
-
(p) =>
|
|
2790
|
+
(p) => import_node_path8.default.basename(p) === MANIFEST_NAME && !zip.files[p].dir
|
|
2223
2791
|
);
|
|
2224
2792
|
let rootPrefix = "";
|
|
2225
2793
|
if (manifestEntry) {
|
|
@@ -2232,27 +2800,27 @@ var frontend_default = {
|
|
|
2232
2800
|
}
|
|
2233
2801
|
const name = toKebab(originalName);
|
|
2234
2802
|
const strapiAppDir = strapi.dirs.app.root;
|
|
2235
|
-
const frontendDir =
|
|
2236
|
-
if (
|
|
2803
|
+
const frontendDir = import_node_path8.default.resolve(strapiAppDir, "..", name);
|
|
2804
|
+
if (import_node_fs8.default.existsSync(frontendDir) && import_node_fs8.default.readdirSync(frontendDir).length > 0) {
|
|
2237
2805
|
return ctx.badRequest(
|
|
2238
2806
|
`A pasta de destino j\xE1 existe e n\xE3o est\xE1 vazia: ${frontendDir}. Renomeie o .zip ou remova a pasta.`
|
|
2239
2807
|
);
|
|
2240
2808
|
}
|
|
2241
2809
|
try {
|
|
2242
|
-
|
|
2810
|
+
import_node_fs8.default.mkdirSync(frontendDir, { recursive: true });
|
|
2243
2811
|
for (const entryName of entryNames) {
|
|
2244
2812
|
const entry = zip.files[entryName];
|
|
2245
2813
|
const rel = rootPrefix && entryName.startsWith(rootPrefix) ? entryName.slice(rootPrefix.length) : entryName;
|
|
2246
2814
|
if (!rel) continue;
|
|
2247
|
-
const dest =
|
|
2248
|
-
if (!
|
|
2815
|
+
const dest = import_node_path8.default.join(frontendDir, rel);
|
|
2816
|
+
if (!ensureInside3(frontendDir, dest)) {
|
|
2249
2817
|
throw new Error(`entrada perigosa no zip bloqueada: ${entryName}`);
|
|
2250
2818
|
}
|
|
2251
2819
|
if (entry.dir) {
|
|
2252
|
-
|
|
2820
|
+
import_node_fs8.default.mkdirSync(dest, { recursive: true });
|
|
2253
2821
|
} else {
|
|
2254
|
-
|
|
2255
|
-
|
|
2822
|
+
import_node_fs8.default.mkdirSync(import_node_path8.default.dirname(dest), { recursive: true });
|
|
2823
|
+
import_node_fs8.default.writeFileSync(dest, await entry.async("nodebuffer"));
|
|
2256
2824
|
}
|
|
2257
2825
|
}
|
|
2258
2826
|
} catch (e) {
|
|
@@ -2280,16 +2848,16 @@ var frontend_default = {
|
|
|
2280
2848
|
const rawManifest = body.manifest;
|
|
2281
2849
|
const frontendDir = body.frontendDir;
|
|
2282
2850
|
if (!rawManifest) return ctx.badRequest('Envie o "manifest".');
|
|
2283
|
-
if (!frontendDir || !
|
|
2851
|
+
if (!frontendDir || !import_node_path8.default.isAbsolute(frontendDir)) {
|
|
2284
2852
|
return ctx.badRequest("frontendDir ausente ou inv\xE1lido.");
|
|
2285
2853
|
}
|
|
2286
2854
|
const strapiAppDir = strapi.dirs.app.root;
|
|
2287
2855
|
const apiRoot = strapi.dirs.app.api;
|
|
2288
|
-
const parent =
|
|
2289
|
-
if (!
|
|
2856
|
+
const parent = import_node_path8.default.resolve(strapiAppDir, "..");
|
|
2857
|
+
if (!ensureInside3(parent, frontendDir) || frontendDir === parent) {
|
|
2290
2858
|
return ctx.badRequest("frontendDir fora da pasta permitida.");
|
|
2291
2859
|
}
|
|
2292
|
-
if (!
|
|
2860
|
+
if (!import_node_fs8.default.existsSync(frontendDir)) {
|
|
2293
2861
|
return ctx.badRequest("frontendDir n\xE3o existe (rode a an\xE1lise primeiro).");
|
|
2294
2862
|
}
|
|
2295
2863
|
const v = validateManifest(rawManifest);
|
|
@@ -2297,8 +2865,8 @@ var frontend_default = {
|
|
|
2297
2865
|
return ctx.badRequest({ message: "Manifest inv\xE1lido", errors: v.errors });
|
|
2298
2866
|
}
|
|
2299
2867
|
try {
|
|
2300
|
-
|
|
2301
|
-
|
|
2868
|
+
import_node_fs8.default.writeFileSync(
|
|
2869
|
+
import_node_path8.default.join(frontendDir, MANIFEST_NAME),
|
|
2302
2870
|
JSON.stringify(v.data, null, 2),
|
|
2303
2871
|
"utf8"
|
|
2304
2872
|
);
|
|
@@ -2360,8 +2928,8 @@ var frontend_default = {
|
|
|
2360
2928
|
if (!dir || !url) {
|
|
2361
2929
|
return ctx.badRequest("Nenhum frontend provisionado para rodar.");
|
|
2362
2930
|
}
|
|
2363
|
-
const parent =
|
|
2364
|
-
if (!
|
|
2931
|
+
const parent = import_node_path8.default.resolve(strapi.dirs.app.root, "..");
|
|
2932
|
+
if (!ensureInside3(parent, dir) || !import_node_fs8.default.existsSync(dir)) {
|
|
2365
2933
|
return ctx.badRequest("Pasta do frontend inv\xE1lida.");
|
|
2366
2934
|
}
|
|
2367
2935
|
ctx.body = await startFrontend(strapi, { dir, url });
|
|
@@ -2385,19 +2953,48 @@ var frontend_default = {
|
|
|
2385
2953
|
frontendDir = st.done?.frontendDir || "";
|
|
2386
2954
|
}
|
|
2387
2955
|
if (!frontendDir) return ctx.badRequest("Nenhum frontend provisionado.");
|
|
2388
|
-
const parent =
|
|
2389
|
-
if (!
|
|
2956
|
+
const parent = import_node_path8.default.resolve(strapi.dirs.app.root, "..");
|
|
2957
|
+
if (!ensureInside3(parent, frontendDir) || !import_node_fs8.default.existsSync(frontendDir)) {
|
|
2390
2958
|
return ctx.badRequest("Pasta do frontend inv\xE1lida.");
|
|
2391
2959
|
}
|
|
2392
2960
|
let manifest;
|
|
2393
2961
|
try {
|
|
2394
|
-
manifest = JSON.parse(
|
|
2962
|
+
manifest = JSON.parse(import_node_fs8.default.readFileSync(import_node_path8.default.join(frontendDir, MANIFEST_NAME), "utf8"));
|
|
2395
2963
|
} catch {
|
|
2396
2964
|
return ctx.badRequest("Manifest do projeto n\xE3o encontrado (rode a provis\xE3o primeiro).");
|
|
2397
2965
|
}
|
|
2398
2966
|
const v = validateManifest(manifest);
|
|
2399
2967
|
if (!v.ok) return ctx.badRequest({ message: "Manifest inv\xE1lido", errors: v.errors });
|
|
2400
2968
|
ctx.body = await integrateFrontend(strapi, { frontendDir, manifest: v.data });
|
|
2969
|
+
},
|
|
2970
|
+
/**
|
|
2971
|
+
* Religa o frontend à Strapi por FETCH AO VIVO: gera a camada de dados (REST,
|
|
2972
|
+
* flat) e religa os componentes (com .bak + fallback + sanidade). Só escreve no
|
|
2973
|
+
* frontend, nunca na Strapi. Usa o último provisionado por padrão.
|
|
2974
|
+
*/
|
|
2975
|
+
async wire(ctx) {
|
|
2976
|
+
const strapi = ctx.strapi ?? global.strapi;
|
|
2977
|
+
if (!devOnly(ctx)) return;
|
|
2978
|
+
const body = ctx.request.body || {};
|
|
2979
|
+
let frontendDir = body.frontendDir;
|
|
2980
|
+
if (!frontendDir) {
|
|
2981
|
+
const st = getProvisionStatus(strapi.dirs.app.root);
|
|
2982
|
+
frontendDir = st.done?.frontendDir || "";
|
|
2983
|
+
}
|
|
2984
|
+
if (!frontendDir) return ctx.badRequest("Nenhum frontend provisionado.");
|
|
2985
|
+
const parent = import_node_path8.default.resolve(strapi.dirs.app.root, "..");
|
|
2986
|
+
if (!ensureInside3(parent, frontendDir) || !import_node_fs8.default.existsSync(frontendDir)) {
|
|
2987
|
+
return ctx.badRequest("Pasta do frontend inv\xE1lida.");
|
|
2988
|
+
}
|
|
2989
|
+
let manifest;
|
|
2990
|
+
try {
|
|
2991
|
+
manifest = JSON.parse(import_node_fs8.default.readFileSync(import_node_path8.default.join(frontendDir, MANIFEST_NAME), "utf8"));
|
|
2992
|
+
} catch {
|
|
2993
|
+
return ctx.badRequest("Manifest do projeto n\xE3o encontrado (rode a provis\xE3o primeiro).");
|
|
2994
|
+
}
|
|
2995
|
+
const v = validateManifest(manifest);
|
|
2996
|
+
if (!v.ok) return ctx.badRequest({ message: "Manifest inv\xE1lido", errors: v.errors });
|
|
2997
|
+
ctx.body = await wireFrontend(strapi, { frontendDir, manifest: v.data });
|
|
2401
2998
|
}
|
|
2402
2999
|
};
|
|
2403
3000
|
|
|
@@ -2481,8 +3078,8 @@ var McpClient = class {
|
|
|
2481
3078
|
};
|
|
2482
3079
|
|
|
2483
3080
|
// server/src/provision/translate.ts
|
|
2484
|
-
var
|
|
2485
|
-
var
|
|
3081
|
+
var OPENAI_URL4 = "https://api.openai.com/v1/chat/completions";
|
|
3082
|
+
var MODEL4 = process.env.OPENAI_CHAT_MODEL || "gpt-4o";
|
|
2486
3083
|
var approxTokens = (s) => Math.ceil((s || "").length / 4);
|
|
2487
3084
|
var MAX_CHUNK_TOKENS = 1200;
|
|
2488
3085
|
var splitParagraphs = (text) => text.split(/\n{2,}/);
|
|
@@ -2527,7 +3124,7 @@ function splitForTranslation(value, _type) {
|
|
|
2527
3124
|
async function translateChunk(apiKey, text, sourceLang, targetLang) {
|
|
2528
3125
|
if (!text || !text.trim()) return text;
|
|
2529
3126
|
const body = {
|
|
2530
|
-
model:
|
|
3127
|
+
model: MODEL4,
|
|
2531
3128
|
temperature: 0,
|
|
2532
3129
|
max_tokens: Math.min(4e3, approxTokens(text) * 3 + 256),
|
|
2533
3130
|
messages: [
|
|
@@ -2538,7 +3135,7 @@ async function translateChunk(apiKey, text, sourceLang, targetLang) {
|
|
|
2538
3135
|
{ role: "user", content: text }
|
|
2539
3136
|
]
|
|
2540
3137
|
};
|
|
2541
|
-
const res = await fetch(
|
|
3138
|
+
const res = await fetch(OPENAI_URL4, {
|
|
2542
3139
|
method: "POST",
|
|
2543
3140
|
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
2544
3141
|
body: JSON.stringify(body)
|
|
@@ -2605,31 +3202,32 @@ function createContentTools(strapi) {
|
|
|
2605
3202
|
for (const [name, a] of Object.entries(attributes)) {
|
|
2606
3203
|
const v = node[name];
|
|
2607
3204
|
if (v == null) continue;
|
|
2608
|
-
const
|
|
3205
|
+
const path10 = [...basePath, name];
|
|
2609
3206
|
if (TEXTUAL.includes(a.type)) {
|
|
2610
3207
|
if (typeof v === "string" && v.toLowerCase().includes(needle)) {
|
|
2611
|
-
collect(
|
|
3208
|
+
collect(path10, name, v);
|
|
2612
3209
|
}
|
|
2613
3210
|
} else if (a.type === "component" && a.component) {
|
|
2614
3211
|
const sub = attrsOf(a.component);
|
|
2615
3212
|
if (a.repeatable && Array.isArray(v)) {
|
|
2616
|
-
v.forEach((item, i) => walkFind(item, sub, [...
|
|
3213
|
+
v.forEach((item, i) => walkFind(item, sub, [...path10, i], needle, collect));
|
|
2617
3214
|
} else {
|
|
2618
|
-
walkFind(v, sub,
|
|
3215
|
+
walkFind(v, sub, path10, needle, collect);
|
|
2619
3216
|
}
|
|
2620
3217
|
} else if (a.type === "dynamiczone" && Array.isArray(v)) {
|
|
2621
3218
|
v.forEach((item, i) => {
|
|
2622
3219
|
if (item?.__component) {
|
|
2623
|
-
walkFind(item, attrsOf(item.__component), [...
|
|
3220
|
+
walkFind(item, attrsOf(item.__component), [...path10, i], needle, collect);
|
|
2624
3221
|
}
|
|
2625
3222
|
});
|
|
2626
3223
|
}
|
|
2627
3224
|
}
|
|
2628
3225
|
};
|
|
2629
3226
|
const MAX_MATCHES = 100;
|
|
2630
|
-
const buscarTexto = async (termo) => {
|
|
3227
|
+
const buscarTexto = async (termo, status = "draft") => {
|
|
2631
3228
|
const needle = String(termo || "").toLowerCase().trim();
|
|
2632
3229
|
if (!needle) return { erro: "termo vazio" };
|
|
3230
|
+
const st = status === "published" ? "published" : "draft";
|
|
2633
3231
|
const matches = [];
|
|
2634
3232
|
for (const ct of apiContentTypes()) {
|
|
2635
3233
|
if (matches.length >= MAX_MATCHES) break;
|
|
@@ -2637,19 +3235,19 @@ function createContentTools(strapi) {
|
|
|
2637
3235
|
const populate = buildPopulate(attributes);
|
|
2638
3236
|
let entries = [];
|
|
2639
3237
|
try {
|
|
2640
|
-
const res = await strapi.documents(ct.uid).findMany({ status:
|
|
3238
|
+
const res = await strapi.documents(ct.uid).findMany({ status: st, populate, limit: 200 });
|
|
2641
3239
|
entries = Array.isArray(res) ? res : res ? [res] : [];
|
|
2642
3240
|
} catch {
|
|
2643
3241
|
continue;
|
|
2644
3242
|
}
|
|
2645
3243
|
const dp = hasDraftAndPublish(ct.uid);
|
|
2646
3244
|
for (const e of entries) {
|
|
2647
|
-
walkFind(e, attributes, [], needle, (
|
|
3245
|
+
walkFind(e, attributes, [], needle, (path10, campo, valor) => {
|
|
2648
3246
|
matches.push({
|
|
2649
3247
|
uid: ct.uid,
|
|
2650
3248
|
tipo: ct.info?.displayName || ct.uid,
|
|
2651
3249
|
documentId: e.documentId,
|
|
2652
|
-
path:
|
|
3250
|
+
path: path10,
|
|
2653
3251
|
campo,
|
|
2654
3252
|
valor_atual: valor.length > 300 ? valor.slice(0, 300) + "\u2026" : valor,
|
|
2655
3253
|
// draftAndPublish=false → não há rascunho; a edição já é o conteúdo
|
|
@@ -2697,8 +3295,8 @@ function createContentTools(strapi) {
|
|
|
2697
3295
|
}
|
|
2698
3296
|
return value;
|
|
2699
3297
|
};
|
|
2700
|
-
const editarCampo = async ({ uid, documentId, path:
|
|
2701
|
-
const p = Array.isArray(
|
|
3298
|
+
const editarCampo = async ({ uid, documentId, path: path10, campo, novo_valor, locale }) => {
|
|
3299
|
+
const p = Array.isArray(path10) && path10.length ? path10 : campo ? [campo] : null;
|
|
2702
3300
|
if (!p) return { erro: 'informe "path" (array) ou "campo"' };
|
|
2703
3301
|
const attributes = strapi.contentTypes?.[uid]?.attributes || {};
|
|
2704
3302
|
const topAttr = p[0];
|
|
@@ -3000,29 +3598,29 @@ var openAiToolSpecs = [
|
|
|
3000
3598
|
];
|
|
3001
3599
|
|
|
3002
3600
|
// server/src/provision/enable-i18n.ts
|
|
3003
|
-
var
|
|
3004
|
-
var
|
|
3601
|
+
var import_node_fs9 = __toESM(require("node:fs"));
|
|
3602
|
+
var import_node_path9 = __toESM(require("node:path"));
|
|
3005
3603
|
var LOCALIZABLE = ["string", "text", "richtext", "component", "dynamiczone"];
|
|
3006
3604
|
var isDev2 = () => process.env.NODE_ENV === "development";
|
|
3007
3605
|
function schemaPathFor(apiRoot, uid) {
|
|
3008
3606
|
const m = /^api::([^.]+)\.([^.]+)$/.exec(uid);
|
|
3009
3607
|
if (!m) return null;
|
|
3010
3608
|
const [, api, ct] = m;
|
|
3011
|
-
return
|
|
3609
|
+
return import_node_path9.default.join(apiRoot, api, "content-types", ct, "schema.json");
|
|
3012
3610
|
}
|
|
3013
3611
|
function listAllUids(apiRoot) {
|
|
3014
3612
|
const out = [];
|
|
3015
3613
|
let apis = [];
|
|
3016
3614
|
try {
|
|
3017
|
-
apis =
|
|
3615
|
+
apis = import_node_fs9.default.readdirSync(apiRoot);
|
|
3018
3616
|
} catch {
|
|
3019
3617
|
return out;
|
|
3020
3618
|
}
|
|
3021
3619
|
for (const api of apis) {
|
|
3022
|
-
const ctDir =
|
|
3023
|
-
if (!
|
|
3024
|
-
for (const ct of
|
|
3025
|
-
if (
|
|
3620
|
+
const ctDir = import_node_path9.default.join(apiRoot, api, "content-types");
|
|
3621
|
+
if (!import_node_fs9.default.existsSync(ctDir)) continue;
|
|
3622
|
+
for (const ct of import_node_fs9.default.readdirSync(ctDir)) {
|
|
3623
|
+
if (import_node_fs9.default.existsSync(import_node_path9.default.join(ctDir, ct, "schema.json"))) out.push(`api::${api}.${ct}`);
|
|
3026
3624
|
}
|
|
3027
3625
|
}
|
|
3028
3626
|
return out;
|
|
@@ -3032,10 +3630,10 @@ var withLocalized = (obj) => ({
|
|
|
3032
3630
|
i18n: { ...(obj || {}).i18n || {}, localized: true }
|
|
3033
3631
|
});
|
|
3034
3632
|
function patchOne(file, campos) {
|
|
3035
|
-
if (!
|
|
3633
|
+
if (!import_node_fs9.default.existsSync(file)) return { erro: `schema.json n\xE3o encontrado (${file})` };
|
|
3036
3634
|
let schema;
|
|
3037
3635
|
try {
|
|
3038
|
-
schema = JSON.parse(
|
|
3636
|
+
schema = JSON.parse(import_node_fs9.default.readFileSync(file, "utf8"));
|
|
3039
3637
|
} catch (e) {
|
|
3040
3638
|
return { erro: `schema.json ileg\xEDvel: ${e?.message ?? e}` };
|
|
3041
3639
|
}
|
|
@@ -3049,7 +3647,7 @@ function patchOne(file, campos) {
|
|
|
3049
3647
|
changed.push(name);
|
|
3050
3648
|
}
|
|
3051
3649
|
try {
|
|
3052
|
-
|
|
3650
|
+
import_node_fs9.default.writeFileSync(file, JSON.stringify(schema, null, 2) + "\n", "utf8");
|
|
3053
3651
|
} catch (e) {
|
|
3054
3652
|
return { erro: `falha ao gravar schema.json: ${e?.message ?? e}` };
|
|
3055
3653
|
}
|
|
@@ -3060,8 +3658,8 @@ function enableI18n(opts) {
|
|
|
3060
3658
|
if (!allowOutsideDev && !isDev2()) {
|
|
3061
3659
|
return { erro: "habilitar i18n s\xF3 \xE9 permitido em desenvolvimento (NODE_ENV=development)." };
|
|
3062
3660
|
}
|
|
3063
|
-
const srcDir = strapi?.dirs?.app?.src ||
|
|
3064
|
-
const apiRoot =
|
|
3661
|
+
const srcDir = strapi?.dirs?.app?.src || import_node_path9.default.join(process.cwd(), "src");
|
|
3662
|
+
const apiRoot = import_node_path9.default.join(srcDir, "api");
|
|
3065
3663
|
if (!uid || uid === "*") {
|
|
3066
3664
|
const uids = listAllUids(apiRoot);
|
|
3067
3665
|
if (!uids.length) return { erro: `nenhuma content-type encontrada em ${apiRoot}` };
|
|
@@ -3084,9 +3682,9 @@ function enableI18n(opts) {
|
|
|
3084
3682
|
}
|
|
3085
3683
|
|
|
3086
3684
|
// server/src/services/chat.ts
|
|
3087
|
-
var
|
|
3685
|
+
var MODEL5 = process.env.OPENAI_CHAT_MODEL || "gpt-4o";
|
|
3088
3686
|
var MAX_TURNS = 10;
|
|
3089
|
-
var
|
|
3687
|
+
var OPENAI_URL5 = "https://api.openai.com/v1/chat/completions";
|
|
3090
3688
|
var SYSTEM = {
|
|
3091
3689
|
pt: `Voc\xEA \xE9 um assistente embutido no admin do Strapi 5 deste projeto. Voc\xEA N\xC3O \xE9 s\xF3 um guia: voc\xEA consegue EDITAR e PUBLICAR conte\xFAdo de verdade atrav\xE9s das ferramentas.
|
|
3092
3690
|
|
|
@@ -3152,7 +3750,7 @@ If the user shares their screen, an image is attached to the last message \u2014
|
|
|
3152
3750
|
Be concise and actionable. ALWAYS answer in English.`
|
|
3153
3751
|
};
|
|
3154
3752
|
var chat_default2 = ({ strapi }) => ({
|
|
3155
|
-
async chat({ messages, image, lang = "pt", previewUrl, autoPublish = false }) {
|
|
3753
|
+
async chat({ messages, image, lang = "pt", previewUrl, previewStatus = "draft", autoPublish = false }) {
|
|
3156
3754
|
const apiKey = process.env.OPENAI_API_KEY;
|
|
3157
3755
|
if (!apiKey) {
|
|
3158
3756
|
throw new Error(
|
|
@@ -3162,7 +3760,7 @@ var chat_default2 = ({ strapi }) => ({
|
|
|
3162
3760
|
const language = lang === "en" ? "en" : "pt";
|
|
3163
3761
|
const { buscarTexto, editarCampo, publicar, listarLocales, criarLocale, traduzir } = createContentTools(strapi);
|
|
3164
3762
|
const LOCAL_TOOLS = {
|
|
3165
|
-
buscar_texto: (a) => buscarTexto(a?.termo),
|
|
3763
|
+
buscar_texto: (a) => buscarTexto(a?.termo, previewStatus),
|
|
3166
3764
|
editar_campo: (a) => editarCampo(a),
|
|
3167
3765
|
publicar: (a) => publicar(a),
|
|
3168
3766
|
listar_locales: () => listarLocales(),
|
|
@@ -3250,12 +3848,12 @@ PUBLISH POLICY: DRAFT MODE (auto-publish OFF). Do NOT call publicar unless the u
|
|
|
3250
3848
|
convo.push({ role: m.role, content: m.content });
|
|
3251
3849
|
}
|
|
3252
3850
|
});
|
|
3253
|
-
const
|
|
3851
|
+
const callOpenAI2 = async (body) => {
|
|
3254
3852
|
const ctrl = new AbortController();
|
|
3255
3853
|
const timer = setTimeout(() => ctrl.abort(), 6e4);
|
|
3256
3854
|
let res;
|
|
3257
3855
|
try {
|
|
3258
|
-
res = await fetch(
|
|
3856
|
+
res = await fetch(OPENAI_URL5, {
|
|
3259
3857
|
method: "POST",
|
|
3260
3858
|
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
3261
3859
|
body: JSON.stringify(body),
|
|
@@ -3272,8 +3870,8 @@ PUBLISH POLICY: DRAFT MODE (auto-publish OFF). Do NOT call publicar unless the u
|
|
|
3272
3870
|
};
|
|
3273
3871
|
let didWrite = false;
|
|
3274
3872
|
for (let turn = 0; turn < MAX_TURNS; turn++) {
|
|
3275
|
-
const data = await
|
|
3276
|
-
model:
|
|
3873
|
+
const data = await callOpenAI2({
|
|
3874
|
+
model: MODEL5,
|
|
3277
3875
|
max_tokens: 2048,
|
|
3278
3876
|
messages: convo,
|
|
3279
3877
|
...tools2.length > 0 ? { tools: tools2, tool_choice: "auto" } : {}
|
|
@@ -3317,7 +3915,7 @@ PUBLISH POLICY: DRAFT MODE (auto-publish OFF). Do NOT call publicar unless the u
|
|
|
3317
3915
|
const text = (typeof msg.content === "string" ? msg.content : "").trim();
|
|
3318
3916
|
return {
|
|
3319
3917
|
reply: text || "(sem resposta)",
|
|
3320
|
-
model:
|
|
3918
|
+
model: MODEL5,
|
|
3321
3919
|
lang: language,
|
|
3322
3920
|
didWrite,
|
|
3323
3921
|
toolsAvailable: tools2.length
|
|
@@ -3325,7 +3923,7 @@ PUBLISH POLICY: DRAFT MODE (auto-publish OFF). Do NOT call publicar unless the u
|
|
|
3325
3923
|
}
|
|
3326
3924
|
return {
|
|
3327
3925
|
reply: language === "en" ? "(agent turn limit reached)" : "(limite de turnos do agente atingido)",
|
|
3328
|
-
model:
|
|
3926
|
+
model: MODEL5,
|
|
3329
3927
|
lang: language,
|
|
3330
3928
|
didWrite,
|
|
3331
3929
|
toolsAvailable: tools2.length
|
|
@@ -3426,6 +4024,12 @@ var routes_default = {
|
|
|
3426
4024
|
path: "/frontend/integrate",
|
|
3427
4025
|
handler: "frontend.integrate",
|
|
3428
4026
|
config: { policies: [] }
|
|
4027
|
+
},
|
|
4028
|
+
{
|
|
4029
|
+
method: "POST",
|
|
4030
|
+
path: "/frontend/wire",
|
|
4031
|
+
handler: "frontend.wire",
|
|
4032
|
+
config: { policies: [] }
|
|
3429
4033
|
}
|
|
3430
4034
|
]
|
|
3431
4035
|
}
|
|
@@ -3640,6 +4244,10 @@ var register_default = ({ strapi }) => {
|
|
|
3640
4244
|
var index_default = {
|
|
3641
4245
|
register: register_default,
|
|
3642
4246
|
async bootstrap({ strapi }) {
|
|
4247
|
+
try {
|
|
4248
|
+
cleanupStaleFrontend(strapi.dirs.app.root);
|
|
4249
|
+
} catch {
|
|
4250
|
+
}
|
|
3643
4251
|
try {
|
|
3644
4252
|
const r = await runPendingProvision(strapi, strapi.dirs.app.root);
|
|
3645
4253
|
if (r.ran) {
|