Compare commits

..

114 Commits

Author SHA1 Message Date
hailin 48ee6b382f . 2025-07-09 22:29:55 +08:00
hailin 659f527012 . 2025-07-05 18:45:47 +08:00
hailin c6c6e97583 . 2025-07-05 18:33:06 +08:00
hailin 2644080a5c . 2025-07-05 17:09:09 +08:00
hailin d28c8f9023 . 2025-07-05 17:00:11 +08:00
hailin c532ff5a26 . 2025-07-03 11:13:19 +08:00
hailin fc74b04ebc . 2025-06-24 17:42:28 +08:00
hailin bb451dd6e4 . 2025-06-24 17:39:55 +08:00
hailin 3d203acf51 . 2025-06-24 16:44:59 +08:00
hailin 04db5e7b0b . 2025-06-24 16:38:52 +08:00
hailin 6e77f629e9 . 2025-06-24 00:11:37 +08:00
hailin 3960aeb7a3 . 2025-06-24 00:09:32 +08:00
hailin 745b67930b . 2025-06-24 00:00:09 +08:00
hailin ebf7120505 . 2025-06-23 23:49:28 +08:00
hailin 0b89cc7d86 . 2025-06-23 23:17:14 +08:00
hailin ad108a7794 . 2025-06-23 23:11:38 +08:00
hailin 89cb341b31 . 2025-06-23 22:29:46 +08:00
hailin 7704cd90c3 . 2025-06-23 22:22:40 +08:00
hailin 058485e5f9 . 2025-06-23 22:01:46 +08:00
hailin 6755707425 . 2025-06-23 21:49:45 +08:00
hailin cd859ecf74 . 2025-06-23 21:27:03 +08:00
hailin c7c6561bc9 . 2025-06-23 20:59:33 +08:00
hailin 479852fff0 . 2025-06-23 14:11:53 +08:00
hailin 16d6abb84a . 2025-06-23 13:58:56 +08:00
hailin 2beac56c1e . 2025-06-23 13:52:49 +08:00
hailin a4d2067d70 . 2025-06-23 13:47:53 +08:00
hailin 617780ef4b . 2025-06-23 13:40:08 +08:00
hailin 8d3643ebf1 . 2025-06-23 13:34:24 +08:00
hailin 54306ea870 . 2025-06-23 13:32:21 +08:00
hailin 2a36e7e08a . 2025-06-23 13:27:03 +08:00
hailin 710b6ebdfc . 2025-06-23 13:24:46 +08:00
hailin 0b9998528f . 2025-06-23 13:21:43 +08:00
hailin ea8768003b . 2025-06-23 13:16:17 +08:00
hailin c50c7eeab5 . 2025-06-23 13:12:48 +08:00
hailin d22a1fbb6b . 2025-06-23 13:01:24 +08:00
hailin ab01d11f31 . 2025-06-23 12:57:35 +08:00
hailin 7e8737eddd . 2025-06-23 12:53:45 +08:00
hailin 42ab656ca3 . 2025-06-23 12:49:41 +08:00
hailin a4085bfc89 . 2025-06-23 12:44:23 +08:00
hailin 185d1413ba . 2025-06-23 12:40:07 +08:00
hailin 895ca4beec . 2025-06-23 12:13:27 +08:00
hailin 9139583aba . 2025-06-23 12:10:54 +08:00
hailin 9161d6acd8 . 2025-06-23 12:05:45 +08:00
hailin 1bb5e03fcf . 2025-06-23 12:00:38 +08:00
hailin d57f222683 . 2025-06-23 11:48:20 +08:00
hailin b88044604e . 2025-06-23 11:45:07 +08:00
hailin de8d122b60 . 2025-06-23 11:40:37 +08:00
hailin a75eebf2e6 . 2025-06-23 11:38:28 +08:00
hailin ea69a0b4fe . 2025-06-23 11:11:30 +08:00
hailin b7d409e666 . 2025-06-23 04:56:27 +08:00
hailin 3cf5902218 . 2025-06-23 04:14:35 +08:00
hailin 3089329514 . 2025-06-23 03:48:41 +08:00
hailin 6ce87f07f4 . 2025-06-23 01:59:29 +08:00
hailin 8f79ad000f . 2025-06-23 01:51:46 +08:00
hailin c15873cf67 . 2025-06-20 23:50:29 +08:00
hailin 334e5c50e9 . 2025-06-20 22:36:43 +08:00
hailin b03a310fc7 . 2025-06-20 20:10:55 +08:00
hailin 5c79a6e6ed . 2025-06-20 19:53:39 +08:00
hailin 854a09e1ff . 2025-06-20 19:44:51 +08:00
hailin a3767c42a5 . 2025-06-18 23:12:03 +08:00
hailin e3d6edbb84 . 2025-06-18 21:06:32 +08:00
hailin 9bedaac5d6 . 2025-06-16 23:31:45 +08:00
hailin 8ff5397e10 . 2025-06-16 23:26:45 +08:00
hailin f5d5e22884 . 2025-06-16 21:21:49 +08:00
hailin cdb93cc88b . 2025-06-16 20:58:23 +08:00
hailin 1f3d7b9a9c . 2025-06-16 15:12:32 +08:00
hailin f326ecc072 . 2025-06-16 15:02:57 +08:00
hailin 2a54758324 . 2025-06-16 14:48:27 +08:00
hailin bc91d8f10a . 2025-06-16 14:47:14 +08:00
hailin 39f4448674 . 2025-06-16 14:22:02 +08:00
hailin 0aa86587c3 . 2025-06-16 14:06:26 +08:00
hailin 1ddbf1c13a . 2025-06-16 12:24:53 +08:00
hailin aae8145f6f . 2025-06-16 12:19:44 +08:00
hailin 6ad20f12b2 . 2025-06-16 12:17:46 +08:00
hailin 9499cf2bb6 . 2025-06-16 12:13:13 +08:00
hailin 76478e6835 . 2025-06-16 02:08:25 +08:00
hailin 2cb7353e6a . 2025-06-16 00:09:11 +08:00
hailin d9415a9b37 . 2025-06-15 23:42:36 +08:00
hailin 8e0b0ed369 . 2025-06-15 22:18:52 +08:00
hailin a93f476e46 . 2025-06-15 21:29:38 +08:00
hailin 012e266a09 . 2025-06-15 20:52:52 +08:00
hailin 870e77efdf . 2025-06-15 20:35:27 +08:00
hailin 1734b7bd85 . 2025-06-15 20:10:56 +08:00
hailin 53e2804240 . 2025-06-15 20:07:31 +08:00
hailin 5cebd1239d . 2025-06-15 19:56:34 +08:00
hailin ec9360a4bb . 2025-06-15 17:00:41 +08:00
hailin b2c988fc1b . 2025-06-15 16:54:21 +08:00
hailin 2ce8eb2e7a . 2025-06-15 15:27:02 +08:00
hailin 7bf90733d5 . 2025-06-15 15:21:49 +08:00
hailin 891fa10860 . 2025-06-15 15:07:48 +08:00
hailin 70bbbf97ad . 2025-06-15 14:31:30 +08:00
hailin dbde58e908 . 2025-06-15 13:35:05 +08:00
hailin b16b726309 . 2025-06-15 13:19:56 +08:00
hailin 8f408e03a9 . 2025-06-15 13:10:58 +08:00
hailin 8d6ed1c5c4 . 2025-06-15 13:02:07 +08:00
hailin f3362cf02e . 2025-06-15 12:58:25 +08:00
hailin 5c65849ded . 2025-06-15 12:48:24 +08:00
hailin d49a5a3bea . 2025-06-15 12:42:43 +08:00
hailin 9b27b288c9 . 2025-06-15 00:42:33 +08:00
hailin dd3dff9543 . 2025-06-15 00:08:43 +08:00
hailin 5c413f4af5 . 2025-06-14 23:24:31 +08:00
hailin 5139bed3fc . 2025-06-14 23:14:28 +08:00
hailin bb4bc11af1 . 2025-06-14 23:04:43 +08:00
hailin 891d7f473b . 2025-06-14 22:59:09 +08:00
hailin 80c79cd779 . 2025-06-14 22:55:08 +08:00
hailin 67c8df29e0 . 2025-06-14 22:33:38 +08:00
hailin 0ed6114539 . 2025-06-14 22:11:27 +08:00
hailin 0bce0eed07 . 2025-06-14 15:12:30 +08:00
hailin 65310bc613 . 2025-06-14 15:09:39 +08:00
hailin b0d6d74853 . 2025-06-14 15:00:26 +08:00
hailin 4c4411843a . 2025-06-14 14:52:37 +08:00
hailin 0443185899 . 2025-06-14 13:51:28 +08:00
hailin 35e99c7c9f . 2025-06-14 13:46:02 +08:00
hailin 9052ee0da3 . 2025-06-14 13:41:26 +08:00
39 changed files with 25641 additions and 52644 deletions

View File

