kiku-agent 1.2.0 → 1.3.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 +459 -407
- package/package.json +27 -3
- package/server/agent/agentCore.js +635 -267
- package/server/agent/appPaths.js +2 -0
- package/server/agent/catalogEntityResolver.js +629 -0
- package/server/agent/citations.js +0 -21
- package/server/agent/clarificationAnalytics.js +1 -1
- package/server/agent/clarifyingQuestions.js +286 -216
- package/server/agent/cli.js +528 -73
- package/server/agent/commandFeatures.js +123 -24
- package/server/agent/commandRegistry.js +128 -62
- package/server/agent/commands.js +1410 -95
- package/server/agent/config.js +253 -163
- package/server/agent/contextCompactor.js +239 -25
- package/server/agent/dynamicCatalog.js +192 -0
- package/server/agent/envBootstrap.js +46 -0
- package/server/agent/envManager.js +317 -0
- package/server/agent/eventBus.js +3 -3
- package/server/agent/feedbackLearning.js +0 -8
- package/server/agent/gpuMetrics.js +188 -55
- package/server/agent/ingestionWorker.js +339 -0
- package/server/agent/lanDiscovery.js +369 -0
- package/server/agent/llmClient.js +111 -39
- package/server/agent/lmStudioWakeup.js +144 -0
- package/server/agent/mcpClient.js +2 -2
- package/server/agent/memory.js +635 -494
- package/server/agent/modelConfig.js +487 -0
- package/server/agent/newsIntelligence.js +219 -0
- package/server/agent/newsWatch.js +85 -0
- package/server/agent/prompts.js +146 -0
- package/server/agent/publicTools.js +7 -0
- package/server/agent/queryDetails.js +62 -1
- package/server/agent/research.js +314 -35
- package/server/agent/routingInterceptor.js +77 -0
- package/server/agent/rumoredProjects.js +15 -0
- package/server/agent/sanitizer.js +117 -17
- package/server/agent/subagent.js +52 -9
- package/server/agent/suggestionTui.js +33 -13
- package/server/agent/suggestions.js +29 -13
- package/server/agent/telemetry.js +0 -11
- package/server/agent/terminalImage.js +0 -1
- package/server/agent/textToolProtocol.js +190 -0
- package/server/agent/tokenTracker.js +220 -0
- package/server/agent/toolRouter.js +362 -216
- package/server/agent/tools.js +1155 -194
- package/server/agent/ui.js +144 -41
- package/server/agent/vramService.js +572 -0
- package/server/agent/webSearch.js +2279 -273
- package/server/agent/youtubeAuth.js +82 -73
- package/server/agent/youtubeTranscript.js +209 -0
- package/server/apiRouter.js +383 -179
- package/server/mcp/data/brands.js +5 -5
- package/server/mcp/data/discography.js +56 -45
- package/server/mcp/data/gallery.js +9 -0
- package/server/mcp/data/index.js +1 -0
- package/server/mcp/data/industryIndex.js +251 -0
- package/server/mcp/data/profile.js +5 -5
- package/server/mcp/data/timeline.js +2 -2
- package/server/mcp/data/works.js +2 -2
- package/server/mcp/handler.js +0 -2
- package/server/mcp/prompts.js +42 -2
- package/server/mcp/resources.js +5 -1
- package/server/mcp/security.js +25 -14
- package/server/mcp/server.js +92 -3
- package/server/mcp/tools.js +20 -11
- package/server/web-chat/core/connection.js +218 -194
- package/server/web-chat/core/conversationContext.js +1 -1
- package/server/web-chat/core/directCatalogEngine.js +190 -184
- package/server/web-chat/core/followUpEngine.js +88 -11
- package/server/web-chat/core/security.js +7 -4
- package/server/web-chat/router.js +54 -17
- package/server/web-chat/routes/bilibiliRoute.js +34 -0
- package/server/web-chat/routes/chatRoute.js +488 -397
- package/server/web-chat/routes/clarifyConfigRoute.js +1 -2
- package/server/web-chat/routes/exportRoute.js +2 -1
- package/server/web-chat/routes/lanDiscoveryRoute.js +90 -0
- package/server/web-chat/routes/newsRoute.js +36 -6
- package/server/web-chat/routes/providerRoute.js +87 -0
- package/server/web-chat/routes/staticRoute.js +61 -13
- package/server/web-chat/routes/statusRoute.js +23 -2
- package/server/web-chat/routes/vramRoute.js +22 -0
- package/server/web-chat/server.js +135 -106
- package/server/web-chat/ui/htmlBuilder.js +8 -8
- package/server/web-chat/ui/previews/modelSettings.preview.html +41 -0
- package/server/web-chat/ui/scripts/agentBubbleSystem.js +0 -5
- package/server/web-chat/ui/scripts/client.js +10526 -8397
- package/server/web-chat/ui/scripts/webchatI18n.js +190 -138
- package/server/web-chat/ui/styles/main.css +7471 -4246
- package/server/web-chat/ui/templates/body.html +305 -179
- package/src/data/brandsData.js +29 -6
- package/src/data/discographyData.js +918 -900
- package/src/data/galleryData.js +66 -0
- package/src/data/industryIndexData.js +292 -0
- package/src/data/musicVideoData.js +48 -5
- package/src/data/profileData.js +15 -1
- package/src/data/timelineData.js +4 -3
- package/src/data/worksData.js +536 -535
- package/src/lib/vram/estimator.js +464 -0
- package/src/lib/vram/modelRegistry.js +387 -0
- package/tokens.css +33 -2
|
@@ -2,195 +2,201 @@ import { executeTool } from '../../agent/tools.js';
|
|
|
2
2
|
import { makeInspectableToolPayload } from './toolInspector.js';
|
|
3
3
|
import { generateDynamicFollowUps } from './followUpEngine.js';
|
|
4
4
|
import { buildDramaSpecialistAnswer } from './dramaKnowledge.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
import { resolveQueryEntities, detectPromotionIntent } from '../../agent/catalogEntityResolver.js';
|
|
6
|
+
|
|
7
|
+
export async function executeDirectCatalogKnowledge(query, _persona = 'default', options = {}) {
|
|
8
|
+
const toolsUsed = [];
|
|
9
|
+
const q = String(query || '').toLowerCase();
|
|
10
|
+
const conversationContext = options.conversationContext || null;
|
|
11
|
+
let answerParts = [];
|
|
12
|
+
|
|
12
13
|
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
|
-
}
|
|
14
|
+
try {
|
|
15
|
+
const t0 = Date.now();
|
|
16
|
+
const res = await executeTool(name, args);
|
|
17
|
+
const dur = Date.now() - t0;
|
|
18
|
+
toolsUsed.push({
|
|
19
|
+
name,
|
|
20
|
+
input: makeInspectableToolPayload(args),
|
|
21
|
+
result: makeInspectableToolPayload(res),
|
|
22
|
+
resultCount: Array.isArray(res?.results || res?.works || res?.songs || res?.brands || res?.awards)
|
|
23
|
+
? (res.results || res.works || res.songs || res.brands || res.awards).length
|
|
24
|
+
: 1,
|
|
25
|
+
durationMs: dur
|
|
26
|
+
});
|
|
27
|
+
return res;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
30
31
|
};
|
|
31
32
|
|
|
32
33
|
const specialistAnswer = await buildDramaSpecialistAnswer(query, addToolExec);
|
|
33
34
|
if (specialistAnswer) answerParts.push(specialistAnswer);
|
|
34
35
|
|
|
36
|
+
const entityRes = resolveQueryEntities(query);
|
|
37
|
+
|
|
35
38
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
39
|
+
const profile = await addToolExec('get_profile_and_fandom', {});
|
|
40
|
+
if (profile) {
|
|
41
|
+
answerParts.push(`### 🌸 ประวัติและข้อมูลส่วนตัว: จวีจิ้งอี (Ju Jingyi / 鞠婧祎)\n\n` +
|
|
42
|
+
`- **ชื่อภาษาจีน:** ${profile.profile?.nameCn || '鞠婧祎'} (${profile.profile?.pinyin || 'Jū Jìngyī'})\n` +
|
|
43
|
+
`- **ชื่อสากล/ฉายา:** Kiku / เสี่ยวจวี (小鞠) / สาวงาม 4,000 ปี\n` +
|
|
44
|
+
`- **วันเกิด:** ${profile.profile?.birthDate || '18 มิถุนายน 1994'}\n` +
|
|
45
|
+
`- **บ้านเกิด:** ${profile.profile?.birthPlace || 'เมืองสุ้ยหนิง มณฑลเสฉวน ประเทศจีน'}\n` +
|
|
46
|
+
`- **ชื่อกลุ่มแฟนคลับ (Fandom):** ${profile.fandom?.name || 'Miju (蜜橘 / ฮันนี่แทงเจอรีน)'}\n` +
|
|
47
|
+
`- **สโลแกนประจำตัว:** "${profile.fandom?.slogan || '鞠世无双,与祎同行'}" (หนึ่งเดียวในใต้หล้า เคียงคู่จิ้งอีตลอดไป)\n` +
|
|
48
|
+
`- **ความสามารถพิเศษ:** ขับร้อง, ไวโอลิน, เปียโน, กู่เจิง, เต้นรำ`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if (!specialistAnswer && (entityRes.hasBrand || q.includes('แบรนด์') || q.includes('พรีเซนเตอร์') || q.includes('ambassador') || q.includes('โฆษณา') || q.includes('โปรโมท') || q.includes('โปรโมต') || entityRes.isPromotion)) {
|
|
54
|
+
const brandQuery = entityRes.primaryBrand ? (entityRes.primaryBrand.nameEn || entityRes.primaryBrand.id) : undefined;
|
|
55
|
+
const brandsRes = await addToolExec('get_brand_endorsements', brandQuery ? { query: brandQuery } : {});
|
|
56
|
+
const brands = brandsRes?.brands || [];
|
|
57
|
+
if (brands.length > 0) {
|
|
58
|
+
if (entityRes.primaryBrand) {
|
|
59
|
+
const b = brands[0];
|
|
60
|
+
let md = `### 👑 ข้อมูลแบรนด์: ${b.brand} (${b.year || 'ปัจจุบัน'})\n\n` +
|
|
61
|
+
`- **ตำแหน่ง:** ${b.title || 'Brand Ambassador'}\n` +
|
|
62
|
+
`- **หมวดหมู่:** ${b.category || 'Luxury'}\n` +
|
|
63
|
+
`- **ไฮไลต์สำคัญ:** ${b.highlight || 'พรีเซนเตอร์ระดับโลกของจวีจิ้งอี'}\n`;
|
|
64
|
+
if (b.bilibiliSearchUrl) md += `- 🔗 [ค้นหาคลิปโปรโมทบน Bilibili](${b.bilibiliSearchUrl})\n`;
|
|
65
|
+
if (b.youtubeCommercialSearch) md += `- 🔗 [ค้นหาโฆษณาบน YouTube](${b.youtubeCommercialSearch})\n`;
|
|
66
|
+
answerParts.push(md);
|
|
67
|
+
} else {
|
|
68
|
+
let md = `### 👑 บทบาท Brand Ambassador และพรีเซนเตอร์ระดับโลก (${brands.length} แบรนด์)\n\n`;
|
|
69
|
+
brands.slice(0, 15).forEach(b => {
|
|
70
|
+
md += `- **${b.brand}** — ตำแหน่ง: **${b.title}** (${b.category || 'Luxury'}) [ปี ${b.year || 'ปัจจุบัน'}]\n`;
|
|
71
|
+
if (b.bilibiliSearchUrl) md += ` - 🔗 [คลิปบน Bilibili](${b.bilibiliSearchUrl})\n`;
|
|
72
|
+
});
|
|
73
|
+
answerParts.push(md);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if (!specialistAnswer && (entityRes.hasWork || q.includes('ซีรีส์') || q.includes('ละคร') || q.includes('หนัง') || q.includes('ผลงาน') || q.includes('บทบาท'))) {
|
|
80
|
+
const workQuery = entityRes.primaryWork ? (entityRes.primaryWork.nameCn || entityRes.primaryWork.nameEn) : q.replace(/(ซีรีส์|ละคร|หนัง|ผลงาน|เรื่อง|เด่น|ของ|จวีจิ้งอี)/g, '').trim();
|
|
81
|
+
const worksRes = await addToolExec('search_works', { query: workQuery });
|
|
82
|
+
if (worksRes?.status === 'rumored_project_info' && worksRes?.rumoredProject) {
|
|
83
|
+
const r = worksRes.rumoredProject;
|
|
84
|
+
answerParts.push(`### 🎬 ข้อมูลซีรีส์ 《${r.titleCn}》 (${r.titleEn})\n\n` +
|
|
85
|
+
`**《${r.titleCn}》** เป็นโปรเจกต์ซีรีส์ที่มีกระแสข่าวลือแคสติ้ง (${r.status}):\n\n` +
|
|
86
|
+
`#### 📖 รายละเอียดโปรเจกต์:\n` +
|
|
87
|
+
`- **ผู้ผลิต:** ${r.producer || 'Youku x Stellar Media'}\n` +
|
|
88
|
+
`- **ต้นฉบับ:** ${r.originalNovel || 'นวนิยายจีน'}\n` +
|
|
89
|
+
`- **เนื้อหา:** ${r.details || ''}\n\n` +
|
|
90
|
+
`#### 🔥 เหตุผลที่เป็นกระแส:\n` +
|
|
91
|
+
`${r.buzzReason || ''}\n\n` +
|
|
92
|
+
`> ⚠️ **ข้อควรระวัง (Disambiguation):** ${r.disambiguation || ''}`);
|
|
93
|
+
} else {
|
|
94
|
+
const works = worksRes?.works || [];
|
|
95
|
+
if (works.length > 0) {
|
|
96
|
+
let md = `### 🎬 ผลงานซีรีส์และภาพยนตร์ที่โดดเด่น (${works.length} เรื่อง)\n\n`;
|
|
97
|
+
works.slice(0, 15).forEach(w => {
|
|
98
|
+
md += `#### 🌟 **${w.titleTh || w.titleCn}** (${w.titleCn || ''})\n` +
|
|
99
|
+
`- **บทบาทที่ได้รับ:** ${w.role || w.character || 'นักแสดงนำ'}\n` +
|
|
100
|
+
`- **ปีที่ฉาย:** ${w.year || 'N/A'} • **ประเภท:** ${w.category || 'ซีรีส์โบราณ/โรแมนติก'}\n` +
|
|
101
|
+
`- **เรื่องย่อ:** ${w.synopsis || w.description || 'ผลงานซีรีส์ชื่อดังของจวีจิ้งอี'}\n` +
|
|
102
|
+
`- **ช่องทางรับชม:** ${w.streamingPlatform || w.platform || 'ไม่ระบุในแคตตาล็อก'}\n`;
|
|
103
|
+
if (w.youtubeSearchUrl) {
|
|
104
|
+
md += `- 🔗 [ดูตัวอย่างบน YouTube](${w.youtubeSearchUrl})\n\n`;
|
|
105
|
+
} else {
|
|
106
|
+
md += `\n`;
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
answerParts.push(md);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
if (!specialistAnswer && (entityRes.hasSong || q.includes('เพลง') || q.includes('อัลบั้ม') || q.includes('ost') || q.includes('ร้อง'))) {
|
|
116
|
+
const songQuery = entityRes.primarySong ? (entityRes.primarySong.nameCn || entityRes.primarySong.nameEn) : q.replace(/(เพลง|อัลบั้ม|ost|ของ|จวีจิ้งอี)/g, '').trim();
|
|
117
|
+
const discoRes = await addToolExec('search_discography', { query: songQuery });
|
|
118
|
+
const songs = discoRes?.songs || [];
|
|
119
|
+
if (songs.length > 0) {
|
|
120
|
+
let md = `### 🎵 ผลงานเพลงและเพลงประกอบซีรีส์ (Discography - ${songs.length} เพลง)\n\n`;
|
|
121
|
+
songs.slice(0, 20).forEach(s => {
|
|
122
|
+
md += `- **${s.titleTh || s.titleCn}** (${s.titleCn || ''}) — *ปี ${s.year || ''}* [ประเภท: ${s.work || 'Single'}]\n`;
|
|
123
|
+
if (s.youtubeUrl) md += ` - 🔗 [ฟังบน YouTube](${s.youtubeUrl})\n`;
|
|
124
|
+
});
|
|
125
|
+
answerParts.push(md);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
if (!specialistAnswer && (entityRes.hasAward || q.includes('รางวัล') || q.includes('award') || q.includes('เกียรติยศ'))) {
|
|
131
|
+
const awardsRes = await addToolExec('get_awards', {});
|
|
132
|
+
const awards = awardsRes?.awards || [];
|
|
133
|
+
if (awards.length > 0) {
|
|
134
|
+
let md = `### 🏆 รางวัลและความสำเร็จสำคัญ (${awards.length} รางวัล)\n\n`;
|
|
135
|
+
awards.slice(0, 20).forEach(a => {
|
|
136
|
+
md += `- **${a.year || ''}**: ${a.award || a.title} (${a.ceremony || 'งานประกาศรางวัล'}) — *${a.category || ''}*\n`;
|
|
137
|
+
});
|
|
138
|
+
answerParts.push(md);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if (!specialistAnswer && (entityRes.hasTimeline || q.includes('ไทม์ไลน์') || q.includes('timeline') || q.includes('snh48') || q.includes('เส้นทาง') || q.includes('อดีต'))) {
|
|
144
|
+
const timelineRes = await addToolExec('get_career_timeline', {});
|
|
145
|
+
const events = timelineRes?.timeline || [];
|
|
146
|
+
if (events.length > 0) {
|
|
147
|
+
let md = `### 🌸 ไทม์ไลน์เส้นทางอาชีพสำคัญ (Career Timeline - ${events.length} เหตุการณ์)\n\n`;
|
|
148
|
+
events.slice(0, 20).forEach(e => {
|
|
149
|
+
md += `- **ปี ${e.year}**: ${e.title} — ${e.description || ''}\n`;
|
|
150
|
+
});
|
|
151
|
+
answerParts.push(md);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
133
156
|
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
|
-
|
|
146
|
-
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
}
|
|
157
|
+
const rolesRes = await addToolExec('get_character_roles', {});
|
|
158
|
+
const roles = rolesRes?.roles || [];
|
|
159
|
+
if (roles.length > 0) {
|
|
160
|
+
let md = `### 🎭 โครงสร้างบทบาทและตัวละครเด่น (${roles.length} บทบาท)\n\n`;
|
|
161
|
+
roles.slice(0, 15).forEach(r => {
|
|
162
|
+
md += `- **${r.nameTh || r.nameCn}** (${r.nameCn || ''}) จากเรื่อง *${r.drama || ''}* — บุคลิก: ${r.personality || r.archetype || 'ตัวเอก'}\n`;
|
|
163
|
+
});
|
|
164
|
+
answerParts.push(md);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (answerParts.length === 0) {
|
|
169
|
+
const shouldPreserveExternalTopic = conversationContext?.contextual || conversationContext?.domain === 'fashion_event';
|
|
170
|
+
if (shouldPreserveExternalTopic) {
|
|
171
|
+
const topic = String(conversationContext?.anchor || query || 'หัวข้อก่อนหน้า').slice(0, 360);
|
|
172
|
+
answerParts.push(`### อ้างอิงหัวข้อเดิม\n\n` +
|
|
173
|
+
`คำถามนี้ต่อเนื่องจาก **${topic}**\n\n` +
|
|
174
|
+
`ขณะนี้ระบบกำลังทำงานในโหมดฐานข้อมูลภายในและไม่มีข้อมูลสดที่เพียงพอสำหรับยืนยันรายละเอียดของหัวข้อนี้ จึงจะไม่เปลี่ยนไปตอบประวัติหรือผลงานของศิลปินแทน หากต้องการข้อสรุปที่แม่นยำ ควรตรวจสอบประกาศทางการล่าสุดของงานก่อนค่ะ`);
|
|
175
|
+
} else {
|
|
176
|
+
await addToolExec('get_profile_and_fandom', {});
|
|
177
|
+
const worksRes = await addToolExec('search_works', { featuredOnly: true });
|
|
178
|
+
const brandRes = await addToolExec('get_brand_endorsements', { isGlobal: true });
|
|
179
|
+
const topBrand = brandRes?.brands?.[0]?.brand || 'Global Brand Ambassador';
|
|
180
|
+
let md = `### 🌸 ข้อมูลสรุป จวีจิ้งอี (Ju Jingyi / 鞠婧祎)\n\n`;
|
|
181
|
+
md += `จวีจิ้งอี เป็นศิลปิน นักร้อง และนักแสดงหญิงชั้นนำของประเทศจีน เจ้าของฉายา "สาวงาม 4,000 ปี" และตัวแทนภาพลักษณ์แบรนด์ระดับสากล (${topBrand})\n\n`;
|
|
182
|
+
md += `#### 🎬 ผลงานซีรีส์เด่น:\n`;
|
|
183
|
+
(worksRes?.works || []).slice(0, 4).forEach(w => {
|
|
184
|
+
md += `- **${w.titleTh || w.titleCn}** รับบทเป็น *${w.role || w.character}* (ปี ${w.year})\n`;
|
|
185
|
+
});
|
|
186
|
+
answerParts.push(md);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const headerNotice = `> 💡 **โหมดฐานข้อมูลอัจฉริยะ (Direct Catalog Knowledge Mode)**\n` +
|
|
191
|
+
`> *ระบบสืบค้นข้อมูลจาก Local Database ให้โดยตรงทันทีอย่างแม่นยำ (เปิด LM Studio เพื่อเปิดใช้งาน Generative AI)*\n\n`;
|
|
192
|
+
|
|
193
|
+
const finalMarkdown = headerNotice + answerParts.join('\n\n');
|
|
194
|
+
const suggestions = generateDynamicFollowUps({ query, answer: finalMarkdown, tools: toolsUsed });
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
answer: finalMarkdown,
|
|
198
|
+
thinking: 'ตรวจพบ LM Studio ออฟไลน์ -> เปิดใช้งาน Auto Direct Catalog Knowledge Engine...',
|
|
199
|
+
tools: toolsUsed,
|
|
200
|
+
suggestions
|
|
201
|
+
};
|
|
202
|
+
}
|
|
@@ -40,6 +40,40 @@ function extractSuggestionItems(raw) {
|
|
|
40
40
|
return items.slice(0, 4);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/** Collapse a suggestion to a comparable key: no emoji, punctuation, case or spacing noise.
|
|
44
|
+
* \p{M} keeps Thai vowels/tone marks intact (they are combining marks, not letters). */
|
|
45
|
+
export function normalizeSuggestionKey(s) {
|
|
46
|
+
return String(s || '')
|
|
47
|
+
.toLowerCase()
|
|
48
|
+
.replace(/[\p{Extended_Pictographic}\uFE0F]/gu, ' ')
|
|
49
|
+
.replace(/[^\p{L}\p{N}\p{M}]/gu, '')
|
|
50
|
+
.trim();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A suggestion is a duplicate when identical, or a long prefix of one already shown. */
|
|
54
|
+
export function isDuplicateSuggestionKey(key, seenKeys) {
|
|
55
|
+
if (!key || seenKeys.has(key)) return true;
|
|
56
|
+
for (const existing of seenKeys) {
|
|
57
|
+
if (key.length >= 18 && existing.length >= 18 && (key.startsWith(existing) || existing.startsWith(key))) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Drop suggestions whose key duplicates something in seenKeys (or earlier in the list). */
|
|
65
|
+
export function dedupeSuggestions(suggestions, seenKeys) {
|
|
66
|
+
const seen = seenKeys instanceof Set ? new Set(seenKeys) : new Set();
|
|
67
|
+
const out = [];
|
|
68
|
+
for (const item of Array.isArray(suggestions) ? suggestions : []) {
|
|
69
|
+
const key = normalizeSuggestionKey(item && (item.query || item.label));
|
|
70
|
+
if (isDuplicateSuggestionKey(key, seen)) continue;
|
|
71
|
+
seen.add(key);
|
|
72
|
+
out.push(item);
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
43
77
|
export function extractAgentFollowUpQuestions(text) {
|
|
44
78
|
if (!text || typeof text !== 'string') {
|
|
45
79
|
return { cleanText: text || '', suggestions: [] };
|
|
@@ -109,10 +143,10 @@ export function extractAgentFollowUpQuestions(text) {
|
|
|
109
143
|
}
|
|
110
144
|
}
|
|
111
145
|
|
|
112
|
-
return { cleanText, suggestions };
|
|
146
|
+
return { cleanText, suggestions: dedupeSuggestions(suggestions, new Set()) };
|
|
113
147
|
}
|
|
114
148
|
|
|
115
|
-
export function generateDynamicFollowUps({ query = '', answer = '', tools = [] } = {}) {
|
|
149
|
+
export function generateDynamicFollowUps({ query = '', answer = '', tools = [], recentKeys = null } = {}) {
|
|
116
150
|
const combined = (query + ' ' + answer).toLowerCase();
|
|
117
151
|
const suggestions = [];
|
|
118
152
|
|
|
@@ -141,9 +175,18 @@ export function generateDynamicFollowUps({ query = '', answer = '', tools = [] }
|
|
|
141
175
|
{ label: '🎵 เพลงคู่ ไม่ทุกข์ตรม (不苦)', query: 'เบื้องหลังเพลง 不苦 (Not Suffering) ร้องคู่กับ เถียนเจียรุ่ย' },
|
|
142
176
|
{ label: '📺 กำหนดการฉายและแพลตฟอร์ม', query: 'อัปเดตสถานะและแพลตฟอร์มการออกอากาศของ Veil of Shadows' }
|
|
143
177
|
);
|
|
144
|
-
} else if (combined.includes('
|
|
178
|
+
} else if (combined.includes('prsr') || combined.includes('pasha') || combined.includes('帕莎')) {
|
|
145
179
|
suggestions.push(
|
|
146
|
-
{ label: '
|
|
180
|
+
{ label: '👓 PRSR Global Ambassador', query: 'เจาะลึกบทบาท Global Brand Ambassador ของ PRSR (Pasha) และลุคไวรัลใน Milan Fashion Week' },
|
|
181
|
+
{ label: '📺 คลิปโปรโมท PRSR บน Bilibili', query: 'ค้นหาคลิปโปรโมทและเบื้องหลังแว่นตาแฟชั่น PRSR บน Bilibili' },
|
|
182
|
+
{ label: '👗 แบรนด์พรีเซนเตอร์ทั้งหมด', query: 'สรุปแบรนด์ที่จวีจิ้งอีเป็นพรีเซนเตอร์และแอมบาสเดอร์ในปัจจุบัน' }
|
|
183
|
+
);
|
|
184
|
+
} else if (combined.includes('versace') || combined.includes('แบรนด์') || combined.includes('แฟชั่น') || combined.includes('พรีเซนเตอร์') || combined.includes('โปรโมท') || combined.includes('โปรโมต') || combined.includes('commercial')) {
|
|
185
|
+
const brandTool = tools.find(t => t.name === 'get_brand_endorsements');
|
|
186
|
+
const firstBrand = brandTool?.result?.brands?.[0];
|
|
187
|
+
const brandTitle = firstBrand?.brand || 'Versace';
|
|
188
|
+
suggestions.push(
|
|
189
|
+
{ label: `💎 ${brandTitle}`, query: `เจาะลึกบทบาท จวีจิ้งอี กับตำแหน่ง ${firstBrand?.title || 'Brand Ambassador'} ของ ${brandTitle}` },
|
|
147
190
|
{ label: '💄 สไตล์เมคอัพ Ju Jingyi', query: 'วิเคราะห์อิทธิพลของสไตล์การแต่งหน้า Ju Jingyi Makeup ที่ฮิตไปทั่วโลก' },
|
|
148
191
|
{ label: '👗 แบรนด์พรีเซนเตอร์ทั้งหมด', query: 'สรุปแบรนด์ที่จวีจิ้งอีเป็นพรีเซนเตอร์และแอมบาสเดอร์ในปัจจุบัน' }
|
|
149
192
|
);
|
|
@@ -154,7 +197,7 @@ export function generateDynamicFollowUps({ query = '', answer = '', tools = [] }
|
|
|
154
197
|
{ label: '💃 สเตจเต้น 红昭愿 ในตำนาน', query: 'สเตจการเต้นเพลง 红昭愿 (Hong Zhao Yuan) ชุดแดงที่เป็นตำนาน' }
|
|
155
198
|
);
|
|
156
199
|
} else {
|
|
157
|
-
|
|
200
|
+
|
|
158
201
|
const workTool = tools.find(t => t.name === 'search_works' || t.name === 'get_work_detail');
|
|
159
202
|
if (workTool && workTool.result?.works?.[0]) {
|
|
160
203
|
const w = workTool.result.works[0];
|
|
@@ -165,17 +208,51 @@ export function generateDynamicFollowUps({ query = '', answer = '', tools = [] }
|
|
|
165
208
|
{ label: `🏆 กระแสตอบรับของ ${title}`, query: `กระแสตอบรับ เรตติ้ง และรางวัลของซีรีส์ ${title}` }
|
|
166
209
|
);
|
|
167
210
|
} else {
|
|
168
|
-
suggestions.push(
|
|
169
|
-
{ label: '🎬 แนะนำซีรีส์ยอดนิยมที่ไม่ควรพลาด', query: 'แนะนำซีรีส์ยอดนิยมและบทบาทมาสเตอร์พีซของจวีจิ้งอี' },
|
|
170
|
-
{ label: '🎵 เพลงฮิตและสเตจแสดงสดเด่นๆ', query: 'รวมบทเพลงฮิตและสเตจการแสดงสดที่เป็นไวรัลของจวีจิ้งอี' },
|
|
171
|
-
{ label: '💎 ก้าวสู่ศิลปินเดี่ยวและรางวัล', query: 'เส้นทางการเป็นศิลปินเดี่ยว การก่อตั้งสตูดิโอ และรางวัลสำคัญ' }
|
|
172
|
-
);
|
|
211
|
+
suggestions.push(...GENERIC_FOLLOW_UP_POOL.slice(0, 3));
|
|
173
212
|
}
|
|
174
213
|
}
|
|
175
214
|
|
|
176
|
-
|
|
215
|
+
const seen = recentKeys instanceof Set ? recentKeys : null;
|
|
216
|
+
if (!seen) return suggestions.slice(0, 3);
|
|
217
|
+
|
|
218
|
+
// Cross-turn freshness: drop questions already shown recently in this session.
|
|
219
|
+
const fresh = dedupeSuggestions(suggestions, seen);
|
|
220
|
+
if (fresh.length >= 2) return fresh.slice(0, 3);
|
|
221
|
+
|
|
222
|
+
// The topic bucket itself was all recently shown — rotate through the generic
|
|
223
|
+
// pool (cursor advances every call) so each turn proposes NEW questions.
|
|
224
|
+
const rotated = [
|
|
225
|
+
...GENERIC_FOLLOW_UP_POOL.slice(followUpPoolCursor),
|
|
226
|
+
...GENERIC_FOLLOW_UP_POOL.slice(0, followUpPoolCursor),
|
|
227
|
+
];
|
|
228
|
+
followUpPoolCursor = (followUpPoolCursor + 1) % GENERIC_FOLLOW_UP_POOL.length;
|
|
229
|
+
const freshGeneric = dedupeSuggestions(rotated, seen);
|
|
230
|
+
const out = [...fresh, ...freshGeneric].slice(0, 3);
|
|
231
|
+
if (out.length >= 2) return out;
|
|
232
|
+
|
|
233
|
+
// Pool fully consumed for this session — better to recycle the least-recent
|
|
234
|
+
// questions than to return nothing.
|
|
235
|
+
return dedupeSuggestions(rotated, new Set()).slice(0, 3);
|
|
177
236
|
}
|
|
178
237
|
|
|
238
|
+
const GENERIC_FOLLOW_UP_POOL = Object.freeze([
|
|
239
|
+
{ label: '🎬 แนะนำซีรีส์ยอดนิยมที่ไม่ควรพลาด', query: 'แนะนำซีรีส์ยอดนิยมและบทบาทมาสเตอร์พีซของจวีจิ้งอี' },
|
|
240
|
+
{ label: '🎵 เพลงฮิตและสเตจแสดงสดเด่นๆ', query: 'รวมบทเพลงฮิตและสเตจการแสดงสดที่เป็นไวรัลของจวีจิ้งอี' },
|
|
241
|
+
{ label: '💎 ก้าวสู่ศิลปินเดี่ยวและรางวัล', query: 'เส้นทางการเป็นศิลปินเดี่ยว การก่อตั้งสตูดิโอ และรางวัลสำคัญ' },
|
|
242
|
+
{ label: '🏆 รางวัลและผลการประกวด', query: 'รางวัลและผลการประกวดสำคัญของจวีจิ้งอีตั้งแต่ยุค SNH48' },
|
|
243
|
+
{ label: '📅 ไทม์ไลน์อาชีพ 2013-2026', query: 'ไทม์ไลน์อาชีพของจวีจิ้งอีตั้งแต่เดบิวต์ SNH48 จนถึงยุคศิลปินเดี่ยว' },
|
|
244
|
+
{ label: '🎤 สเตจไวรัลที่ต้องดู', query: 'สเตจการแสดงสดของจวีจิ้งอีที่กลายเป็นไวรัลมากที่สุด' },
|
|
245
|
+
{ label: '📸 แฟชั่นและลุคเด่น', query: 'ลุคแฟชั่นและงานพรีเซนเตอร์แบรนด์ที่โดดเด่นของจวีจิ้งอี' },
|
|
246
|
+
{ label: '📺 ผลงานซีรีส์ล่าสุด', query: 'ผลงานซีรีส์ล่าสุดและโปรเจกต์ที่กำลังจะมาของจวีจิ้งอี' },
|
|
247
|
+
{ label: '🎬 ฉากไวรัลในซีรีส์', query: 'ฉากที่ไวรัลที่สุดในซีรีส์ที่จวีจิ้งอีแสดง' },
|
|
248
|
+
{ label: '🌟 การแสดงที่ได้รับคำชม', query: 'การแสดงของจวีจิ้งอีที่ได้รับคำชมจากนักวิจารณ์และผู้ชม' },
|
|
249
|
+
{ label: '🎹 บทเพลงที่ขับร้องเอง', query: 'รวมบทเพลงที่จวีจิ้งอีขับร้องเองทั้งหมด' },
|
|
250
|
+
{ label: '🌏 แฟนคลับสากล', query: 'แฟนคลับสากลและชื่อเสียงระดับโลกของจวีจิ้งอี' },
|
|
251
|
+
{ label: '🧧 อีเวนต์และกำหนดการ', query: 'กำหนดการงานเทศกาลและอีเวนต์ของจวีจิ้งอี' },
|
|
252
|
+
{ label: '🌺 ยุค SNH48 ก่อนเดี่ยว', query: 'ช่วงเวลาสำคัญของจวีจิ้งอีในยุค SNH48 ก่อนออกมาเดี่ยว' },
|
|
253
|
+
]);
|
|
254
|
+
let followUpPoolCursor = 0;
|
|
255
|
+
|
|
179
256
|
export default {
|
|
180
257
|
extractAgentFollowUpQuestions,
|
|
181
258
|
generateDynamicFollowUps,
|
|
@@ -10,7 +10,7 @@ export function resolveAllowedOrigin(requestOrigin) {
|
|
|
10
10
|
try {
|
|
11
11
|
const parsed = new URL(requestOrigin);
|
|
12
12
|
const isDevelopment = process.env.NODE_ENV !== 'production';
|
|
13
|
-
if (isDevelopment && (parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1')) {
|
|
13
|
+
if (isDevelopment && (parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1') && (parsed.protocol === 'http:' || parsed.protocol === 'https:')) {
|
|
14
14
|
return requestOrigin;
|
|
15
15
|
}
|
|
16
16
|
} catch { }
|
|
@@ -41,12 +41,12 @@ export function applySecurityHeaders(req, res) {
|
|
|
41
41
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key, Accept');
|
|
42
42
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
43
43
|
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
|
|
44
|
-
res.setHeader('Referrer-Policy', '
|
|
44
|
+
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
45
45
|
res.setHeader('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload');
|
|
46
|
-
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=(), payment=(), usb=()');
|
|
46
|
+
res.setHeader('Permissions-Policy', 'camera=(), microphone=(self), geolocation=(), payment=(), usb=()');
|
|
47
47
|
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
|
|
48
48
|
res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
|
|
49
|
-
res.setHeader('Content-Security-Policy', "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; form-action 'self'; script-src 'self' 'unsafe-inline' https://www.youtube.com https://www.youtube-nocookie.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com data:; img-src 'self' data: blob: https:; media-src 'self' blob: https:; frame-src https://www.youtube.com https://www.youtube-nocookie.com; connect-src 'self' https:;");
|
|
49
|
+
res.setHeader('Content-Security-Policy', "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; form-action 'self'; script-src 'self' 'unsafe-inline' https://www.youtube.com https://www.youtube-nocookie.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com data:; img-src 'self' data: blob: https:; media-src 'self' blob: https:; frame-src https://www.youtube.com https://www.youtube-nocookie.com https://player.bilibili.com https://www.bilibili.com; connect-src 'self' http://localhost:* http://127.0.0.1:* https:;");
|
|
50
50
|
}
|
|
51
51
|
return !origin || Boolean(allowedOrigin);
|
|
52
52
|
}
|
|
@@ -80,6 +80,9 @@ function constantTimeEqual(left, right) {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
export function isWebChatAdmin(req) {
|
|
83
|
+
// Explicit opt-in only: never infer test mode from NODE_ENV/--test flags,
|
|
84
|
+
// otherwise a stray NODE_ENV=test in production would disable admin auth.
|
|
85
|
+
if (process.env.WEBCHAT_ALLOW_TEST_ADMIN === 'true' && process.env.NODE_ENV !== 'production') return true;
|
|
83
86
|
const expected = process.env.WEBCHAT_ADMIN_KEY || '';
|
|
84
87
|
if (!expected) return false;
|
|
85
88
|
return constantTimeEqual(extractApiKey(req), expected);
|