@astro-minimax/ai 0.7.4 → 0.8.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 (80) hide show
  1. package/dist/components/AIChatContainer.d.ts +9 -0
  2. package/dist/components/AIChatContainer.d.ts.map +1 -0
  3. package/dist/components/AIChatContainer.js +936 -0
  4. package/{src → dist}/components/AIChatWidget.astro +1 -1
  5. package/dist/components/ChatPanel.d.ts +19 -0
  6. package/dist/components/ChatPanel.d.ts.map +1 -0
  7. package/dist/components/ChatPanel.js +914 -0
  8. package/dist/data/index.js +18 -1
  9. package/dist/fact-registry/index.js +16 -3
  10. package/dist/index.js +11 -30
  11. package/dist/intelligence/evidence-analysis.d.ts.map +1 -1
  12. package/dist/intelligence/index.js +56 -5
  13. package/dist/intelligence/keyword-extract.d.ts.map +1 -1
  14. package/dist/middleware/index.js +10 -1
  15. package/dist/prompt/index.js +10 -4
  16. package/dist/provider-manager/base.d.ts +1 -0
  17. package/dist/provider-manager/base.d.ts.map +1 -1
  18. package/dist/provider-manager/types.d.ts +1 -0
  19. package/dist/provider-manager/types.d.ts.map +1 -1
  20. package/dist/providers/index.js +5 -1
  21. package/dist/search/index.js +48 -6
  22. package/dist/server/dev-server.js +236 -259
  23. package/dist/server/index.js +39 -6
  24. package/dist/stream/index.js +8 -2
  25. package/package.json +16 -10
  26. package/dist/cache/global-cache.js +0 -141
  27. package/dist/cache/index.js +0 -62
  28. package/dist/cache/kv-adapter.js +0 -102
  29. package/dist/cache/memory-adapter.js +0 -95
  30. package/dist/cache/response-cache.js +0 -85
  31. package/dist/cache/types.js +0 -16
  32. package/dist/data/metadata-loader.js +0 -66
  33. package/dist/data/types.js +0 -1
  34. package/dist/fact-registry/fact-matcher.js +0 -94
  35. package/dist/fact-registry/prompt-injector.js +0 -57
  36. package/dist/fact-registry/registry.js +0 -38
  37. package/dist/fact-registry/types.js +0 -5
  38. package/dist/intelligence/citation-appender.js +0 -65
  39. package/dist/intelligence/citation-guard.js +0 -125
  40. package/dist/intelligence/evidence-analysis.js +0 -88
  41. package/dist/intelligence/intent-detect.js +0 -131
  42. package/dist/intelligence/keyword-extract.js +0 -114
  43. package/dist/intelligence/response-templates.js +0 -116
  44. package/dist/intelligence/types.js +0 -1
  45. package/dist/middleware/rate-limiter.js +0 -129
  46. package/dist/prompt/dynamic-layer.js +0 -67
  47. package/dist/prompt/prompt-builder.js +0 -12
  48. package/dist/prompt/semi-static-layer.js +0 -29
  49. package/dist/prompt/static-layer.js +0 -150
  50. package/dist/prompt/types.js +0 -1
  51. package/dist/provider-manager/base.js +0 -47
  52. package/dist/provider-manager/config.js +0 -134
  53. package/dist/provider-manager/index.js +0 -6
  54. package/dist/provider-manager/manager.js +0 -121
  55. package/dist/provider-manager/mock.js +0 -56
  56. package/dist/provider-manager/openai.js +0 -112
  57. package/dist/provider-manager/types.js +0 -6
  58. package/dist/provider-manager/workers.js +0 -74
  59. package/dist/providers/mock.js +0 -234
  60. package/dist/search/idf.js +0 -31
  61. package/dist/search/search-api.js +0 -119
  62. package/dist/search/search-index.js +0 -35
  63. package/dist/search/search-utils.js +0 -122
  64. package/dist/search/session-cache.js +0 -92
  65. package/dist/search/types.js +0 -1
  66. package/dist/search/vector-reranker.js +0 -135
  67. package/dist/server/chat-handler.js +0 -590
  68. package/dist/server/errors.js +0 -41
  69. package/dist/server/metadata-init.js +0 -47
  70. package/dist/server/notify.js +0 -74
  71. package/dist/server/stream-helpers.js +0 -197
  72. package/dist/server/types.js +0 -13
  73. package/dist/stream/mock-stream.js +0 -27
  74. package/dist/stream/response.js +0 -22
  75. package/dist/utils/i18n.js +0 -164
  76. package/src/components/AIChatContainer.tsx +0 -31
  77. package/src/components/ChatPanel.tsx +0 -866
  78. package/src/providers/mock.ts +0 -240
  79. package/src/server/types.ts +0 -89
  80. package/src/utils/i18n.ts +0 -238
