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

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:
john
2026-06-14 21:30:20 +08:00
parent e5fd568e01
commit 4e21ca937a
359 changed files with 70658 additions and 56 deletions
+300
View File
@@ -0,0 +1,300 @@
import type {
PlazaCategoriesResponse,
PlazaCommentsResponse,
PlazaCreatePostResponse,
PlazaFeedResponse,
PlazaPostResponse,
PlazaSitemapData,
PlazaUserProfileResponse,
} from '@/types/plaza';
import { apiBase } from '@/lib/site';
type FetchOptions = {
revalidate?: number | false;
cache?: RequestCache;
cookie?: string;
};
export type PlazaAuthStatus = {
authenticated: boolean;
user?: { id: string; username: string; slug?: string; displayName: string } | null;
};
class PlazaApiError extends Error {
code: string;
constructor(message: string, code: string) {
super(message);
this.code = code;
}
}
async function plazaFetch<T>(path: string, init?: RequestInit, options?: FetchOptions): Promise<T> {
const base = apiBase();
const url = `${base}${path.startsWith('/') ? path : `/${path}`}`;
const headers = new Headers(init?.headers);
if (options?.cookie) headers.set('cookie', options.cookie);
if (!headers.has('Accept')) headers.set('Accept', 'application/json');
const next =
options?.revalidate === false
? { revalidate: 0 }
: options?.revalidate != null
? { revalidate: options.revalidate }
: undefined;
const response = await fetch(url, {
...init,
headers,
credentials: typeof window !== 'undefined' ? 'include' : init?.credentials,
cache: options?.cache ?? (options?.revalidate === false ? 'no-store' : undefined),
next,
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
const code = payload?.error?.code ?? 'request_failed';
const message = payload?.error?.message ?? `请求失败 (${response.status})`;
throw new PlazaApiError(message, code);
}
return payload.data as T;
}
export async function fetchAuthStatus(cookie?: string): Promise<PlazaAuthStatus> {
try {
const base = apiBase();
const url = `${base}/auth/status`;
const headers = new Headers({ Accept: 'application/json' });
if (cookie) headers.set('cookie', cookie);
const response = await fetch(url, {
headers,
credentials: typeof window !== 'undefined' ? 'include' : undefined,
cache: 'no-store',
});
if (!response.ok) return { authenticated: false };
const status = (await response.json()) as PlazaAuthStatus;
return status;
} catch {
return { authenticated: false };
}
}
export async function fetchCategories(options?: FetchOptions) {
return plazaFetch<PlazaCategoriesResponse>('/api/plaza/v1/categories', undefined, {
revalidate: 300,
...options,
});
}
export async function fetchFeed(
params: {
sort?: 'hot' | 'new';
category?: string;
cursor?: string;
limit?: number;
} = {},
options?: FetchOptions,
) {
const query = new URLSearchParams();
if (params.sort) query.set('sort', params.sort);
if (params.category) query.set('category', params.category);
if (params.cursor) query.set('cursor', params.cursor);
if (params.limit) query.set('limit', String(params.limit));
const suffix = query.size ? `?${query.toString()}` : '';
return plazaFetch<PlazaFeedResponse>(`/api/plaza/v1/feed${suffix}`, undefined, {
revalidate: 60,
...options,
});
}
export async function fetchPost(id: string, options?: FetchOptions) {
return plazaFetch<PlazaPostResponse>(`/api/plaza/v1/posts/${encodeURIComponent(id)}`, undefined, {
revalidate: false,
cache: 'no-store',
...options,
});
}
export async function createPlazaPost(
body: {
publication_id: string;
category_id: string;
tags?: string[];
cover_url?: string;
allow_comment?: boolean;
},
cookie: string,
) {
return plazaFetch<PlazaCreatePostResponse>(
'/api/plaza/v1/posts',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
},
{ revalidate: false, cookie },
);
}
export async function togglePostReaction(
postId: string,
type: 'like' | 'collect' | 'share',
active: boolean,
) {
if (type === 'share') {
return plazaFetch<{ post_id: string; type: string; active: boolean }>(
`/api/plaza/v1/posts/${encodeURIComponent(postId)}/reactions`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'share' }),
},
{ revalidate: false },
);
}
if (active) {
return plazaFetch<{ post_id: string; type: string; active: boolean }>(
`/api/plaza/v1/posts/${encodeURIComponent(postId)}/reactions/${type}`,
{ method: 'DELETE' },
{ revalidate: false },
);
}
return plazaFetch<{ post_id: string; type: string; active: boolean }>(
`/api/plaza/v1/posts/${encodeURIComponent(postId)}/reactions`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type }),
},
{ revalidate: false },
);
}
export async function fetchComments(
postId: string,
params: { parent_id?: string; cursor?: string } = {},
) {
const query = new URLSearchParams();
if (params.parent_id) query.set('parent_id', params.parent_id);
if (params.cursor) query.set('cursor', params.cursor);
const suffix = query.size ? `?${query.toString()}` : '';
return plazaFetch<PlazaCommentsResponse>(
`/api/plaza/v1/posts/${encodeURIComponent(postId)}/comments${suffix}`,
undefined,
{ revalidate: false, cache: 'no-store' },
);
}
export async function createComment(postId: string, content: string, parentId?: string | null) {
return plazaFetch<{ comment: { id: string } }>(
`/api/plaza/v1/posts/${encodeURIComponent(postId)}/comments`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content, parent_id: parentId ?? null }),
},
{ revalidate: false },
);
}
export async function toggleCommentLike(commentId: string, liked: boolean) {
return plazaFetch<{ id: string; liked: boolean }>(
`/api/plaza/v1/comments/${encodeURIComponent(commentId)}/reactions`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ liked }),
},
{ revalidate: false },
);
}
export async function fetchUserProfile(slug: string, options?: FetchOptions) {
return plazaFetch<PlazaUserProfileResponse>(
`/api/plaza/v1/users/${encodeURIComponent(slug)}`,
undefined,
{ revalidate: 300, ...options },
);
}
export async function fetchUserPosts(
slug: string,
params: { cursor?: string; limit?: number } = {},
options?: FetchOptions,
) {
const query = new URLSearchParams();
if (params.cursor) query.set('cursor', params.cursor);
if (params.limit) query.set('limit', String(params.limit));
const suffix = query.size ? `?${query.toString()}` : '';
return plazaFetch<Pick<PlazaFeedResponse, 'posts' | 'next_cursor' | 'has_more'>>(
`/api/plaza/v1/users/${encodeURIComponent(slug)}/posts${suffix}`,
undefined,
{ revalidate: false, cache: 'no-store', ...options },
);
}
export async function toggleFollow(slug: string, following: boolean) {
if (following) {
return plazaFetch<{ following: boolean }>(
`/api/plaza/v1/users/${encodeURIComponent(slug)}/follow`,
{ method: 'DELETE' },
{ revalidate: false },
);
}
return plazaFetch<{ following: boolean }>(
`/api/plaza/v1/users/${encodeURIComponent(slug)}/follow`,
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' },
{ revalidate: false },
);
}
export async function fetchSitemapData(options?: FetchOptions) {
return plazaFetch<PlazaSitemapData>('/api/plaza/v1/seo/sitemap', undefined, {
revalidate: 3600,
...options,
});
}
export async function recordAttributionEvent(body: {
event_type: 'landing' | 'signup';
utm_source: string;
utm_medium?: string;
utm_campaign?: string;
ref_id?: string;
}) {
return plazaFetch<{ id: string; event_type: string }>(
'/api/plaza/v1/attribution/events',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
},
{ revalidate: false },
);
}
export async function submitPostReport(postId: string, reason: string, detail?: string) {
return plazaFetch<{ report: { id: string; status: string } }>(
`/api/plaza/v1/posts/${encodeURIComponent(postId)}/reports`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reason, detail: detail ?? '' }),
},
{ revalidate: false },
);
}
export async function submitCommentReport(commentId: string, reason: string, detail?: string) {
return plazaFetch<{ report: { id: string; status: string } }>(
`/api/plaza/v1/comments/${encodeURIComponent(commentId)}/reports`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reason, detail: detail ?? '' }),
},
{ revalidate: false },
);
}
export { PlazaApiError };
+3
View File
@@ -0,0 +1,3 @@
export const REVALIDATE_HOME = 60;
export const REVALIDATE_CATEGORY = 300;
export const REVALIDATE_USER = 300;
+35
View File
@@ -0,0 +1,35 @@
export function formatCount(value: number): string {
const n = Number(value ?? 0);
if (n >= 100_000) return `${Math.round(n / 10_000)}`;
if (n >= 10_000) return `${(n / 10_000).toFixed(1).replace(/\.0$/, '')}`;
if (n >= 1_000) return `${(n / 1_000).toFixed(1).replace(/\.0$/, '')}k`;
return String(n);
}
export function formatRelativeTime(iso: string): string {
const date = new Date(iso);
if (Number.isNaN(date.getTime())) return '';
const diffMs = Date.now() - date.getTime();
const minutes = Math.floor(diffMs / 60_000);
if (minutes < 1) return '刚刚';
if (minutes < 60) return `${minutes} 分钟前`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours} 小时前`;
const days = Math.floor(hours / 24);
if (days < 30) return `${days} 天前`;
return date.toLocaleDateString('zh-CN');
}
export function categoryAccent(slug: string): string {
const palette = [
'from-emerald-700 to-teal-500',
'from-amber-700 to-orange-500',
'from-violet-700 to-purple-500',
'from-sky-700 to-cyan-500',
'from-rose-700 to-pink-500',
'from-lime-700 to-green-500',
];
let hash = 0;
for (let i = 0; i < slug.length; i += 1) hash = (hash + slug.charCodeAt(i) * (i + 1)) % palette.length;
return palette[hash] ?? palette[0];
}
+180
View File
@@ -0,0 +1,180 @@
import type { Metadata } from 'next';
import type { PlazaPost, PlazaUserProfile } from '@/types/plaza';
import { plazaPath, siteOrigin } from '@/lib/site';
const DEFAULT_OG = `${siteOrigin()}/plaza-og.png`;
function postOgImage(post: PlazaPost): string {
if (post.cover_url) return post.cover_url;
return `${siteOrigin()}${plazaPath(`p/${post.id}/opengraph-image`)}`;
}
export function homeMetadata(): Metadata {
const url = `${siteOrigin()}${plazaPath()}`;
return {
title: 'Plaza - 发现 AI 创作的精彩内容',
description: '浏览来自全球用户用 MindSpace 创作的报告、作品和页面',
openGraph: {
type: 'website',
title: 'Plaza - 发现 AI 创作的精彩内容',
description: '浏览来自全球用户用 MindSpace 创作的报告、作品和页面',
url,
siteName: 'MindSpace Plaza',
images: [{ url: DEFAULT_OG, width: 1200, height: 630 }],
},
alternates: { canonical: url },
robots: { index: true, follow: true },
};
}
export function categoryMetadata(name: string, slug: string, description?: string): Metadata {
const url = `${siteOrigin()}${plazaPath(`cat/${slug}`)}`;
return {
title: `${name} - Plaza | MindSpace`,
description: description || `浏览 Plaza「${name}」分类下的 AI 创作内容`,
openGraph: {
type: 'website',
title: `${name} - Plaza`,
description: description || `Plaza「${name}」分类`,
url,
siteName: 'MindSpace Plaza',
images: [{ url: DEFAULT_OG, width: 1200, height: 630 }],
},
alternates: { canonical: url },
robots: { index: true, follow: true },
};
}
export function postMetadata(post: PlazaPost): Metadata {
const url = `${siteOrigin()}${plazaPath(`p/${post.id}`)}`;
const image = postOgImage(post);
const description =
post.summary || `${post.author.display_name} 用 MindSpace 创作的作品`;
return {
title: `${post.title} - Plaza | MindSpace`,
description,
keywords: post.tags.join(', '),
openGraph: {
title: post.title,
description,
url,
siteName: 'MindSpace Plaza',
images: [{ url: image, width: 1200, height: 630, alt: post.title }],
type: 'article',
publishedTime: post.published_at,
authors: [`${siteOrigin()}/u/${post.author.slug}`],
tags: post.tags,
},
twitter: {
card: 'summary_large_image',
title: post.title,
description,
images: [image],
creator: `@${post.author.slug}`,
},
alternates: { canonical: url },
robots: { index: true, follow: true },
};
}
export function postJsonLd(post: PlazaPost) {
return {
'@context': 'https://schema.org',
'@type': 'Article',
headline: post.title,
description: post.summary,
image: post.cover_url || postOgImage(post),
datePublished: post.published_at,
author: {
'@type': 'Person',
name: post.author.display_name,
url: `${siteOrigin()}/u/${post.author.slug}`,
},
publisher: {
'@type': 'Organization',
name: 'MindSpace',
logo: {
'@type': 'ImageObject',
url: `${siteOrigin()}/logo.png`,
},
},
};
}
export function userMetadata(user: PlazaUserProfile): Metadata {
const url = `${siteOrigin()}/u/${user.slug}`;
const description =
user.bio || `${user.display_name} 在 MindSpace Plaza 发布了 ${user.stats.post_count} 篇作品`;
const image = user.avatar_url || DEFAULT_OG;
return {
title: `${user.display_name} (@${user.slug}) - Plaza | MindSpace`,
description,
openGraph: {
type: 'profile',
title: user.display_name,
description,
url,
siteName: 'MindSpace Plaza',
images: user.avatar_url
? [{ url: image, width: 400, height: 400, alt: user.display_name }]
: [{ url: DEFAULT_OG, width: 1200, height: 630 }],
},
twitter: {
card: user.avatar_url ? 'summary' : 'summary_large_image',
title: user.display_name,
description,
images: [image],
},
alternates: { canonical: url },
robots: { index: true, follow: true },
};
}
export function userJsonLd(user: PlazaUserProfile) {
return {
'@context': 'https://schema.org',
'@type': 'ProfilePage',
name: user.display_name,
description: user.bio || `${user.display_name} 的 MindSpace Plaza 主页`,
url: `${siteOrigin()}/u/${user.slug}`,
mainEntity: {
'@type': 'Person',
name: user.display_name,
alternateName: user.slug,
url: `${siteOrigin()}/u/${user.slug}`,
},
};
}
export function homeJsonLd() {
const url = `${siteOrigin()}${plazaPath()}`;
return {
'@context': 'https://schema.org',
'@type': 'WebSite',
name: 'MindSpace Plaza',
url,
description: '浏览来自全球用户用 MindSpace 创作的报告、作品和页面',
publisher: {
'@type': 'Organization',
name: 'MindSpace',
},
};
}
export function categoryJsonLd(name: string, slug: string, description?: string) {
const url = `${siteOrigin()}${plazaPath(`cat/${slug}`)}`;
return {
'@context': 'https://schema.org',
'@type': 'CollectionPage',
name: `${name} - Plaza`,
description: description || `Plaza「${name}」分类下的 AI 创作内容`,
url,
isPartOf: {
'@type': 'WebSite',
name: 'MindSpace Plaza',
url: `${siteOrigin()}${plazaPath()}`,
},
};
}
+24
View File
@@ -0,0 +1,24 @@
export function apiBase(): string {
if (typeof window === 'undefined') {
return (
process.env.PLAZA_API_BASE ??
process.env.NEXT_PUBLIC_API_BASE ??
'http://127.0.0.1:8080'
);
}
return process.env.NEXT_PUBLIC_API_BASE ?? '';
}
export function siteOrigin(): string {
return process.env.NEXT_PUBLIC_SITE_BASE ?? 'https://go.tkmind.cn';
}
export function mindSpaceOrigin(): string {
return process.env.NEXT_PUBLIC_MINDSPACE_BASE ?? siteOrigin();
}
export function plazaPath(path = ''): string {
const base = process.env.NEXT_PUBLIC_PLAZA_BASE ?? '/plaza';
if (!path) return base;
return `${base.replace(/\/$/, '')}/${path.replace(/^\//, '')}`;
}
+25
View File
@@ -0,0 +1,25 @@
export function plazaUtmQuery(params: {
source?: string;
medium: string;
campaign: string;
ref?: string;
}) {
const query = new URLSearchParams();
query.set('utm_source', params.source ?? 'plaza');
query.set('utm_medium', params.medium);
query.set('utm_campaign', params.campaign);
if (params.ref) query.set('ref', params.ref);
return query.toString();
}
export function mindSpaceSignupUrl(params: {
medium: string;
campaign: string;
ref?: string;
returnTo?: string;
}) {
const base = process.env.NEXT_PUBLIC_MINDSPACE_BASE ?? process.env.NEXT_PUBLIC_SITE_BASE ?? 'https://go.tkmind.cn';
const query = plazaUtmQuery(params);
const returnParam = params.returnTo ? `&return_to=${encodeURIComponent(params.returnTo)}` : '';
return `${base.replace(/\/$/, '')}?${query}${returnParam}`;
}