chore: 同步本地到 main 和 Gitea
Made-with: Cursor
This commit is contained in:
@@ -19,9 +19,9 @@ function isSystemIntent(msg: string): boolean {
|
||||
const m = msg.trim().toLowerCase()
|
||||
return !m || m === '?' ||
|
||||
/状态|统计|总量/.test(m) ||
|
||||
/^rfm|估值|价值$/.test(m) ||
|
||||
/^高价值|top|排行/.test(m) ||
|
||||
/帮助|help/.test(m)
|
||||
/rfm|估值|价值|高价值|top|排行/.test(m) ||
|
||||
/帮助|help/.test(m) ||
|
||||
/\d+\s*人|\d+\s*个|前\s*\d+|top\s*\d+/i.test(m)
|
||||
}
|
||||
|
||||
// 将输入拆成多段(支持一人或多人:手机、QQ、身份证、姓名/关键词)
|
||||
@@ -59,19 +59,25 @@ function parseSegments(message: string): { type: 'phone' | 'qq' | 'id_card' | 'k
|
||||
return segments.length ? segments : [{ type: 'keyword', value: raw }]
|
||||
}
|
||||
|
||||
// 解析用户意图(兼容旧单条逻辑)
|
||||
function parseIntent(message: string): { type: string; query?: string; params?: any } {
|
||||
const msg = message.trim().toLowerCase()
|
||||
if (isSystemIntent(message)) {
|
||||
if (/状态|统计|总量/.test(msg)) return { type: "system_status" }
|
||||
if (/rfm|估值|价值/.test(msg)) return { type: "rfm_analysis" }
|
||||
if (/高价值|top|排行/.test(msg)) {
|
||||
const limitMatch = message.match(/(\d+)/)
|
||||
return { type: "high_value_users", params: { limit: limitMatch ? parseInt(limitMatch[1]) : 10 } }
|
||||
}
|
||||
if (/帮助|help|\?/.test(msg)) return { type: "help" }
|
||||
// 解析用户意图(先匹配「高价值/TOP/排行」再匹配「价值」,避免「找出最高价值10人」被误判为 rfm)
|
||||
function parseIntent(message: string): { type: string; query?: string; params?: any; thinking?: string } {
|
||||
const raw = message.trim()
|
||||
const msg = raw.toLowerCase()
|
||||
// 1) 系统状态
|
||||
if (/状态|统计|总量/.test(msg)) return { type: "system_status", thinking: "意图: 系统状态 → 数据源: KR 统计 → 输出: 连接与库数量" }
|
||||
// 2) 高价值用户(必须优先于「价值」否则「最高价值10人」会被判成 rfm)
|
||||
if (/高价值|最高价值|top|排行|前\s*\d+\s*名?|找出\s*.*价值|\d+\s*人.*价值|价值\s*最高|\d+\s*个.*价值/.test(msg)) {
|
||||
const limitMatch = raw.match(/(\d+)\s*[人个名]?|top\s*(\d+)|前\s*(\d+)/i) || raw.match(/(\d+)/)
|
||||
const limit = limitMatch ? parseInt(limitMatch[1] || limitMatch[2] || limitMatch[3] || limitMatch[0]) : 10
|
||||
const safeLimit = Math.min(Math.max(limit || 10, 1), 50)
|
||||
return { type: "high_value_users", params: { limit: safeLimit }, thinking: `意图: 高价值用户 TOP${safeLimit} → 数据源: KR.用户估值(user_evaluation_score 降序) → 输出: 前${safeLimit}条完整画像+AI分析` }
|
||||
}
|
||||
return { type: "unified_search", query: message }
|
||||
// 3) RFM/估值分析(仅明确是「分布/分析」时)
|
||||
if (/rfm|估值分布|价值分布|用户等级分布/.test(msg)) return { type: "rfm_analysis", thinking: "意图: RFM/等级分布 → 数据源: KR.用户估值 按 user_level 聚合 → 输出: 各等级人数" }
|
||||
// 4) 帮助
|
||||
if (/帮助|help|\?/.test(msg)) return { type: "help", thinking: "意图: 帮助 → 输出: 使用说明" }
|
||||
// 5) 手机/QQ/身份证/姓名/关键词 → 统一搜索
|
||||
return { type: "unified_search", query: message, thinking: "意图: 用户画像查询 → 数据源: KR + KR_腾讯 等 → 输出: 匹配用户完整画像+AI分析" }
|
||||
}
|
||||
|
||||
// 格式化用户数据(简要)
|
||||
@@ -177,20 +183,60 @@ function generateAIAnalysis(fullProfile: { valuation?: any; qqPhone?: any; ckbAs
|
||||
return parts.join(' ')
|
||||
}
|
||||
|
||||
// 处理 AI 聊天
|
||||
async function processChat(message: string): Promise<ChatMessage> {
|
||||
// 处理 AI 聊天(先拆解意图再执行,避免直接返回「未找到」)
|
||||
async function processChat(message: string): Promise<ChatMessage & { thinking?: string }> {
|
||||
const startTime = Date.now()
|
||||
const intent = parseIntent(message)
|
||||
const thinking = intent.thinking || `意图: ${intent.type} → 执行查询`
|
||||
|
||||
try {
|
||||
switch (intent.type) {
|
||||
case "unified_search": {
|
||||
const segments = parseSegments(message)
|
||||
const phones = await unifiedResolveToPhones(segments, 10)
|
||||
// 无结果时先做一次「意图重试」:若像在问高价值/TOP,则按高价值用户查询,避免直接报未找到
|
||||
if (phones.length === 0 && /价值|高|top|排行|前\s*\d+|找出|多少\s*人|\d+\s*人/.test(message)) {
|
||||
const fallback = parseIntent(message)
|
||||
if (fallback.type === "high_value_users") {
|
||||
const limit = fallback.params?.limit || 10
|
||||
const client = await getMongoClient()
|
||||
const db = client.db("KR")
|
||||
const users = await db.collection("用户估值")
|
||||
.find({ user_evaluation_score: { $exists: true } })
|
||||
.sort({ user_evaluation_score: -1 })
|
||||
.limit(limit)
|
||||
.toArray()
|
||||
const profiles: { fullProfile: any; aiAnalysis: string }[] = []
|
||||
for (const u of users) {
|
||||
const phone = u.phone || u.phone_masked
|
||||
if (phone) {
|
||||
const fullProfile = await queryFullProfile(phone)
|
||||
if (fullProfile.valuation || fullProfile.qqPhone) {
|
||||
profiles.push({ fullProfile, aiAnalysis: generateAIAnalysis(fullProfile) })
|
||||
}
|
||||
}
|
||||
}
|
||||
if (profiles.length > 0) {
|
||||
const contentParts = [`🔍 已按「高价值用户」理解,共找到 ${profiles.length} 条(前 ${limit} 名)\n`]
|
||||
profiles.forEach((p, i) => {
|
||||
contentParts.push(formatFullPortrait(p.fullProfile, i))
|
||||
contentParts.push(`\n🤖 AI 分析:${p.aiAnalysis}\n`)
|
||||
})
|
||||
contentParts.push(`\n⏱️ 查询耗时: ${Date.now() - startTime}ms`)
|
||||
return {
|
||||
role: "assistant",
|
||||
content: contentParts.join('\n'),
|
||||
data: { profiles, list: profiles },
|
||||
thinking: `意图重试: 关键字未命中 → 理解为「高价值用户 TOP${limit}」→ 按估值分降序查询 → 输出 ${profiles.length} 条`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (phones.length === 0) {
|
||||
return {
|
||||
role: "assistant",
|
||||
content: `🤔 未找到与「${message}」相关的用户\n\n💡 支持:手机号、QQ、身份证、姓名/城市/省份,可多条用逗号分隔,最多列出前 10 条完整画像。`
|
||||
content: `🤔 未找到与「${message}」直接相关的用户\n\n💡 支持:手机号、QQ、身份证、姓名/城市/省份,可多条用逗号分隔;或输入「高价值用户 TOP10」查看估值前 10 名。`,
|
||||
thinking
|
||||
}
|
||||
}
|
||||
const profiles: { fullProfile: any; aiAnalysis: string }[] = []
|
||||
@@ -206,7 +252,8 @@ async function processChat(message: string): Promise<ChatMessage> {
|
||||
if (profiles.length === 0) {
|
||||
return {
|
||||
role: "assistant",
|
||||
content: `❌ 未解析到有效用户画像\n\n⏱️ 耗时: ${Date.now() - startTime}ms`
|
||||
content: `❌ 未解析到有效用户画像\n\n⏱️ 耗时: ${Date.now() - startTime}ms`,
|
||||
thinking
|
||||
}
|
||||
}
|
||||
const contentParts: string[] = [
|
||||
@@ -220,7 +267,8 @@ async function processChat(message: string): Promise<ChatMessage> {
|
||||
return {
|
||||
role: "assistant",
|
||||
content: contentParts.join('\n'),
|
||||
data: { profiles, list: profiles }
|
||||
data: { profiles, list: profiles },
|
||||
thinking
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +282,8 @@ async function processChat(message: string): Promise<ChatMessage> {
|
||||
`📄 总记录数: ${(stats.totalDocuments / 1e8).toFixed(2)} 亿条\n` +
|
||||
`💾 总数据量: ${(stats.totalSize / 1e9).toFixed(2)} GB\n` +
|
||||
`⏱️ 响应延迟: ${stats.latency}ms`,
|
||||
data: stats
|
||||
data: stats,
|
||||
thinking
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,7 +309,8 @@ async function processChat(message: string): Promise<ChatMessage> {
|
||||
return {
|
||||
role: "assistant",
|
||||
content,
|
||||
data: results
|
||||
data: results,
|
||||
thinking: intent.thinking || "意图: RFM 分布 → 按 user_level 聚合"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,18 +323,27 @@ async function processChat(message: string): Promise<ChatMessage> {
|
||||
.sort({ user_evaluation_score: -1 })
|
||||
.limit(limit)
|
||||
.toArray()
|
||||
|
||||
let content = `🏆 高价值用户 TOP${limit}\n\n`
|
||||
users.forEach((u, i) => {
|
||||
const phone = u.phone?.replace(/(\+?86)?(\d{3})\d{4}(\d{4})/, '$2****$3') || '***'
|
||||
content += `${i + 1}. ${u.name || '未知'} (${phone}) - 估值: ${u.user_evaluation_score}\n`
|
||||
const profiles: { fullProfile: any; aiAnalysis: string }[] = []
|
||||
for (const u of users) {
|
||||
const phone = u.phone || u.phone_masked
|
||||
if (phone) {
|
||||
const fullProfile = await queryFullProfile(phone)
|
||||
if (fullProfile.valuation || fullProfile.qqPhone) {
|
||||
profiles.push({ fullProfile, aiAnalysis: generateAIAnalysis(fullProfile) })
|
||||
}
|
||||
}
|
||||
}
|
||||
const contentParts = [`🏆 高价值用户 TOP${limit}(按估值分降序)\n`]
|
||||
profiles.forEach((p, i) => {
|
||||
contentParts.push(formatFullPortrait(p.fullProfile, i))
|
||||
contentParts.push(`\n🤖 AI 分析:${p.aiAnalysis}\n`)
|
||||
})
|
||||
content += `\n⏱️ 查询耗时: ${Date.now() - startTime}ms`
|
||||
|
||||
contentParts.push(`\n⏱️ 查询耗时: ${Date.now() - startTime}ms`)
|
||||
return {
|
||||
role: "assistant",
|
||||
content,
|
||||
data: users
|
||||
content: contentParts.join('\n'),
|
||||
data: { profiles, list: profiles },
|
||||
thinking
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,15 +354,18 @@ async function processChat(message: string): Promise<ChatMessage> {
|
||||
`📱 手机 / 💬 QQ / 🪪 身份证 / 👤 姓名:统一搜索,最多列出前 10 条完整画像\n\n` +
|
||||
`• 支持多条同时查:用逗号或换行分隔,如 "13800138000, 28533368 qq, 张三"\n` +
|
||||
`• 每条画像含:手机、QQ、地址、统一标签、流量池、存客宝、AI 分析\n\n` +
|
||||
`📊 系统状态 / 🏆 高价值 / 📈 RFM:输入 "系统状态"、"高价值用户 TOP10"、"RFM分析"\n\n` +
|
||||
`💡 数据覆盖: 20亿+用户,曼谷库内数据统一展示`
|
||||
`🏆 高价值用户:支持「找出最高价值10人」「高价值 TOP10」「前10名」等,先拆解意图再查询\n\n` +
|
||||
`📊 系统状态 / 📈 RFM:输入 "系统状态"、"RFM分析"\n\n` +
|
||||
`💡 数据覆盖: 20亿+用户,曼谷库内数据统一展示`,
|
||||
thinking
|
||||
}
|
||||
}
|
||||
|
||||
default: {
|
||||
return {
|
||||
role: "assistant",
|
||||
content: `🤔 未识别指令\n\n输入 "帮助" 查看支持:手机、QQ、身份证、姓名,可多条件,前 10 条完整画像。`
|
||||
content: `🤔 未识别指令\n\n输入 "帮助" 查看支持:手机、QQ、身份证、姓名,可多条件;或「高价值用户 TOP10」查看估值前 10 名。`,
|
||||
thinking
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getMongoClient } from '@/lib/mongodb'
|
||||
|
||||
// API密钥接口
|
||||
interface APIKey {
|
||||
@@ -100,40 +101,13 @@ const DEFAULT_FIELD_PERMISSIONS: FieldPermission[] = [
|
||||
},
|
||||
]
|
||||
|
||||
// 内存存储(生产环境应使用MongoDB)
|
||||
let apiKeys: APIKey[] = [
|
||||
{
|
||||
id: 'key_1',
|
||||
name: '存客宝-生产环境',
|
||||
key: 'sk-archer-ckb-prod-a1b2c3d4e5f6',
|
||||
secret: 'sec-ckb-x9y8z7w6v5u4',
|
||||
status: 'active',
|
||||
createdAt: '2026-01-15',
|
||||
expiresAt: null,
|
||||
lastUsed: '2026-01-31 14:32:15',
|
||||
permissions: JSON.parse(JSON.stringify(DEFAULT_FIELD_PERMISSIONS)),
|
||||
rateLimit: { requestsPerDay: 5000, requestsPerMonth: 150000 },
|
||||
billing: { plan: 'pro', usedCredits: 12580, totalCredits: 50000 },
|
||||
callStats: { today: 342, thisMonth: 8956, total: 45678 }
|
||||
},
|
||||
{
|
||||
id: 'key_2',
|
||||
name: '点了码-测试环境',
|
||||
key: 'sk-archer-dlm-test-g7h8i9j0k1l2',
|
||||
secret: 'sec-dlm-m3n4o5p6q7r8',
|
||||
status: 'active',
|
||||
createdAt: '2026-01-20',
|
||||
expiresAt: '2026-04-20',
|
||||
lastUsed: '2026-01-30 09:15:42',
|
||||
permissions: JSON.parse(JSON.stringify(DEFAULT_FIELD_PERMISSIONS)).map((g: FieldPermission) => ({
|
||||
...g,
|
||||
fields: g.fields.map(f => ({ ...f, enabled: f.price <= 2 }))
|
||||
})),
|
||||
rateLimit: { requestsPerDay: 1000, requestsPerMonth: 30000 },
|
||||
billing: { plan: 'basic', usedCredits: 2340, totalCredits: 10000 },
|
||||
callStats: { today: 56, thisMonth: 1234, total: 5678 }
|
||||
},
|
||||
]
|
||||
const COLLECTION = 'api_keys'
|
||||
const DB_NAME = 'KR'
|
||||
|
||||
async function getCollection() {
|
||||
const client = await getMongoClient()
|
||||
return client.db(DB_NAME).collection<APIKey & { _id?: any }>(COLLECTION)
|
||||
}
|
||||
|
||||
// 生成随机密钥
|
||||
function generateKey(prefix: string): string {
|
||||
@@ -152,12 +126,14 @@ export async function GET(request: NextRequest) {
|
||||
const action = searchParams.get('action')
|
||||
const keyId = searchParams.get('id')
|
||||
|
||||
const col = await getCollection()
|
||||
|
||||
// 验证单个密钥
|
||||
if (action === 'validate') {
|
||||
const apiKey = searchParams.get('key')
|
||||
const apiSecret = searchParams.get('secret')
|
||||
|
||||
const key = apiKeys.find(k => k.key === apiKey && k.secret === apiSecret)
|
||||
const key = await col.findOne({ key: apiKey, secret: apiSecret })
|
||||
|
||||
if (!key) {
|
||||
return NextResponse.json({
|
||||
@@ -173,7 +149,6 @@ export async function GET(request: NextRequest) {
|
||||
}, { status: 403 })
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if (key.expiresAt && new Date(key.expiresAt) < new Date()) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
@@ -196,7 +171,7 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
// 获取单个密钥详情
|
||||
if (keyId) {
|
||||
const key = apiKeys.find(k => k.id === keyId)
|
||||
const key = await col.findOne({ id: keyId })
|
||||
if (!key) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
@@ -210,14 +185,15 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
// 获取所有密钥列表
|
||||
const keys = await col.find({}).sort({ createdAt: -1 }).toArray()
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: apiKeys.map(k => ({
|
||||
data: keys.map(k => ({
|
||||
...k,
|
||||
key: k.key.substring(0, 12) + '••••••••••••', // 脱敏显示
|
||||
key: k.key.substring(0, 12) + '••••••••••••',
|
||||
secret: '••••••••••••••••'
|
||||
})),
|
||||
total: apiKeys.length
|
||||
total: keys.length
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
@@ -266,7 +242,8 @@ export async function POST(request: NextRequest) {
|
||||
callStats: { today: 0, thisMonth: 0, total: 0 }
|
||||
}
|
||||
|
||||
apiKeys.push(newKey)
|
||||
const col = await getCollection()
|
||||
await col.insertOne(newKey)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
@@ -289,53 +266,57 @@ export async function PUT(request: NextRequest) {
|
||||
const body = await request.json()
|
||||
const { id, action, permissions, status } = body
|
||||
|
||||
const keyIndex = apiKeys.findIndex(k => k.id === id)
|
||||
if (keyIndex === -1) {
|
||||
const col = await getCollection()
|
||||
const key = await col.findOne({ id })
|
||||
|
||||
if (!key) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '密钥不存在'
|
||||
}, { status: 404 })
|
||||
}
|
||||
|
||||
// 更新权限
|
||||
if (action === 'updatePermissions' && permissions) {
|
||||
apiKeys[keyIndex].permissions = permissions
|
||||
await col.updateOne({ id }, { $set: { permissions } })
|
||||
const updated = await col.findOne({ id })
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: apiKeys[keyIndex],
|
||||
data: updated,
|
||||
message: '权限更新成功'
|
||||
})
|
||||
}
|
||||
|
||||
// 切换状态
|
||||
if (action === 'toggleStatus') {
|
||||
apiKeys[keyIndex].status = apiKeys[keyIndex].status === 'active' ? 'disabled' : 'active'
|
||||
const newStatus = key.status === 'active' ? 'disabled' : 'active'
|
||||
await col.updateOne({ id }, { $set: { status: newStatus } })
|
||||
const updated = await col.findOne({ id })
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: apiKeys[keyIndex],
|
||||
message: `密钥已${apiKeys[keyIndex].status === 'active' ? '启用' : '禁用'}`
|
||||
data: updated,
|
||||
message: `密钥已${newStatus === 'active' ? '启用' : '禁用'}`
|
||||
})
|
||||
}
|
||||
|
||||
// 重新生成密钥
|
||||
if (action === 'regenerate') {
|
||||
apiKeys[keyIndex].key = generateKey('sk-archer-')
|
||||
apiKeys[keyIndex].secret = generateKey('sec-')
|
||||
const newKey = generateKey('sk-archer-')
|
||||
const newSecret = generateKey('sec-')
|
||||
await col.updateOne({ id }, { $set: { key: newKey, secret: newSecret } })
|
||||
const updated = await col.findOne({ id })
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: apiKeys[keyIndex],
|
||||
data: updated,
|
||||
message: '密钥已重新生成'
|
||||
})
|
||||
}
|
||||
|
||||
// 常规更新
|
||||
if (status) {
|
||||
apiKeys[keyIndex].status = status
|
||||
await col.updateOne({ id }, { $set: { status } })
|
||||
}
|
||||
|
||||
const updated = await col.findOne({ id })
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: apiKeys[keyIndex],
|
||||
data: updated,
|
||||
message: '更新成功'
|
||||
})
|
||||
|
||||
@@ -361,19 +342,19 @@ export async function DELETE(request: NextRequest) {
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
const keyIndex = apiKeys.findIndex(k => k.id === id)
|
||||
if (keyIndex === -1) {
|
||||
const col = await getCollection()
|
||||
const result = await col.findOneAndDelete({ id })
|
||||
|
||||
if (!result) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '密钥不存在'
|
||||
}, { status: 404 })
|
||||
}
|
||||
|
||||
const deletedKey = apiKeys.splice(keyIndex, 1)[0]
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: { id: deletedKey.id, name: deletedKey.name },
|
||||
data: { id: result.id, name: result.name },
|
||||
message: '密钥已删除'
|
||||
})
|
||||
|
||||
|
||||
@@ -203,8 +203,10 @@ const MOCK_API_KEYS: APIKey[] = [
|
||||
]
|
||||
|
||||
export default function APIKeysPage() {
|
||||
const [apiKeys, setApiKeys] = useState<APIKey[]>(MOCK_API_KEYS)
|
||||
const [apiKeys, setApiKeys] = useState<APIKey[]>([])
|
||||
const [apiBaseUrl, setApiBaseUrl] = useState('')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [creating, setCreating] = useState(false)
|
||||
|
||||
// 弹窗状态
|
||||
const [showCreateKeyDialog, setShowCreateKeyDialog] = useState(false)
|
||||
@@ -229,9 +231,25 @@ export default function APIKeysPage() {
|
||||
// 显示/隐藏密钥
|
||||
const [visibleKeys, setVisibleKeys] = useState<Set<string>>(new Set())
|
||||
|
||||
// 拉取密钥列表
|
||||
const fetchKeys = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/api-keys')
|
||||
const json = await res.json()
|
||||
if (json.success && json.data) {
|
||||
setApiKeys(json.data)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('拉取API密钥失败:', e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const host = typeof window !== 'undefined' ? window.location.origin : ''
|
||||
setApiBaseUrl(host)
|
||||
fetchKeys()
|
||||
}, [])
|
||||
|
||||
const copyToClipboard = (text: string, id: string) => {
|
||||
@@ -274,40 +292,64 @@ export default function APIKeysPage() {
|
||||
}
|
||||
|
||||
// 创建新密钥
|
||||
const handleCreateKey = () => {
|
||||
const plan = BILLING_PLANS.find(p => p.id === newKeyForm.plan)!
|
||||
const newKey: APIKey = {
|
||||
id: `key_${Date.now()}`,
|
||||
name: newKeyForm.name,
|
||||
key: `sk-archer-${Math.random().toString(36).substring(2, 14)}`,
|
||||
secret: `sec-${Math.random().toString(36).substring(2, 14)}`,
|
||||
status: 'active',
|
||||
createdAt: new Date().toISOString().split('T')[0],
|
||||
expiresAt: newKeyForm.expiresAt || null,
|
||||
lastUsed: null,
|
||||
permissions: JSON.parse(JSON.stringify(FIELD_GROUPS)),
|
||||
rateLimit: { requestsPerDay: plan.requestsPerDay, requestsPerMonth: plan.requestsPerMonth },
|
||||
billing: { plan: plan.id as 'free' | 'basic' | 'pro' | 'enterprise', usedCredits: 0, totalCredits: plan.credits },
|
||||
callStats: { today: 0, thisMonth: 0, total: 0 }
|
||||
const handleCreateKey = async () => {
|
||||
if (!newKeyForm.name.trim()) return
|
||||
setCreating(true)
|
||||
try {
|
||||
const res = await fetch('/api/api-keys', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: newKeyForm.name.trim(),
|
||||
plan: newKeyForm.plan,
|
||||
expiresAt: newKeyForm.expiresAt || undefined,
|
||||
}),
|
||||
})
|
||||
const json = await res.json()
|
||||
if (json.success && json.data) {
|
||||
setApiKeys([json.data, ...apiKeys])
|
||||
setShowCreateKeyDialog(false)
|
||||
setNewKeyForm({ name: '', plan: 'basic', expiresAt: '' })
|
||||
} else {
|
||||
alert(json.error || '创建失败')
|
||||
}
|
||||
} catch (e) {
|
||||
alert('创建失败')
|
||||
} finally {
|
||||
setCreating(false)
|
||||
}
|
||||
setApiKeys([...apiKeys, newKey])
|
||||
setShowCreateKeyDialog(false)
|
||||
setNewKeyForm({ name: '', plan: 'basic', expiresAt: '' })
|
||||
}
|
||||
|
||||
// 切换密钥状态
|
||||
const toggleKeyStatus = (keyId: string) => {
|
||||
setApiKeys(apiKeys.map(k =>
|
||||
k.id === keyId
|
||||
? { ...k, status: k.status === 'active' ? 'disabled' : 'active' }
|
||||
: k
|
||||
))
|
||||
const toggleKeyStatus = async (keyId: string) => {
|
||||
try {
|
||||
const res = await fetch('/api/api-keys', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: keyId, action: 'toggleStatus' }),
|
||||
})
|
||||
const json = await res.json()
|
||||
if (json.success && json.data) {
|
||||
setApiKeys(apiKeys.map(k => k.id === keyId ? { ...k, ...json.data } : k))
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('切换状态失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除密钥
|
||||
const deleteKey = (keyId: string) => {
|
||||
if (confirm('确定要删除此API密钥吗?此操作不可恢复。')) {
|
||||
setApiKeys(apiKeys.filter(k => k.id !== keyId))
|
||||
const deleteKey = async (keyId: string) => {
|
||||
if (!confirm('确定要删除此API密钥吗?此操作不可恢复。')) return
|
||||
try {
|
||||
const res = await fetch(`/api/api-keys?id=${keyId}`, { method: 'DELETE' })
|
||||
const json = await res.json()
|
||||
if (json.success) {
|
||||
setApiKeys(apiKeys.filter(k => k.id !== keyId))
|
||||
} else {
|
||||
alert(json.error || '删除失败')
|
||||
}
|
||||
} catch (e) {
|
||||
alert('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,12 +362,22 @@ export default function APIKeysPage() {
|
||||
}
|
||||
|
||||
// 保存权限设置
|
||||
const savePermissions = () => {
|
||||
const savePermissions = async () => {
|
||||
if (!selectedKey) return
|
||||
setApiKeys(apiKeys.map(k =>
|
||||
k.id === selectedKey.id ? selectedKey : k
|
||||
))
|
||||
setShowPermissionDialog(false)
|
||||
try {
|
||||
const res = await fetch('/api/api-keys', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: selectedKey.id, action: 'updatePermissions', permissions: selectedKey.permissions }),
|
||||
})
|
||||
const json = await res.json()
|
||||
if (json.success) {
|
||||
setApiKeys(apiKeys.map(k => k.id === selectedKey.id ? { ...k, permissions: selectedKey.permissions } : k))
|
||||
setShowPermissionDialog(false)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('保存权限失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 统计
|
||||
@@ -348,7 +400,7 @@ export default function APIKeysPage() {
|
||||
<p className="text-sm text-gray-500 mt-1">创建和管理第三方系统接入密钥</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={() => setShowCreateKeyDialog(true)}>
|
||||
<Button onClick={() => setShowCreateKeyDialog(true)} disabled={loading}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
创建密钥
|
||||
</Button>
|
||||
@@ -434,6 +486,17 @@ export default function APIKeysPage() {
|
||||
<CardDescription>管理所有API密钥,配置权限和额度</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loading ? (
|
||||
<div className="py-12 text-center text-gray-500">加载中...</div>
|
||||
) : apiKeys.length === 0 ? (
|
||||
<div className="py-12 text-center text-gray-500">
|
||||
<p>暂无API密钥</p>
|
||||
<Button variant="outline" className="mt-4" onClick={() => setShowCreateKeyDialog(true)}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
创建第一个密钥
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -539,11 +602,12 @@ export default function APIKeysPage() {
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* ==================== 弹窗 ==================== */}
|
||||
|
||||
@@ -593,7 +657,9 @@ export default function APIKeysPage() {
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowCreateKeyDialog(false)}>取消</Button>
|
||||
<Button onClick={handleCreateKey} disabled={!newKeyForm.name}>创建密钥</Button>
|
||||
<Button onClick={handleCreateKey} disabled={!newKeyForm.name.trim() || creating}>
|
||||
{creating ? '创建中...' : '创建密钥'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -552,7 +552,9 @@ export default function APIServicePage() {
|
||||
const [activeTab, setActiveTab] = useState('keys')
|
||||
const [activeCategory, setActiveCategory] = useState('all')
|
||||
const [apiBaseUrl, setApiBaseUrl] = useState('')
|
||||
const [apiKeys, setApiKeys] = useState<APIKey[]>(MOCK_API_KEYS)
|
||||
const [apiKeys, setApiKeys] = useState<APIKey[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [creating, setCreating] = useState(false)
|
||||
const [callLogs] = useState<CallLog[]>(MOCK_CALL_LOGS)
|
||||
|
||||
// 弹窗状态
|
||||
@@ -579,9 +581,22 @@ export default function APIServicePage() {
|
||||
// 显示/隐藏密钥
|
||||
const [visibleKeys, setVisibleKeys] = useState<Set<string>>(new Set())
|
||||
|
||||
const fetchKeys = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/api-keys')
|
||||
const json = await res.json()
|
||||
if (json.success && json.data) setApiKeys(json.data)
|
||||
} catch (e) {
|
||||
console.error('拉取API密钥失败:', e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const host = typeof window !== 'undefined' ? window.location.origin : ''
|
||||
setApiBaseUrl(host)
|
||||
fetchKeys()
|
||||
}, [])
|
||||
|
||||
const copyToClipboard = (text: string, id: string) => {
|
||||
@@ -638,40 +653,64 @@ export default function APIServicePage() {
|
||||
: API_ENDPOINTS.filter(e => e.category === activeCategory)
|
||||
|
||||
// 创建新密钥
|
||||
const handleCreateKey = () => {
|
||||
const plan = BILLING_PLANS.find(p => p.id === newKeyForm.plan)!
|
||||
const newKey: APIKey = {
|
||||
id: `key_${Date.now()}`,
|
||||
name: newKeyForm.name,
|
||||
key: `sk-archer-${Math.random().toString(36).substring(2, 14)}`,
|
||||
secret: `sec-${Math.random().toString(36).substring(2, 14)}`,
|
||||
status: 'active',
|
||||
createdAt: new Date().toISOString().split('T')[0],
|
||||
expiresAt: newKeyForm.expiresAt || null,
|
||||
lastUsed: null,
|
||||
permissions: JSON.parse(JSON.stringify(FIELD_GROUPS)),
|
||||
rateLimit: plan.rateLimit,
|
||||
billing: { plan: plan.id, usedCredits: 0, totalCredits: plan.credits },
|
||||
callStats: { today: 0, thisMonth: 0, total: 0 }
|
||||
const handleCreateKey = async () => {
|
||||
if (!newKeyForm.name.trim()) return
|
||||
setCreating(true)
|
||||
try {
|
||||
const res = await fetch('/api/api-keys', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: newKeyForm.name.trim(),
|
||||
plan: newKeyForm.plan,
|
||||
expiresAt: newKeyForm.expiresAt || undefined,
|
||||
}),
|
||||
})
|
||||
const json = await res.json()
|
||||
if (json.success && json.data) {
|
||||
setApiKeys([json.data, ...apiKeys])
|
||||
setShowCreateKeyDialog(false)
|
||||
setNewKeyForm({ name: '', plan: 'basic', expiresAt: '' })
|
||||
} else {
|
||||
alert(json.error || '创建失败')
|
||||
}
|
||||
} catch {
|
||||
alert('创建失败')
|
||||
} finally {
|
||||
setCreating(false)
|
||||
}
|
||||
setApiKeys([...apiKeys, newKey])
|
||||
setShowCreateKeyDialog(false)
|
||||
setNewKeyForm({ name: '', plan: 'basic', expiresAt: '' })
|
||||
}
|
||||
|
||||
// 切换密钥状态
|
||||
const toggleKeyStatus = (keyId: string) => {
|
||||
setApiKeys(apiKeys.map(k =>
|
||||
k.id === keyId
|
||||
? { ...k, status: k.status === 'active' ? 'disabled' : 'active' }
|
||||
: k
|
||||
))
|
||||
const toggleKeyStatus = async (keyId: string) => {
|
||||
try {
|
||||
const res = await fetch('/api/api-keys', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: keyId, action: 'toggleStatus' }),
|
||||
})
|
||||
const json = await res.json()
|
||||
if (json.success && json.data) {
|
||||
setApiKeys(apiKeys.map(k => k.id === keyId ? { ...k, ...json.data } : k))
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('切换状态失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除密钥
|
||||
const deleteKey = (keyId: string) => {
|
||||
if (confirm('确定要删除此API密钥吗?此操作不可恢复。')) {
|
||||
setApiKeys(apiKeys.filter(k => k.id !== keyId))
|
||||
const deleteKey = async (keyId: string) => {
|
||||
if (!confirm('确定要删除此API密钥吗?此操作不可恢复。')) return
|
||||
try {
|
||||
const res = await fetch(`/api/api-keys?id=${keyId}`, { method: 'DELETE' })
|
||||
const json = await res.json()
|
||||
if (json.success) {
|
||||
setApiKeys(apiKeys.filter(k => k.id !== keyId))
|
||||
} else {
|
||||
alert(json.error || '删除失败')
|
||||
}
|
||||
} catch {
|
||||
alert('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -684,12 +723,22 @@ export default function APIServicePage() {
|
||||
}
|
||||
|
||||
// 保存权限设置
|
||||
const savePermissions = () => {
|
||||
const savePermissions = async () => {
|
||||
if (!selectedKey) return
|
||||
setApiKeys(apiKeys.map(k =>
|
||||
k.id === selectedKey.id ? selectedKey : k
|
||||
))
|
||||
setShowPermissionDialog(false)
|
||||
try {
|
||||
const res = await fetch('/api/api-keys', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: selectedKey.id, action: 'updatePermissions', permissions: selectedKey.permissions }),
|
||||
})
|
||||
const json = await res.json()
|
||||
if (json.success) {
|
||||
setApiKeys(apiKeys.map(k => k.id === selectedKey.id ? { ...k, permissions: selectedKey.permissions } : k))
|
||||
setShowPermissionDialog(false)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('保存权限失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 统计数据
|
||||
@@ -846,6 +895,15 @@ export default function APIServicePage() {
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loading ? (
|
||||
<div className="py-12 text-center text-gray-500">加载中...</div>
|
||||
) : apiKeys.length === 0 ? (
|
||||
<div className="py-12 text-center text-gray-500">
|
||||
暂无API密钥,请到
|
||||
<Link href="/data-market/api/keys" className="text-purple-600 hover:underline ml-1">API密钥管理</Link>
|
||||
创建
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -950,6 +1008,7 @@ export default function APIServicePage() {
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1330,7 +1389,9 @@ export default function APIServicePage() {
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowCreateKeyDialog(false)}>取消</Button>
|
||||
<Button onClick={handleCreateKey} disabled={!newKeyForm.name}>创建密钥</Button>
|
||||
<Button onClick={handleCreateKey} disabled={!newKeyForm.name.trim() || creating}>
|
||||
{creating ? '创建中...' : '创建密钥'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -194,7 +194,7 @@ export default function HomePage() {
|
||||
fullProfile: respData?.fullProfile || first?.fullProfile,
|
||||
aiAnalysis: respData?.aiAnalysis || first?.aiAnalysis,
|
||||
profiles: Array.isArray(profiles) ? profiles : undefined,
|
||||
thinking: generateThinking(query),
|
||||
thinking: data.response?.thinking || generateThinking(query),
|
||||
}]
|
||||
})
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 所有网站类服务统一放在 project name: website 下
|
||||
# 只使用唯一 MongoDB:datacenter_mongodb(宿主机 27017),不在此编排中新建 MongoDB
|
||||
# 使用方式:在神射手目录执行 docker compose up -d
|
||||
# 神射手:http://localhost:3117 玩值电竞:http://localhost:3001
|
||||
# 神射手:http://localhost:3117 玩值电竞:http://localhost:3001 OpenClaw 网关:http://localhost:18789
|
||||
|
||||
name: website
|
||||
|
||||
@@ -13,8 +13,8 @@ services:
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: "3117"
|
||||
# 唯一 MongoDB:datacenter_mongodb(宿主机 27017)
|
||||
MONGODB_URI: "mongodb://host.docker.internal:27017"
|
||||
# 唯一 MongoDB:datacenter_mongodb(宿主机 27017),若启用认证需带账号
|
||||
MONGODB_URI: "mongodb://admin:admin123@host.docker.internal:27017/?authSource=admin"
|
||||
ports:
|
||||
- "3117:3117"
|
||||
extra_hosts:
|
||||
@@ -33,7 +33,7 @@ services:
|
||||
image: wanzhi-app:latest
|
||||
container_name: website-wanzhi-web
|
||||
environment:
|
||||
MONGODB_URI: mongodb://host.docker.internal:27017
|
||||
MONGODB_URI: mongodb://admin:admin123@host.docker.internal:27017/?authSource=admin
|
||||
NODE_ENV: production
|
||||
ports:
|
||||
- "3001:3000"
|
||||
@@ -43,6 +43,68 @@ services:
|
||||
networks:
|
||||
- website
|
||||
|
||||
# 抖音解析 API:供 n8n 工作流调用,一键解析文案(可选下载视频)
|
||||
douyin-api:
|
||||
image: python:3.11-slim
|
||||
container_name: website-douyin-api
|
||||
working_dir: /app
|
||||
environment:
|
||||
DOUYIN_OUTPUT_DIR: /data/douyin_output
|
||||
volumes:
|
||||
- ../../../个人/卡若AI/03_卡木(木)/木叶_视频内容/抖音视频解析/脚本:/app
|
||||
- /Users/karuo/Documents/卡若Ai的文件夹/视频:/data/douyin_output
|
||||
ports:
|
||||
- "3099:3099"
|
||||
command: ["sh", "-c", "pip install -q flask requests && python douyin_api.py"]
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- website
|
||||
|
||||
# n8n 工作流自动化:归入 website 分类,端口 5678,界面中文(社区汉化镜像)
|
||||
n8n:
|
||||
image: blowsnow/n8n-chinese:latest
|
||||
container_name: website-n8n
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
GENERIC_TIMEZONE: Asia/Shanghai
|
||||
N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS: "true"
|
||||
N8N_RUNNERS_ENABLED: "true"
|
||||
N8N_DEFAULT_LOCALE: "zh-CN"
|
||||
ports:
|
||||
- "5678:5678"
|
||||
volumes:
|
||||
- n8n_data:/home/node/.n8n
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- website
|
||||
|
||||
# OpenClaw 网关:网站类服务,已迁入 website 编排;镜像需在 OpenClaw 项目内先 build(openclaw:local)
|
||||
# 需访问本机 Ollama 时通过 host.docker.internal:11434
|
||||
openclaw-gateway:
|
||||
image: openclaw:local
|
||||
container_name: website-openclaw-gateway
|
||||
env_file:
|
||||
- ../../8、小工具/Docker项目/OpenClaw/openclaw/.env
|
||||
environment:
|
||||
HOME: /home/node
|
||||
TERM: xterm-256color
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
volumes:
|
||||
- /Users/karuo/.openclaw:/home/node/.openclaw
|
||||
- /Users/karuo/.openclaw/workspace:/home/node/.openclaw/workspace
|
||||
ports:
|
||||
- "18789:18789"
|
||||
- "18790:18790"
|
||||
init: true
|
||||
restart: unless-stopped
|
||||
command: ["node", "dist/index.js", "gateway", "--bind", "lan", "--port", "18789"]
|
||||
networks:
|
||||
- website
|
||||
|
||||
networks:
|
||||
website:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
n8n_data:
|
||||
|
||||
BIN
开发文档/.DS_Store
vendored
Normal file
BIN
开发文档/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -9,6 +9,7 @@
|
||||
|
||||
| 日期 | 当日完成 | 进行中 | 备注 |
|
||||
|:---|:---|:---|:---|
|
||||
| 2026-03-08 | API 密钥持久化:后端改用 MongoDB(KR.api_keys),前端 API 服务页、API 密钥页对接 /api/api-keys,创建/更新/删除/切换状态全部落库 | — | 解决「API 不创建」问题 |
|
||||
| 2026-02-26 | API文档系统:新增 /api/docs 端点提供 OpenAPI 3.0 和 Markdown 格式文档;支持下载和AI直接对接;文档页面增加复制链接、下载按钮、AI对接提示卡片 | — | /api/docs?format=openapi |
|
||||
| 2026-02-26 | 开放API优化:侧边栏移除API密钥/文档子项(整合到API服务内);开放接口增强(实时数据流可视化、字段级授权管理、Webhook配置、5列统计卡片、成功率/响应时间、接入方详情增强) | — | 页面 /data-market/open-api |
|
||||
| 2026-02-26 | 卡若前端标准:神射手+毛狐狸的布局/颜色/毛玻璃/组件/特效提炼为标准文档,写入卡若AI「全栈开发」;全栈开发 Skill 与前端生成 Skill 全量更新,后续项目按此标准开发 | — | 标准见 卡若AI/全栈开发/前端标准_神射手与毛狐狸.md |
|
||||
|
||||
111
开发文档/1、需求/20260308api服务功能.md
Normal file
111
开发文档/1、需求/20260308api服务功能.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# 2026-03-08 API 服务功能需求
|
||||
|
||||
## 一、原始需求
|
||||
|
||||
**功能一:API 不创建**
|
||||
神射手新增 API 后无法直接使用,API 没有写入数据库。需要支持生成或手填 API,并能与神射手正确对接。
|
||||
|
||||
**功能二:API 服务与开放接口是否重叠**
|
||||
分析 API 服务与开放接口两个模块是否重复,若重叠则整合,并确保服务可用、可添加。
|
||||
|
||||
---
|
||||
|
||||
## 二、现状分析(已核查代码)
|
||||
|
||||
### 2.1 API 不创建问题
|
||||
|
||||
| 层级 | 现状 | 问题 |
|
||||
|:---|:---|:---|
|
||||
| **前端** | `data-market/api/page.tsx`、`data-market/api/keys/page.tsx` 使用 `MOCK_API_KEYS` | 创建密钥时仅 `setApiKeys([...apiKeys, newKey])`,未调用后端 |
|
||||
| **后端** | `app/api/api-keys/route.ts` 有完整 GET/POST/PUT/DELETE | 使用内存 `let apiKeys[]`,注释写「生产环境应使用 MongoDB」;重启后数据丢失 |
|
||||
| **结论** | 前端未对接 `/api/api-keys`,后端也未落库 | 新增 API 密钥不会持久化,刷新即丢失 |
|
||||
|
||||
### 2.2 API 服务 vs 开放接口对比
|
||||
|
||||
| 维度 | API 服务 (`/data-market/api`) | 开放接口 (`/data-market/open-api`) |
|
||||
|:---|:---|:---|
|
||||
| **入口** | 数据市场 → API 服务 | 数据市场 → 开放接口 |
|
||||
| **核心概念** | API 密钥 (Key) | 接入方 (Partner) |
|
||||
| **接口路径** | `/api/shensheshou/*`(用户查询、AI、标签、流量包等) | `/api/open-api/*`(数据流入、查询、标签、批量) |
|
||||
| **能力** | 密钥管理、计费套餐、字段权限、调用日志 | 接入方管理、字段授权、Webhook、数据流可视化 |
|
||||
| **数据** | MOCK 数据 | MOCK 数据 |
|
||||
|
||||
**重叠部分**:
|
||||
- 密钥/接入方创建、启用禁用、权限配置
|
||||
- 认证方式均为 Bearer + X-API-Secret
|
||||
- 字段级权限、调用统计
|
||||
|
||||
**差异部分**:
|
||||
- API 服务:强调计费(信用点、套餐)、13 个 shensheshou 端点
|
||||
- 开放接口:强调接入方、数据流入/流出、Webhook 回调
|
||||
|
||||
**结论**:二者在「密钥/接入方 + 权限 + 调用」上高度重叠,可整合为统一模块。
|
||||
|
||||
---
|
||||
|
||||
## 三、解决方案
|
||||
|
||||
### 3.1 修复 API 不创建(优先级高)
|
||||
|
||||
1. **后端持久化**
|
||||
- 新建 MongoDB 集合 `api_keys`(或复用 `KR` 库下 `shensheshou.api_keys`)
|
||||
- 将 `app/api/api-keys/route.ts` 中的内存存储改为 MongoDB CRUD
|
||||
|
||||
2. **前端对接**
|
||||
- `data-market/api/page.tsx`、`data-market/api/keys/page.tsx` 的创建/更新/删除调用 `/api/api-keys`
|
||||
- 页面初始化时 GET `/api/api-keys` 拉取列表,替代 MOCK
|
||||
|
||||
3. **可选:支持手填 API**
|
||||
- 在创建密钥时支持「使用已有 key/secret」选项,仅录入名称和权限,不自动生成
|
||||
|
||||
### 3.2 整合 API 服务与开放接口(可选)
|
||||
|
||||
**方案 A:保留双入口,统一后端**
|
||||
- 导航保留「API 服务」和「开放接口」
|
||||
- 共用同一套 API 密钥/接入方数据源与 API
|
||||
- 两页面分别侧重:API 服务→文档与计费,开放接口→接入方与数据流
|
||||
|
||||
**方案 B:合并为单一「开放接口」**
|
||||
- 导航只保留「开放接口」
|
||||
- 在开放接口页内增加 Tab:接入方、API 文档、计费、调用日志
|
||||
- 逐步下线「API 服务」独立入口
|
||||
|
||||
推荐先做 **3.1**,再按业务需要决定是否做 **3.2**。
|
||||
|
||||
---
|
||||
|
||||
## 四、数据库设计建议
|
||||
|
||||
```javascript
|
||||
// 集合: shensheshou.api_keys (或 KR.api_keys)
|
||||
{
|
||||
_id: ObjectId,
|
||||
id: "key_xxx", // 业务ID
|
||||
name: "存客宝-生产",
|
||||
key: "sk-archer-xxx",
|
||||
secret: "sec-xxx", // 存储时考虑加密
|
||||
status: "active" | "disabled" | "expired",
|
||||
plan: "free" | "basic" | "pro" | "enterprise",
|
||||
permissions: [...],
|
||||
rateLimit: { requestsPerDay, requestsPerMonth },
|
||||
billing: { usedCredits, totalCredits },
|
||||
callStats: { today, thisMonth, total },
|
||||
createdAt: ISODate,
|
||||
expiresAt: ISODate | null,
|
||||
lastUsed: ISODate | null,
|
||||
partnerId: "ckb_001" // 可选,关联开放接口接入方
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、验收标准
|
||||
|
||||
- [x] 在「API 服务」或「API 密钥管理」中创建密钥后,刷新页面密钥仍存在(已实现 MongoDB 持久化)
|
||||
- [x] 新建密钥可正常用于 `/api/shensheshou/user` 等接口认证(shensheshou 路由已调用 api-keys 校验)
|
||||
- [ ] 支持手填已有 key/secret(可选)
|
||||
- [ ] 若整合:API 服务与开放接口共用同一数据源,无重复录入
|
||||
|
||||
---
|
||||
|
||||

|
||||
BIN
开发文档/1、需求/images/2026-03-08-09-03-18.png
Normal file
BIN
开发文档/1、需求/images/2026-03-08-09-03-18.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
@@ -1,87 +0,0 @@
|
||||
# 神射手 - 核心需求提取
|
||||
|
||||
> 📅 从项目代码与文档提取 | 2026-01-31
|
||||
|
||||
---
|
||||
|
||||
## 一、项目目标(金)
|
||||
|
||||
| 目标 | 描述 |
|
||||
|:---|:---|
|
||||
| 核心能力 | 输入手机号/QQ/UID → 秒级返回用户画像 |
|
||||
| 数据规模 | 20亿+ 用户记录 |
|
||||
| 业务价值 | 用户资产数字化、流量池管理、精准营销 |
|
||||
|
||||
---
|
||||
|
||||
## 二、五大模块需求(木)
|
||||
|
||||
| 模块 | 核心功能 | 验收标准 |
|
||||
|:---|:---|:---|
|
||||
| 数据概览 | AI对话、系统状态、数据统计 | 输入手机号可查用户 |
|
||||
| 数据接入 | 数据源、AI引擎、清洗、任务、血缘 | 真实MongoDB连接 |
|
||||
| 标签画像 | 标签、画像、人群圈选 | 按项目分类流量池 |
|
||||
| AI Agent | 渠道、打标、清洗、报告 | 飞书/企微对接 |
|
||||
| 数据市场 | 流量包、API服务 | 导出CSV、发送群 |
|
||||
|
||||
---
|
||||
|
||||
## 三、用户故事(从代码提取)
|
||||
|
||||
### US-001: 手机号查询
|
||||
```
|
||||
As a 运营人员
|
||||
I want 输入11位手机号
|
||||
So that 秒级获取用户画像(姓名、QQ、等级、标签)
|
||||
验收: parseIntent 识别手机号 → queryFullProfile 跨库查询
|
||||
```
|
||||
|
||||
### US-002: QQ号查询
|
||||
```
|
||||
As a 运营人员
|
||||
I want 输入QQ号
|
||||
So that 获取关联手机号及用户信息
|
||||
验收: parseIntent 识别QQ → KR_腾讯.QQ+手机 → KR.用户估值
|
||||
```
|
||||
|
||||
### US-003: 人群圈选
|
||||
```
|
||||
As a 营销人员
|
||||
I want 按项目选择流量池
|
||||
So that 查看真实用户列表
|
||||
验收: /api/crowd-pools 按存客宝/点了码/微博/QQ分类
|
||||
```
|
||||
|
||||
### US-004: 流量包导出
|
||||
```
|
||||
As a 运营人员
|
||||
I want 点击下载按钮
|
||||
So that 导出CSV格式用户数据
|
||||
验收: /api/traffic-packages?action=export → 前端Blob下载
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、MVP边界
|
||||
|
||||
```yaml
|
||||
已实现:
|
||||
- AI对话(手机/QQ/状态/RFM/高价值)
|
||||
- 5大模块前端页面
|
||||
- 25个API端点
|
||||
- MongoDB真实数据对接
|
||||
- 飞书/企微渠道配置
|
||||
|
||||
待完善:
|
||||
- 飞书机器人环境变量配置
|
||||
- Redis缓存层
|
||||
- 更多业务指标
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、关联文档
|
||||
|
||||
- [业务需求.md](./业务需求.md)
|
||||
- [成本.md](./成本.md)
|
||||
- [../2、架构/核心架构逻辑.md](../2、架构/核心架构逻辑.md)
|
||||
BIN
开发文档/4、前端/.DS_Store
vendored
Normal file
BIN
开发文档/4、前端/.DS_Store
vendored
Normal file
Binary file not shown.
Reference in New Issue
Block a user