@@ -1,294 +1,271 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * Standalone local AI dev server using Node.js built-in HTTP.
4
- * Runs alongside `astro dev` to provide /api/chat and /api/ai-info endpoints
5
- * when wrangler pages dev is unavailable.
6
- *
7
- * Usage:
8
- * pnpm exec astro-ai-dev # Start dev server
9
- * pnpm exec astro-ai-dev --init # Initialize datas/ directory
10
- *
11
- * Environment variables:
12
- * AI_DEV_PORT - Port to listen on (default: 8787)
13
- * AI_BASE_URL - AI provider base URL
14
- * AI_API_KEY - AI provider API key
15
- * AI_MODEL - AI model name
16
- */
17
- import { createServer } from 'node:http';
18
- import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
19
- import { resolve, dirname } from 'node:path';
20
- import { fileURLToPath } from 'node:url';
2
+ import { createServer } from "node:http";
3
+ import { readFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
4
+ import { resolve, dirname } from "node:path";
5
+ import { fileURLToPath } from "node:url";
21
6
  const __filename = fileURLToPath(import.meta.url);
22
7
  const __dirname = dirname(__filename);
23
- const DEFAULT_SUMMARIES = { meta: { lastUpdated: new Date().toISOString(), model: 'none', totalProcessed: 0 }, articles: {} };
8
+ const DEFAULT_SUMMARIES = { meta: { lastUpdated: (/* @__PURE__ */ new Date()).toISOString(), model: "none", totalProcessed: 0 }, articles: {} };
24
9
  const DEFAULT_AUTHOR_CONTEXT = { author: {}, posts: [] };
25
10
  const DEFAULT_VOICE_PROFILE = { style: {}, examples: [] };
26
11
  function extractPostTitle(url) {
27
- const match = url.match(/\/posts\/([^/]+)/);
28
- if (match) {
29
- return decodeURIComponent(match[1].replace(/-/g, ' '));
30
- }
31
- return '博客文章';
12
+ const match = url.match(/\/posts\/([^/]+)/);
13
+ if (match) {
14
+ return decodeURIComponent(match[1].replace(/-/g, " "));
15
+ }
16
+ return "\u535A\u5BA2\u6587\u7AE0";
32
17
  }
33
18
  function findBlogRoot() {
34
- let dir = process.cwd();
35
- for (let i = 0; i < 10; i++) {
36
- if (existsSync(resolve(dir, 'datas'))) {
37
- return { root: dir, datasDir: resolve(dir, 'datas'), hasDatas: true };
38
- }
39
- if (existsSync(resolve(dir, 'apps', 'blog', 'datas'))) {
40
- return { root: resolve(dir, 'apps', 'blog'), datasDir: resolve(dir, 'apps', 'blog', 'datas'), hasDatas: true };
41
- }
42
- if (existsSync(resolve(dir, 'src', 'data'))) {
43
- return { root: dir, datasDir: resolve(dir, 'src', 'data'), hasDatas: true };
44
- }
45
- const parent = resolve(dir, '..');
46
- if (parent === dir)
47
- break;
48
- dir = parent;
19
+ let dir = process.cwd();
20
+ for (let i = 0; i < 10; i++) {
21
+ if (existsSync(resolve(dir, "datas"))) {
22
+ return { root: dir, datasDir: resolve(dir, "datas"), hasDatas: true };
23
+ }
24
+ if (existsSync(resolve(dir, "apps", "blog", "datas"))) {
25
+ return { root: resolve(dir, "apps", "blog"), datasDir: resolve(dir, "apps", "blog", "datas"), hasDatas: true };
49
26
  }
50
- return { root: process.cwd(), datasDir: resolve(process.cwd(), 'datas'), hasDatas: false };
27
+ if (existsSync(resolve(dir, "src", "data"))) {
28
+ return { root: dir, datasDir: resolve(dir, "src", "data"), hasDatas: true };
29
+ }
30
+ const parent = resolve(dir, "..");
31
+ if (parent === dir) break;
32
+ dir = parent;
33
+ }
34
+ return { root: process.cwd(), datasDir: resolve(process.cwd(), "datas"), hasDatas: false };
51
35
  }
52
36
  function loadEnv(envPath) {
53
- if (!existsSync(envPath))
54
- return;
55
- const content = readFileSync(envPath, 'utf-8');
56
- for (const line of content.split('\n')) {
57
- const trimmed = line.trim();
58
- if (!trimmed || trimmed.startsWith('#'))
59
- continue;
60
- const eqIndex = trimmed.indexOf('=');
61
- if (eqIndex === -1)
62
- continue;
63
- const key = trimmed.slice(0, eqIndex).trim();
64
- let value = trimmed.slice(eqIndex + 1).trim();
65
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
66
- value = value.slice(1, -1);
67
- }
68
- if (!process.env[key]) {
69
- process.env[key] = value;
70
- }
37
+ if (!existsSync(envPath)) return;
38
+ const content = readFileSync(envPath, "utf-8");
39
+ for (const line of content.split("\n")) {
40
+ const trimmed = line.trim();
41
+ if (!trimmed || trimmed.startsWith("#")) continue;
42
+ const eqIndex = trimmed.indexOf("=");
43
+ if (eqIndex === -1) continue;
44
+ const key = trimmed.slice(0, eqIndex).trim();
45
+ let value = trimmed.slice(eqIndex + 1).trim();
46
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
47
+ value = value.slice(1, -1);
71
48
  }
49
+ if (!process.env[key]) {
50
+ process.env[key] = value;
51
+ }
52
+ }
72
53
  }
73
54
  function initDatasDirectory(datasDir) {
74
- if (!existsSync(datasDir)) {
75
- mkdirSync(datasDir, { recursive: true });
76
- }
77
- const files = [
78
- { name: 'ai-summaries.json', content: DEFAULT_SUMMARIES },
79
- { name: 'author-context.json', content: DEFAULT_AUTHOR_CONTEXT },
80
- { name: 'voice-profile.json', content: DEFAULT_VOICE_PROFILE },
81
- ];
82
- for (const file of files) {
83
- const filePath = resolve(datasDir, file.name);
84
- if (!existsSync(filePath)) {
85
- writeFileSync(filePath, JSON.stringify(file.content, null, 2) + '\n');
86
- console.log(` Created ${file.name}`);
87
- }
88
- else {
89
- console.log(` Skipped ${file.name} (already exists)`);
90
- }
55
+ if (!existsSync(datasDir)) {
56
+ mkdirSync(datasDir, { recursive: true });
57
+ }
58
+ const files = [
59
+ { name: "ai-summaries.json", content: DEFAULT_SUMMARIES },
60
+ { name: "author-context.json", content: DEFAULT_AUTHOR_CONTEXT },
61
+ { name: "voice-profile.json", content: DEFAULT_VOICE_PROFILE }
62
+ ];
63
+ for (const file of files) {
64
+ const filePath = resolve(datasDir, file.name);
65
+ if (!existsSync(filePath)) {
66
+ writeFileSync(filePath, JSON.stringify(file.content, null, 2) + "\n");
67
+ console.log(` Created ${file.name}`);
68
+ } else {
69
+ console.log(` Skipped ${file.name} (already exists)`);
91
70
  }
71
+ }
92
72
  }
93
73
  function loadJson(datasDir, file, defaultValue) {
94
- const path = resolve(datasDir, file);
95
- if (existsSync(path)) {
96
- try {
97
- return JSON.parse(readFileSync(path, 'utf-8'));
98
- }
99
- catch (e) {
100
- console.warn(` Warning: Failed to parse ${file}, using defaults`);
101
- return defaultValue;
102
- }
74
+ const path = resolve(datasDir, file);
75
+ if (existsSync(path)) {
76
+ try {
77
+ return JSON.parse(readFileSync(path, "utf-8"));
78
+ } catch (e) {
79
+ console.warn(` Warning: Failed to parse ${file}, using defaults`);
80
+ return defaultValue;
103
81
  }
104
- return defaultValue;
82
+ }
83
+ return defaultValue;
105
84
  }
106
85
  async function setupHandler(datasDir, hasDatas) {
107
- const { handleChatRequest, initializeMetadata } = await import('./index.js');
108
- // Show warnings for missing metadata
109
- if (!hasDatas) {
110
- console.log('\n ⚠️ No datas/ directory found. AI chat will work with empty context.');
111
- console.log(' Run "pnpm exec astro-ai-dev --init" to create placeholder files.\n');
112
- }
113
- else {
114
- const hasSummaries = existsSync(resolve(datasDir, 'ai-summaries.json'));
115
- const hasAuthor = existsSync(resolve(datasDir, 'author-context.json'));
116
- if (!hasSummaries || !hasAuthor) {
117
- console.log('\n ⚠️ Some metadata files are missing. AI chat may have limited context.\n');
118
- }
86
+ const { handleChatRequest, initializeMetadata } = await import("./index.js");
87
+ if (!hasDatas) {
88
+ console.log("\n \u26A0\uFE0F No datas/ directory found. AI chat will work with empty context.");
89
+ console.log(' Run "pnpm exec astro-ai-dev --init" to create placeholder files.\n');
90
+ } else {
91
+ const hasSummaries = existsSync(resolve(datasDir, "ai-summaries.json"));
92
+ const hasAuthor = existsSync(resolve(datasDir, "author-context.json"));
93
+ if (!hasSummaries || !hasAuthor) {
94
+ console.log("\n \u26A0\uFE0F Some metadata files are missing. AI chat may have limited context.\n");
119
95
  }
120
- const summaries = loadJson(datasDir, 'ai-summaries.json', DEFAULT_SUMMARIES);
121
- const authorContext = loadJson(datasDir, 'author-context.json', DEFAULT_AUTHOR_CONTEXT);
122
- const voiceProfile = loadJson(datasDir, 'voice-profile.json', DEFAULT_VOICE_PROFILE);
123
- const env = { ...process.env };
124
- initializeMetadata({
125
- summaries: summaries,
126
- authorContext: authorContext,
127
- voiceProfile: voiceProfile,
128
- siteUrl: process.env.SITE_URL || 'http://localhost:4321',
129
- }, env);
130
- return { handleChatRequest, env };
96
+ }
97
+ const summaries = loadJson(datasDir, "ai-summaries.json", DEFAULT_SUMMARIES);
98
+ const authorContext = loadJson(datasDir, "author-context.json", DEFAULT_AUTHOR_CONTEXT);
99
+ const voiceProfile = loadJson(datasDir, "voice-profile.json", DEFAULT_VOICE_PROFILE);
100
+ const env = { ...process.env };
101
+ initializeMetadata({
102
+ summaries,
103
+ authorContext,
104
+ voiceProfile,
105
+ siteUrl: process.env.SITE_URL || "http://localhost:4321"
106
+ }, env);
107
+ return { handleChatRequest, env };
131
108
  }
132
109
  function toWebRequest(req) {
133
- return new Promise((resolve, reject) => {
134
- const chunks = [];
135
- req.on('data', (chunk) => chunks.push(chunk));
136
- req.on('end', () => {
137
- const body = Buffer.concat(chunks);
138
- const url = `http://localhost${req.url || '/'}`;
139
- resolve(new Request(url, {
140
- method: req.method || 'GET',
141
- headers: req.headers,
142
- body: req.method !== 'GET' && req.method !== 'HEAD' ? body : undefined,
143
- }));
144
- });
145
- req.on('error', reject);
110
+ return new Promise((resolve2, reject) => {
111
+ const chunks = [];
112
+ req.on("data", (chunk) => chunks.push(chunk));
113
+ req.on("end", () => {
114
+ const body = Buffer.concat(chunks);
115
+ const url = `http://localhost${req.url || "/"}`;
116
+ resolve2(new Request(url, {
117
+ method: req.method || "GET",
118
+ headers: req.headers,
119
+ body: req.method !== "GET" && req.method !== "HEAD" ? body : void 0
120
+ }));
146
121
  });
122
+ req.on("error", reject);
123
+ });
147
124
  }
148
125
  async function sendWebResponse(webRes, res) {
149
- res.writeHead(webRes.status, Object.fromEntries(webRes.headers));
150
- if (!webRes.body) {
151
- res.end();
152
- return;
153
- }
154
- const reader = webRes.body.getReader();
155
- try {
156
- while (true) {
157
- const { done, value } = await reader.read();
158
- if (done)
159
- break;
160
- res.write(value);
161
- }
162
- }
163
- finally {
164
- res.end();
126
+ res.writeHead(webRes.status, Object.fromEntries(webRes.headers));
127
+ if (!webRes.body) {
128
+ res.end();
129
+ return;
130
+ }
131
+ const reader = webRes.body.getReader();
132
+ try {
133
+ while (true) {
134
+ const { done, value } = await reader.read();
135
+ if (done) break;
136
+ res.write(value);
165
137
  }
138
+ } finally {
139
+ res.end();
140
+ }
166
141
  }
167
142
  async function main() {
168
- const args = process.argv.slice(2);
169
- if (args.includes('--init') || args.includes('-i')) {
170
- const { datasDir } = findBlogRoot();
171
- console.log('\n📦 Initializing datas/ directory...\n');
172
- console.log(` Location: ${datasDir}\n`);
173
- initDatasDirectory(datasDir);
174
- console.log('\n✅ Done! You can now configure your AI metadata files.\n');
175
- process.exit(0);
143
+ const args = process.argv.slice(2);
144
+ if (args.includes("--init") || args.includes("-i")) {
145
+ const { datasDir: datasDir2 } = findBlogRoot();
146
+ console.log("\n\u{1F4E6} Initializing datas/ directory...\n");
147
+ console.log(` Location: ${datasDir2}
148
+ `);
149
+ initDatasDirectory(datasDir2);
150
+ console.log("\n\u2705 Done! You can now configure your AI metadata files.\n");
151
+ process.exit(0);
152
+ }
153
+ const port = parseInt(process.env.AI_DEV_PORT || "8787", 10);
154
+ const { root: blogRoot, datasDir, hasDatas } = findBlogRoot();
155
+ loadEnv(resolve(blogRoot, ".env"));
156
+ console.log("\n\u{1F916} AI Dev Server starting...\n");
157
+ console.log(` Working directory: ${blogRoot}`);
158
+ console.log(` Datas directory: ${datasDir} ${hasDatas ? "\u2713" : "(not found)"}`);
159
+ console.log(` Port: ${port}`);
160
+ console.log(` AI_BASE_URL: ${process.env.AI_BASE_URL ? "\u2713 configured" : "\u2717 not set"}`);
161
+ console.log(` AI_API_KEY: ${process.env.AI_API_KEY ? "\u2713 configured" : "\u2717 not set"}`);
162
+ console.log(` AI_MODEL: ${process.env.AI_MODEL || "(default)"}`);
163
+ const { handleChatRequest, env } = await setupHandler(datasDir, hasDatas);
164
+ const server = createServer(async (req, res) => {
165
+ res.setHeader("Access-Control-Allow-Origin", "*");
166
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
167
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, x-session-id");
168
+ if (req.method === "OPTIONS") {
169
+ res.writeHead(204);
170
+ res.end();
171
+ return;
176
172
  }
177
- const port = parseInt(process.env.AI_DEV_PORT || '8787', 10);
178
- const { root: blogRoot, datasDir, hasDatas } = findBlogRoot();
179
- loadEnv(resolve(blogRoot, '.env'));
180
- console.log('\n🤖 AI Dev Server starting...\n');
181
- console.log(` Working directory: ${blogRoot}`);
182
- console.log(` Datas directory: ${datasDir} ${hasDatas ? '✓' : '(not found)'}`);
183
- console.log(` Port: ${port}`);
184
- console.log(` AI_BASE_URL: ${process.env.AI_BASE_URL ? '✓ configured' : '✗ not set'}`);
185
- console.log(` AI_API_KEY: ${process.env.AI_API_KEY ? '✓ configured' : '✗ not set'}`);
186
- console.log(` AI_MODEL: ${process.env.AI_MODEL || '(default)'}`);
187
- const { handleChatRequest, env } = await setupHandler(datasDir, hasDatas);
188
- const server = createServer(async (req, res) => {
189
- res.setHeader('Access-Control-Allow-Origin', '*');
190
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
191
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, x-session-id');
192
- if (req.method === 'OPTIONS') {
193
- res.writeHead(204);
194
- res.end();
195
- return;
196
- }
197
- const url = req.url || '/';
198
- try {
199
- if (url.startsWith('/api/chat')) {
200
- const webReq = await toWebRequest(req);
201
- const webRes = await handleChatRequest({ env: env, request: webReq });
202
- await sendWebResponse(webRes, res);
203
- return;
204
- }
205
- if (url.startsWith('/api/notify/comment')) {
206
- const { createNotifier } = await import('@astro-minimax/notify');
207
- const webReq = await toWebRequest(req);
208
- const body = await webReq.json();
209
- if (body.type !== 'new_comment' && body.type !== 'new_reply') {
210
- res.writeHead(400, { 'Content-Type': 'application/json' });
211
- res.end(JSON.stringify({ error: 'Unsupported event type' }));
212
- return;
213
- }
214
- const data = body.data || {};
215
- const siteUrl = process.env.SITE_URL || 'http://localhost:4321';
216
- const postUrl = data.url?.startsWith('http') ? data.url : `${siteUrl}${data.url || '/'}`;
217
- const notifier = createNotifier({
218
- telegram: process.env.NOTIFY_TELEGRAM_BOT_TOKEN && process.env.NOTIFY_TELEGRAM_CHAT_ID ? {
219
- botToken: process.env.NOTIFY_TELEGRAM_BOT_TOKEN,
220
- chatId: process.env.NOTIFY_TELEGRAM_CHAT_ID,
221
- } : undefined,
222
- webhook: process.env.NOTIFY_WEBHOOK_URL ? {
223
- url: process.env.NOTIFY_WEBHOOK_URL,
224
- } : undefined,
225
- email: process.env.NOTIFY_RESEND_API_KEY && process.env.NOTIFY_RESEND_FROM && process.env.NOTIFY_RESEND_TO ? {
226
- provider: 'resend',
227
- apiKey: process.env.NOTIFY_RESEND_API_KEY,
228
- from: process.env.NOTIFY_RESEND_FROM,
229
- to: process.env.NOTIFY_RESEND_TO,
230
- } : undefined,
231
- });
232
- const result = await notifier.comment({
233
- author: data.nick || '匿名用户',
234
- content: data.comment || '',
235
- postTitle: extractPostTitle(data.url || '/'),
236
- postUrl,
237
- });
238
- console.log('[notify] Comment notification sent:', result.success ? '✓' : '✗', result.results);
239
- res.writeHead(200, { 'Content-Type': 'application/json' });
240
- res.end(JSON.stringify({
241
- success: result.success,
242
- event: 'comment',
243
- channels: result.results.map(r => ({ channel: r.channel, success: r.success, error: r.error })),
244
- }));
245
- return;
246
- }
247
- if (url.startsWith('/api/ai-info')) {
248
- res.writeHead(200, { 'Content-Type': 'application/json' });
249
- res.end(JSON.stringify({
250
- status: 'ok',
251
- mode: 'dev-server',
252
- datas: { found: hasDatas, path: datasDir },
253
- ai: {
254
- configured: !!(process.env.AI_BASE_URL && process.env.AI_API_KEY),
255
- model: process.env.AI_MODEL || 'unknown',
256
- },
257
- }, null, 2));
258
- return;
259
- }
260
- res.writeHead(404, { 'Content-Type': 'application/json' });
261
- res.end(JSON.stringify({ error: 'Not found' }));
262
- }
263
- catch (err) {
264
- console.error('[AI Dev Server] Error:', err);
265
- res.writeHead(500, { 'Content-Type': 'application/json' });
266
- res.end(JSON.stringify({ error: 'Internal server error', detail: err instanceof Error ? err.message : String(err) }));
173
+ const url = req.url || "/";
174
+ try {
175
+ if (url.startsWith("/api/chat")) {
176
+ const webReq = await toWebRequest(req);
177
+ const webRes = await handleChatRequest({ env, request: webReq });
178
+ await sendWebResponse(webRes, res);
179
+ return;
180
+ }
181
+ if (url.startsWith("/api/notify/comment")) {
182
+ const { createNotifier } = await import("@astro-minimax/notify");
183
+ const webReq = await toWebRequest(req);
184
+ const body = await webReq.json();
185
+ if (body.type !== "new_comment" && body.type !== "new_reply") {
186
+ res.writeHead(400, { "Content-Type": "application/json" });
187
+ res.end(JSON.stringify({ error: "Unsupported event type" }));
188
+ return;
267
189
  }
268
- });
269
- server.listen(port, () => {
270
- console.log(`\n✅ AI Dev Server running at http://localhost:${port}`);
271
- console.log(` POST http://localhost:${port}/api/chat`);
272
- console.log(` GET http://localhost:${port}/api/ai-info`);
273
- console.log(`\n Tip: Run "pnpm exec astro-ai-dev --init" to create datas/ directory.\n`);
274
- });
275
- // Graceful shutdown on SIGINT (Ctrl+C) and SIGTERM
276
- const shutdown = (signal) => {
277
- console.log(`\n🛑 AI Dev Server received ${signal}, shutting down...`);
278
- server.close(() => {
279
- console.log('✅ AI Dev Server closed');
280
- process.exit(0);
190
+ const data = body.data || {};
191
+ const siteUrl = process.env.SITE_URL || "http://localhost:4321";
192
+ const postUrl = data.url?.startsWith("http") ? data.url : `${siteUrl}${data.url || "/"}`;
193
+ const notifier = createNotifier({
194
+ telegram: process.env.NOTIFY_TELEGRAM_BOT_TOKEN && process.env.NOTIFY_TELEGRAM_CHAT_ID ? {
195
+ botToken: process.env.NOTIFY_TELEGRAM_BOT_TOKEN,
196
+ chatId: process.env.NOTIFY_TELEGRAM_CHAT_ID
197
+ } : void 0,
198
+ webhook: process.env.NOTIFY_WEBHOOK_URL ? {
199
+ url: process.env.NOTIFY_WEBHOOK_URL
200
+ } : void 0,
201
+ email: process.env.NOTIFY_RESEND_API_KEY && process.env.NOTIFY_RESEND_FROM && process.env.NOTIFY_RESEND_TO ? {
202
+ provider: "resend",
203
+ apiKey: process.env.NOTIFY_RESEND_API_KEY,
204
+ from: process.env.NOTIFY_RESEND_FROM,
205
+ to: process.env.NOTIFY_RESEND_TO
206
+ } : void 0
207
+ });
208
+ const result = await notifier.comment({
209
+ author: data.nick || "\u533F\u540D\u7528\u6237",
210
+ content: data.comment || "",
211
+ postTitle: extractPostTitle(data.url || "/"),
212
+ postUrl
281
213
  });
282
- // Force exit after 3 seconds if connections are pending
283
- setTimeout(() => {
284
- console.log('⚠️ Force closing after timeout');
285
- process.exit(1);
286
- }, 3000);
287
- };
288
- process.on('SIGINT', () => shutdown('SIGINT'));
289
- process.on('SIGTERM', () => shutdown('SIGTERM'));
214
+ console.log("[notify] Comment notification sent:", result.success ? "\u2713" : "\u2717", result.results);
215
+ res.writeHead(200, { "Content-Type": "application/json" });
216
+ res.end(JSON.stringify({
217
+ success: result.success,
218
+ event: "comment",
219
+ channels: result.results.map((r) => ({ channel: r.channel, success: r.success, error: r.error }))
220
+ }));
221
+ return;
222
+ }
223
+ if (url.startsWith("/api/ai-info")) {
224
+ res.writeHead(200, { "Content-Type": "application/json" });
225
+ res.end(JSON.stringify({
226
+ status: "ok",
227
+ mode: "dev-server",
228
+ datas: { found: hasDatas, path: datasDir },
229
+ ai: {
230
+ configured: !!(process.env.AI_BASE_URL && process.env.AI_API_KEY),
231
+ model: process.env.AI_MODEL || "unknown"
232
+ }
233
+ }, null, 2));
234
+ return;
235
+ }
236
+ res.writeHead(404, { "Content-Type": "application/json" });
237
+ res.end(JSON.stringify({ error: "Not found" }));
238
+ } catch (err) {
239
+ console.error("[AI Dev Server] Error:", err);
240
+ res.writeHead(500, { "Content-Type": "application/json" });
241
+ res.end(JSON.stringify({ error: "Internal server error", detail: err instanceof Error ? err.message : String(err) }));
242
+ }
243
+ });
244
+ server.listen(port, () => {
245
+ console.log(`
246
+ \u2705 AI Dev Server running at http://localhost:${port}`);
247
+ console.log(` POST http://localhost:${port}/api/chat`);
248
+ console.log(` GET http://localhost:${port}/api/ai-info`);
249
+ console.log(`
250
+ Tip: Run "pnpm exec astro-ai-dev --init" to create datas/ directory.
251
+ `);
252
+ });
253
+ const shutdown = (signal) => {
254
+ console.log(`
255
+ \u{1F6D1} AI Dev Server received ${signal}, shutting down...`);
256
+ server.close(() => {
257
+ console.log("\u2705 AI Dev Server closed");
258
+ process.exit(0);
259
+ });
260
+ setTimeout(() => {
261
+ console.log("\u26A0\uFE0F Force closing after timeout");
262
+ process.exit(1);
263
+ }, 3e3);
264
+ };
265
+ process.on("SIGINT", () => shutdown("SIGINT"));
266
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
290
267
  }
291
268
  main().catch((err) => {
292
- console.error('\n Failed to start AI Dev Server:', err);
293
- process.exit(1);
269
+ console.error("\n\u274C Failed to start AI Dev Server:", err);
270
+ process.exit(1);
294
271
  });
@@ -1,6 +1,39 @@
1
- export { handleChatRequest } from './chat-handler.js';
2
- export { initializeMetadata, resetMetadataInit } from './metadata-init.js';
3
- export { errors, corsPreflightResponse, chatError } from './errors.js';
4
- export { notifyAiChat } from './notify.js';
5
- export { writeSearchStatus, writeGeneratingStatus, writeDoneStatus, writeSourceArticles, writeTextChunk, writeFinish, streamLLMResponse, streamMockFallback, streamCachedResponse, } from './stream-helpers.js';
6
- export { createChatStatusData, isChatStatusData, } from './types.js';
1
+ import { handleChatRequest } from "./chat-handler.js";
2
+ import { initializeMetadata, resetMetadataInit } from "./metadata-init.js";
3
+ import { errors, corsPreflightResponse, chatError } from "./errors.js";
4
+ import { notifyAiChat } from "./notify.js";
5
+ import {
6
+ writeSearchStatus,
7
+ writeGeneratingStatus,
8
+ writeDoneStatus,
9
+ writeSourceArticles,
10
+ writeTextChunk,
11
+ writeFinish,
12
+ streamLLMResponse,
13
+ streamMockFallback,
14
+ streamCachedResponse
15
+ } from "./stream-helpers.js";
16
+ import {
17
+ createChatStatusData,
18
+ isChatStatusData
19
+ } from "./types.js";
20
+ export {
21
+ chatError,
22
+ corsPreflightResponse,
23
+ createChatStatusData,
24
+ errors,
25
+ handleChatRequest,
26
+ initializeMetadata,
27
+ isChatStatusData,
28
+ notifyAiChat,
29
+ resetMetadataInit,
30
+ streamCachedResponse,
31
+ streamLLMResponse,
32
+ streamMockFallback,
33
+ writeDoneStatus,
34
+ writeFinish,
35
+ writeGeneratingStatus,
36
+ writeSearchStatus,
37
+ writeSourceArticles,
38
+ writeTextChunk
39
+ };
@@ -1,2 +1,8 @@
1
- export { streamMockResponse } from './mock-stream.js';
2
- export { STREAM_HEADERS, JSON_HEADERS, errorResponse } from './response.js';
1
+ import { streamMockResponse } from "./mock-stream.js";
2
+ import { STREAM_HEADERS, JSON_HEADERS, errorResponse } from "./response.js";
3
+ export {
4
+ JSON_HEADERS,
5
+ STREAM_HEADERS,
6
+ errorResponse,
7
+ streamMockResponse
8
+ };