Compare commits
114 Commits
main
...
DynamicIpC
| Author | SHA1 | Date |
|---|---|---|
|
|
48ee6b382f | |
|
|
659f527012 | |
|
|
c6c6e97583 | |
|
|
2644080a5c | |
|
|
d28c8f9023 | |
|
|
c532ff5a26 | |
|
|
fc74b04ebc | |
|
|
bb451dd6e4 | |
|
|
3d203acf51 | |
|
|
04db5e7b0b | |
|
|
6e77f629e9 | |
|
|
3960aeb7a3 | |
|
|
745b67930b | |
|
|
ebf7120505 | |
|
|
0b89cc7d86 | |
|
|
ad108a7794 | |
|
|
89cb341b31 | |
|
|
7704cd90c3 | |
|
|
058485e5f9 | |
|
|
6755707425 | |
|
|
cd859ecf74 | |
|
|
c7c6561bc9 | |
|
|
479852fff0 | |
|
|
16d6abb84a | |
|
|
2beac56c1e | |
|
|
a4d2067d70 | |
|
|
617780ef4b | |
|
|
8d3643ebf1 | |
|
|
54306ea870 | |
|
|
2a36e7e08a | |
|
|
710b6ebdfc | |
|
|
0b9998528f | |
|
|
ea8768003b | |
|
|
c50c7eeab5 | |
|
|
d22a1fbb6b | |
|
|
ab01d11f31 | |
|
|
7e8737eddd | |
|
|
42ab656ca3 | |
|
|
a4085bfc89 | |
|
|
185d1413ba | |
|
|
895ca4beec | |
|
|
9139583aba | |
|
|
9161d6acd8 | |
|
|
1bb5e03fcf | |
|
|
d57f222683 | |
|
|
b88044604e | |
|
|
de8d122b60 | |
|
|
a75eebf2e6 | |
|
|
ea69a0b4fe | |
|
|
b7d409e666 | |
|
|
3cf5902218 | |
|
|
3089329514 | |
|
|
6ce87f07f4 | |
|
|
8f79ad000f | |
|
|
c15873cf67 | |
|
|
334e5c50e9 | |
|
|
b03a310fc7 | |
|
|
5c79a6e6ed | |
|
|
854a09e1ff | |
|
|
a3767c42a5 | |
|
|
e3d6edbb84 | |
|
|
9bedaac5d6 | |
|
|
8ff5397e10 | |
|
|
f5d5e22884 | |
|
|
cdb93cc88b | |
|
|
1f3d7b9a9c | |
|
|
f326ecc072 | |
|
|
2a54758324 | |
|
|
bc91d8f10a | |
|
|
39f4448674 | |
|
|
0aa86587c3 | |
|
|
1ddbf1c13a | |
|
|
aae8145f6f | |
|
|
6ad20f12b2 | |
|
|
9499cf2bb6 | |
|
|
76478e6835 | |
|
|
2cb7353e6a | |
|
|
d9415a9b37 | |
|
|
8e0b0ed369 | |
|
|
a93f476e46 | |
|
|
012e266a09 | |
|
|
870e77efdf | |
|
|
1734b7bd85 | |
|
|
53e2804240 | |
|
|
5cebd1239d | |
|
|
ec9360a4bb | |
|
|
b2c988fc1b | |
|
|
2ce8eb2e7a | |
|
|
7bf90733d5 | |
|
|
891fa10860 | |
|
|
70bbbf97ad | |
|
|
dbde58e908 | |
|
|
b16b726309 | |
|
|
8f408e03a9 | |
|
|
8d6ed1c5c4 | |
|
|
f3362cf02e | |
|
|
5c65849ded | |
|
|
d49a5a3bea | |
|
|
9b27b288c9 | |
|
|
dd3dff9543 | |
|
|
5c413f4af5 | |
|
|
5139bed3fc | |
|
|
bb4bc11af1 | |
|
|
891d7f473b | |
|
|
80c79cd779 | |
|
|
67c8df29e0 | |
|
|
0ed6114539 | |
|
|
0bce0eed07 | |
|
|
65310bc613 | |
|
|
b0d6d74853 | |
|
|
4c4411843a | |
|
|
0443185899 | |
|
|
35e99c7c9f | |
|
|
9052ee0da3 |
|
|
@ -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
|
||||||
ARG BLOGAI_HOST=ai.szaiai.com
|
ENV NODE_ENV=production
|
||||||
|
ENV BLOGAI_HOST=${BLOGAI_HOST}
|
||||||
|
|
||||||
# 设置构建环境变量
|
WORKDIR /app
|
||||||
ENV NODE_ENV=production
|
|
||||||
ENV BLOGAI_HOST=${BLOGAI_HOST}
|
|
||||||
|
|
||||||
# 设置工作目录
|
# ✅ 必须在 COPY 前存在 lock 文件,确保版本一致
|
||||||
WORKDIR /app
|
COPY pnpm-lock.yaml ./
|
||||||
|
COPY pnpm-workspace.yaml ./
|
||||||
|
COPY package.json ./
|
||||||
|
|
||||||
# 安装 pnpm
|
RUN npm install -g pnpm
|
||||||
RUN npm install -g pnpm
|
|
||||||
|
|
||||||
# 复制整项目的代码,排除了.dockerignore中的文件
|
COPY . ./
|
||||||
COPY . ./
|
|
||||||
|
|
||||||
# 拷贝并替换.env
|
# ✅ 配置 .env
|
||||||
COPY apps/blogai/.env.example apps/blogai/.env
|
COPY apps/blogai/.env.example apps/blogai/.env
|
||||||
RUN sed -i "s|{{BLOGAI_HOST}}|${BLOGAI_HOST}|g" apps/blogai/.env
|
RUN sed -i "s|{{BLOGAI_HOST}}|${BLOGAI_HOST}|g" apps/blogai/.env
|
||||||
|
|
||||||
# 安装根目录依赖
|
# ✅ 安装所有 workspace 的依赖,包含 apps/blogai 的 next 等
|
||||||
RUN pnpm install
|
RUN pnpm install --frozen-lockfile --recursive
|
||||||
|
|
||||||
# 编译子项目 apps/blogai
|
WORKDIR /app/apps/blogai
|
||||||
WORKDIR /app/apps/blogai
|
RUN pnpm run build
|
||||||
RUN pnpm run build
|
|
||||||
|
|
||||||
|
|
||||||
# --- 第二阶段:生产环境运行阶段 ---
|
# --- 第二阶段:运行阶段 ---
|
||||||
FROM node:18-slim AS runner
|
FROM node:20-slim AS runner
|
||||||
|
|
||||||
# 重新定义build参数,且默认值一致
|
ARG BLOGAI_HOST=ai.szaiai.com
|
||||||
ARG BLOGAI_HOST=ai.szaiai.com
|
ENV NODE_ENV=production
|
||||||
|
ENV PORT=3008
|
||||||
|
ENV BLOGAI_HOST=${BLOGAI_HOST}
|
||||||
|
|
||||||
# 设置运行环境变量
|
# 安装 supervisor
|
||||||
ENV NODE_ENV=production
|
RUN apt-get update && \
|
||||||
ENV PORT=3008
|
apt-get install -y supervisor curl && \
|
||||||
ENV BLOGAI_HOST=${BLOGAI_HOST}
|
apt-get clean && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# 安装 pm2
|
# 设置 supervisor 配置目录
|
||||||
RUN npm install -g pm2
|
RUN mkdir -p /etc/supervisor/conf.d
|
||||||
|
|
||||||
# 设置根目录下的运行环境
|
# 设置工作目录
|
||||||
WORKDIR /app
|
WORKDIR /plugai
|
||||||
|
|
||||||
# 复制根目录下的node_modules
|
# 拷贝 supervisor.conf 到指定路径
|
||||||
COPY --from=builder /app/node_modules ./node_modules
|
COPY ./supervisor.conf /etc/supervisor/conf.d/supervisor.conf
|
||||||
|
|
||||||
# 设置子项目下的运行目录
|
# 拷贝 node_modules(根目录的)
|
||||||
WORKDIR /app/apps/blogai/
|
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
|
|
||||||
|
|
||||||
|
# 拷贝 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 可执行权限
|
||||||
RUN rm -rf /root/.npm /root/.pnpm-store /tmp/*
|
COPY ./wrapper.sh /plugai/wrapper.sh
|
||||||
|
RUN chmod +x /plugai/wrapper.sh
|
||||||
|
|
||||||
# 暴露端口
|
RUN rm -rf /root/.npm /root/.pnpm-store /tmp/*
|
||||||
EXPOSE 3008
|
|
||||||
|
|
||||||
# 容器启动命令
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=25s --retries=3 CMD curl -fs http://localhost:3008/api/health/ || exit 1
|
||||||
CMD ["pm2-runtime", "npm", "--", "start"]
|
|
||||||
|
|
||||||
|
EXPOSE 3008
|
||||||
|
|
||||||
|
# 使用 supervisor 启动
|
||||||
|
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisor.conf"]
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
|
|
@ -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"]
|
||||||
|
|
||||||
|
|
@ -60,11 +60,11 @@ export default function SignInPage({ params: { locale } }: { params: { locale: s
|
||||||
passwordValue={password}
|
passwordValue={password}
|
||||||
/>
|
/>
|
||||||
<p className="mt-4 text-left text-sm text-md text-black/50 ">
|
<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();
|
e.preventDefault();
|
||||||
toast.success('coming soon')
|
toast.success('coming soon')
|
||||||
}}>
|
}}>
|
||||||
{t("auth.frogot_password")}
|
{t("auth.forgot_password")}
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import toast from "react-hot-toast";
|
||||||
import md5 from "md5";
|
import md5 from "md5";
|
||||||
|
|
||||||
|
|
||||||
import service from '@/lib/http/service';
|
import { getService } from '@/lib/http/service';
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export function EmailSignIn(props: {
|
export function EmailSignIn(props: {
|
||||||
|
|
@ -51,6 +51,7 @@ export function EmailSignIn(props: {
|
||||||
console.log(email, password, (md5(password)))
|
console.log(email, password, (md5(password)))
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const service = await getService();
|
||||||
await service.post('/api/v1/customer/login', {
|
await service.post('/api/v1/customer/login', {
|
||||||
user_name: email,
|
user_name: email,
|
||||||
password: md5(password),
|
password: md5(password),
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import toast from "react-hot-toast";
|
||||||
import md5 from "md5";
|
import md5 from "md5";
|
||||||
|
|
||||||
|
|
||||||
import service from '@/lib/http/service';
|
import { getService } from '@/lib/http/service';
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export function MixSignIn(props: {
|
export function MixSignIn(props: {
|
||||||
|
|
@ -53,6 +53,7 @@ export function MixSignIn(props: {
|
||||||
console.log("--username:", user, "--password:", password, "--md5 password:", (md5(password)))
|
console.log("--username:", user, "--password:", password, "--md5 password:", (md5(password)))
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const service = await getService();
|
||||||
await service.post('/api/v1/customer/login', {
|
await service.post('/api/v1/customer/login', {
|
||||||
user_name: user,
|
user_name: user,
|
||||||
password: md5(password),
|
password: md5(password),
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { Loading } from "@/components/ui/loading";
|
||||||
import { toast } from "@/components/ui/toaster";
|
import { toast } from "@/components/ui/toaster";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { OTPInput, SlotProps } from "input-otp";
|
import { OTPInput, SlotProps } from "input-otp";
|
||||||
import service from "@/lib/http/service";
|
import { getService } from "@/lib/http/service";
|
||||||
import md5 from "md5";
|
import md5 from "md5";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
// import { Minus } from "lucide-react";
|
// import { Minus } from "lucide-react";
|
||||||
|
|
@ -32,7 +32,7 @@ export const EmailCode: React.FC<Props> = ({ setError, emailValue, passwordValue
|
||||||
)
|
)
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const service = await getService();
|
||||||
await service.post('/api/v1/customer/register', {
|
await service.post('/api/v1/customer/register', {
|
||||||
user_name: emailValue,
|
user_name: emailValue,
|
||||||
email: emailValue,
|
email: emailValue,
|
||||||
|
|
@ -68,6 +68,7 @@ export const EmailCode: React.FC<Props> = ({ setError, emailValue, passwordValue
|
||||||
const resendCode = async () => {
|
const resendCode = async () => {
|
||||||
console.log("resendCode", emailValue)
|
console.log("resendCode", emailValue)
|
||||||
try {
|
try {
|
||||||
|
const service = await getService();
|
||||||
await service.post('/api/v1/common/auth-code', {
|
await service.post('/api/v1/common/auth-code', {
|
||||||
user_name: emailValue,
|
user_name: emailValue,
|
||||||
email: emailValue,
|
email: emailValue,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { FadeInStagger } from "@/components/landing/fade-in";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { toast } from "@/components/ui/toaster";
|
import { toast } from "@/components/ui/toaster";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import service from "@/lib/http/service";
|
import { getService } from "@/lib/http/service";
|
||||||
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import showImage from '@/components/images/show.png';
|
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', {
|
await service.post('/api/v1/common/auth-code', {
|
||||||
user_name: email,
|
user_name: email,
|
||||||
email: email,
|
email: email,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { FadeInStagger } from "@/components/landing/fade-in";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { toast } from "@/components/ui/toaster";
|
import { toast } from "@/components/ui/toaster";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import service from "@/lib/http/service";
|
import { getService } from "@/lib/http/service";
|
||||||
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import showImage from '@/components/images/show.png';
|
import showImage from '@/components/images/show.png';
|
||||||
|
|
@ -103,6 +103,7 @@ export function MixSignUp(props: {
|
||||||
}
|
}
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
|
||||||
|
const service = await getService();
|
||||||
await service.post('/api/v1/customer/uregister', {
|
await service.post('/api/v1/customer/uregister', {
|
||||||
user_name: username,
|
user_name: username,
|
||||||
Referral: referrer,
|
Referral: referrer,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { Container } from "@/components/landing/container";
|
import { Container } from "@/components/landing/container";
|
||||||
import { FadeIn } from "@/components/landing/fade-in";
|
import { FadeIn } from "@/components/landing/fade-in";
|
||||||
import { MdxContent } from "@/components/landing/mdx-content";
|
import { MdxContent } from "@/components/landing/mdx-content";
|
||||||
// import { PageLinks } from "@/components/landing/page-links";
|
|
||||||
import { Avatar, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarImage } from "@/components/ui/avatar";
|
||||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||||
import { authors } from "@/content/blog/authors";
|
import { authors } from "@/content/blog/authors";
|
||||||
|
|
@ -11,12 +10,13 @@ import Link from "next/link";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
|
|
||||||
import { BLOG_PATH, getContentData, getFilePaths, getPost, getPostContent } from "@/lib/mdx-helper";
|
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 { Header, NavBack, TimeP } from "@/components/header";
|
||||||
import { baseTitle, baseURL, keywordsRoot } from "@/lib/metadata";
|
import { baseTitle, baseURL, keywordsRoot } from "@/lib/metadata";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { DetailPageHeader } from '@/components/header'
|
import { DetailPageHeader } from '@/components/header'
|
||||||
|
import { getBaseUrl } from "@/lib/http/get-base-url"; // ✅ 不是 export 它,而是 import 用
|
||||||
|
|
||||||
export const runtime = "nodejs";
|
export const runtime = "nodejs";
|
||||||
|
|
||||||
|
|
@ -25,6 +25,7 @@ type Props = {
|
||||||
searchParams: { [key: string]: string | string[] | undefined };
|
searchParams: { [key: string]: string | string[] | undefined };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||||
|
|
||||||
const { serialized, frontmatter, headings } = await getPostContent(params.locale, params.slug);
|
const { serialized, frontmatter, headings } = await getPostContent(params.locale, params.slug);
|
||||||
|
|
@ -35,7 +36,8 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||||
return notFound();
|
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 ogUrl = new URL("/og/blog", baseUrl);
|
||||||
const author = authors[frontmatter.author];
|
const author = authors[frontmatter.author];
|
||||||
ogUrl.searchParams.set("title", frontmatter.title ?? "");
|
ogUrl.searchParams.set("title", frontmatter.title ?? "");
|
||||||
|
|
@ -93,12 +95,14 @@ const BlogArticleWrapper = async ({ params }: { params: { slug: string, locale:
|
||||||
<DetailPageHeader
|
<DetailPageHeader
|
||||||
data={{
|
data={{
|
||||||
id: frontmatter.id,
|
id: frontmatter.id,
|
||||||
|
org_id: frontmatter.org_id,
|
||||||
icon: frontmatter.logo_url,
|
icon: frontmatter.logo_url,
|
||||||
name: frontmatter.p_name,
|
name: frontmatter.p_name,
|
||||||
model_parameter: frontmatter.model_parameter,
|
model_parameter: frontmatter.model_parameter,
|
||||||
category: frontmatter.tags,
|
category: frontmatter.tags,
|
||||||
updated_at: frontmatter.date,
|
updated_at: frontmatter.date,
|
||||||
company: frontmatter.title,
|
company: frontmatter.title,
|
||||||
|
extra_data: frontmatter.extra_data,
|
||||||
progress: progress,
|
progress: progress,
|
||||||
statusText: statusText
|
statusText: statusText
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import { dir } from 'i18next';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import TranslationsProvider from '@/components/TranslationsProvider';
|
import TranslationsProvider from '@/components/TranslationsProvider';
|
||||||
import initTranslations from '../i18n';
|
import initTranslations from '../i18n';
|
||||||
|
import { ConsoleSilencer } from '@/components/dev-only/console-silencer'
|
||||||
|
|
||||||
export const runtime = 'edge' // 'nodejs' (default) | 'edge'
|
export const runtime = 'edge' // 'nodejs' (default) | 'edge'
|
||||||
|
|
||||||
|
|
@ -68,7 +69,6 @@ export default async function RootLayout({
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
params: { locale: string };
|
params: { locale: string };
|
||||||
}) {
|
}) {
|
||||||
|
|
||||||
const { t, resources } = await initTranslations(locale, i18nNamespaces);
|
const { t, resources } = await initTranslations(locale, i18nNamespaces);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -93,6 +93,7 @@ export default async function RootLayout({
|
||||||
resources={resources}>
|
resources={resources}>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<Providers attribute="class" defaultTheme="system" enableSystem>
|
<Providers attribute="class" defaultTheme="system" enableSystem>
|
||||||
|
{/* <ConsoleSilencer /> */}
|
||||||
<div className="flex min-h-screen flex-col">
|
<div className="flex min-h-screen flex-col">
|
||||||
<main className="flex flex-1 flex-col bg-muted/0.3">
|
<main className="flex flex-1 flex-col bg-muted/0.3">
|
||||||
{/* <Header /> */}
|
{/* <Header /> */}
|
||||||
|
|
|
||||||
|
|
@ -122,3 +122,4 @@ export const AI = createAI({
|
||||||
initialUIState,
|
initialUIState,
|
||||||
initialAIState
|
initialAIState
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -61,6 +61,7 @@ export default function RootLayout({
|
||||||
// </html>
|
// </html>
|
||||||
// )
|
// )
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
|
|
@ -70,7 +71,8 @@ export default function RootLayout({
|
||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
{/* <Header /> */}
|
{/* <Header /> */}
|
||||||
<AI>{children}</AI>
|
{children}
|
||||||
|
{/* <AI>{children}</AI> */}
|
||||||
{/* <Footer /> */}
|
{/* <Footer /> */}
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,34 @@ import Link from "next/link";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
|
|
||||||
import { BLOG_PATH, getContentData, getFilePaths, getPost, getQAContent } from "@/lib/mdx-helper";
|
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 { Header, NavBack, TimeP } from "@/components/header";
|
||||||
import { baseTitle, baseURL, keywordsRoot } from "@/lib/metadata";
|
import { baseTitle, baseURL, keywordsRoot } from "@/lib/metadata";
|
||||||
|
import { getBaseUrl } from "@/lib/http/get-base-url"; // ✅ 不是 export 它,而是 import 用
|
||||||
|
|
||||||
export const runtime = "nodejs";
|
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 = {
|
type Props = {
|
||||||
params: { locale: string, slug: string; title: string; description: string; authorName: string };
|
params: { locale: string, slug: string; title: string; description: string; authorName: string };
|
||||||
searchParams: { [key: string]: string | string[] | undefined };
|
searchParams: { [key: string]: string | string[] | undefined };
|
||||||
|
|
@ -32,7 +54,8 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||||
return notFound();
|
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 ogUrl = new URL("/og/blog", baseUrl);
|
||||||
const author = authors[frontmatter.author];
|
const author = authors[frontmatter.author];
|
||||||
ogUrl.searchParams.set("title", frontmatter.title ?? "");
|
ogUrl.searchParams.set("title", frontmatter.title ?? "");
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { Header, NavBack } from "@/components/header";
|
||||||
import { Footer } from "@/components/footer";
|
import { Footer } from "@/components/footer";
|
||||||
import { Button, Checkbox, Form, GetProp, Input, Radio, RadioChangeEvent, Space } from "antd";
|
import { Button, Checkbox, Form, GetProp, Input, Radio, RadioChangeEvent, Space } from "antd";
|
||||||
import { useEffect, useRef, useState } from "react";
|
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 toast from 'react-hot-toast';
|
||||||
import { UserData } from "@/components/user-menu";
|
import { UserData } from "@/components/user-menu";
|
||||||
import { useLocalStorage } from "@/lib/hooks/use-local-storage";
|
import { useLocalStorage } from "@/lib/hooks/use-local-storage";
|
||||||
|
|
@ -132,6 +132,7 @@ export default function PostsPage() {
|
||||||
|
|
||||||
setIsLoadingInfo(true);
|
setIsLoadingInfo(true);
|
||||||
async function initFunc() {
|
async function initFunc() {
|
||||||
|
const service = await getService();
|
||||||
await service.post('/api/v1/customer/sub-info', {
|
await service.post('/api/v1/customer/sub-info', {
|
||||||
email: infoRef.current.email
|
email: infoRef.current.email
|
||||||
}, {
|
}, {
|
||||||
|
|
@ -178,6 +179,7 @@ export default function PostsPage() {
|
||||||
|
|
||||||
setInitLoading(true);
|
setInitLoading(true);
|
||||||
async function initFunc() {
|
async function initFunc() {
|
||||||
|
const service = await getService();
|
||||||
await service.post('/api/v1/tag/list', {
|
await service.post('/api/v1/tag/list', {
|
||||||
}, {
|
}, {
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -268,6 +270,7 @@ export default function PostsPage() {
|
||||||
if (isLoading) return
|
if (isLoading) return
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
const service = await getService();
|
||||||
let result: any = await service.post('/api/v1/customer/edit', {
|
let result: any = await service.post('/api/v1/customer/edit', {
|
||||||
language,
|
language,
|
||||||
"first_name": values.first_name,
|
"first_name": values.first_name,
|
||||||
|
|
@ -303,6 +306,7 @@ export default function PostsPage() {
|
||||||
|
|
||||||
console.log("values.first_name", values.first_name)
|
console.log("values.first_name", values.first_name)
|
||||||
// setIsLoading(true);
|
// setIsLoading(true);
|
||||||
|
const service = await getService();
|
||||||
let result2: any = await service.post('/api/v1/customer/subscribe', {
|
let result2: any = await service.post('/api/v1/customer/subscribe', {
|
||||||
language,
|
language,
|
||||||
"first_name": values.first_name,
|
"first_name": values.first_name,
|
||||||
|
|
@ -331,6 +335,7 @@ export default function PostsPage() {
|
||||||
|
|
||||||
}
|
}
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
const service = await getService();
|
||||||
let result3: any = await service.post('/api/v1/customer/unsubscribe', {
|
let result3: any = await service.post('/api/v1/customer/unsubscribe', {
|
||||||
language,
|
language,
|
||||||
"email": values.email,
|
"email": values.email,
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -21,6 +21,12 @@ export const {
|
||||||
Google,
|
Google,
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// ✅ 加这一行,信任所有 Host,彻底解决动态部署报错问题
|
||||||
|
trustHost: true,
|
||||||
|
|
||||||
|
// ✅ 加这一行,防止缺失 secret 报错
|
||||||
|
secret: 'hardcoded-super-secret-key-please-change',
|
||||||
|
|
||||||
callbacks: {
|
callbacks: {
|
||||||
async jwt({ token, profile }) {
|
async jwt({ token, profile }) {
|
||||||
if (profile?.id) {
|
if (profile?.id) {
|
||||||
|
|
|
||||||
|
|
@ -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 toast from 'react-hot-toast';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
@ -106,7 +106,8 @@ const NineGrid: React.FC<NineGridProps> = ({
|
||||||
|
|
||||||
const [selectedCategory, setSelectedCategory] = useState(categoriesRef.current[0].value);
|
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 [currentPage, setCurrentPage] = React.useState(0);
|
||||||
|
|
||||||
const [isQA, setIsQA] = useState(false);
|
const [isQA, setIsQA] = useState(false);
|
||||||
|
|
@ -151,6 +152,7 @@ const NineGrid: React.FC<NineGridProps> = ({
|
||||||
|
|
||||||
|
|
||||||
setInitLoading(true);
|
setInitLoading(true);
|
||||||
|
const service = await getService();
|
||||||
await service.post('/api/v1/qa/list', {
|
await service.post('/api/v1/qa/list', {
|
||||||
language,
|
language,
|
||||||
"page_no": current - 1,
|
"page_no": current - 1,
|
||||||
|
|
@ -201,7 +203,7 @@ const NineGrid: React.FC<NineGridProps> = ({
|
||||||
|
|
||||||
tag = tag == categoriesRef.current[0].value ? "" : tag
|
tag = tag == categoriesRef.current[0].value ? "" : tag
|
||||||
|
|
||||||
|
const service = await getService();
|
||||||
await service.post('/api/v1/news/list', {
|
await service.post('/api/v1/news/list', {
|
||||||
"id": 0,
|
"id": 0,
|
||||||
language,
|
language,
|
||||||
|
|
@ -331,6 +333,14 @@ const NineGrid: React.FC<NineGridProps> = ({
|
||||||
</ArticleList>
|
</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>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -488,6 +498,7 @@ const Article = () => {
|
||||||
let isMounted = true
|
let isMounted = true
|
||||||
|
|
||||||
async function initFunc() {
|
async function initFunc() {
|
||||||
|
const service = await getService();
|
||||||
await service.post('/api/v1/tag/list', {
|
await service.post('/api/v1/tag/list', {
|
||||||
}, {
|
}, {
|
||||||
headers: {
|
headers: {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import React from "react";
|
||||||
import { FadeIn, FadeInStagger } from "../landing/fade-in";
|
import { FadeIn, FadeInStagger } from "../landing/fade-in";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { truncateString } from "@/lib/utils";
|
import { truncateString } from "@/lib/utils";
|
||||||
|
import { CloudDownload } from 'lucide-react';
|
||||||
interface CardProps {
|
interface CardProps {
|
||||||
image: string;
|
image: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -98,21 +98,31 @@ export const Card: React.FC<ArticleData> = (articleData) => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 🔥 只有当 model_parameter 没有值时显示叹号 */}
|
{/* 🔥 只有当 model_parameter 没有值时显示叹号 */}
|
||||||
{(!articleData.model_parameter) && (
|
{/* {(!articleData.model_parameter) && (
|
||||||
<div style={{
|
<div style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: '8px',
|
bottom: '8px',
|
||||||
right: '8px',
|
right: '8px',
|
||||||
color: 'red', // 只保留文字颜色为红色
|
color: 'red',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
fontSize: '24px', // 字体稍微大一点,更容易看到
|
fontSize: '24px',
|
||||||
zIndex: 2,
|
zIndex: 2,
|
||||||
}}>
|
}}>
|
||||||
!
|
!
|
||||||
</div>
|
</div>
|
||||||
|
)} */}
|
||||||
|
{(!articleData.model_parameter) && (
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '8px',
|
||||||
|
right: '8px',
|
||||||
|
color: 'red',
|
||||||
|
zIndex: 2,
|
||||||
|
}}>
|
||||||
|
<CloudDownload size={24} strokeWidth={2.5} />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 } =
|
const { messages, append, setMessages, reload, stop, isLoading, isSocket, input, setInput } =
|
||||||
useISDK({
|
useISDK({
|
||||||
api: `${process.env.NEXT_PUBLIC_CLIENT_BASE_WS}`,
|
api: `${process.env.NEXT_PUBLIC_CLIENT_BASE_WS}`,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,7 @@ import Link from "next/link";
|
||||||
import { Container } from "@/components/landing/container";
|
import { Container } from "@/components/landing/container";
|
||||||
import { FadeIn } from "@/components/landing/fade-in";
|
import { FadeIn } from "@/components/landing/fade-in";
|
||||||
import { NewsletterForm } from "@/components/landing/newsletter";
|
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 { useLocalStorage } from '@/lib/hooks/use-local-storage';
|
||||||
import { UserData } from './user-menu';
|
import { UserData } from './user-menu';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
@ -68,6 +68,7 @@ export function Footer({ className, ...props }: React.ComponentProps<'p'>) {
|
||||||
console.log('Finish:', values);
|
console.log('Finish:', values);
|
||||||
if (isLoading) return
|
if (isLoading) return
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
const service = await getService();
|
||||||
await service.post('/api/v1/customer/subscribe', {
|
await service.post('/api/v1/customer/subscribe', {
|
||||||
"first_name": values.first_name,
|
"first_name": values.first_name,
|
||||||
"email": values.email,
|
"email": values.email,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -30,7 +30,6 @@ import Image from 'next/image';
|
||||||
import logoImage from '@/components/images/logo.png';
|
import logoImage from '@/components/images/logo.png';
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
||||||
// import { Button } from './ui/button'
|
|
||||||
import { Flex, Text } from '@radix-ui/themes';
|
import { Flex, Text } from '@radix-ui/themes';
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
@ -39,11 +38,10 @@ import { LogoAI } from '@/components/chat'
|
||||||
|
|
||||||
import { useState, useRef, } from "react";
|
import { useState, useRef, } from "react";
|
||||||
|
|
||||||
import { Trash2 } from "lucide-react";
|
import { Trash2, CloudDownload } from "lucide-react";
|
||||||
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BadgeInfo,
|
BadgeInfo,
|
||||||
Tags,
|
Tags,
|
||||||
|
|
@ -73,7 +71,7 @@ export function Header() {
|
||||||
const soonFunc = () => {
|
const soonFunc = () => {
|
||||||
message.info(t("soon"))
|
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 (
|
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">
|
<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 }) {
|
export function DetailPageHeader({ data }: { data: any }) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [statusText, setStatusText] = useState(data?.statusText || "加载中...");
|
const [statusText, setStatusText] = useState(data?.statusText || "加载中...");
|
||||||
|
|
@ -126,14 +171,38 @@ export function DetailPageHeader({ data }: { data: any }) {
|
||||||
const [hasWSConnected, setHasWSConnected] = useState(false);
|
const [hasWSConnected, setHasWSConnected] = useState(false);
|
||||||
const [statusLoaded, setStatusLoaded] = useState(false);
|
const [statusLoaded, setStatusLoaded] = useState(false);
|
||||||
const [canDeploy, setCanDeploy] = useState(true);
|
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 socketRef = useRef<WebSocket | null>(null);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
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();
|
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}`);
|
const socket = new WebSocket(`${wsBase}/status/${userName}/${id}`);
|
||||||
socketRef.current = socket;
|
socketRef.current = socket;
|
||||||
|
|
||||||
|
|
@ -290,6 +359,7 @@ export function DetailPageHeader({ data }: { data: any }) {
|
||||||
|
|
||||||
const code = result?.header?.code;
|
const code = result?.header?.code;
|
||||||
const status = result?.data?.data?.status;
|
const status = result?.data?.data?.status;
|
||||||
|
setCurrentStatus(status || ""); //...............................................
|
||||||
const userData = JSON.parse(localStorage.getItem("UserData") || "null");
|
const userData = JSON.parse(localStorage.getItem("UserData") || "null");
|
||||||
const userName = userData?.user_name;
|
const userName = userData?.user_name;
|
||||||
const id = data?.id;
|
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(() => {
|
useEffect(() => {
|
||||||
fetchDeployStatus();
|
fetchDeployStatus();
|
||||||
}, [data?.id]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (socketRef.current) {
|
if (socketRef.current) {
|
||||||
|
|
@ -465,16 +624,136 @@ export function DetailPageHeader({ data }: { data: any }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{statusLoaded && showDelete && (
|
<div className="flex items-center gap-[5px] self-end">
|
||||||
<button
|
{hasNonEmptyExtraData && (
|
||||||
onClick={handleDelete}
|
<div className="flex flex-col items-center gap-[2px]">
|
||||||
className="hover:text-gray-700 transition self-end"
|
<button
|
||||||
disabled={loading}
|
onClick={handleDownloadNewVersion}
|
||||||
title="删除"
|
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"
|
||||||
<Trash2 size={20} />
|
title="下载并安装新版本"
|
||||||
</button>
|
>
|
||||||
)}
|
{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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import {
|
||||||
DropdownMenuTrigger
|
DropdownMenuTrigger
|
||||||
} from '@/components/ui/dropdown-menu'
|
} from '@/components/ui/dropdown-menu'
|
||||||
import { sync } from 'framer-motion'
|
import { sync } from 'framer-motion'
|
||||||
import service from '@/lib/http/service'
|
import { getService } from '@/lib/http/service'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
@ -118,6 +118,7 @@ export function UserMenu({ user }: { user: UserData }) {
|
||||||
console.log('logout')
|
console.log('logout')
|
||||||
if (isLoading) return
|
if (isLoading) return
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
const service = await getService();
|
||||||
await service.post('/api/v1/customer/logout', {
|
await service.post('/api/v1/customer/logout', {
|
||||||
}, {
|
}, {
|
||||||
headers: {
|
headers: {
|
||||||
|
|
|
||||||
|
|
@ -31,12 +31,80 @@
|
||||||
// } as any;
|
// } 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() {
|
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 配置");
|
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 {
|
return {
|
||||||
baseURL: `http://${ip}:80`, // 端口如需动态可再加参数
|
baseURL: `${protocol}://${ip}:${port}`,
|
||||||
method: 'post',
|
method: 'post',
|
||||||
timeout: 60 * 1000,
|
timeout: 60 * 1000,
|
||||||
headers: { 'Content-Type': 'application/json; charset=UTF-8' },
|
headers: { 'Content-Type': 'application/json; charset=UTF-8' },
|
||||||
|
|
|
||||||
|
|
@ -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}`;
|
||||||
|
}
|
||||||
|
|
@ -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}`;
|
||||||
|
}
|
||||||
|
|
@ -25,7 +25,8 @@ export async function getRuntimeEnv(key: string): Promise<string | undefined> {
|
||||||
// 2. 第3次开始查localStorage兜底
|
// 2. 第3次开始查localStorage兜底
|
||||||
if (retries >= 2) {
|
if (retries >= 2) {
|
||||||
const cached = window.localStorage.getItem(LOCAL_KEY);
|
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") {
|
if (cachedIp && key === "SUPABASE_URL") {
|
||||||
console.warn(`[env] [${key}] 第${retries}次重试后window.RUNTIME_ENV还没拿到合法IP,localStorage兜底: ${cached},提取IP: ${cachedIp}`);
|
console.warn(`[env] [${key}] 第${retries}次重试后window.RUNTIME_ENV还没拿到合法IP,localStorage兜底: ${cached},提取IP: ${cachedIp}`);
|
||||||
return cachedIp;
|
return cachedIp;
|
||||||
|
|
@ -45,7 +46,7 @@ export async function getRuntimeEnv(key: string): Promise<string | undefined> {
|
||||||
// 服务端
|
// 服务端
|
||||||
const val = process.env[key];
|
const val = process.env[key];
|
||||||
const ip = extractIp(val);
|
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;
|
return ip;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import rehypeSlug from "rehype-slug";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import { BUNDLED_LANGUAGES, type HighlighterOptions, getHighlighter } from "shiki";
|
import { BUNDLED_LANGUAGES, type HighlighterOptions, getHighlighter } from "shiki";
|
||||||
import gitHubLight from "shiki/themes/github-light.json";
|
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";
|
import { ArticleData, QAData } from "@/components/article/article";
|
||||||
|
|
||||||
export const BLOG_PATH = path.join(process.cwd(), "content", "blog");
|
export const BLOG_PATH = path.join(process.cwd(), "content", "blog");
|
||||||
|
|
@ -108,6 +108,7 @@ type Frontmatter = {
|
||||||
|
|
||||||
type AIFrontmatter = {
|
type AIFrontmatter = {
|
||||||
id:string;
|
id:string;
|
||||||
|
org_id:string;
|
||||||
p_name:string;
|
p_name:string;
|
||||||
title: string;
|
title: string;
|
||||||
date: string;
|
date: string;
|
||||||
|
|
@ -116,6 +117,7 @@ type AIFrontmatter = {
|
||||||
image_url: string;
|
image_url: string;
|
||||||
logo_url: string;
|
logo_url: string;
|
||||||
model_parameter: number;
|
model_parameter: number;
|
||||||
|
extra_data: Record<string, any>;
|
||||||
visible: boolean | undefined;
|
visible: boolean | undefined;
|
||||||
salary: string | undefined;
|
salary: string | undefined;
|
||||||
level: string | undefined;
|
level: string | undefined;
|
||||||
|
|
@ -253,6 +255,7 @@ export const getPostContent = async (language: string, slug: string): Promise<AI
|
||||||
const PageSize = 1
|
const PageSize = 1
|
||||||
|
|
||||||
|
|
||||||
|
const service = await getService();
|
||||||
let data: ArticleData = await service.post('/api/v1/news/list', {
|
let data: ArticleData = await service.post('/api/v1/news/list', {
|
||||||
"id": Number(slug),
|
"id": Number(slug),
|
||||||
// "tag": "", // #Blockchain
|
// "tag": "", // #Blockchain
|
||||||
|
|
@ -284,6 +287,7 @@ export const getPostContent = async (language: string, slug: string): Promise<AI
|
||||||
const serialized = await mdxSerialized({ rawMdx });
|
const serialized = await mdxSerialized({ rawMdx });
|
||||||
const frontmatter = serialized.frontmatter as AIFrontmatter;
|
const frontmatter = serialized.frontmatter as AIFrontmatter;
|
||||||
frontmatter.id = String(data.id)
|
frontmatter.id = String(data.id)
|
||||||
|
frontmatter.org_id = String(data.org_id)
|
||||||
frontmatter.p_name = data.p_name
|
frontmatter.p_name = data.p_name
|
||||||
frontmatter.title = data.main_title
|
frontmatter.title = data.main_title
|
||||||
frontmatter.description = data.sub_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.image_url = data.image_url
|
||||||
frontmatter.logo_url = data.logo_url
|
frontmatter.logo_url = data.logo_url
|
||||||
frontmatter.model_parameter = data.model_parameter
|
frontmatter.model_parameter = data.model_parameter
|
||||||
|
frontmatter.extra_data = data.extra_data
|
||||||
// const headings = data.main_title;
|
// 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 rawMdx = await raw({ contentPath: BLOG_PATH, filepath: filepath });
|
||||||
|
|
||||||
const PageSize = 1
|
const PageSize = 1
|
||||||
|
const service = await getService();
|
||||||
let data: QAData = await service.post('/api/v1/qa/list', {
|
let data: QAData = await service.post('/api/v1/qa/list', {
|
||||||
"id": Number(slug),
|
"id": Number(slug),
|
||||||
// "tag": "", // #Blockchain
|
// "tag": "", // #Blockchain
|
||||||
|
|
|
||||||
|
|
@ -53,3 +53,9 @@ export type ChainData = {
|
||||||
icon?: string
|
icon?: string
|
||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
RUNTIME_ENV?: Record<string, string>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,7 +18,7 @@ const nextConfig = {
|
||||||
},
|
},
|
||||||
compiler: {
|
compiler: {
|
||||||
removeConsole: false, // 保留所有模式中的 console.log
|
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,
|
emotion: true,
|
||||||
},
|
},
|
||||||
webpack: (config, { isServer }) => {
|
webpack: (config, { isServer }) => {
|
||||||
|
|
@ -26,32 +26,13 @@ const nextConfig = {
|
||||||
type: "memory",
|
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;
|
return config;
|
||||||
},
|
},
|
||||||
eslint: {
|
eslint: {
|
||||||
// Warning: This allows production builds to successfully complete even if
|
// Warning: This allows production builds to successfully complete even if
|
||||||
// your project has ESLint errors.
|
// your project has ESLint errors.
|
||||||
ignoreDuringBuilds: true,
|
ignoreDuringBuilds: true,
|
||||||
},
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
esmExternals: true,
|
esmExternals: true,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
"**/*.tsx",
|
"**/*.tsx",
|
||||||
".next/types/**/*.ts"
|
".next/types/**/*.ts"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"test",
|
"test",
|
||||||
"node_modules"
|
"node_modules"
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import rehypeSlug from "rehype-slug";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import { BUNDLED_LANGUAGES, type HighlighterOptions, getHighlighter } from "shiki";
|
import { BUNDLED_LANGUAGES, type HighlighterOptions, getHighlighter } from "shiki";
|
||||||
import gitHubLight from "shiki/themes/github-light.json";
|
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";
|
import { ArticleData, QAData } from "@/components/article/article";
|
||||||
|
|
||||||
export const BLOG_PATH = path.join(process.cwd(), "content", "blog");
|
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 PageSize = 1
|
||||||
|
|
||||||
|
const service = await getService();
|
||||||
let data: ArticleData = await service.post('/api/v1/news/list', {
|
let data: ArticleData = await service.post('/api/v1/news/list', {
|
||||||
"id": Number(slug),
|
"id": Number(slug),
|
||||||
// "tag": "", // #Blockchain
|
// "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 rawMdx = await raw({ contentPath: BLOG_PATH, filepath: filepath });
|
||||||
|
|
||||||
const PageSize = 1
|
const PageSize = 1
|
||||||
|
const service = await getService();
|
||||||
let data: QAData = await service.post('/api/v1/qa/list', {
|
let data: QAData = await service.post('/api/v1/qa/list', {
|
||||||
"id": Number(slug),
|
"id": Number(slug),
|
||||||
// "tag": "", // #Blockchain
|
// "tag": "", // #Blockchain
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import rehypeSlug from "rehype-slug";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import { BUNDLED_LANGUAGES, type HighlighterOptions, getHighlighter } from "shiki";
|
import { BUNDLED_LANGUAGES, type HighlighterOptions, getHighlighter } from "shiki";
|
||||||
import gitHubLight from "shiki/themes/github-light.json";
|
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";
|
import { ArticleData, QAData } from "@/components/article/article";
|
||||||
|
|
||||||
export const BLOG_PATH = path.join(process.cwd(), "content", "blog");
|
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 PageSize = 1
|
||||||
|
|
||||||
|
const service = await getService();
|
||||||
let data: ArticleData = await service.post('/api/v1/news/list', {
|
let data: ArticleData = await service.post('/api/v1/news/list', {
|
||||||
"id": Number(slug),
|
"id": Number(slug),
|
||||||
// "tag": "", // #Blockchain
|
// "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 rawMdx = await raw({ contentPath: BLOG_PATH, filepath: filepath });
|
||||||
|
|
||||||
const PageSize = 1
|
const PageSize = 1
|
||||||
|
const service = await getService();
|
||||||
let data: QAData = await service.post('/api/v1/qa/list', {
|
let data: QAData = await service.post('/api/v1/qa/list', {
|
||||||
"id": Number(slug),
|
"id": Number(slug),
|
||||||
// "tag": "", // #Blockchain
|
// "tag": "", // #Blockchain
|
||||||
|
|
|
||||||
39
build.sh
39
build.sh
|
|
@ -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
|
rm -rf apps/blogai/.next
|
||||||
docker build \
|
|
||||||
|
echo "📦 构建镜像..."
|
||||||
|
if ! docker build \
|
||||||
--build-arg http_proxy=http://127.0.0.1:7890 \
|
--build-arg http_proxy=http://127.0.0.1:7890 \
|
||||||
--build-arg https_proxy=http://127.0.0.1:7890 \
|
--build-arg https_proxy=http://127.0.0.1:7890 \
|
||||||
--build-arg BLOGAI_HOST=ai.szaiai.com \
|
--build-arg BLOGAI_HOST=ai.szaiai.com \
|
||||||
--no-cache \
|
--no-cache \
|
||||||
--network=host \
|
--network=host \
|
||||||
-t cradle:latest \
|
-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
|
|
||||||
76387
pnpm-lock.yaml
76387
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
Loading…
Reference in New Issue