Merge remote-tracking branch 'origin/main' into fix-#352-install
6
.vscode/settings.json
vendored
|
|
@ -12,5 +12,11 @@
|
|||
],
|
||||
"cSpell.words": [
|
||||
"Eigent"
|
||||
],
|
||||
"i18n-ally.localesPaths": [
|
||||
"backend/lang",
|
||||
"server/lang",
|
||||
"src/i18n",
|
||||
"src/i18n/locales"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
30
SECURITY.md
Normal 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
|
||||
|
|
@ -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")) {
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 161 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 161 KiB |
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||