Merge remote-tracking branch 'origin/main' into fix-#352-install

This commit is contained in:
a7m-1st 2025-09-23 12:18:46 +03:00
commit ab2b0bb43d
11 changed files with 265 additions and 30 deletions

View file

@ -12,5 +12,11 @@
],
"cSpell.words": [
"Eigent"
],
"i18n-ally.localesPaths": [
"backend/lang",
"server/lang",
"src/i18n",
"src/i18n/locales"
]
}

30
SECURITY.md Normal file
View file

@ -0,0 +1,30 @@
# Security Policy
## Supported Versions
The following versions of Eigent are currently being supported with security updates:
| Version | Supported |
| ------- | ------------------ |
| 0.0.x | :white_check_mark: |
| < 0.0 | :x: |
## Reporting a Vulnerability
If you discover a security vulnerability in Eigent, please report it responsibly:
### How to Report
- **Email**: Send details to info@eigent.ai
- **GitHub**: Use GitHub's private security advisory feature
- **Include**: Detailed description, steps to reproduce, and potential impact
### What to Expect
- **Response Time**: We aim to acknowledge reports within 48 hours
- **Updates**: We will provide updates on the investigation progress weekly
- **Resolution**: Critical vulnerabilities will be addressed within 7 days
- **Credit**: We will credit security researchers in our security advisories (if desired)
### Security Disclosure Policy
- We follow responsible disclosure practices
- We request 90 days to address the vulnerability before public disclosure
- We will coordinate disclosure timing with the reporter

View file

