Skip to content
Merged
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
5 changes: 4 additions & 1 deletion .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@ INDEXNOW_API_TOKEN=
#Open的Key
INDEXNOW_KEY=5b6ef14a7406496b8a2ce8ab17820b34
NEXT_PUBLIC_SITE_URL=https://involutionhell.com
# 内部识别/认证 Key
# 书生 Intern-S1 Key(已弃用:默认免费模型已切换至 GLM-4.6V-Flash)
INTERN_KEY=
# 智谱 AI 开放平台 API Key,聊天与建议接口的默认免费模型 GLM-4.6V-Flash
# 在 https://open.bigmodel.cn/ 注册后获取
ZHIPU_API_KEY=
# Neon 项目 ID
NEON_PROJECT_ID=
# Neon 提供的 Postgres 连接。
Expand Down
7 changes: 7 additions & 0 deletions app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,16 @@ export async function POST(req: Request) {
const proxyReq = req.clone();

// ====== 尝试优雅降级代理到 Java 后端 ======
// Java 后端 /openai/responses/stream 带 @SaCheckLogin,匿名请求必 401;
// 直接跳过代理省掉 5s 超时,也避免 401 文案被上游误显示为"unauthorized"。
const hasAuthToken = Boolean(req.headers.get("x-satoken"));
try {
if (!hasAuthToken) {
throw new Error("Anonymous request, skip backend proxy.");
}
Comment on lines +40 to +42
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

这里用 throw 触发 fallback 会导致所有匿名请求都进入 catch 分支并打印带 stack 的 warn 日志(且文案是“Java Backend unavailable...”),实际会在生产环境造成大量噪音。建议改为显式的 if 分支直接跳过代理(不抛错、不进入 catch),或在 catch 中对匿名场景单独处理为低噪日志/不记录 error 对象。

Copilot uses AI. Check for mistakes.
const backendUrl = process.env.BACKEND_URL;
if (!backendUrl) throw new Error("BACKEND_URL is not configured.");

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5秒超时

Expand Down
16 changes: 10 additions & 6 deletions app/components/assistant-ui/SettingsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const SettingsDialog = ({
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="intern" id="intern" />
<Label htmlFor="intern">InternS1 (Free)</Label>
<Label htmlFor="intern">GLM-4.6V-Flash (Free)</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="openai" id="openai" />
Expand Down Expand Up @@ -176,11 +176,15 @@ export const SettingsDialog = ({
)}

{provider === "intern" && (
<div className="space-y-2">
<div className="text-sm text-muted-foreground">
感谢上海AILab的书生大模型对本项目的算力支持,Intern-AI
模型已预配置,无需提供 API Key。
</div>
<div className="space-y-2 text-sm text-muted-foreground">
<p>
当前免费模型为智谱 GLM-4.6V-Flash(128K
上下文,支持多模态),由站点统一承担费用,无需提供 API Key。
</p>
<p className="text-xs">
🙏 特别鸣谢上海 AI Lab 书生 Intern-S1
为本项目提供首发算力支持——第一个金主,永远铭记。
</p>
</div>
)}
</div>
Expand Down
25 changes: 21 additions & 4 deletions app/components/assistant-ui/assistant-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"use client";

import { BotIcon, ChevronDownIcon } from "lucide-react";
import { BotIcon, ChevronDownIcon, XIcon } from "lucide-react";

import { type FC, forwardRef, useState, useEffect } from "react";
import { type FC, forwardRef, useState, useEffect, useCallback } from "react";
import { AssistantModalPrimitive } from "@assistant-ui/react";

import { Thread } from "@/app/components/assistant-ui/thread";
Expand Down Expand Up @@ -30,6 +30,13 @@ export const AssistantModal: FC<AssistantModalProps> = ({
isLoadingWelcome,
}) => {
const [showBubble, setShowBubble] = useState(false);
// 受控状态:允许模态框内部的 X 按钮主动关闭窗口
// issue #285: 原先只能靠 Trigger/点击外部/Esc 关闭,用户反馈不知道怎么关窗
const [open, setOpen] = useState(false);

const handleCloseModal = useCallback(() => {
setOpen(false);
}, []);

useEffect(() => {
// 检查本次访问是否已关闭过气泡
Expand Down Expand Up @@ -61,7 +68,7 @@ export const AssistantModal: FC<AssistantModalProps> = ({
};

return (
<AssistantModalPrimitive.Root>
<AssistantModalPrimitive.Root open={open} onOpenChange={setOpen}>
<AssistantModalPrimitive.Anchor className="aui-root aui-modal-anchor fixed right-4 bottom-4 size-14">
{/* 自定义气泡组件 */}
{showBubble && (
Expand All @@ -88,8 +95,18 @@ export const AssistantModal: FC<AssistantModalProps> = ({
</AssistantModalPrimitive.Anchor>
<AssistantModalPrimitive.Content
sideOffset={16}
className="aui-root aui-modal-content z-50 h-[500px] w-[400px] overflow-clip rounded-xl border bg-popover p-0 text-popover-foreground shadow-md outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-bottom-1/2 data-[state=closed]:slide-out-to-right-1/2 data-[state=closed]:zoom-out data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:slide-in-from-bottom-1/2 data-[state=open]:slide-in-from-right-1/2 data-[state=open]:zoom-in [&>.aui-thread-root]:bg-inherit"
className="aui-root aui-modal-content relative z-50 h-[500px] w-[400px] overflow-clip rounded-xl border bg-popover p-0 text-popover-foreground shadow-md outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-bottom-1/2 data-[state=closed]:slide-out-to-right-1/2 data-[state=closed]:zoom-out data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:slide-in-from-bottom-1/2 data-[state=open]:slide-in-from-right-1/2 data-[state=open]:zoom-in [&>.aui-thread-root]:bg-inherit"
>
{/* 右上角关闭按钮,issue #285:用户找不到关闭模态框的显式入口 */}
<button
type="button"
onClick={handleCloseModal}
aria-label="Close assistant"
className="aui-modal-close absolute top-2 right-2 z-10 inline-flex size-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<XIcon className="size-4" />
<span className="sr-only">Close assistant</span>
</button>
<Thread
errorMessage={errorMessage}
showSettingsAction={showSettingsAction}
Expand Down
35 changes: 23 additions & 12 deletions lib/ai/providers/intern.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,33 @@
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";

/**
* Create Intern-AI model instance
* Uses environment variable INTERN_KEY for API key
* 在开发环境下,临时映射到 Deepseek 进行测试
* @returns Configured Intern-AI model instance
* 免费聊天模型:智谱 GLM-4.6V-Flash
*
* 历史原因:此前默认走上海 AI Lab 的书生 Intern-S1,现因 key 过期改接智谱
* GLM-4.6V-Flash(同样免费、128K 上下文、支持多模态)。为避免破坏用户
* localStorage 里已有的 provider="intern" 设置,函数/Provider 名保留 `intern`
* 作为语义上的"免费兜底模型"标识。
*
* 开发环境仍可通过 DEEPSEEK_API_KEY 快速切换到 Deepseek 接口做调试。
* 生产环境需要在服务端配置 ZHIPU_API_KEY(智谱 AI 开放平台)。
*/
export function createInternModel() {
const isDev = process.env.NODE_ENV === "development";

const intern = createOpenAICompatible({
name: "intern",
baseURL: isDev
? "https://api.deepseek.com" // 开发环境临时使用 Deepseek 接口测试
: "https://chat.intern-ai.org.cn/api/v1/",
apiKey: isDev ? process.env.DEEPSEEK_API_KEY : process.env.INTERN_KEY,
if (isDev && process.env.DEEPSEEK_API_KEY) {
const deepseek = createOpenAICompatible({
name: "deepseek",
baseURL: "https://api.deepseek.com",
apiKey: process.env.DEEPSEEK_API_KEY,
});
return deepseek("deepseek-chat");
}

const glm = createOpenAICompatible({
name: "zhipu",
baseURL: "https://open.bigmodel.cn/api/paas/v4/",
apiKey: process.env.ZHIPU_API_KEY,
Comment on lines +26 to +29
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

createOpenAICompatible 的 apiKey 直接传入 process.env.ZHIPU_API_KEY(可能为 undefined)。当线上漏配环境变量时会变成下游 401/500,仍可能在 UI 里表现为“unauthorized/请求失败”,和本 PR 目标相悖。建议在这里显式校验 ZHIPU_API_KEY,缺失时抛出带明确指引的错误(例如提示需要在 Vercel 配置该变量),或提供可用的本地兜底。

Suggested change
const glm = createOpenAICompatible({
name: "zhipu",
baseURL: "https://open.bigmodel.cn/api/paas/v4/",
apiKey: process.env.ZHIPU_API_KEY,
const zhipuApiKey = process.env.ZHIPU_API_KEY;
if (!zhipuApiKey || zhipuApiKey.trim() === "") {
throw new Error(
"Missing required environment variable ZHIPU_API_KEY. Configure it on the server (for example in Vercel Project Settings -> Environment Variables) before using the intern provider.",
);
}
const glm = createOpenAICompatible({
name: "zhipu",
baseURL: "https://open.bigmodel.cn/api/paas/v4/",
apiKey: zhipuApiKey,

Copilot uses AI. Check for mistakes.
});

// Use the specific model configured for this project
return intern(isDev ? "deepseek-chat" : "intern-s1");
return glm("glm-4.6v-flash");
}
Loading