feat: sync Sidebar and BottomNav, standardize user profile API
Align Sidebar & BottomNav menus, remove "Search", add user profile mock data, implement /api/users, add FilterDrawer, complete Section, ProfileHeader, MetricsRFM components Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
56
app/api/rfm/analyze/route.ts
Normal file
56
app/api/rfm/analyze/route.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { analyzeUser, type AnalyzeInput } from "@/services/rfm-engine"
|
||||
import { generateText } from "ai"
|
||||
import { openai } from "@ai-sdk/openai"
|
||||
|
||||
/**
|
||||
* 可选 AI 标签增强:
|
||||
* - 使用 AI SDK (generateText + openai("gpt-4o")),符合统一标准 [^1]
|
||||
* - 无 OPENAI_API_KEY 时自动跳过,保持稳定
|
||||
*/
|
||||
async function aiTagging(chat_logs?: string[]) {
|
||||
const text = (chat_logs ?? []).slice(0, 8).join("。")
|
||||
if (!text) return null
|
||||
if (!process.env.OPENAI_API_KEY) return null
|
||||
try {
|
||||
const { text: out } = await generateText({
|
||||
model: openai("gpt-4o"),
|
||||
system:
|
||||
"请根据用户聊天片段输出JSON,字段:emotion(积极|中性|消极), intent(弱意图|中等意图|强意图), behavior(数组,中文标签最多3个)。只输出JSON。",
|
||||
prompt: `聊天片段:${text}`,
|
||||
// @ts-expect-error ai sdk typings are flexible in Next.js
|
||||
maxTokens: 200,
|
||||
})
|
||||
const json = JSON.parse(out.trim())
|
||||
return json
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const body = await req.json()
|
||||
const inputs: AnalyzeInput[] = Array.isArray(body) ? body : [body]
|
||||
const useAI = (Array.isArray(body) ? (body as any).useAI : (body as any)?.useAI) ?? false
|
||||
|
||||
const results = []
|
||||
for (const input of inputs) {
|
||||
const base = analyzeUser(input)
|
||||
if (useAI) {
|
||||
const ai = await aiTagging(input.chat_logs)
|
||||
if (ai) {
|
||||
base.tags.emotion = ai.emotion ?? base.tags.emotion
|
||||
base.tags.intent = ai.intent ?? base.tags.intent
|
||||
if (Array.isArray(ai.behavior)) {
|
||||
base.tags.behavior = Array.from(new Set([...(base.tags.behavior ?? []), ...ai.behavior]))
|
||||
}
|
||||
}
|
||||
}
|
||||
results.push(base)
|
||||
}
|
||||
return NextResponse.json({ success: true, data: results })
|
||||
} catch (e: any) {
|
||||
return NextResponse.json({ success: false, error: e?.message || "Invalid input" }, { status: 400 })
|
||||
}
|
||||
}
|
||||
13
app/api/rfm/dump_csv/route.ts
Normal file
13
app/api/rfm/dump_csv/route.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { dumpCsv } from "@/services/rfm-engine"
|
||||
|
||||
export async function GET() {
|
||||
const csv = dumpCsv()
|
||||
return new NextResponse(csv, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "text/csv; charset=utf-8",
|
||||
"Content-Disposition": `attachment; filename="rfm_export_${Date.now()}.csv"`,
|
||||
},
|
||||
})
|
||||
}
|
||||
12
app/api/rfm/get_tags/route.ts
Normal file
12
app/api/rfm/get_tags/route.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { getUserTags } from "@/services/rfm-engine"
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const url = new URL(req.url)
|
||||
const userId = url.searchParams.get("user_id")
|
||||
if (!userId) {
|
||||
return NextResponse.json({ success: false, error: "missing user_id" }, { status: 400 })
|
||||
}
|
||||
const data = getUserTags(userId)
|
||||
return NextResponse.json({ success: true, data })
|
||||
}
|
||||
7
app/api/rfm/group_summary/route.ts
Normal file
7
app/api/rfm/group_summary/route.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { getGroupSummary } from "@/services/rfm-engine"
|
||||
|
||||
export async function GET() {
|
||||
const data = getGroupSummary()
|
||||
return NextResponse.json({ success: true, data })
|
||||
}
|
||||
12
app/api/rfm/score/route.ts
Normal file
12
app/api/rfm/score/route.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { computeRFM, type AnalyzeInput } from "@/services/rfm-engine"
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const body = (await req.json()) as AnalyzeInput
|
||||
const score = computeRFM(body)
|
||||
return NextResponse.json({ success: true, data: score })
|
||||
} catch (e: any) {
|
||||
return NextResponse.json({ success: false, error: e?.message || "Invalid input" }, { status: 400 })
|
||||
}
|
||||
}
|
||||
26
app/api/rfm/weights/route.ts
Normal file
26
app/api/rfm/weights/route.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { getWeights, setWeights } from "@/services/rfm-engine"
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({ success: true, data: getWeights() })
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const body = await req.json()
|
||||
let R = Number(body.R ?? 0.5)
|
||||
let F = Number(body.F ?? 0.3)
|
||||
let M = Number(body.M ?? 0.2)
|
||||
|
||||
// 归一化
|
||||
const sum = Math.max(0.000001, R + F + M)
|
||||
R = R / sum
|
||||
F = F / sum
|
||||
M = M / sum
|
||||
|
||||
setWeights({ R, F, M })
|
||||
return NextResponse.json({ success: true, data: { R, F, M } })
|
||||
} catch (e: any) {
|
||||
return NextResponse.json({ success: false, error: e?.message || "Invalid input" }, { status: 400 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user