Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ COPY web/bun.lock .
RUN bun install
COPY ./web .
COPY ./VERSION .
RUN DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat VERSION) bun run build
RUN DISABLE_ESLINT_PLUGIN='true' NODE_OPTIONS='--max-old-space-size=4096' VITE_REACT_APP_VERSION=$(cat VERSION) bunx vite build --minify=false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep the frontend image build on bun run build.

Calling bunx vite build here sidesteps the repo’s canonical frontend build entrypoint, so Docker can silently diverge from local/CI builds once package.json adds flags, wrappers, or pre/post hooks. Please fold the Vite/minify tweak into the normal build script/config instead of bypassing it.

Based on learnings: Use Bun as the preferred package manager and script runner for the frontend (web/ directory). Use bun install, bun run dev, bun run build, and bun run i18n:* commands.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` at line 9, Replace the direct invocation "bunx vite build
--minify=false" in the Dockerfile RUN line with the canonical frontend
entrypoint by running "bun run build" (preserving the DISABLE_ESLINT_PLUGIN,
NODE_OPTIONS, and VITE_REACT_APP_VERSION env values around the command), and
move any Vite/minify tweaks into the frontend's package.json build script or
Vite config (so that the "build" script in package.json and Vite config control
minification/flags rather than bypassing them); ensure Docker uses Bun as the
package manager/runner for consistency with "bun install", "bun run dev", "bun
run build", and "bun run i18n:*".


FROM golang:1.26.1-alpine@sha256:2389ebfa5b7f43eeafbd6be0c3700cc46690ef842ad962f6c5bd6be49ed82039 AS builder2
ENV GO111MODULE=on CGO_ENABLED=0
Expand Down
2 changes: 1 addition & 1 deletion web/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ import OAuth2Callback from './components/auth/OAuth2Callback';
import PersonalSetting from './components/settings/PersonalSetting';
import Setup from './pages/Setup';
import SetupCheck from './components/layout/SetupCheck';
import Home from './pages/Home';

const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const About = lazy(() => import('./pages/About'));
const UserAgreement = lazy(() => import('./pages/UserAgreement'));
Expand Down
33 changes: 30 additions & 3 deletions web/src/components/common/ErrorBoundary.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,28 @@ import { withTranslation } from 'react-i18next';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
this.state = { hasError: false, errorMessage: '', componentStack: '' };
}

static getDerivedStateFromError() {
return { hasError: true };
static getDerivedStateFromError(error) {
return {
hasError: true,
errorMessage: error?.message || String(error || ''),
};
}

componentDidCatch(error, errorInfo) {
console.error('[ErrorBoundary]', error, errorInfo);
this.setState({
errorMessage: error?.message || String(error || ''),
componentStack: errorInfo?.componentStack || '',
});
Comment on lines +24 to +27

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't expose raw exception details to every user.

Rendering errorMessage and especially componentStack in the fallback leaks internal implementation details on production crashes. Please gate this panel behind a dev/debug flag and keep the default user-facing fallback generic.

Also applies to: 53-70

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/common/ErrorBoundary.jsx` around lines 24 - 27, The
ErrorBoundary currently stores and renders raw errorMessage and componentStack
(see componentDidCatch/setState setting errorMessage and componentStack and the
fallback render block around lines 53-70); change it to avoid exposing details
to users by gating detailed info behind a dev/debug flag: in componentDidCatch
only save full error data when a debug flag is true (use a prop like debug or an
env check such as process.env.NODE_ENV !== 'production'), otherwise set a
generic user-facing message and clear componentStack; update the fallback render
to conditionally show the detailed panel only when debug is enabled and always
render the generic fallback for production users.

}