@ -155,7 +155,7 @@ export async function startBackend(setPort?: (port: number) => void): Promise<an
}
//Redirect output
const displayFilteredLogs = (data:String) => {
const displayFilteredLogs = (data: String) => {
if (!data) return;
const msg = data.toString().trimEnd();
if (msg.toLowerCase().includes("error") || msg.toLowerCase().includes("traceback")) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

After

Width:  |  Height:  |  Size: 157 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 156 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 161 KiB

Before After
Before After

View file

@ -13,13 +13,14 @@ import {
Play,
Image,
FileText,
UploadCloud,
} from "lucide-react";
import { useChatStore } from "@/store/chatStore";
import racPause from "@/assets/rac-pause.svg";
import { fetchDelete, proxyFetchDelete } from "@/api/http";
import { useState, useEffect } from "react";
import { useState, useEffect, useRef } from "react";
import { fetchPut } from "@/api/http";
import { Tag } from "../ui/tag";
import { useTranslation } from "react-i18next";
@ -88,6 +89,8 @@ export const BottomInput = ({
}, [chatStore]);
const [isLoading, setIsLoading] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const dragCounter = useRef(0);
const handleTakeControl = (type: "pause" | "resume") => {
setIsLoading(true);
if (type === "pause") {
@ -135,6 +138,64 @@ export const BottomInput = ({
}
};
// drag & drop files
const isFileDrag = (e: React.DragEvent) => {
try {
return Array.from(e.dataTransfer?.types || []).includes("Files");
} catch {
return false;
}
};
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
if (!privacy || isPending || useCloudModelInDev) return;
if (!isFileDrag(e)) return;
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = "copy";
setIsDragging(true);
};
const handleDragEnter = (e: React.DragEvent<HTMLDivElement>) => {
if (!privacy || isPending || useCloudModelInDev) return;
if (!isFileDrag(e)) return;
e.preventDefault();
e.stopPropagation();
dragCounter.current += 1;
setIsDragging(true);
};
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
dragCounter.current = Math.max(0, dragCounter.current - 1);
if (dragCounter.current === 0) setIsDragging(false);
};
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
dragCounter.current = 0;
if (!privacy || isPending || useCloudModelInDev) return;
try {
const dropped = Array.from(e.dataTransfer?.files || []);
if (dropped.length === 0) return;
const current = chatStore.tasks[chatStore.activeTaskId as string].attaches;
const mapped = dropped.map((f: File) => ({
fileName: f.name,
filePath: (f as any).path || f.name,
}));
const files = [
...current.filter((f: File) => !mapped.find((m) => m.filePath === f.filePath)),
...mapped.filter((m) => !current.find((f) => f.filePath === m.filePath)),
];
chatStore.setAttaches(chatStore.activeTaskId as string, files as File[]);
} catch (error) {
console.error("Drop File Error:", error);
}
};
const handleEditQuery = () => {
fetchDelete(`/chat/${chatStore.activeTaskId}`);
const tempTaskId = chatStore.activeTaskId;
@ -312,7 +373,22 @@ export const BottomInput = ({
)}
</div>
) : (
<div className="mr-2 relative z-10 h-auto min-h-[82px] rounded-2xl bg-input-bg-default !px-2 !pb-2 gap-0 space-x-1 shadow-none border-solid border border-zinc-300">
<div
className={`mr-2 relative z-10 h-auto min-h-[82px] rounded-2xl bg-input-bg-default !px-2 !pb-2 gap-0 space-x-1 shadow-none border-solid border border-zinc-300 transition-colors ${isDragging ? 'border-blue-400 bg-blue-50/40' : ''}`}
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{isDragging && (
<div className="pointer-events-none absolute inset-0 z-20 flex flex-col items-center justify-center gap-2 rounded-2xl border-2 border-dashed border-blue-400 bg-blue-50/70 text-blue-700 backdrop-blur-sm">
<UploadCloud className="w-8 h-8" />
<div className="text-sm font-semibold">
Drop files to attach
</div>
</div>
)}
<Textarea
disabled={!privacy || isPending}
ref={textareaRef}

View file

@ -18,8 +18,10 @@ import { useSidebarStore } from "@/store/sidebarStore";
import chevron_left from "@/assets/chevron_left.svg";
import { getAuthStore } from "@/store/authStore";
import { useTranslation } from "react-i18next";
import { proxyFetchGet } from "@/api/http";
import { toast } from "sonner";
function HeaderWin() {
const {t} = useTranslation();
const { t } = useTranslation();
const titlebarRef = useRef<HTMLDivElement>(null);
const controlsRef = useRef<HTMLDivElement>(null);
const [platform, setPlatform] = useState<string>("");
@ -110,6 +112,22 @@ function HeaderWin() {
chatStore.tasks[chatStore.activeTaskId as string]?.summaryTask,
]);
const getReferFriendsLink = async () => {
try {
const res: any = await proxyFetchGet("/api/user/invite_code");
if (res?.invite_code) {
const inviteLink = `https://www.eigent.ai/signup?invite_code=${res.invite_code}`;
await navigator.clipboard.writeText(inviteLink);
toast.success("Invitation link copied!");
} else {
toast.error("Failed to get invite code");
}
} catch (error) {
console.error("Failed to get referral link:", error);
toast.error("Failed to get invitation link");
}
};
return (
<div
className="flex !h-9 items-center justify-between pl-2 py-1 z-50"
@ -151,7 +169,7 @@ function HeaderWin() {
</Button>
{location.pathname !== "/history" && (
<>
{activeTaskTitle === "New Project" ? (
{activeTaskTitle === t("chat.new-project") ? (
<Button
variant="ghost"
size="sm"
@ -162,7 +180,7 @@ function HeaderWin() {
</Button>
) : (
<div className="font-bold leading-10 text-base min-w-10 max-w-56 truncate">
{t("chat.new-project")}
{activeTaskTitle}
</div>
)}
</>
@ -185,9 +203,7 @@ function HeaderWin() {
{t("layout.report-bug")}
</Button>
<Button
onClick={() => {
window.location.href = "https://www.eigent.ai/dashboard";
}}
onClick={getReferFriendsLink}
variant="primary"
size="xs"
className="no-drag text-button-primary-text-default leading-tight"
@ -239,4 +255,4 @@ function HeaderWin() {
);
}
export default HeaderWin;
export default HeaderWin;

View file

@ -139,7 +139,7 @@ export default function SettingGeneral() {
<div className="flex items-center gap-sm">
<Button
onClick={() => {
window.location.href = `https://www.eigent.ai/dashboard`;
window.location.href = `https://www.eigent.ai/dashboard?email=${authStore.email}`;
}}
variant="primary"
size="xs"
@ -216,4 +216,4 @@ export default function SettingGeneral() {
</div>
</div>
);
}
}

View file

@ -12,19 +12,73 @@ import githubIcon from "@/assets/github.svg";
import { Input } from "@/components/ui/input";
import { useState, FC, useEffect } from "react";
import { useTranslation } from "react-i18next";
interface EnvValue {
value: string;
required: boolean;
tip: string;
error?: string;
}
interface MCPEnvDialogProps {
showEnvConfig: boolean;
onClose: () => void;
onConnect: (mcp:any) => void;
onConnect: (mcp: any) => void;
activeMcp?: any;
}
export async function google_check(apiKey: string, searchEngineId: string) {
const query = "hello"; // rand word
const url = `https://www.googleapis.com/customsearch/v1?key=${apiKey}&cx=${searchEngineId}&q=${encodeURIComponent(
query
)}&num=1`;
try {
const res = await fetch(url);
if (!res.ok) {
throw new Error(`Google API error: ${res.status}`);
}
const data = await res.json();
if ("items" in data) {
return { success: true, message: "Google key is valid ✅", sample: data.items[0] };
} else {
return { success: false, message: "Google key invalid ❌", error: data.error };
}
} catch (err: any) {
return { success: false, message: `Google check failed: ${err.message}` };
}
}
export async function exa_check(apiKey: string) {
const query = "hello"; // rand search word
try {
const res = await fetch("https://api.exa.ai/search", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ query }),
});
if (!res.ok) {
const data = await res.json();
throw new Error(`Exa API error: ${res.status} ${data.error}`);
}
const data = await res.json();
if ("results" in data) {
return { success: true, message: "Exa key is valid ✅", sample: data.results[0] };
} else {
return { success: false, message: "Exa key invalid ❌", error: data };
}
} catch (err: any) {
return { success: false, message: `Exa check failed: ${err.message}` };
}
}
export const MCPEnvDialog: FC<MCPEnvDialogProps> = ({
showEnvConfig,
onClose,
@ -33,6 +87,7 @@ export const MCPEnvDialog: FC<MCPEnvDialogProps> = ({
}) => {
const [envValues, setEnvValues] = useState<{ [key: string]: EnvValue }>({});
const [showKeys, setShowKeys] = useState<{ [key: string]: boolean }>({});
const [isValidating, setIsValidating] = useState<boolean>(false);
const { t } = useTranslation();
useEffect(() => {
initializeEnvValues(activeMcp);
@ -42,7 +97,7 @@ export const MCPEnvDialog: FC<MCPEnvDialogProps> = ({
if (mcp?.install_command?.env) {
const initialValues: { [key: string]: EnvValue } = {};
Object.keys(mcp.install_command.env).forEach((key) => {
initialValues[key] = {
value: "",
required: true,
@ -51,11 +106,11 @@ export const MCPEnvDialog: FC<MCPEnvDialogProps> = ({
?.replace(/{{/g, "")
?.replace(/}}/g, "") || "",
};
if(key==='EXA_API_KEY'){
initialValues[key].required=false;
if (key === 'EXA_API_KEY') {
initialValues[key].required = false;
}
if(key==='GOOGLE_REFRESH_TOKEN'){
initialValues[key].required=false;
if (key === 'GOOGLE_REFRESH_TOKEN') {
initialValues[key].required = false;
}
});
setEnvValues(initialValues);
@ -88,15 +143,63 @@ export const MCPEnvDialog: FC<MCPEnvDialogProps> = ({
onClose();
};
const handleConfigureMcpEnvSetting = () => {
setEnvValues({});
setShowKeys({});
const mcp = { ...activeMcp };
const setFieldError = (key: string, error: string) => {
setEnvValues((prev) => ({
...prev,
[key]: {
...prev[key],
error,
},
}));
};
const clearFieldErrors = () => {
setEnvValues((prev) => {
const updated: typeof prev = {};
Object.keys(prev).forEach((key) => {
updated[key] = { ...prev[key], error: "" };
});
return updated;
});
};
const handleConfigureMcpEnvSetting = async () => {
if (isValidating) return;
setIsValidating(true);
clearFieldErrors();
const env: { [key: string]: string } = {};
Object.keys(envValues).map((key) => {
Object.keys(envValues).forEach((key) => {
env[key] = envValues[key]?.value;
});
mcp.install_command.env = env;
// Validate Google API key
if (env["GOOGLE_API_KEY"] && env["SEARCH_ENGINE_ID"]) {
const result = await google_check(env["GOOGLE_API_KEY"], env["SEARCH_ENGINE_ID"]);
if (!result.success) {
setFieldError("GOOGLE_API_KEY", result.message);
setFieldError("SEARCH_ENGINE_ID", result.message);
setIsValidating(false);
return;
}
}
// Validate Exa API key
if (env["EXA_API_KEY"]) {
const result = await exa_check(env["EXA_API_KEY"]);
if (!result.success) {
setFieldError("EXA_API_KEY", result.message);
setIsValidating(false);
return;
}
}
// Save only if all validations succeed
const mcp = { ...activeMcp, install_command: { ...activeMcp.install_command, env } };
setEnvValues({});
setShowKeys({});
setIsValidating(false);
onConnect(mcp);
};
return (
@ -112,7 +215,7 @@ export const MCPEnvDialog: FC<MCPEnvDialogProps> = ({
<DialogTitle className="m-0">
<div className="flex gap-xs items-center justify-start">
<div className="text-base font-bold leading-10 text-text-action">
{t("setting.configure {name} Toolkit", {name: activeMcp?.name})}
{t("setting.configure {name} Toolkit", { name: activeMcp?.name })}
</div>
<CircleAlert size={16} />
</div>
@ -152,7 +255,7 @@ export const MCPEnvDialog: FC<MCPEnvDialogProps> = ({
{Object.keys(activeMcp?.install_command?.env || {}).map((key) => (
<div key={key}>
<div className="text-text-body text-sm leading-normal font-bold">
{key}{envValues[key]?.required&&'*'}
{key}{envValues[key]?.required && '*'}
</div>
<div className="relative">
<Input
@ -175,23 +278,26 @@ export const MCPEnvDialog: FC<MCPEnvDialogProps> = ({
</div>
<div className="text-input-label-default text-xs leading-normal">
{envValues[key]?.tip}
{envValues[key]?.error && (
<div className="text-red-500 text-xs mt-1">{envValues[key]?.error}</div>
)}
{key === 'SEARCH_ENGINE_ID' && (
<div className="mt-1">
{t("setting.get-it-from")}: <a onClick={()=>{
{t("setting.get-it-from")}: <a onClick={() => {
window.location.href = "https://developers.google.com/custom-search/v1/overview";
}} className="underline text-blue-500">{t("setting.google-custom-search-api")}</a>
</div>
)}
{key === 'GOOGLE_API_KEY' && (
<div className="mt-1">
{t("setting.get-it-from")}: <a onClick={()=>{
{t("setting.get-it-from")}: <a onClick={() => {
window.location.href = "https://console.cloud.google.com/apis/credentials";
}} className="underline text-blue-500">{t("setting.google-cloud-console")}</a>
</div>
)}
{key === 'EXA_API_KEY' && (
<div className="mt-1">
{t("setting.get-it-from")}: <a onClick={()=>{
{t("setting.get-it-from")}: <a onClick={() => {
window.location.href = "https://exa.ai";
}} className="underline text-blue-500">Exa.ai</a> (Optional)
</div>
@ -213,8 +319,9 @@ export const MCPEnvDialog: FC<MCPEnvDialogProps> = ({
onClick={handleConfigureMcpEnvSetting}
variant="primary"
size="md"
disabled={isValidating}
>
{t("setting.connect")}
{isValidating ? "Validating..." : t("setting.connect")}
</Button>
</DialogFooter>
</DialogContent>