mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
feat: enhance translator functionality and UI
This commit is contained in:
+1301
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -54,13 +54,17 @@ function convertMessages(messages, tools, model) {
|
|||||||
description = `Tool: ${name}`;
|
description = `Tool: ${name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const schema = t.function?.parameters || t.parameters || t.input_schema || {};
|
||||||
|
// Normalize schema: Kiro requires required[] and proper type/properties
|
||||||
|
const normalizedSchema = Object.keys(schema).length === 0
|
||||||
|
? { type: "object", properties: {}, required: [] }
|
||||||
|
: { ...schema, required: schema.required ?? [] };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
toolSpecification: {
|
toolSpecification: {
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
inputSchema: {
|
inputSchema: { json: normalizedSchema }
|
||||||
json: t.function?.parameters || t.parameters || t.input_schema || {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,499 +1,297 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useRef, useEffect } from "react";
|
import { useState } from "react";
|
||||||
import { Card, Button, Select } from "@/shared/components";
|
import { Card, Button } from "@/shared/components";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
// Dynamically import Monaco Editor (client-side only)
|
|
||||||
const Editor = dynamic(() => import("@monaco-editor/react"), { ssr: false });
|
const Editor = dynamic(() => import("@monaco-editor/react"), { ssr: false });
|
||||||
|
|
||||||
const PROVIDERS = [
|
// 7 steps matching requestLogger files exactly
|
||||||
{ value: "antigravity", label: "Antigravity" },
|
const STEPS = [
|
||||||
{ value: "gemini-cli", label: "Gemini CLI" },
|
{ id: 1, label: "Client Request", file: "1_req_client.json", lang: "json", desc: "Raw request from client" },
|
||||||
{ value: "claude", label: "Claude" },
|
{ id: 2, label: "Source Body", file: "2_req_source.json", lang: "json", desc: "After initial conversion" },
|
||||||
{ value: "codex", label: "Codex" },
|
{ id: 3, label: "OpenAI Intermediate", file: "3_req_openai.json", lang: "json", desc: "source → openai" },
|
||||||
{ value: "github", label: "GitHub" },
|
{ id: 4, label: "Target Request", file: "4_req_target.json", lang: "json", desc: "openai → target + URL + headers" },
|
||||||
{ value: "qwen", label: "Qwen" },
|
{ id: 5, label: "Provider Response", file: "5_res_provider.txt", lang: "text", desc: "Raw SSE from provider" },
|
||||||
{ value: "iflow", label: "iFlow AI" },
|
{ id: 6, label: "OpenAI Response", file: "6_res_openai.txt", lang: "text", desc: "target → openai (response)" },
|
||||||
{ value: "kiro", label: "Kiro AI" },
|
{ id: 7, label: "Client Response", file: "7_res_client.txt", lang: "text", desc: "Final response to client" },
|
||||||
{ value: "openai", label: "OpenAI" },
|
|
||||||
{ value: "anthropic", label: "Anthropic" },
|
|
||||||
{ value: "gemini", label: "Gemini" },
|
|
||||||
{ value: "openrouter", label: "OpenRouter" },
|
|
||||||
{ value: "glm", label: "GLM" },
|
|
||||||
{ value: "kimi", label: "Kimi" },
|
|
||||||
{ value: "minimax", label: "MiniMax" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const STEPS = [
|
const EDITOR_OPTIONS = {
|
||||||
{ id: 1, name: "Client Request", file: "1_req_client.json" },
|
minimap: { enabled: false },
|
||||||
{ id: 2, name: "Source Format", file: "2_req_source.json" },
|
fontSize: 12,
|
||||||
{ id: 3, name: "OpenAI Intermediate", file: "3_req_openai.json" },
|
lineNumbers: "on",
|
||||||
{ id: 4, name: "Target Format", file: "4_req_target.json" },
|
scrollBeyondLastLine: false,
|
||||||
{ id: 5, name: "Provider Response", file: "5_res_provider.txt" },
|
wordWrap: "on",
|
||||||
];
|
automaticLayout: true,
|
||||||
|
};
|
||||||
|
|
||||||
export default function TranslatorPage() {
|
export default function TranslatorPage() {
|
||||||
const [provider, setProvider] = useState("antigravity");
|
const [contents, setContents] = useState({});
|
||||||
const [steps, setSteps] = useState({
|
const [expanded, setExpanded] = useState({ 1: true });
|
||||||
1: "",
|
|
||||||
2: "",
|
|
||||||
3: "",
|
|
||||||
4: "",
|
|
||||||
5: "",
|
|
||||||
});
|
|
||||||
const [expanded, setExpanded] = useState({
|
|
||||||
1: true,
|
|
||||||
2: false,
|
|
||||||
3: false,
|
|
||||||
4: false,
|
|
||||||
5: false,
|
|
||||||
});
|
|
||||||
const [loading, setLoading] = useState({});
|
const [loading, setLoading] = useState({});
|
||||||
|
// Detected from step 1: { provider, model, sourceFormat, targetFormat }
|
||||||
|
const [meta, setMeta] = useState(null);
|
||||||
|
|
||||||
const toggleExpand = (stepId) => {
|
const setLoad = (key, val) => setLoading(prev => ({ ...prev, [key]: val }));
|
||||||
setExpanded({ ...expanded, [stepId]: !expanded[stepId] });
|
const setContent = (id, val) => setContents(prev => ({ ...prev, [id]: val }));
|
||||||
|
const toggle = (id) => setExpanded(prev => ({ ...prev, [id]: !prev[id] }));
|
||||||
|
|
||||||
|
const openNext = (nextId) => setExpanded(prev => {
|
||||||
|
const next = {};
|
||||||
|
STEPS.forEach(s => { next[s.id] = false; });
|
||||||
|
next[nextId] = true;
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load file from logs/translator/
|
||||||
|
const handleLoad = async (stepId) => {
|
||||||
|
const step = STEPS.find(s => s.id === stepId);
|
||||||
|
setLoad(`load-${stepId}`, true);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/translator/load?file=${step.file}`);
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
setContent(stepId, data.content);
|
||||||
|
if (stepId === 1) await detectMeta(data.content);
|
||||||
|
} else {
|
||||||
|
alert(data.error || "File not found");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert(e.message);
|
||||||
|
}
|
||||||
|
setLoad(`load-${stepId}`, false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSendToProvider = async () => {
|
// Step 1: detect provider/format from model field
|
||||||
setLoading({ ...loading, "send-provider": true });
|
const detectMeta = async (rawContent) => {
|
||||||
try {
|
try {
|
||||||
const step4Content = steps[4];
|
const body = typeof rawContent === "string" ? JSON.parse(rawContent) : rawContent;
|
||||||
if (!step4Content) {
|
const res = await fetch("/api/translator/translate", {
|
||||||
alert("Please load or generate step 4 content first");
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ step: 1, body })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) setMeta(data.result);
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
};
|
||||||
|
|
||||||
|
const save = (file, content) => fetch("/api/translator/save", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ file, content })
|
||||||
|
}).catch(() => {});
|
||||||
|
|
||||||
|
// Step 1 → Step 3: source → OpenAI intermediate
|
||||||
|
const handleToOpenAI = async () => {
|
||||||
|
setLoad("toOpenAI", true);
|
||||||
|
try {
|
||||||
|
const raw = contents[1];
|
||||||
|
const body = JSON.parse(raw);
|
||||||
|
// Save input: 1_req_client.json + 2_req_source.json (body only)
|
||||||
|
save("1_req_client.json", raw);
|
||||||
|
save("2_req_source.json", JSON.stringify({ timestamp: new Date().toISOString(), headers: {}, body: body.body || body }, null, 2));
|
||||||
|
|
||||||
|
const res = await fetch("/api/translator/translate", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ step: 2, body })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!data.success) { alert(data.error); return; }
|
||||||
|
const str = JSON.stringify(data.result.body, null, 2);
|
||||||
|
setContent(3, str);
|
||||||
|
openNext(3);
|
||||||
|
} catch (e) { alert(e.message); }
|
||||||
|
setLoad("toOpenAI", false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Step 3 → Step 4: OpenAI → target + build URL/headers
|
||||||
|
const handleToTarget = async () => {
|
||||||
|
setLoad("toTarget", true);
|
||||||
|
try {
|
||||||
|
const raw = contents[3];
|
||||||
|
const openaiBody = JSON.parse(raw);
|
||||||
|
// Save input: 3_req_openai.json
|
||||||
|
save("3_req_openai.json", raw);
|
||||||
|
|
||||||
|
const res = await fetch("/api/translator/translate", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ step: 3, body: { ...openaiBody, provider: meta?.provider, model: meta?.model } })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!data.success) { alert(data.error); return; }
|
||||||
|
// Embed provider + model so Send works even without meta
|
||||||
|
const step4Content = { ...data.result, provider: meta?.provider, model: meta?.model };
|
||||||
|
setContent(4, JSON.stringify(step4Content, null, 2));
|
||||||
|
openNext(4);
|
||||||
|
} catch (e) { alert(e.message); }
|
||||||
|
setLoad("toTarget", false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Step 4 → Step 5: send to provider via executor
|
||||||
|
const handleSend = async () => {
|
||||||
|
setLoad("send", true);
|
||||||
|
try {
|
||||||
|
const raw = contents[4];
|
||||||
|
const step4 = JSON.parse(raw);
|
||||||
|
// Save input: 4_req_target.json
|
||||||
|
save("4_req_target.json", raw);
|
||||||
|
|
||||||
|
// Read provider/model from step4 content (embedded during build), fallback to meta
|
||||||
|
const provider = step4.provider || meta?.provider;
|
||||||
|
const model = step4.model || meta?.model;
|
||||||
|
|
||||||
|
if (!provider || !model) {
|
||||||
|
alert("Missing provider or model. Please run step 1 first to detect them.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = JSON.parse(step4Content);
|
|
||||||
|
|
||||||
// Get credentials (you may need to prompt user or use stored credentials)
|
|
||||||
const credentials = {
|
|
||||||
accessToken: prompt("Enter access token (or leave empty):") || undefined,
|
|
||||||
apiKey: prompt("Enter API key (or leave empty):") || undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
const res = await fetch("/api/translator/send", {
|
const res = await fetch("/api/translator/send", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({ provider, model, body: step4.body || step4 })
|
||||||
provider,
|
|
||||||
body,
|
|
||||||
credentials
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await res.json();
|
if (!res.ok) {
|
||||||
|
const err = await res.json().catch(() => ({ error: res.statusText }));
|
||||||
if (data.success) {
|
alert(err.error || "Send failed");
|
||||||
// Update step 5 with provider response
|
return;
|
||||||
setSteps({ ...steps, 5: data.body });
|
|
||||||
|
|
||||||
// Save step 5
|
|
||||||
await fetch("/api/translator/save", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
file: "5_res_provider.txt",
|
|
||||||
content: data.body
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
// Expand step 5
|
|
||||||
setExpanded({ ...expanded, 4: false, 5: true });
|
|
||||||
|
|
||||||
alert(`Request sent! Status: ${data.status} ${data.statusText}`);
|
|
||||||
} else {
|
|
||||||
alert(data.error || "Failed to send request");
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
alert("Error sending request: " + err.message);
|
|
||||||
}
|
|
||||||
setLoading({ ...loading, "send-provider": false });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLoad = async (stepId) => {
|
// Accumulate streaming response
|
||||||
setLoading({ ...loading, [`load-${stepId}`]: true });
|
const reader = res.body.getReader();
|
||||||
try {
|
const decoder = new TextDecoder();
|
||||||
const step = STEPS.find(s => s.id === stepId);
|
let full = "";
|
||||||
const res = await fetch(`/api/translator/load?file=${step.file}`);
|
while (true) {
|
||||||
const data = await res.json();
|
const { done, value } = await reader.read();
|
||||||
if (data.success) {
|
if (done) break;
|
||||||
setSteps({ ...steps, [stepId]: data.content });
|
full += decoder.decode(value, { stream: true });
|
||||||
} else {
|
|
||||||
alert(data.error || "Failed to load file");
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
alert("Error loading file: " + err.message);
|
|
||||||
}
|
|
||||||
setLoading({ ...loading, [`load-${stepId}`]: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLean = (stepId) => {
|
setContent(5, full);
|
||||||
try {
|
openNext(5);
|
||||||
const content = steps[stepId];
|
|
||||||
if (!content) return;
|
|
||||||
|
|
||||||
const obj = JSON.parse(content);
|
// Save to logs/translator/5_res_provider.txt
|
||||||
const leaned = leanJSON(obj);
|
|
||||||
setSteps({ ...steps, [stepId]: JSON.stringify(leaned, null, 2) });
|
|
||||||
} catch (err) {
|
|
||||||
alert("Error parsing JSON: " + err.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFormat = (stepId) => {
|
|
||||||
try {
|
|
||||||
const content = steps[stepId];
|
|
||||||
if (!content) return;
|
|
||||||
|
|
||||||
const obj = JSON.parse(content);
|
|
||||||
setSteps({ ...steps, [stepId]: JSON.stringify(obj, null, 2) });
|
|
||||||
} catch (err) {
|
|
||||||
alert("Error parsing JSON: " + err.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCopy = async (stepId) => {
|
|
||||||
try {
|
|
||||||
const content = steps[stepId];
|
|
||||||
if (!content) return;
|
|
||||||
|
|
||||||
await navigator.clipboard.writeText(content);
|
|
||||||
alert("Copied to clipboard!");
|
|
||||||
} catch (err) {
|
|
||||||
alert("Error copying: " + err.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdate = async (stepId) => {
|
|
||||||
setLoading({ ...loading, [`update-${stepId}`]: true });
|
|
||||||
try {
|
|
||||||
const step = STEPS.find(s => s.id === stepId);
|
|
||||||
const res = await fetch("/api/translator/save", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
file: step.file,
|
|
||||||
content: steps[stepId]
|
|
||||||
})
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
if (data.success) {
|
|
||||||
alert("File saved successfully");
|
|
||||||
} else {
|
|
||||||
alert(data.error || "Failed to save file");
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
alert("Error saving file: " + err.message);
|
|
||||||
}
|
|
||||||
setLoading({ ...loading, [`update-${stepId}`]: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (stepId) => {
|
|
||||||
setLoading({ ...loading, [`submit-${stepId}`]: true });
|
|
||||||
try {
|
|
||||||
// 1. Save current step
|
|
||||||
const currentStep = STEPS.find(s => s.id === stepId);
|
|
||||||
await fetch("/api/translator/save", {
|
await fetch("/api/translator/save", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({ file: "5_res_provider.txt", content: full })
|
||||||
file: currentStep.file,
|
|
||||||
content: steps[stepId]
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
} catch (e) { alert(e.message); }
|
||||||
|
setLoad("send", false);
|
||||||
|
};
|
||||||
|
|
||||||
// Step 4: Send to provider instead of translate
|
const handleCopy = async (id) => {
|
||||||
if (stepId === 4) {
|
if (!contents[id]) return;
|
||||||
const res = await fetch("/api/translator/send", {
|
await navigator.clipboard.writeText(contents[id]);
|
||||||
method: "POST",
|
};
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
provider,
|
|
||||||
body: JSON.parse(steps[4])
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
const handleFormat = (id) => {
|
||||||
const errorData = await res.json();
|
try {
|
||||||
alert(errorData.error || "Failed to send request");
|
const obj = JSON.parse(contents[id]);
|
||||||
setLoading({ ...loading, [`submit-${stepId}`]: false });
|
setContent(id, JSON.stringify(obj, null, 2));
|
||||||
return;
|
} catch { /* not JSON, skip */ }
|
||||||
}
|
};
|
||||||
|
|
||||||
// Read streaming response
|
// Render action button per step
|
||||||
const reader = res.body.getReader();
|
const getAction = (stepId) => {
|
||||||
const decoder = new TextDecoder();
|
if (stepId === 1) return <Button size="sm" icon="arrow_forward" loading={loading["toOpenAI"]} onClick={handleToOpenAI}>→ OpenAI</Button>;
|
||||||
let fullResponse = "";
|
if (stepId === 3) return <Button size="sm" icon="arrow_forward" loading={loading["toTarget"]} onClick={handleToTarget}>→ Target</Button>;
|
||||||
|
if (stepId === 4) return <Button size="sm" icon="send" loading={loading["send"]} onClick={handleSend}>Send</Button>;
|
||||||
while (true) {
|
return null;
|
||||||
const { done, value } = await reader.read();
|
|
||||||
if (done) break;
|
|
||||||
|
|
||||||
const chunk = decoder.decode(value, { stream: true });
|
|
||||||
fullResponse += chunk;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save to step 5
|
|
||||||
setSteps({ ...steps, 5: fullResponse });
|
|
||||||
|
|
||||||
await fetch("/api/translator/save", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
file: "5_res_provider.txt",
|
|
||||||
content: fullResponse
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
setExpanded({ ...expanded, [stepId]: false, 5: true });
|
|
||||||
alert("Request sent to provider successfully!");
|
|
||||||
} else {
|
|
||||||
// Steps 1-3: Translate to next step
|
|
||||||
const res = await fetch("/api/translator/translate", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
step: stepId,
|
|
||||||
provider,
|
|
||||||
body: JSON.parse(steps[stepId])
|
|
||||||
})
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
const nextStepId = stepId + 1;
|
|
||||||
const nextContent = JSON.stringify(data.result, null, 2);
|
|
||||||
|
|
||||||
setSteps({ ...steps, [nextStepId]: nextContent });
|
|
||||||
|
|
||||||
const nextStep = STEPS.find(s => s.id === nextStepId);
|
|
||||||
await fetch("/api/translator/save", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
file: nextStep.file,
|
|
||||||
content: nextContent
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
setExpanded({ ...expanded, [stepId]: false, [nextStepId]: true });
|
|
||||||
} else {
|
|
||||||
alert(data.error || "Translation failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
alert("Error: " + err.message);
|
|
||||||
}
|
|
||||||
setLoading({ ...loading, [`submit-${stepId}`]: false });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8 space-y-6">
|
<div className="p-8 space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-text-main">Translator Debug</h1>
|
<h1 className="text-2xl font-bold text-text-main">Translator Debug</h1>
|
||||||
<p className="text-sm text-text-muted mt-1">
|
<p className="text-sm text-text-muted mt-1">Replay request flow — matches log files</p>
|
||||||
Debug translation flow between formats
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
{meta && (
|
||||||
|
<div className="flex items-center gap-2 flex-wrap justify-end">
|
||||||
|
<MetaBadge label="src" value={meta.sourceFormat} color="blue" />
|
||||||
|
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||||
|
<MetaBadge label="dst" value={meta.targetFormat} color="orange" />
|
||||||
|
<MetaBadge label="provider" value={meta.provider} color="green" />
|
||||||
|
<MetaBadge label="model" value={meta.model} color="purple" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Provider Selector */}
|
{STEPS.map((step) => {
|
||||||
<Card>
|
const action = getAction(step.id);
|
||||||
<div className="p-4 flex items-center gap-4">
|
const isExpanded = !!expanded[step.id];
|
||||||
<div className="flex-1">
|
const content = contents[step.id] || "";
|
||||||
<label className="block text-sm font-medium text-text-main mb-2">
|
|
||||||
Provider
|
|
||||||
</label>
|
|
||||||
<Select
|
|
||||||
value={provider}
|
|
||||||
onChange={(e) => setProvider(e.target.value)}
|
|
||||||
options={PROVIDERS}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-6">
|
return (
|
||||||
<Button
|
<Card key={step.id}>
|
||||||
icon="send"
|
<div className="p-4 space-y-3">
|
||||||
onClick={handleSendToProvider}
|
{/* Step header */}
|
||||||
loading={loading["send-provider"]}
|
<div className="flex items-center justify-between">
|
||||||
>
|
<button onClick={() => toggle(step.id)} className="flex items-center gap-2 flex-1 text-left group">
|
||||||
Send to Provider
|
<span className="material-symbols-outlined text-[20px] text-text-muted group-hover:text-primary transition-colors">
|
||||||
</Button>
|
{isExpanded ? "expand_more" : "chevron_right"}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Steps */}
|
|
||||||
{STEPS.map((step) => (
|
|
||||||
<Card key={step.id}>
|
|
||||||
<div className="p-4 space-y-3">
|
|
||||||
{/* Header with expand/collapse */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<button
|
|
||||||
onClick={() => toggleExpand(step.id)}
|
|
||||||
className="flex items-center gap-2 flex-1 text-left group"
|
|
||||||
>
|
|
||||||
<span className="material-symbols-outlined text-[20px] text-text-muted group-hover:text-primary transition-colors">
|
|
||||||
{expanded[step.id] ? "expand_more" : "chevron_right"}
|
|
||||||
</span>
|
|
||||||
<h3 className="text-sm font-semibold text-text-main">
|
|
||||||
{step.id}. {step.name}
|
|
||||||
</h3>
|
|
||||||
<span className="text-xs text-text-muted ml-2">{step.file}</span>
|
|
||||||
{steps[step.id] && (
|
|
||||||
<span className="text-xs text-green-500 ml-2">
|
|
||||||
({steps[step.id].length} chars)
|
|
||||||
</span>
|
</span>
|
||||||
|
<span className="text-xs font-mono text-text-muted/60 w-4">{step.id}</span>
|
||||||
|
<h3 className="text-sm font-semibold text-text-main">{step.label}</h3>
|
||||||
|
<span className="text-xs text-text-muted/60 font-mono">{step.file}</span>
|
||||||
|
{content && <span className="text-xs text-green-500">({content.length} chars)</span>}
|
||||||
|
</button>
|
||||||
|
{!isExpanded && (
|
||||||
|
<div className="flex gap-1 shrink-0">
|
||||||
|
<Button size="sm" variant="ghost" icon="folder_open" loading={loading[`load-${step.id}`]} onClick={() => handleLoad(step.id)} />
|
||||||
|
{action}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</button>
|
</div>
|
||||||
|
|
||||||
{/* Quick actions when collapsed */}
|
{/* Expanded content */}
|
||||||
{!expanded[step.id] && (
|
{isExpanded && (
|
||||||
<div className="flex gap-2">
|
<>
|
||||||
<Button
|
<div className="border border-border rounded-lg overflow-hidden">
|
||||||
size="sm"
|
<Editor
|
||||||
variant="ghost"
|
height="400px"
|
||||||
icon="folder_open"
|
defaultLanguage={step.lang === "text" ? "plaintext" : "json"}
|
||||||
onClick={() => handleLoad(step.id)}
|
value={content}
|
||||||
loading={loading[`load-${step.id}`]}
|
onChange={(v) => {
|
||||||
/>
|
setContent(step.id, v || "");
|
||||||
{step.id <= 4 && (
|
if (step.id === 1) detectMeta(v || "");
|
||||||
<Button
|
}}
|
||||||
size="sm"
|
theme="vs-dark"
|
||||||
variant="ghost"
|
options={EDITOR_OPTIONS}
|
||||||
icon="arrow_forward"
|
|
||||||
onClick={() => handleSubmit(step.id)}
|
|
||||||
loading={loading[`submit-${step.id}`]}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</div>
|
<div className="flex gap-2 flex-wrap">
|
||||||
|
<Button size="sm" variant="outline" icon="folder_open" loading={loading[`load-${step.id}`]} onClick={() => handleLoad(step.id)}>Load</Button>
|
||||||
|
<Button size="sm" variant="outline" icon="data_object" onClick={() => handleFormat(step.id)}>Format</Button>
|
||||||
|
<Button size="sm" variant="outline" icon="content_copy" onClick={() => handleCopy(step.id)}>Copy</Button>
|
||||||
|
{action}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</Card>
|
||||||
{/* Expandable content */}
|
);
|
||||||
{expanded[step.id] && (
|
})}
|
||||||
<>
|
|
||||||
<div className="relative border border-border rounded-lg overflow-hidden">
|
|
||||||
<Editor
|
|
||||||
height="400px"
|
|
||||||
defaultLanguage="json"
|
|
||||||
value={steps[step.id]}
|
|
||||||
onChange={(value) => setSteps({ ...steps, [step.id]: value || "" })}
|
|
||||||
theme="vs-dark"
|
|
||||||
options={{
|
|
||||||
minimap: { enabled: false },
|
|
||||||
fontSize: 12,
|
|
||||||
lineNumbers: "on",
|
|
||||||
scrollBeyondLastLine: false,
|
|
||||||
wordWrap: "on",
|
|
||||||
automaticLayout: true,
|
|
||||||
formatOnPaste: true,
|
|
||||||
formatOnType: true,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-2 flex-wrap">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
icon="folder_open"
|
|
||||||
onClick={() => handleLoad(step.id)}
|
|
||||||
loading={loading[`load-${step.id}`]}
|
|
||||||
>
|
|
||||||
Load
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
icon="compress"
|
|
||||||
onClick={() => handleLean(step.id)}
|
|
||||||
>
|
|
||||||
Lean
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
icon="content_copy"
|
|
||||||
onClick={() => handleCopy(step.id)}
|
|
||||||
>
|
|
||||||
Copy
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
icon="save"
|
|
||||||
onClick={() => handleUpdate(step.id)}
|
|
||||||
loading={loading[`update-${step.id}`]}
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</Button>
|
|
||||||
{step.id <= 4 && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
icon="arrow_forward"
|
|
||||||
onClick={() => handleSubmit(step.id)}
|
|
||||||
loading={loading[`submit-${step.id}`]}
|
|
||||||
>
|
|
||||||
{step.id === 4 ? "Send to Provider" : "Submit"}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lean function: truncate long text
|
function MetaBadge({ label, value, color }) {
|
||||||
function leanJSON(obj, maxTextLen = 2222) {
|
const colors = {
|
||||||
const result = JSON.parse(JSON.stringify(obj)); // Deep clone
|
blue: "bg-blue-500/10 text-blue-500",
|
||||||
|
orange: "bg-orange-500/10 text-orange-500",
|
||||||
// Recursive function to truncate all strings
|
green: "bg-green-500/10 text-green-500",
|
||||||
function truncateDeep(item) {
|
purple: "bg-purple-500/10 text-purple-500",
|
||||||
if (typeof item === "string") {
|
};
|
||||||
return item.length > maxTextLen ? item.slice(0, maxTextLen) + "..." : item;
|
return (
|
||||||
}
|
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-mono ${colors[color]}`}>
|
||||||
|
<span className="text-text-muted/70 font-sans text-[10px]">{label}:</span>{value}
|
||||||
if (Array.isArray(item)) {
|
</span>
|
||||||
return item.map(truncateDeep);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if (item && typeof item === "object") {
|
|
||||||
const truncated = {};
|
|
||||||
for (const key in item) {
|
|
||||||
truncated[key] = truncateDeep(item[key]);
|
|
||||||
}
|
|
||||||
return truncated;
|
|
||||||
}
|
|
||||||
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
return truncateDeep(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
function truncateContent(content, maxLen) {
|
|
||||||
if (typeof content === "string") {
|
|
||||||
return truncateText(content, maxLen);
|
|
||||||
}
|
|
||||||
if (Array.isArray(content)) {
|
|
||||||
return content.map(part => {
|
|
||||||
if (part.type === "text" && part.text) {
|
|
||||||
return { ...part, text: truncateText(part.text, maxLen) };
|
|
||||||
}
|
|
||||||
return part;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
function truncateText(text, maxLen) {
|
|
||||||
if (!text || text.length <= maxLen) return text;
|
|
||||||
return text.slice(0, maxLen) + "...";
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ export async function GET() {
|
|||||||
const { password, ...safeSettings } = settings;
|
const { password, ...safeSettings } = settings;
|
||||||
|
|
||||||
const enableRequestLogs = process.env.ENABLE_REQUEST_LOGS === "true";
|
const enableRequestLogs = process.env.ENABLE_REQUEST_LOGS === "true";
|
||||||
|
const enableTranslator = process.env.ENABLE_TRANSLATOR === "true";
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
...safeSettings,
|
...safeSettings,
|
||||||
enableRequestLogs,
|
enableRequestLogs,
|
||||||
|
enableTranslator,
|
||||||
hasPassword: !!password
|
hasPassword: !!password
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ export async function GET(request) {
|
|||||||
"2_req_source.json",
|
"2_req_source.json",
|
||||||
"3_req_openai.json",
|
"3_req_openai.json",
|
||||||
"4_req_target.json",
|
"4_req_target.json",
|
||||||
"5_res_provider.txt"
|
"5_res_provider.txt",
|
||||||
|
"6_res_openai.txt",
|
||||||
|
"7_res_client.txt",
|
||||||
|
"7_res_client.json",
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!allowedFiles.includes(file)) {
|
if (!allowedFiles.includes(file)) {
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ export async function POST(request) {
|
|||||||
"2_req_source.json",
|
"2_req_source.json",
|
||||||
"3_req_openai.json",
|
"3_req_openai.json",
|
||||||
"4_req_target.json",
|
"4_req_target.json",
|
||||||
"5_res_provider.txt"
|
"5_res_provider.txt",
|
||||||
|
"6_res_openai.txt",
|
||||||
|
"7_res_client.txt",
|
||||||
|
"7_res_client.json",
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!allowedFiles.includes(file)) {
|
if (!allowedFiles.includes(file)) {
|
||||||
|
|||||||
@@ -1,24 +1,18 @@
|
|||||||
import { NextResponse } from "next/server";
|
|
||||||
import { buildProviderUrl, buildProviderHeaders } from "open-sse/services/provider.js";
|
|
||||||
import { getProviderConnections } from "@/lib/localDb.js";
|
import { getProviderConnections } from "@/lib/localDb.js";
|
||||||
|
import { getExecutor, refreshTokenByProvider } from "open-sse/index.js";
|
||||||
|
|
||||||
export async function POST(request) {
|
export async function POST(request) {
|
||||||
try {
|
try {
|
||||||
const { provider, body } = await request.json();
|
const { provider, model, body } = await request.json();
|
||||||
|
|
||||||
if (!provider || !body) {
|
if (!provider || !model || !body) {
|
||||||
return NextResponse.json({ success: false, error: "Provider and body required" }, { status: 400 });
|
return Response.json({ success: false, error: "provider, model, and body required" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get provider credentials from database
|
|
||||||
const connections = await getProviderConnections({ provider });
|
const connections = await getProviderConnections({ provider });
|
||||||
const connection = connections.find(c => c.isActive !== false);
|
const connection = connections.find(c => c.isActive !== false);
|
||||||
|
|
||||||
if (!connection) {
|
if (!connection) {
|
||||||
return NextResponse.json({
|
return Response.json({ success: false, error: `No active connection for provider: ${provider}` }, { status: 400 });
|
||||||
success: false,
|
|
||||||
error: `No active connection found for provider: ${provider}. Available connections: ${connections.length}`
|
|
||||||
}, { status: 400 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const credentials = {
|
const credentials = {
|
||||||
@@ -30,34 +24,26 @@ export async function POST(request) {
|
|||||||
providerSpecificData: connection.providerSpecificData
|
providerSpecificData: connection.providerSpecificData
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build URL and headers using provider service
|
const executor = getExecutor(provider);
|
||||||
const url = buildProviderUrl(provider, body.model || "test-model", true, {
|
const stream = body.stream !== false;
|
||||||
baseUrlIndex: 0,
|
|
||||||
baseUrl: connection.providerSpecificData?.baseUrl,
|
|
||||||
qwenResourceUrl: connection.providerSpecificData?.resourceUrl
|
|
||||||
});
|
|
||||||
console.log("🚀 ~ POST ~ url:", url)
|
|
||||||
const headers = buildProviderHeaders(provider, credentials, true, body);
|
|
||||||
console.log("🚀 ~ POST ~ headers:", headers)
|
|
||||||
|
|
||||||
// Send request to provider
|
let { response } = await executor.execute({ model, body, stream, credentials });
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "POST",
|
// Auto-refresh token on 401/403 and retry (same as chatCore.js)
|
||||||
headers,
|
if (response.status === 401 || response.status === 403) {
|
||||||
body: JSON.stringify(body)
|
const newCredentials = await refreshTokenByProvider(provider, credentials);
|
||||||
});
|
if (newCredentials?.accessToken || newCredentials?.copilotToken) {
|
||||||
|
Object.assign(credentials, newCredentials);
|
||||||
|
({ response } = await executor.execute({ model, body, stream, credentials }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
console.log("🚀 ~ POST ~ errorText:", errorText)
|
console.error(`[Translator] Provider error ${response.status}:`, errorText.slice(0, 500));
|
||||||
return NextResponse.json({
|
return Response.json({ success: false, error: `Provider error: ${response.status}`, details: errorText }, { status: response.status });
|
||||||
success: false,
|
|
||||||
error: `Provider error: ${response.status} ${response.statusText}`,
|
|
||||||
details: errorText
|
|
||||||
}, { status: response.status });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return streaming response
|
|
||||||
return new Response(response.body, {
|
return new Response(response.body, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "text/event-stream",
|
"Content-Type": "text/event-stream",
|
||||||
@@ -66,7 +52,7 @@ export async function POST(request) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error sending request:", error);
|
console.error("[Translator] Send error:", error);
|
||||||
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
return Response.json({ success: false, error: error.message }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,84 +1,66 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { detectFormat, getTargetFormat, buildProviderUrl, buildProviderHeaders } from "open-sse/services/provider.js";
|
import { detectFormat, getTargetFormat } from "open-sse/services/provider.js";
|
||||||
import { translateRequest } from "open-sse/translator/index.js";
|
import { translateRequest } from "open-sse/translator/index.js";
|
||||||
import { FORMATS } from "open-sse/translator/formats.js";
|
import { FORMATS } from "open-sse/translator/formats.js";
|
||||||
|
import { parseModel } from "open-sse/services/model.js";
|
||||||
import { getProviderConnections } from "@/lib/localDb.js";
|
import { getProviderConnections } from "@/lib/localDb.js";
|
||||||
|
import { getExecutor } from "open-sse/executors/index.js";
|
||||||
|
|
||||||
export async function POST(request) {
|
export async function POST(request) {
|
||||||
try {
|
try {
|
||||||
const { step, provider, body } = await request.json();
|
const { step, body } = await request.json();
|
||||||
|
|
||||||
if (!step || !provider || !body) {
|
if (!step || !body) {
|
||||||
return NextResponse.json({ success: false, error: "Step, provider, and body required" }, { status: 400 });
|
return NextResponse.json({ success: false, error: "Step and body required" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
let result;
|
|
||||||
|
|
||||||
switch (step) {
|
switch (step) {
|
||||||
case 1: {
|
case 1: {
|
||||||
// Step 1: Client → Source (detect format)
|
// Detect provider + formats from 1_req_client.json
|
||||||
// Return format: { timestamp, endpoint, headers, body }
|
const clientBody = body.body || body;
|
||||||
const actualBody = body.body || body;
|
const { provider, model } = parseModel(clientBody.model);
|
||||||
const sourceFormat = detectFormat(actualBody);
|
const sourceFormat = detectFormat(clientBody);
|
||||||
|
const targetFormat = getTargetFormat(provider);
|
||||||
result = {
|
return NextResponse.json({ success: true, result: { provider, model, sourceFormat, targetFormat } });
|
||||||
timestamp: body.timestamp || new Date().toISOString(),
|
|
||||||
endpoint: body.endpoint || "/v1/messages",
|
|
||||||
headers: body.headers || {},
|
|
||||||
body: actualBody,
|
|
||||||
_detectedFormat: sourceFormat
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case 2: {
|
case 2: {
|
||||||
// Step 2: Source → OpenAI
|
// source → OpenAI intermediate (mirrors 3_req_openai.json)
|
||||||
// Return format: { timestamp, headers: {}, body }
|
// Translate source→openai only (half of the pipeline)
|
||||||
const actualBody = body.body || body;
|
const clientBody = body.body || body;
|
||||||
const sourceFormat = detectFormat(actualBody);
|
const { provider, model } = parseModel(clientBody.model);
|
||||||
const targetFormat = FORMATS.OPENAI;
|
const sourceFormat = detectFormat(clientBody);
|
||||||
const model = actualBody.model || "test-model";
|
const stream = clientBody.stream !== false;
|
||||||
const translated = translateRequest(sourceFormat, targetFormat, model, actualBody, true, null, provider);
|
|
||||||
|
|
||||||
result = {
|
// translateRequest(source, OPENAI) = only the first half
|
||||||
timestamp: new Date().toISOString(),
|
const result = translateRequest(sourceFormat, FORMATS.OPENAI, model, clientBody, stream, null, provider);
|
||||||
headers: {},
|
delete result._toolNameMap;
|
||||||
body: translated
|
|
||||||
};
|
return NextResponse.json({ success: true, result: { body: result } });
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case 3: {
|
case 3: {
|
||||||
// Step 3: OpenAI → Target
|
// OpenAI intermediate → target + build URL/headers (mirrors 4_req_target.json)
|
||||||
// Return format: { timestamp, body }
|
const openaiBody = body.body || body;
|
||||||
const actualBody = body.body || body;
|
const provider = body.provider;
|
||||||
const sourceFormat = FORMATS.OPENAI;
|
const model = body.model;
|
||||||
|
|
||||||
|
if (!provider || !model) {
|
||||||
|
return NextResponse.json({ success: false, error: "provider and model required" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
const targetFormat = getTargetFormat(provider);
|
const targetFormat = getTargetFormat(provider);
|
||||||
const model = actualBody.model || "test-model";
|
const stream = openaiBody.stream !== false;
|
||||||
const translated = translateRequest(sourceFormat, targetFormat, model, actualBody, true, null, provider);
|
|
||||||
|
|
||||||
result = {
|
// translateRequest(OPENAI, target) = second half of pipeline
|
||||||
timestamp: new Date().toISOString(),
|
const translated = translateRequest(FORMATS.OPENAI, targetFormat, model, openaiBody, stream, null, provider);
|
||||||
body: translated
|
delete translated._toolNameMap;
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 4: {
|
// Build URL + headers via executor (same as chatCore → executor.execute)
|
||||||
// Step 4: Build final request with real URL and headers
|
|
||||||
// Return format: { timestamp, url, headers, body }
|
|
||||||
const actualBody = body.body || body;
|
|
||||||
const model = actualBody.model || "test-model";
|
|
||||||
|
|
||||||
// Get provider credentials
|
|
||||||
const connections = await getProviderConnections({ provider });
|
const connections = await getProviderConnections({ provider });
|
||||||
const connection = connections.find(c => c.isActive !== false);
|
const connection = connections.find(c => c.isActive !== false);
|
||||||
|
|
||||||
if (!connection) {
|
if (!connection) {
|
||||||
return NextResponse.json({
|
return NextResponse.json({ success: false, error: `No active connection for provider: ${provider}` }, { status: 400 });
|
||||||
success: false,
|
|
||||||
error: `No active connection found for provider: ${provider}`
|
|
||||||
}, { status: 400 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const credentials = {
|
const credentials = {
|
||||||
@@ -90,30 +72,19 @@ export async function POST(request) {
|
|||||||
providerSpecificData: connection.providerSpecificData
|
providerSpecificData: connection.providerSpecificData
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build URL and headers
|
const executor = getExecutor(provider);
|
||||||
const url = buildProviderUrl(provider, model, true, {
|
const url = executor.buildUrl(model, stream, 0, credentials);
|
||||||
baseUrlIndex: 0,
|
const headers = executor.buildHeaders(credentials, stream);
|
||||||
baseUrl: connection.providerSpecificData?.baseUrl,
|
const finalBody = executor.transformRequest(model, translated, stream, credentials);
|
||||||
qwenResourceUrl: connection.providerSpecificData?.resourceUrl
|
|
||||||
});
|
|
||||||
const headers = buildProviderHeaders(provider, credentials, true, actualBody);
|
|
||||||
|
|
||||||
result = {
|
return NextResponse.json({ success: true, result: { url, headers, body: finalBody } });
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
url: url,
|
|
||||||
headers: headers,
|
|
||||||
body: actualBody
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return NextResponse.json({ success: false, error: "Invalid step" }, { status: 400 });
|
return NextResponse.json({ success: false, error: "Invalid step (1-3)" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ success: true, result });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error translating:", error);
|
console.error("Error in translator:", error);
|
||||||
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,9 +19,7 @@ const navItems = [
|
|||||||
{ href: "/dashboard/cli-tools", label: "CLI Tools", icon: "terminal" },
|
{ href: "/dashboard/cli-tools", label: "CLI Tools", icon: "terminal" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Debug items (only show when ENABLE_REQUEST_LOGS=true)
|
|
||||||
const debugItems = [
|
const debugItems = [
|
||||||
// { href: "/dashboard/translator", label: "Translator", icon: "translate" },
|
|
||||||
{ href: "/dashboard/console-log", label: "Console Log", icon: "terminal" },
|
{ href: "/dashboard/console-log", label: "Console Log", icon: "terminal" },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -35,6 +33,14 @@ export default function Sidebar({ onClose }) {
|
|||||||
const [isShuttingDown, setIsShuttingDown] = useState(false);
|
const [isShuttingDown, setIsShuttingDown] = useState(false);
|
||||||
const [isDisconnected, setIsDisconnected] = useState(false);
|
const [isDisconnected, setIsDisconnected] = useState(false);
|
||||||
const [updateInfo, setUpdateInfo] = useState(null);
|
const [updateInfo, setUpdateInfo] = useState(null);
|
||||||
|
const [enableTranslator, setEnableTranslator] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/api/settings")
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => { if (data.enableTranslator) setEnableTranslator(true); })
|
||||||
|
.catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Lazy check for new npm version on mount
|
// Lazy check for new npm version on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -129,6 +135,23 @@ export default function Sidebar({ onClose }) {
|
|||||||
<p className="px-4 text-xs font-semibold text-text-muted/60 uppercase tracking-wider mb-2">
|
<p className="px-4 text-xs font-semibold text-text-muted/60 uppercase tracking-wider mb-2">
|
||||||
Debug
|
Debug
|
||||||
</p>
|
</p>
|
||||||
|
{enableTranslator && (
|
||||||
|
<Link
|
||||||
|
href="/dashboard/translator"
|
||||||
|
onClick={onClose}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-3 px-4 py-2 rounded-lg transition-all group",
|
||||||
|
isActive("/dashboard/translator")
|
||||||
|
? "bg-primary/10 text-primary"
|
||||||
|
: "text-text-muted hover:bg-surface/50 hover:text-text-main"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className={cn("material-symbols-outlined text-[18px]", isActive("/dashboard/translator") ? "fill-1" : "group-hover:text-primary transition-colors")}>
|
||||||
|
translate
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">Translator</span>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
{debugItems.map((item) => (
|
{debugItems.map((item) => (
|
||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
|
|||||||
Reference in New Issue
Block a user