kiku-agent 1.0.6 → 1.0.8
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/LICENSE +621 -0
- package/README.md +317 -31
- package/package.json +96 -93
- package/server/agent/agentCore.js +58 -31
- package/server/agent/cli.js +101 -56
- package/server/agent/commandFeatures.js +517 -0
- package/server/agent/commandInput.js +113 -0
- package/server/agent/commandRegistry.js +124 -10
- package/server/agent/commands.js +383 -10
- package/server/agent/config.js +64 -48
- package/server/agent/jobManager.js +96 -0
- package/server/agent/llmClient.js +26 -19
- package/server/agent/mcpClient.js +68 -11
- package/server/agent/memory.js +87 -14
- package/server/agent/modelManager.js +30 -13
- package/server/agent/quizData.js +384 -0
- package/server/agent/terminalImage.js +248 -0
- package/server/agent/tools.js +153 -43
- package/server/agent/ui.js +67 -21
- package/server/agent/youtubeAuth.js +1 -2
- package/src/assets/images/0040jbadgy1id6amcqte3j62c0340qv602.jpg +0 -0
- package/src/assets/images/0040jbadly8icq3if7twwj60qy0qy0uc02.jpg +0 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import jpeg from 'jpeg-js';
|
|
5
|
+
|
|
6
|
+
const moduleDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
export const DEFAULT_IMAGE_DIRECTORY = path.resolve(moduleDirectory, '../../src/assets/images');
|
|
8
|
+
|
|
9
|
+
const RESET = '\x1b[0m';
|
|
10
|
+
const foreground = (r, g, b) => `\x1b[38;2;${r};${g};${b}m`;
|
|
11
|
+
const background = (r, g, b) => `\x1b[48;2;${r};${g};${b}m`;
|
|
12
|
+
const QUADRANT_GLYPHS = [
|
|
13
|
+
' ', '▘', '▝', '▀', '▖', '▌', '▞', '▛',
|
|
14
|
+
'▗', '▚', '▐', '▜', '▄', '▙', '▟', '█',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export function supportsTerminalImages({
|
|
18
|
+
env = process.env,
|
|
19
|
+
isTTY = process.stdout.isTTY,
|
|
20
|
+
} = {}) {
|
|
21
|
+
const setting = String(env.KIKU_TUI_IMAGES || '').trim().toLowerCase();
|
|
22
|
+
if (['0', 'false', 'off', 'no'].includes(setting)) return false;
|
|
23
|
+
if (['1', 'true', 'on', 'yes'].includes(setting)) return true;
|
|
24
|
+
if (!isTTY || env.NO_COLOR !== undefined || String(env.TERM || '').toLowerCase() === 'dumb') return false;
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const clampChannel = value => Math.max(0, Math.min(255, Math.round(value)));
|
|
29
|
+
|
|
30
|
+
function sourceChannel(image, x, y, channel) {
|
|
31
|
+
const safeX = Math.max(0, Math.min(image.width - 1, x));
|
|
32
|
+
const safeY = Math.max(0, Math.min(image.height - 1, y));
|
|
33
|
+
return image.data[(safeY * image.width + safeX) * 4 + channel];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sampleBilinear(image, x, y, targetWidth, targetHeight) {
|
|
37
|
+
const sourceX = ((x + 0.5) * image.width) / targetWidth - 0.5;
|
|
38
|
+
const sourceY = ((y + 0.5) * image.height) / targetHeight - 0.5;
|
|
39
|
+
const left = Math.floor(sourceX);
|
|
40
|
+
const top = Math.floor(sourceY);
|
|
41
|
+
const right = left + 1;
|
|
42
|
+
const bottom = top + 1;
|
|
43
|
+
const mixX = sourceX - left;
|
|
44
|
+
const mixY = sourceY - top;
|
|
45
|
+
const result = [];
|
|
46
|
+
|
|
47
|
+
for (let channel = 0; channel < 3; channel++) {
|
|
48
|
+
const topMix = sourceChannel(image, left, top, channel) * (1 - mixX)
|
|
49
|
+
+ sourceChannel(image, right, top, channel) * mixX;
|
|
50
|
+
const bottomMix = sourceChannel(image, left, bottom, channel) * (1 - mixX)
|
|
51
|
+
+ sourceChannel(image, right, bottom, channel) * mixX;
|
|
52
|
+
result.push(topMix * (1 - mixY) + bottomMix * mixY);
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function resizeAndEnhance(image, targetWidth, targetHeight) {
|
|
58
|
+
const pixels = new Float32Array(targetWidth * targetHeight * 3);
|
|
59
|
+
for (let y = 0; y < targetHeight; y++) {
|
|
60
|
+
for (let x = 0; x < targetWidth; x++) {
|
|
61
|
+
const color = sampleBilinear(image, x, y, targetWidth, targetHeight);
|
|
62
|
+
const offset = (y * targetWidth + x) * 3;
|
|
63
|
+
pixels[offset] = color[0];
|
|
64
|
+
pixels[offset + 1] = color[1];
|
|
65
|
+
pixels[offset + 2] = color[2];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const enhanced = new Uint8Array(pixels.length);
|
|
70
|
+
const at = (x, y, channel) => {
|
|
71
|
+
const safeX = Math.max(0, Math.min(targetWidth - 1, x));
|
|
72
|
+
const safeY = Math.max(0, Math.min(targetHeight - 1, y));
|
|
73
|
+
return pixels[(safeY * targetWidth + safeX) * 3 + channel];
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
for (let y = 0; y < targetHeight; y++) {
|
|
77
|
+
for (let x = 0; x < targetWidth; x++) {
|
|
78
|
+
const offset = (y * targetWidth + x) * 3;
|
|
79
|
+
const sharpened = [0, 1, 2].map(channel => {
|
|
80
|
+
const center = at(x, y, channel);
|
|
81
|
+
const neighbourAverage = (
|
|
82
|
+
at(x - 1, y, channel) + at(x + 1, y, channel)
|
|
83
|
+
+ at(x, y - 1, channel) + at(x, y + 1, channel)
|
|
84
|
+
) / 4;
|
|
85
|
+
return center * 1.18 - neighbourAverage * 0.18;
|
|
86
|
+
});
|
|
87
|
+
const luminance = sharpened[0] * 0.2126 + sharpened[1] * 0.7152 + sharpened[2] * 0.0722;
|
|
88
|
+
for (let channel = 0; channel < 3; channel++) {
|
|
89
|
+
const saturated = luminance + (sharpened[channel] - luminance) * 1.06;
|
|
90
|
+
enhanced[offset + channel] = clampChannel((saturated - 128) * 1.03 + 128);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return enhanced;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function colorDistance(left, right) {
|
|
98
|
+
const red = left[0] - right[0];
|
|
99
|
+
const green = left[1] - right[1];
|
|
100
|
+
const blue = left[2] - right[2];
|
|
101
|
+
return red * red + green * green + blue * blue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function averageColors(colors, fallback) {
|
|
105
|
+
if (colors.length === 0) return fallback;
|
|
106
|
+
const sum = colors.reduce((total, color) => [
|
|
107
|
+
total[0] + color[0],
|
|
108
|
+
total[1] + color[1],
|
|
109
|
+
total[2] + color[2],
|
|
110
|
+
], [0, 0, 0]);
|
|
111
|
+
return sum.map(value => clampChannel(value / colors.length));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function encodeQuadrants(colors) {
|
|
115
|
+
let first = colors[0];
|
|
116
|
+
let second = colors[1];
|
|
117
|
+
let furthestDistance = -1;
|
|
118
|
+
for (let left = 0; left < colors.length; left++) {
|
|
119
|
+
for (let right = left + 1; right < colors.length; right++) {
|
|
120
|
+
const distance = colorDistance(colors[left], colors[right]);
|
|
121
|
+
if (distance > furthestDistance) {
|
|
122
|
+
furthestDistance = distance;
|
|
123
|
+
first = colors[left];
|
|
124
|
+
second = colors[right];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let assignments = [0, 0, 0, 0];
|
|
130
|
+
for (let iteration = 0; iteration < 3; iteration++) {
|
|
131
|
+
assignments = colors.map(color => colorDistance(color, second) < colorDistance(color, first) ? 1 : 0);
|
|
132
|
+
first = averageColors(colors.filter((_, index) => assignments[index] === 0), first);
|
|
133
|
+
second = averageColors(colors.filter((_, index) => assignments[index] === 1), second);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const mask = assignments.reduce((value, assignment, index) => value | (assignment << index), 0);
|
|
137
|
+
return {
|
|
138
|
+
glyph: QUADRANT_GLYPHS[mask],
|
|
139
|
+
foregroundColor: second,
|
|
140
|
+
backgroundColor: first,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function renderJpegToAnsi(buffer, {
|
|
145
|
+
columns = process.stdout.columns || 80,
|
|
146
|
+
rows = process.stdout.rows || 24,
|
|
147
|
+
maxWidth = 60,
|
|
148
|
+
maxRows = 20,
|
|
149
|
+
indent = ' ',
|
|
150
|
+
} = {}) {
|
|
151
|
+
const image = jpeg.decode(buffer, { useTArray: true, formatAsRGBA: true });
|
|
152
|
+
if (!image?.width || !image?.height) throw new Error('Unable to decode gallery image');
|
|
153
|
+
|
|
154
|
+
const indentWidth = Array.from(indent).length;
|
|
155
|
+
const availableWidth = Math.max(8, Math.floor(columns) - indentWidth - 2);
|
|
156
|
+
let targetWidth = Math.max(8, Math.min(Math.floor(maxWidth), availableWidth, image.width));
|
|
157
|
+
const availableRows = Math.max(3, Math.min(Math.floor(maxRows), Math.floor(rows) - 7));
|
|
158
|
+
let targetPixelHeight = Math.max(2, Math.round((image.height / image.width) * targetWidth));
|
|
159
|
+
let targetRows = Math.ceil(targetPixelHeight / 2);
|
|
160
|
+
|
|
161
|
+
if (targetRows > availableRows) {
|
|
162
|
+
targetRows = availableRows;
|
|
163
|
+
targetPixelHeight = targetRows * 2;
|
|
164
|
+
targetWidth = Math.max(8, Math.min(targetWidth, Math.round((image.width / image.height) * targetPixelHeight)));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const sampledWidth = targetWidth * 2;
|
|
168
|
+
const resized = resizeAndEnhance(image, sampledWidth, targetPixelHeight);
|
|
169
|
+
const pixelAt = (x, y) => {
|
|
170
|
+
const offset = (y * sampledWidth + x) * 3;
|
|
171
|
+
return [resized[offset], resized[offset + 1], resized[offset + 2]];
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const lines = [];
|
|
175
|
+
for (let row = 0; row < targetRows; row++) {
|
|
176
|
+
let line = indent;
|
|
177
|
+
const topY = Math.min(targetPixelHeight - 1, row * 2);
|
|
178
|
+
const bottomY = Math.min(targetPixelHeight - 1, row * 2 + 1);
|
|
179
|
+
for (let column = 0; column < targetWidth; column++) {
|
|
180
|
+
const leftX = column * 2;
|
|
181
|
+
const rightX = leftX + 1;
|
|
182
|
+
const cell = encodeQuadrants([
|
|
183
|
+
pixelAt(leftX, topY),
|
|
184
|
+
pixelAt(rightX, topY),
|
|
185
|
+
pixelAt(leftX, bottomY),
|
|
186
|
+
pixelAt(rightX, bottomY),
|
|
187
|
+
]);
|
|
188
|
+
line += `${foreground(...cell.foregroundColor)}${background(...cell.backgroundColor)}${cell.glyph}`;
|
|
189
|
+
}
|
|
190
|
+
lines.push(`${line}${RESET}`);
|
|
191
|
+
}
|
|
192
|
+
return lines;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function renderJpegFile(filePath, options = {}) {
|
|
196
|
+
return renderJpegToAnsi(readFileSync(filePath), options);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function discoverImages(directory) {
|
|
200
|
+
try {
|
|
201
|
+
return readdirSync(directory, { withFileTypes: true })
|
|
202
|
+
.filter(entry => entry.isFile() && /\.jpe?g$/i.test(entry.name))
|
|
203
|
+
.map(entry => path.join(directory, entry.name))
|
|
204
|
+
.sort((a, b) => a.localeCompare(b));
|
|
205
|
+
} catch {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function createTerminalImageGallery({
|
|
211
|
+
directory = DEFAULT_IMAGE_DIRECTORY,
|
|
212
|
+
frequency = 3,
|
|
213
|
+
env = process.env,
|
|
214
|
+
isTTY = process.stdout.isTTY,
|
|
215
|
+
getColumns = () => process.stdout.columns || 80,
|
|
216
|
+
getRows = () => process.stdout.rows || 24,
|
|
217
|
+
} = {}) {
|
|
218
|
+
const images = discoverImages(directory);
|
|
219
|
+
const enabled = images.length > 0 && supportsTerminalImages({ env, isTTY });
|
|
220
|
+
let imageIndex = 0;
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
enabled,
|
|
224
|
+
imageCount: images.length,
|
|
225
|
+
maybeRender(completedQueries = 0, { force = false } = {}) {
|
|
226
|
+
const cadence = Math.max(1, Math.floor(frequency));
|
|
227
|
+
if (!enabled || (!force && (completedQueries <= 0 || completedQueries % cadence !== 0))) return [];
|
|
228
|
+
|
|
229
|
+
const currentIndex = imageIndex % images.length;
|
|
230
|
+
const filePath = images[currentIndex];
|
|
231
|
+
try {
|
|
232
|
+
const imageLines = renderJpegFile(filePath, {
|
|
233
|
+
columns: getColumns(),
|
|
234
|
+
rows: getRows(),
|
|
235
|
+
});
|
|
236
|
+
imageIndex++;
|
|
237
|
+
return [
|
|
238
|
+
'',
|
|
239
|
+
` \x1b[1m\x1b[38;2;255;126;179m◆ KIKU MOMENT\x1b[0m \x1b[38;2;148;163;184m• Gallery ${currentIndex + 1}/${images.length}\x1b[0m`,
|
|
240
|
+
...imageLines,
|
|
241
|
+
'',
|
|
242
|
+
];
|
|
243
|
+
} catch {
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
package/server/agent/tools.js
CHANGED
|
@@ -30,9 +30,16 @@ export function buildQqMusicSearchUrl(query) {
|
|
|
30
30
|
return `https://y.qq.com/n/ryqq/search?w=${encodeURIComponent(query)}`;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
export function buildYoukuSearchUrl(query) {
|
|
34
|
-
return `https://so.youku.com/search_video/q_${encodeURIComponent(query)}`;
|
|
35
|
-
}
|
|
33
|
+
export function buildYoukuSearchUrl(query) {
|
|
34
|
+
return `https://so.youku.com/search_video/q_${encodeURIComponent(query)}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function formatCatalogTitle(primary, secondary) {
|
|
38
|
+
const first = String(primary || secondary || '').trim();
|
|
39
|
+
const second = String(secondary || '').trim();
|
|
40
|
+
if (!second || first.toLocaleLowerCase().includes(second.toLocaleLowerCase())) return first;
|
|
41
|
+
return `${first} (${second})`;
|
|
42
|
+
}
|
|
36
43
|
|
|
37
44
|
export class ToolCache {
|
|
38
45
|
constructor(maxSize = 300, defaultTtlMs = 600_000) {
|
|
@@ -302,9 +309,11 @@ export const toolsSchema = [
|
|
|
302
309
|
description: "Query awards and honors received by Ju Jingyi across music, television, and cultural influence.",
|
|
303
310
|
parameters: {
|
|
304
311
|
type: "object",
|
|
305
|
-
properties: {
|
|
306
|
-
category: { type: "string", description: "Category (Acting, Music, Honor)" },
|
|
307
|
-
year: { type: "number", description: "Year filter" }
|
|
312
|
+
properties: {
|
|
313
|
+
category: { type: "string", description: "Category (Acting, Music, Honor)" },
|
|
314
|
+
year: { type: "number", description: "Year filter" },
|
|
315
|
+
query: { type: "string", description: "Award title, ceremony, or category keyword" },
|
|
316
|
+
limit: { type: "number", description: "Maximum results from 1 to 20" }
|
|
308
317
|
},
|
|
309
318
|
required: []
|
|
310
319
|
}
|
|
@@ -332,9 +341,11 @@ export const toolsSchema = [
|
|
|
332
341
|
description: "Search visual photo gallery of Ju Jingyi (red carpet, photoshoot, drama stills).",
|
|
333
342
|
parameters: {
|
|
334
343
|
type: "object",
|
|
335
|
-
properties: {
|
|
336
|
-
category: { type: "string", description: "Category (fashion-magazine, red-carpet, drama-stills)" },
|
|
337
|
-
|
|
344
|
+
properties: {
|
|
345
|
+
category: { type: "string", description: "Category (fashion-magazine, red-carpet, drama-stills)" },
|
|
346
|
+
query: { type: "string", description: "Photo title or category keyword" },
|
|
347
|
+
year: { type: "number", description: "Year filter" },
|
|
348
|
+
limit: { type: "number", description: "Number of photos from 1 to 20" }
|
|
338
349
|
},
|
|
339
350
|
required: []
|
|
340
351
|
}
|
|
@@ -726,6 +737,30 @@ export const toolsSchema = [
|
|
|
726
737
|
required: ["action"]
|
|
727
738
|
}
|
|
728
739
|
}
|
|
740
|
+
},
|
|
741
|
+
{
|
|
742
|
+
type: "function",
|
|
743
|
+
function: {
|
|
744
|
+
name: "run_quiz",
|
|
745
|
+
description: "Fetch the Ju Jingyi quiz challenge session and return all 15 questions with shuffled options for the user to answer interactively via /quiz command.",
|
|
746
|
+
parameters: {
|
|
747
|
+
type: "object",
|
|
748
|
+
properties: {
|
|
749
|
+
lang: { type: "string", enum: ["th", "en", "zh"], description: "Language for quiz questions (default: auto-detect from user preference)." }
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
type: "function",
|
|
756
|
+
function: {
|
|
757
|
+
name: "get_api_key",
|
|
758
|
+
description: "Retrieve the stored Developer VIP API key for authenticated requests to the Ju Jingyi API.",
|
|
759
|
+
parameters: {
|
|
760
|
+
type: "object",
|
|
761
|
+
properties: {}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
729
764
|
}
|
|
730
765
|
];
|
|
731
766
|
|
|
@@ -829,7 +864,7 @@ export const toolHandlers = {
|
|
|
829
864
|
youtubeTrailerSearch: buildYouTubeSearchUrl(`${titleSearch} Trailer Official`),
|
|
830
865
|
youkuSearchUrl: buildYoukuSearchUrl(w.titleCn || w.titleEn),
|
|
831
866
|
themeSong: w.themeSong ? {
|
|
832
|
-
title:
|
|
867
|
+
title: formatCatalogTitle(w.themeSong.titleTh, w.themeSong.titleCn),
|
|
833
868
|
youtubeUrl: themeSongYtId ? buildYouTubeWatchUrl(themeSongYtId) : null,
|
|
834
869
|
youtubeSearchUrl: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${w.themeSong.titleCn || w.themeSong.titleTh} OST`)
|
|
835
870
|
} : null
|
|
@@ -875,7 +910,7 @@ export const toolHandlers = {
|
|
|
875
910
|
synopsis: work.synopsis?.slice(0, 250) + (work.synopsis?.length > 250 ? '...' : ''),
|
|
876
911
|
highlights: (work.highlights || []).slice(0, 3),
|
|
877
912
|
themeSong: work.themeSong ? {
|
|
878
|
-
title:
|
|
913
|
+
title: formatCatalogTitle(work.themeSong.titleTh, work.themeSong.titleCn),
|
|
879
914
|
youtubeUrl: themeSongYtId ? buildYouTubeWatchUrl(themeSongYtId) : null,
|
|
880
915
|
youtubeSearchUrl: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${work.themeSong.titleCn || work.themeSong.titleTh} OST`)
|
|
881
916
|
} : null
|
|
@@ -885,7 +920,7 @@ export const toolHandlers = {
|
|
|
885
920
|
|
|
886
921
|
get_character_roles: async ({ query } = {}) => {
|
|
887
922
|
const roles = worksData.map(w => ({
|
|
888
|
-
workTitle:
|
|
923
|
+
workTitle: formatCatalogTitle(w.titleTh, w.titleCn),
|
|
889
924
|
year: w.year,
|
|
890
925
|
role: w.role,
|
|
891
926
|
roleCn: w.roleCn,
|
|
@@ -940,7 +975,7 @@ export const toolHandlers = {
|
|
|
940
975
|
count: list.length,
|
|
941
976
|
songs: list.slice(0, 8).map(s => ({
|
|
942
977
|
id: s.id,
|
|
943
|
-
title:
|
|
978
|
+
title: formatCatalogTitle(s.titleTh, s.titleCn),
|
|
944
979
|
titleEn: s.titleEn,
|
|
945
980
|
year: s.year,
|
|
946
981
|
type: s.work,
|
|
@@ -991,7 +1026,7 @@ export const toolHandlers = {
|
|
|
991
1026
|
const searchTitle = `Ju Jingyi 鞠婧祎 ${m.titleCn || m.titleTh} ${m.category === 'Stage-Performance' ? 'Stage Live' : 'Official MV'}`;
|
|
992
1027
|
return {
|
|
993
1028
|
id: m.id,
|
|
994
|
-
title:
|
|
1029
|
+
title: formatCatalogTitle(m.titleTh, m.titleCn),
|
|
995
1030
|
category: m.category,
|
|
996
1031
|
year: m.releaseYear || m.year,
|
|
997
1032
|
drama: m.dramaName || null,
|
|
@@ -1021,7 +1056,7 @@ export const toolHandlers = {
|
|
|
1021
1056
|
(m.dramaName && m.dramaName.toLowerCase().includes(q))
|
|
1022
1057
|
).map(m => ({
|
|
1023
1058
|
type: m.category || 'MV',
|
|
1024
|
-
title:
|
|
1059
|
+
title: formatCatalogTitle(m.titleTh, m.titleCn),
|
|
1025
1060
|
year: m.releaseYear || m.year,
|
|
1026
1061
|
duration: m.duration,
|
|
1027
1062
|
youtubeUrl: m.youtubeId ? buildYouTubeWatchUrl(m.youtubeId) : null,
|
|
@@ -1036,7 +1071,7 @@ export const toolHandlers = {
|
|
|
1036
1071
|
(s.work && s.work.toLowerCase().includes(q))
|
|
1037
1072
|
).map(s => ({
|
|
1038
1073
|
type: s.work || 'Song',
|
|
1039
|
-
title:
|
|
1074
|
+
title: formatCatalogTitle(s.titleTh, s.titleCn),
|
|
1040
1075
|
year: s.year,
|
|
1041
1076
|
duration: s.duration,
|
|
1042
1077
|
youtubeUrl: s.youtubeUrl || (s.youtubeId ? buildYouTubeWatchUrl(s.youtubeId) : null),
|
|
@@ -1051,14 +1086,14 @@ export const toolHandlers = {
|
|
|
1051
1086
|
(w.titleEn && w.titleEn.toLowerCase().includes(q))
|
|
1052
1087
|
).map(w => ({
|
|
1053
1088
|
type: 'Drama / Series',
|
|
1054
|
-
title:
|
|
1089
|
+
title: formatCatalogTitle(w.titleTh, w.titleCn),
|
|
1055
1090
|
year: w.year,
|
|
1056
1091
|
platform: w.streamPlatform,
|
|
1057
1092
|
streamUrl: w.streamUrl || null,
|
|
1058
1093
|
youtubeTrailerSearch: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${w.titleCn || w.titleEn} Trailer Official`),
|
|
1059
1094
|
youkuSearchUrl: buildYoukuSearchUrl(w.titleCn || w.titleEn),
|
|
1060
1095
|
themeSong: w.themeSong ? {
|
|
1061
|
-
title:
|
|
1096
|
+
title: formatCatalogTitle(w.themeSong.titleTh, w.themeSong.titleCn),
|
|
1062
1097
|
youtubeUrl: w.themeSong.youtubeId ? buildYouTubeWatchUrl(w.themeSong.youtubeId) : null,
|
|
1063
1098
|
youtubeSearchUrl: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${w.themeSong.titleCn || w.themeSong.titleTh} OST`)
|
|
1064
1099
|
} : null
|
|
@@ -1135,26 +1170,32 @@ export const toolHandlers = {
|
|
|
1135
1170
|
count: eras.length,
|
|
1136
1171
|
milestones: eras.slice(0, 6).map(e => ({
|
|
1137
1172
|
period: e.period || e.year,
|
|
1138
|
-
title:
|
|
1173
|
+
title: formatCatalogTitle(e.title, e.titleCn),
|
|
1139
1174
|
summary: e.summary?.slice(0, 150),
|
|
1140
1175
|
youtubeSearchUrl: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${e.titleCn || e.title}`)
|
|
1141
1176
|
}))
|
|
1142
1177
|
};
|
|
1143
1178
|
},
|
|
1144
1179
|
|
|
1145
|
-
get_awards: async ({ category, year } = {}) => {
|
|
1146
|
-
let list = Array.isArray(awardsData) ? awardsData : (awardsData.awards || []);
|
|
1147
|
-
if (year) list = list.filter(a => a.year === Number(year));
|
|
1148
|
-
if (category && category !== 'all') list = list.filter(a => a.category?.toLowerCase().includes(category.toLowerCase()));
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1180
|
+
get_awards: async ({ category, year, query, limit = 8 } = {}) => {
|
|
1181
|
+
let list = Array.isArray(awardsData) ? awardsData : (awardsData.awards || []);
|
|
1182
|
+
if (year) list = list.filter(a => a.year === Number(year));
|
|
1183
|
+
if (category && category !== 'all') list = list.filter(a => a.category?.toLowerCase().includes(category.toLowerCase()));
|
|
1184
|
+
if (query) {
|
|
1185
|
+
const q = String(query).toLowerCase().trim();
|
|
1186
|
+
list = list.filter(a => [a.titleTh, a.titleCn, a.titleEn, a.ceremony, a.category].filter(Boolean).join(' ').toLowerCase().includes(q));
|
|
1187
|
+
}
|
|
1188
|
+
const safeLimit = Math.min(Math.max(Number(limit) || 8, 1), 20);
|
|
1189
|
+
|
|
1190
|
+
return {
|
|
1191
|
+
status: "success",
|
|
1192
|
+
count: list.length,
|
|
1193
|
+
awards: list.slice(0, safeLimit).map((a, index) => ({
|
|
1194
|
+
id: a.id || `award-${a.year || 'unknown'}-${index + 1}`,
|
|
1195
|
+
year: a.year,
|
|
1196
|
+
title: `${a.titleTh || a.titleEn || a.titleCn || 'Award'}${a.titleCn && a.titleCn !== a.titleTh ? ` (${a.titleCn})` : ''}`,
|
|
1197
|
+
ceremony: a.ceremony,
|
|
1198
|
+
category: a.category,
|
|
1158
1199
|
youtubeSearchUrl: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${a.titleCn || a.ceremony}`)
|
|
1159
1200
|
}))
|
|
1160
1201
|
};
|
|
@@ -1176,15 +1217,22 @@ export const toolHandlers = {
|
|
|
1176
1217
|
};
|
|
1177
1218
|
},
|
|
1178
1219
|
|
|
1179
|
-
get_gallery_photos: async ({ category, limit = 4 } = {}) => {
|
|
1180
|
-
let photos = Array.isArray(galleryData) ? galleryData : (galleryData.photos || []);
|
|
1181
|
-
if (category && category !== 'all') photos = photos.filter(p => p.category?.toLowerCase().includes(category.toLowerCase()));
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1220
|
+
get_gallery_photos: async ({ category, query, year, limit = 4 } = {}) => {
|
|
1221
|
+
let photos = Array.isArray(galleryData) ? galleryData : (galleryData.photos || []);
|
|
1222
|
+
if (category && category !== 'all') photos = photos.filter(p => p.category?.toLowerCase().includes(category.toLowerCase()));
|
|
1223
|
+
if (query) {
|
|
1224
|
+
const q = String(query).toLowerCase().trim();
|
|
1225
|
+
photos = photos.filter(p => [p.titleTh, p.titleCn, p.titleEn, p.category, p.description].filter(Boolean).join(' ').toLowerCase().includes(q));
|
|
1226
|
+
}
|
|
1227
|
+
if (year) photos = photos.filter(p => Number(p.era || p.year) === Number(year));
|
|
1228
|
+
const safeLimit = Math.min(Math.max(Number(limit) || 4, 1), 20);
|
|
1229
|
+
|
|
1230
|
+
return {
|
|
1231
|
+
status: "success",
|
|
1232
|
+
count: Math.min(photos.length, safeLimit),
|
|
1233
|
+
photos: photos.slice(0, safeLimit).map((p, index) => ({
|
|
1234
|
+
id: p.id || `photo-${p.era || p.year || 'unknown'}-${index + 1}`,
|
|
1235
|
+
title: p.titleTh || p.titleCn,
|
|
1188
1236
|
category: p.category,
|
|
1189
1237
|
year: p.era || p.year
|
|
1190
1238
|
}))
|
|
@@ -1220,7 +1268,7 @@ export const toolHandlers = {
|
|
|
1220
1268
|
const mvs = getAllMusicVideos().filter(m =>
|
|
1221
1269
|
matchesQuery(m.titleTh, m.titleCn, m.titlePinyin, m.dramaName, m.description)
|
|
1222
1270
|
).slice(0, 4).map(m => ({
|
|
1223
|
-
title: `${m.titleTh
|
|
1271
|
+
title: `${formatCatalogTitle(m.titleTh, m.titleCn)} [${m.category}]`,
|
|
1224
1272
|
youtubeUrl: m.youtubeId ? buildYouTubeWatchUrl(m.youtubeId) : null,
|
|
1225
1273
|
youtubeSearchUrl: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${m.titleCn || m.titleTh}`)
|
|
1226
1274
|
}));
|
|
@@ -1616,7 +1664,69 @@ export const toolHandlers = {
|
|
|
1616
1664
|
};
|
|
1617
1665
|
}
|
|
1618
1666
|
}
|
|
1619
|
-
}
|
|
1667
|
+
},
|
|
1668
|
+
|
|
1669
|
+
run_quiz: async ({ lang } = {}, options = {}) => {
|
|
1670
|
+
let memory = options?.agent?.memory || options?.memory;
|
|
1671
|
+
if (!memory) {
|
|
1672
|
+
const { AgentMemory } = await import('./memory.js');
|
|
1673
|
+
memory = new AgentMemory();
|
|
1674
|
+
await memory.load();
|
|
1675
|
+
}
|
|
1676
|
+
const quizLang = ['th', 'en', 'zh'].includes(lang) ? lang : (memory.preferences?.language || 'en');
|
|
1677
|
+
const { shuffleQuizQuestions } = await import('./quizData.js');
|
|
1678
|
+
const { QUIZ_QUESTIONS, QUIZ_I18N, LETTER_LABELS } = await import('./quizData.js');
|
|
1679
|
+
const i18n = QUIZ_I18N[quizLang] || QUIZ_I18N.en;
|
|
1680
|
+
const shuffled = shuffleQuizQuestions(QUIZ_QUESTIONS);
|
|
1681
|
+
|
|
1682
|
+
let challengeToken = null;
|
|
1683
|
+
try {
|
|
1684
|
+
const baseUrl = options?.baseUrl || 'http://localhost:3000';
|
|
1685
|
+
const res = await fetch(`${baseUrl}/api/v1/quiz/challenge`);
|
|
1686
|
+
if (res.ok) {
|
|
1687
|
+
const json = await res.json();
|
|
1688
|
+
challengeToken = json.data?.challengeToken;
|
|
1689
|
+
}
|
|
1690
|
+
} catch {}
|
|
1691
|
+
|
|
1692
|
+
return {
|
|
1693
|
+
status: 'success',
|
|
1694
|
+
message: i18n.subtitle,
|
|
1695
|
+
lang: quizLang,
|
|
1696
|
+
challengeToken,
|
|
1697
|
+
instructions: 'Present each question one by one. User answers with A/B/C/D or 1-4. After all 15, use POST /api/v1/keys/claim with challengeToken and answers array [{qIndex, answerIndex}].',
|
|
1698
|
+
questions: shuffled.map((q, i) => ({
|
|
1699
|
+
index: i,
|
|
1700
|
+
question: q.q[quizLang] || q.q.en,
|
|
1701
|
+
options: (q.options[quizLang] || q.options.en).map((opt, idx) => `${LETTER_LABELS[idx]}) ${opt}`),
|
|
1702
|
+
hint: q.hint[quizLang] || q.hint.en,
|
|
1703
|
+
}))
|
|
1704
|
+
};
|
|
1705
|
+
},
|
|
1706
|
+
|
|
1707
|
+
get_api_key: async (_args = {}, options = {}) => {
|
|
1708
|
+
let memory = options?.agent?.memory || options?.memory;
|
|
1709
|
+
if (!memory) {
|
|
1710
|
+
const { AgentMemory } = await import('./memory.js');
|
|
1711
|
+
memory = new AgentMemory();
|
|
1712
|
+
await memory.load();
|
|
1713
|
+
}
|
|
1714
|
+
const key = memory.preferences?.vipKey;
|
|
1715
|
+
if (key) {
|
|
1716
|
+
return {
|
|
1717
|
+
status: 'success',
|
|
1718
|
+
apiKey: key,
|
|
1719
|
+
tier: 'Developer VIP',
|
|
1720
|
+
rateLimit: '1,000 requests/minute',
|
|
1721
|
+
usage: `Header: X-API-Key: ${key} | Query: ?apiKey=${key}`
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
return {
|
|
1725
|
+
status: 'info',
|
|
1726
|
+
message: 'No VIP key stored. Run /quiz in the CLI to complete the challenge and claim one.',
|
|
1727
|
+
apiKey: null
|
|
1728
|
+
};
|
|
1729
|
+
},
|
|
1620
1730
|
};
|
|
1621
1731
|
|
|
1622
1732
|
const toolSchemaByName = new Map(
|