kiku-agent 1.0.11 → 1.2.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.
Files changed (221) hide show
  1. package/README.md +300 -495
  2. package/package.json +12 -11
  3. package/public/fonts/GoogleSans-Bold.ttf +0 -0
  4. package/public/fonts/GoogleSans-Bold.woff2 +0 -0
  5. package/public/fonts/GoogleSans-BoldItalic.ttf +0 -0
  6. package/public/fonts/GoogleSans-Italic.ttf +0 -0
  7. package/public/fonts/GoogleSans-Latin-400.woff2 +0 -0
  8. package/public/fonts/GoogleSans-Latin-500.woff2 +0 -0
  9. package/public/fonts/GoogleSans-Latin-600.woff2 +0 -0
  10. package/public/fonts/GoogleSans-Latin-700.woff2 +0 -0
  11. package/public/fonts/GoogleSans-Medium.ttf +0 -0
  12. package/public/fonts/GoogleSans-Medium.woff2 +0 -0
  13. package/public/fonts/GoogleSans-Regular.ttf +0 -0
  14. package/public/fonts/GoogleSans-Regular.woff2 +0 -0
  15. package/public/fonts/GoogleSans-Thai-400.woff2 +0 -0
  16. package/public/fonts/GoogleSans-Thai-500.woff2 +0 -0
  17. package/public/fonts/GoogleSans-Thai-600.woff2 +0 -0
  18. package/public/fonts/GoogleSans-Thai-700.woff2 +0 -0
  19. package/public/fonts/NotoSansThai-Bold.woff2 +0 -0
  20. package/public/fonts/NotoSansThai-Regular.woff2 +0 -0
  21. package/public/fonts/google-sans-official.css +900 -0
  22. package/public/images/0040jbadgy1id6am4yxqfj61s82dn1kx02.jpg +0 -0
  23. package/public/images/0040jbadgy1id6am708pbj62c0340qv602.jpg +0 -0
  24. package/public/images/0040jbadgy1id6amb81uqj62c0340x6q02.jpg +0 -0
  25. package/public/images/0040jbadgy1id6amcqte3j62c0340qv602.jpg +0 -0
  26. package/public/images/0040jbadgy1id6amdg3xcj62b232wqv502.jpg +0 -0
  27. package/public/images/0040jbadgy1id6amexv23j620v2pau0x02.jpg +0 -0
  28. package/public/images/0040jbadly8icq3if7twwj60qy0qy0uc02.jpg +0 -0
  29. package/public/images/008ET7Legy1ig9s89nri5j30i20n1gn4.jpg +0 -0
  30. package/public/images/008ET7Legy1ig9s8a2lmlj30i20ny409.jpg +0 -0
  31. package/public/images/008ET7Legy1ig9s8ay1hsj30i20nk3zv.jpg +0 -0
  32. package/public/images/008ET7Legy1ig9s8bab6sj30i20o7go1.jpg +0 -0
  33. package/public/images/008ET7Legy1ig9s8blqsxj30i20oiq4r.jpg +0 -0
  34. package/public/images/008ET7Legy1ig9s8cbxkzj30i20mxmzh.jpg +0 -0
  35. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-399915.jpg +0 -0
  36. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-424041.jpg +0 -0
  37. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-588166.jpg +0 -0
  38. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-672198.jpg +0 -0
  39. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-700522.jpg +0 -0
  40. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-839722.jpg +0 -0
  41. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026825-460586.jpg +0 -0
  42. package/public/images/douyin-update/thumbs//346/212/226/351/237/263202694-689407.jpg +0 -0
  43. package/public/images/douyin-update//346/212/226/351/237/2632026820-399915.mp4 +0 -0
  44. package/public/images/douyin-update//346/212/226/351/237/2632026820-424041.mp4 +0 -0
  45. package/public/images/douyin-update//346/212/226/351/237/2632026820-588166.mp4 +0 -0
  46. package/public/images/douyin-update//346/212/226/351/237/2632026820-672198.mp4 +0 -0
  47. package/public/images/douyin-update//346/212/226/351/237/2632026820-700522.mp4 +0 -0
  48. package/public/images/douyin-update//346/212/226/351/237/2632026820-839722.mp4 +0 -0
  49. package/public/images/douyin-update//346/212/226/351/237/2632026825-460586.mp4 +0 -0
  50. package/public/images/douyin-update//346/212/226/351/237/263202694-689407.mp4 +0 -0
  51. package/public/images/general/0040jbadgy1id6am4yxqfj61s82dn1kx02.jpg +0 -0
  52. package/public/images/general/0040jbadgy1id6am708pbj62c0340qv602.jpg +0 -0
  53. package/public/images/general/0040jbadgy1id6amb81uqj62c0340x6q02.jpg +0 -0
  54. package/public/images/general/0040jbadgy1id6amcqte3j62c0340qv602.jpg +0 -0
  55. package/public/images/general/0040jbadgy1id6amdg3xcj62b232wqv502.jpg +0 -0
  56. package/public/images/general/0040jbadgy1id6amexv23j620v2pau0x02.jpg +0 -0
  57. package/public/images/general/0040jbadly8icq3if7twwj60qy0qy0uc02.jpg +0 -0
  58. package/public/images/general/008ET7Legy1ig9s89nri5j30i20n1gn4.jpg +0 -0
  59. package/public/images/general/008ET7Legy1ig9s8a2lmlj30i20ny409.jpg +0 -0
  60. package/public/images/general/008ET7Legy1ig9s8ay1hsj30i20nk3zv.jpg +0 -0
  61. package/public/images/general/008ET7Legy1ig9s8bab6sj30i20o7go1.jpg +0 -0
  62. package/public/images/general/008ET7Legy1ig9s8blqsxj30i20oiq4r.jpg +0 -0
  63. package/public/images/general/008ET7Legy1ig9s8cbxkzj30i20mxmzh.jpg +0 -0
  64. package/public/images/general/jujingyi_portrait.jpg +0 -0
  65. package/public/images/jujingyi_portrait.jpg +0 -0
  66. package/public/images/series/a-millenniums-wait/poster.jpg +0 -0
  67. package/public/images/series/beauty-of-resilience/poster.jpg +0 -0
  68. package/public/images/series/detective-samoyeds/poster.jpg +0 -0
  69. package/public/images/series/everyday/poster.jpg +0 -0
  70. package/public/images/series/fu-rong-ancient-painting/poster.jpg +0 -0
  71. package/public/images/series/in-a-class-of-her-own/poster.jpg +0 -0
  72. package/public/images/series/in-blossom/poster.jpg +0 -0
  73. package/public/images/series/legend-of-yunxi/poster.jpg +0 -0
  74. package/public/images/series/lets-fight/poster-2.jpg +0 -0
  75. package/public/images/series/lets-fight/poster.jpg +0 -0
  76. package/public/images/series/love-emergency/poster.jpg +0 -0
  77. package/public/images/series/love-under-the-full-moon/poster.jpg +0 -0
  78. package/public/images/series/money-bastion/poster.jpg +0 -0
  79. package/public/images/series/mr-swimmer/poster.jpg +0 -0
  80. package/public/images/series/novoland-castle-in-the-sky/poster.jpg +0 -0
  81. package/public/images/series/please-give-me-a-pair-of-wings/poster.jpg +0 -0
  82. package/public/images/series/rebirth-for-you/poster.jpg +0 -0
  83. package/public/images/series/sword-and-fairy-4/poster.jpg +0 -0
  84. package/public/images/series/the-blooms-at-ruyi-pavilion/poster.jpg +0 -0
  85. package/public/images/series/the-eternal-fragrance/poster-2.jpg +0 -0
  86. package/public/images/series/the-eternal-fragrance/poster.jpg +0 -0
  87. package/public/images/series/the-fallen-flowers/poster.jpg +0 -0
  88. package/public/images/series/the-legend-of-white-snake/poster.jpg +0 -0
  89. package/public/images/series/veil-of-shadows/poster.jpg +0 -0
  90. package/public/images/series/wanhua-sin/poster-2.jpg +0 -0
  91. package/public/images/series/wanhua-sin/poster.jpg +0 -0
  92. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj10yogj62u03s0npe02.jpg +0 -0
  93. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj2pdvyj61li25jkjl02.jpg +0 -0
  94. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj71177j62672w9e8302.jpg +0 -0
  95. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj7twpfj62192197rw02.jpg +0 -0
  96. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj8jfu8j62c03401kx02.jpg +0 -0
  97. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj9dhdsj622k2l77wh02.jpg +0 -0
  98. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkja31s4j62bw3407r802.jpg +0 -0
  99. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjcf331j61402ey1kx02.jpg +0 -0
  100. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjdx8gqj62602pihdt02.jpg +0 -0
  101. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjfvxnzj62432n57wh02.jpg +0 -0
  102. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixniv2yj627y4fxb2b02.jpg +0 -0
  103. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixsvbdaj625k2vfu1102.jpg +0 -0
  104. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixxxdaoj62c03501l202.jpg +0 -0
  105. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixzeef9j62c0340npd02.jpg +0 -0
  106. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3iy0vs6tj626t4rbhdu02.jpg +0 -0
  107. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3iy21wwuj62bt34hx6q02.jpg +0 -0
  108. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly4wr85j62z629y7wh02.jpg +0 -0
  109. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly67oilj623u2t5npd02.jpg +0 -0
  110. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly9qtw8j62c0340u0x02.jpg +0 -0
  111. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlyem4m4j63402c01kz02.jpg +0 -0
  112. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlygkoq4j627s2yeavd02.jpg +0 -0
  113. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlyjh0daj62bw340nm102.jpg +0 -0
  114. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlylejlhj62bw3401kx02.jpg +0 -0
  115. package/public/images/weibo-update/today/0040jbadgy1igaiaa8b7fj627j2y67wh02.jpg +0 -0
  116. package/public/images/weibo-update/today/0040jbadgy1igaiagw3ezj61s02ebu0x02.jpg +0 -0
  117. package/public/images/weibo-update/today/0040jbadgy1igaiaivng3j61rv2ebx6p02.jpg +0 -0
  118. package/public/images/weibo-update/today/0040jbadgy1igaiajuixvj62bw340qv502.jpg +0 -0
  119. package/public/images/weibo-update/today/0040jbadgy1igaiapyartj62bw340hdt02.jpg +0 -0
  120. package/public/images/weibo-update/today/0040jbadgy1igaibkwu4ij61vy2iq1kx02.jpg +0 -0
  121. package/public/images/weibo-update/today/0040jbadgy1igaic1fvkpj61u92eo1ky02.jpg +0 -0
  122. package/public/images/weibo-update/today/0040jbadgy1igaic3cruhj62822ywb2902.jpg +0 -0
  123. package/public/manifest.json +25 -0
  124. package/public/mcp-logo-128.png +0 -0
  125. package/public/mcp-logo-64.png +0 -0
  126. package/public/mcp-logo.svg +16 -0
  127. package/server/agent/agentCore.js +542 -74
  128. package/server/agent/appPaths.js +1 -0
  129. package/server/agent/audioPlayer.js +4 -2
  130. package/server/agent/citations.js +294 -0
  131. package/server/agent/clarificationAnalytics.js +477 -0
  132. package/server/agent/clarifyingQuestions.js +1089 -0
  133. package/server/agent/cli.js +549 -125
  134. package/server/agent/commandFeatures.js +1 -0
  135. package/server/agent/commandRegistry.js +946 -879
  136. package/server/agent/commands.js +546 -272
  137. package/server/agent/config.js +581 -412
  138. package/server/agent/contextCompactor.js +2 -2
  139. package/server/agent/debate.js +235 -0
  140. package/server/agent/eventBus.js +3 -0
  141. package/server/agent/feedbackLearning.js +217 -0
  142. package/server/agent/llmClient.js +512 -394
  143. package/server/agent/mcpClient.js +182 -32
  144. package/server/agent/memory.js +283 -224
  145. package/server/agent/modelManager.js +79 -39
  146. package/server/agent/profiles.js +178 -0
  147. package/server/agent/publicTools.js +22 -0
  148. package/server/agent/queryDetails.js +68 -8
  149. package/server/agent/quizData.js +398 -384
  150. package/server/agent/research.js +121 -79
  151. package/server/agent/sanitizer.js +104 -13
  152. package/server/agent/session.js +319 -11
  153. package/server/agent/subagent.js +8 -3
  154. package/server/agent/suggestionTui.js +294 -0
  155. package/server/agent/suggestions.js +69 -3
  156. package/server/agent/telemetry.js +119 -0
  157. package/server/agent/terminalImage.js +146 -27
  158. package/server/agent/theme.js +17 -0
  159. package/server/agent/toolRouter.js +16 -1
  160. package/server/agent/tools.js +424 -225
  161. package/server/agent/ui.js +486 -64
  162. package/server/agent/version.js +2 -4
  163. package/server/agent/webSearch.js +111 -11
  164. package/server/agent/youtubeAuth.js +24 -6
  165. package/server/apiRouter.js +4222 -0
  166. package/server/mcp/data/discography.js +25 -16
  167. package/server/mcp/data/gallery.js +308 -20
  168. package/server/mcp/data/profile.js +74 -44
  169. package/server/mcp/data/timeline.js +1 -1
  170. package/server/mcp/data/works.js +51 -17
  171. package/server/mcp/dataAdapter.js +5 -3
  172. package/server/mcp/handler.js +5 -11
  173. package/server/mcp/prompts.js +86 -0
  174. package/server/mcp/security.js +18 -15
  175. package/server/mcp/server.js +7 -2
  176. package/server/mcp/tools.js +66 -2
  177. package/server/web-chat/core/browserLauncher.js +18 -0
  178. package/server/web-chat/core/connection.js +194 -0
  179. package/server/web-chat/core/conversationContext.js +82 -0
  180. package/server/web-chat/core/directCatalogEngine.js +196 -0
  181. package/server/web-chat/core/dramaKnowledge.js +142 -0
  182. package/server/web-chat/core/errorHandler.js +51 -0
  183. package/server/web-chat/core/followUpEngine.js +182 -0
  184. package/server/web-chat/core/futureEventGuard.js +80 -0
  185. package/server/web-chat/core/memorySnapshot.js +12 -0
  186. package/server/web-chat/core/security.js +146 -0
  187. package/server/web-chat/core/toolInspector.js +52 -0
  188. package/server/web-chat/router.js +120 -0
  189. package/server/web-chat/routes/chatRoute.js +397 -0
  190. package/server/web-chat/routes/clarifyConfigRoute.js +98 -0
  191. package/server/web-chat/routes/clarifyRoute.js +83 -0
  192. package/server/web-chat/routes/compareRoute.js +79 -0
  193. package/server/web-chat/routes/exportRoute.js +59 -0
  194. package/server/web-chat/routes/feedbackRoute.js +57 -0
  195. package/server/web-chat/routes/newsRoute.js +33 -0
  196. package/server/web-chat/routes/quizRoute.js +11 -0
  197. package/server/web-chat/routes/staticRoute.js +103 -0
  198. package/server/web-chat/routes/statusRoute.js +52 -0
  199. package/server/web-chat/routes/suggestRoute.js +77 -0
  200. package/server/web-chat/routes/timelineRoute.js +34 -0
  201. package/server/web-chat/routes/verifyRoute.js +57 -0
  202. package/server/web-chat/server.js +116 -0
  203. package/server/web-chat/ui/htmlBuilder.js +91 -0
  204. package/server/web-chat/ui/previews/agentBubble.preview.html +134 -0
  205. package/server/web-chat/ui/scripts/agentBubbleSystem.js +320 -0
  206. package/server/web-chat/ui/scripts/client.js +8397 -0
  207. package/server/web-chat/ui/scripts/kikuDb.js +177 -0
  208. package/server/web-chat/ui/scripts/suggestionsData.js +118 -0
  209. package/server/web-chat/ui/scripts/webchatI18n.js +291 -0
  210. package/server/web-chat/ui/styles/main.css +8568 -0
  211. package/server/web-chat/ui/templates/body.html +796 -0
  212. package/server/webChatHtml.js +1 -0
  213. package/server/webChatServer.js +6 -0
  214. package/src/data/discographyData.js +892 -866
  215. package/src/data/galleryData.js +1805 -561
  216. package/src/data/profileData.js +79 -2
  217. package/src/data/socialUpdatesData.js +106 -47
  218. package/src/data/timelineData.js +1 -1
  219. package/src/data/worksData.js +25 -4
  220. package/tokens.css +97 -0
  221. package/server/agent/updateManager.js +0 -456