render() {
if (this.state.hasError) {
const { t } = this.props;
const { errorMessage, componentStack } = this.state;
return (
<div className='flex flex-col justify-center items-center h-screen p-8'>
<Empty
Expand All @@ -42,6 +50,25 @@ class ErrorBoundary extends React.Component {
>
{t('刷新页面')}
</Button>
{errorMessage && (
<div
className='mt-6 w-full max-w-3xl rounded border border-semi-color-border bg-semi-color-bg-1 p-4 text-left'
style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}
>
<div className='text-sm font-semibold mb-2'>Error</div>
<div className='text-xs text-semi-color-text-1'>{errorMessage}</div>
{componentStack && (
<>
<div className='text-sm font-semibold mt-4 mb-2'>
Component Stack
</div>
Comment on lines +58 to +64

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Localize the new diagnostics headings.

If this panel stays, Error and Component Stack should also go through t(...); they currently bypass the app’s i18n flow.

As per coding guidelines: web/src/**/*.{ts,tsx,js,jsx}: Frontend i18n: Use i18next + react-i18next + i18next-browser-languagedetector. Translation files in web/src/i18n/locales/{lang}.json must be flat JSON with Chinese source strings as keys. Use useTranslation() hook and call t('中文key') in components.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/common/ErrorBoundary.jsx` around lines 58 - 64, The
headings "Error" and "Component Stack" in the ErrorBoundary component should be
routed through i18n: import and use the useTranslation hook inside the
ErrorBoundary component (referencing ErrorBoundary, errorMessage, and
componentStack) and replace the hardcoded strings with t('中文 key for Error') and
t('中文 key for Component Stack'); ensure the chosen Chinese keys are added to the
locale JSONs under web/src/i18n/locales/{lang}.json as flat keys and call t(...)
where the divs render the headings.

<div className='text-xs text-semi-color-text-1'>
{componentStack}
</div>
</>
)}
</div>
)}
</div>
);
}
Expand Down
222 changes: 67 additions & 155 deletions web/src/pages/Home/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,11 @@ import {
Button,
Typography,
Input,
ScrollList,
ScrollItem,
} from '@douyinfe/semi-ui';
import { API, showError, copy, showSuccess } from '../../helpers';
import { useIsMobile } from '../../hooks/common/useIsMobile';
import { API_ENDPOINTS } from '../../constants/common.constant';
import { StatusContext } from '../../context/Status';
import { useActualTheme } from '../../context/Theme';
import { marked } from 'marked';
import { useTranslation } from 'react-i18next';
import {
Expand All @@ -39,67 +36,49 @@ import {
IconCopy,
} from '@douyinfe/semi-icons';
import { Link } from 'react-router-dom';
import NoticeModal from '../../components/layout/NoticeModal';
import {
Moonshot,
OpenAI,
XAI,
Zhipu,
Volcengine,
Cohere,
Claude,
Gemini,
Suno,
Minimax,
Wenxin,
Spark,
Qingyan,
DeepSeek,
Qwen,
Midjourney,
Grok,
AzureAI,
Hunyuan,
Xinference,
} from '@lobehub/icons';

const { Text } = Typography;

const Home = () => {
const { t, i18n } = useTranslation();
const [statusState] = useContext(StatusContext);
const actualTheme = useActualTheme();
const [homePageContentLoaded, setHomePageContentLoaded] = useState(false);
const [homePageContent, setHomePageContent] = useState('');
const [noticeVisible, setNoticeVisible] = useState(false);
const isMobile = useIsMobile();
const isDemoSiteMode = statusState?.status?.demo_site_enabled || false;
const docsLink = statusState?.status?.docs_link || '';
const serverAddress =
statusState?.status?.server_address || `${window.location.origin}`;
const endpointItems = API_ENDPOINTS.map((e) => ({ value: e }));
const [endpointIndex, setEndpointIndex] = useState(0);
const isChinese = i18n.language.startsWith('zh');
const currentLanguage =
typeof i18n.language === 'string' && i18n.language
? i18n.language
: 'zh-CN';
const isChinese = currentLanguage.startsWith('zh');
const isExternalHomePage =
typeof homePageContent === 'string' && homePageContent.startsWith('https://');

const displayHomePageContent = async () => {
setHomePageContent(localStorage.getItem('home_page_content') || '');
const res = await API.get('/api/home_page_content');
const { success, message, data } = res.data;
if (success) {
let content = data;
if (!data.startsWith('https://')) {
content = marked.parse(data);
const rawContent = typeof data === 'string' ? data : '';
let content = rawContent;
if (!rawContent.startsWith('https://')) {
content = marked.parse(rawContent);
}
setHomePageContent(content);
localStorage.setItem('home_page_content', content);

// 如果内容是 URL,则发送主题模式
if (data.startsWith('https://')) {
if (rawContent.startsWith('https://')) {
const iframe = document.querySelector('iframe');
if (iframe) {
iframe.onload = () => {
iframe.contentWindow.postMessage({ themeMode: actualTheme }, '*');
iframe.contentWindow.postMessage({ lang: i18n.language }, '*');
iframe.contentWindow.postMessage({ themeMode: 'light' }, '*');
iframe.contentWindow.postMessage({ lang: currentLanguage }, '*');
Comment on lines +80 to +81

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't hardcode the iframe theme to light.

This regresses dark/auto mode for external homepages. web/src/hooks/common/useHeaderBar.js:106-117 already uses actualTheme for iframe sync, but this code now forces 'light' during the homepage handshake.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/Home/index.jsx` around lines 80 - 81, The iframe handshake in
Home/index.jsx is forcing themeMode to 'light' which breaks dark/auto syncing;
update the postMessage to send the current theme value (use the same actualTheme
/ theme variable used in useHeaderBar.js) instead of the hardcoded 'light', and
add a safe fallback (e.g., 'light' only if actualTheme is undefined) so
iframe.contentWindow.postMessage uses the real theme state.

};
Comment on lines 72 to 82

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Attach iframe messaging after the iframe is rendered.

setHomePageContent(content) only schedules the rerender. On a cold load, querySelector('iframe') runs before the iframe exists, so the onload handler is never registered and the external page usually never receives the initial lang payload.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/Home/index.jsx` around lines 72 - 82, The postMessage handler
is being attached before the iframe is rendered because
setHomePageContent(content) is async; update the logic so messaging is attached
after the iframe actually mounts: after calling setHomePageContent (and when
rawContent.startsWith('https://')), move the iframe query/iframe.onload
registration into a follow-up effect or callback that runs after the DOM updates
(e.g., a useEffect that depends on the state updated by setHomePageContent or a
ref callback for the iframe), locate the code around setHomePageContent,
rawContent, document.querySelector('iframe') and iframe.onload, and ensure you
add and clean up the load listener before calling
iframe.contentWindow.postMessage({ themeMode: 'light' }) and postMessage({ lang:
currentLanguage }).

}
}
Expand All @@ -117,44 +96,12 @@ const Home = () => {
}
};

useEffect(() => {
const checkNoticeAndShow = async () => {
const lastCloseDate = localStorage.getItem('notice_close_date');
const today = new Date().toDateString();
if (lastCloseDate !== today) {
try {
const res = await API.get('/api/notice');
const { success, data } = res.data;
if (success && data && data.trim() !== '') {
setNoticeVisible(true);
}
} catch (error) {
console.error('获取公告失败:', error);
}
}
};

checkNoticeAndShow();
}, []);

useEffect(() => {
displayHomePageContent().then();
}, []);

useEffect(() => {
const timer = setInterval(() => {
setEndpointIndex((prev) => (prev + 1) % endpointItems.length);
}, 3000);
return () => clearInterval(timer);
}, [endpointItems.length]);

return (
<div className='w-full overflow-x-hidden'>
<NoticeModal
visible={noticeVisible}
onClose={() => setNoticeVisible(false)}
isMobile={isMobile}
/>
{homePageContentLoaded && homePageContent === '' ? (
<div className='w-full overflow-x-hidden'>
{/* Banner 部分 */}
Expand Down Expand Up @@ -182,33 +129,39 @@ const Home = () => {
<div className='flex flex-col md:flex-row items-center justify-center gap-4 w-full mt-4 md:mt-6 max-w-md'>
<Input
readonly
value={serverAddress}
value={`${serverAddress}${endpointItems[endpointIndex] || ''}`}
className='flex-1 !rounded-full'
size={isMobile ? 'default' : 'large'}
suffix={
<div className='flex items-center gap-2'>
<ScrollList
bodyHeight={32}
style={{ border: 'unset', boxShadow: 'unset' }}
>
<ScrollItem
mode='wheel'
cycled={true}
list={endpointItems}
selectedIndex={endpointIndex}
onSelect={({ index }) => setEndpointIndex(index)}
/>
</ScrollList>
<Button
type='primary'
onClick={handleCopyBaseURL}
icon={<IconCopy />}
className='!rounded-full'
/>
</div>
<Button
type='primary'
onClick={handleCopyBaseURL}
icon={<IconCopy />}
className='!rounded-full'
/>
}
/>
</div>
{endpointItems.length > 0 && (
<div className='flex flex-wrap justify-center gap-2 mt-4 max-w-3xl'>
{endpointItems.slice(0, 6).map((endpoint) => (
<button
key={endpoint}
type='button'
className={`px-3 py-1.5 rounded-full text-xs md:text-sm border transition-colors ${
endpoint === endpointItems[endpointIndex]
? 'bg-blue-500 text-white border-blue-500'
: 'bg-transparent text-semi-color-text-1 border-semi-color-border'
}`}
onClick={() =>
setEndpointIndex(endpointItems.indexOf(endpoint))
}
>
{endpoint}
Comment on lines +132 to +160

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Render the endpoint value, not the whole object.

endpointItems contains { value } objects, so ${endpointItems[endpointIndex]}, key={endpoint}, and {endpoint} all stringify to [object Object]. That breaks the displayed base URL and gives every button the same React key.

Proposed fix
-                      value={`${serverAddress}${endpointItems[endpointIndex] || ''}`}
+                      value={`${serverAddress}${endpointItems[endpointIndex]?.value || ''}`}
@@
-                      {endpointItems.slice(0, 6).map((endpoint) => (
+                      {endpointItems.slice(0, 6).map(({ value }) => (
                         <button
-                          key={endpoint}
+                          key={value}
                           type='button'
                           className={`px-3 py-1.5 rounded-full text-xs md:text-sm border transition-colors ${
-                            endpoint === endpointItems[endpointIndex]
+                            value === endpointItems[endpointIndex]?.value
                               ? 'bg-blue-500 text-white border-blue-500'
                               : 'bg-transparent text-semi-color-text-1 border-semi-color-border'
                           }`}
-                          onClick={() =>
-                            setEndpointIndex(endpointItems.indexOf(endpoint))
-                          }
+                          onClick={() =>
+                            setEndpointIndex(
+                              endpointItems.findIndex((item) => item.value === value),
+                            )
+                          }
                         >
-                          {endpoint}
+                          {value}
                         </button>
                       ))}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/Home/index.jsx` around lines 132 - 160, endpointItems contains
objects like { value }, but the JSX treats each item as a string, causing
[object Object] rendering and duplicate keys; update the input value to use
endpointItems[endpointIndex]?.value (e.g.,
value={`${serverAddress}${endpointItems[endpointIndex]?.value || ''}`), change
the map to use .map((endpoint, idx) => ...) and use key={endpoint.value},
display {endpoint.value}, compare against endpointItems[endpointIndex]?.value
for active styling, and setEndpointIndex(idx) in the onClick handler (avoid
indexOf on objects).

</button>
))}
</div>
)}
</div>

{/* 操作按钮 */}
Expand Down Expand Up @@ -262,72 +215,31 @@ const Home = () => {
{t('支持众多的大模型供应商')}
</Text>
</div>
<div className='flex flex-wrap items-center justify-center gap-3 sm:gap-4 md:gap-6 lg:gap-8 max-w-5xl mx-auto px-4'>
<div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'>
<Moonshot size={40} />
</div>
<div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'>
<OpenAI size={40} />
</div>
<div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'>
<XAI size={40} />
</div>
<div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'>
<Zhipu.Color size={40} />
</div>
<div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'>
<Volcengine.Color size={40} />
</div>
<div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'>
<Cohere.Color size={40} />
</div>
<div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'>
<Claude.Color size={40} />
</div>
<div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'>
<Gemini.Color size={40} />
</div>
<div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'>
<Suno size={40} />
</div>
<div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'>
<Minimax.Color size={40} />
</div>
<div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'>
<Wenxin.Color size={40} />
</div>
<div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'>
<Spark.Color size={40} />
</div>
<div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'>
<Qingyan.Color size={40} />
</div>
<div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'>
<DeepSeek.Color size={40} />
</div>
<div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'>
<Qwen.Color size={40} />
</div>
<div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'>
<Midjourney size={40} />
</div>
<div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'>
<Grok size={40} />
</div>
<div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'>
<AzureAI.Color size={40} />
</div>
<div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'>
<Hunyuan.Color size={40} />
</div>
<div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'>
<Xinference.Color size={40} />
</div>
<div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'>
<Typography.Text className='!text-lg sm:!text-xl md:!text-2xl lg:!text-3xl font-bold'>
30+
</Typography.Text>
</div>
<div className='flex flex-wrap items-center justify-center gap-3 md:gap-4 max-w-5xl mx-auto px-4'>
{[
'OpenAI',
'Claude',
'Gemini',
'DeepSeek',
'Qwen',
'Grok',
'Midjourney',
'Azure OpenAI',
'Volcengine',
'Cohere',
'Moonshot',
'Minimax',
].map((provider) => (
<span
key={provider}
className='px-3 py-2 rounded-full border border-semi-color-border text-sm text-semi-color-text-1'
>
{provider}
</span>
))}
<span className='px-3 py-2 rounded-full border border-semi-color-border text-sm font-semibold'>
30+
</span>
</div>
</div>
</div>
Expand All @@ -336,7 +248,7 @@ const Home = () => {
</div>
) : (
<div className='overflow-x-hidden w-full'>
{homePageContent.startsWith('https://') ? (
{isExternalHomePage ? (
<iframe
src={homePageContent}
className='w-full h-screen border-none'
Expand Down
Loading
Loading