secondme-nextjs
npx skills add https://github.com/mindverse/second-me-skills --skill secondme-nextjs
Agent 安装分布
Skill 文档
SecondMe Next.js 项ç®çæ
åºäº /secondme-init çé
ç½®å /secondme-prd çéæ±å®ä¹ï¼çæå®æ´ç Next.js 项ç®ã
åç½®æ¡ä»¶æ£æ¥
1. æ£æ¥ state.json
é¦å
æ£æ¥ .secondme/state.json æ¯å¦åå¨ï¼
- ä¸åå¨ â æç¤ºï¼
请å è¿è¡ /secondme-init åå§å项ç®é ç½® - åå¨ â ç»§ç»
2. æ£æ¥æ§è¡æ¨¡å¼
æ£æ¥åæ°æ¯å¦å
å« --quickï¼
å¿«éæ¨¡å¼ (–quick)ï¼
- è·³è¿ stage æ£æ¥
- 使ç¨é»è®¤ PRD é ç½®
- ç´æ¥å¼å§çæé¡¹ç®
æ 忍¡å¼ï¼
- æ£æ¥
stage >= "prd" - 妿
stage == "init"â æç¤ºï¼è¯·å è¿è¡ /secondme-prd å®ä¹éæ±ï¼æä½¿ç¨ /secondme-nextjs --quick å¿«éçæ - 妿
stage >= "prd"â ç»§ç»
读åé ç½®
ä» .secondme/state.json 读åï¼
const state = {
app_name: "secondme-tinder", // åºç¨åç§°
modules: ["auth", "chat", "profile"], // 已鿍¡å
config: {
client_id: "71658da7-659c-414a-abdf-cb6472037fc2",
client_secret: "xxx",
redirect_uri: "http://localhost:3000/api/auth/callback",
redirect_uris: [...],
database_url: "postgresql://...",
allowed_scopes: [...]
},
api: {
base_url: "https://app.mindos.com/gate/lab",
oauth_url: "https://go.second.me/oauth/",
token_endpoint: "https://app.mindos.com/gate/lab/api/oauth/token/code",
refresh_endpoint: "https://app.mindos.com/gate/lab/api/oauth/token/refresh",
access_token_ttl: 7200,
refresh_token_ttl: 2592000
},
docs: {
quickstart: "https://develop-docs.second.me/zh/docs",
oauth2: "...",
api_reference: "...",
errors: "..."
},
prd: {
summary: "åºç¨æ¦è¦",
features: ["åè½1", "åè½2"],
design_preference: "ç®çº¦ç°ä»£"
}
}
éè¦ï¼ ææ API 端ç¹ãææ¡£é¾æ¥åä» state.api å state.docs 读åï¼ä¸è¦ç¡¬ç¼ç ã
åç«¯è®¾è®¡è¦æ±
éè¦ï¼ å¨æå»ºå端ç颿¶ï¼å¿
é¡»ä½¿ç¨ frontend-design:frontend-design skill æ¥çæé«è´¨éç UI ç»ä»¶ã
设计ååï¼
- 亮è²ä¸»é¢ï¼ä» 使ç¨äº®è²/æµ è²ä¸»é¢ï¼ä¸ä½¿ç¨æè²/æ·±è²ä¸»é¢
- ç®çº¦ä¼é ï¼éµå¾ªæç®è®¾è®¡ç念ï¼åå°è§è§åªé³
- 产åç¹æ§é©±å¨ï¼UI 设计åºç´§å¯ç»åè¦å®ç°çåè½ç¹æ§
- ç°ä»£æï¼éç¨å½ä¸æµè¡ç设计è¶å¿ï¼é¿å è¿æ¶ç UI 模å¼
- ä¸è´æ§ï¼ä¿ææ´ä½è§è§é£æ ¼ç»ä¸
- ååºå¼ï¼éé åç§å±å¹å°ºå¯¸
- 䏿çé¢ï¼ææç¨æ·å¯è§çæåï¼æé®ãæç¤ºãæ ç¾ã说æçï¼å¿ 须使ç¨ä¸æ
- 稳å®ä¼å ï¼é¿å 夿å¨ç»ææï¼ä» 使ç¨ç®åçè¿æ¸¡å¨ç»ï¼å¦ hoverãfadeï¼ï¼ç¡®ä¿çé¢ç¨³å®æµç
项ç®çææµç¨
1. åå§å Next.js 项ç®
å¨å½åç®å½ç´æ¥åå§å Next.js 项ç®ï¼
npx create-next-app@latest . --typescript --tailwind --app --src-dir --import-alias "@/*" --yes
2. å®è£ ä¾èµ
npm install prisma @prisma/client
npx prisma init
3. çæ .env.local
ä» state.config å state.api çæç¯å¢åéï¼
# SecondMe OAuth2 é
ç½®
SECONDME_CLIENT_ID=[config.client_id]
SECONDME_CLIENT_SECRET=[config.client_secret]
SECONDME_REDIRECT_URI=[config.redirect_uri]
# æ°æ®åº
DATABASE_URL=[config.database_url]
# SecondMe APIï¼ä» state.api 读åï¼
SECONDME_API_BASE_URL=[api.base_url]
SECONDME_OAUTH_URL=[api.oauth_url]
SECONDME_TOKEN_ENDPOINT=[api.token_endpoint]
SECONDME_REFRESH_ENDPOINT=[api.refresh_endpoint]
4. çæ Prisma Schema
æ ¹æ®å·²é模åå¨æçæ prisma/schema.prismaã
auth 模åï¼å¿ æï¼- User è¡¨å¿ é¡»å å«çåæ®µ
User è¡¨å¿ é¡»å å« Token ç¸å ³å段ç¨äºåå¨åå·æ°ç¨æ·åè¯ï¼
model User {
id String @id @default(cuid())
secondmeUserId String @unique @map("secondme_user_id")
accessToken String @map("access_token")
refreshToken String @map("refresh_token")
tokenExpiresAt DateTime @map("token_expires_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// å
¶ä»åæ®µæ ¹æ®æ¨¡åéæ±èªè¡æ·»å
@@map("users")
}
å ¶ä»æ¨¡å
æ ¹æ®å·²é模åï¼profileãchatãnoteï¼çå®é éæ±ï¼èªè¡è®¾è®¡ç¸åºçæ°æ®åºè¡¨ç»æåå ³èå ³ç³»ã
5. çæä»£ç
æ ¹æ®å·²é模åçæå¯¹åºä»£ç ï¼
auth 模å
| æä»¶ | 说æ |
|---|---|
src/app/api/auth/login/route.ts |
OAuth ç»å½è·³è½¬ |
src/app/api/auth/callback/route.ts |
OAuth åè°å¤ç |
src/app/api/auth/logout/route.ts |
ç»åºå¤ç |
src/lib/auth.ts |
认è¯å·¥å ·å½æ° |
src/components/LoginButton.tsx |
ç»å½æé®ç»ä»¶ |
profile 模å
| æä»¶ | 说æ |
|---|---|
src/app/api/user/info/route.ts |
è·åç¨æ·ä¿¡æ¯ |
src/app/api/user/shades/route.ts |
è·åå ´è¶£æ ç¾ |
src/components/UserProfile.tsx |
ç¨æ·èµæç»ä»¶ |
chat 模å
| æä»¶ | 说æ |
|---|---|
src/app/api/chat/route.ts |
æµå¼è天 API |
src/app/api/sessions/route.ts |
ä¼è¯å表 API |
src/components/ChatWindow.tsx |
è天çé¢ç»ä»¶ |
act 模å
| æä»¶ | 说æ |
|---|---|
src/app/api/act/route.ts |
æµå¼å¨ä½å¤æ APIï¼ç»æå JSON è¾åºï¼ |
src/lib/act.ts |
Act API å·¥å ·å½æ°ï¼åé actionControlãè§£æ SSE JSON ç»æï¼ |
note 模å
| æä»¶ | 说æ |
|---|---|
src/app/api/note/route.ts |
æ·»å ç¬è®° API |
6. æ´æ° state.json
{
"stage": "ready",
...
}
ææ¯æ
- æ¡æ¶ï¼ Next.js 14+ (App Router)
- è¯è¨ï¼ TypeScript
- æ ·å¼ï¼ Tailwind CSS
- æ°æ®åº ORMï¼ Prisma
- åç«¯è®¾è®¡ï¼ ä½¿ç¨
frontend-design:frontend-designskill çæ - API è°ç¨ï¼ fetch
- ç¶æç®¡çï¼ React hooks
- è¿è¡ç«¯å£ï¼ å¿ é¡»ä½¿ç¨ 3000 端å£
常è§é®é¢ä¸æ³¨æäºé¡¹
CSS @import 顺åºé®é¢
é®é¢ï¼ å¨ globals.css ä¸ä½¿ç¨ @import url(...) å¼å
¥ Google Fonts ä¼å¯¼è´æå»ºå¤±è´¥ã
é误信æ¯ï¼
@import rules must precede all rules aside from @charset and @layer statements
åå ï¼ Tailwind CSS ç @import "tailwindcss" å±å¼åçæå¤§éè§åï¼å¯¼è´åç»ç @import url(...) ä¸åæ¯ç¬¬ä¸ä¸ªã
æ£ç¡®åæ³ï¼
-
使ç¨
<link>æ ç¾ï¼æ¨èï¼ï¼ å¨layout.tsxä¸å¼å ¥<head> <link rel="preconnect" href="https://fonts.googleapis.com" /> <link href="https://fonts.googleapis.com/css2?family=..." rel="stylesheet" /> </head> -
使ç¨
next/fontï¼æä½³å®è·µï¼ï¼import { Noto_Sans_SC } from 'next/font/google' const notoSans = Noto_Sans_SC({ subsets: ['latin'], weight: ['400', '500'] })
æå»ºéªè¯
éè¦ï¼ æ¯æ¬¡å®æä»£ç ä¿®æ¹åï¼å¿ é¡»è¿è¡ä»¥ä¸å½ä»¤éªè¯æå»ºï¼
npm run build
ç¹å«å ³æ³¨ï¼
- CSS è§£æé误
- TypeScript ç±»åé误
- å¯¼å ¥è·¯å¾é误
WebView OAuth 认è¯é®é¢
é®é¢ï¼ å¨ WebView ç¯å¢ä¸ï¼å¦ç§»å¨ç«¯ App å åµé¡µé¢ã微信å°ç¨åºçï¼ï¼OAuth state éªè¯å¯è½å¤±è´¥ï¼å 为 WebView ä¸ç³»ç»æµè§å¨ä¹é´çåå¨ä¸å ±äº«ã
è§£å³æ¹æ¡ï¼ 使ç¨å®½æ¾ç state éªè¯ï¼éªè¯å¤±è´¥æ¶è®°å½è¦åä½ç»§ç»å¤çç»å½æµç¨ã
// ä¹åï¼éªè¯å¤±è´¥ç´æ¥æç»
if (!isValidState) {
return NextResponse.redirect('/?error=invalid_state');
}
// ä¹åï¼éªè¯å¤±è´¥è®°å½è¦åï¼ç»§ç»å¤ç
if (!isValidState) {
console.warn('OAuth state éªè¯å¤±è´¥ï¼å¯è½æ¯è·¨ WebView åºæ¯');
// ç»§ç»å¤çï¼ä¸é»æ¢ç»å½
}
注æï¼ è¿ç§æ¹å¼éä½äº CSRF 鲿¤ï¼ä» 建议å¨å¯ä¿¡ç WebView ç¯å¢ä¸ä½¿ç¨
è¾åºç»æ
â
Next.js 项ç®å·²çæï¼
å·²çææ¨¡å: auth, chat, profile
æ°æ®åº: PostgreSQL
å¯å¨æ¥éª¤:
1. npm install
2. npx prisma db push
3. npm run dev
项ç®å°å¨ http://localhost:3000 å¯å¨
API ååºæ ¼å¼
éè¦ï¼ææ SecondMe API ååºé½éµå¾ªç»ä¸æ ¼å¼ï¼
{
"code": 0,
"data": { ... }
}
æ¬å°è·¯ç±ä¸ä¸æ¸¸ API çå ³ç³»ï¼ Next.js æ¬å°è·¯ç±ï¼å¦
/api/secondme/user/shadesï¼ä½ä¸ºä»£çå±ï¼ å°è¯·æ±è½¬åå°ä¸æ¸¸ SecondMe APIï¼{state.api.base_url}/api/secondme/...ï¼ï¼å¹¶éä¼ ä¸æ¸¸çååºæ ¼å¼ã å端代ç è°ç¨æ¬å°è·¯ç±å³å¯ï¼æ éç´æ¥è®¿é®ä¸æ¸¸ APIã
å端代ç å¿ é¡»æ£ç¡®æåæ°æ®ï¼
// â
æ£ç¡®åæ³ï¼è°ç¨ Next.js æ¬å°è·¯ç±ï¼ååºæ ¼å¼ä¸ä¸æ¸¸ä¸è´ï¼
const response = await fetch('/api/secondme/user/shades');
const result = await response.json();
if (result.code === 0) {
const shades = result.data.shades;
shades.map(item => ...)
}
宿¹ææ¡£
ä» state.docs è¯»åææ¡£é¾æ¥ï¼
| ææ¡£ | é ç½®é® |
|---|---|
| å¿«éå ¥é¨ | docs.quickstart |
| OAuth2 æå | docs.oauth2 |
| API åè | docs.api_reference |
| é误ç | docs.errors |