@ -1,72 +1,77 @@
# --- 第一阶段:构建阶段 ---
FROM node:18-bullseye-slim AS builder
FROM node:20-bullseye-slim AS builder
# 设置构建参数提前定义后续build、ENV都能用
ARG BLOGAI_HOST=ai.szaiai.com
# 设置构建环境变量
ENV NODE_ENV=production
ENV BLOGAI_HOST=${BLOGAI_HOST}
# 设置工作目录
WORKDIR /app
# 安装 pnpm
RUN npm install -g pnpm
# 复制整项目的代码,排除了.dockerignore中的文件
COPY . ./
ARG BLOGAI_HOST=ai.szaiai.com
ENV NODE_ENV=production
ENV BLOGAI_HOST=${BLOGAI_HOST}
# 拷贝并替换.env
COPY apps/blogai/.env.example apps/blogai/.env
RUN sed -i "s|{{BLOGAI_HOST}}|${BLOGAI_HOST}|g" apps/blogai/.env
WORKDIR /app
# 安装根目录依赖
RUN pnpm install
# 编译子项目 apps/blogai
WORKDIR /app/apps/blogai
RUN pnpm run build
# --- 第二阶段:生产环境运行阶段 ---
FROM node:18-slim AS runner
# 重新定义build参数且默认值一致
ARG BLOGAI_HOST=ai.szaiai.com
# 设置运行环境变量
ENV NODE_ENV=production
ENV PORT=3008
ENV BLOGAI_HOST=${BLOGAI_HOST}
# 安装 pm2
RUN npm install -g pm2
# 设置根目录下的运行环境
WORKDIR /app
# 复制根目录下的node_modules
COPY --from=builder /app/node_modules ./node_modules
# 设置子项目下的运行目录
WORKDIR /app/apps/blogai/
# 只复制子项目运行需要的文件
COPY --from=builder /app/apps/blogai/package.json ./package.json
COPY --from=builder /app/apps/blogai/node_modules ./node_modules
COPY --from=builder /app/apps/blogai/.next ./.next
COPY --from=builder /app/apps/blogai/public ./public
COPY --from=builder /app/apps/blogai/next.config.js ./next.config.js
COPY --from=builder /app/apps/blogai/next-i18next.config.js ./next-i18next.config.js
# 清理无用缓存,减小体积
RUN rm -rf /root/.npm /root/.pnpm-store /tmp/*
# 暴露端口
EXPOSE 3008
# 容器启动命令
CMD ["pm2-runtime", "npm", "--", "start"]
# ✅ 必须在 COPY 前存在 lock 文件,确保版本一致
COPY pnpm-lock.yaml ./
COPY pnpm-workspace.yaml ./
COPY package.json ./
RUN npm install -g pnpm
COPY . ./
# ✅ 配置 .env
COPY apps/blogai/.env.example apps/blogai/.env
RUN sed -i "s|{{BLOGAI_HOST}}|${BLOGAI_HOST}|g" apps/blogai/.env
# ✅ 安装所有 workspace 的依赖,包含 apps/blogai 的 next 等
RUN pnpm install --frozen-lockfile --recursive
WORKDIR /app/apps/blogai
RUN pnpm run build
# --- 第二阶段:运行阶段 ---
FROM node:20-slim AS runner
ARG BLOGAI_HOST=ai.szaiai.com
ENV NODE_ENV=production
ENV PORT=3008
ENV BLOGAI_HOST=${BLOGAI_HOST}
# 安装 supervisor
RUN apt-get update && \
apt-get install -y supervisor curl && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# 设置 supervisor 配置目录
RUN mkdir -p /etc/supervisor/conf.d
# 设置工作目录
WORKDIR /plugai
# 拷贝 supervisor.conf 到指定路径
COPY ./supervisor.conf /etc/supervisor/conf.d/supervisor.conf
# 拷贝 node_modules根目录的
COPY --from=builder /app/node_modules ./node_modules
WORKDIR /plugai/zerostack/t1/
# 拷贝 blogai 应用产物
COPY --from=builder /app/apps/blogai/package.json ./package.json
COPY --from=builder /app/apps/blogai/node_modules ./node_modules
COPY --from=builder /app/apps/blogai/.next ./.next
COPY --from=builder /app/apps/blogai/public ./public
COPY --from=builder /app/apps/blogai/next.config.js ./next.config.js
COPY --from=builder /app/apps/blogai/next-i18next.config.js ./next-i18next.config.js
# 确保 wrapper.sh 可执行权限
COPY ./wrapper.sh /plugai/wrapper.sh
RUN chmod +x /plugai/wrapper.sh
RUN rm -rf /root/.npm /root/.pnpm-store /tmp/*
HEALTHCHECK --interval=30s --timeout=3s --start-period=25s --retries=3 CMD curl -fs http://localhost:3008/api/health/ || exit 1
EXPOSE 3008
# 使用 supervisor 启动
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisor.conf"]

View File

@ -0,0 +1,75 @@
# --- 第一阶段:构建阶段 ---
FROM node:20-bullseye-slim AS builder
ARG BLOGAI_HOST=ai.szaiai.com
ENV NODE_ENV=production
ENV BLOGAI_HOST=${BLOGAI_HOST}
WORKDIR /app
# ✅ 必须在 COPY 前存在 lock 文件,确保版本一致
COPY pnpm-lock.yaml ./
COPY pnpm-workspace.yaml ./
COPY package.json ./
RUN npm install -g pnpm
COPY . ./
COPY apps/blogai/.env.example apps/blogai/.env
RUN sed -i "s|{{BLOGAI_HOST}}|${BLOGAI_HOST}|g" apps/blogai/.env
RUN pnpm install
WORKDIR /app/apps/blogai
RUN pnpm run build
# --- 第二阶段:运行阶段 ---
FROM node:20-slim AS runner
ARG BLOGAI_HOST=ai.szaiai.com
ENV NODE_ENV=production
ENV PORT=3008
ENV BLOGAI_HOST=${BLOGAI_HOST}
# 安装 supervisor
RUN apt-get update && \
apt-get install -y supervisor curl && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# 设置 supervisor 配置目录
RUN mkdir -p /etc/supervisor/conf.d
# 设置工作目录
WORKDIR /plugai
# 拷贝 supervisor.conf 到指定路径
COPY ./supervisor.conf /etc/supervisor/conf.d/supervisor.conf
# 拷贝 node_modules
COPY --from=builder /app/node_modules ./node_modules
WORKDIR /plugai/zerostack/t1/
COPY --from=builder /app/apps/blogai/package.json ./package.json
COPY --from=builder /app/apps/blogai/node_modules ./node_modules
COPY --from=builder /app/apps/blogai/.next ./.next
COPY --from=builder /app/apps/blogai/public ./public
COPY --from=builder /app/apps/blogai/next.config.js ./next.config.js
COPY --from=builder /app/apps/blogai/next-i18next.config.js ./next-i18next.config.js
# 确保 wrapper.sh 可执行权限
COPY ./wrapper.sh /plugai/wrapper.sh
RUN chmod +x /plugai/wrapper.sh
RUN rm -rf /root/.npm /root/.pnpm-store /tmp/*
HEALTHCHECK --interval=30s --timeout=3s --start-period=25s --retries=3 CMD curl -fs http://localhost:3008/api/health/ || exit 1
EXPOSE 3008
# 使用 supervisor 启动
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisor.conf"]

View File

@ -0,0 +1,72 @@
# --- 第一阶段:构建阶段 ---
FROM node:18-bullseye-slim AS builder
# 设置构建参数提前定义后续build、ENV都能用
ARG BLOGAI_HOST=ai.szaiai.com
# 设置构建环境变量
ENV NODE_ENV=production
ENV BLOGAI_HOST=${BLOGAI_HOST}
# 设置工作目录
WORKDIR /app
# 安装 pnpm
RUN npm install -g pnpm
# 复制整项目的代码,排除了.dockerignore中的文件
COPY . ./
# 拷贝并替换.env
COPY apps/blogai/.env.example apps/blogai/.env
RUN sed -i "s|{{BLOGAI_HOST}}|${BLOGAI_HOST}|g" apps/blogai/.env
# 安装根目录依赖
RUN pnpm install
# 编译子项目 apps/blogai
WORKDIR /app/apps/blogai
RUN pnpm run build
# --- 第二阶段:生产环境运行阶段 ---
FROM node:18-slim AS runner
# 重新定义build参数且默认值一致
ARG BLOGAI_HOST=ai.szaiai.com
# 设置运行环境变量
ENV NODE_ENV=production
ENV PORT=3008
ENV BLOGAI_HOST=${BLOGAI_HOST}
# 安装 pm2
RUN npm install -g pm2
# 设置根目录下的运行环境
WORKDIR /app
# 复制根目录下的node_modules
COPY --from=builder /app/node_modules ./node_modules
# 设置子项目下的运行目录
WORKDIR /app/apps/blogai/
# 只复制子项目运行需要的文件
COPY --from=builder /app/apps/blogai/package.json ./package.json
COPY --from=builder /app/apps/blogai/node_modules ./node_modules
COPY --from=builder /app/apps/blogai/.next ./.next
COPY --from=builder /app/apps/blogai/public ./public
COPY --from=builder /app/apps/blogai/next.config.js ./next.config.js
COPY --from=builder /app/apps/blogai/next-i18next.config.js ./next-i18next.config.js
# 清理无用缓存,减小体积
RUN rm -rf /root/.npm /root/.pnpm-store /tmp/*
# 暴露端口
EXPOSE 3008
# 容器启动命令
CMD ["pm2-runtime", "npm", "--", "start"]

View File

@ -60,11 +60,11 @@ export default function SignInPage({ params: { locale } }: { params: { locale: s
passwordValue={password}
/>
<p className="mt-4 text-left text-sm text-md text-black/50 ">
<Link href="/auth/sign-up" className="ml-2 underline text-black hover:underline" onClick={(e) => {
<Link href="/auth/sign-up" className="ml-2 underline text-black hover:underline" onClick={(e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
toast.success('coming soon')
}}>
{t("auth.frogot_password")}
{t("auth.forgot_password")}
</Link>
</p>
</div>

View File

@ -12,7 +12,7 @@ import toast from "react-hot-toast";
import md5 from "md5";
import service from '@/lib/http/service';
import { getService } from '@/lib/http/service';
import { useTranslation } from "react-i18next";
export function EmailSignIn(props: {
@ -51,6 +51,7 @@ export function EmailSignIn(props: {
console.log(email, password, (md5(password)))
setIsLoading(true);
const service = await getService();
await service.post('/api/v1/customer/login', {
user_name: email,
password: md5(password),

View File

@ -12,7 +12,7 @@ import toast from "react-hot-toast";
import md5 from "md5";
import service from '@/lib/http/service';
import { getService } from '@/lib/http/service';
import { useTranslation } from "react-i18next";
export function MixSignIn(props: {
@ -53,6 +53,7 @@ export function MixSignIn(props: {
console.log("--username:", user, "--password:", password, "--md5 password:", (md5(password)))
setIsLoading(true);
const service = await getService();
await service.post('/api/v1/customer/login', {
user_name: user,
password: md5(password),

View File

@ -8,7 +8,7 @@ import { Loading } from "@/components/ui/loading";
import { toast } from "@/components/ui/toaster";
import { cn } from "@/lib/utils";
import { OTPInput, SlotProps } from "input-otp";
import service from "@/lib/http/service";
import { getService } from "@/lib/http/service";
import md5 from "md5";
import { useTranslation } from "react-i18next";
// import { Minus } from "lucide-react";
@ -32,7 +32,7 @@ export const EmailCode: React.FC<Props> = ({ setError, emailValue, passwordValue
)
setIsLoading(true);
const service = await getService();
await service.post('/api/v1/customer/register', {
user_name: emailValue,
email: emailValue,
@ -68,6 +68,7 @@ export const EmailCode: React.FC<Props> = ({ setError, emailValue, passwordValue
const resendCode = async () => {
console.log("resendCode", emailValue)
try {
const service = await getService();
await service.post('/api/v1/common/auth-code', {
user_name: emailValue,
email: emailValue,

View File

@ -7,7 +7,7 @@ import { FadeInStagger } from "@/components/landing/fade-in";
import { Input } from "@/components/ui/input";
import { toast } from "@/components/ui/toaster";
import { useRouter } from "next/navigation";
import service from "@/lib/http/service";
import { getService } from "@/lib/http/service";
import Image from 'next/image';
import showImage from '@/components/images/show.png';
@ -132,6 +132,7 @@ export function EmailSignUp(props: {
// }
// });
const service = await getService();
await service.post('/api/v1/common/auth-code', {
user_name: email,
email: email,

View File

@ -7,7 +7,7 @@ import { FadeInStagger } from "@/components/landing/fade-in";
import { Input } from "@/components/ui/input";
import { toast } from "@/components/ui/toaster";
import { useRouter } from "next/navigation";
import service from "@/lib/http/service";
import { getService } from "@/lib/http/service";
import Image from 'next/image';
import showImage from '@/components/images/show.png';
@ -103,6 +103,7 @@ export function MixSignUp(props: {
}
setIsLoading(true)
const service = await getService();
await service.post('/api/v1/customer/uregister', {
user_name: username,
Referral: referrer,

View File

@ -1,7 +1,6 @@
import { Container } from "@/components/landing/container";
import { FadeIn } from "@/components/landing/fade-in";
import { MdxContent } from "@/components/landing/mdx-content";
// import { PageLinks } from "@/components/landing/page-links";
import { Avatar, AvatarImage } from "@/components/ui/avatar";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { authors } from "@/content/blog/authors";
@ -11,12 +10,13 @@ import Link from "next/link";
import { notFound } from "next/navigation";
import { BLOG_PATH, getContentData, getFilePaths, getPost, getPostContent } from "@/lib/mdx-helper";
import service from "@/lib/http/service";
import { getService } from "@/lib/http/service";
import { Header, NavBack, TimeP } from "@/components/header";
import { baseTitle, baseURL, keywordsRoot } from "@/lib/metadata";
import { useTranslation } from "react-i18next";
import { DetailPageHeader } from '@/components/header'
import { getBaseUrl } from "@/lib/http/get-base-url"; // ✅ 不是 export 它,而是 import 用
export const runtime = "nodejs";
@ -25,6 +25,7 @@ type Props = {
searchParams: { [key: string]: string | string[] | undefined };
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { serialized, frontmatter, headings } = await getPostContent(params.locale, params.slug);
@ -35,7 +36,8 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
return notFound();
}
const baseUrl = process.env.VERCEL_URL ? process.env.VERCEL_URL : baseURL;
//const baseUrl = process.env.VERCEL_URL ? process.env.VERCEL_URL : baseURL;
const baseUrl = await getBaseUrl();
const ogUrl = new URL("/og/blog", baseUrl);
const author = authors[frontmatter.author];
ogUrl.searchParams.set("title", frontmatter.title ?? "");
@ -93,12 +95,14 @@ const BlogArticleWrapper = async ({ params }: { params: { slug: string, locale:
<DetailPageHeader
data={{
id: frontmatter.id,
org_id: frontmatter.org_id,
icon: frontmatter.logo_url,
name: frontmatter.p_name,
model_parameter: frontmatter.model_parameter,
category: frontmatter.tags,
updated_at: frontmatter.date,
company: frontmatter.title,
extra_data: frontmatter.extra_data,
progress: progress,
statusText: statusText
}}

View File

@ -19,6 +19,7 @@ import { dir } from 'i18next';
import { ReactNode } from 'react';
import TranslationsProvider from '@/components/TranslationsProvider';
import initTranslations from '../i18n';
import { ConsoleSilencer } from '@/components/dev-only/console-silencer'
export const runtime = 'edge' // 'nodejs' (default) | 'edge'
@ -68,7 +69,6 @@ export default async function RootLayout({
children: ReactNode;
params: { locale: string };
}) {
const { t, resources } = await initTranslations(locale, i18nNamespaces);
return (
@ -93,6 +93,7 @@ export default async function RootLayout({
resources={resources}>
<Toaster />
<Providers attribute="class" defaultTheme="system" enableSystem>
{/* <ConsoleSilencer /> */}
<div className="flex min-h-screen flex-col">
<main className="flex flex-1 flex-col bg-muted/0.3">
{/* <Header /> */}

