diff --git a/.DS_Store b/.DS_Store index 8aca9b9..2ae3a4c 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/app/api/ai-chat/route.ts b/app/api/ai-chat/route.ts index 5763139..7d957df 100644 --- a/app/api/ai-chat/route.ts +++ b/app/api/ai-chat/route.ts @@ -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 { +// 处理 AI 聊天(先拆解意图再执行,避免直接返回「未找到」) +async function processChat(message: string): Promise { 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 { 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 { 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 { `📄 总记录数: ${(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 { return { role: "assistant", content, - data: results + data: results, + thinking: intent.thinking || "意图: RFM 分布 → 按 user_level 聚合" } } @@ -273,18 +323,27 @@ async function processChat(message: string): Promise { .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 { `📱 手机 / 💬 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 } } } diff --git a/app/api/api-keys/route.ts b/app/api/api-keys/route.ts index 536aedd..b3baab4 100644 --- a/app/api/api-keys/route.ts +++ b/app/api/api-keys/route.ts @@ -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(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: '密钥已删除' }) diff --git a/app/data-market/api/keys/page.tsx b/app/data-market/api/keys/page.tsx index 8201f89..5565b1c 100644 --- a/app/data-market/api/keys/page.tsx +++ b/app/data-market/api/keys/page.tsx @@ -203,8 +203,10 @@ const MOCK_API_KEYS: APIKey[] = [ ] export default function APIKeysPage() { - const [apiKeys, setApiKeys] = useState(MOCK_API_KEYS) + const [apiKeys, setApiKeys] = useState([]) 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>(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() {

创建和管理第三方系统接入密钥

- @@ -434,6 +486,17 @@ export default function APIKeysPage() { 管理所有API密钥,配置权限和额度 + {loading ? ( +
加载中...
+ ) : apiKeys.length === 0 ? ( +
+

暂无API密钥

+ +
+ ) : ( @@ -539,11 +602,12 @@ export default function APIKeysPage() { - ))} - -
-
- + ))} + + + )} + + {/* ==================== 弹窗 ==================== */} @@ -593,7 +657,9 @@ export default function APIKeysPage() { - + diff --git a/app/data-market/api/page.tsx b/app/data-market/api/page.tsx index 71a63f2..b3f9785 100644 --- a/app/data-market/api/page.tsx +++ b/app/data-market/api/page.tsx @@ -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(MOCK_API_KEYS) + const [apiKeys, setApiKeys] = useState([]) + const [loading, setLoading] = useState(true) + const [creating, setCreating] = useState(false) const [callLogs] = useState(MOCK_CALL_LOGS) // 弹窗状态 @@ -579,9 +581,22 @@ export default function APIServicePage() { // 显示/隐藏密钥 const [visibleKeys, setVisibleKeys] = useState>(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() { + {loading ? ( +
加载中...
+ ) : apiKeys.length === 0 ? ( +
+ 暂无API密钥,请到 + API密钥管理 + 创建 +
+ ) : ( @@ -950,6 +1008,7 @@ export default function APIServicePage() { ))}
+ )}
@@ -1330,7 +1389,9 @@ export default function APIServicePage() { - + diff --git a/app/page.tsx b/app/page.tsx index 63000fc..9c9210d 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -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) { diff --git a/docker-compose.yml b/docker-compose.yml index 2f99fcf..e2e2b02 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/开发文档/.DS_Store b/开发文档/.DS_Store new file mode 100644 index 0000000..01b7a5c Binary files /dev/null and b/开发文档/.DS_Store differ diff --git a/开发文档/10、项目管理/开发进度.md b/开发文档/10、项目管理/开发进度.md index 6664734..38f364a 100644 --- a/开发文档/10、项目管理/开发进度.md +++ b/开发文档/10、项目管理/开发进度.md @@ -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 | diff --git a/开发文档/1、需求/20260308api服务功能.md b/开发文档/1、需求/20260308api服务功能.md new file mode 100644 index 0000000..8732280 --- /dev/null +++ b/开发文档/1、需求/20260308api服务功能.md @@ -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 服务与开放接口共用同一数据源,无重复录入 + +--- + +![](images/2026-03-08-09-03-18.png) \ No newline at end of file diff --git a/开发文档/1、需求/images/2026-03-08-09-03-18.png b/开发文档/1、需求/images/2026-03-08-09-03-18.png new file mode 100644 index 0000000..98b1754 Binary files /dev/null and b/开发文档/1、需求/images/2026-03-08-09-03-18.png differ diff --git a/开发文档/1、需求/核心需求提取.md b/开发文档/1、需求/核心需求提取.md deleted file mode 100644 index bbd3617..0000000 --- a/开发文档/1、需求/核心需求提取.md +++ /dev/null @@ -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) diff --git a/开发文档/4、前端/.DS_Store b/开发文档/4、前端/.DS_Store new file mode 100644 index 0000000..fcba9c0 Binary files /dev/null and b/开发文档/4、前端/.DS_Store differ