Major updates: - December 2025 crisis documentation and separation agreement - Daily check system v2 with multiple card categories - Xiaozhu rental search tools and results - Exit plan documentation - Message drafts for family communication - Confluent moved to CONSTANT - Updated profiles and promises 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
325 lines
9.5 KiB
JavaScript
325 lines
9.5 KiB
JavaScript
const https = require('https');
|
|
const fs = require('fs');
|
|
|
|
/**
|
|
* Amap Distance Checker - Calculate transit time to Jiaotong University
|
|
* Uses Amap (高德地图) Web Service API
|
|
*/
|
|
|
|
const CONFIG = {
|
|
// Target: Jiaotong University Xujiahui Campus
|
|
targetName: '上海交通大学徐汇校区',
|
|
targetCoords: '121.4367,31.1880', // lon,lat format for Amap
|
|
|
|
// Amap API (free tier, no key needed for basic geocoding)
|
|
// For route planning, we'll use web scraping approach
|
|
|
|
// Input/Output
|
|
inputFile: './xiaozhu_raw_listings.json',
|
|
outputFile: './xiaozhu_with_distances.json',
|
|
outputMarkdown: './xiaozhu_with_distances.md'
|
|
};
|
|
|
|
console.log('🗺️ Amap Distance Checker');
|
|
console.log(`🎯 Target: ${CONFIG.targetName}`);
|
|
console.log(`📍 Coordinates: ${CONFIG.targetCoords}\n`);
|
|
|
|
// Helper to make HTTPS requests
|
|
function httpsGet(url) {
|
|
return new Promise((resolve, reject) => {
|
|
https.get(url, (res) => {
|
|
let data = '';
|
|
res.on('data', chunk => data += chunk);
|
|
res.on('end', () => {
|
|
try {
|
|
resolve(JSON.parse(data));
|
|
} catch (e) {
|
|
resolve(data);
|
|
}
|
|
});
|
|
}).on('error', reject);
|
|
});
|
|
}
|
|
|
|
// Extract location from title
|
|
function extractLocation(title) {
|
|
// Common patterns in Xiaozhu titles
|
|
const patterns = [
|
|
/近(.{2,10})/, // 近X
|
|
/(.{2,10})地铁/, // X地铁
|
|
/(.{2,10})医院/, // X医院
|
|
/(.{2,10})路/, // X路
|
|
];
|
|
|
|
const locations = [];
|
|
|
|
for (const pattern of patterns) {
|
|
const matches = title.matchAll(new RegExp(pattern, 'g'));
|
|
for (const match of matches) {
|
|
if (match[1] && match[1].length >= 2) {
|
|
locations.push(match[1]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Also extract subway lines
|
|
const metroPattern = /(\d+)号线/g;
|
|
const metroMatches = [...title.matchAll(metroPattern)];
|
|
const metroLines = metroMatches.map(m => `Line ${m[1]}`);
|
|
|
|
return {
|
|
landmarks: [...new Set(locations)].slice(0, 3),
|
|
metroLines: [...new Set(metroLines)]
|
|
};
|
|
}
|
|
|
|
// Estimate distance category based on landmarks and metro lines
|
|
function estimateDistance(listing) {
|
|
const title = listing.title || '';
|
|
const location = listing.location || '';
|
|
const fullText = (title + ' ' + location).toLowerCase();
|
|
|
|
// Extract location info
|
|
const locationInfo = extractLocation(title);
|
|
|
|
// Keywords indicating proximity to Jiaotong University
|
|
const veryClose = [
|
|
'交通大学', '徐家汇', '上海交大', 'jiaotong', 'xujiahui'
|
|
];
|
|
|
|
const close = [
|
|
'衡山路', 'hengshan', '淮海路', 'huaihai',
|
|
'常熟路', 'changshu', '肇嘉浜路'
|
|
];
|
|
|
|
const medium = [
|
|
'上海体育馆', 'stadium', '龙华', 'longhua',
|
|
'东安路', 'dongan', '漕河泾'
|
|
];
|
|
|
|
const far = [
|
|
'南站', 'south station', '华东理工', '闵行', 'minhang'
|
|
];
|
|
|
|
// Check keywords
|
|
for (const keyword of veryClose) {
|
|
if (fullText.includes(keyword)) {
|
|
return {
|
|
category: 'very_close',
|
|
estimatedMinutes: 10,
|
|
confidence: 'high',
|
|
reason: `Mentions ${keyword}`,
|
|
locationInfo
|
|
};
|
|
}
|
|
}
|
|
|
|
for (const keyword of close) {
|
|
if (fullText.includes(keyword)) {
|
|
return {
|
|
category: 'close',
|
|
estimatedMinutes: 15,
|
|
confidence: 'medium',
|
|
reason: `Near ${keyword}`,
|
|
locationInfo
|
|
};
|
|
}
|
|
}
|
|
|
|
for (const keyword of medium) {
|
|
if (fullText.includes(keyword)) {
|
|
return {
|
|
category: 'medium',
|
|
estimatedMinutes: 25,
|
|
confidence: 'medium',
|
|
reason: `Around ${keyword}`,
|
|
locationInfo
|
|
};
|
|
}
|
|
}
|
|
|
|
for (const keyword of far) {
|
|
if (fullText.includes(keyword)) {
|
|
return {
|
|
category: 'far',
|
|
estimatedMinutes: 40,
|
|
confidence: 'medium',
|
|
reason: `Far area - ${keyword}`,
|
|
locationInfo
|
|
};
|
|
}
|
|
}
|
|
|
|
// Check metro lines (10, 11 are best for Jiaotong Uni)
|
|
if (title.includes('10号线') || title.includes('11号线')) {
|
|
return {
|
|
category: 'close',
|
|
estimatedMinutes: 15,
|
|
confidence: 'high',
|
|
reason: 'On Line 10 or 11 (direct to campus)',
|
|
locationInfo
|
|
};
|
|
}
|
|
|
|
// Lines with transfer
|
|
if (title.includes('1号线') || title.includes('9号线')) {
|
|
return {
|
|
category: 'medium',
|
|
estimatedMinutes: 25,
|
|
confidence: 'medium',
|
|
reason: 'Requires 1 transfer to Line 10/11',
|
|
locationInfo
|
|
};
|
|
}
|
|
|
|
return {
|
|
category: 'unknown',
|
|
estimatedMinutes: 30,
|
|
confidence: 'low',
|
|
reason: 'Unable to determine location',
|
|
locationInfo
|
|
};
|
|
}
|
|
|
|
async function processListings() {
|
|
// Load listings
|
|
console.log(`📂 Loading listings from ${CONFIG.inputFile}...`);
|
|
|
|
let listings;
|
|
try {
|
|
const data = fs.readFileSync(CONFIG.inputFile, 'utf8');
|
|
listings = JSON.parse(data);
|
|
} catch (err) {
|
|
console.error(`❌ Error loading file: ${err.message}`);
|
|
console.log('\n💡 Make sure you ran xiaozhu_fixed.js first to generate the listings.');
|
|
return;
|
|
}
|
|
|
|
console.log(`✅ Loaded ${listings.length} listings\n`);
|
|
|
|
// Process each listing
|
|
console.log('🔍 Analyzing distances...\n');
|
|
|
|
const processed = listings.map((listing, i) => {
|
|
const distance = estimateDistance(listing);
|
|
|
|
console.log(`${i + 1}. ${listing.title?.substring(0, 60) || 'Untitled'}`);
|
|
console.log(` 💰 ¥${listing.priceDaily}/day`);
|
|
console.log(` 📍 Distance: ${distance.category.toUpperCase()} (~${distance.estimatedMinutes} min)`);
|
|
console.log(` 🔍 Reason: ${distance.reason}`);
|
|
if (distance.locationInfo.landmarks.length > 0) {
|
|
console.log(` 🏷️ Landmarks: ${distance.locationInfo.landmarks.join(', ')}`);
|
|
}
|
|
if (distance.locationInfo.metroLines.length > 0) {
|
|
console.log(` 🚇 Metro: ${distance.locationInfo.metroLines.join(', ')}`);
|
|
}
|
|
console.log('');
|
|
|
|
return {
|
|
...listing,
|
|
distance: {
|
|
...distance,
|
|
toJiaotongUniversity: distance.estimatedMinutes
|
|
}
|
|
};
|
|
});
|
|
|
|
// Sort by distance (closest first)
|
|
const sorted = [...processed].sort((a, b) => {
|
|
// Prioritize by distance, then by price
|
|
if (a.distance.toJiaotongUniversity !== b.distance.toJiaotongUniversity) {
|
|
return a.distance.toJiaotongUniversity - b.distance.toJiaotongUniversity;
|
|
}
|
|
return (a.priceDaily || 9999) - (b.priceDaily || 9999);
|
|
});
|
|
|
|
// Save results
|
|
fs.writeFileSync(CONFIG.outputFile, JSON.stringify(sorted, null, 2));
|
|
console.log(`💾 Saved results to ${CONFIG.outputFile}\n`);
|
|
|
|
// Generate markdown
|
|
generateMarkdown(sorted);
|
|
|
|
// Print summary
|
|
printSummary(sorted);
|
|
}
|
|
|
|
function generateMarkdown(listings) {
|
|
let md = '# Xiaozhu Listings - With Distance to Jiaotong University\n\n';
|
|
md += `**Target:** ${CONFIG.targetName}\n`;
|
|
md += `**Sorted by:** Distance (closest first)\n\n`;
|
|
|
|
md += '| # | Title | Price/Day | Total (29d) | Distance | Est. Time | Kitchen | Metro Lines |\n';
|
|
md += '|---|-------|-----------|-------------|----------|-----------|---------|-------------|\n';
|
|
|
|
listings.forEach((l, i) => {
|
|
const total = (l.priceDaily || 0) * 29;
|
|
const distanceEmoji =
|
|
l.distance.category === 'very_close' ? '🟢' :
|
|
l.distance.category === 'close' ? '🟡' :
|
|
l.distance.category === 'medium' ? '🟠' :
|
|
l.distance.category === 'far' ? '🔴' : '⚪';
|
|
|
|
md += `| ${i + 1} `;
|
|
md += `| ${(l.title || 'Untitled').substring(0, 50)}... `;
|
|
md += `| ¥${l.priceDaily || '-'} `;
|
|
md += `| ¥${total} `;
|
|
md += `| ${distanceEmoji} ${l.distance.category} `;
|
|
md += `| ~${l.distance.toJiaotongUniversity} min `;
|
|
md += `| ${l.hasKitchen ? '✓' : '✗'} `;
|
|
md += `| ${l.distance.locationInfo.metroLines.join(', ') || '-'} |\n`;
|
|
});
|
|
|
|
md += '\n## Distance Categories\n\n';
|
|
md += '- 🟢 **VERY CLOSE**: < 15 min - Direct area (徐家汇, 交通大学)\n';
|
|
md += '- 🟡 **CLOSE**: 15-20 min - Nearby (衡山路, Line 10/11)\n';
|
|
md += '- 🟠 **MEDIUM**: 20-30 min - Requires transfer (Line 1/9)\n';
|
|
md += '- 🔴 **FAR**: 30+ min - Far areas (南站, 闵行)\n';
|
|
md += '- ⚪ **UNKNOWN**: Distance unclear\n\n';
|
|
|
|
md += '## Best Options (Closest + Kitchen + Budget)\n\n';
|
|
|
|
const best = listings
|
|
.filter(l => l.hasKitchen)
|
|
.filter(l => (l.priceDaily * 29) <= 5800) // Allow slight budget flex
|
|
.slice(0, 5);
|
|
|
|
best.forEach((l, i) => {
|
|
md += `### ${i + 1}. ${l.title}\n\n`;
|
|
md += `- **Price:** ¥${l.priceDaily}/day (¥${l.priceDaily * 29} total)\n`;
|
|
md += `- **Distance:** ${l.distance.category} (~${l.distance.toJiaotongUniversity} min)\n`;
|
|
md += `- **Reason:** ${l.distance.reason}\n`;
|
|
md += `- **Kitchen:** ${l.hasKitchen ? 'Yes ✓' : 'No'}\n`;
|
|
md += `- **Metro:** ${l.distance.locationInfo.metroLines.join(', ') || 'Not specified'}\n\n`;
|
|
});
|
|
|
|
fs.writeFileSync(CONFIG.outputMarkdown, md);
|
|
console.log(`📝 Saved markdown to ${CONFIG.outputMarkdown}\n`);
|
|
}
|
|
|
|
function printSummary(listings) {
|
|
console.log('=' .repeat(60));
|
|
console.log('📊 SUMMARY - TOP 5 CLOSEST WITH KITCHEN\n');
|
|
|
|
const topClosest = listings
|
|
.filter(l => l.hasKitchen)
|
|
.slice(0, 5);
|
|
|
|
topClosest.forEach((l, i) => {
|
|
const total = l.priceDaily * 29;
|
|
const inBudget = total <= 5000 ? '✅' : '⚠️';
|
|
|
|
console.log(`${i + 1}. ${l.title?.substring(0, 60)}`);
|
|
console.log(` 💰 ¥${l.priceDaily}/day = ¥${total} total ${inBudget}`);
|
|
console.log(` 📍 ${l.distance.category.toUpperCase()} - ~${l.distance.toJiaotongUniversity} min`);
|
|
console.log(` 🔍 ${l.distance.reason}`);
|
|
console.log('');
|
|
});
|
|
|
|
console.log('=' .repeat(60));
|
|
console.log('\n✅ Done! Check xiaozhu_with_distances.md for full results.');
|
|
}
|
|
|
|
// Run
|
|
processListings().catch(console.error);
|