kiku-agent 1.0.11 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +300 -495
- package/package.json +12 -11
- package/public/fonts/GoogleSans-Bold.ttf +0 -0
- package/public/fonts/GoogleSans-Bold.woff2 +0 -0
- package/public/fonts/GoogleSans-BoldItalic.ttf +0 -0
- package/public/fonts/GoogleSans-Italic.ttf +0 -0
- package/public/fonts/GoogleSans-Latin-400.woff2 +0 -0
- package/public/fonts/GoogleSans-Latin-500.woff2 +0 -0
- package/public/fonts/GoogleSans-Latin-600.woff2 +0 -0
- package/public/fonts/GoogleSans-Latin-700.woff2 +0 -0
- package/public/fonts/GoogleSans-Medium.ttf +0 -0
- package/public/fonts/GoogleSans-Medium.woff2 +0 -0
- package/public/fonts/GoogleSans-Regular.ttf +0 -0
- package/public/fonts/GoogleSans-Regular.woff2 +0 -0
- package/public/fonts/GoogleSans-Thai-400.woff2 +0 -0
- package/public/fonts/GoogleSans-Thai-500.woff2 +0 -0
- package/public/fonts/GoogleSans-Thai-600.woff2 +0 -0
- package/public/fonts/GoogleSans-Thai-700.woff2 +0 -0
- package/public/fonts/NotoSansThai-Bold.woff2 +0 -0
- package/public/fonts/NotoSansThai-Regular.woff2 +0 -0
- package/public/fonts/google-sans-official.css +900 -0
- package/public/images/0040jbadgy1id6am4yxqfj61s82dn1kx02.jpg +0 -0
- package/public/images/0040jbadgy1id6am708pbj62c0340qv602.jpg +0 -0
- package/public/images/0040jbadgy1id6amb81uqj62c0340x6q02.jpg +0 -0
- package/public/images/0040jbadgy1id6amcqte3j62c0340qv602.jpg +0 -0
- package/public/images/0040jbadgy1id6amdg3xcj62b232wqv502.jpg +0 -0
- package/public/images/0040jbadgy1id6amexv23j620v2pau0x02.jpg +0 -0
- package/public/images/0040jbadly8icq3if7twwj60qy0qy0uc02.jpg +0 -0
- package/public/images/008ET7Legy1ig9s89nri5j30i20n1gn4.jpg +0 -0
- package/public/images/008ET7Legy1ig9s8a2lmlj30i20ny409.jpg +0 -0
- package/public/images/008ET7Legy1ig9s8ay1hsj30i20nk3zv.jpg +0 -0
- package/public/images/008ET7Legy1ig9s8bab6sj30i20o7go1.jpg +0 -0
- package/public/images/008ET7Legy1ig9s8blqsxj30i20oiq4r.jpg +0 -0
- package/public/images/008ET7Legy1ig9s8cbxkzj30i20mxmzh.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-399915.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-424041.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-588166.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-672198.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-700522.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-839722.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026825-460586.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/263202694-689407.jpg +0 -0
- package/public/images/douyin-update//346/212/226/351/237/2632026820-399915.mp4 +0 -0
- package/public/images/douyin-update//346/212/226/351/237/2632026820-424041.mp4 +0 -0
- package/public/images/douyin-update//346/212/226/351/237/2632026820-588166.mp4 +0 -0
- package/public/images/douyin-update//346/212/226/351/237/2632026820-672198.mp4 +0 -0
- package/public/images/douyin-update//346/212/226/351/237/2632026820-700522.mp4 +0 -0
- package/public/images/douyin-update//346/212/226/351/237/2632026820-839722.mp4 +0 -0
- package/public/images/douyin-update//346/212/226/351/237/2632026825-460586.mp4 +0 -0
- package/public/images/douyin-update//346/212/226/351/237/263202694-689407.mp4 +0 -0
- package/public/images/general/0040jbadgy1id6am4yxqfj61s82dn1kx02.jpg +0 -0
- package/public/images/general/0040jbadgy1id6am708pbj62c0340qv602.jpg +0 -0
- package/public/images/general/0040jbadgy1id6amb81uqj62c0340x6q02.jpg +0 -0
- package/public/images/general/0040jbadgy1id6amcqte3j62c0340qv602.jpg +0 -0
- package/public/images/general/0040jbadgy1id6amdg3xcj62b232wqv502.jpg +0 -0
- package/public/images/general/0040jbadgy1id6amexv23j620v2pau0x02.jpg +0 -0
- package/public/images/general/0040jbadly8icq3if7twwj60qy0qy0uc02.jpg +0 -0
- package/public/images/general/008ET7Legy1ig9s89nri5j30i20n1gn4.jpg +0 -0
- package/public/images/general/008ET7Legy1ig9s8a2lmlj30i20ny409.jpg +0 -0
- package/public/images/general/008ET7Legy1ig9s8ay1hsj30i20nk3zv.jpg +0 -0
- package/public/images/general/008ET7Legy1ig9s8bab6sj30i20o7go1.jpg +0 -0
- package/public/images/general/008ET7Legy1ig9s8blqsxj30i20oiq4r.jpg +0 -0
- package/public/images/general/008ET7Legy1ig9s8cbxkzj30i20mxmzh.jpg +0 -0
- package/public/images/general/jujingyi_portrait.jpg +0 -0
- package/public/images/jujingyi_portrait.jpg +0 -0
- package/public/images/series/a-millenniums-wait/poster.jpg +0 -0
- package/public/images/series/beauty-of-resilience/poster.jpg +0 -0
- package/public/images/series/detective-samoyeds/poster.jpg +0 -0
- package/public/images/series/everyday/poster.jpg +0 -0
- package/public/images/series/fu-rong-ancient-painting/poster.jpg +0 -0
- package/public/images/series/in-a-class-of-her-own/poster.jpg +0 -0
- package/public/images/series/in-blossom/poster.jpg +0 -0
- package/public/images/series/legend-of-yunxi/poster.jpg +0 -0
- package/public/images/series/lets-fight/poster-2.jpg +0 -0
- package/public/images/series/lets-fight/poster.jpg +0 -0
- package/public/images/series/love-emergency/poster.jpg +0 -0
- package/public/images/series/love-under-the-full-moon/poster.jpg +0 -0
- package/public/images/series/money-bastion/poster.jpg +0 -0
- package/public/images/series/mr-swimmer/poster.jpg +0 -0
- package/public/images/series/novoland-castle-in-the-sky/poster.jpg +0 -0
- package/public/images/series/please-give-me-a-pair-of-wings/poster.jpg +0 -0
- package/public/images/series/rebirth-for-you/poster.jpg +0 -0
- package/public/images/series/sword-and-fairy-4/poster.jpg +0 -0
- package/public/images/series/the-blooms-at-ruyi-pavilion/poster.jpg +0 -0
- package/public/images/series/the-eternal-fragrance/poster-2.jpg +0 -0
- package/public/images/series/the-eternal-fragrance/poster.jpg +0 -0
- package/public/images/series/the-fallen-flowers/poster.jpg +0 -0
- package/public/images/series/the-legend-of-white-snake/poster.jpg +0 -0
- package/public/images/series/veil-of-shadows/poster.jpg +0 -0
- package/public/images/series/wanhua-sin/poster-2.jpg +0 -0
- package/public/images/series/wanhua-sin/poster.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj10yogj62u03s0npe02.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj2pdvyj61li25jkjl02.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj71177j62672w9e8302.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj7twpfj62192197rw02.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj8jfu8j62c03401kx02.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj9dhdsj622k2l77wh02.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkja31s4j62bw3407r802.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjcf331j61402ey1kx02.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjdx8gqj62602pihdt02.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjfvxnzj62432n57wh02.jpg +0 -0
- package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixniv2yj627y4fxb2b02.jpg +0 -0
- package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixsvbdaj625k2vfu1102.jpg +0 -0
- package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixxxdaoj62c03501l202.jpg +0 -0
- package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixzeef9j62c0340npd02.jpg +0 -0
- package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3iy0vs6tj626t4rbhdu02.jpg +0 -0
- package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3iy21wwuj62bt34hx6q02.jpg +0 -0
- package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly4wr85j62z629y7wh02.jpg +0 -0
- package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly67oilj623u2t5npd02.jpg +0 -0
- package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly9qtw8j62c0340u0x02.jpg +0 -0
- package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlyem4m4j63402c01kz02.jpg +0 -0
- package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlygkoq4j627s2yeavd02.jpg +0 -0
- package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlyjh0daj62bw340nm102.jpg +0 -0
- package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlylejlhj62bw3401kx02.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaiaa8b7fj627j2y67wh02.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaiagw3ezj61s02ebu0x02.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaiaivng3j61rv2ebx6p02.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaiajuixvj62bw340qv502.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaiapyartj62bw340hdt02.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaibkwu4ij61vy2iq1kx02.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaic1fvkpj61u92eo1ky02.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaic3cruhj62822ywb2902.jpg +0 -0
- package/public/manifest.json +25 -0
- package/public/mcp-logo-128.png +0 -0
- package/public/mcp-logo-64.png +0 -0
- package/public/mcp-logo.svg +16 -0
- package/server/agent/agentCore.js +542 -74
- package/server/agent/appPaths.js +1 -0
- package/server/agent/audioPlayer.js +4 -2
- package/server/agent/citations.js +294 -0
- package/server/agent/clarificationAnalytics.js +477 -0
- package/server/agent/clarifyingQuestions.js +1089 -0
- package/server/agent/cli.js +549 -125
- package/server/agent/commandFeatures.js +1 -0
- package/server/agent/commandRegistry.js +946 -879
- package/server/agent/commands.js +546 -272
- package/server/agent/config.js +581 -412
- package/server/agent/contextCompactor.js +2 -2
- package/server/agent/debate.js +235 -0
- package/server/agent/eventBus.js +3 -0
- package/server/agent/feedbackLearning.js +217 -0
- package/server/agent/llmClient.js +512 -394
- package/server/agent/mcpClient.js +182 -32
- package/server/agent/memory.js +283 -224
- package/server/agent/modelManager.js +79 -39
- package/server/agent/profiles.js +178 -0
- package/server/agent/publicTools.js +22 -0
- package/server/agent/queryDetails.js +68 -8
- package/server/agent/quizData.js +398 -384
- package/server/agent/research.js +121 -79
- package/server/agent/sanitizer.js +104 -13
- package/server/agent/session.js +319 -11
- package/server/agent/subagent.js +8 -3
- package/server/agent/suggestionTui.js +294 -0
- package/server/agent/suggestions.js +69 -3
- package/server/agent/telemetry.js +119 -0
- package/server/agent/terminalImage.js +146 -27
- package/server/agent/theme.js +17 -0
- package/server/agent/toolRouter.js +16 -1
- package/server/agent/tools.js +424 -225
- package/server/agent/ui.js +486 -64
- package/server/agent/version.js +2 -4
- package/server/agent/webSearch.js +111 -11
- package/server/agent/youtubeAuth.js +24 -6
- package/server/apiRouter.js +4222 -0
- package/server/mcp/data/discography.js +25 -16
- package/server/mcp/data/gallery.js +308 -20
- package/server/mcp/data/profile.js +74 -44
- package/server/mcp/data/timeline.js +1 -1
- package/server/mcp/data/works.js +51 -17
- package/server/mcp/dataAdapter.js +5 -3
- package/server/mcp/handler.js +5 -11
- package/server/mcp/prompts.js +86 -0
- package/server/mcp/security.js +18 -15
- package/server/mcp/server.js +7 -2
- package/server/mcp/tools.js +66 -2
- package/server/web-chat/core/browserLauncher.js +18 -0
- package/server/web-chat/core/connection.js +194 -0
- package/server/web-chat/core/conversationContext.js +82 -0
- package/server/web-chat/core/directCatalogEngine.js +196 -0
- package/server/web-chat/core/dramaKnowledge.js +142 -0
- package/server/web-chat/core/errorHandler.js +51 -0
- package/server/web-chat/core/followUpEngine.js +182 -0
- package/server/web-chat/core/futureEventGuard.js +80 -0
- package/server/web-chat/core/memorySnapshot.js +12 -0
- package/server/web-chat/core/security.js +146 -0
- package/server/web-chat/core/toolInspector.js +52 -0
- package/server/web-chat/router.js +120 -0
- package/server/web-chat/routes/chatRoute.js +397 -0
- package/server/web-chat/routes/clarifyConfigRoute.js +98 -0
- package/server/web-chat/routes/clarifyRoute.js +83 -0
- package/server/web-chat/routes/compareRoute.js +79 -0
- package/server/web-chat/routes/exportRoute.js +59 -0
- package/server/web-chat/routes/feedbackRoute.js +57 -0
- package/server/web-chat/routes/newsRoute.js +33 -0
- package/server/web-chat/routes/quizRoute.js +11 -0
- package/server/web-chat/routes/staticRoute.js +103 -0
- package/server/web-chat/routes/statusRoute.js +52 -0
- package/server/web-chat/routes/suggestRoute.js +77 -0
- package/server/web-chat/routes/timelineRoute.js +34 -0
- package/server/web-chat/routes/verifyRoute.js +57 -0
- package/server/web-chat/server.js +116 -0
- package/server/web-chat/ui/htmlBuilder.js +91 -0
- package/server/web-chat/ui/previews/agentBubble.preview.html +134 -0
- package/server/web-chat/ui/scripts/agentBubbleSystem.js +320 -0
- package/server/web-chat/ui/scripts/client.js +8397 -0
- package/server/web-chat/ui/scripts/kikuDb.js +177 -0
- package/server/web-chat/ui/scripts/suggestionsData.js +118 -0
- package/server/web-chat/ui/scripts/webchatI18n.js +291 -0
- package/server/web-chat/ui/styles/main.css +8568 -0
- package/server/web-chat/ui/templates/body.html +796 -0
- package/server/webChatHtml.js +1 -0
- package/server/webChatServer.js +6 -0
- package/src/data/discographyData.js +892 -866
- package/src/data/galleryData.js +1805 -561
- package/src/data/profileData.js +79 -2
- package/src/data/socialUpdatesData.js +106 -47
- package/src/data/timelineData.js +1 -1
- package/src/data/worksData.js +25 -4
- package/tokens.css +97 -0
- package/server/agent/updateManager.js +0 -456
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { LLMClient, LMStudioClient } from '../../agent/llmClient.js';
|
|
5
|
+
import { KikuAgent } from '../../agent/agentCore.js';
|
|
6
|
+
import { AgentMemory } from '../../agent/memory.js';
|
|
7
|
+
import { ModelManager } from '../../agent/modelManager.js';
|
|
8
|
+
import { config } from '../../agent/config.js';
|
|
9
|
+
import { PUBLIC_REMOTE_TOOL_NAMES } from '../../agent/publicTools.js';
|
|
10
|
+
|
|
11
|
+
export const webAgentCache = new Map();
|
|
12
|
+
export let sharedWebMemory = new AgentMemory({ persistence: false });
|
|
13
|
+
export let currentAgent = null;
|
|
14
|
+
const MAX_WEB_AGENT_SESSIONS = 24;
|
|
15
|
+
|
|
16
|
+
export function setSharedWebMemory(mem) {
|
|
17
|
+
if (mem) sharedWebMemory = mem;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function setCurrentAgent(agent) {
|
|
21
|
+
if (agent) {
|
|
22
|
+
currentAgent = agent;
|
|
23
|
+
if (agent.memory) sharedWebMemory = agent.memory;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function normalizeWebSessionId(value) {
|
|
28
|
+
const normalized = String(value || '').trim().replace(/[^a-zA-Z0-9_-]+/g, '_').slice(0, 100);
|
|
29
|
+
return normalized || null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function credentialFingerprint(value) {
|
|
33
|
+
if (!value) return 'no-key';
|
|
34
|
+
return createHash('sha256').update(String(value)).digest('hex').slice(0, 12);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function computeSessionHash(messages) {
|
|
38
|
+
if (!Array.isArray(messages) || messages.length === 0) return '';
|
|
39
|
+
const payload = messages.map(m => (m.role || '') + ':' + (m.parts?.[0]?.content || m.text || '')).join('|');
|
|
40
|
+
return createHash('sha256').update(payload).digest('hex').slice(0, 16);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getWebAgentCacheKey({ sessionId, provider, model, apiKey }) {
|
|
44
|
+
if (!sessionId) return null;
|
|
45
|
+
return [sessionId, provider || 'lmstudio', model || 'default', credentialFingerprint(apiKey)].join(':');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function prepareWebAgent(agent, options = {}) {
|
|
49
|
+
await agent.ready();
|
|
50
|
+
if (Array.isArray(options.history) && options.history.length > 0) {
|
|
51
|
+
const agentMsgCount = agent.session?.messages?.length || 0;
|
|
52
|
+
const frontendMsgCount = options.history.length;
|
|
53
|
+
await agent.hydrateConversationHistory(options.history, {
|
|
54
|
+
sessionId: options.sessionId,
|
|
55
|
+
maxMessages: 24,
|
|
56
|
+
force: frontendMsgCount > agentMsgCount,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return agent;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function cacheWebAgentConnection(cacheKey, connection) {
|
|
63
|
+
if (!cacheKey) return;
|
|
64
|
+
webAgentCache.delete(cacheKey);
|
|
65
|
+
webAgentCache.set(cacheKey, connection);
|
|
66
|
+
while (webAgentCache.size > MAX_WEB_AGENT_SESSIONS) {
|
|
67
|
+
const oldestKey = webAgentCache.keys().next().value;
|
|
68
|
+
const oldest = webAgentCache.get(oldestKey);
|
|
69
|
+
oldest?.agent?.saveSession?.().catch(() => {});
|
|
70
|
+
const msgCount = oldest?.agent?.session?.messages?.length || 0;
|
|
71
|
+
if (msgCount === 0) {
|
|
72
|
+
const storageFile = oldest?.agent?.session?.storageFile;
|
|
73
|
+
if (storageFile) {
|
|
74
|
+
fs.promises.unlink(storageFile).catch(() => {});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
webAgentCache.delete(oldestKey);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function waitForLMStudio(baseUrl, timeoutMs = 8000) {
|
|
82
|
+
const client = new LMStudioClient({ baseUrl });
|
|
83
|
+
const start = Date.now();
|
|
84
|
+
while (Date.now() - start < timeoutMs) {
|
|
85
|
+
try {
|
|
86
|
+
const health = await client.checkConnection();
|
|
87
|
+
if (health.connected) return health;
|
|
88
|
+
} catch {}
|
|
89
|
+
await new Promise(r => setTimeout(r, 600));
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function attemptAutoStartLMStudio(baseUrl, model) {
|
|
95
|
+
const isWin = process.platform === 'win32';
|
|
96
|
+
const isMac = process.platform === 'darwin';
|
|
97
|
+
|
|
98
|
+
const candidates = isWin
|
|
99
|
+
? [
|
|
100
|
+
`${process.env.LOCALAPPDATA}\\LM Studio\\LM Studio.exe`,
|
|
101
|
+
`${process.env.LOCALAPPDATA}\\Programs\\LM Studio\\LM Studio.exe`,
|
|
102
|
+
'C:\\Program Files\\LM Studio\\LM Studio.exe'
|
|
103
|
+
]
|
|
104
|
+
: isMac
|
|
105
|
+
? ['/Applications/LM Studio.app/Contents/MacOS/LM Studio']
|
|
106
|
+
: ['lmstudio', '/usr/local/bin/lmstudio', '/opt/lmstudio/bin/lmstudio'];
|
|
107
|
+
|
|
108
|
+
const exe = candidates.find(p => {
|
|
109
|
+
if (p.includes('/') || p.includes('\\')) {
|
|
110
|
+
try { return fs.existsSync(p); } catch { return false; }
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (!exe) return null;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
let port = '1234';
|
|
119
|
+
try { const parsedUrl = new URL(baseUrl); if (parsedUrl.port) port = parsedUrl.port; } catch {}
|
|
120
|
+
|
|
121
|
+
const child = spawn(exe, isWin
|
|
122
|
+
? ['--model', model || 'google/gemma-4-e4b', '--serve', '--port', port]
|
|
123
|
+
: ['serve', '--model', model || 'google/gemma-4-e4b', '--port', port],
|
|
124
|
+
{ detached: true, stdio: 'ignore', windowsHide: true }
|
|
125
|
+
);
|
|
126
|
+
child.unref();
|
|
127
|
+
|
|
128
|
+
return await waitForLMStudio(baseUrl, 10000);
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function resolveAgentConnection(customOptions = {}) {
|
|
135
|
+
const provider = customOptions.provider || 'lmstudio';
|
|
136
|
+
const requestedModel = customOptions.model || config.providers?.[provider]?.model || 'google/gemma-4-e4b';
|
|
137
|
+
const baseUrl = customOptions.baseUrl || config.providers?.[provider]?.baseUrl || 'http://127.0.0.1:1234/v1';
|
|
138
|
+
const sessionId = normalizeWebSessionId(customOptions.sessionId);
|
|
139
|
+
// A remotely reachable endpoint must never inherit provider credentials from
|
|
140
|
+
// the server process. Only an explicitly supplied per-request key is used.
|
|
141
|
+
const apiKey = customOptions.apiKey || '';
|
|
142
|
+
|
|
143
|
+
const client = new LLMClient({
|
|
144
|
+
provider,
|
|
145
|
+
baseUrl,
|
|
146
|
+
model: requestedModel,
|
|
147
|
+
apiKey
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const modelManager = new ModelManager(client, { requestedModel });
|
|
151
|
+
let resolution = await modelManager.resolve({ requestedModel });
|
|
152
|
+
|
|
153
|
+
if (provider === 'lmstudio' && resolution.state === 'server_offline' && process.env.NODE_ENV !== 'test' && !process.execArgv.includes('--test')) {
|
|
154
|
+
const started = await attemptAutoStartLMStudio(baseUrl, requestedModel);
|
|
155
|
+
if (started) {
|
|
156
|
+
resolution = await modelManager.resolve({ requestedModel });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (resolution.connected && resolution.selectedModel) {
|
|
161
|
+
const agent = new KikuAgent({
|
|
162
|
+
client,
|
|
163
|
+
thinkingMode: customOptions.thinking || 'show',
|
|
164
|
+
memory: new AgentMemory({ persistence: false }),
|
|
165
|
+
sessionId,
|
|
166
|
+
sessionPersistence: false,
|
|
167
|
+
allowedToolNames: PUBLIC_REMOTE_TOOL_NAMES,
|
|
168
|
+
});
|
|
169
|
+
await prepareWebAgent(agent, { history: customOptions.history, sessionId });
|
|
170
|
+
const connection = {
|
|
171
|
+
ready: true,
|
|
172
|
+
agent,
|
|
173
|
+
client,
|
|
174
|
+
provider,
|
|
175
|
+
model: resolution.selectedModel,
|
|
176
|
+
models: resolution.models || [],
|
|
177
|
+
usedFallback: resolution.usedFallback || false,
|
|
178
|
+
resolution,
|
|
179
|
+
contentHash: computeSessionHash(agent.session?.messages || [])
|
|
180
|
+
};
|
|
181
|
+
return connection;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
ready: false,
|
|
186
|
+
client,
|
|
187
|
+
provider,
|
|
188
|
+
model: requestedModel,
|
|
189
|
+
models: resolution.models || [],
|
|
190
|
+
state: resolution.state,
|
|
191
|
+
error: resolution.error || 'Server offline',
|
|
192
|
+
resolution
|
|
193
|
+
};
|
|
194
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const FOLLOW_UP_PATTERNS = [
|
|
2
|
+
/^(?:แล้ว)?ทำไม(?:ถึง)?(?:น่า|จึง)?/i,
|
|
3
|
+
/^(?:แล้ว)?(?:เรื่อง|งาน|อัน|สิ่ง|คน)(?:นี้|นั้น)/i,
|
|
4
|
+
/^(?:เขา|เธอ|มัน|นี่|นั่น)(?:\s|$)/i,
|
|
5
|
+
/^(?:น่า)?จับตามอง(?:อย่างไร|ยังไง|ไหม|มั้ย)?[?.!]?$/i,
|
|
6
|
+
/^(?:why|how|what)\s+(?:is|are|does|do|about)\s+(?:it|this|that|they|she|he)\b/i,
|
|
7
|
+
/^(?:why|how)\s+(?:so|is that)\b/i,
|
|
8
|
+
/^(?:为什么|为何|怎么|这个|那个|她|他|它)/
|
|
9
|
+
,/^(?:ดำเนินการต่อ|ทำต่อ|ต่อเลย|ต่อไป|ช่วย(?:วิเคราะห์|ขยาย|เจาะลึก)(?:ต่อ)?|continue|go on|proceed)(?:\s|:|$)/i
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const DOMAIN_PATTERNS = {
|
|
13
|
+
fashion_event: /(?:vogue|gala|red\s*carpet|fashion|แฟชั่น|กาล่า|พรมแดง|งานแฟชั่น|盛典|红毯|时尚)/i,
|
|
14
|
+
drama: /(?:ซีรีส์|ละคร|ภาพยนตร์|หนัง|นักแสดง|บทบาท|ตัวละคร|in\s*blossom|yunxi|ฮวาเจียน|หยุนซี|剧|电视剧|电影|角色)/i,
|
|
15
|
+
music: /(?:เพลง|อัลบั้ม|ดนตรี|ost|single|concert|คอนเสิร์ต|music|song|歌曲|音乐|演唱会)/i,
|
|
16
|
+
brand: /(?:แบรนด์|พรีเซนเตอร์|ambassador|endorsement|versace|luxury|代言|品牌)/i,
|
|
17
|
+
awards: /(?:รางวัล|เกียรติยศ|award|ceremony|获奖|奖项)/i,
|
|
18
|
+
profile: /(?:ประวัติ|จวีจิ้งอี|ju\s*jingyi|kiku|鞠婧祎|profile|who\s+is)/i
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function messageText(message) {
|
|
22
|
+
if (!message || typeof message !== 'object') return '';
|
|
23
|
+
if (typeof message.text === 'string') return message.text;
|
|
24
|
+
if (typeof message.content === 'string') return message.content;
|
|
25
|
+
if (Array.isArray(message.parts)) {
|
|
26
|
+
return message.parts.map(part => part?.content || part?.text || '').join(' ');
|
|
27
|
+
}
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function cleanHistoryText(value) {
|
|
32
|
+
return String(value || '')
|
|
33
|
+
.replace(/\[SYSTEM (?:DIRECTIVE|INSTRUCTION)[\s\S]*?\]/gi, ' ')
|
|
34
|
+
.replace(/\[SUGGESTED_FOLLOW_UPS\][\s\S]*?\[\/SUGGESTED_FOLLOW_UPS\]/gi, ' ')
|
|
35
|
+
.replace(/\s+/g, ' ')
|
|
36
|
+
.trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function detectConversationDomain(value) {
|
|
40
|
+
const text = String(value || '');
|
|
41
|
+
for (const [domain, pattern] of Object.entries(DOMAIN_PATTERNS)) {
|
|
42
|
+
if (pattern.test(text)) return domain;
|
|
43
|
+
}
|
|
44
|
+
return 'general';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function isContextDependentQuery(query) {
|
|
48
|
+
const text = String(query || '').trim();
|
|
49
|
+
if (!text) return false;
|
|
50
|
+
const explicitContinuation = /^(?:ดำเนินการต่อ|ทำต่อ|ต่อเลย|ต่อไป|ช่วย(?:วิเคราะห์|ขยาย|เจาะลึก)(?:ต่อ)?|continue|go on|proceed)(?:\s|:|$)/i.test(text);
|
|
51
|
+
if (text.length > (explicitContinuation ? 500 : 100)) return false;
|
|
52
|
+
return FOLLOW_UP_PATTERNS.some(pattern => pattern.test(text));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function resolveConversationContext(query, history = [], now = new Date()) {
|
|
56
|
+
const originalQuery = String(query || '').trim();
|
|
57
|
+
const recent = (Array.isArray(history) ? history : [])
|
|
58
|
+
.slice(-8)
|
|
59
|
+
.map(message => ({ role: message?.role, text: cleanHistoryText(messageText(message).slice(0, 6000)) }))
|
|
60
|
+
.filter(message => message.text);
|
|
61
|
+
const contextDependent = isContextDependentQuery(originalQuery);
|
|
62
|
+
const previousUser = [...recent].reverse().find(message => message.role === 'user');
|
|
63
|
+
const previousAssistant = [...recent].reverse().find(message => message.role === 'assistant' || message.role === 'bot');
|
|
64
|
+
const anchorSource = [previousUser?.text, previousAssistant?.text].filter(Boolean).join(' — ');
|
|
65
|
+
const anchor = anchorSource.slice(0, 1200);
|
|
66
|
+
const contextual = Boolean(contextDependent && anchor);
|
|
67
|
+
const domain = detectConversationDomain(contextual ? anchor : originalQuery);
|
|
68
|
+
const today = now.toISOString().slice(0, 10);
|
|
69
|
+
|
|
70
|
+
if (!contextual) {
|
|
71
|
+
return { originalQuery, contextual: false, anchor: '', domain, today, resolvedQuery: originalQuery };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const resolvedQuery = `[CONVERSATION TOPIC ANCHOR — priority over persona defaults]\n` +
|
|
75
|
+
`Current date: ${today}\n` +
|
|
76
|
+
`Previous topic: ${anchor}\n` +
|
|
77
|
+
`Current follow-up: ${originalQuery}\n` +
|
|
78
|
+
`Answer only the new request in the follow-up. Use the previous answer as context but do not copy or restate it unless a short recap is necessary. Do not switch the subject to Ju Jingyi, her biography, music, or dramas unless the previous topic or current follow-up explicitly requires it. If the reference is still ambiguous, ask one focused clarification question.\n` +
|
|
79
|
+
`[/CONVERSATION TOPIC ANCHOR]`;
|
|
80
|
+
|
|
81
|
+
return { originalQuery, contextual: true, anchor, domain, today, resolvedQuery };
|
|
82
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { executeTool } from '../../agent/tools.js';
|
|
2
|
+
import { makeInspectableToolPayload } from './toolInspector.js';
|
|
3
|
+
import { generateDynamicFollowUps } from './followUpEngine.js';
|
|
4
|
+
import { buildDramaSpecialistAnswer } from './dramaKnowledge.js';
|
|
5
|
+
|
|
6
|
+
export async function executeDirectCatalogKnowledge(query, _persona = 'default', options = {}) {
|
|
7
|
+
const toolsUsed = [];
|
|
8
|
+
const q = String(query || '').toLowerCase();
|
|
9
|
+
const conversationContext = options.conversationContext || null;
|
|
10
|
+
let answerParts = [];
|
|
11
|
+
|
|
12
|
+
const addToolExec = async (name, args) => {
|
|
13
|
+
try {
|
|
14
|
+
const t0 = Date.now();
|
|
15
|
+
const res = await executeTool(name, args);
|
|
16
|
+
const dur = Date.now() - t0;
|
|
17
|
+
toolsUsed.push({
|
|
18
|
+
name,
|
|
19
|
+
input: makeInspectableToolPayload(args),
|
|
20
|
+
result: makeInspectableToolPayload(res),
|
|
21
|
+
resultCount: Array.isArray(res?.results || res?.works || res?.songs || res?.brands || res?.awards)
|
|
22
|
+
? (res.results || res.works || res.songs || res.brands || res.awards).length
|
|
23
|
+
: 1,
|
|
24
|
+
durationMs: dur
|
|
25
|
+
});
|
|
26
|
+
return res;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const specialistAnswer = await buildDramaSpecialistAnswer(query, addToolExec);
|
|
33
|
+
if (specialistAnswer) answerParts.push(specialistAnswer);
|
|
34
|
+
|
|
35
|
+
if (!specialistAnswer && (q.includes('ประวัติ') || q.includes('who is') || q.includes('profile') || q.includes('slogan') || q.includes('ฉายา'))) {
|
|
36
|
+
const profile = await addToolExec('get_profile_and_fandom', {});
|
|
37
|
+
if (profile) {
|
|
38
|
+
answerParts.push(`### 🌸 ประวัติและข้อมูลส่วนตัว: จวีจิ้งอี (Ju Jingyi / 鞠婧祎)\n\n` +
|
|
39
|
+
`- **ชื่อภาษาจีน:** ${profile.profile?.nameCn || '鞠婧祎'} (${profile.profile?.pinyin || 'Jū Jìngyī'})\n` +
|
|
40
|
+
`- **ชื่อสากล/ฉายา:** Kiku / เสี่ยวจวี (小鞠) / สาวงาม 4,000 ปี\n` +
|
|
41
|
+
`- **วันเกิด:** ${profile.profile?.birthDate || '18 มิถุนายน 1994'}\n` +
|
|
42
|
+
`- **บ้านเกิด:** ${profile.profile?.birthPlace || 'เมืองสุ้ยหนิง มณฑลเสฉวน ประเทศจีน'}\n` +
|
|
43
|
+
`- **ชื่อกลุ่มแฟนคลับ (Fandom):** ${profile.fandom?.name || 'Miju (蜜橘 / ฮันนี่แทงเจอรีน)'}\n` +
|
|
44
|
+
`- **สโลแกนประจำตัว:** "${profile.fandom?.slogan || '鞠世无双,与祎同行'}" (หนึ่งเดียวในใต้หล้า เคียงคู่จิ้งอีตลอดไป)\n` +
|
|
45
|
+
`- **ความสามารถพิเศษ:** ขับร้อง, ไวโอลิน, เปียโน, กู่เจิง, เต้นรำ`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!specialistAnswer && (q.includes('七星彩') || q.includes('qi xiang cai') || q.includes('qi xing cai') || q.includes('ชีซิงไฉ่'))) {
|
|
50
|
+
await addToolExec('search_works', { query: '七星彩' });
|
|
51
|
+
answerParts.push(`### 🎬 ข้อมูลซีรีส์ 《七星彩》 (Qi Xing Cai / ชีซิงไฉ่)\n\n` +
|
|
52
|
+
`**《七星彩》** เป็นโปรเจกต์ซีรีส์จีนโบราณฟอร์มยักษ์ (ระดับ S+) ผลิตโดย **Youku (优酷) และ Stellar Media (恒星引力传媒)** ดัดแปลงจากนวนิยายชื่อดังของ **หมิงเยว่ตัง (明月珰)**\n\n` +
|
|
53
|
+
`#### 📖 เรื่องย่อและเนื้อหาหลัก:\n` +
|
|
54
|
+
`- เล่าเรื่องราวของ **'จี้เฉิง' (纪澄)** บุตรสาวตระกูลพ่อค้าที่ครอบครัวประสบเคราะห์กรรม ถูกบีบให้แต่งงาน นางจึงต้องใช้ความฉลาด ปฏิภาณไหวพริบ และมารยาเอาตัวรอด เดินทางไปพึ่งพาญาติในตระกูลขุนนางใหญ่เพื่อหาคู่ครองที่พึ่งพาได้\n` +
|
|
55
|
+
`- นางได้พบกับ **'เสิ่นเช่อ' (沈彻)** แม่ทัพหนุ่มผู้ชาญฉลาด ทั้งสองชิงไหวชิงพริบ ผลัดกันรุกผลัดกันรับจนก่อเกิดเป็นความผูกพันและร่วมมือกันกอบกู้วิกฤตตระกูลและบ้านเมือง\n\n` +
|
|
56
|
+
`#### 🔥 ทำไมชาวเน็ตถึงให้ความสนใจอย่างมาก (เหตุผลที่เป็นกระแส):\n` +
|
|
57
|
+
`1. **ค่ายผู้ผลิตระดับท็อป (Stellar Media / 恒星引力):** เป็นค่ายเดียวกับที่สร้างผลงานภาพสวยอลังการอย่าง *ของรักของข้า (Love Between Fairy and Devil)* และ *สื่อรักยี่สิบสี่เทศกาล (Fox Spirit Matchmaker)* ทำให้คอซีรีส์ตั้งตารองานภาพและคอสตูมเป็นอย่างมาก\n` +
|
|
58
|
+
`2. **กระแสข่าวลือแคสติ้งนักแสดง (Casting Rumors / 选角瓜):** บทนางเอก 'จี้เฉิง' ต้องเป็นหญิงงามล่มเมืองที่ทั้งสวย ฉลาด ร้ายลึก และมีเสน่ห์ จึงมีการปล่อยข่าวลือในโซเชียลจีน (Weibo, Douban, Bilibili) จับคู่กับนักแสดงหญิงแถวหน้าหลายคน รวมถึงมีชื่อของ **'จวีจิ้งอี' (Ju Jingyi)**, หลูอวี้เสี่ยว, ซ่งจู่เอ๋อร์ ฯลฯ ถูกนำมาพูดถึงและถกเถียงกันอย่างกว้างขวาง\n` +
|
|
59
|
+
`3. **สถานะปัจจุบัน:** ยังอยู่ในขั้นตอนการเตรียมการ/รอประกาศอย่างเป็นทางการ (ยังไม่มีการยืนยันรายชื่อนักแสดงทางการจากทาง Youku หรือสตูดิโอของศิลปิน)\n\n` +
|
|
60
|
+
`> ⚠️ **ข้อควรระวัง (Disambiguation):** 《七星彩》 เป็นคนละผลงานกับซีรีส์และเพลง 《千香》 (The Eternal Fragrance / พันกลิ่นบุปผา) ที่จวีจิ้งอีนำแสดงร่วมกับ ซ่งเวยหลง นะคะ`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!specialistAnswer && (q.includes('ซีรีส์') || q.includes('ละคร') || q.includes('หนัง') || q.includes('ผลงาน') || q.includes('ฮวาเจียนหลิง') || q.includes('หยุนซี') || q.includes('blossom') || q.includes('yunxi') || q.includes('fairy') || q.includes('บทบาท'))) {
|
|
64
|
+
const worksRes = await addToolExec('search_works', { query: q.replace(/(ซีรีส์|ละคร|หนัง|ผลงาน|เรื่อง|เด่น|ของ|จวีจิ้งอี)/g, '').trim() });
|
|
65
|
+
const works = worksRes?.works || [];
|
|
66
|
+
if (works.length > 0) {
|
|
67
|
+
let md = `### 🎬 ผลงานซีรีส์และภาพยนตร์ที่โดดเด่น (${works.length} เรื่อง)\n\n`;
|
|
68
|
+
works.slice(0, 15).forEach(w => {
|
|
69
|
+
md += `#### 🌟 **${w.titleTh || w.titleCn}** (${w.titleCn || ''})\n` +
|
|
70
|
+
`- **บทบาทที่ได้รับ:** ${w.role || w.character || 'นักแสดงนำ'}\n` +
|
|
71
|
+
`- **ปีที่ฉาย:** ${w.year || 'N/A'} • **ประเภท:** ${w.category || 'ซีรีส์โบราณ/โรแมนติก'}\n` +
|
|
72
|
+
`- **เรื่องย่อ:** ${w.synopsis || w.description || 'ผลงานซีรีส์ชื่อดังของจวีจิ้งอี'}\n` +
|
|
73
|
+
`- **ช่องทางรับชม:** ${w.streamingPlatform || w.platform || 'ไม่ระบุในแคตตาล็อก'}\n`;
|
|
74
|
+
if (w.youtubeSearchUrl) {
|
|
75
|
+
md += `- 🔗 [ดูตัวอย่างบน YouTube](${w.youtubeSearchUrl})\n\n`;
|
|
76
|
+
} else {
|
|
77
|
+
md += `\n`;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
answerParts.push(md);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!specialistAnswer && (q.includes('เพลง') || q.includes('อัลบั้ม') || q.includes('ost') || q.includes('everyday') || q.includes('don\'t touch') || q.includes('ร้อง'))) {
|
|
85
|
+
const discoRes = await addToolExec('search_discography', { query: q.replace(/(เพลง|อัลบั้ม|ost|ของ|จวีจิ้งอี)/g, '').trim() });
|
|
86
|
+
const songs = discoRes?.songs || [];
|
|
87
|
+
if (songs.length > 0) {
|
|
88
|
+
let md = `### 🎵 ผลงานเพลงและเพลงประกอบซีรีส์ (Discography - ${songs.length} เพลง)\n\n`;
|
|
89
|
+
songs.slice(0, 20).forEach(s => {
|
|
90
|
+
md += `- **${s.titleTh || s.titleCn}** (${s.titleCn || ''}) — *ปี ${s.year || ''}* [ประเภท: ${s.work || 'Single'}]\n`;
|
|
91
|
+
if (s.youtubeUrl) md += ` - 🔗 [ฟังบน YouTube](${s.youtubeUrl})\n`;
|
|
92
|
+
});
|
|
93
|
+
answerParts.push(md);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!specialistAnswer && (q.includes('แบรนด์') || q.includes('พรีเซนเตอร์') || q.includes('ambassador') || q.includes('versace') || q.includes('luxury'))) {
|
|
98
|
+
const brandsRes = await addToolExec('get_brand_endorsements', {});
|
|
99
|
+
const brands = brandsRes?.brands || [];
|
|
100
|
+
if (brands.length > 0) {
|
|
101
|
+
let md = `### 👑 บทบาท Brand Ambassador และพรีเซนเตอร์ระดับโลก (${brands.length} แบรนด์)\n\n`;
|
|
102
|
+
brands.slice(0, 15).forEach(b => {
|
|
103
|
+
md += `- **${b.brand}** — ตำแหน่ง: **${b.role}** (${b.category || 'Luxury'}) [ปี ${b.year || 'ปัจจุบัน'}]\n`;
|
|
104
|
+
});
|
|
105
|
+
answerParts.push(md);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!specialistAnswer && (q.includes('รางวัล') || q.includes('award') || q.includes('เกียรติยศ') || q.includes('weibo'))) {
|
|
110
|
+
const awardsRes = await addToolExec('get_awards', {});
|
|
111
|
+
const awards = awardsRes?.awards || [];
|
|
112
|
+
if (awards.length > 0) {
|
|
113
|
+
let md = `### 🏆 รางวัลและความสำเร็จสำคัญ (${awards.length} รางวัล)\n\n`;
|
|
114
|
+
awards.slice(0, 20).forEach(a => {
|
|
115
|
+
md += `- **${a.year || ''}**: ${a.award || a.title} (${a.ceremony || 'งานประกาศรางวัล'}) — *${a.category || ''}*\n`;
|
|
116
|
+
});
|
|
117
|
+
answerParts.push(md);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!specialistAnswer && (q.includes('ไทม์ไลน์') || q.includes('timeline') || q.includes('snh48') || q.includes('เส้นทาง') || q.includes('อดีต'))) {
|
|
122
|
+
const timelineRes = await addToolExec('get_career_timeline', {});
|
|
123
|
+
const events = timelineRes?.timeline || [];
|
|
124
|
+
if (events.length > 0) {
|
|
125
|
+
let md = `### 🌸 ไทม์ไลน์เส้นทางอาชีพสำคัญ (Career Timeline - ${events.length} เหตุการณ์)\n\n`;
|
|
126
|
+
events.slice(0, 20).forEach(e => {
|
|
127
|
+
md += `- **ปี ${e.year}**: ${e.title} — ${e.description || ''}\n`;
|
|
128
|
+
});
|
|
129
|
+
answerParts.push(md);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!specialistAnswer && (q.includes('บทบาท') || q.includes('ตัวละคร') || q.includes('character') || q.includes('role'))) {
|
|
134
|
+
const rolesRes = await addToolExec('get_character_roles', {});
|
|
135
|
+
const roles = rolesRes?.roles || [];
|
|
136
|
+
if (roles.length > 0) {
|
|
137
|
+
let md = `### 🎭 โครงสร้างบทบาทและตัวละครเด่น (${roles.length} บทบาท)\n\n`;
|
|
138
|
+
roles.slice(0, 15).forEach(r => {
|
|
139
|
+
md += `- **${r.nameTh || r.nameCn}** (${r.nameCn || ''}) จากเรื่อง *${r.drama || ''}* — บุคลิก: ${r.personality || r.archetype || 'ตัวเอก'}\n`;
|
|
140
|
+
});
|
|
141
|
+
answerParts.push(md);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Handle Continuation Intents (เช่น "ดำเนินการต่อ...", "เจาะลึกด้วยเครื่องมือ")
|
|
146
|
+
if (!specialistAnswer && (q.includes('ดำเนินการต่อ') || q.includes('ต่อยอด') || q.includes('เจาะลึก'))) {
|
|
147
|
+
if (q.includes('ซีรีส์') || q.includes('ละคร') || q.includes('ฮวาเจียน') || q.includes('หยุนซี')) {
|
|
148
|
+
if (!toolsUsed.some(t => t.name === 'search_works')) {
|
|
149
|
+
await addToolExec('search_works', { query: 'In Blossom ฮวาเจียนลิ่ง' });
|
|
150
|
+
}
|
|
151
|
+
if (!toolsUsed.some(t => t.name === 'get_character_roles')) {
|
|
152
|
+
await addToolExec('get_character_roles', {});
|
|
153
|
+
}
|
|
154
|
+
} else if (q.includes('เพลง') || q.includes('ost') || q.includes('ดนตรี')) {
|
|
155
|
+
if (!toolsUsed.some(t => t.name === 'search_discography')) {
|
|
156
|
+
await addToolExec('search_discography', { query: 'เพลง' });
|
|
157
|
+
}
|
|
158
|
+
if (!toolsUsed.some(t => t.name === 'get_music_videos')) {
|
|
159
|
+
await addToolExec('get_music_videos', { query: 'MV' });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (answerParts.length === 0) {
|
|
165
|
+
const shouldPreserveExternalTopic = conversationContext?.contextual || conversationContext?.domain === 'fashion_event';
|
|
166
|
+
if (shouldPreserveExternalTopic) {
|
|
167
|
+
const topic = String(conversationContext?.anchor || query || 'หัวข้อก่อนหน้า').slice(0, 360);
|
|
168
|
+
answerParts.push(`### อ้างอิงหัวข้อเดิม\n\n` +
|
|
169
|
+
`คำถามนี้ต่อเนื่องจาก **${topic}**\n\n` +
|
|
170
|
+
`ขณะนี้ระบบกำลังทำงานในโหมดฐานข้อมูลภายในและไม่มีข้อมูลสดที่เพียงพอสำหรับยืนยันรายละเอียดของหัวข้อนี้ จึงจะไม่เปลี่ยนไปตอบประวัติหรือผลงานของศิลปินแทน หากต้องการข้อสรุปที่แม่นยำ ควรตรวจสอบประกาศทางการล่าสุดของงานก่อนค่ะ`);
|
|
171
|
+
} else {
|
|
172
|
+
await addToolExec('get_profile_and_fandom', {});
|
|
173
|
+
const worksRes = await addToolExec('search_works', { featuredOnly: true });
|
|
174
|
+
let md = `### 🌸 ข้อมูลสรุป จวีจิ้งอี (Ju Jingyi / 鞠婧祎)\n\n`;
|
|
175
|
+
md += `จวีจิ้งอี เป็นศิลปิน นักร้อง และนักแสดงหญิงชั้นนำของประเทศจีน เจ้าของฉายา "สาวงาม 4,000 ปี" และ Global Brand Ambassador ของ Versace\n\n`;
|
|
176
|
+
md += `#### 🎬 ผลงานซีรีส์เด่น:\n`;
|
|
177
|
+
(worksRes?.works || []).slice(0, 4).forEach(w => {
|
|
178
|
+
md += `- **${w.titleTh || w.titleCn}** รับบทเป็น *${w.role || w.character}* (ปี ${w.year})\n`;
|
|
179
|
+
});
|
|
180
|
+
answerParts.push(md);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const headerNotice = `> 💡 **โหมดฐานข้อมูลอัจฉริยะ (Direct Catalog Knowledge Mode)**\n` +
|
|
185
|
+
`> *ระบบสืบค้นข้อมูลจาก Local Database ให้โดยตรงทันทีอย่างแม่นยำ (เปิด LM Studio เพื่อเปิดใช้งาน Generative AI)*\n\n`;
|
|
186
|
+
|
|
187
|
+
const finalMarkdown = headerNotice + answerParts.join('\n\n');
|
|
188
|
+
const suggestions = generateDynamicFollowUps({ query, answer: finalMarkdown, tools: toolsUsed });
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
answer: finalMarkdown,
|
|
192
|
+
thinking: 'ตรวจพบ LM Studio ออฟไลน์ -> เปิดใช้งาน Auto Direct Catalog Knowledge Engine...',
|
|
193
|
+
tools: toolsUsed,
|
|
194
|
+
suggestions
|
|
195
|
+
};
|
|
196
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
const WHITE_SNAKE_PATTERN = /(?:ตำนานนางพญางูขาว|นางพญางูขาว|the\s+legend\s+of\s+white\s+snake|新白娘子传奇|ไป๋ซู่เจิน|bai\s+suzhen)/i;
|
|
2
|
+
const STORY_PATTERN = /(?:เล่า|เรื่องราว|เรื่องย่อ|เกร็ด|story|storyline|synopsis|trivia)/i;
|
|
3
|
+
const COMPARISON_PATTERN = /(?:เปรียบเทียบ|เทียบ|พัฒนาการ|จุดเปลี่ยน|compare|comparison|evolution|breakthrough)/i;
|
|
4
|
+
const PAST_ERA_PATTERN = /(?:อดีต|ยุคไอดอล|snh48|ช่วงแรก|ก่อนหน้า|early|past|idol)/i;
|
|
5
|
+
|
|
6
|
+
export function detectDramaKnowledgeIntent(query) {
|
|
7
|
+
const text = String(query || '').trim();
|
|
8
|
+
if (WHITE_SNAKE_PATTERN.test(text) && STORY_PATTERN.test(text)) return 'white-snake-story';
|
|
9
|
+
if (COMPARISON_PATTERN.test(text) && /2024/.test(text) && PAST_ERA_PATTERN.test(text)) {
|
|
10
|
+
return 'acting-era-comparison';
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function assessDramaAnswerQuality(query, answer) {
|
|
16
|
+
const intent = detectDramaKnowledgeIntent(query);
|
|
17
|
+
if (!intent) return { applicable: false, valid: true, issues: [] };
|
|
18
|
+
|
|
19
|
+
const text = String(answer || '');
|
|
20
|
+
const issues = [];
|
|
21
|
+
const unsupported = /(?:ระดับแนวหน้า|ไม่มีใครเทียบได้|ก้าวข้ามจากไอดอล(?:ได้)?อย่างสมบูรณ์|ได้รับรางวัลมากมาย)/i;
|
|
22
|
+
if (unsupported.test(text)) issues.push('unsupported-career-claim');
|
|
23
|
+
if (/★\s*\d+(?:\.\d+)?\s*\/\s*10/.test(text)) issues.push('unattributed-rating');
|
|
24
|
+
|
|
25
|
+
if (intent === 'white-snake-story') {
|
|
26
|
+
if (!/(?:ไป๋ซู่เจิน|bai\s+suzhen).{0,100}(?:งูขาว|white[- ]snake spirit)/is.test(text)) {
|
|
27
|
+
issues.push('missing-bai-suzhen-identity');
|
|
28
|
+
}
|
|
29
|
+
if (!/(?:สวี่เซียน|xu\s+xian).{0,80}(?:มนุษย์ชาย|human man|human scholar|mortal)/is.test(text)) {
|
|
30
|
+
issues.push('missing-xu-xian-identity');
|
|
31
|
+
}
|
|
32
|
+
if (/(?:ตัวละครเอก|นางเอก|protagonist).{0,80}(?:มนุษย์สาว|human woman).{0,200}(?:พญางูขาว|white snake)/is.test(text)) {
|
|
33
|
+
issues.push('reversed-species-or-gender');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (intent === 'acting-era-comparison') {
|
|
38
|
+
if (!/In\s+Blossom/i.test(text) || !/Sword\s+and\s+Fairy\s+4/i.test(text)) {
|
|
39
|
+
issues.push('incomplete-2024-comparison');
|
|
40
|
+
}
|
|
41
|
+
if (!/(?:2016|2017|2018|2019)/.test(text)) issues.push('missing-past-work-evidence');
|
|
42
|
+
if (!/(?:ตัวตน|identity).{0,100}(?:หยางไฉ่เวย|yang\s+caiwei).{0,160}(?:รูปลักษณ์|ร่าง|appearance).{0,100}(?:ซ่างกวนจื่อ|shangguan\s+zhi)/is.test(text)) {
|
|
43
|
+
issues.push('imprecise-in-blossom-role');
|
|
44
|
+
}
|
|
45
|
+
if (/(?:สลับเล่น\s*2|เล่น\s*2\s*(?:บทบาท|คาแรคเตอร์)|dual-role performance)/i.test(text)) {
|
|
46
|
+
issues.push('false-dual-role-framing');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { applicable: true, valid: issues.length === 0, issues };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function bulletList(items = []) {
|
|
54
|
+
return items.filter(Boolean).map(item => `- ${item}`).join('\n');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function loadWork(addToolExec, workIdOrTitle) {
|
|
58
|
+
const result = await addToolExec('get_work_detail', { workIdOrTitle });
|
|
59
|
+
return result?.status === 'success' ? result.work : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function renderWhiteSnakeStory(work) {
|
|
63
|
+
const title = work.titleTh || work.titleEn || 'ตำนานนางพญางูขาว';
|
|
64
|
+
const facts = [
|
|
65
|
+
work.year ? `ออกอากาศในปี **${work.year}**` : '',
|
|
66
|
+
work.role ? `จวีจิ้งอีรับบท **${work.role}**` : '',
|
|
67
|
+
work.episodes ? `จำนวน **${work.episodes}**` : '',
|
|
68
|
+
work.platform ? `แพลตฟอร์มที่ระบุในแคตตาล็อก: **${work.platform}**` : ''
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const highlights = (work.highlights || []).slice(0, 3).map(item => `**เกร็ด:** ${item}`);
|
|
72
|
+
|
|
73
|
+
return `### ${title}
|
|
74
|
+
|
|
75
|
+
**เรื่องย่อที่ถูกต้อง:** ${work.synopsis}
|
|
76
|
+
|
|
77
|
+
จุดสำคัญที่ต้องไม่สลับกันคือ **ไป๋ซู่เจินเป็นวิญญาณ/ปีศาจงูขาวที่บำเพ็ญเพียรจนแปลงกายเป็นหญิงมนุษย์** ส่วน **สวี่เซียนเป็นมนุษย์ชาย** ความขัดแย้งจึงเกิดจากความรักข้ามเผ่าพันธุ์ การช่วยรักษาผู้คน และการเผชิญหน้ากับฝาไห่ ไม่ใช่เรื่องของมนุษย์สาวที่ไปพบ “พญางูขาว” เพศชายค่ะ
|
|
78
|
+
|
|
79
|
+
#### ข้อมูลจากแคตตาล็อก
|
|
80
|
+
|
|
81
|
+
${bulletList([...facts, ...highlights])}
|
|
82
|
+
|
|
83
|
+
#### มองในแง่การแสดง
|
|
84
|
+
|
|
85
|
+
บทไป๋ซู่เจินต้องประสานความอ่อนโยนของคนรักและผู้เยียวยาเข้ากับสถานะของสิ่งเหนือธรรมชาติ รวมถึงรับน้ำหนักฉากรัก การพลัดพราก และการเสียสละ อย่างไรก็ดี ข้อความนี้เป็น **การวิเคราะห์จากโครงสร้างบท** ไม่ใช่หลักฐานสำหรับคำตัดสินเชิงจัดอันดับหรือข้อสรุปเด็ดขาดเกี่ยวกับสถานะทางอาชีพค่ะ`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function renderActingEraComparison(works) {
|
|
89
|
+
const [inBlossom, swordFairy, novoland, detective, yunxi, whiteSnake] = works;
|
|
90
|
+
const missing = works.some(work => !work);
|
|
91
|
+
|
|
92
|
+
const sourceNote = missing
|
|
93
|
+
? '> หมายเหตุ: แคตตาล็อกบางรายการไม่พร้อมใช้งาน จึงวิเคราะห์เฉพาะรายการที่ค้นคืนได้ค่ะ\n\n'
|
|
94
|
+
: '';
|
|
95
|
+
|
|
96
|
+
return `### ข้อสรุปก่อน
|
|
97
|
+
|
|
98
|
+
ปี 2024 แสดงให้เห็น **การเพิ่มภาระของตัวละครต่อการขับเคลื่อนเรื่อง** มากกว่าการเปลี่ยนจาก “ภาพลักษณ์” ไปเป็น “การแสดงจริง” แบบฉับพลัน เพราะผลงานก่อนหน้านั้นก็มีบทโศกนาฏกรรม สืบสวน และตัวละครที่ต้องตัดสินใจหนักอยู่แล้ว จุดเปลี่ยนจึงควรอธิบายว่าเป็น **วิวัฒนาการต่อเนื่องของชนิดบทและพื้นที่การแสดง** ค่ะ
|
|
99
|
+
|
|
100
|
+
${sourceNote}| ช่วง | ตัวอย่างบทที่ยืนยันได้ | สิ่งที่บทเรียกร้อง |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| ระยะแรก 2016–2017 | **${novoland?.role || 'เสวี่ยเฟยซวง'}** ใน *${novoland?.titleEn || 'Novoland: The Castle in the Sky'}*; **${detective?.role || 'ซ่างกวนจื่อซู'}** ใน *${detective?.titleEn || 'Detective Samoyeds'}* | จากบทเจ้าหญิงโศกนาฏกรรมไปสู่สมาชิกทีมสืบสวน เป็นช่วงทดลองคาแรกเตอร์ที่ต่างกัน ไม่ได้มีแต่บทสดใสแบบไอดอล |
|
|
103
|
+
| ช่วงสร้างฐาน 2018–2019 | **${yunxi?.role || 'หานอวิ๋นซี'}** ใน *${yunxi?.titleEn || 'Legend of Yunxi'}*; **${whiteSnake?.role || 'ไป๋ซู่เจิน'}** ใน *${whiteSnake?.titleEn || 'The Legend of White Snake'}* | รับบทนำที่ต้องแบกเส้นเรื่องยาวขึ้น ทั้งการแพทย์/การเมือง ความรักข้ามเผ่าพันธุ์ และโศกนาฏกรรม |
|
|
104
|
+
| ปี 2024 | **${inBlossom?.role || 'ตัวตนหยางไฉ่เวยในร่างซ่างกวนจื่อ'}** ใน *${inBlossom?.titleEn || 'In Blossom'}*; **${swordFairy?.role || 'หานหลิงซา'}** ใน *${swordFairy?.titleEn || 'Sword and Fairy 4'}* | บทหนึ่งขับเคลื่อนการสืบคดีผ่านปัญหาเรื่องตัวตนและรูปลักษณ์ อีกบทใช้พลังทางกาย จังหวะผจญภัย และความสัมพันธ์แบบคณะ |
|
|
105
|
+
|
|
106
|
+
#### จุดเปลี่ยนที่เห็นได้จากตัวบท
|
|
107
|
+
|
|
108
|
+
- **จากตัวละครในวงเรื่อง ไปสู่ผู้ขับเคลื่อนกลไกเรื่อง:** งานปี 2024 ให้ตัวละครของเธอมีหน้าที่ลงมือสืบ ค้น และตัดสินใจต่อเนื่อง ไม่ได้มีไว้รองรับเส้นโรแมนติกอย่างเดียวค่ะ
|
|
109
|
+
- **เครื่องมือการแสดงหลากหลายขึ้น:** *In Blossom* เน้นความระแวดระวัง การซ่อนตัวตน และการอ่านคู่สนทนา ขณะที่ *Sword and Fairy 4* เน้นพลังงานทางกาย ความคล่องแคล่ว และจังหวะของทีมนักผจญภัยค่ะ
|
|
110
|
+
- **มีความต่อเนื่องมากกว่าการหักมุม:** หานอวิ๋นซีและซ่างกวนจื่อซูในผลงานก่อนปี 2024 ก็เป็นหญิงฉลาดที่แก้ปัญหา ส่วนไป๋ซู่เจินและเสวี่ยเฟยซวงก็มีน้ำหนักทางโศกนาฏกรรมอยู่แล้ว ปี 2024 จึงต่อยอดสิ่งเหล่านี้ ไม่ได้เริ่มจากศูนย์ค่ะ
|
|
111
|
+
- **ข้อควรระวังเรื่อง “สองบทบาท” ใน In Blossom:** จวีจิ้งอีปรากฏในรูปลักษณ์ของ **ซ่างกวนจื่อหลังเหตุเปลี่ยนใบหน้า/สลับชะตา แต่ตัวตนภายในคือหยางไฉ่เวย** จึงแม่นกว่าหากเรียกว่า “ตัวละครที่มีความไม่ตรงกันระหว่างตัวตนกับรูปลักษณ์” แทนการบอกว่าเธอสลับเล่นสองคนที่แยกจากกันตลอดเรื่องค่ะ
|
|
112
|
+
|
|
113
|
+
#### สิ่งที่คำตอบเดิมสรุปแรงเกินหลักฐาน
|
|
114
|
+
|
|
115
|
+
- คำยกย่องเชิงจัดอันดับและข้อสรุปเด็ดขาดเรื่องสถานะทางอาชีพต้องมีแหล่งอ้างอิงเฉพาะ จึงไม่ควรใช้เป็นข้อเท็จจริงลอย ๆ ค่ะ
|
|
116
|
+
- คะแนน 9.x/10 ในแคตตาล็อกไม่มีชื่อสำนักหรือเกณฑ์กำกับ จึงไม่ควรนำมาใช้พิสูจน์คุณภาพการแสดงค่ะ
|
|
117
|
+
- การวิเคราะห์ด้านแววตา ภาษากาย หรือคุณภาพฉากใดฉากหนึ่งควรอ้างฉากจริงหรือบทวิจารณ์ที่ตรวจสอบได้ ไม่ควรอนุมานจากเรื่องย่อเพียงอย่างเดียวค่ะ
|
|
118
|
+
|
|
119
|
+
**สรุป:** จุดเปลี่ยนในปี 2024 คือความซับซ้อนของ “หน้าที่ในเรื่อง” และวิธีรักษาตัวตนภายใต้สถานการณ์กดดัน มากกว่าการเปลี่ยนจากนักแสดงที่ขายภาพลักษณ์ไปเป็นนักแสดงคุณภาพแบบขาว–ดำค่ะ`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function buildDramaSpecialistAnswer(query, addToolExec) {
|
|
123
|
+
const intent = detectDramaKnowledgeIntent(query);
|
|
124
|
+
if (!intent) return null;
|
|
125
|
+
|
|
126
|
+
if (intent === 'white-snake-story') {
|
|
127
|
+
const work = await loadWork(addToolExec, 'the-legend-of-white-snake');
|
|
128
|
+
return work ? renderWhiteSnakeStory(work) : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const workIds = [
|
|
132
|
+
'in-blossom',
|
|
133
|
+
'sword-and-fairy-4',
|
|
134
|
+
'novoland-castle-in-the-sky',
|
|
135
|
+
'detective-samoyeds',
|
|
136
|
+
'legend-of-yunxi',
|
|
137
|
+
'the-legend-of-white-snake'
|
|
138
|
+
];
|
|
139
|
+
const works = [];
|
|
140
|
+
for (const workId of workIds) works.push(await loadWork(addToolExec, workId));
|
|
141
|
+
return renderActingEraComparison(works);
|
|
142
|
+
}
|