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
|
@@ -1,15 +1,46 @@
|
|
|
1
|
+
import { dynamicToolRouter } from './toolRouter.js';
|
|
2
|
+
|
|
3
|
+
export function estimateTextTokens(text) {
|
|
4
|
+
if (!text) return 0;
|
|
5
|
+
const str = String(text).trim();
|
|
6
|
+
if (!str) return 0;
|
|
7
|
+
const dense = (str.match(/[\u0E00-\u0E7F\u3040-\u30FF\u3400-\u9FFF\uF900-\uFAFF\uAC00-\uD7AF]/g) || []).length;
|
|
8
|
+
const ascii = str.length - dense;
|
|
9
|
+
const denseTokens = Math.ceil(dense * 1.25);
|
|
10
|
+
if (ascii === 0) return denseTokens;
|
|
11
|
+
const remainder = str.replace(/[\u0E00-\u0E7F\u3040-\u30FF\u3400-\u9FFF\uF900-\uFAFF\uAC00-\uD7AF]/g, ' ');
|
|
12
|
+
const words = remainder.split(/\s+/).filter(Boolean).length;
|
|
13
|
+
return denseTokens + Math.max(Math.ceil(words * 1.3), Math.ceil(ascii / 3.5));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const messageTokenCache = new WeakMap();
|
|
17
|
+
|
|
18
|
+
export function estimateMessageTokens(msg) {
|
|
19
|
+
if (!msg || typeof msg !== 'object') return 0;
|
|
20
|
+
const contentKey = typeof msg.content === 'string'
|
|
21
|
+
? msg.content
|
|
22
|
+
: (msg.content != null ? JSON.stringify(msg.content) : '');
|
|
23
|
+
const cached = messageTokenCache.get(msg);
|
|
24
|
+
if (cached && cached.contentKey === contentKey && cached.toolCalls === msg.tool_calls) {
|
|
25
|
+
return cached.tokens;
|
|
26
|
+
}
|
|
27
|
+
const tokens = (contentKey ? estimateTextTokens(contentKey) : 0)
|
|
28
|
+
+ (msg.tool_calls ? estimateTextTokens(JSON.stringify(msg.tool_calls)) : 0);
|
|
29
|
+
messageTokenCache.set(msg, { contentKey, toolCalls: msg.tool_calls, tokens });
|
|
30
|
+
return tokens;
|
|
31
|
+
}
|
|
32
|
+
|
|
1
33
|
export function estimateTokenCount(textOrMessages) {
|
|
2
34
|
if (!textOrMessages) return 0;
|
|
3
35
|
if (typeof textOrMessages === 'string') {
|
|
4
|
-
return
|
|
36
|
+
return estimateTextTokens(textOrMessages);
|
|
5
37
|
}
|
|
6
38
|
if (Array.isArray(textOrMessages)) {
|
|
7
|
-
let
|
|
39
|
+
let tokens = 0;
|
|
8
40
|
for (const msg of textOrMessages) {
|
|
9
|
-
|
|
10
|
-
if (msg.tool_calls) chars += JSON.stringify(msg.tool_calls).length;
|
|
41
|
+
tokens += estimateMessageTokens(msg);
|
|
11
42
|
}
|
|
12
|
-
return
|
|
43
|
+
return tokens;
|
|
13
44
|
}
|
|
14
45
|
return 0;
|
|
15
46
|
}
|
|
@@ -38,42 +69,225 @@ export class ContextCompactor {
|
|
|
38
69
|
return points.slice(-8).join(' | ');
|
|
39
70
|
}
|
|
40
71
|
|
|
41
|
-
|
|
72
|
+
|
|
73
|
+
_splitWindow(conversationHistory, keepRecent) {
|
|
74
|
+
const systemMessage = conversationHistory[0]?.role === 'system' ? conversationHistory[0] : null;
|
|
75
|
+
const nonSystemMessages = systemMessage ? conversationHistory.slice(1) : conversationHistory;
|
|
76
|
+
|
|
77
|
+
let start = Math.max(0, nonSystemMessages.length - keepRecent);
|
|
78
|
+
while (start > 0 && nonSystemMessages[start]?.role === 'tool') start--;
|
|
79
|
+
|
|
80
|
+
while (start < nonSystemMessages.length && nonSystemMessages[start]?.role === 'tool') start++;
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
systemMessage,
|
|
84
|
+
olderMessages: nonSystemMessages.slice(0, start),
|
|
85
|
+
recentMessages: nonSystemMessages.slice(start),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
_assemble(systemMessage, olderMessages, recentMessages, summaryText) {
|
|
90
|
+
const text = summaryText ?? this.summarizeHistorySync(olderMessages);
|
|
91
|
+
const compacted = [];
|
|
92
|
+
if (systemMessage) compacted.push(systemMessage);
|
|
93
|
+
if (text) compacted.push({ role: 'system', content: `[CONVERSATION CONTEXT SUMMARY OF EARLIER MESSAGES: ${text}]` });
|
|
94
|
+
compacted.push(...recentMessages);
|
|
95
|
+
return compacted;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
_shouldCompact(conversationHistory, options) {
|
|
42
99
|
if (!Array.isArray(conversationHistory) || conversationHistory.length <= this.keepRecentMessages + 1) {
|
|
43
|
-
return
|
|
100
|
+
return false;
|
|
44
101
|
}
|
|
45
|
-
|
|
46
102
|
const currentTokens = estimateTokenCount(conversationHistory);
|
|
47
103
|
const maxTokens = options.maxTokens || this.maxTokens;
|
|
48
104
|
const keepRecent = options.keepRecentMessages || this.keepRecentMessages;
|
|
49
|
-
|
|
50
105
|
if (currentTokens <= maxTokens && conversationHistory.length <= keepRecent + 2) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
compact(conversationHistory = [], options = {}) {
|
|
112
|
+
if (!this._shouldCompact(conversationHistory, options)) {
|
|
51
113
|
return conversationHistory;
|
|
52
114
|
}
|
|
115
|
+
const keepRecent = options.keepRecentMessages || this.keepRecentMessages;
|
|
116
|
+
const { systemMessage, olderMessages, recentMessages } = this._splitWindow(conversationHistory, keepRecent);
|
|
117
|
+
if (olderMessages.length === 0) return conversationHistory;
|
|
118
|
+
return this._assemble(systemMessage, olderMessages, recentMessages);
|
|
119
|
+
}
|
|
53
120
|
|
|
54
|
-
const systemMessage = conversationHistory[0]?.role === 'system' ? conversationHistory[0] : null;
|
|
55
|
-
const nonSystemMessages = systemMessage ? conversationHistory.slice(1) : conversationHistory;
|
|
56
121
|
|
|
57
|
-
|
|
122
|
+
async compactAsync(conversationHistory = [], options = {}) {
|
|
123
|
+
if (!this._shouldCompact(conversationHistory, options)) {
|
|
58
124
|
return conversationHistory;
|
|
59
125
|
}
|
|
126
|
+
const keepRecent = options.keepRecentMessages || this.keepRecentMessages;
|
|
127
|
+
const { systemMessage, olderMessages, recentMessages } = this._splitWindow(conversationHistory, keepRecent);
|
|
128
|
+
if (olderMessages.length === 0) return conversationHistory;
|
|
60
129
|
|
|
61
|
-
|
|
62
|
-
|
|
130
|
+
let llmSummary = null;
|
|
131
|
+
if (typeof options.summarizer === 'function') {
|
|
132
|
+
try {
|
|
133
|
+
llmSummary = String(await options.summarizer(olderMessages) || '').trim() || null;
|
|
134
|
+
} catch { }
|
|
135
|
+
}
|
|
136
|
+
return this._assemble(systemMessage, olderMessages, recentMessages, llmSummary ?? undefined);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
63
139
|
|
|
64
|
-
|
|
65
|
-
const summaryContextMessage = {
|
|
66
|
-
role: 'system',
|
|
67
|
-
content: `[CONVERSATION CONTEXT SUMMARY OF EARLIER MESSAGES: ${summaryText}]`
|
|
68
|
-
};
|
|
140
|
+
export const contextCompactor = new ContextCompactor();
|
|
69
141
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
compacted.push(...recentMessages);
|
|
142
|
+
export function compactToolsForPrompt(query = '', options = {}) {
|
|
143
|
+
const rawQuery = String(query || '');
|
|
144
|
+
const router = options.router || dynamicToolRouter;
|
|
74
145
|
|
|
75
|
-
|
|
146
|
+
let routed = router.routeToolsForQuery(rawQuery, options) || [];
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if (/photoshoot|magazine|editorial|port|大片|九月刊|写真|นิตยสาร|แฟชั่นเซ็ต/i.test(rawQuery)) {
|
|
150
|
+
|
|
151
|
+
if (!/contract|ambassador|commercial|代言|พรีเซนเตอร์|ทูต/i.test(rawQuery)) {
|
|
152
|
+
routed = routed.filter(t => (t.function?.name || t.schema?.name || t.name) !== 'get_brand_endorsements');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const hasGallery = routed.some(t => (t.function?.name || t.schema?.name || t.name) === 'get_gallery_photos');
|
|
156
|
+
if (!hasGallery) {
|
|
157
|
+
const galleryTool = router.allTools?.find(t => (t.function?.name || t.schema?.name) === 'get_gallery_photos');
|
|
158
|
+
if (galleryTool) {
|
|
159
|
+
routed.unshift(galleryTool);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
76
162
|
}
|
|
163
|
+
|
|
164
|
+
return routed.map(t => {
|
|
165
|
+
if (!t.schema && t.function) return { ...t, schema: t.function };
|
|
166
|
+
if (!t.function && t.schema) return { ...t, function: t.schema };
|
|
167
|
+
return t;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const SEARCH_URL_REGEX = /(SearchUrl|SearchQuery|TrailerSearch|commercialSearch|youtubeCommercialSearch|bilibiliSearchUrl)/i;
|
|
172
|
+
const ESSENTIAL_WORK_KEYS = new Set([
|
|
173
|
+
'id', 'title', 'titleTh', 'titleCn', 'titleEn', 'year', 'role', 'roleCn', 'duration', 'category', 'synopsis'
|
|
174
|
+
]);
|
|
175
|
+
const ESSENTIAL_BRAND_KEYS = new Set([
|
|
176
|
+
'brand', 'brandNameEn', 'brandNameCn', 'year', 'title', 'category', 'isGlobal', 'highlight', 'tier', 'salesHighlights', 'stat', 'revenue', 'detail'
|
|
177
|
+
]);
|
|
178
|
+
const ESSENTIAL_SALES_KEYS = new Set([
|
|
179
|
+
'brand', 'brandId', 'stat', 'revenue', 'detail'
|
|
180
|
+
]);
|
|
181
|
+
const ESSENTIAL_TIMELINE_KEYS = new Set([
|
|
182
|
+
'year', 'period', 'title', 'titleCn', 'era', 'description', 'highlight', 'details'
|
|
183
|
+
]);
|
|
184
|
+
const ESSENTIAL_AWARD_KEYS = new Set([
|
|
185
|
+
'year', 'award', 'title', 'ceremony', 'category', 'work', 'result'
|
|
186
|
+
]);
|
|
187
|
+
const ESSENTIAL_VIDEO_KEYS = new Set([
|
|
188
|
+
'id', 'title', 'titleTh', 'titleCn', 'titleEn', 'year', 'releaseYear', 'category', 'duration', 'youtubeId', 'work', 'dramaName'
|
|
189
|
+
]);
|
|
190
|
+
const ESSENTIAL_SONG_KEYS = new Set([
|
|
191
|
+
'id', 'title', 'titleTh', 'titleCn', 'titleEn', 'year', 'category', 'type', 'work', 'role', 'duration', 'youtubeId', 'youtubeUrl', 'isDirectLink'
|
|
192
|
+
]);
|
|
193
|
+
const ESSENTIAL_ROLE_KEYS = new Set([
|
|
194
|
+
'workTitle', 'year', 'role', 'roleCn'
|
|
195
|
+
]);
|
|
196
|
+
|
|
197
|
+
function stripSearchUrls(obj) {
|
|
198
|
+
if (Array.isArray(obj)) {
|
|
199
|
+
return obj.map(stripSearchUrls);
|
|
200
|
+
}
|
|
201
|
+
if (obj && typeof obj === 'object') {
|
|
202
|
+
const clean = {};
|
|
203
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
204
|
+
if (SEARCH_URL_REGEX.test(k)) continue;
|
|
205
|
+
clean[k] = stripSearchUrls(v);
|
|
206
|
+
}
|
|
207
|
+
return clean;
|
|
208
|
+
}
|
|
209
|
+
return obj;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function compactToolResultForContext(result, options = {}) {
|
|
213
|
+
if (!result || typeof result !== 'object') return result;
|
|
214
|
+
const maxTokens = options.maxTokens || 800;
|
|
215
|
+
|
|
216
|
+
let stripped = stripSearchUrls(result);
|
|
217
|
+
|
|
218
|
+
const projectList = (arr, keys) => {
|
|
219
|
+
if (!Array.isArray(arr)) return arr;
|
|
220
|
+
return arr.map(item => {
|
|
221
|
+
if (!item || typeof item !== 'object') return item;
|
|
222
|
+
const p = {};
|
|
223
|
+
for (const k of keys) {
|
|
224
|
+
if (item[k] !== undefined) p[k] = item[k];
|
|
225
|
+
}
|
|
226
|
+
return p;
|
|
227
|
+
});
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
if (Array.isArray(stripped.works)) stripped.works = projectList(stripped.works, ESSENTIAL_WORK_KEYS);
|
|
231
|
+
if (Array.isArray(stripped.discography)) stripped.discography = projectList(stripped.discography, ESSENTIAL_SONG_KEYS);
|
|
232
|
+
if (Array.isArray(stripped.songs)) stripped.songs = projectList(stripped.songs, ESSENTIAL_SONG_KEYS);
|
|
233
|
+
if (Array.isArray(stripped.brands)) stripped.brands = projectList(stripped.brands, ESSENTIAL_BRAND_KEYS);
|
|
234
|
+
if (Array.isArray(stripped.salesHighlights)) stripped.salesHighlights = projectList(stripped.salesHighlights, ESSENTIAL_SALES_KEYS);
|
|
235
|
+
if (Array.isArray(stripped.timeline)) stripped.timeline = projectList(stripped.timeline, ESSENTIAL_TIMELINE_KEYS);
|
|
236
|
+
if (Array.isArray(stripped.milestones)) stripped.milestones = projectList(stripped.milestones, ESSENTIAL_TIMELINE_KEYS);
|
|
237
|
+
if (Array.isArray(stripped.eras)) stripped.eras = projectList(stripped.eras, ESSENTIAL_TIMELINE_KEYS);
|
|
238
|
+
if (Array.isArray(stripped.awards)) stripped.awards = projectList(stripped.awards, ESSENTIAL_AWARD_KEYS);
|
|
239
|
+
if (Array.isArray(stripped.videos)) stripped.videos = projectList(stripped.videos, ESSENTIAL_VIDEO_KEYS);
|
|
240
|
+
if (Array.isArray(stripped.musicVideos)) stripped.musicVideos = projectList(stripped.musicVideos, ESSENTIAL_VIDEO_KEYS);
|
|
241
|
+
if (Array.isArray(stripped.roles)) stripped.roles = projectList(stripped.roles, ESSENTIAL_ROLE_KEYS);
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
let serialized = JSON.stringify(stripped);
|
|
245
|
+
let tokenCount = estimateTextTokens(serialized);
|
|
246
|
+
if (tokenCount <= maxTokens) return stripped;
|
|
247
|
+
|
|
248
|
+
const truncateNode = (node) => {
|
|
249
|
+
if (Array.isArray(node)) {
|
|
250
|
+
if (node.length > 5) {
|
|
251
|
+
return node.slice(0, 5).map(truncateNode);
|
|
252
|
+
}
|
|
253
|
+
return node.map(truncateNode);
|
|
254
|
+
}
|
|
255
|
+
if (node && typeof node === 'object') {
|
|
256
|
+
const out = {};
|
|
257
|
+
for (const [k, v] of Object.entries(node)) {
|
|
258
|
+
out[k] = truncateNode(v);
|
|
259
|
+
}
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
262
|
+
if (typeof node === 'string' && node.length > 240) {
|
|
263
|
+
return `${node.slice(0, 240)}…`;
|
|
264
|
+
}
|
|
265
|
+
return node;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
stripped = truncateNode(stripped);
|
|
269
|
+
serialized = JSON.stringify(stripped);
|
|
270
|
+
tokenCount = estimateTextTokens(serialized);
|
|
271
|
+
|
|
272
|
+
if (tokenCount > maxTokens) {
|
|
273
|
+
const aggressive = (node) => {
|
|
274
|
+
if (Array.isArray(node)) return node.slice(0, 3).map(aggressive);
|
|
275
|
+
if (node && typeof node === 'object') {
|
|
276
|
+
const out = {};
|
|
277
|
+
for (const [k, v] of Object.entries(node)) {
|
|
278
|
+
out[k] = aggressive(v);
|
|
279
|
+
}
|
|
280
|
+
return out;
|
|
281
|
+
}
|
|
282
|
+
if (typeof node === 'string' && node.length > 120) {
|
|
283
|
+
return `${node.slice(0, 120)}…`;
|
|
284
|
+
}
|
|
285
|
+
return node;
|
|
286
|
+
};
|
|
287
|
+
stripped = aggressive(stripped);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
stripped._compactedForContext = true;
|
|
291
|
+
return stripped;
|
|
77
292
|
}
|
|
78
293
|
|
|
79
|
-
export const contextCompactor = new ContextCompactor();
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { KIKU_DATA_DIR } from './appPaths.js';
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_DYNAMIC_CATALOG_PATH = path.join(KIKU_DATA_DIR, 'dynamic-catalog.json');
|
|
7
|
+
|
|
8
|
+
const VALID_CATEGORIES = new Set(['brands', 'timeline', 'gallery', 'works', 'events']);
|
|
9
|
+
|
|
10
|
+
export function createEmptyCatalog() {
|
|
11
|
+
return {
|
|
12
|
+
version: 1,
|
|
13
|
+
updatedAt: new Date().toISOString(),
|
|
14
|
+
brands: [],
|
|
15
|
+
timeline: [],
|
|
16
|
+
gallery: [],
|
|
17
|
+
works: [],
|
|
18
|
+
events: []
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function loadDynamicCatalog(catalogPath = DEFAULT_DYNAMIC_CATALOG_PATH) {
|
|
23
|
+
try {
|
|
24
|
+
if (fs.existsSync(catalogPath)) {
|
|
25
|
+
const raw = fs.readFileSync(catalogPath, 'utf8');
|
|
26
|
+
const data = JSON.parse(raw);
|
|
27
|
+
if (data && typeof data === 'object') {
|
|
28
|
+
return {
|
|
29
|
+
version: data.version || 1,
|
|
30
|
+
updatedAt: data.updatedAt || new Date().toISOString(),
|
|
31
|
+
brands: Array.isArray(data.brands) ? data.brands : [],
|
|
32
|
+
timeline: Array.isArray(data.timeline) ? data.timeline : [],
|
|
33
|
+
gallery: Array.isArray(data.gallery) ? data.gallery : [],
|
|
34
|
+
works: Array.isArray(data.works) ? data.works : [],
|
|
35
|
+
events: Array.isArray(data.events) ? data.events : []
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} catch (err) {
|
|
40
|
+
|
|
41
|
+
}
|
|
42
|
+
return createEmptyCatalog();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function saveDynamicCatalog(catalog, catalogPath = DEFAULT_DYNAMIC_CATALOG_PATH) {
|
|
46
|
+
try {
|
|
47
|
+
const dir = path.dirname(catalogPath);
|
|
48
|
+
if (!fs.existsSync(dir)) {
|
|
49
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
const safeCatalog = {
|
|
52
|
+
version: 1,
|
|
53
|
+
updatedAt: new Date().toISOString(),
|
|
54
|
+
brands: Array.isArray(catalog?.brands) ? catalog.brands : [],
|
|
55
|
+
timeline: Array.isArray(catalog?.timeline) ? catalog.timeline : [],
|
|
56
|
+
gallery: Array.isArray(catalog?.gallery) ? catalog.gallery : [],
|
|
57
|
+
works: Array.isArray(catalog?.works) ? catalog.works : [],
|
|
58
|
+
events: Array.isArray(catalog?.events) ? catalog.events : []
|
|
59
|
+
};
|
|
60
|
+
fs.writeFileSync(catalogPath, JSON.stringify(safeCatalog, null, 2), 'utf8');
|
|
61
|
+
return true;
|
|
62
|
+
} catch (err) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function validateCatalogEntry(category, entry) {
|
|
68
|
+
if (!VALID_CATEGORIES.has(category)) {
|
|
69
|
+
return { valid: false, reason: `Invalid category "${category}". Allowed: ${Array.from(VALID_CATEGORIES).join(', ')}` };
|
|
70
|
+
}
|
|
71
|
+
if (!entry || typeof entry !== 'object') {
|
|
72
|
+
return { valid: false, reason: 'Entry must be a non-null object' };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
const hasTitle = Boolean(
|
|
77
|
+
entry.title || entry.titleCn || entry.titleEn || entry.titleTh ||
|
|
78
|
+
entry.brand || entry.brandNameEn || entry.brandNameCn || entry.name
|
|
79
|
+
);
|
|
80
|
+
if (!hasTitle) {
|
|
81
|
+
return { valid: false, reason: 'Entry must specify at least one title or brand name' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { valid: true };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function addDynamicEntry(category, rawEntry, { catalogPath = DEFAULT_DYNAMIC_CATALOG_PATH, trustTier = 1, source = 'manual' } = {}) {
|
|
88
|
+
const validation = validateCatalogEntry(category, rawEntry);
|
|
89
|
+
if (!validation.valid) {
|
|
90
|
+
throw new Error(validation.reason);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const catalog = loadDynamicCatalog(catalogPath);
|
|
94
|
+
const now = new Date().toISOString();
|
|
95
|
+
const currentYear = new Date().getFullYear();
|
|
96
|
+
|
|
97
|
+
const idPrefix = `dyn-${category.slice(0, 3)}-`;
|
|
98
|
+
const cleanId = rawEntry.id
|
|
99
|
+
? String(rawEntry.id).trim()
|
|
100
|
+
: `${idPrefix}${Date.now().toString(36)}-${crypto.randomBytes(3).toString('hex')}`;
|
|
101
|
+
|
|
102
|
+
const entry = {
|
|
103
|
+
...rawEntry,
|
|
104
|
+
id: cleanId,
|
|
105
|
+
year: Number(rawEntry.year) || currentYear,
|
|
106
|
+
trustTier: Number(trustTier) || 1,
|
|
107
|
+
source: String(source || 'manual'),
|
|
108
|
+
createdAt: rawEntry.createdAt || now,
|
|
109
|
+
updatedAt: now
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
const list = catalog[category];
|
|
114
|
+
const existingIdx = list.findIndex(item => item.id === cleanId);
|
|
115
|
+
if (existingIdx >= 0) {
|
|
116
|
+
list[existingIdx] = entry;
|
|
117
|
+
} else {
|
|
118
|
+
list.unshift(entry);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
saveDynamicCatalog(catalog, catalogPath);
|
|
122
|
+
return entry;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function getDynamicEntries(category, filter = null, catalogPath = DEFAULT_DYNAMIC_CATALOG_PATH) {
|
|
126
|
+
if (!VALID_CATEGORIES.has(category)) return [];
|
|
127
|
+
const catalog = loadDynamicCatalog(catalogPath);
|
|
128
|
+
let entries = catalog[category] || [];
|
|
129
|
+
|
|
130
|
+
if (typeof filter === 'function') {
|
|
131
|
+
entries = entries.filter(filter);
|
|
132
|
+
} else if (filter && typeof filter === 'object') {
|
|
133
|
+
entries = entries.filter(item => {
|
|
134
|
+
for (const [k, v] of Object.entries(filter)) {
|
|
135
|
+
if (v !== undefined && item[k] !== v) return false;
|
|
136
|
+
}
|
|
137
|
+
return true;
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return entries;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function clearDynamicCatalog(catalogPath = DEFAULT_DYNAMIC_CATALOG_PATH) {
|
|
145
|
+
const empty = createEmptyCatalog();
|
|
146
|
+
saveDynamicCatalog(empty, catalogPath);
|
|
147
|
+
return empty;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function getDynamicCatalogStats(catalogPath = DEFAULT_DYNAMIC_CATALOG_PATH) {
|
|
151
|
+
const catalog = loadDynamicCatalog(catalogPath);
|
|
152
|
+
return {
|
|
153
|
+
totalEntries: (catalog.brands.length + catalog.timeline.length + catalog.gallery.length + catalog.works.length + catalog.events.length),
|
|
154
|
+
brandsCount: catalog.brands.length,
|
|
155
|
+
timelineCount: catalog.timeline.length,
|
|
156
|
+
galleryCount: catalog.gallery.length,
|
|
157
|
+
worksCount: catalog.works.length,
|
|
158
|
+
eventsCount: catalog.events.length,
|
|
159
|
+
updatedAt: catalog.updatedAt
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function pruneExpiredEntries(maxDaysOrOptions = 90, maybeCatalogPath = null) {
|
|
164
|
+
let maxDays = 90;
|
|
165
|
+
let catalogPath = DEFAULT_DYNAMIC_CATALOG_PATH;
|
|
166
|
+
|
|
167
|
+
if (typeof maxDaysOrOptions === 'object' && maxDaysOrOptions !== null) {
|
|
168
|
+
maxDays = maxDaysOrOptions.maxDays ?? maxDaysOrOptions.ttlDays ?? 90;
|
|
169
|
+
catalogPath = maxDaysOrOptions.catalogPath || DEFAULT_DYNAMIC_CATALOG_PATH;
|
|
170
|
+
} else {
|
|
171
|
+
maxDays = Number(maxDaysOrOptions) || 90;
|
|
172
|
+
if (maybeCatalogPath) catalogPath = maybeCatalogPath;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const catalog = loadDynamicCatalog(catalogPath);
|
|
176
|
+
const cutoff = Date.now() - (maxDays * 86400000);
|
|
177
|
+
let prunedCount = 0;
|
|
178
|
+
|
|
179
|
+
for (const cat of VALID_CATEGORIES) {
|
|
180
|
+
const before = catalog[cat].length;
|
|
181
|
+
catalog[cat] = catalog[cat].filter(item => {
|
|
182
|
+
const created = item.createdAt ? new Date(item.createdAt).getTime() : 0;
|
|
183
|
+
return !created || created >= cutoff;
|
|
184
|
+
});
|
|
185
|
+
prunedCount += (before - catalog[cat].length);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (prunedCount > 0) {
|
|
189
|
+
saveDynamicCatalog(catalog, catalogPath);
|
|
190
|
+
}
|
|
191
|
+
return { prunedCount };
|
|
192
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* envBootstrap — one-shot .env loading for the kiku-agent process graph.
|
|
3
|
+
*
|
|
4
|
+
* This module MUST be imported before any module that snapshots the
|
|
5
|
+
* environment at evaluation time (config.js provider defaults, etc.).
|
|
6
|
+
* cli.js imports it first; youtubeAuth.js imports it defensively because
|
|
7
|
+
* it is also reached through other entry graphs (mcp stdio, web chat).
|
|
8
|
+
*
|
|
9
|
+
* Loading rules (unchanged):
|
|
10
|
+
* 1. package-root/.env — development checkouts
|
|
11
|
+
* 2. <KIKU_DATA_DIR>/.env — user-owned location (global installs)
|
|
12
|
+
* Both files are read; existing process.env values always win.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import { PACKAGE_ENV_PATH, USER_ENV_PATH } from './envManager.js';
|
|
17
|
+
|
|
18
|
+
let applied = false;
|
|
19
|
+
let appliedFiles = [];
|
|
20
|
+
|
|
21
|
+
export function bootstrapEnv({ packageEnvPath = PACKAGE_ENV_PATH, userEnvPath = USER_ENV_PATH, force = false } = {}) {
|
|
22
|
+
if (applied && !force) {
|
|
23
|
+
return { applied: true, files: [...appliedFiles] };
|
|
24
|
+
}
|
|
25
|
+
applied = true;
|
|
26
|
+
appliedFiles = [];
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
for (const envFile of [packageEnvPath, userEnvPath]) {
|
|
30
|
+
if (!fs.existsSync(envFile)) continue;
|
|
31
|
+
appliedFiles.push(envFile);
|
|
32
|
+
const text = fs.readFileSync(envFile, 'utf8');
|
|
33
|
+
for (const line of text.split('\n')) {
|
|
34
|
+
const m = line.match(/^\s*([\w.-]+)\s*=\s*(.*)\s*$/);
|
|
35
|
+
if (!m || line.trim().startsWith('#')) continue;
|
|
36
|
+
const key = m[1].trim();
|
|
37
|
+
const val = m[2].trim().replace(/^["']|["']$/g, '');
|
|
38
|
+
if (process.env[key] === undefined) process.env[key] = val;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} catch {}
|
|
42
|
+
|
|
43
|
+
return { applied: true, files: [...appliedFiles] };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
bootstrapEnv();
|