@@ -4,7 +4,5 @@ const packageManifest = JSON.parse(
4
4
  readFileSync(new URL('../../package.json', import.meta.url), 'utf8')
5
5
  );
6
6
 
7
- export const PACKAGE_NAME = packageManifest.name;
8
- export const PACKAGE_VERSION = packageManifest.version;
9
- export const PACKAGE_ENGINES = Object.freeze({ ...(packageManifest.engines || {}) });
10
- export const PACKAGE_UPDATE_METADATA = Object.freeze({ ...(packageManifest.kikuUpdate || {}) });
7
+ export const PACKAGE_NAME = packageManifest.name;
8
+ export const PACKAGE_VERSION = packageManifest.version;
@@ -3,6 +3,7 @@ import { execFile } from 'node:child_process';
3
3
  import { mkdtemp, readFile, rm } from 'node:fs/promises';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { join } from 'node:path';
6
+ import { assertSafeUrlHostname, sanitizeUrl } from './sanitizer.js';
6
7
 
7
8
  let _cheerioLoad = null;
8
9
  async function loadCheerio() {
@@ -41,6 +42,73 @@ function cleanHtml(html) {
41
42
  .trim();
42
43
  }
43
44
 
45
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
46
+
47
+ export async function safeFetch(inputUrl, options = {}, maxRedirects = 5) {
48
+ let currentUrl = String(inputUrl || '');
49
+ let currentOptions = { ...options, redirect: 'manual' };
50
+
51
+ for (let redirects = 0; redirects <= maxRedirects; redirects++) {
52
+ const sanitized = sanitizeUrl(currentUrl);
53
+ if (!sanitized.safe) throw new Error(`URL rejected by SSRF guard: ${sanitized.reason}`);
54
+ const dnsCheck = await assertSafeUrlHostname(sanitized.hostname);
55
+ if (!dnsCheck.safe) throw new Error(`URL rejected by SSRF guard: ${dnsCheck.reason}`);
56
+
57
+ const response = await fetch(sanitized.url.href, currentOptions);
58
+ if (!REDIRECT_STATUSES.has(response.status)) return response;
59
+
60
+ const location = response.headers.get('location');
61
+ if (!location) return response;
62
+ if (redirects === maxRedirects) throw new Error('Too many redirects');
63
+
64
+ const nextUrl = new URL(location, sanitized.url).href;
65
+ const nextOrigin = new URL(nextUrl).origin;
66
+ const headers = new Headers(currentOptions.headers || {});
67
+ if (nextOrigin !== sanitized.url.origin) {
68
+ headers.delete('authorization');
69
+ headers.delete('cookie');
70
+ headers.delete('proxy-authorization');
71
+ }
72
+ const method = String(currentOptions.method || 'GET').toUpperCase();
73
+ if (response.status === 303 || ((response.status === 301 || response.status === 302) && method === 'POST')) {
74
+ headers.delete('content-type');
75
+ currentOptions = { ...currentOptions, method: 'GET', body: undefined, headers };
76
+ } else {
77
+ currentOptions = { ...currentOptions, headers };
78
+ }
79
+ currentUrl = nextUrl;
80
+ }
81
+ throw new Error('Too many redirects');
82
+ }
83
+
84
+ async function readResponseTextLimited(response, maxBytes) {
85
+ const limit = Math.max(1024, Math.min(Number(maxBytes) || 1024, 2_000_000));
86
+ const declared = Number(response.headers.get('content-length') || 0);
87
+ if (declared > limit) throw new Error(`Response exceeds ${limit} byte limit`);
88
+ if (!response.body?.getReader) {
89
+ const text = await response.text();
90
+ if (Buffer.byteLength(text, 'utf8') > limit) throw new Error(`Response exceeds ${limit} byte limit`);
91
+ return text;
92
+ }
93
+ const reader = response.body.getReader();
94
+ const decoder = new TextDecoder();
95
+ let bytes = 0;
96
+ let text = '';
97
+ try {
98
+ while (true) {
99
+ const { done, value } = await reader.read();
100
+ if (done) break;
101
+ bytes += value.byteLength;
102
+ if (bytes > limit) throw new Error(`Response exceeds ${limit} byte limit`);
103
+ text += decoder.decode(value, { stream: true });
104
+ }
105
+ text += decoder.decode();
106
+ return text;
107
+ } finally {
108
+ if (bytes > limit) await reader.cancel().catch(() => {});
109
+ }
110
+ }
111
+
44
112
  function detectLanguage(query) {
45
113
  if (/[\u0E00-\u0E7F]/.test(query)) return 'th';
46
114
  if (/[\u4E00-\u9FA5]/.test(query)) return 'zh';
@@ -276,18 +344,28 @@ export async function fetchWebpage(url, maxLength = 8000) {
276
344
  return { status: 'error', message: 'Valid HTTP/HTTPS URL is required.' };
277
345
  }
278
346
 
347
+ let parsedUrl;
348
+ try {
349
+ parsedUrl = new URL(url);
350
+ } catch {
351
+ return { status: 'error', message: 'Invalid URL format.' };
352
+ }
353
+ const ssrf = await assertSafeUrlHostname(parsedUrl.hostname);
354
+ if (!ssrf.safe) {
355
+ return { status: 'error', message: `URL rejected by SSRF guard: ${ssrf.reason}` };
356
+ }
357
+
279
358
  try {
280
359
  const controller = new AbortController();
281
360
  const timeoutId = setTimeout(() => controller.abort(), 15000);
282
361
 
283
- const res = await fetch(url, {
362
+ const res = await safeFetch(url, {
284
363
  headers: {
285
364
  'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
286
365
  'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
287
366
  'Accept-Language': 'en-US,en;q=0.9,th;q=0.8,zh-CN;q=0.7'
288
367
  },
289
- signal: controller.signal,
290
- redirect: 'follow'
368
+ signal: controller.signal
291
369
  });
292
370
 
293
371
  clearTimeout(timeoutId);
@@ -300,7 +378,8 @@ export async function fetchWebpage(url, maxLength = 8000) {
300
378
  const finalUrl = res.url || url;
301
379
 
302
380
  if (contentType.includes('application/json')) {
303
- const json = await res.json();
381
+ const raw = await readResponseTextLimited(res, Math.max(64_000, maxLength * 8));
382
+ const json = JSON.parse(raw);
304
383
  const text = JSON.stringify(json, null, 2);
305
384
  return {
306
385
  status: 'success',
@@ -311,7 +390,7 @@ export async function fetchWebpage(url, maxLength = 8000) {
311
390
  };
312
391
  }
313
392
 
314
- const html = await res.text();
393
+ const html = await readResponseTextLimited(res, Math.max(256_000, maxLength * 16));
315
394
  const cheerioLoad = await loadCheerio();
316
395
 
317
396
  if (cheerioLoad) {
@@ -429,6 +508,17 @@ export async function fetchJson(url, options = {}) {
429
508
  return { status: 'error', message: 'Valid HTTP/HTTPS URL is required.' };
430
509
  }
431
510
 
511
+ let parsedUrl;
512
+ try {
513
+ parsedUrl = new URL(url);
514
+ } catch {
515
+ return { status: 'error', message: 'Invalid URL format.' };
516
+ }
517
+ const ssrf = await assertSafeUrlHostname(parsedUrl.hostname);
518
+ if (!ssrf.safe) {
519
+ return { status: 'error', message: `URL rejected by SSRF guard: ${ssrf.reason}` };
520
+ }
521
+
432
522
  const {
433
523
  method = 'GET',
434
524
  headers = {},
@@ -456,19 +546,19 @@ export async function fetchJson(url, options = {}) {
456
546
  fetchOptions.headers['Content-Type'] = 'application/json';
457
547
  }
458
548
 
459
- const res = await fetch(url, fetchOptions);
549
+ const res = await safeFetch(url, fetchOptions);
460
550
  clearTimeout(timeoutId);
461
551
 
462
552
  const responseInfo = {
463
- status: res.status,
464
- statusText: res.statusText,
553
+ httpStatus: res.status,
554
+ httpStatusText: res.statusText,
465
555
  contentType: res.headers.get('content-type') || '',
466
556
  finalUrl: res.url || url
467
557
  };
468
558
 
469
559
  if (!res.ok) {
470
560
  let errorBody = '';
471
- try { errorBody = (await res.text()).slice(0, 1000); } catch {}
561
+ try { errorBody = (await readResponseTextLimited(res, 1000)).slice(0, 1000); } catch {}
472
562
  return {
473
563
  status: 'error',
474
564
  ...responseInfo,
@@ -477,7 +567,7 @@ export async function fetchJson(url, options = {}) {
477
567
  };
478
568
  }
479
569
 
480
- const rawText = await res.text();
570
+ const rawText = await readResponseTextLimited(res, Math.min(1_000_000, maxResponseLength + 1));
481
571
  let data;
482
572
  try {
483
573
  data = JSON.parse(rawText);
@@ -542,6 +632,17 @@ export async function screenshotWebpage(url, options = {}) {
542
632
  return { status: 'error', message: 'Valid HTTP/HTTPS URL is required.' };
543
633
  }
544
634
 
635
+ let parsedUrl;
636
+ try {
637
+ parsedUrl = new URL(url);
638
+ } catch {
639
+ return { status: 'error', message: 'Invalid URL format.' };
640
+ }
641
+ const ssrf = await assertSafeUrlHostname(parsedUrl.hostname);
642
+ if (!ssrf.safe) {
643
+ return { status: 'error', message: `URL rejected by SSRF guard: ${ssrf.reason}` };
644
+ }
645
+
545
646
  const { width = 1280, height = 800, fullPage = false, timeoutMs = 20000 } = options;
546
647
  const chromePath = findChromePath();
547
648
 
@@ -557,7 +658,6 @@ export async function screenshotWebpage(url, options = {}) {
557
658
  const screenshotPath = join(tmpDir, screenshotFile);
558
659
  const args = [
559
660
  '--headless=new',
560
- '--no-sandbox',
561
661
  '--disable-gpu',
562
662
  '--disable-dev-shm-usage',
563
663
  '--disable-software-rasterizer',
@@ -122,8 +122,8 @@ function loadEnv() {
122
122
  for (const line of text.split('\n')) {
123
123
  const m = line.match(/^\s*([\w.-]+)\s*=\s*(.*)\s*$/);
124
124
  if (!m) continue;
125
- const key = m[1];
126
- const val = m[2].replace(/^["']|["']$/g, '');
125
+ const key = m[1].trim();
126
+ const val = m[2].trim().replace(/^["']|["']$/g, '');
127
127
  if (process.env[key] === undefined) process.env[key] = val;
128
128
  }
129
129
  } catch {}
@@ -233,12 +233,27 @@ export async function ensureYouTubeCookies(opts = {}) {
233
233
  return _inFlightPromise;
234
234
  }
235
235
 
236
+ export function isCookieFileValid(filePath) {
237
+ try {
238
+ if (!filePath || typeof filePath !== 'string' || !fs.existsSync(filePath)) return false;
239
+ const stat = fs.statSync(filePath);
240
+ if (stat.size < 10) return false;
241
+ const content = fs.readFileSync(filePath, 'utf8');
242
+ const lines = content.split('\n').filter(l => l.trim() && !l.trim().startsWith('#'));
243
+ if (lines.length === 0) return false;
244
+ return lines.some(l => l.includes('\t') && l.split('\t').length >= 4);
245
+ } catch {
246
+ return false;
247
+ }
248
+ }
249
+
236
250
  async function _ensureYouTubeCookiesImpl(opts = {}) {
237
251
  loadEnv();
238
252
 
239
-
240
- const cookiesCandidate = process.env.KIKU_YT_COOKIES || (fs.existsSync(SECURE_COOKIE_PATH) ? SECURE_COOKIE_PATH : null);
241
- if (cookiesCandidate && fs.existsSync(cookiesCandidate)) {
253
+ const cookiesCandidate = (process.env.KIKU_YT_COOKIES && isCookieFileValid(process.env.KIKU_YT_COOKIES))
254
+ ? process.env.KIKU_YT_COOKIES
255
+ : (isCookieFileValid(SECURE_COOKIE_PATH) ? SECURE_COOKIE_PATH : null);
256
+ if (cookiesCandidate && isCookieFileValid(cookiesCandidate)) {
242
257
  _ensured = { ok: true, source: 'file', cookiesPath: cookiesCandidate };
243
258
  return _ensured;
244
259
  }
@@ -367,10 +382,13 @@ export function resetCookieCheck() {
367
382
  }
368
383
 
369
384
  export function getCookieStatus() {
385
+ const envCookies = (process.env.KIKU_YT_COOKIES && isCookieFileValid(process.env.KIKU_YT_COOKIES))
386
+ ? process.env.KIKU_YT_COOKIES
387
+ : (isCookieFileValid(SECURE_COOKIE_PATH) ? SECURE_COOKIE_PATH : null);
370
388
  return {
371
389
  ensured: _ensured,
372
390
  envBrowser: process.env.KIKU_YT_BROWSER || null,
373
- envCookies: process.env.KIKU_YT_COOKIES || (fs.existsSync(SECURE_COOKIE_PATH) ? SECURE_COOKIE_PATH : null),
391
+ envCookies,
374
392
  envPath: ENV_PATH,
375
393
  authConfigPath: AUTH_CONFIG_PATH
376
394
  };