kiku-agent 1.0.0 → 1.0.4
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 +293 -122
- package/package.json +60 -61
- package/server/agent/agentCore.js +892 -782
- package/server/agent/appPaths.js +21 -0
- package/server/agent/audioPlayer.js +739 -673
- package/server/agent/cli.js +1414 -1111
- package/server/agent/commandRegistry.js +74 -58
- package/server/agent/commands.js +1694 -788
- package/server/agent/config.js +264 -90
- package/server/agent/contextCompactor.js +79 -0
- package/server/agent/doctor.js +69 -0
- package/server/agent/eventBus.js +107 -103
- package/server/agent/gpuMetrics.js +189 -0
- package/server/agent/llmClient.js +369 -200
- package/server/agent/mcpClient.js +364 -0
- package/server/agent/memory.js +670 -566
- package/server/agent/modelManager.js +77 -0
- package/server/agent/research.js +202 -0
- package/server/agent/session.js +244 -239
- package/server/agent/subagent.js +116 -0
- package/server/agent/theme.js +125 -124
- package/server/agent/toolRouter.js +201 -0
- package/server/agent/tools.js +1679 -1528
- package/server/agent/ui.js +760 -469
- package/server/agent/youtubeAuth.js +144 -65
- package/server/mcp/claude-desktop-config.example.json +1 -1
- package/server/mcp/data/discography.js +36 -36
- package/server/mcp/dataAdapter.js +102 -0
- package/server/mcp/examples/claude-code.mcp.example.json +19 -0
- package/server/mcp/examples/claude-desktop.example.json +11 -0
- package/server/mcp/examples/cline.example.json +13 -0
- package/server/mcp/examples/cursor.example.json +14 -0
- package/server/mcp/examples/gemini-cli.example.json +17 -0
- package/server/mcp/examples/opencode.example.json +21 -0
- package/server/mcp/examples/vscode.example.json +19 -0
- package/server/mcp/examples/windsurf.example.json +14 -0
- package/server/mcp/handler.js +85 -159
- package/server/mcp/security.js +140 -138
- package/server/mcp/server.js +129 -100
- package/server/mcp/stdio.js +4 -2
|
@@ -2,41 +2,64 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { spawn, execSync } from 'child_process';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
|
+
import { KIKU_DATA_DIR } from './appPaths.js';
|
|
5
6
|
|
|
6
7
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
8
|
const ENV_PATH = path.resolve(__dirname, '..', '..', '.env');
|
|
9
|
+
const AUTH_CONFIG_PATH = path.join(KIKU_DATA_DIR, 'yt_auth.json');
|
|
10
|
+
const SECURE_COOKIE_PATH = path.join(KIKU_DATA_DIR, 'cookies.txt');
|
|
8
11
|
const YTDLP_CMD = process.env.KIKU_YTDLP || 'yt-dlp';
|
|
9
12
|
|
|
10
|
-
const BROWSERS = ['chrome', 'edge', 'brave', 'firefox'];
|
|
11
|
-
|
|
12
13
|
|
|
14
|
+
const BROWSERS = ['firefox', 'chrome', 'edge', 'brave'];
|
|
13
15
|
|
|
14
16
|
function findBrowserExe(browser) {
|
|
15
17
|
const isWin = process.platform === 'win32';
|
|
16
18
|
const probes = {
|
|
17
|
-
|
|
18
|
-
'
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
19
|
+
firefox: {
|
|
20
|
+
cmd: isWin ? 'firefox' : 'firefox',
|
|
21
|
+
paths: [
|
|
22
|
+
'C:\\Program Files\\Mozilla Firefox\\firefox.exe',
|
|
23
|
+
'C:\\Program Files (x86)\\Mozilla Firefox\\firefox.exe',
|
|
24
|
+
(process.env.LOCALAPPDATA || '') + '\\Mozilla Firefox\\firefox.exe'
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
chrome: {
|
|
28
|
+
cmd: isWin ? 'chrome' : 'google-chrome',
|
|
29
|
+
paths: [
|
|
30
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
31
|
+
(process.env.LOCALAPPDATA || '') + '\\Google\\Chrome\\Application\\chrome.exe',
|
|
32
|
+
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe'
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
edge: {
|
|
36
|
+
cmd: isWin ? 'msedge' : 'microsoft-edge',
|
|
37
|
+
paths: [
|
|
38
|
+
'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
39
|
+
'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
40
|
+
(process.env.LOCALAPPDATA || '') + '\\Microsoft\\Edge\\Application\\msedge.exe'
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
brave: {
|
|
44
|
+
cmd: isWin ? 'brave' : 'brave-browser',
|
|
45
|
+
paths: [
|
|
46
|
+
'C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe',
|
|
47
|
+
(process.env.LOCALAPPDATA || '') + '\\BraveSoftware\\Brave-Browser\\Application\\brave.exe'
|
|
48
|
+
]
|
|
49
|
+
}
|
|
34
50
|
};
|
|
51
|
+
|
|
35
52
|
const p = probes[browser];
|
|
36
53
|
if (!p) return null;
|
|
37
|
-
try {
|
|
54
|
+
try {
|
|
55
|
+
execSync(isWin ? `where ${p.cmd}` : `which ${p.cmd}`, { stdio: 'ignore', windowsHide: true });
|
|
56
|
+
return p.cmd;
|
|
57
|
+
} catch {}
|
|
58
|
+
|
|
38
59
|
if (isWin) {
|
|
39
|
-
for (const fp of p.paths) {
|
|
60
|
+
for (const fp of p.paths) {
|
|
61
|
+
if (fp && fs.existsSync(fp)) return fp;
|
|
62
|
+
}
|
|
40
63
|
}
|
|
41
64
|
return null;
|
|
42
65
|
}
|
|
@@ -63,9 +86,35 @@ function openBrowser(url, browser) {
|
|
|
63
86
|
}
|
|
64
87
|
}
|
|
65
88
|
|
|
89
|
+
function readAuthConfig() {
|
|
90
|
+
try {
|
|
91
|
+
if (fs.existsSync(AUTH_CONFIG_PATH)) {
|
|
92
|
+
return JSON.parse(fs.readFileSync(AUTH_CONFIG_PATH, 'utf8')) || {};
|
|
93
|
+
}
|
|
94
|
+
} catch {}
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
66
97
|
|
|
98
|
+
function writeAuthConfig(updates) {
|
|
99
|
+
try {
|
|
100
|
+
fs.mkdirSync(KIKU_DATA_DIR, { recursive: true });
|
|
101
|
+
const current = readAuthConfig();
|
|
102
|
+
const merged = { ...current, ...updates };
|
|
103
|
+
fs.writeFileSync(AUTH_CONFIG_PATH, JSON.stringify(merged, null, 2), 'utf8');
|
|
104
|
+
} catch {}
|
|
105
|
+
}
|
|
67
106
|
|
|
68
107
|
function loadEnv() {
|
|
108
|
+
|
|
109
|
+
const userConfig = readAuthConfig();
|
|
110
|
+
if (userConfig.KIKU_YT_BROWSER && process.env.KIKU_YT_BROWSER === undefined) {
|
|
111
|
+
process.env.KIKU_YT_BROWSER = userConfig.KIKU_YT_BROWSER;
|
|
112
|
+
}
|
|
113
|
+
if (userConfig.KIKU_YT_COOKIES && process.env.KIKU_YT_COOKIES === undefined) {
|
|
114
|
+
process.env.KIKU_YT_COOKIES = userConfig.KIKU_YT_COOKIES;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
69
118
|
try {
|
|
70
119
|
if (!fs.existsSync(ENV_PATH)) return;
|
|
71
120
|
const text = fs.readFileSync(ENV_PATH, 'utf8');
|
|
@@ -73,7 +122,7 @@ function loadEnv() {
|
|
|
73
122
|
const m = line.match(/^\s*([\w.-]+)\s*=\s*(.*)\s*$/);
|
|
74
123
|
if (!m) continue;
|
|
75
124
|
const key = m[1];
|
|
76
|
-
|
|
125
|
+
const val = m[2].replace(/^["']|["']$/g, '');
|
|
77
126
|
if (process.env[key] === undefined) process.env[key] = val;
|
|
78
127
|
}
|
|
79
128
|
} catch {}
|
|
@@ -92,24 +141,32 @@ function readEnvMap() {
|
|
|
92
141
|
return map;
|
|
93
142
|
}
|
|
94
143
|
|
|
95
|
-
|
|
96
144
|
function writeEnv(updates) {
|
|
97
|
-
const map = readEnvMap();
|
|
98
|
-
Object.assign(map, updates);
|
|
99
|
-
const out = Object.entries(map).map(([k, v]) => `${k}=${v}`).join('\n') + '\n';
|
|
100
|
-
fs.writeFileSync(ENV_PATH, out, 'utf8');
|
|
101
|
-
}
|
|
102
145
|
|
|
146
|
+
for (const [k, v] of Object.entries(updates)) {
|
|
147
|
+
process.env[k] = v;
|
|
148
|
+
}
|
|
149
|
+
writeAuthConfig(updates);
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const map = readEnvMap();
|
|
154
|
+
Object.assign(map, updates);
|
|
155
|
+
const out = Object.entries(map).map(([k, v]) => `${k}=${v}`).join('\n') + '\n';
|
|
156
|
+
fs.writeFileSync(ENV_PATH, out, 'utf8');
|
|
157
|
+
} catch {}
|
|
158
|
+
}
|
|
103
159
|
|
|
104
160
|
function hasSession(browser) {
|
|
105
161
|
return new Promise((resolve) => {
|
|
106
|
-
const timeoutMs =
|
|
162
|
+
const timeoutMs = 8000;
|
|
107
163
|
const args = [
|
|
108
164
|
'--cookies-from-browser', browser,
|
|
109
165
|
'--print', '%(title)s',
|
|
110
166
|
'--no-warnings',
|
|
167
|
+
'--no-playlist',
|
|
111
168
|
'--no-check-certificates',
|
|
112
|
-
'
|
|
169
|
+
'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
|
|
113
170
|
];
|
|
114
171
|
const p = spawn(YTDLP_CMD, args, {
|
|
115
172
|
windowsHide: true,
|
|
@@ -128,51 +185,71 @@ function hasSession(browser) {
|
|
|
128
185
|
}
|
|
129
186
|
}, timeoutMs);
|
|
130
187
|
|
|
131
|
-
p.stdout
|
|
132
|
-
p.stderr
|
|
133
|
-
p.on('error', () => {
|
|
188
|
+
p.stdout?.on('data', d => { out += d.toString(); });
|
|
189
|
+
p.stderr?.on('data', d => { err += d.toString(); });
|
|
190
|
+
p.on('error', () => {
|
|
191
|
+
if (!resolved) {
|
|
192
|
+
resolved = true;
|
|
193
|
+
clearTimeout(timer);
|
|
194
|
+
resolve(false);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
134
197
|
p.on('close', (code) => {
|
|
135
198
|
if (resolved) return;
|
|
136
199
|
resolved = true;
|
|
137
200
|
clearTimeout(timer);
|
|
138
201
|
const title = out.trim();
|
|
139
|
-
const blocked = /403|Forbidden|Sign in|confirm you'?re not a bot|Login Required|Private Video/i.test(err);
|
|
202
|
+
const blocked = /403|Forbidden|Sign in|confirm you'?re not a bot|Login Required|Private Video|database is locked/i.test(err);
|
|
140
203
|
resolve(code === 0 && title.length > 0 && !blocked);
|
|
141
204
|
});
|
|
142
205
|
});
|
|
143
206
|
}
|
|
144
207
|
|
|
145
|
-
|
|
146
|
-
|
|
147
208
|
let _ensured = null;
|
|
148
209
|
let _lastFailTime = 0;
|
|
149
|
-
|
|
150
|
-
|
|
210
|
+
let _inFlightPromise = null;
|
|
211
|
+
const RETRY_COOLDOWN_MS = 30000;
|
|
151
212
|
|
|
152
213
|
export async function ensureYouTubeCookies(opts = {}) {
|
|
153
|
-
|
|
154
214
|
if (_ensured && _ensured.ok) return _ensured;
|
|
155
215
|
if (_ensured && !_ensured.ok && (Date.now() - _lastFailTime) < RETRY_COOLDOWN_MS && !opts.force) {
|
|
156
216
|
return _ensured;
|
|
157
217
|
}
|
|
158
218
|
|
|
219
|
+
|
|
220
|
+
if (_inFlightPromise) {
|
|
221
|
+
return _inFlightPromise;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
_inFlightPromise = (async () => {
|
|
225
|
+
try {
|
|
226
|
+
return await _ensureYouTubeCookiesImpl(opts);
|
|
227
|
+
} finally {
|
|
228
|
+
_inFlightPromise = null;
|
|
229
|
+
}
|
|
230
|
+
})();
|
|
231
|
+
|
|
232
|
+
return _inFlightPromise;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function _ensureYouTubeCookiesImpl(opts = {}) {
|
|
159
236
|
loadEnv();
|
|
160
237
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
238
|
+
|
|
239
|
+
const cookiesCandidate = process.env.KIKU_YT_COOKIES || (fs.existsSync(SECURE_COOKIE_PATH) ? SECURE_COOKIE_PATH : null);
|
|
240
|
+
if (cookiesCandidate && fs.existsSync(cookiesCandidate)) {
|
|
241
|
+
_ensured = { ok: true, source: 'file', cookiesPath: cookiesCandidate };
|
|
164
242
|
return _ensured;
|
|
165
243
|
}
|
|
166
244
|
|
|
167
245
|
|
|
168
246
|
const available = detectBrowsers();
|
|
169
247
|
if (!available.length) {
|
|
170
|
-
_ensured = { ok: false, reason: 'no-browser', detail: 'No supported browser found (Chrome, Edge, Brave
|
|
248
|
+
_ensured = { ok: false, reason: 'no-browser', detail: 'No supported browser found (Firefox, Chrome, Edge, Brave)' };
|
|
171
249
|
_lastFailTime = Date.now();
|
|
172
250
|
return _ensured;
|
|
173
251
|
}
|
|
174
252
|
|
|
175
|
-
|
|
176
253
|
const preferred = process.env.KIKU_YT_BROWSER;
|
|
177
254
|
const candidates = preferred && available.includes(preferred)
|
|
178
255
|
? [preferred, ...available.filter(b => b !== preferred)]
|
|
@@ -180,9 +257,7 @@ export async function ensureYouTubeCookies(opts = {}) {
|
|
|
180
257
|
|
|
181
258
|
for (const b of candidates) {
|
|
182
259
|
if (await hasSession(b)) {
|
|
183
|
-
|
|
184
260
|
writeEnv({ KIKU_YT_BROWSER: b });
|
|
185
|
-
process.env.KIKU_YT_BROWSER = b;
|
|
186
261
|
_ensured = { ok: true, source: 'existing-session', browser: b };
|
|
187
262
|
return _ensured;
|
|
188
263
|
}
|
|
@@ -195,7 +270,7 @@ export async function ensureYouTubeCookies(opts = {}) {
|
|
|
195
270
|
return _ensured;
|
|
196
271
|
}
|
|
197
272
|
|
|
198
|
-
|
|
273
|
+
|
|
199
274
|
const loginBrowser = candidates[0];
|
|
200
275
|
const loginUrl = 'https://accounts.google.com/ServiceLogin?service=youtube&continue=https://www.youtube.com/';
|
|
201
276
|
openBrowser(loginUrl, loginBrowser);
|
|
@@ -208,21 +283,24 @@ export async function ensureYouTubeCookies(opts = {}) {
|
|
|
208
283
|
await new Promise(r => setTimeout(r, 2000));
|
|
209
284
|
|
|
210
285
|
const deadline = Date.now() + (opts.timeout || 120000);
|
|
211
|
-
const pollInterval =
|
|
286
|
+
const pollInterval = 4000;
|
|
212
287
|
let attempt = 0;
|
|
213
288
|
|
|
214
289
|
while (Date.now() < deadline) {
|
|
215
290
|
attempt++;
|
|
216
291
|
const remaining = Math.round((deadline - Date.now()) / 1000);
|
|
217
|
-
process.stdout.
|
|
292
|
+
if (!opts.silent && process.stdout.isTTY) {
|
|
293
|
+
process.stdout.write(`\r Checking session... (attempt ${attempt}, ${remaining}s remaining) `);
|
|
294
|
+
}
|
|
218
295
|
|
|
219
296
|
for (const b of available) {
|
|
220
297
|
if (await hasSession(b)) {
|
|
221
298
|
writeEnv({ KIKU_YT_BROWSER: b });
|
|
222
|
-
process.
|
|
223
|
-
|
|
299
|
+
if (process.stdout.isTTY) {
|
|
300
|
+
process.stdout.write('\r' + ' '.repeat(65) + '\r');
|
|
301
|
+
}
|
|
224
302
|
console.log(` [OK] YouTube session detected in ${b}.`);
|
|
225
|
-
console.log(` Saved KIKU_YT_BROWSER=${b}
|
|
303
|
+
console.log(` Saved preference (KIKU_YT_BROWSER=${b})\n`);
|
|
226
304
|
_ensured = { ok: true, source: 'new-session', browser: b };
|
|
227
305
|
return _ensured;
|
|
228
306
|
}
|
|
@@ -231,14 +309,15 @@ export async function ensureYouTubeCookies(opts = {}) {
|
|
|
231
309
|
await new Promise(r => setTimeout(r, pollInterval));
|
|
232
310
|
}
|
|
233
311
|
|
|
234
|
-
process.stdout.
|
|
312
|
+
if (process.stdout.isTTY) {
|
|
313
|
+
process.stdout.write('\r' + ' '.repeat(65) + '\r');
|
|
314
|
+
}
|
|
235
315
|
console.log(` [ERROR] Login timed out. You can retry by running the /login command.\n`);
|
|
236
316
|
_ensured = { ok: false, reason: 'login-timeout' };
|
|
237
317
|
_lastFailTime = Date.now();
|
|
238
318
|
return _ensured;
|
|
239
319
|
}
|
|
240
320
|
|
|
241
|
-
|
|
242
321
|
export async function exportCookiesToFile() {
|
|
243
322
|
loadEnv();
|
|
244
323
|
|
|
@@ -249,22 +328,22 @@ export async function exportCookiesToFile() {
|
|
|
249
328
|
: available;
|
|
250
329
|
|
|
251
330
|
for (const b of candidates) {
|
|
252
|
-
const cookiesPath = path.resolve(__dirname, '..', '..', '.kiku_yt_cookies.txt');
|
|
253
331
|
try {
|
|
332
|
+
fs.mkdirSync(KIKU_DATA_DIR, { recursive: true });
|
|
254
333
|
const args = [
|
|
255
334
|
'--cookies-from-browser', b,
|
|
256
|
-
'--cookies',
|
|
335
|
+
'--cookies', SECURE_COOKIE_PATH,
|
|
257
336
|
'--no-warnings',
|
|
258
337
|
'--skip-download',
|
|
259
|
-
'
|
|
338
|
+
'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
|
|
260
339
|
];
|
|
261
340
|
await new Promise((resolve, reject) => {
|
|
262
341
|
const p = spawn(YTDLP_CMD, args, { windowsHide: true, stdio: ['ignore', 'ignore', 'pipe'] });
|
|
263
342
|
let err = '';
|
|
264
|
-
p.stderr
|
|
343
|
+
p.stderr?.on('data', d => { err += d.toString(); });
|
|
265
344
|
p.on('error', reject);
|
|
266
345
|
p.on('close', code => {
|
|
267
|
-
if (code === 0 && fs.existsSync(
|
|
346
|
+
if (code === 0 && fs.existsSync(SECURE_COOKIE_PATH)) {
|
|
268
347
|
resolve();
|
|
269
348
|
} else {
|
|
270
349
|
reject(new Error(err || `yt-dlp exited ${code}`));
|
|
@@ -272,9 +351,8 @@ export async function exportCookiesToFile() {
|
|
|
272
351
|
});
|
|
273
352
|
});
|
|
274
353
|
|
|
275
|
-
writeEnv({ KIKU_YT_COOKIES:
|
|
276
|
-
|
|
277
|
-
return { ok: true, browser: b, cookiesPath };
|
|
354
|
+
writeEnv({ KIKU_YT_COOKIES: SECURE_COOKIE_PATH });
|
|
355
|
+
return { ok: true, browser: b, cookiesPath: SECURE_COOKIE_PATH };
|
|
278
356
|
} catch {}
|
|
279
357
|
}
|
|
280
358
|
|
|
@@ -284,16 +362,17 @@ export async function exportCookiesToFile() {
|
|
|
284
362
|
export function resetCookieCheck() {
|
|
285
363
|
_ensured = null;
|
|
286
364
|
_lastFailTime = 0;
|
|
365
|
+
_inFlightPromise = null;
|
|
287
366
|
}
|
|
288
367
|
|
|
289
368
|
export function getCookieStatus() {
|
|
290
369
|
return {
|
|
291
370
|
ensured: _ensured,
|
|
292
371
|
envBrowser: process.env.KIKU_YT_BROWSER || null,
|
|
293
|
-
envCookies: process.env.KIKU_YT_COOKIES || null,
|
|
294
|
-
envPath: ENV_PATH
|
|
372
|
+
envCookies: process.env.KIKU_YT_COOKIES || (fs.existsSync(SECURE_COOKIE_PATH) ? SECURE_COOKIE_PATH : null),
|
|
373
|
+
envPath: ENV_PATH,
|
|
374
|
+
authConfigPath: AUTH_CONFIG_PATH
|
|
295
375
|
};
|
|
296
376
|
}
|
|
297
377
|
|
|
298
|
-
|
|
299
378
|
loadEnv();
|
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
export const discographyData = [
|
|
2
|
-
{ id: "find-the-way", titleEn: "FIND THE WAY", titleCn: "FIND THE WAY", year: 2026, category: "single", duration: "3:30", youtubeId: "9AeErG03SZw" },
|
|
3
|
-
{ id: "set-fire", titleEn: "Set Fire to the Silent World", titleCn: "着火的沉默世界", year: 2026, category: "ost", duration: "3:55", youtubeId: "7FXGhAmHIAo", work: "Wanhua Sin" },
|
|
4
|
-
{ id: "not-suffering", titleEn: "Not Suffering", titleCn: "不苦", year: 2026, category: "ost", duration: "3:50", youtubeId: "DafSXZZo9AI", work: "Veil of Shadows" },
|
|
5
|
-
{ id: "eternal-fragrance-song", titleEn: "The Eternal Fragrance", titleCn: "千香", year: 2026, category: "ost", duration: "3:46", youtubeId: "vIanAvWb_HU", work: "The Eternal Fragrance" },
|
|
6
|
-
{ id: "rain-love", titleEn: "Yu Ai (Rain Love)", titleCn: "雨爱", year: 2026, category: "cover", duration: "4:10", youtubeId: "OMLpNyp8T7Q" },
|
|
7
|
-
{ id: "rosa", titleEn: "Rosa", titleCn: "Rosa", year: 2025, category: "single", duration: "3:20", youtubeId: "jZDzOLbd9OU" },
|
|
8
|
-
{ id: "sleepless-sunset", titleEn: "Sleepless Sunset", titleCn: "落日未眠", year: 2025, category: "single", duration: "3:45", youtubeId: "mYYD2JqZ1tE" },
|
|
9
|
-
{ id: "fall", titleEn: "Fall", titleCn: "落", year: 2024, category: "single", duration: "3:36", youtubeId: "8Nh4ZPhYKEc" },
|
|
10
|
-
{ id: "view-from-wall", titleEn: "View From The Wall", titleCn: "壁上观", year: 2024, category: "single", duration: "3:50", youtubeId: "Lge5XhVl9Fc" },
|
|
11
|
-
{ id: "ru-sha", titleEn: "Ru Sha", titleCn: "如纱", year: 2024, category: "ost", duration: "3:42", youtubeId: "lzDP2MfP3eA", work: "Sword and Fairy 4" },
|
|
12
|
-
{ id: "in-blossom-ost", titleEn: "In Blossom Main Theme", titleCn: "花间令", year: 2024, category: "ost", youtubeId: null, work: "In Blossom" },
|
|
13
|
-
{ id: "thoughts-of-joy", titleEn: "Thoughts of Joy", titleCn: "念思凡", year: 2023, category: "ost", duration: "4:05", youtubeId: "D-vEPdasbLo", work: "Beauty of Resilience" },
|
|
14
|
-
{ id: "dew-before-dawn", titleEn: "Dew Before Dawn", titleCn: "露珠在晨曦前消散", year: 2023, category: "ost", youtubeId: null, work: "Beauty of Resilience" },
|
|
15
|
-
{ id: "0.2s", titleEn: "0.2s", titleCn: "0.2s", year: 2022, category: "single", duration: "3:20", youtubeId: null },
|
|
16
|
-
{ id: "mo-li", titleEn: "Mo Li", titleCn: "莫离", year: 2021, category: "ost", duration: "4:12", youtubeId: "7VSAArsR-go", work: "Rebirth for You" },
|
|
17
|
-
{ id: "qing-cheng-shan-xia", titleEn: "Qing Cheng Shan Xia", titleCn: "青城山下", year: 2021, category: "single", youtubeId: null },
|
|
18
|
-
{ id: "fu-rong", titleEn: "Fu Rong", titleCn: "芙蓉", year: 2020, category: "ost", duration: "3:58", youtubeId: "9ke3efb9U7A", work: "The Blooms at Ruyi Pavilion" },
|
|
19
|
-
{ id: "ancient-painting", titleEn: "Ancient Painting", titleCn: "古画", year: 2020, category: "ost", youtubeId: null, work: "The Blooms at Ruyi Pavilion" },
|
|
20
|
-
{ id: "thousand-years", titleEn: "A Millennium's Wait", titleCn: "千年等一回", year: 2019, category: "ost", duration: "3:44", youtubeId: "-vf6jU96WI4", work: "The Legend of White Snake" },
|
|
21
|
-
{ id: "du-qing", titleEn: "Du Qing", titleCn: "渡情", year: 2019, category: "ost", youtubeId: null, work: "The Legend of White Snake" },
|
|
22
|
-
{ id: "wudaoying", titleEn: "Wu Dao Ying", titleCn: "舞导演", year: 2019, category: "single", youtubeId: null },
|
|
23
|
-
{ id: "gufeng-shanshui", titleEn: "Gufeng Shanshui", titleCn: "古风山水", year: 2019, category: "single", youtubeId: null },
|
|
24
|
-
{ id: "meng-lu", titleEn: "Meng Lu (Dream Road)", titleCn: "梦路", year: 2018, category: "single", youtubeId: null },
|
|
25
|
-
{ id: "zui-chi-luo", titleEn: "Zui Chi Luo", titleCn: "最赤裸", year: 2018, category: "single", youtubeId: null },
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
export const musicVideos = [
|
|
29
|
-
{ id: "mv-find-the-way", titleEn: "FIND THE WAY MV", titleCn: "FIND THE WAY", year: 2026, category: "Official MV", youtubeId: "9AeErG03SZw", duration: "3:30" },
|
|
30
|
-
{ id: "mv-not-suffering", titleEn: "Not Suffering MV", titleCn: "不苦", year: 2026, category: "OST MV", youtubeId: "DafSXZZo9AI", duration: "3:50", work: "Veil of Shadows" },
|
|
31
|
-
{ id: "mv-rain-love", titleEn: "Yu Ai (Rain Love) Stage", titleCn: "雨爱", year: 2026, category: "Stage Performance", youtubeId: "OMLpNyp8T7Q", duration: "4:10" },
|
|
32
|
-
{ id: "mv-set-fire", titleEn: "Set Fire to the Silent World MV", titleCn: "着火的沉默世界", year: 2026, category: "OST MV", youtubeId: "7FXGhAmHIAo", duration: "3:55", work: "Wanhua Sin" },
|
|
33
|
-
{ id: "mv-rosa", titleEn: "Rosa MV", titleCn: "Rosa", year: 2025, category: "Official MV", youtubeId: "jZDzOLbd9OU", duration: "3:20" },
|
|
34
|
-
{ id: "mv-fall", titleEn: "Fall MV", titleCn: "落", year: 2024, category: "Official MV", youtubeId: "8Nh4ZPhYKEc", duration: "3:36" },
|
|
35
|
-
{ id: "mv-ru-sha", titleEn: "Ru Sha MV", titleCn: "如纱", year: 2024, category: "OST MV", youtubeId: "lzDP2MfP3eA", duration: "3:42", work: "Sword and Fairy 4" },
|
|
36
|
-
];
|
|
1
|
+
export const discographyData = [
|
|
2
|
+
{ id: "find-the-way", titleEn: "FIND THE WAY", titleCn: "FIND THE WAY", year: 2026, category: "single", duration: "3:30", youtubeId: "9AeErG03SZw" },
|
|
3
|
+
{ id: "set-fire", titleEn: "Set Fire to the Silent World", titleCn: "着火的沉默世界", year: 2026, category: "ost", duration: "3:55", youtubeId: "7FXGhAmHIAo", work: "Wanhua Sin" },
|
|
4
|
+
{ id: "not-suffering", titleEn: "Not Suffering", titleCn: "不苦", year: 2026, category: "ost", duration: "3:50", youtubeId: "DafSXZZo9AI", work: "Veil of Shadows" },
|
|
5
|
+
{ id: "eternal-fragrance-song", titleEn: "The Eternal Fragrance", titleCn: "千香", year: 2026, category: "ost", duration: "3:46", youtubeId: "vIanAvWb_HU", work: "The Eternal Fragrance" },
|
|
6
|
+
{ id: "rain-love", titleEn: "Yu Ai (Rain Love)", titleCn: "雨爱", year: 2026, category: "cover", duration: "4:10", youtubeId: "OMLpNyp8T7Q" },
|
|
7
|
+
{ id: "rosa", titleEn: "Rosa", titleCn: "Rosa", year: 2025, category: "single", duration: "3:20", youtubeId: "jZDzOLbd9OU" },
|
|
8
|
+
{ id: "sleepless-sunset", titleEn: "Sleepless Sunset", titleCn: "落日未眠", year: 2025, category: "single", duration: "3:45", youtubeId: "mYYD2JqZ1tE" },
|
|
9
|
+
{ id: "fall", titleEn: "Fall", titleCn: "落", year: 2024, category: "single", duration: "3:36", youtubeId: "8Nh4ZPhYKEc" },
|
|
10
|
+
{ id: "view-from-wall", titleEn: "View From The Wall", titleCn: "壁上观", year: 2024, category: "single", duration: "3:50", youtubeId: "Lge5XhVl9Fc" },
|
|
11
|
+
{ id: "ru-sha", titleEn: "Ru Sha", titleCn: "如纱", year: 2024, category: "ost", duration: "3:42", youtubeId: "lzDP2MfP3eA", work: "Sword and Fairy 4" },
|
|
12
|
+
{ id: "in-blossom-ost", titleEn: "In Blossom Main Theme", titleCn: "花间令", year: 2024, category: "ost", youtubeId: null, work: "In Blossom" },
|
|
13
|
+
{ id: "thoughts-of-joy", titleEn: "Thoughts of Joy", titleCn: "念思凡", year: 2023, category: "ost", duration: "4:05", youtubeId: "D-vEPdasbLo", work: "Beauty of Resilience" },
|
|
14
|
+
{ id: "dew-before-dawn", titleEn: "Dew Before Dawn", titleCn: "露珠在晨曦前消散", year: 2023, category: "ost", youtubeId: null, work: "Beauty of Resilience" },
|
|
15
|
+
{ id: "0.2s", titleEn: "0.2s", titleCn: "0.2s", year: 2022, category: "single", duration: "3:20", youtubeId: null },
|
|
16
|
+
{ id: "mo-li", titleEn: "Mo Li", titleCn: "莫离", year: 2021, category: "ost", duration: "4:12", youtubeId: "7VSAArsR-go", work: "Rebirth for You" },
|
|
17
|
+
{ id: "qing-cheng-shan-xia", titleEn: "Qing Cheng Shan Xia", titleCn: "青城山下", year: 2021, category: "single", youtubeId: null },
|
|
18
|
+
{ id: "fu-rong", titleEn: "Fu Rong", titleCn: "芙蓉", year: 2020, category: "ost", duration: "3:58", youtubeId: "9ke3efb9U7A", work: "The Blooms at Ruyi Pavilion" },
|
|
19
|
+
{ id: "ancient-painting", titleEn: "Ancient Painting", titleCn: "古画", year: 2020, category: "ost", youtubeId: null, work: "The Blooms at Ruyi Pavilion" },
|
|
20
|
+
{ id: "thousand-years", titleEn: "A Millennium's Wait", titleCn: "千年等一回", year: 2019, category: "ost", duration: "3:44", youtubeId: "-vf6jU96WI4", work: "The Legend of White Snake" },
|
|
21
|
+
{ id: "du-qing", titleEn: "Du Qing", titleCn: "渡情", year: 2019, category: "ost", youtubeId: null, work: "The Legend of White Snake" },
|
|
22
|
+
{ id: "wudaoying", titleEn: "Wu Dao Ying", titleCn: "舞导演", year: 2019, category: "single", youtubeId: null },
|
|
23
|
+
{ id: "gufeng-shanshui", titleEn: "Gufeng Shanshui", titleCn: "古风山水", year: 2019, category: "single", youtubeId: null },
|
|
24
|
+
{ id: "meng-lu", titleEn: "Meng Lu (Dream Road)", titleCn: "梦路", year: 2018, category: "single", youtubeId: null },
|
|
25
|
+
{ id: "zui-chi-luo", titleEn: "Zui Chi Luo", titleCn: "最赤裸", year: 2018, category: "single", youtubeId: null },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export const musicVideos = [
|
|
29
|
+
{ id: "mv-find-the-way", titleEn: "FIND THE WAY MV", titleCn: "FIND THE WAY", year: 2026, category: "Official MV", youtubeId: "9AeErG03SZw", duration: "3:30" },
|
|
30
|
+
{ id: "mv-not-suffering", titleEn: "Not Suffering MV", titleCn: "不苦", year: 2026, category: "OST MV", youtubeId: "DafSXZZo9AI", duration: "3:50", work: "Veil of Shadows" },
|
|
31
|
+
{ id: "mv-rain-love", titleEn: "Yu Ai (Rain Love) Stage", titleCn: "雨爱", year: 2026, category: "Stage Performance", youtubeId: "OMLpNyp8T7Q", duration: "4:10" },
|
|
32
|
+
{ id: "mv-set-fire", titleEn: "Set Fire to the Silent World MV", titleCn: "着火的沉默世界", year: 2026, category: "OST MV", youtubeId: "7FXGhAmHIAo", duration: "3:55", work: "Wanhua Sin" },
|
|
33
|
+
{ id: "mv-rosa", titleEn: "Rosa MV", titleCn: "Rosa", year: 2025, category: "Official MV", youtubeId: "jZDzOLbd9OU", duration: "3:20" },
|
|
34
|
+
{ id: "mv-fall", titleEn: "Fall MV", titleCn: "落", year: 2024, category: "Official MV", youtubeId: "8Nh4ZPhYKEc", duration: "3:36" },
|
|
35
|
+
{ id: "mv-ru-sha", titleEn: "Ru Sha MV", titleCn: "如纱", year: 2024, category: "OST MV", youtubeId: "lzDP2MfP3eA", duration: "3:42", work: "Sword and Fairy 4" },
|
|
36
|
+
];
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { profileData } from '../../src/data/profileData.js';
|
|
2
|
+
import { worksData } from '../../src/data/worksData.js';
|
|
3
|
+
import { discographyData } from '../../src/data/discographyData.js';
|
|
4
|
+
import { musicVideoData } from '../../src/data/musicVideoData.js';
|
|
5
|
+
import { brandsData } from '../../src/data/brandsData.js';
|
|
6
|
+
import { socialUpdatesData } from '../../src/data/socialUpdatesData.js';
|
|
7
|
+
import { galleryData } from '../../src/data/galleryData.js';
|
|
8
|
+
import { awardsData } from '../../src/data/awardsData.js';
|
|
9
|
+
import { timelineData } from '../../src/data/timelineData.js';
|
|
10
|
+
|
|
11
|
+
export function getUnifiedProfile() {
|
|
12
|
+
return {
|
|
13
|
+
...profileData,
|
|
14
|
+
nameEn: profileData.nameEn || 'Ju Jingyi',
|
|
15
|
+
nameCn: profileData.nameCn || '鞠婧祎',
|
|
16
|
+
nameTh: profileData.nameTh || 'จวีจิ้งอี',
|
|
17
|
+
fandomName: profileData.fandomName || 'Miju (蜜橘)',
|
|
18
|
+
slogan: profileData.slogan || '鞠世无双,婧然倾心',
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getUnifiedWorks() {
|
|
23
|
+
return worksData.map(w => ({
|
|
24
|
+
id: w.id,
|
|
25
|
+
titleEn: w.titleEn || w.titleTh || w.titleCn,
|
|
26
|
+
titleCn: w.titleCn,
|
|
27
|
+
titleTh: w.titleTh,
|
|
28
|
+
year: w.year,
|
|
29
|
+
role: w.role,
|
|
30
|
+
characterName: w.role,
|
|
31
|
+
genre: w.genre || w.type || 'Drama',
|
|
32
|
+
featured: Boolean(w.featured),
|
|
33
|
+
synopsis: w.synopsis || '',
|
|
34
|
+
platform: w.platform || '',
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getUnifiedDiscography() {
|
|
39
|
+
const songs = [
|
|
40
|
+
...(discographyData.latestReleases || []),
|
|
41
|
+
...(discographyData.ostList || []),
|
|
42
|
+
...(discographyData.singles || []),
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const map = new Map();
|
|
46
|
+
for (const s of songs) {
|
|
47
|
+
const key = s.titleCn || s.titleTh || s.titleEn || s.id;
|
|
48
|
+
if (!map.has(key)) {
|
|
49
|
+
map.set(key, {
|
|
50
|
+
id: s.id,
|
|
51
|
+
titleEn: s.titleEn || s.titleTh || s.titleCn,
|
|
52
|
+
titleCn: s.titleCn,
|
|
53
|
+
titleTh: s.titleTh,
|
|
54
|
+
year: s.year,
|
|
55
|
+
type: s.type || 'Single',
|
|
56
|
+
category: s.work ? 'ost' : 'single',
|
|
57
|
+
work: s.work || null,
|
|
58
|
+
youtubeId: s.youtubeId || null,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return Array.from(map.values()).sort((a, b) => (b.year || 0) - (a.year || 0));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getUnifiedAwards() {
|
|
66
|
+
return awardsData.map(a => ({
|
|
67
|
+
id: a.id,
|
|
68
|
+
title: a.title,
|
|
69
|
+
titleCn: a.titleCn || a.title,
|
|
70
|
+
year: a.year,
|
|
71
|
+
ceremony: a.ceremony || a.organization || '',
|
|
72
|
+
category: a.category || 'General',
|
|
73
|
+
})).sort((a, b) => (b.year || 0) - (a.year || 0));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getUnifiedBrands() {
|
|
77
|
+
return brandsData.map(b => ({
|
|
78
|
+
id: b.id,
|
|
79
|
+
brandNameEn: b.brand || b.nameEn || '',
|
|
80
|
+
brandNameCn: b.brandCn || b.nameCn || b.brand || '',
|
|
81
|
+
title: b.title || 'Brand Ambassador',
|
|
82
|
+
year: b.year,
|
|
83
|
+
isGlobal: Boolean(b.isGlobal),
|
|
84
|
+
category: b.category || 'Beauty / Fashion',
|
|
85
|
+
})).sort((a, b) => (b.year || 0) - (a.year || 0));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getUnifiedTimeline() {
|
|
89
|
+
return timelineData.map(m => ({
|
|
90
|
+
id: m.id,
|
|
91
|
+
year: m.year,
|
|
92
|
+
period: m.period || String(m.year),
|
|
93
|
+
title: m.title,
|
|
94
|
+
titleCn: m.titleCn || m.title,
|
|
95
|
+
description: m.description || '',
|
|
96
|
+
era: m.era || 'Solo Career',
|
|
97
|
+
})).sort((a, b) => (b.year || 0) - (a.year || 0));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function getUnifiedGallery() {
|
|
101
|
+
return galleryData;
|
|
102
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"kiku": {
|
|
4
|
+
"type": "stdio",
|
|
5
|
+
"command": "npx",
|
|
6
|
+
"args": ["-y", "-p", "kiku-agent", "kiku-mcp"],
|
|
7
|
+
"env": {
|
|
8
|
+
"KIKU_MCP_ENABLE_WEB_TOOLS": "true"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"kiku-remote": {
|
|
12
|
+
"type": "http",
|
|
13
|
+
"url": "https://jujingyi-kikuuu.vercel.app/mcp",
|
|
14
|
+
"headers": {
|
|
15
|
+
"X-API-Key": "YOUR_API_KEY"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"kiku": {
|
|
4
|
+
"command": "npx",
|
|
5
|
+
"args": ["-y", "-p", "kiku-agent", "kiku-mcp"],
|
|
6
|
+
"env": {
|
|
7
|
+
"KIKU_MCP_ENABLE_WEB_TOOLS": "true"
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"kiku-remote": {
|
|
11
|
+
"httpUrl": "https://jujingyi-kikuuu.vercel.app/mcp",
|
|
12
|
+
"headers": {
|
|
13
|
+
"X-API-Key": "YOUR_API_KEY"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|