diff --git a/package-lock.json b/package-lock.json
index 412bbae..b14e071 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,7 @@
"name": "trip-planner",
"version": "0.0.0",
"dependencies": {
+ "cors": "^2.8.6",
"express": "^4.22.2",
"leaflet": "^1.9.4",
"pinia": "^3.0.4",
@@ -1108,6 +1109,23 @@
"url": "https://github.com/sponsors/mesqueeb"
}
},
+ "node_modules/cors": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -1640,6 +1658,15 @@
"node": ">= 0.6"
}
},
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
diff --git a/package.json b/package.json
index c5547e9..42e74e8 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
+ "cors": "^2.8.6",
"express": "^4.22.2",
"leaflet": "^1.9.4",
"pinia": "^3.0.4",
diff --git a/server.js b/server.js
index 93f1e55..102f3df 100644
--- a/server.js
+++ b/server.js
@@ -1,5 +1,5 @@
import express from 'express'
-import { createServer as createViteServer } from 'vite'
+import { spawn } from 'child_process'
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
@@ -43,7 +43,15 @@ function saveConfig(config) {
}
}
+import cors from 'cors'
+
const app = express()
+app.use(cors({
+ origin: 'http://localhost:5173',
+ methods: ['GET', 'POST', 'OPTIONS'],
+ allowedHeaders: ['Content-Type'],
+ exposedHeaders: ['Content-Type']
+}))
app.use(express.json())
// API: Get config
@@ -122,6 +130,7 @@ app.post('/api/config/test', async (req, res) => {
// API: Proxy AI chat request (keeps API key on server)
app.post('/api/chat', async (req, res) => {
+ console.log('[api/chat] POST received, body:', JSON.stringify(req.body).substring(0, 100))
const config = loadConfig()
if (!config.apiKey) {
@@ -130,7 +139,10 @@ app.post('/api/chat', async (req, res) => {
// Handle client disconnect
let clientDisconnected = false
- req.on('close', () => { clientDisconnected = true })
+ req.socket.on('close', () => {
+ clientDisconnected = true
+ console.log('[api/chat] Client socket closed')
+ })
try {
const response = await fetch(`${config.baseUrl}/chat/completions`, {
@@ -143,8 +155,7 @@ app.post('/api/chat', async (req, res) => {
...req.body,
model: req.body.model || config.model,
temperature: req.body.temperature ?? config.temperature,
- max_tokens: req.body.max_tokens || config.maxTokens,
- response_format: { type: 'json_object' }
+ max_tokens: req.body.max_tokens || config.maxTokens
})
})
@@ -153,14 +164,19 @@ app.post('/api/chat', async (req, res) => {
return res.status(response.status).json(error)
}
+ console.log('[api/chat] AI response status:', response.status)
+
// Stream the response to client
- res.setHeader('Content-Type', 'text/event-stream')
+ res.setHeader('Content-Type', 'application/x-ndjson')
res.setHeader('Cache-Control', 'no-cache')
res.setHeader('Connection', 'keep-alive')
res.setHeader('X-Accel-Buffering', 'no')
+ console.log('[api/chat] Streaming started')
const reader = response.body.getReader()
const decoder = new TextDecoder()
+ let totalBytes = 0
+ let chunkCount = 0
while (true) {
if (clientDisconnected) {
@@ -171,15 +187,17 @@ app.post('/api/chat', async (req, res) => {
if (done) break
const chunk = decoder.decode(value, { stream: true })
- try {
- res.write(chunk)
- } catch (e) {
- // Client disconnected, stop writing
- reader.cancel()
- return
+ totalBytes += chunk.length
+ chunkCount++
+
+ res.write(chunk)
+
+ if (chunkCount % 10 === 0) {
+ console.log('[api/chat] Chunk count:', chunkCount, 'bytes:', totalBytes)
}
}
+ console.log('[api/chat] Streaming done, total chunks:', chunkCount, 'bytes:', totalBytes)
res.end()
} catch (error) {
if (!clientDisconnected) {
@@ -188,22 +206,35 @@ app.post('/api/chat', async (req, res) => {
}
})
+// Catch-all for debugging
+app.all('/api/*', (req, res) => {
+ console.log('[DEBUG CATCH-ALL]', req.method, req.path)
+ res.status(404).json({ error: 'Route not found' })
+})
+
async function start() {
const isDev = process.env.NODE_ENV !== 'production'
if (isDev) {
- // Development: use Vite middleware
- const vite = await createViteServer({
- server: { middlewareMode: true },
- appType: 'spa'
+ console.log('[Server] Starting Vite dev server...')
+ // Run Vite dev server on port 5173
+ // API routes are handled by Express on port 3001
+ // We use a simple approach: start both servers
+ const viteProcess = spawn('npx', ['vite'], {
+ stdio: 'inherit',
+ shell: true,
+ env: { ...process.env, NODE_ENV: 'development' }
})
- app.use(vite.middlewares)
+ viteProcess.on('error', (err) => {
+ console.error('[Server] Failed to start Vite:', err)
+ })
- const port = 5173
- app.listen(port, () => {
- console.log(`Server running at http://localhost:${port}`)
- console.log(`API Key stored in: ${CONFIG_FILE}`)
+ // Start Express API server on port 3001
+ const apiPort = 3001
+ app.listen(apiPort, () => {
+ console.log(`[Server] API server running at http://localhost:${apiPort}`)
+ console.log(`[Server] API Key stored in: ${CONFIG_FILE}`)
})
} else {
// Production: serve static files
diff --git a/src/components/CustomPlanPanel.vue b/src/components/CustomPlanPanel.vue
new file mode 100644
index 0000000..1137d38
--- /dev/null
+++ b/src/components/CustomPlanPanel.vue
@@ -0,0 +1,473 @@
+
+ 输入你的行程安排,AI 帮你评估合理性并给出优化建议 {{ loadingText }} {{ evaluationResult.summary }} 输入目的地和天数,立即生成多个旅行方案 {{ loadingText }} {{ day.desc }} {{ currentDetailScheme.tips }}✏️ 自定义行程
+ {{ evaluationResult.isReasonable ? '✅ 行程合理' : '⚠️ 行程有待优化' }}
+ 优化建议
+
+
+ 优化后的行程方案
+ 按你的原始输入生成
+ 快速规划
+ 当前方案
+ {{ scheme.name }}
+
+ 历史方案 ({{ historySchemes.length }})
+
+
+ {{ scheme.name }}
+ {{ currentDetailScheme.name }}
+
+
+ 亮点
+
+
+ 每日行程
+ 旅行贴士
+