diff --git a/.env.template b/.env.template index f4c8e135cc..c42d03ba7e 100644 --- a/.env.template +++ b/.env.template @@ -19,3 +19,5 @@ TMDB_API_KEY= S3_ACCESS_KEY= S3_SECRET_KEY= + +GH_TOKEN= \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000000..3e4ed2d08b --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,42 @@ +on: + push: + branches: [main] + +name: CI Build + +jobs: + build: + name: Upload CI Build artifact + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x] + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + lfs: true + - name: Checkout LFS objects + run: git lfs checkout + + - uses: pnpm/action-setup@v2.4.0 + with: + version: 8.x.x + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + - name: Install dependencies + run: pnpm install + - name: Build project + + run: | + sh ./ci-release-build.sh + + - uses: actions/upload-artifact@v4 + with: + name: artifact + path: assets/release.zip diff --git a/.github/workflows/release-docker.yml b/.github/workflows/release-docker.yml new file mode 100644 index 0000000000..d24a37b889 --- /dev/null +++ b/.github/workflows/release-docker.yml @@ -0,0 +1,47 @@ +name: Docker (release) + +on: + push: + # Sequence of patterns matched against refs/tags + tags: + - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + innei/mx-server + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha + type=raw,value=latest + - name: Build and export to Docker + uses: docker/build-push-action@v5 + with: + context: . + load: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + push: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..88d89b6983 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,63 @@ +on: + push: + # Sequence of patterns matched against refs/tags + tags: + - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 + +name: Release + +jobs: + build: + name: Upload Release Asset + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x] + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + lfs: true + - name: Checkout LFS objects + run: git lfs checkout + + - uses: pnpm/action-setup@v2.4.0 + with: + version: 8.x.x + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + - name: Install dependencies + run: pnpm install + - name: Build project + + run: | + sh ./ci-release-build.sh + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + draft: false + prerelease: false + - run: npx changelogithub + continue-on-error: true + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + - name: Upload Release Asset + id: upload-release-asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + asset_name: release.zip + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: ./assets/release.zip + asset_content_type: application/zip diff --git a/Dockerfile b/Dockerfile index c4cf6d9713..cdcf719a5b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,17 @@ FROM node:18-alpine AS base +RUN npm install -g --arch=x64 --platform=linux sharp + FROM base AS deps RUN apk add --no-cache libc6-compat - RUN apk add --no-cache python3 make g++ WORKDIR /app COPY . . + RUN npm install -g pnpm RUN pnpm install @@ -26,11 +28,21 @@ ENV NODE_ENV production ARG BASE_URL ARG NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY ARG CLERK_SECRET_KEY +ARG S3_ACCESS_KEY +ARG S3_SECRET_KEY +ARG WEBHOOK_SECRET +ARG TMDB_API_KEY +ARG GH_TOKEN ENV BASE_URL=${BASE_URL} ENV NEXT_PUBLIC_API_URL=${BASE_URL}/api/v2 ENV NEXT_PUBLIC_GATEWAY_URL=${BASE_URL} ENV NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=${NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY} ENV CLERK_SECRET_KEY=${CLERK_SECRET_KEY} +ENV S3_ACCESS_KEY=${S3_ACCESS_KEY} +ENV S3_SECRET_KEY=${S3_SECRET_KEY} +ENV TMDB_API_KEY=${TMDB_API_KEY} +ENV WEBHOOK_SECRET=${WEBHOOK_SECRET} +ENV GH_TOKEN=${GH_TOKEN} RUN pnpm build @@ -48,5 +60,5 @@ COPY --from=builder /app/.next/server ./.next/server EXPOSE 2323 ENV PORT 2323 - -CMD echo "Mix Space Web [Shiro] Image." && node server.js; \ No newline at end of file +ENV NEXT_SHARP_PATH=/usr/local/lib/node_modules/sharp +CMD echo "Mix Space Web [Shiro] Image." && node server.js; \ No newline at end of file diff --git a/ci-release-build.sh b/ci-release-build.sh new file mode 100644 index 0000000000..05e02fa8c6 --- /dev/null +++ b/ci-release-build.sh @@ -0,0 +1,22 @@ +#!env bash +set -e +CWD=$(pwd) + +npm run build +cd .next +pwd +rm -rf cache +cp -r ../public ./standalone/public + +cd ./standalone +echo ';process.title = "Shiro (NextJS)"' >>server.js +mv ../static/ ./.next/static + +cp $CWD/ecosystem.standalone.config.js ./ecosystem.config.js +cp $CWD/.env.template .env + +cd .. + +mkdir -p $CWD/assets +rm -rf $CWD/assets/release.zip +zip --symlinks -r $CWD/assets/release.zip ./* diff --git a/docker-compose.yml b/docker-compose.yml index ad2113424c..1bf1968532 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,12 @@ -version: "3" +version: '3' services: shiro: container_name: shiro build: context: . - args: - - BASE_URL=REPLACE_WITH_YOUR_BASE_URL - - NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=REPLACE_WITH_YOUR_PUBLISHABLE_KEY - - CLERK_SECRET_KEY=REPLACE_WITH_YOUR_SECRET_KEY + volumes: + - /app/.env:./.docker-env restart: always ports: - - 2323:2323 \ No newline at end of file + - 2323:2323 diff --git a/package.json b/package.json index ca1c6a5f73..2bc13ff22f 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ "mermaid": "10.9.0", "nanoid": "^5.0.6", "next": "14.1.4", + "next-runtime-env": "3.2.1", "next-themes": "0.2.1", "ofetch": "1.3.4", "openai": "4.29.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c18dec79ae..70d0a3ae04 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -197,6 +197,9 @@ dependencies: next: specifier: 14.1.4 version: 14.1.4(@babel/core@7.24.3)(react-dom@18.2.0)(react@18.2.0) + next-runtime-env: + specifier: 3.2.1 + version: 3.2.1(next@14.1.4)(react@18.2.0) next-themes: specifier: 0.2.1 version: 0.2.1(next@14.1.4)(react-dom@18.2.0)(react@18.2.0) @@ -7943,6 +7946,16 @@ packages: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true + /next-runtime-env@3.2.1(next@14.1.4)(react@18.2.0): + resolution: {integrity: sha512-vyuTpUMwnGUA9GjOGXNnuosqFQOa52IcKmu4mnTaQi4EqeFoCy3aYUxSF6mLaKkQ5yNC8yLa/gFjny74czpc9Q==} + peerDependencies: + next: ^14 + react: ^18 + dependencies: + next: 14.1.4(@babel/core@7.24.3)(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + dev: false + /next-themes@0.2.1(next@14.1.4)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==} peerDependencies: diff --git a/src/app/(app)/(home)/layout.tsx b/src/app/(app)/(home)/layout.tsx index 70629c7804..78a10e7322 100644 --- a/src/app/(app)/(home)/layout.tsx +++ b/src/app/(app)/(home)/layout.tsx @@ -9,6 +9,7 @@ import { requestErrorHandler } from '~/lib/request.server' import { queryKey } from './query' +export const dynamic = 'force-dynamic' export const revalidate = 3600 export default async function HomeLayout(props: PropsWithChildren) { diff --git a/src/app/(app)/(note-topic)/notes/(topic-detail)/topics/[slug]/layout.tsx b/src/app/(app)/(note-topic)/notes/(topic-detail)/topics/[slug]/layout.tsx index ece497bff5..349987fa79 100644 --- a/src/app/(app)/(note-topic)/notes/(topic-detail)/topics/[slug]/layout.tsx +++ b/src/app/(app)/(note-topic)/notes/(topic-detail)/topics/[slug]/layout.tsx @@ -9,6 +9,7 @@ import { getQueryClient } from '~/lib/query-client.server' import { getTopicQuery } from './query' +export const dynamic = 'force-dynamic' export const generateMetadata = async ( props: NextPageParams<{ slug: string diff --git a/src/app/(app)/(note-topic)/notes/(topic-detail)/topics/[slug]/page.tsx b/src/app/(app)/(note-topic)/notes/(topic-detail)/topics/[slug]/page.tsx index 2a3462f4ec..0d52c21122 100644 --- a/src/app/(app)/(note-topic)/notes/(topic-detail)/topics/[slug]/page.tsx +++ b/src/app/(app)/(note-topic)/notes/(topic-detail)/topics/[slug]/page.tsx @@ -14,6 +14,7 @@ import { routeBuilder, Routes } from '~/lib/route-builder' import { getTopicQuery } from './query' +export const dynamic = 'force-dynamic' export default function Page() { const { slug } = useParams() const { data } = useQuery({ diff --git a/src/app/(app)/(note-topic)/notes/topics/layout.tsx b/src/app/(app)/(note-topic)/notes/topics/layout.tsx index 841b2cef17..7a450591fe 100644 --- a/src/app/(app)/(note-topic)/notes/topics/layout.tsx +++ b/src/app/(app)/(note-topic)/notes/topics/layout.tsx @@ -9,6 +9,7 @@ import { getQueryClient } from '~/lib/query-client.server' import { topicsQuery } from './query' +export const dynamic = 'force-dynamic' export const metadata: Metadata = { title: '专栏', } diff --git a/src/app/(app)/(page-detail)/[slug]/layout.tsx b/src/app/(app)/(page-detail)/[slug]/layout.tsx index e72ade454a..bba546e396 100644 --- a/src/app/(app)/(page-detail)/[slug]/layout.tsx +++ b/src/app/(app)/(page-detail)/[slug]/layout.tsx @@ -30,6 +30,7 @@ import { PageTitle, } from './pageExtra' +export const dynamic = 'force-dynamic' const getData = async (params: PageParams) => { attachUAAndRealIp() const data = await getQueryClient() diff --git a/src/app/(app)/categories/[slug]/layout.tsx b/src/app/(app)/categories/[slug]/layout.tsx index 86bf922afa..3f82d035d4 100644 --- a/src/app/(app)/categories/[slug]/layout.tsx +++ b/src/app/(app)/categories/[slug]/layout.tsx @@ -9,6 +9,7 @@ import { getQueryClient } from '~/lib/query-client.server' import { getPageBySlugQuery } from './query' +export const dynamic = 'force-dynamic' const getData = async (params: { slug: string }) => { attachUAAndRealIp() const data = await getQueryClient().fetchQuery( diff --git a/src/app/(app)/layout.tsx b/src/app/(app)/layout.tsx index d858791f25..e87f7f7e96 100644 --- a/src/app/(app)/layout.tsx +++ b/src/app/(app)/layout.tsx @@ -1,6 +1,7 @@ /* eslint-disable no-console */ import { cache } from 'react' import { ToastContainer } from 'react-toastify' +import { env, PublicEnvScript } from 'next-runtime-env' import type { Metadata, Viewport } from 'next' import type { PropsWithChildren } from 'react' @@ -128,7 +129,7 @@ export const generateMetadata = async (): Promise => { }, } satisfies Metadata } - +export const dynamic = 'force-dynamic' export default async function RootLayout(props: PropsWithChildren) { const { children } = props @@ -137,7 +138,7 @@ export default async function RootLayout(props: PropsWithChildren) { const themeConfig = data.theme return ( - + + diff --git a/src/app/(app)/notes/[id]/layout.tsx b/src/app/(app)/notes/[id]/layout.tsx index f88c857343..5807256237 100644 --- a/src/app/(app)/notes/[id]/layout.tsx +++ b/src/app/(app)/notes/[id]/layout.tsx @@ -21,6 +21,7 @@ import { Paper } from '../../../../components/layout/container/Paper' import { getData } from './api' import { Transition } from './Transition' +export const dynamic = 'force-dynamic' export const generateMetadata = async ({ params, }: { diff --git a/src/app/(app)/notes/[id]/page.tsx b/src/app/(app)/notes/[id]/page.tsx index bd8b28c24e..1acbcdb4c4 100644 --- a/src/app/(app)/notes/[id]/page.tsx +++ b/src/app/(app)/notes/[id]/page.tsx @@ -34,6 +34,7 @@ import { NoteTitle, } from './pageExtra' +export const dynamic = 'force-dynamic' export default async function Page(props: { params: { id: string diff --git a/src/app/(app)/posts/(post-detail)/[category]/[slug]/layout.tsx b/src/app/(app)/posts/(post-detail)/[category]/[slug]/layout.tsx index 5a0f38efd1..c46f7292c2 100644 --- a/src/app/(app)/posts/(post-detail)/[category]/[slug]/layout.tsx +++ b/src/app/(app)/posts/(post-detail)/[category]/[slug]/layout.tsx @@ -15,6 +15,7 @@ import { LayoutRightSideProvider } from '~/providers/shared/LayoutRightSideProvi import { getData } from './api' +export const dynamic = 'force-dynamic' export const generateMetadata = async ({ params, }: { diff --git a/src/app/(app)/posts/(post-detail)/[category]/[slug]/page.tsx b/src/app/(app)/posts/(post-detail)/[category]/[slug]/page.tsx index c308c97e90..a0f4edd7e6 100644 --- a/src/app/(app)/posts/(post-detail)/[category]/[slug]/page.tsx +++ b/src/app/(app)/posts/(post-detail)/[category]/[slug]/page.tsx @@ -28,6 +28,7 @@ import { PostTitle, } from './pageExtra' +export const dynamic = 'force-dynamic' const PostPage = async ({ params }: { params: PageParams }) => { const data = await getData(params) const { id } = data diff --git a/src/app/(app)/timeline/layout.tsx b/src/app/(app)/timeline/layout.tsx index 9bafb234fb..5b0f9426bb 100644 --- a/src/app/(app)/timeline/layout.tsx +++ b/src/app/(app)/timeline/layout.tsx @@ -14,7 +14,7 @@ import { apiClient } from '~/lib/request' export const metadata = { title: '时间线', } - +export const dynamic = 'force-dynamic' export default async (props: PropsWithChildren) => { attachUAAndRealIp() const header = headers() diff --git a/src/app/(app)/web-dev/layout.tsx b/src/app/(app)/web-dev/layout.tsx deleted file mode 100644 index 6c0fc3a003..0000000000 --- a/src/app/(app)/web-dev/layout.tsx +++ /dev/null @@ -1,15 +0,0 @@ -export const metadata = { - title: 'dev', -} - -export default function RootLayout({ - children, -}: { - children: React.ReactNode -}) { - return ( - <> - <>{children} - - ) -} diff --git a/src/app/(app)/web-dev/page.tsx b/src/app/(app)/web-dev/page.tsx deleted file mode 100644 index 09eed89439..0000000000 --- a/src/app/(app)/web-dev/page.tsx +++ /dev/null @@ -1,102 +0,0 @@ -/* eslint-disable react/display-name */ -'use client' - -import { useQuery } from '@tanstack/react-query' -import { useCallback, useEffect, useMemo } from 'react' -import { m } from 'framer-motion' - -import { - useActivityPresenceBySessionId, - useSocketIsConnect, - useSocketSessionId, -} from '~/atoms/hooks' -import { StyledButton } from '~/components/ui/button' -import { FloatPopover } from '~/components/ui/float-popover' -import { debounce } from '~/lib/lodash' -import { apiClient } from '~/lib/request' -import { usePageScrollLocation } from '~/providers/root/page-scroll-info-provider' -import { queries } from '~/queries/definition' -import { socketClient } from '~/socket' -import { SocketEmitEnum } from '~/types/events' - -export default () => { - const roomName = useMemo(() => `article-${111112222}`, []) - const identity = useSocketSessionId() - const update = useCallback( - debounce((position) => { - apiClient.activity.proxy.presence.update.post({ - data: { - identity, - position, - ts: Date.now(), - roomName, - sid: socketClient.socket.id, - }, - }) - }, 1000), - [identity], - ) - - const socketIsConnected = useSocketIsConnect() - useEffect(() => { - socketClient.emit(SocketEmitEnum.Join, { - roomName, - }) - - return () => { - socketClient.emit(SocketEmitEnum.Leave, { - roomName, - }) - } - }, [roomName, identity, socketIsConnected]) - - const { refetch } = useQuery({ - ...queries.activity.presence(roomName), - enabled: false, - }) - - const scrollLocation = usePageScrollLocation() - - useEffect(() => { - update((scrollLocation / document.body.scrollHeight) * 100) - }, [scrollLocation, update]) - - return ( - <> -
- update - refetch()}>get - -
-
- - ) -} - -const ReadPresenceTimeline = () => { - const sessionId = useSocketSessionId() - const activityPresence = useActivityPresenceBySessionId(sessionId) - - return ( -
- - } - > -

你在这里。

-

阅读进度 50%

-
-
- ) -} diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx index cf1ace258b..0369b243a2 100644 --- a/src/app/(dashboard)/layout.tsx +++ b/src/app/(dashboard)/layout.tsx @@ -13,6 +13,7 @@ import { queries } from '~/queries/definition' import './dashboard.css' +import { PublicEnvScript } from 'next-runtime-env' import type { Viewport } from 'next' import { MainLayout } from '~/components/modules/dashboard/layouts' @@ -61,6 +62,7 @@ export default async function RootLayout({ children }: PropsWithChildren) { type="image/x-icon" media="(prefers-color-scheme: light)" /> + { + const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, + }) const { searchParams } = req.nextUrl const dataString = searchParams.get('data') as string diff --git a/src/app/feed/route.tsx b/src/app/feed/route.tsx index ae276fd2c5..befda11c95 100644 --- a/src/app/feed/route.tsx +++ b/src/app/feed/route.tsx @@ -17,7 +17,7 @@ import { MentionRule } from '~/components/ui/markdown/parsers/mention' import { SpoilerRule } from '~/components/ui/markdown/parsers/spoiler' import { apiClient } from '~/lib/request' -// export const dynamic = 'force-dynamic' +export const dynamic = 'force-dynamic' export const revalidate = 86400 // 1 day interface RSSProps { diff --git a/src/constants/env.ts b/src/constants/env.ts index 8329e3dd77..45cfc7d4d2 100644 --- a/src/constants/env.ts +++ b/src/constants/env.ts @@ -1,12 +1,14 @@ +import { env } from 'next-runtime-env' + import { isClientSide, isDev } from '~/lib/env' export const API_URL: string = (() => { - if (isDev) return process.env.NEXT_PUBLIC_API_URL + if (isDev) return env('NEXT_PUBLIC_API_URL') - if (isClientSide && process.env.NEXT_PUBLIC_CLIENT_API_URL) { - return process.env.NEXT_PUBLIC_CLIENT_API_URL + if (isClientSide && env('NEXT_PUBLIC_CLIENT_API_URL')) { + return env('NEXT_PUBLIC_CLIENT_API_URL') } - return process.env.NEXT_PUBLIC_API_URL || '/api/v2' + return env('NEXT_PUBLIC_API_URL') || '/api/v2' })() as string -export const GATEWAY_URL = process.env.NEXT_PUBLIC_GATEWAY_URL || '' +export const GATEWAY_URL = env('NEXT_PUBLIC_GATEWAY_URL') || ''