View File

@ -122,3 +122,4 @@ export const AI = createAI({
initialUIState,
initialAIState
})

View File

@ -61,6 +61,7 @@ export default function RootLayout({
// </html>
// )
return (
<ThemeProvider
@ -70,7 +71,8 @@ export default function RootLayout({
disableTransitionOnChange
>
{/* <Header /> */}
<AI>{children}</AI>
{children}
{/* <AI>{children}</AI> */}
{/* <Footer /> */}
</ThemeProvider>
)

View File

@ -11,12 +11,34 @@ import Link from "next/link";
import { notFound } from "next/navigation";
import { BLOG_PATH, getContentData, getFilePaths, getPost, getQAContent } from "@/lib/mdx-helper";
import service from "@/lib/http/service";
import { getService } from "@/lib/http/service";
import { Header, NavBack, TimeP } from "@/components/header";
import { baseTitle, baseURL, keywordsRoot } from "@/lib/metadata";
import { getBaseUrl } from "@/lib/http/get-base-url"; // ✅ 不是 export 它,而是 import 用
export const runtime = "nodejs";
// // 推荐用法async 获取
// export async function getBaseUrl() {
// let ip = await getRuntimeEnv("SUPABASE_URL");
// if (!ip) throw new Error("SUPABASE_URL 获取失败,无法构建 baseUrl");
// // 判断协议
// let protocol = "http";
// if (
// typeof window !== "undefined" &&
// window.location &&
// window.location.protocol === "https:"
// ) {
// protocol = "https";
// // ✅ 只在 HTTPS 客户端场景下用 hostname 替换 IP
// ip = window.location.hostname;
// }
// // 拼接
// return `${protocol}://${ip}`;
// }
type Props = {
params: { locale: string, slug: string; title: string; description: string; authorName: string };
searchParams: { [key: string]: string | string[] | undefined };
@ -32,7 +54,8 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
return notFound();
}
const baseUrl = process.env.VERCEL_URL ? process.env.VERCEL_URL : baseURL;
//const baseUrl = process.env.VERCEL_URL ? process.env.VERCEL_URL : baseURL;
const baseUrl = await getBaseUrl();
const ogUrl = new URL("/og/blog", baseUrl);
const author = authors[frontmatter.author];
ogUrl.searchParams.set("title", frontmatter.title ?? "");

View File

@ -10,7 +10,7 @@ import { Header, NavBack } from "@/components/header";
import { Footer } from "@/components/footer";
import { Button, Checkbox, Form, GetProp, Input, Radio, RadioChangeEvent, Space } from "antd";
import { useEffect, useRef, useState } from "react";
import service from "@/lib/http/service";
import { getService } from "@/lib/http/service";
import toast from 'react-hot-toast';
import { UserData } from "@/components/user-menu";
import { useLocalStorage } from "@/lib/hooks/use-local-storage";
@ -132,6 +132,7 @@ export default function PostsPage() {
setIsLoadingInfo(true);
async function initFunc() {
const service = await getService();
await service.post('/api/v1/customer/sub-info', {
email: infoRef.current.email
}, {
@ -178,6 +179,7 @@ export default function PostsPage() {
setInitLoading(true);
async function initFunc() {
const service = await getService();
await service.post('/api/v1/tag/list', {
}, {
headers: {
@ -268,6 +270,7 @@ export default function PostsPage() {
if (isLoading) return
setIsLoading(true);
const service = await getService();
let result: any = await service.post('/api/v1/customer/edit', {
language,
"first_name": values.first_name,
@ -303,6 +306,7 @@ export default function PostsPage() {
console.log("values.first_name", values.first_name)
// setIsLoading(true);
const service = await getService();
let result2: any = await service.post('/api/v1/customer/subscribe', {
language,
"first_name": values.first_name,
@ -331,6 +335,7 @@ export default function PostsPage() {
}
setIsLoading(true);
const service = await getService();
let result3: any = await service.post('/api/v1/customer/unsubscribe', {
language,
"email": values.email,

View File

@ -0,0 +1,42 @@
export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
import { NextResponse } from 'next/server'
import { getRuntimeEnv } from '@/lib/ipconfig'
import fs from 'fs'
// ✅ 重定向 console.log / console.error 到文件
const logStream = fs.createWriteStream('/tmp/health-api.log', { flags: 'a' });
console.log = (...args: any[]) => {
logStream.write(`[log ${new Date().toISOString()}] ${args.join(' ')}\n`);
};
console.error = (...args: any[]) => {
logStream.write(`[error ${new Date().toISOString()}] ${args.join(' ')}\n`);
};
export async function GET() {
let ip: string | null = null
let res: string | undefined = undefined
try {
console.log('✅ [health-check] API HIT');
res = await getRuntimeEnv('SUPABASE_URL')
ip = res ?? null
console.log('[health-check] getRuntimeEnv("SUPABASE_URL") 返回:', res)
console.log('[health-check] ip 赋值结果:', ip)
} catch (e: any) {
ip = null
console.error('[health-check] 捕获异常:', e)
}
console.log('[health-check] process.env.SUPABASE_URL =', process.env.SUPABASE_URL)
return NextResponse.json({
status: 'ok',
ip,
version: '1.0.2', // ✅ 新增版本号字段
name: 'cradle',
})
}

View File

@ -21,6 +21,12 @@ export const {
Google,
],
// ✅ 加这一行,信任所有 Host彻底解决动态部署报错问题
trustHost: true,
// ✅ 加这一行,防止缺失 secret 报错
secret: 'hardcoded-super-secret-key-please-change',
callbacks: {
async jwt({ token, profile }) {
if (profile?.id) {

View File

@ -4,7 +4,7 @@ import React, { useEffect, useRef, useState } from 'react';
import service from '@/lib/http/service';
import { getService } from '@/lib/http/service';
import toast from 'react-hot-toast';
import { Button } from '../ui/button';
import { useRouter } from 'next/navigation';
@ -106,7 +106,8 @@ const NineGrid: React.FC<NineGridProps> = ({
const [selectedCategory, setSelectedCategory] = useState(categoriesRef.current[0].value);
const [articleDataList, setArticleDataList] = useState([]);
//const [articleDataList, setArticleDataList] = useState([]);
const [articleDataList, setArticleDataList] = useState<ArticleData[]>([]);
const [currentPage, setCurrentPage] = React.useState(0);
const [isQA, setIsQA] = useState(false);
@ -151,6 +152,7 @@ const NineGrid: React.FC<NineGridProps> = ({
setInitLoading(true);
const service = await getService();
await service.post('/api/v1/qa/list', {
language,
"page_no": current - 1,
@ -201,7 +203,7 @@ const NineGrid: React.FC<NineGridProps> = ({
tag = tag == categoriesRef.current[0].value ? "" : tag
const service = await getService();
await service.post('/api/v1/news/list', {
"id": 0,
language,
@ -331,6 +333,14 @@ const NineGrid: React.FC<NineGridProps> = ({
</ArticleList>
})}
{/* {articleDataList
.filter(item => item.image_url?.trim() && item.logo_url?.trim()) // 👈 加过滤判断
.map((item: ArticleData, index) => {
return <ArticleList key={index} className="">
<Card {...item} />
</ArticleList>
})} */}
</div>
@ -488,6 +498,7 @@ const Article = () => {
let isMounted = true
async function initFunc() {
const service = await getService();
await service.post('/api/v1/tag/list', {
}, {
headers: {

View File

@ -11,7 +11,7 @@ import React from "react";
import { FadeIn, FadeInStagger } from "../landing/fade-in";
import { useTranslation } from "react-i18next";
import { truncateString } from "@/lib/utils";
import { CloudDownload } from 'lucide-react';
interface CardProps {
image: string;
title: string;
@ -98,21 +98,31 @@ export const Card: React.FC<ArticleData> = (articleData) => {
/>
{/* 🔥 只有当 model_parameter 没有值时显示叹号 */}
{(!articleData.model_parameter) && (
{/* {(!articleData.model_parameter) && (
<div style={{
position: 'absolute',
bottom: '8px',
right: '8px',
color: 'red', // 只保留文字颜色为红色
color: 'red',
fontWeight: 'bold',
fontSize: '24px', // 字体稍微大一点,更容易看到
fontSize: '24px',
zIndex: 2,
}}>
!
</div>
)} */}
{(!articleData.model_parameter) && (
<div style={{
position: 'absolute',
bottom: '8px',
right: '8px',
color: 'red',
zIndex: 2,
}}>
<CloudDownload size={24} strokeWidth={2.5} />
</div>
)}
</div>
</figure>

View File

@ -277,50 +277,6 @@ export function ChatModel({
}
}
// const isSocket = true
// const { messages, append, reload, stop, isLoading, input, setInput } =
// useChat({
// experimental_onFunctionCall: functionCallHandler,
// initialMessages,
// id,
// body: {
// id
// },
// onResponse(response) {
// setIsGenerating(true)
// if (!isChatPage) {
// router.prefetch(`${i18n.language}/chat/${id}`, {
// kind: PrefetchKind.FULL
// })
// }
// if (response.status === 401) {
// toast.error(response.statusText)
// }
// },
// onFinish() {
// setIsGenerating(false)
// if (!isChatPage) {
// history.pushState({}, '', `${i18n.language}/chat/${id}`)
// history.go(1)
// }
// }
// })
// const optins = !!q ? {
// // initialInput: `{
// // "method": "REQUEST",
// // "params":
// // [
// // "@account",
// // "@balance"
// // ],
// // "id": 12
// // }`
// initialInput: q
// } : {}
// const { messages, append, isLoading, input, setInput, } = useISDK('ws://116.213.39.234:8083/ws', optins);
const { messages, append, setMessages, reload, stop, isLoading, isSocket, input, setInput } =
useISDK({
api: `${process.env.NEXT_PUBLIC_CLIENT_BASE_WS}`,

View File

@ -0,0 +1,26 @@
'use client'
import { useEffect } from 'react'
export function ConsoleSilencer() {
if (typeof window !== 'undefined' && process.env.NODE_ENV === 'development') {
// 全局替换 console 方法(同步执行)
console.log = () => {}
console.debug = () => {}
console.info = () => {}
console.warn = () => {}
console.error = () => {}
}
useEffect(() => {
if (process.env.NODE_ENV === 'development') {
console.log = () => {};
console.debug = () => {};
console.info = () => {};
console.warn = () => {};
console.error = () => {};
}
}, [])
return null
}

View File

@ -11,7 +11,7 @@ import Link from "next/link";
import { Container } from "@/components/landing/container";
import { FadeIn } from "@/components/landing/fade-in";
import { NewsletterForm } from "@/components/landing/newsletter";
import service from '@/lib/http/service';
import { getService } from '@/lib/http/service';
import { useLocalStorage } from '@/lib/hooks/use-local-storage';
import { UserData } from './user-menu';
import toast from 'react-hot-toast';
@ -68,6 +68,7 @@ export function Footer({ className, ...props }: React.ComponentProps<'p'>) {
console.log('Finish:', values);
if (isLoading) return
setIsLoading(true);
const service = await getService();
await service.post('/api/v1/customer/subscribe', {
"first_name": values.first_name,
"email": values.email,

View File

@ -0,0 +1,682 @@
'use client'
import React from 'react'
import { Suspense } from 'react'
import { auth } from '@/auth'
import { clearChats } from '@/app/actions'
import { Sidebar } from '@/components/sidebar'
import { SidebarList } from '@/components/sidebar-list'
import { IconSeparator } from '@/components/ui/icons'
import { SidebarFooter } from '@/components/sidebar-footer'
import { ClearHistory } from '@/components/clear-history'
import { UserMenu, UserData } from '@/components/user-menu'
import { LoginButton } from '@/components/login-button'
import {
Tooltip,
TooltipContent,
TooltipTrigger
} from '@/components/ui/tooltip'
import { Badge } from '@/components/ui/badge'
import { ConnectButton } from '@/components/connect-button'
import { SettingsDropDown } from './settings-drop-down'
import { useLocalStorage } from '@/lib/hooks/use-local-storage'
import { Button } from './ui/button'
import Image from 'next/image';
import logoImage from '@/components/images/logo.png';
import { useRouter } from 'next/navigation'
import { Flex, Text } from '@radix-ui/themes';
import Link from 'next/link'
import { useTranslation } from 'react-i18next'
import { message } from 'antd'
import { LogoAI } from '@/components/chat'
import { useState, useRef, } from "react";
import { Trash2, CloudDownload } from "lucide-react";
import { useEffect } from "react";
import {
BadgeInfo,
Tags,
CalendarClock,
Building2
} from "lucide-react";
export function Header() {
const router = useRouter();
const { t } = useTranslation();
const [userData, setUserData] = useLocalStorage(
'UserData',
{
auth_token: "",
id: 1,
login_ip: "",
login_time: 0,
role: "",
user_name: "",
version: ""
} as UserData
)
const soonFunc = () => {
message.info(t("soon"))
}
return (
<header className="sticky top-0 z-50 flex shrink-0 items-center justify-between bg-background px-4 py-[1.5rem] w-[80%] mx-auto">
<a href="/">
<LogoAI className="flex text-center ml-0" />
</a>
<div className="flex items-center justify-between ">
<div className="flex items-center ">
{(!!userData && !!userData.user_name) ? (
<div className='flex text-[#1A1A1A]'>
<UserMenu user={userData} />
</div>
) : (
<div className='flex gap-4 grid-cols-2 text-[#1A1A1A]'>
<Button variant="ghost" className='text-base bg-[#f4f4f5] hover:bg-[#e5e7eb]'>
<Link href="/#subscribe-target"
>
{t('subscribe.subscribe')}
</Link>
</Button>
<LoginButton
variant="ghost"
// variant="link"
showGithubIcon={true}
text={t('login')}
className="-ml-2 text-base bg-[#f4f4f5] hover:bg-[#e5e7eb]"
/>
</div>
)}
</div>
</div>
</header>
)
}
import { getRuntimeEnv } from "@/lib/ipconfig";
export async function getWsBase() {
let ip = await getRuntimeEnv("SUPABASE_URL");
if (!ip) throw new Error("SUPABASE_URL 获取失败,无法构建 wsBase");
let wsProtocol = "ws";
if (typeof window !== "undefined") {
// ✅ 客户端环境(浏览器执行)
if (window.location.protocol === "https:") {
wsProtocol = "wss";
ip = window.location.hostname; // ✅ 用 hostname 避免 HTTPS + IP 的 TLS 报错
}
} else {
// ✅ SSR 环境,延迟导入 headers不能放顶层
const { headers } = await import("next/headers");
const hdrs = headers();
const forwardedProto = hdrs.get("x-forwarded-proto");
const hostHeader = hdrs.get("host");
if (forwardedProto === "https") {
wsProtocol = "wss";
}
if (hostHeader) {
// ✅ host 可能带端口,需处理
ip = hostHeader.includes(":") ? hostHeader.split(":")[0] : hostHeader;
}
}
const finalUrl = `${wsProtocol}://${ip}/api/v1/deploy/ws`;
console.log("✅ [WebSocket] Final URL:", finalUrl);
return finalUrl;
}
export function DetailPageHeader({ data }: { data: any }) {
const [loading, setLoading] = useState(false);
const [statusText, setStatusText] = useState(data?.statusText || "加载中...");
const [progress, setProgress] = useState(data?.progress || "0%");
const [progressBarColor, setProgressBarColor] = useState("bg-blue-500"); // ✅ 默认蓝色
const [showProgressBar, setShowProgressBar] = useState(false);
const [showDelete, setShowDelete] = useState(false);
const [hasWSConnected, setHasWSConnected] = useState(false);
const [statusLoaded, setStatusLoaded] = useState(false);
const [canDeploy, setCanDeploy] = useState(true);
const [currentStatus, setCurrentStatus] = useState(""); // 当前部署状态running / stopped
const [switchLoading, setSwitchLoading] = useState(false); // 控制按钮 loading 状态
const [downloadPercent, setDownloadPercent] = useState("0"); // 下载百分比0 ~ 100
const [showDownloadBar, setShowDownloadBar] = useState(false);
const socketRef = useRef<WebSocket | null>(null);
const { t } = useTranslation();
console.log("...........model_parameter =", data?.model_parameter);
// const hasNonEmptyExtraData = data?.extra_data && typeof data.extra_data === 'object' && !Array.isArray(data.extra_data) && Object.keys(data.extra_data).length > 0 data?.model_parameter !== 0 &&
// data?.model_parameter !== "";
const hasNonEmptyExtraData =
data?.extra_data &&
typeof data.extra_data === 'object' &&
!Array.isArray(data.extra_data) &&
Object.keys(data.extra_data).length > 0 &&
data?.model_parameter !== 0 &&
data?.model_parameter !== "";
const initWebSocket = async (userName: string, id: number) => {
if (socketRef.current) socketRef.current.close();
//const wsBase = process.env.NEXT_PUBLIC_CLIENT_BASE_WS;
const wsBase = await getWsBase();
const socket = new WebSocket(`${wsBase}/status/${userName}/${id}`);
socketRef.current = socket;
socket.onopen = () => console.log("WebSocket 已连接");
socket.onmessage = (event) => {
const msg = event.data;
console.log("收到进度信息:", msg);
setStatusText(msg);
const percentMatch = msg.match(/(\d+)%/);
if (percentMatch && percentMatch[1]) {
setProgress(percentMatch[1] + "%");
if (percentMatch[1] === "100") {
console.log("部署完成 ✅,重新拉取状态!");
fetchDeployStatus();
}
}
setProgressBarColor("bg-blue-500"); // 保持默认颜色
};
socket.onerror = (err) => {
console.error("WebSocket 出错:", err);
setStatusText(t("deploy.ws_error"));
};
socket.onclose = () => {
console.log("🔌 WebSocket 已关闭");
setHasWSConnected(false);
socketRef.current = null;
};
setHasWSConnected(true);
};
const handleClick = async (source: "icon" | "info") => {
setLoading(true);
setStatusText(source === "icon" ? "正在处理图标操作..." : "正在处理信息操作...");
setShowProgressBar(true)
try {
const userData = JSON.parse(localStorage.getItem("UserData") || "null");
if (!userData?.user_name) {
setStatusText("未登录,跳转中...");
window.location.href = "/auth/sign-in/";
return;
}
const userName = userData.user_name;
const id = data?.id;
if (!id) {
setStatusText("缺少组件 ID");
return;
}
const res = await fetch("/api/v1/deploy/deploy", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id, user_name: userName }),
});
const json = await res.json();
if (json?.header?.code === 0) {
setStatusText("部署已启动,监听中...");
setProgressBarColor("bg-blue-500");
initWebSocket(userName, id);
} else {
setStatusText(json.header.message || "操作失败(后端)");
}
} catch (err) {
console.error("请求出错:", err);
setStatusText("请求失败");
} finally {
setLoading(false);
}
};
const handleDelete = async () => {
if (loading) return;
const confirmed = window.confirm(t("deploy.confirm_delete"));
if (!confirmed) return;
setLoading(true);
setStatusText(t("deploy.deleting"));
setProgressBarColor("bg-gray-400");
try {
const userData = JSON.parse(localStorage.getItem("UserData") || "null");
const userName = userData?.user_name;
const id = data?.id;
if (!userName || !id) {
setStatusText(t("deploy.missing_user_or_id"));
return;
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 60_000); // ⏰ 60秒超时
const res = await fetch("/api/v1/deploy/delete", {
method: "POST",
headers: { "Content-Type": "application/json" },
signal: controller.signal,
body: JSON.stringify({ user_name: userName, n_id: id }),
});
clearTimeout(timeoutId); // ✅ 成功返回前清除 timeout
if (!res.ok) {
throw new Error(`HTTP 请求失败: ${res.status}`);
}
const json = await res.json();
if (json?.header?.code === 0) {
setStatusText(t("deploy.deletion_success"));
setProgress("0%");
setShowDelete(false); // 删除成功后隐藏按钮
await fetchDeployStatus(); // ✅ 删除后刷新真实状态
} else {
setStatusText(json?.header?.message || "删除失败(后端返回错误)");
}
} catch (err) {
if ((err as any).name === "AbortError") {
setStatusText(t("deploy.timeout"));
} else {
console.error("删除请求出错:", err);
setStatusText(t("deploy.deletion_failed_network"));
}
} finally {
setLoading(false);
}
};
const fetchDeployStatus = async () => {
try {
const result = await fetch("/api/v1/deploy/status", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id: data?.id }),
}).then((res) => res.json());
console.log("====> deploy status result:", result);
setStatusLoaded(true);
if (!result) {
setStatusText(t("deploy.empty_response"));
setShowDelete(false);
return;
}
const code = result?.header?.code;
const status = result?.data?.data?.status;
setCurrentStatus(status || ""); //...............................................
const userData = JSON.parse(localStorage.getItem("UserData") || "null");
const userName = userData?.user_name;
const id = data?.id;
console.log("🟡 状态码 code =", code, "状态 status =", status);
if (code === 1006) {
setStatusText(t("deploy.not_deployed"));
setShowDelete(false);
setProgress("0%");
setShowProgressBar(false);
setCanDeploy(true); // ✅ 允许部署
return;
}
if (status === "deploying" && userName && id && !hasWSConnected) {
setStatusText(t("deploy.connecting"));
initWebSocket(userName, id);
setShowProgressBar(true);
setCanDeploy(false); // ✅ 正在部署中,禁止点击
}
if (status === "running" || status === "stopped") {
console.log("✅ 允许删除status =", status, ")");
setShowDelete(true);
setProgress("100%");
setShowProgressBar(true);
setCanDeploy(false); // ✅ 已部署/已停止,不允许再次 deploy
if (status === "running") {
setStatusText(t("deploy.running"));
setProgressBarColor("bg-green-500");
} else if (status === "stopped") {
setStatusText(t("deploy.stopped"));
setProgressBarColor("bg-gray-400");
}
} else {
console.log("❌ 不允许删除status =", status, ")");
setShowDelete(false);
setProgressBarColor("bg-blue-500");
}
} catch (err) {
console.error("获取状态失败:", err);
setStatusText(t("deploy.fetch_failed"));
setShowDelete(false);
}
};
const handleSwitchStatus = async () => {
if (!data?.id) return;
setSwitchLoading(true);
const userData = JSON.parse(localStorage.getItem("UserData") || "null");
const userName = userData?.user_name;
const id = data?.id;
if (!userName || !id) {
setStatusText("缺少组件 ID 或用户信息");
return;
}
if (!userName) {
setStatusText("未登录,跳转中...");
window.location.href = "/auth/sign-in/";
return;
}
const endpoint = currentStatus === "running" ? "/api/v1/deploy/stop" : "/api/v1/deploy/start";
try {
const res = await fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user_name: userName, n_id: id }),
});
const json = await res.json();
if (json?.header?.code === 0) {
setStatusText(currentStatus === "running" ? "已停止" : "已启动");
fetchDeployStatus(); // ✅ 状态切换成功后刷新
} else {
setStatusText(json?.header?.message || "操作失败(后端)");
}
} catch (err) {
console.error("切换请求失败:", err);
setStatusText("网络异常");
} finally {
setSwitchLoading(false);
}
};
useEffect(() => {
fetchDeployStatus();
}, [data?.id]);
useEffect(() => {
return () => {
if (socketRef.current) {
socketRef.current.close();
}
};
}, []);
const isImagePath =
typeof data?.icon === "string" &&
(data.icon.startsWith("http") || data.icon.startsWith("/"));
const resolvedIconSrc =
isImagePath && !data.icon.startsWith("http")
? process.env.NEXT_PUBLIC_CLIENT_IMAGE_URL + data.icon
: data.icon;
return (
<div className="sticky top-0 z-30 bg-white">
<div className="mt-4 mb-1 px-6 lg:px-8 w-11/12 lg:w-2/3 xl:w-3/5 mx-auto">
<div className="flex items-start space-x-6">
<button
// className="group flex items-center justify-center w-24 h-24 md:w-32 md:h-32 border transition"
className={`group flex items-center justify-center w-24 h-24 md:w-32 md:h-32 border transition ${(!data?.model_parameter) ? 'opacity-50 cursor-not-allowed' : ''}`}
onClick={() => handleClick("icon")}
disabled={loading || !canDeploy || !data?.model_parameter}
>
{isImagePath ? (
<img
src={resolvedIconSrc}
alt="icon"
className="w-full h-full object-contain transition-all duration-200 group-hover:border-2 group-hover:border-blue-500 group-active:border-2 group-active:border-green-500 group-hover:scale-105 group-active:scale-95"
/>
) : (
data?.icon || "Deploy"
)}
</button>
<div className="flex justify-between flex-1 items-end">
<div className="text-sm leading-7 space-y-1.5">
<div className="flex items-center gap-2">
<BadgeInfo size={16} /> {data?.name || "未命名组件"}
</div>
<div className="flex items-center gap-2">
<Tags size={16} /> {data?.category || "未知"}
</div>
<div className="flex items-center gap-2">
<CalendarClock size={16} /> {data?.updated_at || "未提供"}
</div>
<div className="flex items-center gap-2">
<Building2 size={16} /> {data?.company || "未知公司"}
</div>
</div>
<div className="flex items-center gap-[5px] self-end">
{hasNonEmptyExtraData && (
<button
onClick={() => {
console.log("🟢 点击了更新按钮extra_data = ", data.extra_data);
// TODO: 后续处理逻辑写在这里
}}
className="hover:text-gray-700 transition self-end text-sm border border-gray-300 rounded px-2 py-1 bg-white"
title="更新"
>
🔄 更新
</button>
)}
{statusLoaded && (currentStatus === "running" || currentStatus === "stopped") && (
<button
onClick={handleSwitchStatus}
disabled={switchLoading || loading}
className="hover:text-gray-700 transition self-end text-sm border border-gray-300 rounded px-2 py-1 bg-white"
title={currentStatus === "running" ? "停止运行" : "启动运行"}
>
{switchLoading
? "处理中..."
: currentStatus === "running"
? "⏹ 停止"
: "▶️ 启动"}
</button>
)}
{statusLoaded && showDelete && (
<button
onClick={handleDelete}
className="hover:text-gray-700 transition self-end"
disabled={loading}
title="删除"
>
<Trash2 size={20} />
</button>
)}
{!data?.model_parameter && (
<div className="flex flex-col items-center gap-[2px]">
<button
onClick={async () => {
try {
const download_url = data?.extra_data?.download_url;
const callback_url = data?.extra_data?.callback_url;
const digest = data?.extra_data?.digest;
const version = data?.extra_data?.version;
const size = data?.extra_data?.size;
const date1 = data?.extra_data?.date;
const id = data?.org_id;
if (!download_url || !size || !id) {
message.warning("缺少必要的下载参数");
return;
}
console.log("📥 调用下载接口...", { url: download_url, size, id });
const res = await fetch("https://ai.szaiai.com/api/v1/cloud/download", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
date: date1,
url: download_url,
callback_url: callback_url,
digest: digest,
version: version,
size: size,
id: id,
}),
});
const result = await res.json();
console.log("✅ 下载接口返回:", result);
if (result?.header?.code === 0) {
const percentStr = result?.data?.percent ?? "0";
const percentNum = parseFloat(percentStr);
const percent = (percentNum * 100).toFixed(1);
setDownloadPercent(percent);
setShowDownloadBar(true);
message.success(`📦 下载进度:${percent}%`);
} else {
message.warning(`❌ 下载失败:${result?.header?.message || "未知错误"}`);
}
} catch (err) {
console.error("❌ 下载请求异常:", err);
message.error("请求出错,无法下载");
}
}}
className="hover:text-gray-700 transition self-end"
title="下载模型文件"
>
<CloudDownload size={20} />
</button>
{showDownloadBar && (
<div className="w-[20px] h-[4px] bg-gray-200 rounded overflow-hidden">
<div
className="h-full bg-blue-500 transition-all duration-300"
style={{ width: `${downloadPercent}%` }}
/>
</div>
)}
</div>
)}
</div>
</div>
</div>
{showProgressBar && (progress !== "0%" || statusText) && (
<div className="relative w-full bg-gray-200 h-6 rounded overflow-hidden">
{/* 蓝色进度条 */}
<div
className={`${progressBarColor} h-full transition-all duration-300`}
style={{ width: typeof progress === "string" ? progress : `${progress}%` }}
/>
{/* 浮动在中间的文字层 */}
<div className="absolute inset-0 flex items-center justify-center text-sm font-medium text-white">
{loading ? t("deploy.processing") : statusText}
</div>
</div>
)}
</div>
</div>
);
}
export function NavBack() {
const router = useRouter();
const { t } = useTranslation();
// const query = router.query; // 假设页面 URL 是 /mypage?id=123
console.log("query", router)
return (
<nav className="fixed top-[1rem] container flex items-center justify-between " >
<Link href="/">
{t("home")}
</Link>
</nav>
)
}
export function TimeP({
date,
}: {
date: string
}) {
const router = useRouter();
const { t } = useTranslation();
return (
<p className=" text-left text-[1rem] text-[#808080]">{date} {t("JellyAI")}</p>
)
}

View File

@ -30,7 +30,6 @@ import Image from 'next/image';
import logoImage from '@/components/images/logo.png';
import { useRouter } from 'next/navigation'
// import { Button } from './ui/button'
import { Flex, Text } from '@radix-ui/themes';
import Link from 'next/link'
import { useTranslation } from 'react-i18next'
@ -39,11 +38,10 @@ import { LogoAI } from '@/components/chat'
import { useState, useRef, } from "react";
import { Trash2 } from "lucide-react";
import { Trash2, CloudDownload } from "lucide-react";
import { useEffect } from "react";
import {
BadgeInfo,
Tags,
@ -73,7 +71,7 @@ export function Header() {
const soonFunc = () => {
message.info(t("soon"))
}
//w-11/12 sm:w-5/6 md:w-3/4 lg:w-2/3 xl:w-3/5 2xl:w-1/2
return (
<header className="sticky top-0 z-50 flex shrink-0 items-center justify-between bg-background px-4 py-[1.5rem] w-[80%] mx-auto">
@ -116,6 +114,53 @@ export function Header() {
)
}
import { getRuntimeEnv } from "@/lib/ipconfig";
export async function getWsBase() {
let ip = await getRuntimeEnv("SUPABASE_URL");
if (!ip) throw new Error("SUPABASE_URL 获取失败,无法构建 wsBase");
let wsProtocol = "ws";
if (typeof window !== "undefined") {
// ✅ 客户端环境(浏览器执行)
if (window.location.protocol === "https:") {
wsProtocol = "wss";
ip = window.location.hostname; // ✅ 用 hostname 避免 HTTPS + IP 的 TLS 报错
}
} else {
// ✅ SSR 环境,延迟导入 headers不能放顶层
const { headers } = await import("next/headers");
const hdrs = headers();
const forwardedProto = hdrs.get("x-forwarded-proto");
const hostHeader = hdrs.get("host");
if (forwardedProto === "https") {
wsProtocol = "wss";
}
if (hostHeader) {
// ✅ host 可能带端口,需处理
ip = hostHeader.includes(":") ? hostHeader.split(":")[0] : hostHeader;
}
}
const finalUrl = `${wsProtocol}://${ip}/api/v1/deploy/ws`;
console.log("✅ [WebSocket] Final URL:", finalUrl);
return finalUrl;
}
function getApiBaseUrl(): string {
if (typeof window === "undefined" || !window.location) {
if (process.env.NODE_ENV === "development") {
console.error("❌ getApiBaseUrl 被错误地调用于非浏览器环境");
}
throw new Error("❌ getApiBaseUrl 必须在浏览器端调用");
}
return `${window.location.protocol}//${window.location.host}`;
}
export function DetailPageHeader({ data }: { data: any }) {
const [loading, setLoading] = useState(false);
const [statusText, setStatusText] = useState(data?.statusText || "加载中...");
@ -126,14 +171,38 @@ export function DetailPageHeader({ data }: { data: any }) {
const [hasWSConnected, setHasWSConnected] = useState(false);
const [statusLoaded, setStatusLoaded] = useState(false);
const [canDeploy, setCanDeploy] = useState(true);
const [currentStatus, setCurrentStatus] = useState(""); // 当前部署状态running / stopped
const [switchLoading, setSwitchLoading] = useState(false); // 控制按钮 loading 状态
const [downloadPercent, setDownloadPercent] = useState("0"); // 下载百分比0 ~ 100
const [showDownloadBar, setShowDownloadBar] = useState(false);
const [newVersionLoading, setNewVersionLoading] = useState(false);
const socketRef = useRef<WebSocket | null>(null);
const { t } = useTranslation();
const initWebSocket = (userName: string, id: number) => {
console.log("...........model_parameter =", data?.model_parameter);
// const hasNonEmptyExtraData = data?.extra_data && typeof data.extra_data === 'object' && !Array.isArray(data.extra_data) && Object.keys(data.extra_data).length > 0 data?.model_parameter !== 0 &&
// data?.model_parameter !== "";
const hasNonEmptyExtraData =
data?.extra_data &&
typeof data.extra_data === 'object' &&
!Array.isArray(data.extra_data) &&
Object.keys(data.extra_data).length > 0 &&
data?.model_parameter !== 0 &&
data?.model_parameter !== "";
const initWebSocket = async (userName: string, id: number) => {
if (socketRef.current) socketRef.current.close();
const wsBase = process.env.NEXT_PUBLIC_CLIENT_BASE_WS;
//const wsBase = process.env.NEXT_PUBLIC_CLIENT_BASE_WS;
const wsBase = await getWsBase();
const socket = new WebSocket(`${wsBase}/status/${userName}/${id}`);
socketRef.current = socket;
@ -290,6 +359,7 @@ export function DetailPageHeader({ data }: { data: any }) {
const code = result?.header?.code;
const status = result?.data?.data?.status;
setCurrentStatus(status || ""); //...............................................
const userData = JSON.parse(localStorage.getItem("UserData") || "null");
const userName = userData?.user_name;
const id = data?.id;
@ -338,79 +408,168 @@ export function DetailPageHeader({ data }: { data: any }) {
}
};
const handleSwitchStatus = async () => {
if (!data?.id) return;
setSwitchLoading(true);
const userData = JSON.parse(localStorage.getItem("UserData") || "null");
const userName = userData?.user_name;
const id = data?.id;
if (!userName || !id) {
setStatusText("缺少组件 ID 或用户信息");
return;
}
if (!userName) {
setStatusText("未登录,跳转中...");
window.location.href = "/auth/sign-in/";
return;
}
const endpoint = currentStatus === "running" ? "/api/v1/deploy/stop" : "/api/v1/deploy/start";
try {
const res = await fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user_name: userName, n_id: id }),
});
const json = await res.json();
if (json?.header?.code === 0) {
setStatusText(currentStatus === "running" ? "已停止" : "已启动");
fetchDeployStatus(); // ✅ 状态切换成功后刷新
} else {
setStatusText(json?.header?.message || "操作失败(后端)");
}
} catch (err) {
console.error("切换请求失败:", err);
setStatusText("网络异常");
} finally {
setSwitchLoading(false);
}
};
//软件更新
const handleDownloadNewVersion = async () => {
// ➜ digest 为空(没版本信息)⇒ 直接调用下载
if (!data?.extra_data?.digest) {
await downloadModel();
return;
}
const download_url = data?.extra_data?.download_url;
let callback_url = data?.extra_data?.callback_url;
const digest = data?.extra_data?.digest;
const version = data?.extra_data?.version;
const size = data?.extra_data?.size;
const date1 = data?.extra_data?.date;
const id = data?.org_id;
if (!download_url || !size || !id) {
message.warning("缺少必要的下载参数");
return;
}
setNewVersionLoading(true);
setShowDownloadBar(true); // 复用原进度条
try {
const apiHost = getApiBaseUrl(); // ✅ 会自动校验是否在客户端
//这个值是固定的,没有必要从其他渠道传过来,而且这个调用是服务器完成调用的
callback_url = "http://127.0.0.1:8083/api/v1/cloud/newversioncallback";
// const res = await fetch("https://ai.szaiai.com/api/v1/cloud/newversion", {
const res = await fetch(`${apiHost}/api/v1/cloud/newversion`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
date: String(date1),
url: download_url,
callback_url,
digest,
version,
size,
id,
}),
});
const result = await res.json();
if (result?.header?.code === 0) {
const percent = (parseFloat(result?.data?.percent ?? "0") * 100).toFixed(1);
setDownloadPercent(percent);
message.success(`📦 更新进度:${percent}%`);
if (percent === "100.0") message.success("🎉 新版本下载完成!");
} else {
message.warning(`❌ 更新失败:${result?.header?.message || "未知错误"}`);
}
} catch (err) {
console.error(err);
message.error("请求出错,无法更新");
} finally {
setNewVersionLoading(false);
}
};
// ================= 下载模型 =================
const downloadModel = async () => {
try {
const download_url = data?.extra_data?.download_url;
let callback_url = data?.extra_data?.callback_url;
const digest = data?.extra_data?.digest;
const version = data?.extra_data?.version;
const size = data?.extra_data?.size;
const date1 = data?.extra_data?.date;
const id = data?.org_id;
if (!download_url || !size || !id) {
message.warning("缺少必要的下载参数");
return;
}
console.log("📥 调用下载接口...", { url: download_url, size, id });
const apiHost = getApiBaseUrl();
callback_url = "http://127.0.0.1:8083/api/v1/cloud/callback";
const res = await fetch(`${apiHost}/api/v1/cloud/download`, {
method : "POST",
headers: { "Content-Type": "application/json" },
body : JSON.stringify({
date: String(date1),
url : download_url,
callback_url,
digest,
version,
size,
id,
}),
});
const result = await res.json();
console.log("✅ 下载接口返回:", result);
if (result?.header?.code === 0) {
const percent = (parseFloat(result?.data?.percent ?? "0") * 100).toFixed(1);
setDownloadPercent(percent);
setShowDownloadBar(true);
message.success(`📦 下载进度:${percent}%`);
} else {
message.warning(`❌ 下载失败:${result?.header?.message || "未知错误"}`);
}
} catch (err) {
console.error("❌ 下载请求异常:", err);
message.error("请求出错,无法下载");
}
};
useEffect(() => {
fetchDeployStatus();
}, [data?.id]);
// useEffect(() => {
// const fetchDeployStatus = async () => {
// try {
// const result = await fetch("/api/v1/deploy/status", {
// method: "POST",
// headers: { "Content-Type": "application/json" },
// body: JSON.stringify({ id: data?.id }),
// }).then((res) => res.json());
// console.log("====> deploy status result:", result);
// setStatusLoaded(true);
// if (!result) {
// setStatusText("接口响应为空");
// setShowDelete(false);
// return;
// }
// const code = result?.header?.code;
// const status = result?.data?.data?.status;
// const userData = JSON.parse(localStorage.getItem("UserData") || "null");
// const userName = userData?.user_name;
// const id = data?.id;
// console.log("🟡 状态码 code =", code, "状态 status =", status);
// if (code === 1006) {
// setStatusText("尚未部署");
// setShowDelete(false);
// setProgress("0%");
// setShowProgressBar(false)
// return;
// }
// if (status === "deploying" && userName && id && !hasWSConnected) {
// setStatusText("检测到正在部署,连接中...");
// initWebSocket(userName, id);
// setShowProgressBar(true)
// }
// if (status === "running" || status === "stopped") {
// console.log("✅ 允许删除status =", status, ")");
// setShowDelete(true);
// setProgress("100%");
// setShowProgressBar(true)
// if (status === "running") {
// setStatusText("运行中");
// setProgressBarColor("bg-green-500");
// } else if (status === "stopped") {
// setStatusText("已停止");
// setProgressBarColor("bg-gray-400");
// }
// } else {
// console.log("❌ 不允许删除status =", status, ")");
// setShowDelete(false);
// setProgressBarColor("bg-blue-500"); // 回到默认蓝色
// }
// } catch (err) {
// console.error("获取状态失败:", err);
// setStatusText("状态拉取失败");
// setShowDelete(false);
// }
// };
// fetchDeployStatus();
// }, [data?.id]);
useEffect(() => {
return () => {
if (socketRef.current) {
@ -465,16 +624,136 @@ export function DetailPageHeader({ data }: { data: any }) {
</div>
</div>
{statusLoaded && showDelete && (
<button
onClick={handleDelete}
className="hover:text-gray-700 transition self-end"
disabled={loading}
title="删除"
>
<Trash2 size={20} />
</button>
)}
<div className="flex items-center gap-[5px] self-end">
{hasNonEmptyExtraData && (
<div className="flex flex-col items-center gap-[2px]">
<button
onClick={handleDownloadNewVersion}
disabled={newVersionLoading || loading}
className="hover:text-gray-700 transition self-end text-sm border border-gray-300 rounded px-2 py-1 bg-white mt-1"
title="下载并安装新版本"
>
{newVersionLoading ? "处理中..." : "⬇️ 更新"}
</button>
{showDownloadBar && (
<div className="w-full h-[4px] bg-gray-200 rounded overflow-hidden mt-1">
<div
className="h-full bg-blue-500 transition-all duration-300"
style={{ width: `${downloadPercent}%` }}
/>
</div>
)}
</div>
)}
{statusLoaded && (currentStatus === "running" || currentStatus === "stopped") && (
<button
onClick={handleSwitchStatus}
disabled={switchLoading || loading}
className="hover:text-gray-700 transition self-end text-sm border border-gray-300 rounded px-2 py-1 bg-white"
title={currentStatus === "running" ? "停止运行" : "启动运行"}
>
{switchLoading
? "处理中..."
: currentStatus === "running"
? "⏹ 停止"
: "▶️ 启动"}
</button>
)}
{statusLoaded && showDelete && (
<button
onClick={handleDelete}
className="hover:text-gray-700 transition self-end"
disabled={loading}
title="删除"
>
<Trash2 size={20} />
</button>
)}
{!data?.model_parameter && (
<div className="flex flex-col items-center gap-[2px]">
<button
onClick={async () => {
try {
const download_url = data?.extra_data?.download_url;
let callback_url = data?.extra_data?.callback_url;
const digest = data?.extra_data?.digest;
const version = data?.extra_data?.version;
const size = data?.extra_data?.size;
const date1 = data?.extra_data?.date;
const id = data?.org_id;
if (!download_url || !size || !id) {
message.warning("缺少必要的下载参数");
return;
}
console.log("📥 调用下载接口...", { url: download_url, size, id });
const apiHost = getApiBaseUrl(); // ✅ 会自动校验是否在客户端
//const res = await fetch("https://ai.szaiai.com/api/v1/cloud/download", {
callback_url = "http://127.0.0.1:8083/api/v1/cloud/callback";
const res = await fetch(`${apiHost}/api/v1/cloud/download`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
date: String(date1),
url: download_url,
callback_url: callback_url,
digest: digest,
version: version,
size: size,
id: id,
}),
});
const result = await res.json();
console.log("✅ 下载接口返回:", result);
if (result?.header?.code === 0) {
const percentStr = result?.data?.percent ?? "0";
const percentNum = parseFloat(percentStr);
const percent = (percentNum * 100).toFixed(1);
setDownloadPercent(percent);
setShowDownloadBar(true);
message.success(`📦 下载进度:${percent}%`);
} else {
message.warning(`❌ 下载失败:${result?.header?.message || "未知错误"}`);
}
} catch (err) {
console.error("❌ 下载请求异常:", err);
message.error("请求出错,无法下载");
}
}}
className="hover:text-gray-700 transition self-end"
title="下载模型文件"
>
<CloudDownload size={20} />
</button>
{showDownloadBar && (
<div className="w-[20px] h-[4px] bg-gray-200 rounded overflow-hidden">
<div
className="h-full bg-blue-500 transition-all duration-300"
style={{ width: `${downloadPercent}%` }}
/>
</div>
)}
</div>
)}
</div>
</div>
</div>

View File

@ -12,7 +12,7 @@ import {
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { sync } from 'framer-motion'
import service from '@/lib/http/service'
import { getService } from '@/lib/http/service'
import React from 'react'
import toast from 'react-hot-toast'
import { useRouter } from 'next/navigation'
@ -118,6 +118,7 @@ export function UserMenu({ user }: { user: UserData }) {
console.log('logout')
if (isLoading) return
setIsLoading(true);
const service = await getService();
await service.post('/api/v1/customer/logout', {
}, {
headers: {

View File

@ -31,12 +31,80 @@
// } as any;
import { getRuntimeEnv } from "./ipconfig";
// import { getRuntimeEnv } from "../ipconfig";
// export async function getAxiosConfig() {
// let ip = await getRuntimeEnv("SUPABASE_URL"); // 直接用你 lib/ipconfig 里的方法
// if (!ip) throw new Error("SUPABASE_URL 获取失败,无法构建 axios 配置");
// let protocol = "http";
// let port = 80;
// if (typeof window !== "undefined" && window.location && window.location.protocol) {
// protocol = window.location.protocol.replace(":", "");
// port = protocol === "https" ? 443 : 80;
// // ✅ HTTPS + 浏览器时,替换 IP避免 TLS/CORS 报错
// if (protocol === "https") {
// ip = window.location.hostname;
// }
// }
// return {
// baseURL: `${protocol}://${ip}:${port}`, // 端口如需动态可再加参数
// method: 'post',
// timeout: 60 * 1000,
// headers: { 'Content-Type': 'application/json; charset=UTF-8' },
// responseType: 'json'
// } as const;
// }
import { getRuntimeEnv } from "../ipconfig";
//import { headers } from "next/headers";
export async function getAxiosConfig() {
const ip = await getRuntimeEnv("SUPABASE_URL"); // 直接用你 lib/ipconfig 里的方法
let ip = await getRuntimeEnv("SUPABASE_URL");
if (!ip) throw new Error("SUPABASE_URL 获取失败,无法构建 axios 配置");
let protocol = "http";
let port = 80;
if (typeof window !== "undefined" && window.location && window.location.protocol) {
// ✅ CSR 场景
protocol = window.location.protocol.replace(":", "");
port = protocol === "https" ? 443 : 80;
if (protocol === "https") {
ip = window.location.hostname;
}
} else {
// ✅ SSR 场景,延迟导入 headers
const { headers } = await import("next/headers");
const hdrs = headers();
const forwardedProto = hdrs.get("x-forwarded-proto");
const hostHeader = hdrs.get("host");
if (forwardedProto) {
protocol = forwardedProto;
}
if (hostHeader) {
ip = hostHeader;
if (hostHeader.includes(":")) {
const parts = hostHeader.split(":");
ip = parts[0];
port = parseInt(parts[1]);
} else {
port = (protocol === "https") ? 443 : 80;
}
}
}
return {
baseURL: `http://${ip}:80`, // 端口如需动态可再加参数
baseURL: `${protocol}://${ip}:${port}`,
method: 'post',
timeout: 60 * 1000,
headers: { 'Content-Type': 'application/json; charset=UTF-8' },

View File

@ -0,0 +1,18 @@
import { getRuntimeEnv } from "@/lib/ipconfig";
export async function getBaseUrl() {
let ip = await getRuntimeEnv("SUPABASE_URL");
if (!ip) throw new Error("SUPABASE_URL 获取失败");
if (typeof window === "undefined") {
// ✅ 只在服务端导入(避免构建失败)
const { getServerBaseUrl } = await import("./getServerBaseUrl");
return getServerBaseUrl(ip);
}
// ✅ 客户端逻辑
let protocol = window.location.protocol.replace(":", "");
let hostname = window.location.hostname;
let port = protocol === "https" ? 443 : 80;
return `${protocol}://${hostname}:${port}`;
}

View File

@ -0,0 +1,11 @@
// lib/http/getServerBaseUrl.ts
import { headers } from "next/headers";
export function getServerBaseUrl(defaultIp: string) {
const h = headers();
const proto = h.get("x-forwarded-proto") || "http";
const host = h.get("host") || defaultIp;
const port = proto === "https" ? 443 : 80;
return `${proto}://${host}:${port}`;
}

View File

@ -25,7 +25,8 @@ export async function getRuntimeEnv(key: string): Promise<string | undefined> {
// 2. 第3次开始查localStorage兜底
if (retries >= 2) {
const cached = window.localStorage.getItem(LOCAL_KEY);
const cachedIp = extractIp(cached);
const cachedIp = extractIp(cached ?? undefined);
//const cachedIp = extractIp(cached);
if (cachedIp && key === "SUPABASE_URL") {
console.warn(`[env] [${key}] 第${retries}次重试后window.RUNTIME_ENV还没拿到合法IPlocalStorage兜底: ${cached}提取IP: ${cachedIp}`);
return cachedIp;
@ -45,7 +46,7 @@ export async function getRuntimeEnv(key: string): Promise<string | undefined> {
// 服务端
const val = process.env[key];
const ip = extractIp(val);
console.log(`[env][server] 直接读取 process.env[${key}]:`, val, "提取IP:", ip);
console.log(`................[env][server] 直接读取 process.env[${key}]:`, val, "提取IP:", ip);
return ip;
}
}

View File

@ -11,7 +11,7 @@ import rehypeSlug from "rehype-slug";
import remarkGfm from "remark-gfm";
import { BUNDLED_LANGUAGES, type HighlighterOptions, getHighlighter } from "shiki";
import gitHubLight from "shiki/themes/github-light.json";
import service from "./http/service";
import { getService } from "./http/service";
import { ArticleData, QAData } from "@/components/article/article";
export const BLOG_PATH = path.join(process.cwd(), "content", "blog");
@ -108,6 +108,7 @@ type Frontmatter = {
type AIFrontmatter = {
id:string;
org_id:string;
p_name:string;
title: string;
date: string;
@ -116,6 +117,7 @@ type AIFrontmatter = {
image_url: string;
logo_url: string;
model_parameter: number;
extra_data: Record<string, any>;
visible: boolean | undefined;
salary: string | undefined;
level: string | undefined;
@ -252,7 +254,8 @@ export const getPostContent = async (language: string, slug: string): Promise<AI
const PageSize = 1
const service = await getService();
let data: ArticleData = await service.post('/api/v1/news/list', {
"id": Number(slug),
// "tag": "", // #Blockchain
@ -284,6 +287,7 @@ export const getPostContent = async (language: string, slug: string): Promise<AI
const serialized = await mdxSerialized({ rawMdx });
const frontmatter = serialized.frontmatter as AIFrontmatter;
frontmatter.id = String(data.id)
frontmatter.org_id = String(data.org_id)
frontmatter.p_name = data.p_name
frontmatter.title = data.main_title
frontmatter.description = data.sub_title || data.main_title
@ -292,6 +296,7 @@ export const getPostContent = async (language: string, slug: string): Promise<AI
frontmatter.image_url = data.image_url
frontmatter.logo_url = data.logo_url
frontmatter.model_parameter = data.model_parameter
frontmatter.extra_data = data.extra_data
// const headings = data.main_title;
@ -340,6 +345,7 @@ export const getQAContent = async (language: string, slug: string): Promise<AIPo
// const rawMdx = await raw({ contentPath: BLOG_PATH, filepath: filepath });
const PageSize = 1
const service = await getService();
let data: QAData = await service.post('/api/v1/qa/list', {
"id": Number(slug),
// "tag": "", // #Blockchain

View File

@ -53,3 +53,9 @@ export type ChainData = {
icon?: string
}[]
}
declare global {
interface Window {
RUNTIME_ENV?: Record<string, string>;
}
}

View File

@ -18,7 +18,7 @@ const nextConfig = {
},
compiler: {
removeConsole: false, // 保留所有模式中的 console.log
//removeConsole: process.env.VERCEL_ENV === 'production' ? { exclude: ['error'] } : false,
//removeConsole: process.env.NODE_ENV === 'production' ? { exclude: ['error'] } : false, // ✅ 用 NODE_ENV
emotion: true,
},
webpack: (config, { isServer }) => {
@ -26,32 +26,13 @@ const nextConfig = {
type: "memory",
});
// if (!isServer) {
// config.resolve.fallback = {
// fs: false,
// };
// }
// config.devServer = {
// ...config.devServer,
// proxy: {
// ...config.devServer.proxy,
// '/ws': {
// target: 'ws://116.213.39.234:8083/ws', // WebSocket服务器地址
// changeOrigin: true,
// ws: true, // 确保WebSocket连接也被代理
// },
// },
// };
return config;
},
eslint: {
// Warning: This allows production builds to successfully complete even if
// your project has ESLint errors.
ignoreDuringBuilds: true,
},
},
experimental: {
esmExternals: true,
},

View File

@ -43,7 +43,7 @@
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
],
"exclude": [
"test",
"node_modules"

View File

@ -11,7 +11,7 @@ import rehypeSlug from "rehype-slug";
import remarkGfm from "remark-gfm";
import { BUNDLED_LANGUAGES, type HighlighterOptions, getHighlighter } from "shiki";
import gitHubLight from "shiki/themes/github-light.json";
import service from "./http/service";
import { getService } from "./http/service";
import { ArticleData, QAData } from "@/components/article/article";
export const BLOG_PATH = path.join(process.cwd(), "content", "blog");
@ -248,7 +248,7 @@ export const getPostContent = async (language: string, slug: string): Promise<AI
const PageSize = 1
const service = await getService();
let data: ArticleData = await service.post('/api/v1/news/list', {
"id": Number(slug),
// "tag": "", // #Blockchain
@ -332,6 +332,7 @@ export const getQAContent = async (language: string, slug: string): Promise<AIPo
// const rawMdx = await raw({ contentPath: BLOG_PATH, filepath: filepath });
const PageSize = 1
const service = await getService();
let data: QAData = await service.post('/api/v1/qa/list', {
"id": Number(slug),
// "tag": "", // #Blockchain

View File

@ -11,7 +11,7 @@ import rehypeSlug from "rehype-slug";
import remarkGfm from "remark-gfm";
import { BUNDLED_LANGUAGES, type HighlighterOptions, getHighlighter } from "shiki";
import gitHubLight from "shiki/themes/github-light.json";
import service from "./http/service";
import { getService } from "./http/service";
import { ArticleData, QAData } from "@/components/article/article";
export const BLOG_PATH = path.join(process.cwd(), "content", "blog");
@ -248,7 +248,7 @@ export const getPostContent = async (language: string, slug: string): Promise<AI
const PageSize = 1
const service = await getService();
let data: ArticleData = await service.post('/api/v1/news/list', {
"id": Number(slug),
// "tag": "", // #Blockchain
@ -332,6 +332,7 @@ export const getQAContent = async (language: string, slug: string): Promise<AIPo
// const rawMdx = await raw({ contentPath: BLOG_PATH, filepath: filepath });
const PageSize = 1
const service = await getService();
let data: QAData = await service.post('/api/v1/qa/list', {
"id": Number(slug),
// "tag": "", // #Blockchain

View File

@ -1,13 +1,42 @@
# 1. 构建镜像(名字直接叫 cradle
#!/bin/bash
set -e # 构建失败就退出
IMAGE_NAME="cradle:latest"
CONTAINER_NAME="Cradle"
echo "🔍 检查旧容器是否存在..."
if docker ps -a --format '{{.Names}}' | grep -Eq "^${CONTAINER_NAME}\$"; then
echo "🛑 停止旧容器: $CONTAINER_NAME"
docker stop "$CONTAINER_NAME"
echo "❌ 删除旧容器: $CONTAINER_NAME"
docker rm "$CONTAINER_NAME"
fi
echo "🧹 删除旧镜像(如果存在)..."
if docker images --format '{{.Repository}}:{{.Tag}}' | grep -Eq "^cradle:latest\$"; then
docker rmi -f "$IMAGE_NAME"
fi
echo "⬇️ 拉取最新代码..."
git pull
echo "🧼 清除 .next..."
rm -rf apps/blogai/.next
docker build \
echo "📦 构建镜像..."
if ! docker build \
--build-arg http_proxy=http://127.0.0.1:7890 \
--build-arg https_proxy=http://127.0.0.1:7890 \
--build-arg BLOGAI_HOST=ai.szaiai.com \
--no-cache \
--network=host \
-t cradle:latest \
-f apps/blogai/Dockerfile .
-f apps/blogai/Dockerfile .; then
echo "❌ 构建失败,退出"
exit 1
fi
echo "🚀 启动新容器..."
docker run -d --name Cradle -p 3008:3008 --restart always -e SUPABASE_URL="http://183.36.35.42:80" cradle:latest
# 2. 运行容器(容器名叫 Cradle
docker run -d --name Cradle -p 3008:3008 --restart always cradle:latest

File diff suppressed because it is too large Load Diff

22
supervisor.conf Normal file
View File

@ -0,0 +1,22 @@
[supervisord]
nodaemon=true
logfile=/tmp/supervisord.log
pidfile=/tmp/supervisord.pid
[unix_http_server]
file=/var/run/supervisor.sock
chmod=0700
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[supervisorctl]
serverurl=unix:///var/run/supervisor.sock
[program:web]
command=/plugai/wrapper.sh
directory=/plugai/zerostack/t1
autostart=true
autorestart=true
redirect_stderr=true
stdout_logfile=/dev/stdout

25
wrapper.sh Normal file
View File

@ -0,0 +1,25 @@
#!/bin/bash
# ✅ 设置 SUPABASE_URL如果为空则使用默认值
if [ -z "${SUPABASE_URL:-}" ]; then
SUPABASE_URL="http://localhost:8000"
fi
# ✅ 删除旧的 env.js如果存在
ENV_FILE="/plugai/zerostack/t1/public/env.js"
if [ -f "$ENV_FILE" ]; then
echo "[plugai-ui] Removing old env.js"
rm -f "$ENV_FILE"
fi
# ✅ 写入新的 env.js
echo "[plugai-ui] Writing env.js with SUPABASE_URL=$SUPABASE_URL"
cat <<EOF > "$ENV_FILE"
window.RUNTIME_ENV = {
"SUPABASE_URL": "${SUPABASE_URL}"
};
EOF
cd /plugai/zerostack/t1
exec npm run start