Add TKMind platform extensions, H5/MindSpace stack, and deployment tooling.
Deploy Documentation / deploy (push) Has been cancelled
Canary / Prepare Version (push) Has been cancelled
Canary / build-cli (push) Has been cancelled
Canary / Upload Install Script (push) Has been cancelled
Canary / bundle-desktop (push) Has been cancelled
Canary / bundle-desktop-intel (push) Has been cancelled
Canary / bundle-desktop-linux (push) Has been cancelled
Canary / bundle-desktop-windows (push) Has been cancelled
Canary / bundle-desktop-windows-cuda (push) Has been cancelled
Canary / Release (push) Has been cancelled
Unused Dependencies / machete (push) Has been cancelled
CI / changes (push) Has been cancelled
CI / Check Rust Code Format (push) Has been cancelled
CI / Build and Test Rust Project (push) Has been cancelled
CI / Build Rust Project on Windows (push) Has been cancelled
CI / Check MSRV (push) Has been cancelled
CI / Lint Rust Code (push) Has been cancelled
CI / Check Generated Schemas are Up-to-Date (push) Has been cancelled
CI / Test and Lint Electron Desktop App (push) Has been cancelled
CI / H5 Plaza Tests and Build (push) Has been cancelled
Live Provider Tests / check-fork (push) Has been cancelled
Live Provider Tests / changes (push) Has been cancelled
Live Provider Tests / Build Binary (push) Has been cancelled
Live Provider Tests / Smoke Tests (push) Has been cancelled
Live Provider Tests / Smoke Tests (Code Execution) (push) Has been cancelled
Live Provider Tests / Compaction Tests (push) Has been cancelled
Live Provider Tests / goose server HTTP integration tests (push) Has been cancelled
Publish Ask AI Bot Docker Image / docker (push) Has been cancelled
Publish Docker Image / docker (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Deploy Documentation / deploy (push) Has been cancelled
Canary / Prepare Version (push) Has been cancelled
Canary / build-cli (push) Has been cancelled
Canary / Upload Install Script (push) Has been cancelled
Canary / bundle-desktop (push) Has been cancelled
Canary / bundle-desktop-intel (push) Has been cancelled
Canary / bundle-desktop-linux (push) Has been cancelled
Canary / bundle-desktop-windows (push) Has been cancelled
Canary / bundle-desktop-windows-cuda (push) Has been cancelled
Canary / Release (push) Has been cancelled
Unused Dependencies / machete (push) Has been cancelled
CI / changes (push) Has been cancelled
CI / Check Rust Code Format (push) Has been cancelled
CI / Build and Test Rust Project (push) Has been cancelled
CI / Build Rust Project on Windows (push) Has been cancelled
CI / Check MSRV (push) Has been cancelled
CI / Lint Rust Code (push) Has been cancelled
CI / Check Generated Schemas are Up-to-Date (push) Has been cancelled
CI / Test and Lint Electron Desktop App (push) Has been cancelled
CI / H5 Plaza Tests and Build (push) Has been cancelled
Live Provider Tests / check-fork (push) Has been cancelled
Live Provider Tests / changes (push) Has been cancelled
Live Provider Tests / Build Binary (push) Has been cancelled
Live Provider Tests / Smoke Tests (push) Has been cancelled
Live Provider Tests / Smoke Tests (Code Execution) (push) Has been cancelled
Live Provider Tests / Compaction Tests (push) Has been cancelled
Live Provider Tests / goose server HTTP integration tests (push) Has been cancelled
Publish Ask AI Bot Docker Image / docker (push) Has been cancelled
Publish Docker Image / docker (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Fork goose with custom MCP widgets, platform extensions (aider, git, web, search), MindSpace H5 backend/frontend, Plaza/Ops UIs, and deploy scripts for tkmind.cn. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,19 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #f5f0e5;
|
||||
--foreground: #17221d;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Geist, Geist_Mono } from 'next/font/google';
|
||||
import { Suspense } from 'react';
|
||||
import { Footer } from '@/components/layout/Footer';
|
||||
import { Header } from '@/components/layout/Header';
|
||||
import { MobileNav } from '@/components/layout/MobileNav';
|
||||
import { AttributionTracker } from '@/components/seo/AttributionTracker';
|
||||
import { homeMetadata } from '@/lib/metadata';
|
||||
import './globals.css';
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: '--font-geist-sans',
|
||||
subsets: ['latin'],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: '--font-geist-mono',
|
||||
subsets: ['latin'],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = homeMetadata();
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="zh-CN" className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}>
|
||||
<body className="min-h-full flex flex-col bg-[#f5f0e5] pb-16 text-[#17221d] md:pb-0">
|
||||
<Suspense fallback={null}>
|
||||
<AttributionTracker />
|
||||
</Suspense>
|
||||
<Header />
|
||||
<main className="mx-auto w-full max-w-6xl flex-1 px-4 py-6">{children}</main>
|
||||
<Footer />
|
||||
<MobileNav />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
import { plazaPath } from '@/lib/site';
|
||||
|
||||
export default function Home() {
|
||||
redirect(plazaPath());
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
import { CategoryNav } from '@/components/feed/CategoryNav';
|
||||
import { FeedLoadMore } from '@/components/feed/FeedLoadMore';
|
||||
import { FeedTabs } from '@/components/feed/FeedTabs';
|
||||
import { fetchCategories, fetchFeed } from '@/lib/api';
|
||||
import { categoryJsonLd, categoryMetadata } from '@/lib/metadata';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function generateStaticParams() {
|
||||
try {
|
||||
const { categories } = await fetchCategories();
|
||||
return categories.map((cat) => ({ slug: cat.slug }));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
|
||||
const { slug } = await params;
|
||||
try {
|
||||
const { categories } = await fetchCategories();
|
||||
const category = categories.find((item) => item.slug === slug);
|
||||
if (!category) return { title: '分类未找到' };
|
||||
return categoryMetadata(category.name, category.slug, category.description);
|
||||
} catch {
|
||||
return { title: 'Plaza 分类' };
|
||||
}
|
||||
}
|
||||
|
||||
export default async function PlazaCategoryPage({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
const query = await searchParams;
|
||||
const sort = query.sort === 'new' ? 'new' : 'hot';
|
||||
|
||||
const { categories } = await fetchCategories();
|
||||
const category = categories.find((item) => item.slug === slug);
|
||||
if (!category) notFound();
|
||||
|
||||
const feed = await fetchFeed({ sort, category: slug, limit: 20 });
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(categoryJsonLd(category.name, category.slug, category.description)),
|
||||
}}
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.2em] text-[#68716c]">分类</p>
|
||||
<h1 className="text-3xl font-bold text-[#17221d]">
|
||||
{category.icon} {category.name}
|
||||
</h1>
|
||||
{category.description ? (
|
||||
<p className="text-[#4a5751]">{category.description}</p>
|
||||
) : (
|
||||
<p className="text-[#4a5751]">{category.post_count ?? 0} 篇作品</p>
|
||||
)}
|
||||
</div>
|
||||
<CategoryNav categories={categories} activeSlug={slug} />
|
||||
<FeedTabs sort={sort} categorySlug={slug} />
|
||||
<FeedLoadMore
|
||||
initialPosts={feed.posts}
|
||||
initialCursor={feed.next_cursor}
|
||||
initialHasMore={feed.has_more}
|
||||
sort={sort}
|
||||
categorySlug={slug}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { PostGridSkeleton } from '@/components/feed/PostGridSkeleton';
|
||||
|
||||
export default function PlazaLoading() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 w-16 animate-pulse rounded bg-[#ebe4d6]" />
|
||||
<div className="h-9 w-2/3 animate-pulse rounded bg-[#ebe4d6]" />
|
||||
<div className="h-4 w-full max-w-xl animate-pulse rounded bg-[#f0ebe0]" />
|
||||
</div>
|
||||
<PostGridSkeleton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { fetchPost, PlazaApiError } from '@/lib/api';
|
||||
|
||||
export const runtime = 'edge';
|
||||
export const size = { width: 1200, height: 630 };
|
||||
export const contentType = 'image/png';
|
||||
|
||||
export default async function PostOpenGraphImage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
|
||||
let title = 'MindSpace Plaza';
|
||||
let author = '创作者';
|
||||
|
||||
try {
|
||||
const { post } = await fetchPost(id, { revalidate: false, cache: 'no-store' });
|
||||
title = post.title;
|
||||
author = post.author.display_name;
|
||||
} catch (error) {
|
||||
if (!(error instanceof PlazaApiError)) throw error;
|
||||
}
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
padding: '64px',
|
||||
background: 'linear-gradient(135deg, #2f6f57 0%, #1f4f3f 55%, #17221d 100%)',
|
||||
color: '#fffdf7',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 28, opacity: 0.85 }}>MindSpace Plaza</div>
|
||||
<div style={{ fontSize: 56, fontWeight: 700, lineHeight: 1.15, maxWidth: 980 }}>{title}</div>
|
||||
<div style={{ fontSize: 28, opacity: 0.9 }}>{author}</div>
|
||||
</div>
|
||||
),
|
||||
{ ...size },
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { cookies } from 'next/headers';
|
||||
import { CommentSection } from '@/components/comment/CommentSection';
|
||||
import { PostReportButton } from '@/components/post/PostReportButton';
|
||||
import { PostActions } from '@/components/post/PostActions';
|
||||
import { PostEmbed } from '@/components/post/PostEmbed';
|
||||
import { PostMeta } from '@/components/post/PostMeta';
|
||||
import { mindSpaceSignupUrl } from '@/lib/utm';
|
||||
import { fetchPost } from '@/lib/api';
|
||||
import { PlazaApiError } from '@/lib/api';
|
||||
import { postJsonLd, postMetadata } from '@/lib/metadata';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
try {
|
||||
const { post } = await fetchPost(id);
|
||||
return postMetadata(post);
|
||||
} catch {
|
||||
return { title: '帖子未找到' };
|
||||
}
|
||||
}
|
||||
|
||||
export default async function PlazaPostPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
const cookieStore = await cookies();
|
||||
const cookieHeader = cookieStore.toString();
|
||||
|
||||
let post;
|
||||
try {
|
||||
({ post } = await fetchPost(id, { cookie: cookieHeader }));
|
||||
} catch (error) {
|
||||
if (error instanceof PlazaApiError && error.code === 'POST_NOT_FOUND') notFound();
|
||||
throw error;
|
||||
}
|
||||
|
||||
const embedUrl = post.publication_url;
|
||||
const jsonLd = postJsonLd(post);
|
||||
|
||||
return (
|
||||
<article className="space-y-6">
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
/>
|
||||
<PostMeta post={post} />
|
||||
{embedUrl ? (
|
||||
<PostEmbed src={embedUrl} title={post.title} />
|
||||
) : (
|
||||
<div className="rounded-2xl border border-dashed border-[#d6d0c3] bg-[#fffdf7] px-6 py-12 text-center text-[#68716c]">
|
||||
页面内容暂不可用
|
||||
</div>
|
||||
)}
|
||||
<PostActions
|
||||
postId={post.id}
|
||||
stats={post.stats}
|
||||
viewerReacted={post.viewer_reacted}
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<PostReportButton postId={post.id} />
|
||||
</div>
|
||||
<CommentSection postId={post.id} allowComment={post.allow_comment !== false} />
|
||||
<div className="rounded-2xl border border-[#d6d0c3] bg-[#f0ebe0] px-5 py-4 text-center">
|
||||
<Link
|
||||
href={mindSpaceSignupUrl({
|
||||
medium: 'footer',
|
||||
campaign: 'post_watermark',
|
||||
ref: post.id,
|
||||
})}
|
||||
className="inline-flex items-center gap-2 text-sm font-semibold text-[#2f6f57]"
|
||||
>
|
||||
用 MindSpace 制作 →
|
||||
</Link>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { CategoryNav } from '@/components/feed/CategoryNav';
|
||||
import { FeaturedBanner } from '@/components/feed/FeaturedBanner';
|
||||
import { FeedLoadMore } from '@/components/feed/FeedLoadMore';
|
||||
import { FeedTabs } from '@/components/feed/FeedTabs';
|
||||
import { fetchCategories, fetchFeed } from '@/lib/api';
|
||||
import { REVALIDATE_HOME } from '@/lib/cache';
|
||||
import { homeJsonLd, homeMetadata } from '@/lib/metadata';
|
||||
|
||||
export const metadata = homeMetadata();
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function PlazaHomePage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
||||
}) {
|
||||
const params = await searchParams;
|
||||
const sort = params.sort === 'new' ? 'new' : 'hot';
|
||||
|
||||
let categories: Awaited<ReturnType<typeof fetchCategories>>['categories'] = [];
|
||||
let feed: Awaited<ReturnType<typeof fetchFeed>> = {
|
||||
posts: [],
|
||||
featured: { homepage_banner: [], trending: [] },
|
||||
next_cursor: null,
|
||||
has_more: false,
|
||||
};
|
||||
|
||||
try {
|
||||
const [categoryData, feedData] = await Promise.all([
|
||||
fetchCategories({ revalidate: REVALIDATE_HOME }),
|
||||
fetchFeed({ sort, limit: 20 }, { revalidate: REVALIDATE_HOME }),
|
||||
]);
|
||||
categories = categoryData.categories;
|
||||
feed = feedData;
|
||||
} catch {
|
||||
categories = [];
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(homeJsonLd()) }}
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.2em] text-[#68716c]">Plaza</p>
|
||||
<h1 className="text-3xl font-bold text-[#17221d]">发现 AI 创作的精彩内容</h1>
|
||||
<p className="max-w-2xl text-[#4a5751]">
|
||||
浏览 MindSpace 用户发布的报告、作品与页面,按热门或最新探索。
|
||||
</p>
|
||||
</div>
|
||||
<CategoryNav categories={categories} />
|
||||
<FeedTabs sort={sort} />
|
||||
<FeaturedBanner posts={feed.featured.homepage_banner} />
|
||||
<FeedLoadMore
|
||||
initialPosts={feed.posts}
|
||||
initialCursor={feed.next_cursor}
|
||||
initialHasMore={feed.has_more}
|
||||
sort={sort}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { MetadataRoute } from 'next';
|
||||
import { siteOrigin } from '@/lib/site';
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: [
|
||||
{
|
||||
userAgent: '*',
|
||||
allow: ['/plaza', '/u/'],
|
||||
disallow: ['/ops', '/api', '/_next'],
|
||||
},
|
||||
],
|
||||
sitemap: `${siteOrigin()}/sitemap.xml`,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { MetadataRoute } from 'next';
|
||||
import { fetchSitemapData } from '@/lib/api';
|
||||
import { plazaPath, siteOrigin } from '@/lib/site';
|
||||
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const origin = siteOrigin();
|
||||
const now = new Date();
|
||||
|
||||
const staticPages: MetadataRoute.Sitemap = [
|
||||
{
|
||||
url: `${origin}${plazaPath()}`,
|
||||
lastModified: now,
|
||||
changeFrequency: 'hourly',
|
||||
priority: 1,
|
||||
},
|
||||
];
|
||||
|
||||
try {
|
||||
const data = await fetchSitemapData({ revalidate: 3600 });
|
||||
const categoryPages = data.categories.map((category) => ({
|
||||
url: `${origin}${plazaPath(`cat/${category.slug}`)}`,
|
||||
lastModified: now,
|
||||
changeFrequency: 'hourly' as const,
|
||||
priority: 0.8,
|
||||
}));
|
||||
const postPages = data.posts.map((post) => ({
|
||||
url: `${origin}${plazaPath(`p/${post.id}`)}`,
|
||||
lastModified: new Date(post.updated_at),
|
||||
changeFrequency: 'weekly' as const,
|
||||
priority: 0.6,
|
||||
}));
|
||||
const userPages = data.users.map((user) => ({
|
||||
url: `${origin}/u/${user.slug}`,
|
||||
lastModified: new Date(user.last_post_at),
|
||||
changeFrequency: 'weekly' as const,
|
||||
priority: 0.5,
|
||||
}));
|
||||
return [...staticPages, ...categoryPages, ...postPages, ...userPages];
|
||||
} catch {
|
||||
return staticPages;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
import { cookies } from 'next/headers';
|
||||
import { UserCard } from '@/components/user/UserCard';
|
||||
import { UserPostGrid } from '@/components/user/UserPostGrid';
|
||||
import { fetchUserProfile, fetchUserPosts, PlazaApiError } from '@/lib/api';
|
||||
import { userJsonLd, userMetadata } from '@/lib/metadata';
|
||||
|
||||
export const revalidate = 300;
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
|
||||
const { slug } = await params;
|
||||
try {
|
||||
const { user } = await fetchUserProfile(slug, { revalidate: 300 });
|
||||
return userMetadata(user);
|
||||
} catch {
|
||||
return { title: '用户未找到' };
|
||||
}
|
||||
}
|
||||
|
||||
export default async function UserProfilePage({ params }: { params: Promise<{ slug: string }> }) {
|
||||
const { slug } = await params;
|
||||
const cookieStore = await cookies();
|
||||
const cookieHeader = cookieStore.toString();
|
||||
|
||||
let profile;
|
||||
let postsData;
|
||||
try {
|
||||
[profile, postsData] = await Promise.all([
|
||||
fetchUserProfile(slug, { revalidate: 300, cookie: cookieHeader }),
|
||||
fetchUserPosts(slug, { limit: 20 }, { revalidate: 300 }),
|
||||
]);
|
||||
} catch (error) {
|
||||
if (error instanceof PlazaApiError && error.code === 'user_not_found') notFound();
|
||||
throw error;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(userJsonLd(profile.user)) }}
|
||||
/>
|
||||
<UserCard user={profile.user} />
|
||||
<UserPostGrid
|
||||
slug={slug}
|
||||
initialPosts={postsData.posts}
|
||||
initialCursor={postsData.next_cursor}
|
||||
initialHasMore={postsData.has_more}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user