kiku-agent 1.2.0 → 1.3.1
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 +32 -3
- package/server/agent/agentCore.js +635 -267
- package/server/agent/appPaths.js +2 -0
- package/server/agent/catalogEntityResolver.js +637 -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 +532 -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/ink/App.jsx +348 -0
- package/server/agent/ink/Panel.jsx +54 -0
- package/server/agent/ink/PromptInput.jsx +321 -0
- package/server/agent/ink/TaskPlanPanel.jsx +88 -0
- package/server/agent/ink/ThinkingPanel.jsx +68 -0
- package/server/agent/ink/ToolResult.jsx +133 -0
- package/server/agent/ink/main.jsx +195 -0
- package/server/agent/ink/useEventBus.js +218 -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/contextAllocator.js +214 -0
- package/server/agent/memory/hierarchicalStore.js +523 -0
- package/server/agent/memory/reflectionWorker.js +110 -0
- 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/hardwareTiers.js +332 -0
- package/server/agent/telemetry/runtimeWatchdog.js +66 -0
- package/server/agent/telemetry/techPowerUpClient.js +682 -0
- package/server/agent/telemetry/techpowerup-mcp.json +22 -0
- 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/websearch/adapters/duckduckgo.js +97 -0
- package/server/agent/websearch/adapters/jina.js +77 -0
- package/server/agent/websearch/adapters/searxng.js +57 -0
- package/server/agent/websearch/core/canonical.js +89 -0
- package/server/agent/websearch/core/fusion.js +81 -0
- package/server/agent/websearch/core/types.js +29 -0
- package/server/agent/websearch/extract.js +605 -0
- package/server/agent/websearch/index.js +43 -0
- package/server/agent/websearch/orchestrator.js +266 -0
- package/server/agent/websearch/resilience/breaker.js +97 -0
- package/server/agent/websearch/resilience/cache.js +59 -0
- package/server/agent/websearch/resilience/limiter.js +93 -0
- package/server/agent/websearch/security/filter.js +36 -0
- package/server/agent/websearch/security/ssrf.js +289 -0
- package/server/agent/websearch/telemetry.js +71 -0
- package/server/agent/youtubeAuth.js +82 -73
- package/server/agent/youtubeTranscript.js +209 -0
- package/server/apiRouter.js +390 -190
- 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/routes/agent.js +55 -0
- 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
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { classifySource, extractArticleDate } from './newsIntelligence.js';
|
|
2
|
+
import { simhashText, hamming64 } from './websearch/extract.js';
|
|
3
|
+
import {
|
|
4
|
+
addDynamicEntry,
|
|
5
|
+
getDynamicEntries,
|
|
6
|
+
validateCatalogEntry,
|
|
7
|
+
DEFAULT_DYNAMIC_CATALOG_PATH
|
|
8
|
+
} from './dynamicCatalog.js';
|
|
9
|
+
import { searchNews } from './webSearch.js';
|
|
10
|
+
|
|
11
|
+
export const KNOWN_OFFICIAL_PLATFORMS = new Set([
|
|
12
|
+
'weibo', 'weibo.com', 'weibo.cn',
|
|
13
|
+
'douyin', 'douyin.com',
|
|
14
|
+
'bilibili', 'bilibili.com',
|
|
15
|
+
'youtube', 'youtube.com',
|
|
16
|
+
'instagram', 'instagram.com',
|
|
17
|
+
'xiaohongshu', 'xiaohongshu.com', 'xhs',
|
|
18
|
+
'tiktok', 'tiktok.com'
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export function isTrustedSource(source) {
|
|
22
|
+
if (!source || typeof source !== 'string') {
|
|
23
|
+
return { trusted: false, tier: 3, label: 'unknown' };
|
|
24
|
+
}
|
|
25
|
+
const sLower = source.toLowerCase().trim();
|
|
26
|
+
|
|
27
|
+
if (KNOWN_OFFICIAL_PLATFORMS.has(sLower)) {
|
|
28
|
+
return { trusted: true, tier: 1, label: 'official' };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (/^https?:\/\//i.test(sLower)) {
|
|
32
|
+
const classified = classifySource(source);
|
|
33
|
+
return {
|
|
34
|
+
trusted: classified.tier <= 2,
|
|
35
|
+
tier: classified.tier,
|
|
36
|
+
label: classified.label
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (/(weibo|douyin|bilibili|youtube|instagram|xhs|xiaohongshu|tiktok|official|工作室)/i.test(sLower)) {
|
|
41
|
+
return { trusted: true, tier: 1, label: 'official' };
|
|
42
|
+
}
|
|
43
|
+
if (/(press|news|sohu|netease|sina|tencent|qq|thairath|sanook|kapook|pptv)/i.test(sLower)) {
|
|
44
|
+
return { trusted: true, tier: 2, label: 'press' };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { trusted: false, tier: 3, label: 'community' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function computeEventSimHash(text) {
|
|
51
|
+
return simhashText(String(text || ''));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isDuplicateEvent(entry, existingList = [], { maxHamming = 8 } = {}) {
|
|
55
|
+
if (!entry || !Array.isArray(existingList) || existingList.length === 0) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const entryText = `${entry.title || entry.brand || entry.brandNameEn || ''} ${entry.highlight || ''} ${entry.summary || ''}`.trim();
|
|
60
|
+
const entryHash = entryText ? computeEventSimHash(entryText) : null;
|
|
61
|
+
|
|
62
|
+
for (const existing of existingList) {
|
|
63
|
+
if (entry.id && existing.id && entry.id === existing.id) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const entryBrand = (entry.brand || entry.brandNameEn || entry.brandNameCn || '').toLowerCase().trim();
|
|
68
|
+
const existingBrand = (existing.brand || existing.brandNameEn || existing.brandNameCn || '').toLowerCase().trim();
|
|
69
|
+
if (entryBrand && existingBrand && entryBrand === existingBrand && entry.year === existing.year) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const entryTitle = (entry.title || entry.titleCn || entry.titleTh || entry.titleEn || '').toLowerCase().trim();
|
|
74
|
+
const existingTitle = (existing.title || existing.titleCn || existing.titleTh || existing.titleEn || '').toLowerCase().trim();
|
|
75
|
+
if (entryTitle && existingTitle && entryTitle === existingTitle && entry.year === existing.year) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const eH = (entry.highlight || entry.summary || '').toLowerCase().trim();
|
|
80
|
+
const xH = (existing.highlight || existing.summary || '').toLowerCase().trim();
|
|
81
|
+
if (eH && xH && (eH.includes(xH) || xH.includes(eH))) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (entryHash != null) {
|
|
86
|
+
const existText = `${existing.title || existing.brand || existing.brandNameEn || ''} ${existing.highlight || ''} ${existing.summary || ''}`.trim();
|
|
87
|
+
if (existText) {
|
|
88
|
+
const existHash = computeEventSimHash(existText);
|
|
89
|
+
if (hamming64(entryHash, existHash) <= maxHamming) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function extractStructuredEvent(text, { client = null } = {}) {
|
|
100
|
+
const rawText = String(text || '').trim();
|
|
101
|
+
if (!rawText) {
|
|
102
|
+
throw new Error('Event text cannot be empty');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (client && typeof client.chatCompletion === 'function') {
|
|
106
|
+
try {
|
|
107
|
+
const prompt = `Extract a structured event for artist Ju Jingyi (鞠婧祎) from this text into JSON:
|
|
108
|
+
Text: "${rawText}"
|
|
109
|
+
|
|
110
|
+
Return ONLY a JSON object with:
|
|
111
|
+
- category: one of ["brands", "timeline", "gallery", "works", "events"]
|
|
112
|
+
- year: integer year (e.g. 2026)
|
|
113
|
+
- date: ISO date string or "YYYY-MM-DD" if present
|
|
114
|
+
- title: concise title of the event / work
|
|
115
|
+
- titleCn: Chinese title or null
|
|
116
|
+
- titleTh: Thai title or null
|
|
117
|
+
- brand: brand name (if brand endorsement) or null
|
|
118
|
+
- brandNameCn: Chinese brand name or null
|
|
119
|
+
- brandNameEn: English brand name or null
|
|
120
|
+
- role: role/title (e.g. "Brand Ambassador", "Lead Actress", "Performer")
|
|
121
|
+
- highlight: key highlight sentence
|
|
122
|
+
- summary: full description
|
|
123
|
+
- imageUrl: image URL or null
|
|
124
|
+
- source: origin platform or media outlet`;
|
|
125
|
+
|
|
126
|
+
const response = await client.chatCompletion({
|
|
127
|
+
messages: [{ role: 'user', content: prompt }],
|
|
128
|
+
temperature: 0.1,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const jsonStr = response?.choices?.[0]?.message?.content || '';
|
|
132
|
+
const match = jsonStr.match(/\{[\s\S]*\}/);
|
|
133
|
+
if (match) {
|
|
134
|
+
const parsed = JSON.parse(match[0]);
|
|
135
|
+
if (parsed && (parsed.title || parsed.brand || parsed.brandNameEn)) {
|
|
136
|
+
return parsed;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const currentYear = new Date().getFullYear();
|
|
145
|
+
const dateInfo = extractArticleDate({ snippet: rawText, title: rawText });
|
|
146
|
+
const eventYear = dateInfo?.date ? dateInfo.date.getUTCFullYear() : currentYear;
|
|
147
|
+
let isoDate = dateInfo?.date ? dateInfo.date.toISOString().slice(0, 10) : null;
|
|
148
|
+
|
|
149
|
+
if (!isoDate) {
|
|
150
|
+
const THAI_MONTHS_MAP = {
|
|
151
|
+
'มกราคม': 1, 'กุมภาพันธ์': 2, 'มีนาคม': 3, 'เมษายน': 4, 'พฤษภาคม': 5, 'มิถุนายน': 6,
|
|
152
|
+
'กรกฎาคม': 7, 'สิงหาคม': 8, 'กันยายน': 9, 'ตุลาคม': 10, 'พฤศจิกายน': 11, 'ธันวาคม': 12,
|
|
153
|
+
'มค': 1, 'กพ': 2, 'มีค': 3, 'เมย': 4, 'พค': 5, 'มิย': 6, 'กค': 7, 'สค': 8, 'กย': 9, 'ตค': 10, 'พย': 11, 'ธค': 12,
|
|
154
|
+
};
|
|
155
|
+
const thMatch = rawText.match(/(?:วันที่\s*)?(\d{1,2})\s*(มกราคม|กุมภาพันธ์|มีนาคม|เมษายน|พฤษภาคม|มิถุนายน|กรกฎาคม|สิงหาคม|กันยายน|ตุลาคม|พฤศจิกายน|ธันวาคม|ม\.?ค\.?|ก\.?พ\.?|มี\.?ค\.?|เม\.?ย\.?|พ\.?ค\.?|มิ\.?ย\.?|ก\.?ค\.?|ส\.?ค\.?|ก\.?ย\.?|ต\.?ค\.?|พ\.?ย\.?|ธ\.?ค\.?)/i);
|
|
156
|
+
if (thMatch) {
|
|
157
|
+
const day = String(thMatch[1]).padStart(2, '0');
|
|
158
|
+
const mKey = thMatch[2].replace(/\./g, '');
|
|
159
|
+
const monthNum = THAI_MONTHS_MAP[mKey] || 1;
|
|
160
|
+
const month = String(monthNum).padStart(2, '0');
|
|
161
|
+
isoDate = `${eventYear}-${month}-${day}`;
|
|
162
|
+
} else {
|
|
163
|
+
isoDate = `${eventYear}-01-01`;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let category = 'events';
|
|
168
|
+
const isBrand = /(แบรนด์|พรีเซ็นเตอร์|โฆษณา|brand|ambassador|endorsement|CF|Commercial|代言|Lafang|拉芳|FRED|TuTuCare)/i.test(rawText);
|
|
169
|
+
const isGallery = /(ลุค|ชุดเดรส|ภาพ|รูปถ่าย|photoshoot|look|dress|gallery|fashion)/i.test(rawText);
|
|
170
|
+
const isWork = /(ซีรีส์|ละคร|เพลง|อัลบั้ม|ซิงเกิล|OST|drama|album|single|series|film)/i.test(rawText);
|
|
171
|
+
const isTimeline = /(milestone|เปิดตัว|ครบรอบ|debut|graduation|timeline)/i.test(rawText);
|
|
172
|
+
|
|
173
|
+
if (isBrand) category = 'brands';
|
|
174
|
+
else if (isWork) category = 'works';
|
|
175
|
+
else if (isGallery) category = 'gallery';
|
|
176
|
+
else if (isTimeline) category = 'timeline';
|
|
177
|
+
|
|
178
|
+
let brandNameEn = '';
|
|
179
|
+
let brandNameCn = '';
|
|
180
|
+
if (isBrand) {
|
|
181
|
+
if (/lafang|拉芳/i.test(rawText)) {
|
|
182
|
+
brandNameEn = 'Lafang';
|
|
183
|
+
brandNameCn = '拉芳';
|
|
184
|
+
} else if (/fred/i.test(rawText)) {
|
|
185
|
+
brandNameEn = 'FRED Paris';
|
|
186
|
+
brandNameCn = '斐登';
|
|
187
|
+
} else if (/tutucare/i.test(rawText)) {
|
|
188
|
+
brandNameEn = 'TuTuCare';
|
|
189
|
+
brandNameCn = '途途呵护';
|
|
190
|
+
} else {
|
|
191
|
+
const bMatch = rawText.match(/แบรนด์\s+([A-Za-z0-9\u4e00-\u9fa5]+)/i) || rawText.match(/brand\s+([A-Za-z0-9\u4e00-\u9fa5]+)/i);
|
|
192
|
+
if (bMatch) {
|
|
193
|
+
brandNameEn = bMatch[1];
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let title = '';
|
|
199
|
+
const quoteMatch = rawText.match(/["“'「]([^"”'」]{3,50})["”'」]/);
|
|
200
|
+
if (quoteMatch) {
|
|
201
|
+
title = quoteMatch[1];
|
|
202
|
+
} else {
|
|
203
|
+
const firstSentence = rawText.split(/[.\n!?|]/)[0]?.trim();
|
|
204
|
+
title = firstSentence && firstSentence.length <= 80 ? firstSentence : `${brandNameEn || 'Ju Jingyi'} Event ${eventYear}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
category,
|
|
209
|
+
year: eventYear,
|
|
210
|
+
date: isoDate,
|
|
211
|
+
title,
|
|
212
|
+
titleCn: brandNameCn || null,
|
|
213
|
+
titleTh: title,
|
|
214
|
+
brand: brandNameEn || (isBrand ? title : null),
|
|
215
|
+
brandNameEn: brandNameEn || null,
|
|
216
|
+
brandNameCn: brandNameCn || null,
|
|
217
|
+
role: isBrand ? 'Brand Ambassador' : 'Artist',
|
|
218
|
+
highlight: rawText.slice(0, 200).trim(),
|
|
219
|
+
summary: rawText,
|
|
220
|
+
source: 'extracted_text'
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function ingestRawEvent(text, {
|
|
225
|
+
category = null,
|
|
226
|
+
source = 'manual',
|
|
227
|
+
trustTier = null,
|
|
228
|
+
client = null,
|
|
229
|
+
catalogPath = DEFAULT_DYNAMIC_CATALOG_PATH
|
|
230
|
+
} = {}) {
|
|
231
|
+
try {
|
|
232
|
+
const trustInfo = isTrustedSource(source);
|
|
233
|
+
const finalTrustTier = trustTier != null ? Number(trustTier) : trustInfo.tier;
|
|
234
|
+
|
|
235
|
+
if (finalTrustTier > 2 && !trustInfo.trusted && trustTier == null) {
|
|
236
|
+
return {
|
|
237
|
+
status: 'rejected',
|
|
238
|
+
reason: 'Untrusted source (Tier 3 community/unverified). Provide explicit trustTier to override.',
|
|
239
|
+
source,
|
|
240
|
+
tier: finalTrustTier
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const structured = await extractStructuredEvent(text, { client });
|
|
245
|
+
const targetCategory = category || structured.category || 'events';
|
|
246
|
+
|
|
247
|
+
const entryToSave = {
|
|
248
|
+
...structured,
|
|
249
|
+
source: source || structured.source || 'manual',
|
|
250
|
+
trustTier: finalTrustTier
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const existing = getDynamicEntries(targetCategory, null, catalogPath);
|
|
254
|
+
if (isDuplicateEvent(entryToSave, existing)) {
|
|
255
|
+
return {
|
|
256
|
+
status: 'duplicate',
|
|
257
|
+
message: `Event already exists in dynamic catalog category "${targetCategory}".`,
|
|
258
|
+
entry: entryToSave
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const saved = addDynamicEntry(targetCategory, entryToSave, {
|
|
263
|
+
catalogPath,
|
|
264
|
+
trustTier: finalTrustTier,
|
|
265
|
+
source
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
status: 'success',
|
|
270
|
+
message: `Successfully ingested event into category "${targetCategory}".`,
|
|
271
|
+
entry: saved
|
|
272
|
+
};
|
|
273
|
+
} catch (err) {
|
|
274
|
+
return {
|
|
275
|
+
status: 'error',
|
|
276
|
+
message: `Ingestion failed: ${err.message}`
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export async function runIngestionScan({
|
|
282
|
+
topic = 'Ju Jingyi 鞠婧祎',
|
|
283
|
+
fetcher = null,
|
|
284
|
+
client = null,
|
|
285
|
+
catalogPath = DEFAULT_DYNAMIC_CATALOG_PATH,
|
|
286
|
+
minTrustTier = 2,
|
|
287
|
+
maxItems = 8
|
|
288
|
+
} = {}) {
|
|
289
|
+
let items = [];
|
|
290
|
+
if (typeof fetcher === 'function') {
|
|
291
|
+
items = await fetcher(topic);
|
|
292
|
+
} else {
|
|
293
|
+
const res = await searchNews(topic, maxItems);
|
|
294
|
+
items = res?.results || [];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
let totalScanned = items.length;
|
|
298
|
+
let ingestedCount = 0;
|
|
299
|
+
let skippedDuplicates = 0;
|
|
300
|
+
let skippedUntrusted = 0;
|
|
301
|
+
const entries = [];
|
|
302
|
+
|
|
303
|
+
for (const item of items) {
|
|
304
|
+
const url = item.url || '';
|
|
305
|
+
const trust = isTrustedSource(url);
|
|
306
|
+
|
|
307
|
+
if (trust.tier > minTrustTier) {
|
|
308
|
+
skippedUntrusted++;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const combinedText = `${item.title || ''}\n${item.snippet || ''}`.trim();
|
|
313
|
+
if (!combinedText) continue;
|
|
314
|
+
|
|
315
|
+
const res = await ingestRawEvent(combinedText, {
|
|
316
|
+
source: url || item.source || 'news_scan',
|
|
317
|
+
trustTier: trust.tier,
|
|
318
|
+
client,
|
|
319
|
+
catalogPath
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
if (res.status === 'success') {
|
|
323
|
+
ingestedCount++;
|
|
324
|
+
entries.push(res.entry);
|
|
325
|
+
} else if (res.status === 'duplicate') {
|
|
326
|
+
skippedDuplicates++;
|
|
327
|
+
} else if (res.status === 'rejected') {
|
|
328
|
+
skippedUntrusted++;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
totalScanned,
|
|
334
|
+
ingestedCount,
|
|
335
|
+
skippedDuplicates,
|
|
336
|
+
skippedUntrusted,
|
|
337
|
+
entries
|
|
338
|
+
};
|
|
339
|
+
}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
+
import { Box, Text, Static, useInput, useApp, useStdin } from 'ink';
|
|
3
|
+
import { PromptInput } from './PromptInput.jsx';
|
|
4
|
+
import { ThinkingPanel } from './ThinkingPanel.jsx';
|
|
5
|
+
import { ToolCompleted, ToolSummary, ToolRunning } from './ToolResult.jsx';
|
|
6
|
+
import { TaskPlanPanel } from './TaskPlanPanel.jsx';
|
|
7
|
+
import { Panel, Separator } from './Panel.jsx';
|
|
8
|
+
import { useAgentState, useStreamingText, useThinking, useToolResults, useTaskPlan, useEvent } from './useEventBus.js';
|
|
9
|
+
import { eventBus, Events } from '../eventBus.js';
|
|
10
|
+
import { getInkPalette } from '../theme.js';
|
|
11
|
+
import { PACKAGE_VERSION } from '../version.js';
|
|
12
|
+
|
|
13
|
+
const FAN_NAMES = ['Mìjú / 蜜橘'];
|
|
14
|
+
function randomFanName() {
|
|
15
|
+
return FAN_NAMES[Math.floor(Math.random() * FAN_NAMES.length)];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function Header() {
|
|
19
|
+
const theme = getInkPalette();
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Box flexDirection="column">
|
|
23
|
+
<Text color={theme.primary}>
|
|
24
|
+
{' ██╗ ██╗██╗██╗ ██╗██╗ ██╗ █████╗ ██╗'}
|
|
25
|
+
</Text>
|
|
26
|
+
<Text color={theme.primary}>
|
|
27
|
+
{' ██║ ██╔╝██║██║ ██╔╝██║ ██║ ██╔══██╗██║'}
|
|
28
|
+
</Text>
|
|
29
|
+
<Text color={theme.secondary}>
|
|
30
|
+
{' █████═╝ ██║█████═╝ ██║ ██║ ███████║██║'}
|
|
31
|
+
</Text>
|
|
32
|
+
<Text color={theme.secondary}>
|
|
33
|
+
{' ██╔═██╗ ██║██╔═██╗ ██║ ██║ ██╔══██║██║'}
|
|
34
|
+
</Text>
|
|
35
|
+
<Text color={theme.primary}>
|
|
36
|
+
{' ██║ ╚██╗██║██║ ╚██╗╚██████╔╝ ██║ ██║██║'}
|
|
37
|
+
</Text>
|
|
38
|
+
<Text color={theme.primary}>
|
|
39
|
+
{' ╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝'}
|
|
40
|
+
</Text>
|
|
41
|
+
<Text />
|
|
42
|
+
<Box>
|
|
43
|
+
<Text color={theme.muted}>Architecture: </Text>
|
|
44
|
+
<Text bold color={theme.highlight}>Next-Gen v{PACKAGE_VERSION} </Text>
|
|
45
|
+
<Text color={theme.muted}>• SSOT Catalog • Dynamic Tool Router • Semantic Memory</Text>
|
|
46
|
+
</Box>
|
|
47
|
+
<Text />
|
|
48
|
+
<Text bold color={theme.secondary}>Example Questions & Capabilities:</Text>
|
|
49
|
+
<Text color={theme.text}> <Text color={theme.primary}>[Dramas 2026]</Text> <Text>"What are Ju Jingyi's upcoming 2026 drama series (Veil of Shadows, Eternal Fragrance)?"</Text></Text>
|
|
50
|
+
<Text color={theme.text}> <Text color={theme.primary}>[Characters ]</Text> <Text>"Tell me about Yang Caiwei in In Blossom and Han Yunxi in Legend of Yunxi."</Text></Text>
|
|
51
|
+
<Text color={theme.text}> <Text color={theme.info}>[Music & OST]</Text> <Text>"What OST songs are from The Blooms at Ruyi Pavilion? /play Rain of Ginkgo"</Text></Text>
|
|
52
|
+
<Text color={theme.text}> <Text color={theme.info}>[Global Brand]</Text> <Text>"Tell me about her Global Fragrance Ambassador role for Versace and luxury brands."</Text></Text>
|
|
53
|
+
<Text color={theme.text}> <Text color={theme.success}>[Fact-Check ]</Text> <Text>"/verify Check Star48 contract status or /research 2026 broadcast licenses"</Text></Text>
|
|
54
|
+
<Text color={theme.text}> <Text color={theme.success}>[Persona/Mem]</Text> <Text>"/skill learn Always answer politely as a passionate Miju fan club member"</Text></Text>
|
|
55
|
+
<Text color={theme.text}> <Text color={theme.tertiary}>[Multi-Agent]</Text> <Text>"/subagent Investigate her upcoming drama production timeline and Weibo updates"</Text></Text>
|
|
56
|
+
<Text color={theme.text}> <Text color={theme.tertiary}>[System Stats]</Text> <Text>"/stats or /doctor to inspect database records, models, and audio engine"</Text></Text>
|
|
57
|
+
<Text />
|
|
58
|
+
<Text>
|
|
59
|
+
<Text color={theme.tertiary}>Quick Commands: </Text>
|
|
60
|
+
<Text color={theme.info}>/help</Text>
|
|
61
|
+
<Text dimColor> • </Text>
|
|
62
|
+
<Text color={theme.success}>/subagent</Text>
|
|
63
|
+
<Text dimColor> • </Text>
|
|
64
|
+
<Text color={theme.success}>/research</Text>
|
|
65
|
+
<Text dimColor> • </Text>
|
|
66
|
+
<Text color={theme.success}>/verify</Text>
|
|
67
|
+
<Text dimColor> • </Text>
|
|
68
|
+
<Text color={theme.success}>/skill</Text>
|
|
69
|
+
<Text dimColor> • </Text>
|
|
70
|
+
<Text color={theme.success}>/exit</Text>
|
|
71
|
+
<Text dimColor> (Tab or ↑/↓ to navigate)</Text>
|
|
72
|
+
</Text>
|
|
73
|
+
<Text />
|
|
74
|
+
</Box>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function UserMessage({ text }) {
|
|
79
|
+
const theme = getInkPalette();
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Box flexDirection="column">
|
|
83
|
+
<Text>
|
|
84
|
+
<Text color={theme.secondary} bold>{randomFanName()}: </Text>
|
|
85
|
+
<Text color={theme.text}>{text}</Text>
|
|
86
|
+
</Text>
|
|
87
|
+
</Box>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function StreamingResponse({ text, isStreaming }) {
|
|
92
|
+
if (!text && !isStreaming) return null;
|
|
93
|
+
const theme = getInkPalette();
|
|
94
|
+
|
|
95
|
+
const lines = text.split('\n');
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<Box flexDirection="column">
|
|
99
|
+
{lines.map((line, i) => (
|
|
100
|
+
<Box key={i}>
|
|
101
|
+
<Text> </Text>
|
|
102
|
+
<Text color={theme.text}>{line || ' '}</Text>
|
|
103
|
+
</Box>
|
|
104
|
+
))}
|
|
105
|
+
{isStreaming && (
|
|
106
|
+
<Box>
|
|
107
|
+
<Text color={theme.muted} dimColor> ▌</Text>
|
|
108
|
+
</Box>
|
|
109
|
+
)}
|
|
110
|
+
</Box>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function CompletedResponse({ text }) {
|
|
115
|
+
if (!text) return null;
|
|
116
|
+
const theme = getInkPalette();
|
|
117
|
+
|
|
118
|
+
const lines = text.split('\n');
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<Box flexDirection="column">
|
|
122
|
+
{lines.map((line, i) => (
|
|
123
|
+
<Box key={i}>
|
|
124
|
+
<Text> </Text>
|
|
125
|
+
<Text color={theme.text}>{line || ' '}</Text>
|
|
126
|
+
</Box>
|
|
127
|
+
))}
|
|
128
|
+
</Box>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function MusicStatusBar({ musicStatus }) {
|
|
133
|
+
if (!musicStatus) return null;
|
|
134
|
+
const theme = getInkPalette();
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<Box>
|
|
138
|
+
<Text color={theme.info}>{musicStatus} </Text>
|
|
139
|
+
</Box>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function App({ agent, client, baseUrl, printHelp, printAd }) {
|
|
144
|
+
const { exit } = useApp();
|
|
145
|
+
const theme = getInkPalette();
|
|
146
|
+
const [outputLines, setOutputLines] = useState([]);
|
|
147
|
+
const [isProcessing, setIsProcessing] = useState(false);
|
|
148
|
+
const [lastQuery, setLastQuery] = useState(null);
|
|
149
|
+
const [finalAnswer, setFinalAnswer] = useState('');
|
|
150
|
+
const [, setThemeRevision] = useState(0);
|
|
151
|
+
|
|
152
|
+
const agentState = useAgentState();
|
|
153
|
+
const { text: streamingText, isStreaming, reset: resetStreaming } = useStreamingText();
|
|
154
|
+
const thinking = useThinking();
|
|
155
|
+
const { tools, clearTools } = useToolResults();
|
|
156
|
+
const { plan: taskPlan, clearPlan } = useTaskPlan();
|
|
157
|
+
const musicStatus = useEvent('music:state:change', '');
|
|
158
|
+
|
|
159
|
+
const agentStateLabel = isProcessing ? agentState.label : '';
|
|
160
|
+
const showStreaming = isStreaming && streamingText.length > 0;
|
|
161
|
+
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
const unsub = eventBus.on(Events.AGENT_FINAL_ANSWER, (data) => {
|
|
164
|
+
setFinalAnswer(data?.answer || '');
|
|
165
|
+
setIsProcessing(false);
|
|
166
|
+
clearTools();
|
|
167
|
+
});
|
|
168
|
+
return unsub;
|
|
169
|
+
}, []);
|
|
170
|
+
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
const unsub = eventBus.on(Events.AGENT_QUERY_ERROR, (data) => {
|
|
173
|
+
setOutputLines(prev => [...prev, {
|
|
174
|
+
type: 'error',
|
|
175
|
+
text: data?.error || 'Unknown error',
|
|
176
|
+
id: Date.now(),
|
|
177
|
+
}]);
|
|
178
|
+
setIsProcessing(false);
|
|
179
|
+
});
|
|
180
|
+
return unsub;
|
|
181
|
+
}, []);
|
|
182
|
+
|
|
183
|
+
const handleSubmit = useCallback(async (input) => {
|
|
184
|
+
const trimmed = input.trim();
|
|
185
|
+
if (!trimmed) return;
|
|
186
|
+
|
|
187
|
+
setOutputLines(prev => [...prev, {
|
|
188
|
+
type: 'user',
|
|
189
|
+
text: trimmed,
|
|
190
|
+
id: Date.now(),
|
|
191
|
+
}]);
|
|
192
|
+
|
|
193
|
+
if (trimmed.startsWith('/')) {
|
|
194
|
+
const { routeCommand } = await import('../commands.js');
|
|
195
|
+
const cmdCtx = {
|
|
196
|
+
agent,
|
|
197
|
+
client,
|
|
198
|
+
baseUrl,
|
|
199
|
+
sub: trimmed.split(/\s+/)[1] || '',
|
|
200
|
+
extra: trimmed.split(/\s+/).slice(2).join(' '),
|
|
201
|
+
parts: trimmed.split(/\s+/),
|
|
202
|
+
lastQuery,
|
|
203
|
+
printHelp,
|
|
204
|
+
printWelcome: () => {},
|
|
205
|
+
printAd,
|
|
206
|
+
enqueueQuery: () => {},
|
|
207
|
+
clearQueryQueue: () => {},
|
|
208
|
+
getQueryQueueLength: () => 0,
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const result = await routeCommand(trimmed, cmdCtx);
|
|
212
|
+
const commandName = trimmed.split(/\s+/, 1)[0].toLowerCase();
|
|
213
|
+
if (commandName === '/theme' || commandName === '/themes') {
|
|
214
|
+
setThemeRevision(revision => revision + 1);
|
|
215
|
+
}
|
|
216
|
+
if (result === 'EXIT') {
|
|
217
|
+
exit();
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
setLastQuery(trimmed);
|
|
223
|
+
setIsProcessing(true);
|
|
224
|
+
setFinalAnswer('');
|
|
225
|
+
resetStreaming();
|
|
226
|
+
clearTools();
|
|
227
|
+
clearPlan();
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const { sanitizeQuery } = await import('../sanitizer.js');
|
|
231
|
+
const { sanitized } = sanitizeQuery(trimmed);
|
|
232
|
+
const promptForLLM = sanitized || trimmed;
|
|
233
|
+
|
|
234
|
+
await agent.ask(promptForLLM, {
|
|
235
|
+
onStepStart: () => {},
|
|
236
|
+
onReasoningStart: () => {},
|
|
237
|
+
onReasoningDelta: () => {},
|
|
238
|
+
onReasoningEnd: () => {},
|
|
239
|
+
onToolStart: () => {},
|
|
240
|
+
onToolResult: () => {},
|
|
241
|
+
onTokenStart: () => {},
|
|
242
|
+
onTokenDelta: () => {},
|
|
243
|
+
onFinalAnswer: (answer) => {
|
|
244
|
+
setOutputLines(prev => [...prev, {
|
|
245
|
+
type: 'assistant',
|
|
246
|
+
text: answer,
|
|
247
|
+
tools: [...tools],
|
|
248
|
+
thinking: thinking.thinking,
|
|
249
|
+
thinkingDuration: thinking.duration,
|
|
250
|
+
id: Date.now(),
|
|
251
|
+
}]);
|
|
252
|
+
setIsProcessing(false);
|
|
253
|
+
setFinalAnswer('');
|
|
254
|
+
resetStreaming();
|
|
255
|
+
clearTools();
|
|
256
|
+
},
|
|
257
|
+
onError: (err) => {
|
|
258
|
+
setOutputLines(prev => [...prev, {
|
|
259
|
+
type: 'error',
|
|
260
|
+
text: err.message,
|
|
261
|
+
id: Date.now(),
|
|
262
|
+
}]);
|
|
263
|
+
setIsProcessing(false);
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
} catch (err) {
|
|
267
|
+
setOutputLines(prev => [...prev, {
|
|
268
|
+
type: 'error',
|
|
269
|
+
text: err.message,
|
|
270
|
+
id: Date.now(),
|
|
271
|
+
}]);
|
|
272
|
+
setIsProcessing(false);
|
|
273
|
+
}
|
|
274
|
+
}, [agent, client, baseUrl, lastQuery, tools, thinking]);
|
|
275
|
+
|
|
276
|
+
return (
|
|
277
|
+
<Box flexDirection="column" width="100%">
|
|
278
|
+
<Static items={[0]}>
|
|
279
|
+
{() => <Header key="header" />}
|
|
280
|
+
</Static>
|
|
281
|
+
|
|
282
|
+
<Box flexDirection="column">
|
|
283
|
+
{outputLines.map((line) => {
|
|
284
|
+
if (line.type === 'user') {
|
|
285
|
+
return <UserMessage key={line.id} text={line.text} />;
|
|
286
|
+
}
|
|
287
|
+
if (line.type === 'assistant') {
|
|
288
|
+
return (
|
|
289
|
+
<Box key={line.id} flexDirection="column">
|
|
290
|
+
{line.thinking && line.thinking.length > 0 && (
|
|
291
|
+
<ThinkingPanel
|
|
292
|
+
text={line.thinking}
|
|
293
|
+
isActive={false}
|
|
294
|
+
duration={line.thinkingDuration}
|
|
295
|
+
/>
|
|
296
|
+
)}
|
|
297
|
+
{line.tools && line.tools.length > 0 && (
|
|
298
|
+
<ToolSummary tools={line.tools} />
|
|
299
|
+
)}
|
|
300
|
+
<CompletedResponse text={line.text} />
|
|
301
|
+
</Box>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
if (line.type === 'error') {
|
|
305
|
+
return (
|
|
306
|
+
<Box key={line.id}>
|
|
307
|
+
<Text> </Text>
|
|
308
|
+
<Text color={theme.error}>[ERROR] {line.text}</Text>
|
|
309
|
+
</Box>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
return null;
|
|
313
|
+
})}
|
|
314
|
+
</Box>
|
|
315
|
+
|
|
316
|
+
{isProcessing && (
|
|
317
|
+
<Box flexDirection="column">
|
|
318
|
+
{taskPlan && <TaskPlanPanel plan={taskPlan} />}
|
|
319
|
+
|
|
320
|
+
{thinking.isActive && (
|
|
321
|
+
<ThinkingPanel
|
|
322
|
+
text={thinking.thinking}
|
|
323
|
+
isActive={true}
|
|
324
|
+
duration={0}
|
|
325
|
+
/>
|
|
326
|
+
)}
|
|
327
|
+
|
|
328
|
+
{tools.map((t, idx) => (
|
|
329
|
+
t.status === 'running'
|
|
330
|
+
? <ToolRunning key={t.id || idx} name={t.name} args={t.args} />
|
|
331
|
+
: <ToolCompleted key={t.id || idx} name={t.name} result={t.result} duration={t.duration} />
|
|
332
|
+
))}
|
|
333
|
+
|
|
334
|
+
{showStreaming && (
|
|
335
|
+
<StreamingResponse text={streamingText} isStreaming={true} />
|
|
336
|
+
)}
|
|
337
|
+
</Box>
|
|
338
|
+
)}
|
|
339
|
+
|
|
340
|
+
<PromptInput
|
|
341
|
+
onSubmit={handleSubmit}
|
|
342
|
+
musicStatus={musicStatus}
|
|
343
|
+
isProcessing={isProcessing}
|
|
344
|
+
agentStateLabel={agentStateLabel}
|
|
345
|
+
/>
|
|
346
|
+
</Box>
|
|
347
|
+
);
|
|
348
|
+
}
|