@clawhub-jiajiaoy-cba025e9ff
Compare real-time flights and hotels with price forecasts, coupon stacking, and destination news to find the best time and deal for your trip.
# TravelHound
> Flight and hotel price comparison with booking timing analysis, OTA coupon stacking, and destination intelligence — all in one skill.
TravelHound combines real-time price data from Google Flights, Skyscanner, Kayak, Booking.com, Agoda, and Trip.com with coupon stacking via CouponClaw and destination news via NewsToday. It tells you not just the cheapest option, but whether now is the right time to book.
## What TravelHound does
- **Flight comparison**: Google Flights + Skyscanner + Kayak + Trip.com, with Kayak's "Buy now vs Wait" forecast
- **Hotel comparison**: Booking.com + Agoda + Hotels.com + Trip.com, with OTA coupon stacking
- **Full trip planner**: Flights + hotels in one output with total budget estimate
- **Destination intelligence**: Visa requirements, exchange rate trend, safety advisories, latest news
- **Booking timing**: Price history analysis to decide whether to book now or wait
## Trigger phrases
- "cheap flights to..."
- "flights from X to Y"
- "机票"
- "查机票"
- "hotel in..."
- "酒店"
- "查酒店"
- "trip to..."
- "travel to..."
- "旅行计划"
- "全程规划"
- "去X旅游"
- "订酒店"
- "best time to book"
- "should I book now"
## Scripts
| Script | Command | Description |
|---|---|---|
| `flights.js` | `node scripts/flights.js <from> <to> [--date YYYY-MM-DD] [--return YYYY-MM-DD] [--class economy\|business] [--passengers N] [--lang zh\|en]` | Flight price comparison across 4 platforms with booking timing advice |
| `hotels.js` | `node scripts/hotels.js <destination> [--checkin YYYY-MM-DD] [--checkout YYYY-MM-DD] [--guests N] [--budget budget\|mid\|luxury] [--lang zh\|en]` | Hotel price comparison with OTA coupon stacking |
| `trip.js` | `node scripts/trip.js <destination> [--from city] [--date YYYY-MM-DD] [--nights N] [--budget budget\|mid\|luxury] [--lang zh\|en]` | Full trip planner: flights + hotels + destination intel in one report |
## Data sources
| Platform | Type | Strength |
|---|---|---|
| Google Flights | Flights | Best aggregator; date flexibility view; price insights |
| Skyscanner | Flights | Lowest fares including budget carriers; "Everywhere" flexible origin |
| Kayak | Flights + Hotels | Price Forecast (Buy now / Wait); price history chart |
| Trip.com / 携程 | Flights + Hotels | Often cheapest for Asian routes; Chinese carrier coverage |
| Booking.com | Hotels | Widest inventory globally; Genius member discounts |
| Agoda | Hotels | Best rates for Southeast and East Asia |
| Hotels.com | Hotels | 10-night loyalty program |
## Ecosystem integration
TravelHound calls:
- **CouponClaw** — finds promo codes for Booking.com, Agoda, Trip.com, and Skyscanner on top of platform prices
- **NewsToday** — pulls destination-relevant news (political situation, weather events, local festivals)
## No API required
TravelHound uses browser navigation to read live pricing from each platform. No API keys or subscriptions needed.
FILE:README.md
# TravelHound — Find Cheap Flights & Hotels Before You Book
> Compare Google Flights, Skyscanner, Kayak, Booking.com, Agoda, Trip.com side by side. Get a Buy/Wait verdict. Stack OTA coupon codes.
[](https://clawhub.ai/skills/travelhound)
[](https://clawhub.ai/skills/travelhound)
[](LICENSE)
## What it does
TravelHound is an [OpenClaw](https://openclaw.ai) skill that does for flights and hotels what [BuyWise](https://github.com/jiajiaoy/BuyWise) does for products: compares real prices across platforms, checks whether now is the right time to book, finds promo codes, and gives you a clear verdict.
**Flight comparison** — Google Flights + Skyscanner + Kayak + Trip.com, with Kayak's *Buy now / Wait* forecast
**Hotel comparison** — Booking.com + Agoda + Hotels.com + Trip.com, with OTA coupon stacking
**Full trip planner** — flights + hotels in one report with total budget estimate
**Destination intelligence** — visa requirements, exchange rate trend, safety advisories, latest news
**OTA coupon stacking** — calls [CouponClaw](https://github.com/jiajiaoy/CouponClaw) to find promo codes for Booking.com, Agoda, Trip.com
**News check** — calls [NewsToday](https://github.com/jiajiaoy/NewsToday) for destination-relevant headlines (political situation, weather, local events)
## Data sources
| Platform | Type | Strength |
|---|---|---|
| Google Flights | Flights | Best aggregator; price insights; date flexibility view |
| Skyscanner | Flights | Lowest fares incl. budget carriers; flexible origin |
| Kayak | Flights + Hotels | Price Forecast (Buy / Wait) + price history chart |
| Trip.com / 携程 | Flights + Hotels | Best rates for Asian routes; Chinese carrier coverage |
| Booking.com | Hotels | Widest global inventory; Genius discounts |
| Agoda | Hotels | Best rates for Southeast and East Asia |
| Hotels.com | Hotels | 10-night loyalty reward |
## Installation
```bash
openclaw install travelhound
```
## Usage
```bash
# Full trip planner: flights + hotels + destination intel
openclaw run travelhound trip "Tokyo" --from "London" --date 2026-08-01 --nights 7 --budget mid
# Flights only
openclaw run travelhound flights "London" "Tokyo" --date 2026-08-01 --return 2026-08-08
# Hotels only
openclaw run travelhound hotels "Tokyo" --checkin 2026-08-01 --checkout 2026-08-08 --budget mid
# Budget options
# --budget budget | mid | luxury
# --class economy | business | first
# --lang zh | en
```
## Ecosystem
Part of the **OpenClaw Smart Consumer** skill suite:
```
NewsToday ←→ TravelHound → CouponClaw
↑
TrendRadar (trending destinations)
```
| Skill | How it connects |
|---|---|
| **TravelHound** | Travel pricing ← you are here |
| [CouponClaw](https://github.com/jiajiaoy/CouponClaw) | OTA promo codes stacked on top of platform prices |
| [NewsToday](https://github.com/jiajiaoy/NewsToday) | Destination news (visa changes, safety, events) |
| [BuyWise](https://github.com/jiajiaoy/BuyWise) | Same buy/wait decision logic, applied to products |
## Keywords
cheap flights · flight comparison · best time to book flights · flight price prediction · Google Flights · Skyscanner · Kayak · cheap hotels · hotel comparison · Booking.com · Agoda · budget travel · travel hacks · OTA coupon · visa requirements · exchange rate · 机票 · 酒店 · 旅行攻略 · 特价机票 · 穷游 · 自由行
---
Built for [OpenClaw](https://openclaw.ai) · Published on [clawhub.ai/skills/travelhound](https://clawhub.ai/skills/travelhound)
FILE:package.json
{
"name": "travelhound",
"version": "1.1.2",
"description": "Find cheap flights & hotels before you book — compare Google Flights, Skyscanner, Booking.com, Agoda side-by-side. Fare Buy/Wait forecast + OTA coupon stacking.",
"keywords": [
"flights",
"cheap flights",
"flight deals",
"flight comparison",
"airfare",
"best time to book flights",
"flight price prediction",
"flight price tracker",
"Google Flights",
"Skyscanner",
"Kayak",
"Trip.com",
"携程",
"hotels",
"cheap hotels",
"hotel deals",
"hotel comparison",
"accommodation",
"Booking.com",
"Agoda",
"Hotels.com",
"travel",
"travel planning",
"travel deals",
"travel savings",
"travel hacks",
"budget travel",
"backpacking",
"vacation",
"vacation planning",
"holiday",
"holiday deals",
"holiday planning",
"trip planner",
"itinerary",
"OTA",
"OTA coupon",
"OTA promo code",
"travel coupon",
"visa requirements",
"exchange rate",
"travel safety",
"destination guide",
"low cost flights",
"budget airline",
"flight hack",
"travel budget",
"机票",
"廉价机票",
"特价机票",
"机票比价",
"机票攻略",
"酒店",
"特价酒店",
"低价酒店",
"酒店比价",
"酒店攻略",
"旅行",
"旅游",
"旅行计划",
"旅行省钱",
"旅游攻略",
"自由行",
"穷游",
"出境游",
"签证",
"汇率",
"订机票",
"订酒店",
"机酒套餐",
"旅游优惠"
],
"main": "scripts/trip.js",
"scripts": {
"flights": "node scripts/flights.js",
"hotels": "node scripts/hotels.js",
"trip": "node scripts/trip.js"
},
"openclaw": {
"triggers": [
"flights",
"cheap flights",
"flight deals",
"hotels",
"hotel",
"travel",
"trip to",
"vacation",
"holiday",
"机票",
"酒店",
"旅行",
"旅游",
"订机票",
"订酒店",
"出行",
"去哪玩"
],
"entrypoint": "scripts/trip.js"
},
"dependencies": {}
}
FILE:scripts/flights.js
#!/usr/bin/env node
'use strict';
/**
* TravelHound — 机票比价 + 最佳订票时机
* 用法: node scripts/flights.js <出发地> <目的地> [--date YYYY-MM-DD] [--return YYYY-MM-DD] [--class economy|business] [--passengers N] [--lang zh|en]
*/
const ALLOWED_CLASSES = new Set(['economy', 'business', 'first']);
const args = process.argv.slice(2);
const langIdx = args.indexOf('--lang');
const lang = langIdx !== -1 && args[langIdx + 1] === 'en' ? 'en' : 'zh';
const dateIdx = args.indexOf('--date');
const date = dateIdx !== -1 ? args[dateIdx + 1] : null;
const returnIdx = args.indexOf('--return');
const returnDate = returnIdx !== -1 ? args[returnIdx + 1] : null;
const classIdx = args.indexOf('--class');
const rawClass = classIdx !== -1 ? args[classIdx + 1] : 'economy';
const cabinClass = ALLOWED_CLASSES.has(rawClass) ? rawClass : 'economy';
const passIdx = args.indexOf('--passengers');
const passengers = passIdx !== -1 ? parseInt(args[passIdx + 1], 10) || 1 : 1;
const locationArgs = args.filter((a, i) => {
const flags = ['--lang', '--date', '--return', '--class', '--passengers'];
if (flags.includes(a)) return false;
if (flags.includes(args[i - 1])) return false;
return true;
});
const [from = '', to = ''] = locationArgs;
if (!from || !to) {
console.error(lang === 'zh'
? '用法: node scripts/flights.js <出发地> <目的地> [--date YYYY-MM-DD] [--return YYYY-MM-DD]'
: 'Usage: node scripts/flights.js <from> <to> [--date YYYY-MM-DD] [--return YYYY-MM-DD]');
process.exit(1);
}
const encFrom = encodeURIComponent(from);
const encTo = encodeURIComponent(to);
const encQuery = encodeURIComponent(`from to to`);
const encQueryZH = encodeURIComponent(`from到to机票`);
const dateNote = date ? (lang === 'zh' ? `出发日期:date` : `Departure: date`) : (lang === 'zh' ? '出发日期:未指定(请查询近期最低价区间)' : 'Departure: not specified — look for lowest fare window');
const returnNote = returnDate ? (lang === 'zh' ? `返程日期:returnDate` : `Return: returnDate`) : '';
const tripType = returnDate ? (lang === 'zh' ? '往返' : 'Round trip') : (lang === 'zh' ? '单程' : 'One way');
const classNote = lang === 'zh'
? ({ economy: '经济舱', business: '商务舱', first: '头等舱' }[cabinClass])
: ({ economy: 'Economy', business: 'Business', first: 'First' }[cabinClass]);
if (lang === 'zh') {
console.log(`请为以下行程查询机票价格并分析最佳订票时机。
行程:from → to(tripType · classNote · passengers人)
dateNote''
使用 browser 工具依次导航以下页面,提取真实票价:
1. 打开 https://www.google.com/travel/flights?q=encQueryZH
→ Google Flights:提取各航空公司价格、最便宜日期(日历视图),以及页面底部的"价格走势"和"订票建议"(Buy now / Wait)
2. 打开 https://www.skyscanner.com/transport/flights/encFrom/encTo/date || ''
→ Skyscanner:提取最低价航班(含中转)、价格预警提示(当月最低 / 偏高),记录前3条推荐
3. 打开 https://www.kayak.com/flights/encFrom-encTo/date || 'flexible'
→ Kayak:重点查看 "Price Forecast" 模块(显示 Buy / Wait 建议和理由),以及 Price History 图表
4. 打开 https://flights.trip.com/flights/list?depCityName=encFrom&arrCityName=encTodate ? `&flightDate=${date` : ''}
→ 携程/Trip.com:提取含中文航司的价格,尤其是国内航线和亚洲航线通常比国际平台便宜
5. web_search「from to 机票 date || '近期' 最低价 航班」
→ 补充获取近期促销航班信息
【历史价格核查】
6. 在步骤1 Google Flights 中查看"价格图表",判断当前价格是否处于历史低位
7. web_search「from to to flights cheap months best time to fly」→ 了解该航线的淡旺季规律
【判断标准】
- 🟢 立即订:当前价格 ≤ 近3个月均价的 90%,或 Google Flights/Kayak 明确显示 "Buy now"
- 🟡 再等等:价格高于均价但有下降趋势,或距出发超过6周仍有降价空间
- 🔴 贵,考虑替代:旺季/节假日价格,建议改签日期或换出发城市
输出格式:
✈️ TravelHound · from → to
━━━━━━━━━━━━━━━━━━━━━━━
📋 行程:tripType · classNote · passengers人 · date || '日期灵活'''
💰 各平台票价对比
平台 | 最低价 | 航班号/航司 | 飞行时长 | 中转
Google Flights | ¥/$ XXX | | |
Skyscanner | ¥/$ XXX | | |
Kayak | ¥/$ XXX | | |
携程/Trip.com | ¥/$ XXX | | |
📊 价格分析
当前价格位置:历史低位 / 正常 / 偏高
Kayak 预测:Buy now / Wait(原因)
最便宜出发日:± X天调整可省 ¥/$ XXX
✅ 订票建议
[🟢/🟡/🔴 + 1-2句理由 + 推荐平台]
🎟️ 订票优惠码(运行 CouponClaw 查询):
openclaw run couponclaw find "Trip.com" --region all
openclaw run couponclaw find "Skyscanner" --region all
━━━━━━━━━━━━━━━━━━━━━━━
💡 回复"查酒店"继续规划住宿 · 回复"全程规划"一站搞定`);
} else {
console.log(`Please research flight prices for the following itinerary and advise on the best time to book.
Route: from → to (tripType · classNote · passengers pax)
dateNote''
Use the browser tool to navigate each page and extract real fares:
1. Open https://www.google.com/travel/flights?q=encQuery
→ Google Flights: extract prices per airline, cheapest date calendar, and the "Price insights" / "Buy now vs Wait" recommendation at the bottom
2. Open https://www.skyscanner.com/transport/flights/encFrom/encTo/date || ''
→ Skyscanner: extract lowest fares (incl. connections), price alert badge (lowest this month / high), top 3 results
3. Open https://www.kayak.com/flights/encFrom-encTo/date || 'flexible'
→ Kayak: focus on the "Price Forecast" module (Buy / Wait + reason) and Price History chart
4. Open https://uk.trip.com/flights/list?depCityName=encFrom&arrCityName=encTodate ? `&flightDate=${date` : ''}
→ Trip.com: often has lower fares for Asian routes and Chinese carriers
5. web_search "cheap flights from to to date || 'next month' deals"
→ Supplement with any current fare sales
[Price history check]
6. In Google Flights from step 1, check the price chart — is the current price at/near a low?
7. web_search "from to to cheapest months best time to fly" → understand seasonal pricing patterns
[Verdict criteria]
- 🟢 Book now: current price ≤ 90% of 3-month average, or Google Flights/Kayak says "Buy now"
- 🟡 Wait: price above average but trending down, or 6+ weeks out with room to drop
- 🔴 Expensive — consider alternatives: peak season/holiday pricing; suggest date shift or alternate origin
Output format:
✈️ TravelHound · from → to
━━━━━━━━━━━━━━━━━━━━━━━
📋 Trip: tripType · classNote · passengers pax · date || 'flexible dates'''
💰 Price comparison
Platform | Lowest fare | Airline | Duration | Stops
Google Flights | $XXX | | |
Skyscanner | $XXX | | |
Kayak | $XXX | | |
Trip.com | $XXX | | |
📊 Price analysis
Current price position: near low / average / high
Kayak forecast: Buy now / Wait (reason)
Cheapest nearby date: ±X days saves $XXX
✅ Booking recommendation
[🟢/🟡/🔴 + 1-2 sentence rationale + recommended platform]
🎟️ OTA promo codes (run CouponClaw):
openclaw run couponclaw find "Trip.com" --region all
openclaw run couponclaw find "Skyscanner" --region all
━━━━━━━━━━━━━━━━━━━━━━━
💡 Reply "find hotels" to continue planning · Reply "full trip" for combined itinerary`);
}
FILE:scripts/hotels.js
#!/usr/bin/env node
'use strict';
/**
* TravelHound — 酒店比价 + OTA优惠码叠加
* 用法: node scripts/hotels.js <目的地> [--checkin YYYY-MM-DD] [--checkout YYYY-MM-DD] [--guests N] [--budget budget|mid|luxury] [--lang zh|en]
*/
const ALLOWED_BUDGETS = new Set(['budget', 'mid', 'luxury']);
const args = process.argv.slice(2);
const langIdx = args.indexOf('--lang');
const lang = langIdx !== -1 && args[langIdx + 1] === 'en' ? 'en' : 'zh';
const checkinIdx = args.indexOf('--checkin');
const checkin = checkinIdx !== -1 ? args[checkinIdx + 1] : null;
const checkoutIdx = args.indexOf('--checkout');
const checkout = checkoutIdx !== -1 ? args[checkoutIdx + 1] : null;
const guestsIdx = args.indexOf('--guests');
const guests = guestsIdx !== -1 ? parseInt(args[guestsIdx + 1], 10) || 1 : 1;
const budgetIdx = args.indexOf('--budget');
const rawBudget = budgetIdx !== -1 ? args[budgetIdx + 1] : 'mid';
const budget = ALLOWED_BUDGETS.has(rawBudget) ? rawBudget : 'mid';
const destArgs = args.filter((a, i) => {
const flags = ['--lang', '--checkin', '--checkout', '--guests', '--budget'];
if (flags.includes(a)) return false;
if (flags.includes(args[i - 1])) return false;
return true;
});
const destination = destArgs.join(' ').trim();
if (!destination) {
console.error(lang === 'zh'
? '用法: node scripts/hotels.js <目的地> [--checkin YYYY-MM-DD] [--checkout YYYY-MM-DD] [--guests N] [--budget budget|mid|luxury]'
: 'Usage: node scripts/hotels.js <destination> [--checkin YYYY-MM-DD] [--checkout YYYY-MM-DD] [--guests N] [--budget budget|mid|luxury]');
process.exit(1);
}
const encoded = encodeURIComponent(destination);
const nights = (checkin && checkout)
? Math.round((new Date(checkout) - new Date(checkin)) / 86400000)
: null;
const budgetLabelZH = { budget: '经济型(¥300以下/晚)', mid: '中档(¥300-800/晚)', luxury: '高档(¥800+/晚)' }[budget];
const budgetLabelEN = { budget: 'Budget (under $50/night)', mid: 'Mid-range ($50-150/night)', luxury: 'Luxury ($150+/night)' }[budget];
if (lang === 'zh') {
const dateNote = checkin ? `入住:checkincheckout ? ` → 退房:${checkout(共nights晚)` : ''}` : '日期:未指定';
console.log(`请为以下住宿需求查询酒店价格,并找到最优 OTA 优惠。
目的地:destination
dateNote
人数:guests人 · 预算档位:budgetLabelZH
使用 browser 工具依次导航以下页面,提取真实酒店价格:
1. 打开 https://www.booking.com/searchresults.html?ss=encodedcheckin ? `&checkin=${checkin` : ''}checkout ? `&checkout=${checkout` : ''}&group_adults=guests
→ Booking.com:提取该档位前5家酒店(名称、价格/晚、评分、距市中心距离)
→ 查看是否有"天才会员折扣"或"限时特惠"标签
2. 打开 https://www.agoda.com/search?q=encodedcheckin ? `&checkIn=${checkin` : ''}checkout ? `&checkOut=${checkout` : ''}&numberOfGuest=guests
→ Agoda:同样提取前5家,Agoda 在亚洲目的地通常有独家低价
→ 注意查看"银行卡专属折扣"(特定信用卡额外减)
3. 打开 https://www.hotels.com/search.do?q-destination=encodedcheckin ? `&q-check-in=${checkin` : ''}checkout ? `&q-check-out=${checkout` : ''}
→ Hotels.com:集满10晚送1晚,查询是否有额外促销
4. 打开 https://hotels.trip.com/hotels/list?city=encodedcheckin ? `&startDate=${checkin` : ''}checkout ? `&endDate=${checkout` : ''}
→ 携程酒店:国内及亚洲目的地必查,会员价通常比国际平台低 10-20%
5. web_search「destination 酒店 checkin || '近期' 优惠 团购」
→ 补充:美团、飞猪等国内平台是否有团购价
【价格对比规则】
- 仅记录实际从页面读取的价格(标注含税/不含税)
- 标注每个平台是否需要会员登录才能看到最低价
- 同一家酒店跨平台比价,找出实际最低平台
【OTA 优惠码叠加】
查询各平台专属优惠码(在价格基础上再减):
- web_search「Booking.com promo code new Date().getFullYear()」
- web_search「Agoda coupon code new Date().getFullYear()」
或直接运行:openclaw run couponclaw find "Booking.com" --region all
openclaw run couponclaw find "Agoda" --region all
输出格式:
🏨 TravelHound · destination 酒店
━━━━━━━━━━━━━━━━━━━━━━━
📋 dateNotenights ? `(${nights晚)` : ''} · guests人 · budgetLabelZH
💰 各平台价格对比(budgetLabelZH)
酒店名 | Booking | Agoda | Hotels.com | 携程 | 最低
[酒店A] | ¥XXX | ¥XXX | ¥XXX | ¥XXX | [平台] ¥XXX
[酒店B] | ...
(列出3-5家同档位酒店)
🏷️ 平台优惠
[平台] 优惠码/活动:[描述] | 额外节省:¥XXX
🔢 最优方案
[酒店名] × nights || 'X'晚 = ¥XXX(通过 [平台] + [优惠码] 到手价 ¥XXX/晚)
📌 注意事项
[取消政策 / 含不含早餐 / 需不需提前付款]
━━━━━━━━━━━━━━━━━━━━━━━
💡 回复"查机票"继续规划 · 回复"全程规划"一站搞定`);
} else {
const dateNote = checkin ? `Check-in: checkin${checkout (nights nights)` : ''}` : 'Dates: not specified';
console.log(`Please search for hotels at the destination below and find the best OTA deal with coupon stacking.
Destination: destination
dateNote
Guests: guests · Budget: budgetLabelEN
Use the browser tool to navigate each page and extract real hotel prices:
1. Open https://www.booking.com/searchresults.html?ss=encodedcheckin ? `&checkin=${checkin` : ''}checkout ? `&checkout=${checkout` : ''}&group_adults=guests
→ Booking.com: extract top 5 hotels in this budget tier (name, price/night, rating, distance from center)
→ Check for "Genius" member discounts or "Limited-time deal" labels
2. Open https://www.agoda.com/search?q=encodedcheckin ? `&checkIn=${checkin` : ''}checkout ? `&checkOut=${checkout` : ''}&numberOfGuest=guests
→ Agoda: often has exclusive lower rates for Asian destinations
→ Look for "Bank card exclusive discount" offers
3. Open https://www.hotels.com/search.do?q-destination=encodedcheckin ? `&q-check-in=${checkin` : ''}checkout ? `&q-check-out=${checkout` : ''}
→ Hotels.com: earn 1 free night per 10 booked; check for additional promos
4. Open https://uk.trip.com/hotels/list?city=encodedcheckin ? `&startDate=${checkin` : ''}checkout ? `&endDate=${checkout` : ''}
→ Trip.com: member rates often 10-20% cheaper for Asian destinations
5. web_search "destination hotel deals checkin || 'this month' discount"
→ Supplement with any flash sales or coupon sites
[Comparison rules]
- Only record prices actually read from the page (note if tax-inclusive or not)
- Flag any platform requiring login to see lowest price
- Cross-compare the same hotel across platforms to find true best price
[OTA coupon stacking]
openclaw run couponclaw find "Booking.com" --region all
openclaw run couponclaw find "Agoda" --region all
Output format:
🏨 TravelHound · destination Hotels
━━━━━━━━━━━━━━━━━━━━━━━
📋 dateNotenights ? ` (${nights nights)` : ''} · guests guest(s) · budgetLabelEN
💰 Price comparison (budgetLabelEN)
Hotel | Booking | Agoda | Hotels.com | Trip.com | Best
[Hotel A] | $XXX | $XXX | $XXX | $XXX | [platform] $XXX
[Hotel B] | ...
(list 3-5 hotels in this tier)
🏷️ Platform deals
[Platform] Code/promotion: [description] | Extra saving: $XXX
🔢 Best deal
[Hotel name] × nights || 'X' nights = $XXX (via [platform] + [code] → $XXX/night effective)
📌 Watch out for
[Cancellation policy / breakfast included / prepayment required]
━━━━━━━━━━━━━━━━━━━━━━━
💡 Reply "find flights" to continue · Reply "full trip" for combined planning`);
}
FILE:scripts/trip.js
#!/usr/bin/env node
'use strict';
/**
* TravelHound — 全程行程规划(机票+酒店+目的地情报)
* 用法: node scripts/trip.js <目的地> [--from 出发城市] [--date YYYY-MM-DD] [--nights N] [--budget budget|mid|luxury] [--lang zh|en]
*/
const ALLOWED_BUDGETS = new Set(['budget', 'mid', 'luxury']);
const args = process.argv.slice(2);
const langIdx = args.indexOf('--lang');
const lang = langIdx !== -1 && args[langIdx + 1] === 'en' ? 'en' : 'zh';
const fromIdx = args.indexOf('--from');
const from = fromIdx !== -1 ? args[fromIdx + 1] : null;
const dateIdx = args.indexOf('--date');
const date = dateIdx !== -1 ? args[dateIdx + 1] : null;
const nightsIdx = args.indexOf('--nights');
const nights = nightsIdx !== -1 ? parseInt(args[nightsIdx + 1], 10) || null : null;
const budgetIdx = args.indexOf('--budget');
const rawBudget = budgetIdx !== -1 ? args[budgetIdx + 1] : 'mid';
const budget = ALLOWED_BUDGETS.has(rawBudget) ? rawBudget : 'mid';
const destArgs = args.filter((a, i) => {
const flags = ['--lang', '--from', '--date', '--nights', '--budget'];
if (flags.includes(a)) return false;
if (flags.includes(args[i - 1])) return false;
return true;
});
const destination = destArgs.join(' ').trim();
if (!destination) {
console.error(lang === 'zh'
? '用法: node scripts/trip.js <目的地> [--from 出发城市] [--date YYYY-MM-DD] [--nights N] [--budget budget|mid|luxury]'
: 'Usage: node scripts/trip.js <destination> [--from city] [--date YYYY-MM-DD] [--nights N] [--budget budget|mid|luxury]');
process.exit(1);
}
const encoded = encodeURIComponent(destination);
const encodedFrom = from ? encodeURIComponent(from) : null;
const returnDate = (date && nights)
? new Date(new Date(date).getTime() + nights * 86400000).toISOString().slice(0, 10)
: null;
const budgetLabelZH = { budget: '经济型', mid: '中档', luxury: '高档' }[budget];
const budgetLabelEN = { budget: 'Budget', mid: 'Mid-range', luxury: 'Luxury' }[budget];
const now = new Date();
const year = now.getFullYear();
if (lang === 'zh') {
console.log(`请为以下旅行规划完整的行程方案,包括机票、酒店和目的地情报。
目的地:destination
from ? `出发城市:${from` : '出发城市:未指定(请询问或按最近主要机场)'}
date ? `出发日期:${date` : '出发日期:未指定(请查询灵活日期最低价)'}
nights ? `住宿天数:${nights晚returnDate ? `(返程:${returnDate)` : ''}` : '行程天数:未指定'}
预算档位:budgetLabelZH
═══ 第一步:目的地情报 ═══
1. web_search「destination 旅游 签证要求 year」
→ 中国护照/目标护照是否需要签证,办理周期和费用
2. web_search「destination 汇率 人民币 year」
→ 当前汇率,近期走势(是否适合换汇)
3. web_search「destination 旅游安全 year 最新」
→ 目前是否有安全警告或特别注意事项
4. openclaw run newstoday morning "destination" --lang zh
→ 获取目的地相关最新新闻(政治/天气/活动)
═══ 第二步:机票比价 ═══
//www.google.com/travel/flights?q=${encodeURIComponent(from + ' to ' + destination)date ? `+${date` : ''}
→ Google Flights:提取最低价、最便宜日期和"订票建议"` : `5. 打开 https://www.google.com/travel/flights?q=encodeURIComponent('flights to ' + destination)
→ Google Flights:提取各出发城市到destination的最低价`}
6. 打开 https://www.skyscanner.com/transport/flights/encodedFrom || 'anywhere'/encoded/date || ''
→ Skyscanner:提取最低价航班,查看 "Everywhere" 视图找最便宜出发地
7. 打开 https://www.kayak.com/flights/encodedFrom || 'anywhere'-encoded/date || 'flexible'
→ Kayak:查看 Price Forecast(Buy now / Wait)
═══ 第三步:酒店比价 ═══
8. 打开 https://www.booking.com/searchresults.html?ss=encodeddate ? `&checkin=${date` : ''}returnDate ? `&checkout=${returnDate` : ''}
→ Booking.com:提取budgetLabelZH档位前3家酒店(价格/评分/位置)
9. 打开 https://www.agoda.com/search?q=encodeddate ? `&checkIn=${date` : ''}returnDate ? `&checkOut=${returnDate` : ''}
→ Agoda:对比价格,亚洲目的地通常更优惠
═══ 第四步:OTA优惠码 ═══
10. openclaw run couponclaw find "Booking.com" --region all
11. openclaw run couponclaw find "Agoda" --region all
12. openclaw run couponclaw find "Trip.com" --region all
→ 在以上平台价格基础上叠加券码,计算实际到手价
═══ 整合输出 ═══
输出格式:
🌍 TravelHound · ''destination
━━━━━━━━━━━━━━━━━━━━━━━
📋 date || '日期灵活' · '天数待定' · budgetLabelZH
🛂 目的地情报
签证:[需要/免签/落地签 + 费用]
汇率:1 CNY = X.XX [当地货币](近期走势:升/贬/稳)
安全:[安全/注意事项/警告]
近期新闻:[1-2条相关资讯]
✈️ 最优机票
[航班] | ¥/$ XXX | [订票建议:立即订/等待]
最便宜日期:± X天调整可省 ¥/$ XXX
🏨 最优酒店(budgetLabelZH)
[酒店名] | ¥/$ XXX/晚 | 评分 X.X | [平台]
叠加优惠码后:¥/$ XXX/晚
💰 全程预算估算(nights || 'X'晚)
机票:¥/$ XXX(from || '出发城市' 往返)
住宿:¥/$ XXX × nights || 'X'晚 = ¥/$ XXX
合计:¥/$ XXX(含已知优惠)
✅ 综合建议
[🟢/🟡/🔴 + 是否是好时机出行 + 1-2句核心理由]
━━━━━━━━━━━━━━━━━━━━━━━
💡 回复"机票详情"或"酒店详情"深入查询`);
} else {
console.log(`Please plan a complete trip to destination, covering flights, hotels, and destination intelligence.
Destination: destination
${from` : 'From: not specified — use nearest major airport or ask'}
${date` : 'Departure: not specified — check flexible date lowest fares'}
${nights nights${returnDate)` : ''}` : 'Duration: not specified'}
Budget tier: budgetLabelEN
═══ Step 1: Destination intelligence ═══
1. web_search "destination visa requirements year passport"
→ Check if visa required, processing time, and cost
2. web_search "destination exchange rate USD EUR year"
→ Current rate and recent trend (good time to exchange?)
3. web_search "destination travel safety advisory year"
→ Active warnings or special precautions
4. openclaw run newstoday morning "destination" --lang en
→ Latest news relevant to the destination (political/weather/events)
═══ Step 2: Flight comparison ═══
//www.google.com/travel/flights?q=${encodeURIComponent(from + ' to ' + destination)date ? `+${date` : ''}
→ Google Flights: lowest fares, cheapest date calendar, booking recommendation` : `5. Open https://www.google.com/travel/flights?q=encodeURIComponent('flights to ' + destination)
→ Google Flights: fares from multiple origins to destination`}
6. Open https://www.skyscanner.com/transport/flights/encodedFrom || 'anywhere'/encoded/date || ''
→ Skyscanner: lowest fares including connections; check "Everywhere" view if origin flexible
7. Open https://www.kayak.com/flights/encodedFrom || 'anywhere'-encoded/date || 'flexible'
→ Kayak: Price Forecast (Buy now / Wait) and price history chart
═══ Step 3: Hotel comparison ═══
8. Open https://www.booking.com/searchresults.html?ss=encodeddate ? `&checkin=${date` : ''}returnDate ? `&checkout=${returnDate` : ''}
→ Booking.com: top 3 budgetLabelEN hotels (price/rating/location)
9. Open https://www.agoda.com/search?q=encodeddate ? `&checkIn=${date` : ''}returnDate ? `&checkOut=${returnDate` : ''}
→ Agoda: compare prices — often cheaper for Asian destinations
═══ Step 4: OTA promo codes ═══
10. openclaw run couponclaw find "Booking.com" --region all
11. openclaw run couponclaw find "Agoda" --region all
12. openclaw run couponclaw find "Trip.com" --region all
→ Stack coupon codes on top of platform prices; calculate effective final price
═══ Output ═══
Output format:
🌍 TravelHound · ''destination
━━━━━━━━━━━━━━━━━━━━━━━
📋 date || 'Flexible dates' · 'duration TBD' · budgetLabelEN
🛂 Destination intelligence
Visa: [required / visa-free / on-arrival + fee]
Exchange rate: 1 USD = X.XX [local currency] (trend: rising/falling/stable)
Safety: [safe / advisory / warning]
Latest news: [1-2 relevant headlines]
✈️ Best flight
[Flight] | $XXX | [Booking advice: buy now / wait]
Cheapest nearby date: ±X days saves $XXX
🏨 Best hotel (budgetLabelEN)
[Hotel name] | $XXX/night | Rating X.X | [Platform]
After coupon code: $XXX/night effective
💰 Total trip budget estimate (nights || 'X' nights)
Flights: $XXX (from || 'origin' round trip)
Hotels: $XXX × nights || 'X' nights = $XXX
Total: $XXX (after known savings)
✅ Overall verdict
[🟢/🟡/🔴 + is this a good time to go + 1-2 key reasons]
━━━━━━━━━━━━━━━━━━━━━━━
💡 Reply "flight details" or "hotel details" to dig deeper`);
}
Scan social media and communities to detect product trends rising or peaking, helping time purchases or avoid declining items.
# TrendRadar
> Scan social media and communities to detect trending products before they peak — then act on them with BuyWise and CouponClaw.
TrendRadar monitors 小红书, 微博, Reddit, Google Trends, and Product Hunt in real time. It assigns a trend direction (↑↑ surging / ↑ rising / → stable / ↓ cooling) and a commercial signal to each item, so you know whether to buy now, wait, or skip.
## What TrendRadar does differently
Most tools show you what's already popular. TrendRadar shows you what's about to peak — so you can get the best price before demand drives it up, or avoid buying into something already fading.
It is the upstream signal source for the entire ecosystem:
- Feed trending products into **BuyWise** for price and review analysis
- Feed trending stores into **CouponClaw** for coupon and cashback stacking
- Daily briefing surfaces the top 3 most commercially interesting trends each morning
## Trigger phrases
- "什么在爆"
- "最近什么在火"
- "小红书在推什么"
- "Reddit trending"
- "今日爆款"
- "热销商品"
- "trending products"
- "what's hot right now"
- "what's going viral"
- "trending on TikTok"
- "trending on xiaohongshu"
- "hot items"
## Scripts
| Script | Command | Description |
|---|---|---|
| `scan.js` | `node scripts/scan.js [keyword/category] [--region cn\|us\|global\|all] [--lang zh\|en]` | Scan social platforms for trending products related to a keyword, or scan all categories if no keyword given |
| `daily-hot.js` | `node scripts/daily-hot.js [--region cn\|us\|global\|all] [--lang zh\|en]` | Generate full daily trending briefing across all categories (for cron push) |
To schedule a daily push, add a cron job directly:
```
openclaw cron add --schedule "0 0 8 * * *" --cmd "node scripts/daily-hot.js --region all --lang zh" --channel telegram
```
## Trend signals explained
| Direction | Meaning | Commercial action |
|---|---|---|
| ↑↑ Surging | >200% growth in 7 days | Buy before price rises |
| ↑ Rising | 50-200% growth in 7 days | Good timing — more competition = better deals |
| → Stable | High volume, growth slowing | Safe choice, no urgency |
| ↓ Cooling | Declining 3+ days | Wait for price drop |
## Ecosystem integration
```
TrendRadar (signal)
↓
BuyWise (analysis: price / reviews / buy timing)
↓
CouponClaw (action: coupons + cashback stacking)
```
TrendRadar is also called by:
- **NewsToday** — surfaces trending consumer products from the news feed
- **GiftRadar** *(planned)* — uses trending items to inform gift recommendations
## Data sources
| Platform | Region | What it tracks |
|---|---|---|
| 小红书 (Xiaohongshu) | CN | Post volume, engagement velocity |
| 微博热搜 | CN | Search trend ranking |
| 什么值得买 | CN | Save/comment growth rate |
| 抖音热榜 | CN | Viral product videos |
| Reddit | US/Global | Upvotes, post frequency |
| Google Trends | Global | Search volume trajectory |
| Product Hunt | US/Global | New product launches |
## No API required
TrendRadar uses browser navigation to read live platform data directly. No API keys needed.
FILE:README.md
# TrendRadar — Detect Trending Products Before They Peak
> Know what's going viral before everyone else — track 小红书, 微博, Reddit, Google Trends. Buy before prices spike.
[](https://clawhub.ai/skills/trendradar)
[](https://clawhub.ai/skills/trendradar)
[](LICENSE)
## What it does
TrendRadar is an [OpenClaw](https://openclaw.ai) skill that monitors social platforms for rising products and assigns a **trend direction signal**:
| Signal | Meaning | Commercial action |
|---|---|---|
| ↑↑ Surging | >200% growth in 7 days | Buy before price rises with demand |
| ↑ Rising | 50–200% growth in 7 days | Good timing — more deals as competition increases |
| → Stable | High volume, growth slowing | Safe choice, no urgency |
| ↓ Cooling | Declining 3+ days | Wait for price drop, or try alternatives |
It is the **upstream signal** for the entire smart shopping ecosystem — trending items flow into [BuyWise](https://github.com/jiajiaoy/BuyWise) for decision analysis and [CouponClaw](https://github.com/jiajiaoy/CouponClaw) for deal stacking.
## Data sources
| Platform | Region | Signal |
|---|---|---|
| 小红书 (Xiaohongshu) | China | Post volume & engagement velocity |
| 微博热搜 | China | Search trend ranking |
| 什么值得买 | China | Save/comment growth rate |
| 抖音 (Douyin) | China | Viral product videos |
| Reddit | US / Global | Upvotes and post frequency |
| Google Trends | Global | Search volume trajectory |
| Product Hunt | US / Global | New product launches & upvotes |
## Installation
```bash
openclaw install trendradar
```
## Usage
```bash
# Scan a keyword or category for trending items
openclaw run trendradar scan "wireless earbuds" --region all
# Full daily trending briefing across all categories
openclaw run trendradar daily-hot --region all --lang en
# China only, Chinese output
openclaw run trendradar scan --region cn --lang zh
# Schedule daily trending push at 8am
openclaw cron add --schedule "0 0 8 * * *" \
--cmd "node scripts/daily-hot.js --region all --lang zh" \
--channel telegram
```
## Ecosystem
Part of the **OpenClaw Smart Consumer** skill suite:
```
TrendRadar (signal source)
↓ ↑↑ surging item detected
BuyWise (is it worth buying? price + reviews)
↓ confirmed: buy now
CouponClaw (find coupon codes + stack cashback)
```
| Skill | Description |
|---|---|
| **TrendRadar** | Trend detection ← you are here |
| [BuyWise](https://github.com/jiajiaoy/BuyWise) | Buying decision for trending items |
| [CouponClaw](https://github.com/jiajiaoy/CouponClaw) | Coupons + cashback for confirmed buys |
| [NewsToday](https://github.com/jiajiaoy/NewsToday) | News feed including consumer trend signals |
## Keywords
trending products · viral products · what's hot · going viral · TikTok trends · xiaohongshu trending · Reddit hot · Google Trends · Product Hunt · social commerce · product discovery · 爆款 · 热销 · 种草 · 小红书爆款 · 消费趋势 · 热门商品
---
Built for [OpenClaw](https://openclaw.ai) · Published on [clawhub.ai/skills/trendradar](https://clawhub.ai/skills/trendradar)
FILE:package.json
{
"name": "trendradar",
"version": "1.1.2",
"description": "Spot viral products before prices spike — track 小红书, 微博, Reddit, Google Trends & Product Hunt for rising trends. Get surge signals and optimal buy timing.",
"keywords": [
"trending products",
"trending",
"trending now",
"what's trending",
"viral products",
"viral items",
"going viral",
"what's going viral",
"hot items",
"hot products",
"what's hot",
"rising products",
"TikTok trends",
"TikTok shop",
"trending on TikTok",
"xiaohongshu trends",
"trending on xiaohongshu",
"little red book",
"Reddit trending",
"Reddit hot",
"Google Trends",
"Product Hunt",
"new product launch",
"social media trends",
"social commerce",
"social listening",
"product discovery",
"product trends",
"consumer trends",
"buying trends",
"trend radar",
"trend tracking",
"trend analysis",
"爆款",
"今日爆款",
"爆款预警",
"爆款发现",
"热销",
"热门商品",
"热门产品",
"新品推荐",
"小红书爆款",
"小红书热门",
"小红书种草",
"抖音热门",
"微博热搜",
"什么在火",
"趋势",
"热度",
"种草",
"种草必备",
"消费趋势",
"产品发现",
"社交电商",
"daily hot",
"daily trending",
"top products today"
],
"main": "scripts/scan.js",
"scripts": {
"scan": "node scripts/scan.js",
"daily-hot": "node scripts/daily-hot.js"
},
"openclaw": {
"triggers": [
"trending",
"what's trending",
"what's hot",
"viral",
"trending products",
"hot items",
"going viral",
"爆款",
"热销",
"热门",
"什么在火",
"今日爆款",
"种草",
"最近流行"
],
"entrypoint": "scripts/scan.js"
},
"dependencies": {}
}
FILE:scripts/daily-hot.js
#!/usr/bin/env node
'use strict';
/**
* TrendRadar — 每日爆款简报
* 由 openclaw cron 驱动,每日推送全品类热度榜
* 用法: node scripts/daily-hot.js [--region cn|us|global|all] [--lang zh|en]
*/
const ALLOWED_REGIONS = new Set(['cn', 'us', 'global', 'all']);
const args = process.argv.slice(2);
const langIdx = args.indexOf('--lang');
const lang = langIdx !== -1 && args[langIdx + 1] === 'en' ? 'en' : 'zh';
const regionIdx = args.indexOf('--region');
const rawRegion = regionIdx !== -1 ? args[regionIdx + 1] : 'all';
const region = ALLOWED_REGIONS.has(rawRegion) ? rawRegion : 'all';
const now = new Date();
const WEEKDAYS_ZH = ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const MONTHS_EN = ['January','February','March','April','May','June','July','August','September','October','November','December'];
const dateZH = `now.getFullYear()年now.getMonth()+1月now.getDate()日 WEEKDAYS_ZH[now.getDay()]`;
const dateEN = `MONTHS_EN[now.getMonth()] now.getDate(), now.getFullYear()`;
// 分品类数据源
const categorySources = {
cn: [
{ cat: '数码/家电', sources: [
{ url: 'https://www.smzdm.com/fenlei/shuma/', note: '提取今日收藏/评论增长最快的前3条' },
{ url: 'https://www.xiaohongshu.com/explore?type=normal&tag=%E6%95%B0%E7%A0%81', note: '数码标签热门笔记' },
]},
{ cat: '美妆/护肤', sources: [
{ url: 'https://www.smzdm.com/fenlei/meizhuang/', note: '提取今日热门美妆商品' },
{ url: 'https://www.xiaohongshu.com/explore?type=normal&tag=%E7%BE%8E%E5%A6%86', note: '美妆标签热门笔记前3条' },
]},
{ cat: '家居/生活', sources: [
{ url: 'https://www.smzdm.com/fenlei/jiajushenghuoyongpin/', note: '今日热门家居商品' },
]},
{ cat: '服饰/穿搭', sources: [
{ url: 'https://www.xiaohongshu.com/explore?type=normal&tag=%E7%A9%BF%E6%90%AD', note: '穿搭标签热门推荐' },
]},
],
us: [
{ cat: 'Electronics', sources: [
{ url: 'https://www.reddit.com/r/gadgets/hot/', note: 'Top 3 posts this week' },
{ url: 'https://www.producthunt.com/', note: "Today's top new tech products" },
]},
{ cat: 'Beauty & Health', sources: [
{ url: 'https://www.reddit.com/r/SkincareAddiction/hot/', note: 'Top 3 recommended products' },
]},
{ cat: 'Home & Living', sources: [
{ url: 'https://www.reddit.com/r/malelivingspace/hot/', note: 'Popular home products' },
]},
{ cat: 'Fashion', sources: [
{ url: 'https://www.reddit.com/r/femalefashionadvice/hot/', note: 'Trending fashion picks' },
]},
],
};
const activeCategories = region === 'cn' ? categorySources.cn
: region === 'us' ? categorySources.us
: [...categorySources.cn, ...categorySources.us];
if (lang === 'zh') {
const catList = activeCategories.map((c, ci) => {
const srcList = c.sources.map((s, si) =>
` ci + 1.si + 1 打开 s.url\n → s.note`
).join('\n');
return `【c.cat】\nsrcList`;
}).join('\n\n');
console.log(`请扫描今日各品类热门商品,生成每日爆款简报。
当前日期:dateZH
使用 browser 工具导航以下页面,每个品类提取 1-3 个热度最高的商品:
catList
补充信号:
- web_search "今日 now.getFullYear()年now.getMonth()+1月now.getDate()日 爆款 好物 推荐" → 获取今日全网热词
- web_search "小红书 今日爆款 now.getMonth()+1月" → 补充小红书月度爆款
整理规则:
- 每品类最多3条,全榜共 8-10 条
- 每条注明:商品名、热度来源平台、趋势方向(↑↑/↑/→/↓)、商业信号
- 优先选择"正在爆发"的商品(↑↑),而非已经见顶的
- 跨品类对比,最后选出 TOP 3 最值得关注
输出格式:
📡 TrendRadar · 今日爆款 · dateZH
━━━━━━━━━━━━━━━━━━━━━━━
📱 数码/家电
① [商品名] ↑↑ | [热度说明] | 🔥爆发期
② ...
💄 美妆/护肤
① [商品名] ↑ | [热度说明] | 📈上升期
② ...
🏠 家居/生活
...
👗 服饰/穿搭
...
━━━━━━━━━━━━━━━━━━━━━━━
🏆 今日 TOP 3 最值关注
① [商品] — [1句理由]
→ 立即分析:openclaw run buywise advise "[商品名]"
→ 找券:openclaw run couponclaw find "[商品名]"
② ...
③ ...
💡 回复商品名查详细趋势 · 回复"找券"叠加优惠`);
} else {
const catList = activeCategories.map((c, ci) => {
const srcList = c.sources.map((s, si) =>
` ci + 1.si + 1 Open s.url\n → s.note`
).join('\n');
return `[c.cat]\nsrcList`;
}).join('\n\n');
console.log(`Please scan today's trending products across categories and generate a daily hot items briefing.
Date: dateEN
Use the browser tool to navigate each page and extract the top 1-3 trending items per category:
catList
Supplement with:
- web_search "trending products today now.getFullYear() Reddit TikTok" → global trend signals
- web_search "viral product TikTok MONTHS_EN[now.getMonth()] now.getFullYear()" → TikTok viral items this month
Rules:
- Max 3 items per category, 8-10 total
- Each entry: product name, source platform, trend direction (↑↑/↑/→/↓), commercial signal
- Prioritize surging items (↑↑) over items already at peak
- Compare across categories; select TOP 3 overall
Output format:
📡 TrendRadar · Daily Hot · dateEN
━━━━━━━━━━━━━━━━━━━━━━━
📱 Electronics
① [product] ↑↑ | [signal data] | 🔥 Surging
② ...
💄 Beauty & Health
① [product] ↑ | [signal data] | 📈 Rising
② ...
🏠 Home & Living
...
👗 Fashion
...
━━━━━━━━━━━━━━━━━━━━━━━
🏆 Today's TOP 3 Picks
① [product] — [one-sentence reason]
→ Deep analysis: openclaw run buywise advise "[product]"
→ Find coupons: openclaw run couponclaw find "[product]"
② ...
③ ...
💡 Reply with a product name for trend details · Reply "find coupons" to stack deals`);
}
FILE:scripts/scan.js
#!/usr/bin/env node
'use strict';
/**
* TrendRadar — 热搜雷达主入口
* 扫描社交媒体和社区,识别正在爆火的商品/品牌/话题,输出趋势方向和商业信号
* 用法: node scripts/scan.js [关键词/品类] [--region cn|us|global|all] [--lang zh|en]
*/
const ALLOWED_REGIONS = new Set(['cn', 'us', 'global', 'all']);
const args = process.argv.slice(2);
const langIdx = args.indexOf('--lang');
const lang = langIdx !== -1 && args[langIdx + 1] === 'en' ? 'en' : 'zh';
const regionIdx = args.indexOf('--region');
const rawRegion = regionIdx !== -1 ? args[regionIdx + 1] : 'all';
const region = ALLOWED_REGIONS.has(rawRegion) ? rawRegion : 'all';
const queryArgs = args.filter((a, i) => {
if (['--lang', '--region'].includes(a)) return false;
if (['--lang', '--region'].includes(args[i - 1])) return false;
return true;
});
const query = queryArgs.join(' ').trim();
const now = new Date();
const dateISO = `now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const WEEKDAYS_ZH = ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const MONTHS_EN = ['January','February','March','April','May','June','July','August','September','October','November','December'];
const dateZH = `now.getFullYear()年now.getMonth()+1月now.getDate()日 WEEKDAYS_ZH[now.getDay()]`;
const dateEN = `MONTHS_EN[now.getMonth()] now.getDate(), now.getFullYear()`;
const encoded = query ? encodeURIComponent(query) : '';
const sources = {
cn: query ? [
{ label: '小红书', url: `https://www.xiaohongshu.com/search_result?keyword=encoded&type=51`, note: '搜索笔记数量、最近发布时间、热门标签,判断是否处于爆发期' },
{ label: '微博热搜', url: `https://s.weibo.com/weibo?q=encoded&Refer=top`, note: '查是否进入微博热搜,记录热搜排名和阅读量' },
{ label: '什么值得买', url: `https://search.smzdm.com/?c=home&s=encoded&v=b`, note: '查近7天收藏数/评论数增长趋势,判断购买意愿热度' },
] : [
{ label: '小红书今日热门', url: `https://www.xiaohongshu.com/explore`, note: '提取首页"发现"板块热门商品笔记,取互动量最高的前5条' },
{ label: '微博热搜', url: `https://s.weibo.com/top/summary`, note: '提取消费/商品相关热搜词,过滤娱乐话题' },
{ label: '什么值得买热门', url: `https://www.smzdm.com/`, note: '提取今日收藏量增长最快的前5个商品' },
{ label: '抖音热销', url: `https://www.douyin.com/hot`, note: '如可访问,提取热销商品榜单前5条' },
],
us: query ? [
{ label: 'Reddit', url: `https://www.reddit.com/search/?q=encoded&sort=hot&t=week`, note: '提取过去7天热帖数量、upvotes、评论数,判断讨论热度' },
{ label: 'Google Trends', url: `https://trends.google.com/trends/explore?q=encoded&date=now%207-d`, note: '查近7天搜索量趋势,是否"突破性上升"' },
{ label: 'Product Hunt', url: `https://www.producthunt.com/search?q=encoded`, note: '科技/新品类产品查热度排名和点赞数' },
] : [
{ label: 'Reddit /r/deals', url: `https://www.reddit.com/r/deals/hot/`, note: '提取热帖中的热销商品,记录 upvotes 和评论数' },
{ label: 'Reddit /r/BuyItForLife', url: `https://www.reddit.com/r/BuyItForLife/hot/`, note: '口碑型热销品,提取近7天热帖' },
{ label: 'Google Trends', url: `https://trends.google.com/trending?geo=US`, note: '提取今日美国热门搜索中的商品类词条' },
{ label: 'Product Hunt', url: `https://www.producthunt.com/`, note: '提取今日/本周热门新品' },
],
};
const activeSources = [];
if (region === 'cn') activeSources.push(...sources.cn);
else if (region === 'us') activeSources.push(...sources.us);
else activeSources.push(...sources.cn, ...sources.us);
if (lang === 'zh') {
const sourceList = activeSources.map((s, i) =>
`i + 1. 打开 s.url\n → s.label:s.note`
).join('\n\n');
const subject = query ? `「query」` : '全品类';
console.log(`请扫描subject的社交媒体热度,识别正在爆火的商品和趋势信号。
当前日期:dateZH
使用 browser 工具依次导航以下页面,提取真实热度数据:
sourceList
`
另外补充执行:
- web_search "今日 ${dateISO 小红书爆款 好物推荐" → 获取今日热词
- web_search "trending products Reddit dateISO" → 获取英文社区今日趋势
`}
【趋势判断规则】
根据以下信号判断每个商品的趋势方向:
- ↑↑ 急速上升:近7天讨论量增长 >200%,或刚进热搜前50
- ↑ 上升中:近7天讨论量增长 50-200%,热度持续
- → 高位平稳:热度高但增速放缓,已进入成熟期
- ↓ 开始降温:讨论量连续3天下降
【商业信号判断】
- 🔥 爆发期(↑↑):先买先得,价格可能随热度上涨
- 📈 上升期(↑):好时机,热度带来更多优惠竞争
- 📊 成熟期(→):稳定选择,但不会有价格红利
- 🧊 降温期(↓):可等待价格下跌,或改选替代品
输出格式:
📡 TrendRadar · subject · dateZH
━━━━━━━━━━━━━━━━━━━━━━━
🔥 热度趋势排行
① [商品/品牌名] [↑↑/↑/→/↓]
热度来源:[平台] [数据,如:12.3k笔记 / 热搜第X位 / 5.2k upvotes]
商业信号:[🔥爆发期 / 📈上升期 / 📊成熟期 / 🧊降温期]
→ 深度分析:openclaw run buywise advise "[商品名]"
→ 找券省钱:openclaw run couponclaw find "[商品名]"
② [重复格式]
③ ...(共列出5-8条)
📊 趋势概览
↑↑ 急速上升:X个 | ↑ 上升中:X个 | → 平稳:X个 | ↓ 降温:X个
💡 最值得关注:[最有商业价值的1个趋势 + 1句理由]
━━━━━━━━━━━━━━━━━━━━━━━
💬 回复商品名查热度详情 · 回复"今日爆款"查全品类热榜`);
} else {
const sourceList = activeSources.map((s, i) =>
`i + 1. Open s.url\n → s.label: s.note`
).join('\n\n');
const subject = query ? `"query"` : 'all categories';
console.log(`Please scan social media and community platforms for trending products/brandsquery ? ` related to ${subject` : ''}.
Date: dateEN
Use the browser tool to navigate each page and extract real trend data:
sourceList
`
Also run:
- web_search "trending products today ${dateISO Reddit TikTok" → supplement with keyword signals
- web_search "viral product TikTok dateISO" → TikTok viral items
`}
[Trend direction rules]
Assign a trend direction to each item based on the data:
- ↑↑ Surging: discussion volume up >200% in 7 days, or just entered top trending
- ↑ Rising: discussion volume up 50-200% in 7 days, sustained momentum
- → Peak / stable: high volume but growth slowing, entering maturity
- ↓ Cooling: discussion volume declining 3+ consecutive days
[Commercial signal]
- 🔥 Surging (↑↑): buy before price rises with demand
- 📈 Rising (↑): good timing — more deals as competition increases
- 📊 Mature (→): stable choice, no price advantage
- 🧊 Cooling (↓): wait for price drop, or switch to alternatives
Output format:
📡 TrendRadar · subject · dateEN
━━━━━━━━━━━━━━━━━━━━━━━
🔥 Trending Now
① [Product/Brand] [↑↑/↑/→/↓]
Signal source: [Platform] [data, e.g.: 12.3k posts / trending #X / 5.2k upvotes]
Commercial signal: [🔥 Surging / 📈 Rising / 📊 Mature / 🧊 Cooling]
→ Deep analysis: openclaw run buywise advise "[product name]"
→ Find coupons: openclaw run couponclaw find "[product name]"
② [repeat format]
③ ... (list 5-8 items total)
📊 Trend overview
↑↑ Surging: X | ↑ Rising: X | → Stable: X | ↓ Cooling: X
💡 Top pick: [the 1 item with most commercial value + one-sentence reason]
━━━━━━━━━━━━━━━━━━━━━━━
💬 Reply with a product name for detailed trend data · Reply "hot today" for full trending list`);
}
Find verified coupons, stack cashback, and get final prices with savings across China, US, UK, Australia, Southeast Asia, and global DTC brands.
# CouponClaw
> Find coupons, stack cashback, and maximize savings — across China, US, UK, Australia, Southeast Asia, and DTC brands worldwide.
CouponClaw is the only coupon skill that covers every major market in one place. It searches verified coupon databases, finds cashback portal rates, and calculates the best stacking strategy so you always know the exact final price before checkout.
## What makes CouponClaw different
Most coupon tools check one platform in one country. CouponClaw runs a 3-layer strategy:
1. **Layer 1 — Coupons**: Real-time browser search across region-specific coupon sites (smzdm, RetailMeNot, VoucherCodes, OzBargain, ShopBack, and more)
2. **Layer 2 — Cashback stacking**: Compares 返利网, Rakuten, TopCashback, and ShopBack rates — and checks if they stack with the coupon
3. **Layer 3 — DTC brand check**: Detects hidden first-order discounts and newsletter signup offers on brand official sites
It also runs a daily deals briefing (via cron) that surfaces the hottest community-verified deals from each region every morning.
## Trigger phrases
Use CouponClaw when you say things like:
- "is there a coupon for..."
- "promo code for..."
- "discount code"
- "voucher code"
- "coupon code"
- "cashback for..."
- "有没有优惠券"
- "有没有券"
- "优惠码"
- "折扣码"
- "返利"
- "省钱"
- "领券"
- "打折"
- "今日优惠"
- "daily deals"
- "best deals today"
## Scripts
| Script | Command | Description |
|---|---|---|
| `find.js` | `node scripts/find.js <product or store> [--region cn\|us\|uk\|au\|sea\|all] [--lang zh\|en]` | Find all available coupons + cashback stacking for a product or store |
| `cashback.js` | `node scripts/cashback.js <store> [--spend amount] [--lang zh\|en]` | Look up and compare cashback rates across all platforms |
| `daily-deals.js` | `node scripts/daily-deals.js [--region cn\|us\|uk\|au\|sea\|all] [--lang zh\|en]` | Generate today's top deals briefing (for cron push) |
| `push-toggle.js` | `node scripts/push-toggle.js on\|off\|status <userId> [--morning HH:MM] [--region ...] [--channel telegram\|slack\|feishu\|discord] [--lang zh\|en]` | Manage daily deal push subscription |
## Region coverage
| Region | Coupon sources | Cashback |
|---|---|---|
| 🇨🇳 China | 什么值得买, 京东领券中心, 淘宝聚划算, 折800 | 返利网, 什么值得买返利 |
| 🌏 Chinese overseas | Dealmoon (North America) | Rakuten |
| 🇺🇸 US | RetailMeNot, Slickdeals, Amazon Coupons | Rakuten, TopCashback |
| 🇬🇧 UK | VoucherCodes, HotUKDeals, MyVoucherCodes | TopCashback |
| 🇦🇺 Australia | OzBargain, Cashrewards | ShopBack |
| 🌏 Southeast Asia | ShopBack, iPrice | ShopBack |
| 🏷️ DTC brands | Official site popup detection, newsletter signup offers | Rakuten / TopCashback |
## Recommended companion
Install **gstack** for full browser navigation support — CouponClaw uses the browser tool to read live coupon pages and get real (not cached) data.
## No API required
CouponClaw uses browser navigation to read real-time data directly from coupon and cashback sites. No API keys, no subscriptions.
FILE:README.md
# CouponClaw — Find Coupons & Stack Cashback Worldwide
> Never pay full price — find every coupon code and stack it with cashback. China · US · UK · Australia · Southeast Asia · DTC brands.
[](https://clawhub.ai/skills/couponclaw)
[](https://clawhub.ai/skills/couponclaw)
[](LICENSE)
## What it does
CouponClaw is an [OpenClaw](https://openclaw.ai) skill that runs a **3-layer savings strategy** on any product or store:
1. **Layer 1 — Coupon codes**: real-time browser search across region-specific coupon sites
2. **Layer 2 — Cashback stacking**: compares rates across Rakuten, TopCashback, ShopBack, 返利网 — and checks if they stack with the coupon
3. **Layer 3 — DTC brand check**: detects first-order discounts and newsletter signup offers on brand official sites
Also generates a daily deals briefing surfacing the hottest community-verified deals from smzdm, Slickdeals, HotUKDeals, OzBargain, and ShopBack each morning.
## Coverage
| Region | Coupon sources | Cashback |
|---|---|---|
| 🇨🇳 China | 什么值得买, 京东领券, 淘宝聚划算, 折800 | 返利网, 什么值得买返利 |
| 🌏 Chinese overseas | Dealmoon | Rakuten |
| 🇺🇸 US | RetailMeNot, Slickdeals, Amazon Coupons | Rakuten, TopCashback |
| 🇬🇧 UK | VoucherCodes, HotUKDeals, MyVoucherCodes | TopCashback |
| 🇦🇺 Australia | OzBargain, Cashrewards | ShopBack |
| 🌏 Southeast Asia | ShopBack, iPrice | ShopBack |
| 🏷️ DTC brands | Official site popup detection, newsletter offers | Rakuten / TopCashback |
## Installation
```bash
openclaw install couponclaw
```
## Usage
```bash
# Find all coupons for a product or store
openclaw run couponclaw find "Nike" --region us
# Compare cashback rates across platforms
openclaw run couponclaw cashback "Booking.com" --spend 300
# Today's top deals briefing
openclaw run couponclaw daily-deals --region all --lang en
# All regions, both languages
openclaw run couponclaw find "AirPods Pro" --region all --lang en
```
## Ecosystem
Part of the **OpenClaw Smart Consumer** skill suite:
| Skill | How it connects |
|---|---|
| [BuyWise](https://github.com/jiajiaoy/BuyWise) | Calls CouponClaw after confirming a product is worth buying |
| [TravelHound](https://github.com/jiajiaoy/TravelHound) | Calls CouponClaw for Booking.com, Agoda, Trip.com promo codes |
| [TrendRadar](https://github.com/jiajiaoy/TrendRadar) | Calls CouponClaw for surging products before prices spike |
| **CouponClaw** | Coupon + cashback layer ← you are here |
## Keywords
coupon code · promo code · discount code · cashback · cashback stacking · RetailMeNot · Slickdeals · Rakuten · TopCashback · ShopBack · OzBargain · HotUKDeals · VoucherCodes · Amazon coupon · DTC discount · never pay full price · 优惠券 · 优惠码 · 返利 · 省钱 · 领券 · Black Friday · Cyber Monday
---
Built for [OpenClaw](https://openclaw.ai) · Published on [clawhub.ai/skills/couponclaw](https://clawhub.ai/skills/couponclaw)
FILE:package.json
{
"name": "couponclaw",
"version": "1.1.2",
"description": "Never pay full price — stack coupon codes with cashback across Amazon, JD, Taobao, Slickdeals, RetailMeNot and 50+ global stores. 3-layer savings strategy.",
"keywords": [
"coupon",
"coupon code",
"coupon finder",
"coupon hunter",
"promo code",
"promo code finder",
"discount code",
"voucher",
"voucher code",
"cashback",
"cashback stacking",
"cashback maximizer",
"deals",
"daily deals",
"deal stacking",
"deal finder",
"savings",
"save money",
"never pay full price",
"maximum savings",
"Black Friday coupons",
"Cyber Monday deals",
"holiday deals",
"Amazon coupon",
"Amazon promo code",
"Amazon deals",
"student discount",
"first order discount",
"newsletter signup discount",
"RetailMeNot",
"Slickdeals",
"Rakuten",
"TopCashback",
"ShopBack",
"OzBargain",
"HotUKDeals",
"VoucherCodes",
"Dealmoon",
"DTC brand",
"DTC discount",
"brand coupon",
"优惠券",
"优惠码",
"折扣码",
"券码",
"隐藏优惠",
"返利",
"返利叠加",
"最高返利",
"省钱",
"领券",
"什么值得买",
"京东优惠券",
"淘宝券",
"天猫券",
"每日优惠",
"今日特价",
"白菜价",
"不要全价",
"最优到手价",
"黑五优惠",
"海淘优惠",
"叠加优惠",
"隐藏券",
"global coupons",
"international coupons",
"worldwide deals"
],
"main": "scripts/find.js",
"scripts": {
"find": "node scripts/find.js",
"cashback": "node scripts/cashback.js",
"daily-deals": "node scripts/daily-deals.js"
},
"openclaw": {
"triggers": [
"coupon",
"coupon code",
"promo code",
"discount code",
"voucher",
"cashback",
"deals today",
"daily deals",
"save money",
"有没有券",
"优惠券",
"优惠码",
"折扣码",
"返利",
"省钱",
"今日优惠",
"有券吗",
"打折吗"
],
"entrypoint": "scripts/find.js"
},
"dependencies": {}
}
FILE:scripts/cashback.js
#!/usr/bin/env node
'use strict';
/**
* CouponClaw — 返利叠加查询
* 查询某商家在主流返利平台的返利比例,计算叠加后实际到手价
* 用法: node scripts/cashback.js <商家名> [--spend 消费金额] [--lang zh|en]
*/
const args = process.argv.slice(2);
const langIdx = args.indexOf('--lang');
const lang = langIdx !== -1 && args[langIdx + 1] === 'en' ? 'en' : 'zh';
const spendIdx = args.indexOf('--spend');
const spend = spendIdx !== -1 ? parseFloat(args[spendIdx + 1]) : null;
const storeArgs = args.filter((a, i) => {
if (['--lang', '--spend'].includes(a)) return false;
if (['--lang', '--spend'].includes(args[i - 1])) return false;
return true;
});
const store = storeArgs.join(' ').trim();
if (!store) {
console.error(lang === 'zh'
? '用法: node scripts/cashback.js <商家名> [--spend 金额] [--lang zh|en]'
: 'Usage: node scripts/cashback.js <store> [--spend amount] [--lang zh|en]');
process.exit(1);
}
const encoded = encodeURIComponent(store);
const spendNote = spend
? (lang === 'zh' ? `\n消费金额:¥/$ spend,请据此计算各平台实际返利金额。` : `\nSpend amount: $spend — calculate actual cashback amount for each platform.`)
: '';
if (lang === 'zh') {
console.log(`请查询「store」在各大返利平台的返利比例,并计算最优叠加方案。spendNote
使用 browser 工具导航以下页面:
【中国市场返利】
1. 打开 https://www.fanli.com/search?q=encoded
→ 返利网:提取该商家的返利比例(%)和单笔封顶金额
2. 打开 https://search.smzdm.com/?c=home&s=encoded&v=b
→ 什么值得买返利:提取返利率和活动说明
【国际市场返利】
3. 打开 https://www.rakuten.com/search?q=encoded
→ Rakuten(美/英/日):提取返利比例,注意是否有提现门槛
4. 打开 https://www.topcashback.co.uk/search/?searchInput=encoded
→ TopCashback(英/美):提取该商家最高返利比例
5. 打开 https://www.shopback.com/search?q=encoded
→ ShopBack(新加坡/马来/泰/印尼/澳洲):提取各国返利比例
【叠加提示】
- 大多数返利平台可与优惠券码叠加(先用券,再通过返利平台入口下单)
- 信用卡积分/返现可在上述两层之上再叠加(提醒用户选择合适的信用卡)
- 注意:部分平台规定"使用优惠码不享受返利",需单独标注
输出格式:
💰 CouponClaw · store 返利查询
━━━━━━━━━━━━━━━━━━━━━━━
平台 | 返利比例 | 可叠加券码 | 备注
返利网 | X% | ✅/❌ |
什么值得买 | X% | ✅/❌ |
Rakuten | X% | ✅/❌ | 美/英/日
TopCashback | X% | ✅/❌ |
ShopBack | X% | ✅/❌ | 东南亚/澳洲
🔢 最优叠加方案spend ? `(消费 ¥/$ ${spend)` : ''}
优惠券节省:¥/$ XXX
+ 返利收入:¥/$ XXX(通过 [最高返利平台] 下单)
+ 信用卡返现:¥/$ XXX(建议使用 [适合该平台的卡])
= 综合节省:¥/$ XXX(折合 X 折 / X% off)
━━━━━━━━━━━━━━━━━━━━━━━`);
} else {
console.log(`Please look up cashback rates for "store" across major cashback platforms and calculate the best stacking strategy.spendNote
Use the browser tool to navigate each page:
[China cashback]
1. Open https://www.fanli.com/search?q=encoded
→ Fanli.com: extract cashback rate (%) and per-order cap
2. Open https://search.smzdm.com/?c=home&s=encoded&v=b
→ smzdm cashback: extract rate and promotion notes
[International cashback]
3. Open https://www.rakuten.com/search?q=encoded
→ Rakuten (US/UK/JP): extract cashback rate; note minimum payout threshold
4. Open https://www.topcashback.co.uk/search/?searchInput=encoded
→ TopCashback (UK/US): extract highest available cashback rate
5. Open https://www.shopback.com/search?q=encoded
→ ShopBack (SG/MY/TH/ID/AU): extract rates by country
[Stacking notes]
- Most cashback platforms stack with coupon codes (use code first, then click through cashback portal)
- Credit card rewards can stack on top of both layers — suggest a suitable card
- Flag any platforms where "using a promo code voids cashback"
Output format:
💰 CouponClaw · store Cashback Lookup
━━━━━━━━━━━━━━━━━━━━━━━
Platform | Rate | Stackable | Notes
Fanli (CN) | X% | ✅/❌ |
smzdm (CN) | X% | ✅/❌ |
Rakuten | X% | ✅/❌ | US/UK/JP
TopCashback | X% | ✅/❌ |
ShopBack | X% | ✅/❌ | SEA/AU
🔢 Best stacking strategy$${spend)` : ''}
Coupon savings: $XXX
+ Cashback: $XXX (via [highest-rate platform])
+ Card rewards: $XXX (recommended: [suitable card])
= Total saving: $XXX (effective X% off)
━━━━━━━━━━━━━━━━━━━━━━━`);
}
FILE:scripts/daily-deals.js
#!/usr/bin/env node
'use strict';
/**
* CouponClaw — 每日热门优惠推送
* 由 openclaw cron 驱动,每日早晨执行
* 用法: node scripts/daily-deals.js [--region cn|us|uk|au|sea|all] [--lang zh|en]
*/
const ALLOWED_REGIONS = new Set(['cn', 'us', 'uk', 'au', 'sea', 'all']);
const args = process.argv.slice(2);
const langIdx = args.indexOf('--lang');
const lang = langIdx !== -1 && args[langIdx + 1] === 'en' ? 'en' : 'zh';
const regionIdx = args.indexOf('--region');
const rawRegion = regionIdx !== -1 ? args[regionIdx + 1] : 'all';
const region = ALLOWED_REGIONS.has(rawRegion) ? rawRegion : 'all';
const now = new Date();
const WEEKDAYS_ZH = ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const WEEKDAYS_EN = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
const MONTHS_EN = ['January','February','March','April','May','June','July','August','September','October','November','December'];
const dateISO = `now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const dateZH = `now.getFullYear()年now.getMonth()+1月now.getDate()日 WEEKDAYS_ZH[now.getDay()]`;
const dateEN = `WEEKDAYS_EN[now.getDay()], MONTHS_EN[now.getMonth()] now.getDate(), now.getFullYear()`;
// 按区域配置今日热门优惠来源
const dealSources = {
cn: [
{ label: '什么值得买今日热门', url: 'https://www.smzdm.com/', note: '首页"今日最热"板块,提取评论数最多的前5个优惠' },
{ label: '什么值得买优惠券', url: 'https://www.smzdm.com/fenlei/youhuiquan/', note: '今日新上优惠券,取前5条,标注来源平台和有效期' },
{ label: '京东秒杀', url: 'https://miaosha.jd.com/', note: '提取今日限时秒杀中折扣力度最大的3-5个商品' },
],
us: [
{ label: 'Slickdeals Frontpage', url: 'https://slickdeals.net/', note: 'frontpage 热门优惠,提取当前投票数最高的前5个' },
{ label: 'Dealmoon', url: 'https://www.dealmoon.com/', note: '华人社区精选,提取编辑推荐的前3-5个优惠' },
{ label: 'DealNews', url: 'https://dealnews.com/', note: '编辑精选今日最值优惠,提取前3条' },
],
uk: [
{ label: 'HotUKDeals', url: 'https://www.hotukdeals.com/', note: '英国社区热帖,提取温度值最高的前5个优惠' },
],
au: [
{ label: 'OzBargain', url: 'https://www.ozbargain.com.au/', note: '澳洲社区热帖,提取投票数最高的前5个优惠' },
],
sea: [
{ label: 'ShopBack 今日优惠', url: 'https://www.shopback.com/all-deals', note: '东南亚今日精选优惠,提取前5条' },
],
};
const activeSources = region === 'all'
? Object.values(dealSources).flat()
: (dealSources[region] || []);
if (lang === 'zh') {
const sourceList = activeSources.map((s, i) =>
`i + 1. 打开 s.url\n → s.label:s.note`
).join('\n\n');
console.log(`请抓取今日全球热门优惠,生成每日优惠简报。
当前日期:dateZH
使用 browser 工具导航以下页面,提取真实有效的今日热门优惠:
sourceList
整理规则:
- 共选取 8-10 条今日最值优惠,覆盖不同品类
- 每条含:商品/品牌、折扣幅度或券码、原价→现价、来源平台、有效期(若有)
- 优先标注:限时抢购(今日截止)、隐藏券、叠加返利机会
- 已过期或库存售罄的优惠不列入
输出格式:
🎟️ CouponClaw · 今日优惠 · dateZH
━━━━━━━━━━━━━━━━━━━━━━━
🇨🇳 国内热门
[条目:商品 | 折扣 | 券码(若有)| 平台 | ⏰截止时间]
🌍 海外热门
[条目:商品 | 折扣 | 券码(若有)| 平台 | ⏰截止时间]
💎 今日最值 TOP 3
① [最值优惠,附简短理由]
② ...
③ ...
━━━━━━━━━━━━━━━━━━━━━━━
💡 回复商品名查专属券 · 回复"比价"跳转 BuyWise`);
} else {
const sourceList = activeSources.map((s, i) =>
`i + 1. Open s.url\n → s.label: s.note`
).join('\n\n');
console.log(`Please fetch today's top deals and generate a daily deals briefing.
Date: dateEN
Use the browser tool to navigate each page below and extract real, currently-valid hot deals:
sourceList
Rules:
- Select 8-10 best deals of the day across different categories
- Each entry: product/brand, discount amount or promo code, original → current price, platform, expiry (if applicable)
- Highlight: today-only flash sales, hidden coupons, stackable cashback opportunities
- Do not include expired or sold-out deals
Output format:
🎟️ CouponClaw · Daily Deals · dateEN
━━━━━━━━━━━━━━━━━━━━━━━
🇨🇳 China Deals
[item: product | discount | code (if any) | platform | ⏰ expires]
🌍 International Deals
[item: product | discount | code (if any) | platform | ⏰ expires]
💎 Today's Top 3 Picks
① [best deal with brief reason]
② ...
③ ...
━━━━━━━━━━━━━━━━━━━━━━━
💡 Reply with a product name to find its coupons · Reply "compare" to jump to BuyWise`);
}
FILE:scripts/find.js
#!/usr/bin/env node
'use strict';
/**
* CouponClaw — 主入口:查询商品或店铺的优惠券 + 返利叠加策略
* 用法: node scripts/find.js <商品名或店铺名> [--region cn|us|uk|au|sea|all] [--lang zh|en]
*/
const ALLOWED_REGIONS = new Set(['cn', 'us', 'uk', 'au', 'sea', 'all']);
const args = process.argv.slice(2);
const langIdx = args.indexOf('--lang');
const lang = langIdx !== -1 && args[langIdx + 1] === 'en' ? 'en' : 'zh';
const regionIdx = args.indexOf('--region');
const rawRegion = regionIdx !== -1 ? args[regionIdx + 1] : 'all';
const region = ALLOWED_REGIONS.has(rawRegion) ? rawRegion : 'all';
const queryArgs = args.filter((a, i) => {
if (['--lang', '--region'].includes(a)) return false;
if (['--lang', '--region'].includes(args[i - 1])) return false;
return true;
});
const query = queryArgs.join(' ').trim();
if (!query) {
console.error(lang === 'zh'
? '用法: node scripts/find.js <商品或店铺> [--region cn|us|uk|au|sea|all] [--lang zh|en]'
: 'Usage: node scripts/find.js <product or store> [--region cn|us|uk|au|sea|all] [--lang zh|en]');
process.exit(1);
}
const encoded = encodeURIComponent(query);
// ── 区域数据源配置 ──────────────────────────────────────────────────────────
const sources = {
cn: [
{ label: '什么值得买优惠券专区', url: `https://search.smzdm.com/?c=home&s=encoded&v=b`, note: '筛选"优惠券"标签,提取券码、面额、有效期、适用商品' },
{ label: '京东领券中心', url: `https://coupon.jd.com/`, note: '搜索「query」相关优惠券,提取可领取的券面额和使用门槛' },
{ label: '淘宝聚划算', url: `https://ju.taobao.com/jusp/index.html?spm=a21bo.jianhua.201867-main.1.5af911d9mZFx5p&keyword=encoded`, note: '提取限时折扣和专属优惠' },
{ label: '折800', url: `https://www.zhe800.com/search?keyword=encoded`, note: '提取优惠码和返利信息' },
],
us: [
{ label: 'RetailMeNot', url: `https://www.retailmenot.com/search#query=encoded`, note: '提取验证通过的优惠码、折扣幅度、到期时间、成功率' },
{ label: 'Slickdeals', url: `https://slickdeals.net/newsearch.php?q=encoded&searchin=first&sort=newest`, note: '从社区验证热门优惠中提取当前有效的券和折扣' },
{ label: 'Amazon Coupons', url: `https://www.amazon.com/coupons#search=encoded`, note: '提取可直接 clip 的 Amazon 官方券,标注节省金额' },
{ label: 'Dealmoon (华人专区)', url: `https://www.dealmoon.com/search?q=encoded`, note: '提取北美华人社区精选优惠,标注是否仍有效' },
],
uk: [
{ label: 'VoucherCodes UK', url: `https://www.vouchercodes.co.uk/search/?q=encoded`, note: '提取券码、折扣幅度、到期日' },
{ label: 'HotUKDeals', url: `https://www.hotukdeals.com/search?q=encoded&type=deals`, note: '社区热帖中的最新折扣,提取温度值(热度)和链接' },
{ label: 'MyVoucherCodes UK', url: `https://www.myvouchercodes.co.uk/search?q=encoded`, note: '补充验证英国券码' },
],
au: [
{ label: 'OzBargain', url: `https://www.ozbargain.com.au/search?q=encoded&action=search`, note: '提取澳洲社区验证优惠,标注是否有效及过期时间' },
{ label: 'Cashrewards AU', url: `https://www.cashrewards.com.au/stores?q=encoded`, note: '提取澳洲返利比例' },
],
sea: [
{ label: 'ShopBack', url: `https://www.shopback.com/search?q=encoded`, note: '提取新加坡/马来/泰/菲/印尼返利比例和优惠' },
{ label: 'iPrice', url: `https://iprice.my/search/?q=encoded`, note: '东南亚价格+优惠聚合' },
],
};
// 选择要查的区域
let activeSources = [];
if (region === 'all') {
activeSources = [...sources.cn, ...sources.us, ...sources.uk, ...sources.au, ...sources.sea];
} else {
activeSources = sources[region] || [];
}
// 返利平台(全局叠加)
const cashbackSources = region === 'cn' ? [
{ label: '返利网', url: `https://www.fanli.com/search?q=encoded` },
{ label: '什么值得买返利', url: `https://search.smzdm.com/?c=home&s=encoded&v=b` },
] : [
{ label: 'Rakuten (US/UK/JP)', url: `https://www.rakuten.com/search?q=encoded` },
{ label: 'TopCashback', url: `https://www.topcashback.co.uk/search/?searchInput=encoded` },
{ label: 'ShopBack (SEA/AU)', url: `https://www.shopback.com/search?q=encoded` },
];
// DTC 品牌检测提示
const dtcHint = `如「query」是品牌官网直销(DTC)商品,额外执行:
- web_search「query site:retailmenot.com OR site:slickdeals.net coupon code」获取品牌专属券码
- browser 导航品牌官网首页,检查是否有订阅弹窗优惠(通常首单 10-15% off)或顶部 banner 促销
- web_search「query first order discount newsletter signup」`;
if (lang === 'zh') {
const sourceList = activeSources.map((s, i) =>
`i + 1. 打开 s.url\n → s.label:s.note`
).join('\n\n');
const cashbackList = cashbackSources.map((s, i) =>
`activeSources.length + i + 1. 打开 s.url\n → s.label:查询该商品/商家的返利比例`
).join('\n\n');
console.log(`请为「query」查找所有可用优惠券,并制定最优叠加省钱方案。
使用 browser 工具依次导航以下页面,直接从页面提取真实有效的券码和折扣信息。
═══ 第一层:优惠券查询 ═══
sourceList
═══ 第二层:返利叠加查询 ═══
cashbackList
═══ 第三层:DTC 品牌检测 ═══
dtcHint
提取规则:
- 只记录**当前有效**的券码(标注到期时间;若已过期,标注"已过期"不列入推荐)
- 注明每个券的:券码(若有)、面额/折扣幅度、使用门槛、到期时间、来源平台
- 某页面无法访问则标"无法获取",不得编造券码
- 无券码的平台折扣(如直接打折)也列出,标注"无需码/直接享"
输出格式:
🎟️ CouponClaw · query
━━━━━━━━━━━━━━━━━━━━━━━
🏷️ 可用券码 & 折扣
[按节省金额从高到低排列]
① [来源] 券码: XXXXXX | 满¥/$ XX 减¥/$ XX | 到期: XXXX-XX-XX | ✅有效
② [来源] 券码: XXXXXX | X折 / X% off | 到期: XXXX-XX-XX | ✅有效
③ ...
💰 返利叠加
[平台] 返利比例 X% / 返¥XXX(可与以上券码叠加 ✅ / 不可叠加 ❌)
🔢 最优叠加方案
用券 ① + [返利平台] 返利 = 实际节省 ¥/$ XXX(原价 ¥/$ XXX → 到手价 ¥/$ XXX)
📌 数据来源:activeSources.map(s => s.label).join(' / ')
━━━━━━━━━━━━━━━━━━━━━━━
💡 回复"每日优惠"订阅每日推送 · 回复"比价"跳转 BuyWise 查最低价`);
} else {
const sourceList = activeSources.map((s, i) =>
`i + 1. Open s.url\n → s.label: s.note`
).join('\n\n');
const cashbackList = cashbackSources.map((s, i) =>
`activeSources.length + i + 1. Open s.url\n → s.label: find cashback rate for this merchant`
).join('\n\n');
console.log(`Please find all available coupons for "query" and recommend the best stacking strategy.
Use the browser tool to navigate each page below and extract real, currently-valid coupons.
═══ Layer 1: Coupon Search ═══
sourceList
═══ Layer 2: Cashback Stacking ═══
cashbackList
═══ Layer 3: DTC Brand Check ═══
If "query" is a DTC brand:
- web_search "query site:retailmenot.com OR site:slickdeals.net coupon code" for brand-specific codes
- browser navigate to brand's official homepage, check for signup popup offer (usually 10-15% off first order) or banner promotions
- web_search "query first order discount newsletter signup"
Extraction rules:
- Only record **currently valid** coupons (note expiry; mark expired ones as such — do not recommend them)
- For each coupon: code (if any), discount amount/%, minimum spend, expiry date, source
- Mark "unavailable" if a page fails to load — never fabricate coupon codes
- Platform-wide sales with no code (auto-applied) should also be listed as "no code needed"
Output format:
🎟️ CouponClaw · query
━━━━━━━━━━━━━━━━━━━━━━━
🏷️ Available Coupons & Deals
[sorted by savings, highest first]
① [Source] Code: XXXXXX | $XX off $XX+ | Expires: XXXX-XX-XX | ✅ Valid
② [Source] Code: XXXXXX | X% off | Expires: XXXX-XX-XX | ✅ Valid
③ ...
💰 Cashback Stacking
[Platform] X% cashback / $XXX back (stackable with above codes ✅ / not stackable ❌)
🔢 Best Stacking Strategy
Code ① + [cashback platform] = total saving $XXX (original $XXX → final price $XXX)
📌 Sources: activeSources.map(s => s.label).join(' / ')
━━━━━━━━━━━━━━━━━━━━━━━
💡 Reply "daily deals" to subscribe · Reply "compare prices" to jump to BuyWise`);
}
BuyWise is your personal shopping advisor — it helps you decide whether to buy, where to buy, and when to buy, across all major global and Chinese platforms....
---
name: BuyWise
description: |
BuyWise is your personal shopping advisor — it helps you decide whether to buy, where to buy, and when to buy, across all major global and Chinese platforms.
Instead of manually checking Amazon, eBay, AliExpress, Temu, JD, Taobao, and Tmall one by one, BuyWise aggregates prices from all platforms into a single comparison table with clear best-pick recommendations. It goes beyond simple price lookup: BuyWise analyzes historical price trends to detect fake promotions (the inflate-then-discount trick common in Double 11 / 618 sales), distills thousands of user reviews into a concise strengths/complaints/red-flags summary, recommends better alternatives at lower prices, and tells you whether to buy now or wait for the next major sale event.
Just tell BuyWise what you want to buy — it handles the rest.
keywords: 购物助手, 比价, 值不值得买, 买前必看, 识别假促销, 双11, 618, 京东, 淘宝, 天猫, 拼多多, 评价分析, 好物推荐, 省钱, 替代品, 购物时机, 历史价格, 最低价, 购物决策, 剁手前, 亚马逊, 速卖通, shopping assistant, price comparison, buy or wait, deal checker, Amazon, eBay, AliExpress, Temu, JD, Taobao, Tmall, price tracker, review summary, best price, shopping advisor, is it worth buying, fake discount, product review, alternatives, best deal
metadata:
openclaw:
runtime:
node: ">=18"
recommends:
skills:
- gstack
---
# BuyWise — 全球购物决策助手
> 比价 · 识别假促销 · 评价提炼 · 替代品推荐 · 买入时机判断
## 何时使用
- 用户问"这个值得买吗""这个怎么样"
- 用户说"帮我比价""最低价在哪""哪里买最便宜"
- 用户问"双11能便宜多少""618值得等吗""现在买合适吗"
- 用户说"帮我看看评价""这个评价怎么样""有没有问题"
- 用户说"有没有平替""有没有更便宜的""有没有更好的"
- 用户发来商品链接,询问是否值得购买
---
## 🌐 语言规则
- 默认中文;用户英文提问切英文
- 价格按用户所在市场显示(国内用 ¥,国际用 $)
- 平台名称保留原文(不翻译 Amazon / JD / Temu 等)
---
## 🛒 覆盖平台
| 市场 | 平台 |
|------|------|
| 中国国内 | 京东、淘宝、天猫、拼多多(百亿补贴)、闲鱼(二手) |
| 国际 | Amazon、eBay、AliExpress、Temu |
---
## 🔌 数据来源
BuyWise 通过 **browser 工具直接导航到真实数据页面**获取价格,不依赖 web_search 猜测:
| 数据类型 | 来源 | 覆盖平台 |
|---------|------|---------|
| 中国市场价格 | **什么值得买 smzdm.com** | 京东、淘宝、天猫、拼多多 |
| Amazon 历史价格 | **CamelCamelCamel** | Amazon 全球 |
| Amazon 当前价格 | amazon.com 搜索页 | Amazon |
| eBay 价格 | ebay.com 搜索页 | eBay |
| AliExpress 价格 | aliexpress.com 搜索页 | AliExpress |
| Temu / 闲鱼 | web_search 补充 | Temu、闲鱼 |
| 评价 | web_search + 小红书/知乎/Reddit | 多平台 |
> 需要 `gstack` skill 已安装以启用 browser 导航能力。未安装时降级为 web_search(精度下降)。
---
## 📋 功能说明
### 综合决策(主功能)
用户说"帮我看看 XX"时触发完整分析:
1. **多平台比价** — browser 导航 smzdm/Amazon/eBay/AliExpress 等获取真实价格,汇总对比表
2. **促销真实性** — browser 导航 CamelCamelCamel + smzdm 价格走势图,判断是否真实低价
3. **评价提炼** — 从京东/知乎/小红书/Reddit 等多源提炼 3 优点 / 3 槽点 / 适合人群 / 红旗警告
4. **替代品推荐** — 搜索同类更高性价比产品,推荐 2-3 个选项
5. **购买时机** — 结合历史低价、大促节点、当前价格给出 🟢立即买 / 🟡再等等 / 🔴不建议
### 独立比价
用户只问"XX 多少钱""哪里最便宜"时,仅输出比价表,不做完整分析。
### 促销核查
用户说"这个双11真的便宜吗""这个折扣是真的吗"时,专项分析价格历史,判断促销真实性。
### 评价深读
用户说"详细说说评价""口碑怎么样"时,深度搜索多平台评测、论坛讨论、投诉记录。
---
## 🔧 脚本说明
```bash
# 完整购物决策(主入口)
node scripts/advise.js <商品名> [--lang zh|en]
# 示例:
node scripts/advise.js "戴森吸尘器 V15"
node scripts/advise.js "Sony WH-1000XM5" --lang en
# 仅比价
node scripts/compare.js <商品名> [--lang zh|en]
node scripts/compare.js "AirPods Pro 2"
# 促销真实性核查
node scripts/deal-check.js <商品名> [--price 当前价] [--was 标称原价] [--lang zh|en]
node scripts/deal-check.js "iPhone 16" --price 5999 --was 7999
# 评价提炼
node scripts/review-scan.js <商品名> [--lang zh|en]
node scripts/review-scan.js "小米路由器 BE7000"
```
---
## ⚠️ 注意事项
1. 价格基于 WebSearch 实时搜索,可能存在轻微延迟,购买前以平台实际页面为准
2. 历史价格分析依赖公开信息,若平台不公示价格历史则以搜索结果为准
3. 评价摘要基于公开评测,不代表所有用户体验
4. 促销节点判断(双11/618等)以中国电商日历为基准;国际促销参考 Black Friday / Prime Day
5. 替代品推荐不含广告或赞助,仅基于性价比分析
FILE:README.md
# BuyWise — Global Shopping Decision Assistant
> Should you buy it? Compare prices on 8+ platforms, detect fake discounts, analyze reviews, and get a clear Buy / Wait / Skip verdict.
[](https://clawhub.ai/skills/buywise)
[](https://clawhub.ai/skills/buywise)
[](LICENSE)
## What it does
BuyWise is an [OpenClaw](https://openclaw.ai) skill that answers the question you ask before every purchase: *is this actually a good deal?*
It navigates real product pages using your AI's browser tool — no fabricated prices — and returns a verdict with evidence.
**Price comparison** — Amazon, eBay, AliExpress, Temu, JD, Taobao, Tmall, smzdm in one report
**Fake discount detection** — checks CamelCamelCamel and smzdm price history to expose "inflate then discount" tactics
**Review analysis** — extracts top strengths, complaints, who it's for, and red flags
**Alternatives** — recommends 2-3 better-value options with price ranges
**Buy timing** — 🟢 Buy now / 🟡 Wait / 🔴 Skip verdict with reasoning
**Coupon stacking** — hands off to [CouponClaw](https://github.com/jiajiaoy/CouponClaw) to stack promo codes + cashback on confirmed buys
## Price sources
| Platform | Region | Notes |
|---|---|---|
| Google Shopping (`?tbm=shop`) | Global | Aggregates Amazon, eBay, Walmart, Best Buy and dozens more |
| smzdm (什么值得买) | China | Aggregates JD / Taobao / Tmall / Pinduoduo in one page |
| Amazon | US/Global | Cross-verified with Google Shopping |
| AliExpress | Global | Budget / wholesale reference |
| Temu | Global | Via web_search (no standard search URL) |
| CamelCamelCamel | Amazon history | All-time low, 90-day average, pre-sale spike detection |
| JD (京东) | China | Self-operated listings |
## Installation
```bash
openclaw install buywise
```
## Usage
```bash
# Full buying decision
openclaw run buywise advise "Sony WH-1000XM5"
# Price comparison only
openclaw run buywise compare "iPhone 16 Pro"
# Check if a sale is genuine
openclaw run buywise deal "Dyson V15 --price 399 --was 649"
# Review deep-dive
openclaw run buywise review "Kindle Paperwhite"
```
## Ecosystem
Part of the **OpenClaw Smart Consumer** skill suite:
| Skill | Description |
|---|---|
| [TrendRadar](https://github.com/jiajiaoy/TrendRadar) | Detect trending products → feed into BuyWise |
| **BuyWise** | Shopping decision ← you are here |
| [CouponClaw](https://github.com/jiajiaoy/CouponClaw) | Stack coupons + cashback after BuyWise confirms it's worth buying |
| [TravelHound](https://github.com/jiajiaoy/TravelHound) | Same logic applied to flights and hotels |
| [NewsToday](https://github.com/jiajiaoy/NewsToday) | Daily news including consumer/market trends |
## Keywords
price comparison · buy or wait · fake discount · Amazon price history · CamelCamelCamel · shopping assistant · deal checker · product review · is it worth buying · 比价 · 值不值得买 · 假折扣 · 购物决策 · Black Friday · Cyber Monday
---
Built for [OpenClaw](https://openclaw.ai) · Published on [clawhub.ai/skills/buywise](https://clawhub.ai/skills/buywise)
FILE:package.json
{
"name": "buywise",
"version": "1.5.2",
"description": "Should you buy it? Price-compare across Amazon, JD, AliExpress, Temu, eBay — detect fake discounts, analyze reviews, and get a clear Buy / Wait / Skip verdict.",
"keywords": [
"should I buy",
"is it worth buying",
"buy or wait",
"worth it",
"price comparison",
"compare prices",
"best price",
"lowest price",
"price tracker",
"Amazon price history",
"price history",
"price drop",
"price alert",
"fake discount",
"fake sale",
"discount checker",
"deal checker",
"deal authenticator",
"Black Friday",
"Cyber Monday",
"11.11",
"618 deals",
"double 11",
"product review",
"review summary",
"review analysis",
"product research",
"shopping assistant",
"shopping guide",
"shopping tips",
"consumer guide",
"alternatives",
"product alternatives",
"better alternative",
"Amazon",
"eBay",
"AliExpress",
"Temu",
"JD",
"Taobao",
"Tmall",
"CamelCamelCamel",
"Google Shopping",
"smzdm",
"是否值得买",
"值不值得买",
"买不买",
"买前必看",
"比价",
"价格历史",
"假折扣",
"假促销",
"识别假折扣",
"购物决策",
"购物攻略",
"省钱",
"海淘",
"跨境购物",
"双11",
"618",
"好物推荐",
"替代品",
"平替",
"最低价",
"历史低价",
"购物时机",
"剁手前必看",
"消费决策"
],
"author": "jiajiaoy",
"license": "MIT-0",
"scripts": {
"advise": "node scripts/advise.js",
"compare": "node scripts/compare.js",
"review": "node scripts/review-scan.js",
"deal": "node scripts/deal-check.js"
},
"openclaw": {
"triggers": [
"should I buy",
"is it worth buying",
"buy or wait",
"price comparison",
"compare prices",
"fake discount",
"product review",
"值不值得买",
"买不买",
"比价",
"值得买吗",
"买前",
"怎么样",
"推荐吗",
"好用吗"
],
"entrypoint": "scripts/advise.js"
}
}
FILE:scripts/advise.js
#!/usr/bin/env node
'use strict';
/**
* BuyWise — 综合购物决策主入口
* 输入商品名或链接,输出完整的"买不买、在哪买、等不等"分析报告
* 用法: node scripts/advise.js <商品名或URL> [--lang zh|en]
*/
const args = process.argv.slice(2);
const langIdx = args.indexOf('--lang');
const lang = langIdx !== -1 && args[langIdx + 1] === 'en' ? 'en' : 'zh';
const productArgs = args.filter((a, i) => a !== '--lang' && args[i - 1] !== '--lang');
const product = productArgs.join(' ').trim();
if (!product) {
console.error(lang === 'zh' ? '用法: node scripts/advise.js <商品名或链接>' : 'Usage: node scripts/advise.js <product name or URL>');
process.exit(1);
}
const now = new Date();
const dateISO = `now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
// 判断是否临近大促节点
function nearPromo() {
const m = now.getMonth() + 1, d = now.getDate();
if ((m === 11 && d >= 1) || (m === 6 && d >= 1 && d <= 20)) return true;
if ((m === 3 && d >= 8 && d <= 15) || (m === 12 && d >= 12 && d <= 13)) return true;
return false;
}
const promoHint = nearPromo()
? (lang === 'zh' ? '\n⚠️ 当前临近大促节点,请重点分析是否值得等待促销,并核查价格是否真实优惠。' : '\n⚠️ Near a major sale event — prioritize analyzing whether waiting is worthwhile and verify if the discount is genuine.')
: '';
const encoded = encodeURIComponent(product);
if (lang === 'zh') {
console.log(`请对以下商品进行全面的购物决策分析:「product」
当前日期:dateISOpromoHint
## 第一步:多平台比价(用 browser 导航,不要用 web_search 猜价格)
按顺序打开以下 URL,从页面直接提取价格:
1. https://search.smzdm.com/?c=home&s=encoded&v=b → 一站获取京东/淘宝/天猫/拼多多聚合价格
2. https://search.jd.com/Search?keyword=encoded&enc=utf-8 → 京东直查,取自营商品价格
3. https://www.google.com/search?q=encoded&tbm=shop → Google 购物:一站聚合 Amazon/eBay/Walmart 等几十家零售商真实价格,取价格区间和最低价
4. 如 Google 购物无法访问 → 备用 https://www.bing.com/shop?q=encoded
5. https://www.amazon.com/s?k=encoded → 与 Google Shopping 交叉验证 Amazon 价格
6. https://www.aliexpress.com/wholesale?SearchText=encoded → AliExpress 前3条价格
7. web_search「product Temu price」→ Temu 补充(无标准搜索 URL)
8. web_search「product 闲鱼 价格」→ 二手参考
⚠️ 规则:只记录从页面实际读取到的价格;某平台无法访问则标"无法获取",不得编造数字。
## 第二步:促销真实性核查(用 browser 查历史价格)
1. https://camelcamelcamel.com/search?search=encoded → Amazon 历史最低/均价/是否促销前涨价
2. 在步骤1的 smzdm 商品详情页查看"价格走势图"和"历史低价"
判断:当前价 vs 近90天均价,是否真实低位,是否有先涨后降迹象
## 第三步:评价提炼
- 搜索「product 评价」「product 使用体验」「product 踩坑」
- 从真实用户反馈中提炼:
✅ 3个核心优点
❌ 3个主要槽点或质量问题
👤 适合人群 vs 不适合人群
🚩 红旗警告(若有质量投诉、虚假宣传等)
## 第四步:替代品推荐
- 搜索「product 平替」「比 product 更好的」「product 竞品」
- 推荐 2-3 个性价比更高或功能更好的替代品,含价格区间
## 第五步:购买时机建议
综合以上信息,给出明确结论:
- 🟢 立即购买:价格处于低位 + 评价良好 + 无更好替代
- 🟡 再等等:临近大促 / 价格有下降空间
- 🔴 不建议:质量问题多 / 有更好替代品 / 性价比低
---
## 输出格式
🛒 BuyWise · product
━━━━━━━━━━━━━━━━━━━━━━━
📊 各平台比价
[价格对比表,含平台、价格、差价]
🔍 促销真实性
[历史价格分析,真优惠 or 假折扣]
⭐ 评价摘要
✅ 优点 | ❌ 槽点 | 👤 适合人群
🔄 替代品推荐
[2-3个替代选项,含价格]
✅ 购买建议
[🟢/🟡/🔴 + 1-2句理由 + 最推荐购买渠道]
🎟️ 优惠券 & 返利
在确认购买前,运行 CouponClaw 查找可用券码和返利叠加方案:
openclaw run couponclaw find "product" --region all
可额外节省的金额请补充到"实际到手价"中。
━━━━━━━━━━━━━━━━━━━━━━━
💡 回复"详细评价"深度分析口碑 · 回复"比价 XX"单独查某平台 · 回复"找券"调用 CouponClaw`);
} else {
console.log(`Please provide a comprehensive buying decision analysis for: "product"
Date: dateISOpromoHint
## Step 1: Cross-Platform Price Comparison (use browser — do NOT guess prices via web_search)
Open each URL with the browser tool and extract prices directly from the page:
1. https://search.smzdm.com/?c=home&s=encoded&v=b → aggregated JD/Taobao/Tmall/Pinduoduo prices in one page
2. https://search.jd.com/Search?keyword=encoded&enc=utf-8 → JD direct, first self-operated listing
3. https://www.google.com/search?q=encoded&tbm=shop → Google Shopping: aggregates Amazon, eBay, Walmart, Best Buy and dozens more — extract price range and lowest retailer
4. If Google Shopping unavailable → fallback https://www.bing.com/shop?q=encoded
5. https://www.amazon.com/s?k=encoded → cross-verify Amazon price against Google Shopping
6. https://www.aliexpress.com/wholesale?SearchText=encoded → AliExpress top 3 prices
7. web_search "product Temu price" → Temu supplement (no standard search URL)
8. web_search "product used price" → second-hand reference
⚠️ Rule: only record prices actually read from the page. Mark "unavailable" if a page fails — never invent prices.
## Step 2: Deal Authenticity Check (use browser for real price history)
1. https://camelcamelcamel.com/search?search=encoded → Amazon all-time low, 90-day average, pre-sale spike detection
2. In the smzdm product detail page from Step 1, check the price history chart and lowest-ever price
Assess: current price vs 90-day average; genuine low or inflate-then-discount pattern?
## Step 3: Review Analysis
- Search "product reviews" "product problems" "product honest review"
- Extract from real user feedback:
✅ 3 core strengths
❌ 3 main complaints or quality issues
👤 Who it's great for vs. who should avoid it
🚩 Red flags (quality complaints, misleading claims, etc.)
## Step 4: Alternatives
- Search "alternatives to product" "product competitors" "better than product"
- Recommend 2-3 alternatives with better value or features, including price range
## Step 5: Buy Timing Recommendation
Synthesize everything into a clear verdict:
- 🟢 Buy now: price is at/near low + good reviews + no better alternative
- 🟡 Wait: near a major sale / price likely to drop
- 🔴 Skip: quality issues / better alternatives exist / poor value
---
## Output Format
🛒 BuyWise · product
━━━━━━━━━━━━━━━━━━━━━━━
📊 Price Comparison
[Table: platform, price, price gap, shipping]
🔍 Deal Authenticity
[Historical price analysis: genuine deal or fake discount]
⭐ Review Summary
✅ Strengths | ❌ Complaints | 👤 Who it's for
🔄 Alternatives
[2-3 alternatives with prices]
✅ Buying Recommendation
[🟢/🟡/🔴 + 1-2 sentence rationale + best platform to buy]
🎟️ Coupons & Cashback
Before finalizing, run CouponClaw to find available promo codes and cashback stacking:
openclaw run couponclaw find "product" --region all
Add any additional savings to the final "effective price" above.
━━━━━━━━━━━━━━━━━━━━━━━
💡 Reply "deep reviews" for in-depth reputation analysis · Reply "find coupons" to run CouponClaw`);
}
FILE:scripts/compare.js
#!/usr/bin/env node
'use strict';
/**
* BuyWise — 多平台比价
* 使用 browser 工具导航到真实比价页,不依赖 web_search 盲猜价格
* 用法: node scripts/compare.js <商品名> [--lang zh|en]
*/
const args = process.argv.slice(2);
const langIdx = args.indexOf('--lang');
const lang = langIdx !== -1 && args[langIdx + 1] === 'en' ? 'en' : 'zh';
const productArgs = args.filter((a, i) => a !== '--lang' && args[i - 1] !== '--lang');
const product = productArgs.join(' ').trim();
if (!product) {
console.error(lang === 'zh' ? '用法: node scripts/compare.js <商品名>' : 'Usage: node scripts/compare.js <product>');
process.exit(1);
}
const encoded = encodeURIComponent(product);
if (lang === 'zh') {
console.log(`请对「product」进行真实价格比对。使用 browser 工具依次导航以下 URL,从页面直接提取价格,不要用 web_search 猜测价格。
【中国市场 — 一站聚合】
1. 打开 https://search.smzdm.com/?c=home&s=encoded&v=b
→ 什么值得买已聚合京东/淘宝/天猫/拼多多价格,直接从页面提取各平台最低价和商品标题。
【中国市场 — 补充直查】
2. 打开 https://search.jd.com/Search?keyword=encoded&enc=utf-8
→ 从京东搜索结果提取价格(第一屏自营商品优先)。
3. 如商品在京东有自营链接,可进入商品详情页获取精确价格。
【国际市场 — Google Shopping 一站聚合】
4. 打开 https://www.google.com/search?q=encoded&tbm=shop
→ Google 购物页面聚合 Amazon、eBay、Walmart、Best Buy 等几十家零售商真实价格,
→ 从结果列表提取:商品名、各零售商价格、是否包邮。这是国际市场最高效的单一来源。
5. 如 Google 购物无法访问,备用:打开 https://www.bing.com/shop?q=encoded
→ Bing 购物同样聚合多家零售商,效果类似。
【国际市场 — 专项直查】
6. 打开 https://www.amazon.com/s?k=encoded
→ 从 Amazon 搜索结果提取前3条价格(含 Prime 标识),与 Google Shopping 数据交叉验证。
7. 打开 https://www.aliexpress.com/wholesale?SearchText=encoded
→ 从 AliExpress 提取前3条价格(美元),AliExpress 通常比 Amazon/eBay 便宜。
8. web_search「product Temu price」— Temu 无标准搜索 URL,用搜索补充。
【二手】
9. web_search「product 闲鱼 成色好 价格」— 获取二手参考价。
提取规则:
- 只取有明确数字的价格,不要写"价格不详"或编造数字
- 如某平台页面无法访问或加载失败,标注"无法获取"而非填假数据
- 价格含税含运费(若可判断)
- Google Shopping 结果中同一商品多家零售商报价,取最低价和最高价区间
输出格式:
🛒 product · 全平台比价
━━━━━━━━━━━━━━━━━━━━━━━
平台 | 价格 | 包邮 | 来源
京东(自营) | ¥XXX | ✓ | smzdm / 京东直查
淘宝/天猫 | ¥XXX | - | smzdm
拼多多 | ¥XXX | ✓ | smzdm
──────────────────────────────────
Google Shopping| $XXX~$XXX | - | 多家零售商区间
Amazon | $XXX | - | Google Shopping / 直查
eBay | $XXX | - | Google Shopping
Walmart/其他 | $XXX | - | Google Shopping
AliExpress | $XXX | - | 直查
Temu | $XXX | - | 搜索
──────────────────────────────────
闲鱼(二手) | ¥XXX | 协议 | 搜索
━━━━━━━━━━━━━━━━━━━━━━━
💰 全球最低价:[平台] ¥/$ XXX
💰 国内最低价:[平台] ¥XXX
💡 综合推荐:[综合价格+配送+保障的最佳选择]
📌 数据来源:browser 直取(smzdm / Google Shopping / Amazon / AliExpress)+ 搜索补充(Temu / 闲鱼)`);
} else {
console.log(`Please compare real prices for "product" across all major platforms. Use the browser tool to navigate to actual pricing pages — do NOT use web_search to guess prices.
[China market — aggregated]
1. Open https://search.smzdm.com/?c=home&s=encoded&v=b
→ smzdm.com already aggregates JD / Taobao / Tmall / Pinduoduo prices. Extract prices and product titles directly from the page.
[China market — direct check]
2. Open https://search.jd.com/Search?keyword=encoded&enc=utf-8
→ Extract price from the first JD official/self-operated listing.
[International — Google Shopping (single best source)]
3. Open https://www.google.com/search?q=encoded&tbm=shop
→ Google Shopping aggregates real prices from Amazon, eBay, Walmart, Best Buy, and dozens more.
→ Extract: retailer names, prices, shipping status. This covers most international retailers in one page.
4. If Google Shopping is inaccessible, fallback: open https://www.bing.com/shop?q=encoded
→ Bing Shopping provides similar multi-retailer aggregation.
[International — direct spot-check]
5. Open https://www.amazon.com/s?k=encoded
→ Verify Amazon price against Google Shopping data; note Prime eligibility.
6. Open https://www.aliexpress.com/wholesale?SearchText=encoded
→ AliExpress is typically cheapest for unbranded/generic items; extract top 3 prices.
7. web_search "product Temu price" — Temu has no standard search URL.
[Second-hand]
8. web_search "product used price good condition" — reference only.
Extraction rules:
- Only record prices actually read from the page; mark "unavailable" if a page fails — never invent prices
- For Google Shopping, note the price range (lowest to highest) across retailers
- Note whether price includes tax/shipping when determinable
Output format:
🛒 product · Price Comparison
━━━━━━━━━━━━━━━━━━━━━━━
Platform | Price | Shipping | Source
JD (official) | ¥XXX | Free | smzdm / direct
Taobao/Tmall | ¥XXX | - | smzdm
Pinduoduo | ¥XXX | Free | smzdm
────────────────────────────────────
Google Shopping | $XXX~$XXX | - | multi-retailer range
Amazon | $XXX | - | G.Shopping / direct
eBay | $XXX | - | G.Shopping
Walmart/others | $XXX | - | G.Shopping
AliExpress | $XXX | - | direct
Temu | $XXX | - | search
────────────────────────────────────
Used | $XXX | Varies | search
━━━━━━━━━━━━━━━━━━━━━━━
💰 Global best price: [retailer] $XXX
💰 China best price: [platform] ¥XXX
💡 Best overall: [price + delivery + buyer protection]
📌 Sources: browser-direct (smzdm / Google Shopping / Amazon / AliExpress) + search (Temu / used)`);
}
FILE:scripts/deal-check.js
#!/usr/bin/env node
'use strict';
/**
* BuyWise — 促销真实性核查
* 用 browser 导航到历史价格追踪站点,获取真实历史价格数据
* 用法: node scripts/deal-check.js <商品名> [--price 当前价格] [--was 标称原价] [--lang zh|en]
*/
const args = process.argv.slice(2);
const langIdx = args.indexOf('--lang');
const lang = langIdx !== -1 && args[langIdx + 1] === 'en' ? 'en' : 'zh';
const priceIdx = args.indexOf('--price');
const price = priceIdx !== -1 ? args[priceIdx + 1] : null;
const wasIdx = args.indexOf('--was');
const was = wasIdx !== -1 ? args[wasIdx + 1] : null;
const productArgs = args.filter((a, i) => {
if (['--lang','--price','--was'].includes(a)) return false;
if (['--lang','--price','--was'].includes(args[i - 1])) return false;
return true;
});
const product = productArgs.join(' ').trim();
if (!product) {
console.error(lang === 'zh' ? '用法: node scripts/deal-check.js <商品名> [--price 299] [--was 599]' : 'Usage: node scripts/deal-check.js <product> [--price 299] [--was 599]');
process.exit(1);
}
const encoded = encodeURIComponent(product);
const now = new Date();
const dateISO = `now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const priceContext = price
? (lang === 'zh' ? `当前促销价:¥pricewas ? `,标称原价:¥${was,折扣幅度:Math.round((1 - price/was)*100)%` : ''}` : `Current price: $price$${was, discount: Math.round((1 - price/was)*100)%` : ''}`)
: '';
if (lang === 'zh') {
console.log(`请核查「product」的促销是否真实。''
当前日期:dateISO
使用 browser 工具导航以下页面,获取真实历史价格数据:
【中国市场 — 历史价格】
1. 打开 https://search.smzdm.com/?c=home&s=encoded&v=b
→ 从什么值得买搜索结果中找到该商品,点击商品进入详情,查看"历史低价""价格走势图"
→ 记录:当前价格、历史最低价、近30/90天价格趋势
2. web_search「product 历史最低价 site:smzdm.com OR site:camelcamelcamel.com」
→ 补充获取价格区间参考
【Amazon 历史价格(若适用)】
3. 打开 https://camelcamelcamel.com/search?search=encoded
→ CamelCamelCamel 是 Amazon 专业价格追踪站,从页面提取:
- Amazon 历史最低价 / 最高价 / 当前价
- 90天价格走势
- 是否"先涨后降"(图表中促销前有明显价格抬高)
【判断标准】
根据获取的历史数据判断:
- ✅ 真实优惠:当前价格 ≤ 近90天均价的 90%,且无促销前涨价迹象
- ⚠️ 轻微注水:当前价格比均价低 0-10%,折扣幅度被夸大
- ❌ 假折扣:促销前1-4周内有明显涨价,实际折扣 < 标称折扣的 50%
【大促节点参考】
- 双11:每年11月1-11日(预售10月)
- 618:每年6月1-18日(预售5月)
- 38女王节:3月8-15日
- 双12:12月12日
输出格式:
🔍 促销核查 · product
━━━━━━━━━━━━━━━━━━━━━━━
priceContext || '当前价格:请提供以便分析'
历史最低价:¥XXX(XXXX-XX,来源: smzdm/CamelCamelCamel)
近90天均价:¥XXX
当前价格位置:历史低位 / 中等 / 偏高
促销真实性:✅ 真实优惠 / ⚠️ 轻微注水 / ❌ 假折扣
判断依据:[1-2句说明,引用具体历史价格数据]
📅 购买时机建议:
- 立即:[是/否,理由]
- 下次大促:预计 XXXX年XX月,届时预计 ¥XXX
🎟️ 如果决定立即购买,运行 CouponClaw 叠加优惠券和返利:
openclaw run couponclaw find "product" --region all
━━━━━━━━━━━━━━━━━━━━━━━`);
} else {
console.log(`Please verify whether the deal on "product" is genuine.''
Date: dateISO
Use the browser tool to navigate to these pages for real historical price data — do not guess:
[Amazon price history]
1. Open https://camelcamelcamel.com/search?search=encoded
→ CamelCamelCamel tracks Amazon prices. Extract:
- All-time low / high / current price on Amazon
- 90-day price trend
- Whether there was a pre-sale price spike (visible in the chart)
[International shopping trends]
2. Open https://www.amazon.com/s?k=encoded
→ Check current Amazon price and compare to CamelCamelCamel data.
3. web_search "product price history Black Friday Prime Day discount"
→ Supplement with seasonal pricing context.
[Verdict criteria]
Based on the historical data:
- ✅ Genuine deal: current price ≤ 90% of 90-day average, no pre-sale price spike
- ⚠️ Slightly inflated: marginal discount (0-10% below average), overstated savings claim
- ❌ Fake discount: price was raised 1-4 weeks before sale; real discount < 50% of claimed
Output format:
🔍 Deal Check · product
━━━━━━━━━━━━━━━━━━━━━━━
please provide for analysis'
All-time low: $XXX (XXXX-XX, source: CamelCamelCamel)
90-day average: $XXX
Current price position: near low / mid / high
Verdict: ✅ Genuine deal / ⚠️ Slightly inflated / ❌ Fake discount
Evidence: [1-2 sentences citing actual historical price data]
📅 Buy timing:
- Now: [yes/no, reason]
- Next major sale: approx. XXXX-XX, expected price $XXX
🎟️ If buying now, run CouponClaw to stack coupons and cashback:
openclaw run couponclaw find "product" --region all
━━━━━━━━━━━━━━━━━━━━━━━`);
}
FILE:scripts/review-scan.js
#!/usr/bin/env node
'use strict';
/**
* BuyWise — 评价提炼
* 用法: node scripts/review-scan.js <商品名> [--lang zh|en]
*/
const args = process.argv.slice(2);
const langIdx = args.indexOf('--lang');
const lang = langIdx !== -1 && args[langIdx + 1] === 'en' ? 'en' : 'zh';
const productArgs = args.filter((a, i) => a !== '--lang' && args[i - 1] !== '--lang');
const product = productArgs.join(' ').trim();
if (!product) {
console.error(lang === 'zh' ? '用法: node scripts/review-scan.js <商品名>' : 'Usage: node scripts/review-scan.js <product>');
process.exit(1);
}
if (lang === 'zh') {
console.log(`请深度分析「product」的用户评价口碑。
搜索以下多个来源:
1. 搜索「product 评测 真实体验」
2. 搜索「product 差评 问题」「product 踩坑 注意」
3. 搜索「product 值得买 知乎」或「product 小红书 使用感受」
4. 搜索「product reddit review」或「product honest review」(国际口碑)
5. 若有重大质量投诉,搜索「product 质量问题 投诉」
分析维度:
- 综合评分感知(用户整体满意度)
- 高频好评关键词(反复出现的优点)
- 高频差评关键词(反复出现的问题)
- 严重问题(安全隐患、致命缺陷、虚假宣传)
- 适合场景 vs 不适合场景
输出格式:
⭐ 评价分析 · product
━━━━━━━━━━━━━━━━━━━━━━━
综合口碑:[好评如潮 / 褒贬不一 / 差评较多]
✅ 核心优点
1. [优点一]
2. [优点二]
3. [优点三]
❌ 主要槽点
1. [问题一]
2. [问题二]
3. [问题三]
🚩 红旗警告(若有)
[严重质量问题 / 虚假宣传 / 安全投诉]
👤 适合人群
适合:[具体场景/人群]
不适合:[具体场景/人群]
━━━━━━━━━━━━━━━━━━━━━━━
💡 回复"比价"查看最低购买渠道`);
} else {
console.log(`Please deeply analyze user reviews and reputation for "product".
Search multiple sources:
1. Search "product review honest experience"
2. Search "product problems complaints" "product issues to know"
3. Search "product reddit review" "product forum"
4. Search "product 1 star review" "product worst problems"
5. If quality issues suspected, search "product recall" "product safety issue"
Analysis dimensions:
- Overall sentiment (user satisfaction level)
- Frequently praised features
- Frequently complained issues
- Serious problems (safety hazards, fatal flaws, misleading claims)
- Best use cases vs. situations to avoid
Output format:
⭐ Review Analysis · product
━━━━━━━━━━━━━━━━━━━━━━━
Overall reputation: [Highly rated / Mixed / Poorly rated]
✅ Core strengths
1. [Strength 1]
2. [Strength 2]
3. [Strength 3]
❌ Main complaints
1. [Issue 1]
2. [Issue 2]
3. [Issue 3]
🚩 Red flags (if any)
[Quality defects / misleading claims / safety complaints]
👤 Best for
Good fit: [specific use case / user type]
Not for: [specific use case / user type]
━━━━━━━━━━━━━━━━━━━━━━━
💡 Reply "compare" to find the best price`);
}
NewsToday solves information overload for users who want to stay informed without spending an hour checking scattered sources. Instead of manually browsing W...
---
name: NewsToday
description: |
NewsToday solves information overload for users who want to stay informed without spending an hour checking scattered sources. Instead of manually browsing Weibo, Zhihu, Baidu, and news apps separately, NewsToday aggregates, deduplicates, and summarizes the most important stories into a single readable briefing delivered at the right time.
Every morning, NewsToday pushes a curated briefing of 10 top stories spanning politics, finance, technology, international affairs, and society — pulled from both RSS feeds (Sina News, The Paper, 36Kr, BBC Chinese, Reuters Chinese) and real-time WebSearch, each with a 2-sentence summary and source attribution. Every evening, a recap highlights what developed throughout the day and previews tomorrow's key events. Breaking news alerts fire automatically every 2 hours during daytime whenever a major story breaks — earthquakes, market crashes, political announcements — so users never miss what matters.
Users can tune their experience by setting topic preferences — weighting finance over entertainment, or boosting international coverage — so every briefing reflects what actually matters to them. Supports Chinese and English output. Deliverable via Telegram, Feishu, Slack, or Discord. No registration required for on-demand queries; optional user profile unlocks personalized daily push and breaking alerts.
Trigger words: 早报, 晚报, 今日新闻, 新闻摘要, 热榜, 热搜, 追踪, 最新消息, 突发, 微博热搜, 知乎热榜, X热帖, 科技新闻, 财经新闻, AI早报, AI最新, 人工智能动态, 军事新闻, 军事动态, 头条, 订阅新闻, morning briefing, daily news, news summary, Chinese news, trending, breaking news, news push, hot topics, topic tracking, international news, AI news, military news.
keywords: 新闻推送, 早报, 新闻摘要, 每日新闻, 今日新闻, 热榜, 热搜, 订阅新闻, 晚报, 突发新闻, 微博热搜, 知乎热榜, 百度热搜, X热帖, 头条, 科技新闻, 财经新闻, 娱乐新闻, 体育新闻, 社会新闻, 国际新闻, 军事新闻, 地区冲突, 国防政策, AI早报, AI新闻, 大模型动态, 人工智能, 话题追踪, 最新消息, RSS新闻, 新闻聚合, 资讯, 快讯, 要闻, 每天新闻, 看新闻, 新闻助手, 资讯助手, 新闻机器人, 每日资讯, news push, daily briefing, news summary, Chinese news, morning briefing, evening news, trending, hot topics, breaking news, topic tracking, news aggregator, RSS feeds, personalized news, military news, AI news, news bot, daily news bot, news digest, news alert, China news, top stories, news reader
metadata:
openclaw:
runtime:
node: ">=18"
---
# NewsToday
> 私人新闻助手 — 早报 · 晚报 · RSS聚合 · 突发提醒 · 话题追踪 · 个性化推送
## 何时使用
- 用户说"早报""今天新闻""新闻摘要""今天发生了什么"
- 用户问"热搜""微博热榜""知乎热榜""X热帖"
- 用户说"AI 早报""AI 最新""人工智能动态"
- 用户想看某类新闻:科技、AI、财经、娱乐、体育、社会、国际、军事
- 用户说"追踪 XX""XX 最新消息""XX 怎么样了"
- 用户说"开启推送""订阅早报""每天推新闻"
- 用户说"突发""重大消息""有什么大事"
---
## 🌐 语言规则
- 默认中文;用户英文提问切英文
- 新闻标题保留原文,摘要用回复语言改写
---
## 📋 功能说明
### 早报
从 RSS(新浪/澎湃/36氪/BBC中文/Reuters中文)+ WebSearch 双源聚合,去重后选10条覆盖不同领域,按用户话题偏好加权排序。头部显示今日条数和预估阅读时长。第1条为**头条**(重要性最高,3-4句详细摘要+影响分析),其余9条常规格式(标题、来源、2句摘要)。财经类每条含影响评级:📈 利好 / 📉 利空 / ➡️ 中性。
### 晚报
收官3-5条当日重要新闻 + 1-2条热点最新进展 + 明日日程预告。
### 突发新闻提醒
每2小时检测(08:00-22:00),仅在满足阈值(7级以上地震、市场熔断、重大政策等)时推送,不骚扰用户。
### 热榜聚合
搜索微博热搜 + 知乎热榜 + 百度热搜 + X(Twitter)热帖,去重合并,标注来源,多平台共同热点置顶。X 热帖作为第三方实时信号,补充国内平台之前的舆情风向;若 X 数据不可用则静默降级,不影响其他来源输出。
### 话题追踪
搜索 `{关键词} 最新 {日期}` + `{关键词} 进展` + `{关键词} 官方回应`,时间线倒序输出,含各方反应。
### 深读
用户回复序号或说"详细说说 XX"时,多角度搜索,交叉验证,呈现详细经过、各方反应、延伸阅读。
### AI 早报(独立模式)
用户说"AI 早报""AI 最新""人工智能动态"时触发独立模式:专门搜索 `AI 最新进展 {日期}`、`大模型 新闻`、`OpenAI Anthropic Google DeepMind 动态`,输出 5 条 AI 专项摘要,含产品发布、研究突破、行业动向,与常规早报格式一致但信源更聚焦。
### 分类浏览
| 分类 | 搜索词 |
|------|--------|
| 科技 | 科技新闻 今日、AI新闻 |
| AI | AI 最新进展、大模型 新闻、OpenAI Anthropic 动态 |
| 财经 | 财经新闻 今日、股市 |
| 娱乐 | 娱乐新闻 今日 |
| 体育 | 体育新闻 今日、赛事结果 |
| 社会 | 社会新闻 今日、民生 |
| 国际 | 国际新闻 今日、外交 |
| 军事 | 军事新闻 今日、地区冲突、国防政策、军事演习 |
---
## 🔧 脚本说明
```bash
# 注册(可选,解锁个性化推送)
node scripts/register.js <userId> [language] [topics] [channel]
# 示例:
node scripts/register.js alice zh 科技,财经,国际 telegram
node scripts/register.js bob en tech,finance telegram
# 话题偏好
node scripts/preference.js show <userId>
node scripts/preference.js set <userId> <话题> <权重0-1>
node scripts/preference.js reset <userId>
# 手动触发(不需要注册)
node scripts/morning-push.js [userId]
node scripts/evening-push.js [userId]
node scripts/rss-fetch.js [--lang zh|en] [--topics 科技,财经,国际]
node scripts/breaking-alert.js <userId>
# 推送管理
node scripts/push-toggle.js on <userId> [--morning 08:00] [--evening 20:00] [--channel telegram]
node scripts/push-toggle.js off <userId>
node scripts/push-toggle.js status <userId>
```
支持渠道:`telegram` / `feishu` / `slack` / `discord`
---
## ⚠️ 注意事项
1. 每条新闻必须标注来源媒体
2. 涉及争议内容呈现多方视角,不做立场判断
3. 不注册可直接使用早晚报;注册后可按话题个性化、开启突发提醒
4. 用户数据仅存储推送偏好和话题权重(`data/users/<userId>.json`),不含新闻内容
5. RSS 源无法访问时自动降级为 WebSearch,不影响正常使用
FILE:README.md
# NewsToday — Daily News Briefing Skill
> Get 10 curated stories in 5 minutes. Hero story deep-dive · financial impact ratings · morning & evening push · breaking alerts.
[](https://clawhub.ai/skills/newstoday)
[](https://clawhub.ai/skills/newstoday)
[](LICENSE)
## What it does
NewsToday is an [OpenClaw](https://openclaw.ai) skill that delivers a personalized daily news briefing without you having to read anything. Every morning it picks the 10 most important stories, gives the top one a full **Hero Story** treatment (context + analysis + financial impact), and rates each finance/market story with 📈📉➡️ so you immediately know what moved the needle.
**Morning briefing** — 10 curated stories, 1 Hero Story deep-dive, financial impact ratings
**Evening briefing** — day recap + what to watch tomorrow
**Breaking alerts** — checked every 2 hours, only fires on genuinely significant events
**RSS aggregation** — pull in your own feeds on top of default sources
**AI briefing mode** — dedicated mode for LLM/AI industry news
**Personalization** — weight topics you care about (tech / finance / geopolitics / military)
Fully bilingual: **Chinese and English**.
## Data sources
| Source | What it covers |
|---|---|
| 微博热搜 | China trending topics |
| 知乎热榜 | China in-depth discussions |
| 百度热搜 | China general news |
| X (Twitter) | Global trending |
| Google News | International headlines |
| Hacker News | Tech & startup news |
| Reuters / AP | Breaking international news |
| 36Kr / The Paper | China tech & business |
## Installation
```bash
openclaw install newstoday
```
Or search `newstoday` on [clawhub.ai](https://clawhub.ai).
## Usage
```bash
# Morning briefing (on-demand, no setup needed)
openclaw run newstoday morning
# Evening briefing
openclaw run newstoday evening
# Enable daily push (Telegram / Slack / Feishu / Discord)
openclaw run newstoday push-on <userId> --morning 08:00 --evening 20:00 --channel telegram
# Breaking news check
openclaw run newstoday breaking
# AI/LLM news mode
openclaw run newstoday morning --lang en # triggers AI briefing for tech topics
# Personalize topic weights
node scripts/preference.js set <userId> 财经 0.9
node scripts/preference.js set <userId> 娱乐 0.2
```
## Ecosystem
Part of the **OpenClaw Smart Consumer** skill suite:
| Skill | Description |
|---|---|
| **NewsToday** | Daily news briefing ← you are here |
| [TrendRadar](https://github.com/jiajiaoy/TrendRadar) | Detect trending products from news + social media |
| [BuyWise](https://github.com/jiajiaoy/BuyWise) | Shopping decision: buy / wait / skip |
| [CouponClaw](https://github.com/jiajiaoy/CouponClaw) | Find coupons and stack cashback |
| [TravelHound](https://github.com/jiajiaoy/TravelHound) | Flight and hotel price comparison |
## Keywords
daily news · news briefing · morning briefing · news digest · news bot · news aggregator · Chinese news · breaking news · RSS · AI news · financial news · 每日新闻 · 早报 · 晚报 · 新闻推送 · 突发新闻 · AI早报 · 财经新闻
---
Built for [OpenClaw](https://openclaw.ai) · Published on [clawhub.ai/skills/newstoday](https://clawhub.ai/skills/newstoday)
FILE:_meta.json
{
"slug": "newstoday",
"version": "2.1.0",
"runtime": {
"node": ">=18"
}
}
FILE:package.json
{
"name": "newstoday",
"version": "2.3.1",
"description": "Daily news digest in 5 minutes — 10 curated stories, Hero Story deep-dive, financial impact ratings, breaking alerts, morning & evening push. Bilingual EN/CN.",
"keywords": [
"news",
"daily news",
"news briefing",
"news digest",
"news summary",
"news aggregator",
"morning briefing",
"evening briefing",
"daily briefing",
"daily digest",
"daily newsletter",
"breaking news",
"breaking alert",
"news alert",
"news push",
"news automation",
"current events",
"world news",
"business news",
"tech news",
"finance news",
"AI news",
"AI briefing",
"China news",
"Chinese news",
"top stories",
"RSS",
"RSS aggregation",
"news feed",
"news curation",
"topic tracking",
"trending topics",
"hot topics",
"news bot",
"news reader",
"news roundup",
"5 minute news",
"morning routine",
"daily update",
"newsletter",
"新闻",
"新闻推送",
"早报",
"晚报",
"新闻摘要",
"每日新闻",
"今日新闻",
"热榜",
"热搜",
"订阅新闻",
"突发新闻",
"话题追踪",
"微博热搜",
"知乎热榜",
"百度热搜",
"X热帖",
"科技新闻",
"财经新闻",
"AI早报",
"AI新闻",
"大模型动态",
"人工智能",
"军事新闻",
"头条",
"新闻聚合",
"资讯",
"快讯",
"要闻",
"新闻助手",
"新闻机器人",
"每日资讯",
"财经早报",
"深度解读",
"3分钟新闻",
"新闻自动化"
],
"author": "jiajiaoy",
"license": "MIT",
"scripts": {
"morning": "node scripts/morning-push.js",
"evening": "node scripts/evening-push.js",
"rss": "node scripts/rss-fetch.js",
"breaking": "node scripts/breaking-alert.js",
"register": "node scripts/register.js",
"preference": "node scripts/preference.js",
"push-on": "node scripts/push-toggle.js on",
"push-off": "node scripts/push-toggle.js off",
"push-status": "node scripts/push-toggle.js status"
}
}
FILE:scripts/breaking-alert.js
#!/usr/bin/env node
/**
* NewsToday — 突发新闻检测 prompt 生成器
* 由 openclaw cron 每 2 小时执行(08:00-22:00)
* 无文件 I/O:所有参数由 push-toggle.js 在设置 cron 时嵌入命令行。
* 只有检测到重大突发新闻时,Claude 才发送提醒(否则静默)。
*
* 用法:
* node breaking-alert.js [--lang zh|en] [--topics 科技,财经,国际]
*/
const ALLOWED_TOPICS = new Set(['科技','财经','国际','社会','娱乐','体育','tech','finance','international','society','entertainment','sports']);
const args = process.argv.slice(2);
const langIdx = args.indexOf('--lang');
const rawLang = langIdx !== -1 ? args[langIdx + 1] : null;
const lang = rawLang === 'en' ? 'en' : 'zh';
const topicsIdx = args.indexOf('--topics');
const rawTopics = topicsIdx !== -1 ? args[topicsIdx + 1] : null;
const topicList = rawTopics
? rawTopics.split(',').map(t => t.trim()).filter(t => ALLOWED_TOPICS.has(t))
: [];
const now = new Date();
const dateISO = `now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const timeStr = `String(now.getHours()).padStart(2,'0'):String(now.getMinutes()).padStart(2,'0')`;
if (lang === 'zh') {
const topicHint = topicList.length
? `用户重点关注领域:topicList.join('、'),优先检测这些领域的突发事件。`
: '';
console.log(`请检测当前是否有重大突发新闻,仅在有真正重要的突发事件时才发送提醒。
当前时间:dateISO timeStr
topicHint
检测步骤:
1. 搜索「突发新闻 dateISO」
2. 搜索「今日重大事件 最新」
3. 如话题包含财经,额外搜索「市场暴跌 OR 股市熔断 dateISO」
4. 如话题包含国际,额外搜索「国际突发 dateISO」
判断标准(满足以下任一条才发送提醒):
- 自然灾害:7级以上地震、大型台风、洪灾
- 重大事故:重大交通/安全事故,伤亡较大
- 金融市场:主要指数单日跌幅 >5%,或熔断
- 政治外交:重大政策发布、外交冲突升级
- 公共卫生:疫情爆发、重大食品安全事件
- 科技事件:重大数据泄露、主流平台大规模宕机
如果没有符合标准的突发事件:输出一个空行,不发送任何内容。
如果有符合标准的突发事件,按以下格式输出:
🚨 突发 · timeStr
━━━━━━━━━━━━━━━━━━━━━━━
[事件标题]
[3-4句描述:什么事、哪里、影响范围、最新进展]
📌 来源:[媒体名称]
💡 回复"追踪"获取持续更新`);
} else {
const topicHint = topicList.length
? `User's priority topics: topicList.join(', '). Focus detection on these areas.`
: '';
console.log(`Check for major breaking news right now. Only send an alert if there is a genuinely significant breaking event.
Current time: dateISO timeStr
topicHint
Detection steps:
1. Search "breaking news dateISO"
2. Search "major event today latest"
3. If topics include finance, also search "market crash OR circuit breaker dateISO"
4. If topics include international, also search "international breaking news dateISO"
Alert criteria (send only if at least one applies):
- Natural disaster: magnitude 7+ earthquake, major hurricane/typhoon, severe flooding
- Major accident: significant casualties in transport or industrial accident
- Financial markets: major index drops >5% in a day, or trading halt triggered
- Politics/diplomacy: major policy announcement, significant escalation
- Public health: disease outbreak, major food safety incident
- Tech: large-scale data breach, major platform outage
If no qualifying breaking event found: output a single blank line. Send nothing.
If a qualifying breaking event is found:
🚨 Breaking · timeStr
━━━━━━━━━━━━━━━━━━━━━━━
[Event headline]
[3–4 sentences: what happened, where, scale, latest update]
📌 Source: [media name]
💡 Reply "track" for continuous updates`);
}
FILE:scripts/evening-push.js
#!/usr/bin/env node
/**
* NewsToday — 晚间推送 prompt 生成器
* 由 openclaw cron 驱动,每日 20:00 执行
* 无文件 I/O:所有个性化参数由 push-toggle.js 在设置 cron 时嵌入命令行。
*
* 用法:
* node evening-push.js [--lang zh|en] [--topics 科技,财经,国际]
*/
const ALLOWED_TOPICS = new Set(['科技','财经','国际','社会','娱乐','体育','tech','finance','international','society','entertainment','sports']);
const args = process.argv.slice(2);
const langIdx = args.indexOf('--lang');
const rawLang = langIdx !== -1 ? args[langIdx + 1] : null;
const lang = rawLang === 'en' ? 'en' : 'zh';
const topicsIdx = args.indexOf('--topics');
const rawTopics = topicsIdx !== -1 ? args[topicsIdx + 1] : null;
const topicList = rawTopics
? rawTopics.split(',').map(t => t.trim()).filter(t => ALLOWED_TOPICS.has(t))
: [];
const now = new Date();
const tomorrow = new Date(now);
tomorrow.setDate(tomorrow.getDate() + 1);
const dateISO = `now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const tomorrowISO = `tomorrow.getFullYear()-String(tomorrow.getMonth()+1).padStart(2,'0')-String(tomorrow.getDate()).padStart(2,'0')`;
if (lang === 'zh') {
const WEEKDAYS = ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const dateStr = `now.getFullYear()年now.getMonth()+1月now.getDate()日 WEEKDAYS[now.getDay()]`;
const tomorrowStr = `tomorrow.getMonth()+1月tomorrow.getDate()日WEEKDAYS[tomorrow.getDay()]`;
const topicHint = topicList.length
? `\n用户重点关注:topicList.join('、'),收官和预告请侧重这些领域。`
: '';
console.log(`请生成今日晚间新闻汇总与明日预告。当前日期:dateStrtopicHint
执行步骤:
1. 搜索「今日晚间重要新闻 dateISO」
2. 搜索「今日热点事件最新进展」
3. 搜索「tomorrowISO 重要日程 财经 政治 体育」
输出格式:
🌙 晚间快报 · dateStr
━━━━━━━━━━━━━━━━━━━━━━━
📋 今日收官(3-5条下午/晚间重要新闻,每条附2句摘要及来源)
🔄 今日热点进展(1-2条今天持续发酵事件的最新动态)
📅 明日预告(tomorrowStr值得关注)
· 重要会议 / 政策发布 / 赛事
· 财经数据公布时间
· 预计有进展的持续事件
━━━━━━━━━━━━━━━━━━━━━━━
💡 回复序号深读 · 明日早报08:00见`);
} else {
const WEEKDAYS_EN = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
const MONTHS_EN = ['January','February','March','April','May','June','July','August','September','October','November','December'];
const dateStr = `WEEKDAYS_EN[now.getDay()], MONTHS_EN[now.getMonth()] now.getDate(), now.getFullYear()`;
const tomorrowStr = `WEEKDAYS_EN[tomorrow.getDay()], MONTHS_EN[tomorrow.getMonth()] tomorrow.getDate()`;
const topicHint = topicList.length
? `\nUser's priority topics: topicList.join(', ') — weight recap and preview toward these.`
: '';
console.log(`Please generate the evening news recap and tomorrow's preview. Date: dateStrtopicHint
Steps:
1. Search "top news this evening dateISO"
2. Search "today's major story latest update"
3. Search "tomorrowISO key events schedule finance politics sports"
Output format:
🌙 Evening Recap · dateStr
━━━━━━━━━━━━━━━━━━━━━━━
📋 Today's wrap-up (3–5 significant afternoon/evening stories, each with 2-sentence summary and source)
🔄 Developing stories (1–2 ongoing stories with latest updates)
📅 Tomorrow's preview (tomorrowStr)
· Key meetings / policy announcements / sports events
· Economic data releases
· Expected developments in ongoing stories
━━━━━━━━━━━━━━━━━━━━━━━
💡 Reply a number to deep-read · Morning briefing tomorrow at 8 AM`);
}
FILE:scripts/morning-push.js
#!/usr/bin/env node
/**
* NewsToday — 早报 prompt 生成器
* 由 openclaw cron 驱动,每日 08:00 执行
* 无文件 I/O:所有个性化参数由 push-toggle.js 在设置 cron 时嵌入命令行。
*
* 用法:
* node morning-push.js [--lang zh|en] [--topics 科技,财经,国际]
*/
// 允许的话题白名单
const ALLOWED_TOPICS = new Set(['科技','财经','国际','社会','娱乐','体育','tech','finance','international','society','entertainment','sports']);
const args = process.argv.slice(2);
const langIdx = args.indexOf('--lang');
const rawLang = langIdx !== -1 ? args[langIdx + 1] : null;
const lang = rawLang === 'en' ? 'en' : 'zh';
const topicsIdx = args.indexOf('--topics');
const rawTopics = topicsIdx !== -1 ? args[topicsIdx + 1] : null;
// 仅保留白名单内的话题,过滤任意外部输入
const topicList = rawTopics
? rawTopics.split(',').map(t => t.trim()).filter(t => ALLOWED_TOPICS.has(t))
: [];
const now = new Date();
const dateISO = `now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
if (lang === 'zh') {
const WEEKDAYS = ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const dateStr = `now.getFullYear()年now.getMonth()+1月now.getDate()日 WEEKDAYS[now.getDay()]`;
const topicHint = topicList.length
? `\n用户重点关注:topicList.join('、')(优先多选这些领域的新闻)。`
: '';
console.log(`请生成今日个性化早报。当前日期:dateStrtopicHint
信息来源(按优先级):
1. 【WebSearch 主源】
- 搜索「今日重要新闻 dateISO」
- 搜索「今日国际新闻 dateISO」
- 搜索「今日财经新闻 dateISO」
2. 【RSS 补充】可运行 node scripts/rss-fetch.js --lang zh 获取 RSS 源列表
处理要求:
- 去重后选取 10 条,覆盖不同领域(重要/财经/国际/科技/社会各至少 1 条)
- 第1条为【头条】:重要性最高的单条新闻,含标题、来源、时间、3-4句详细摘要、影响分析
- 其余9条为常规条目:标题、来源媒体、发布时间、2句摘要
- 财经类每条额外标注影响评级:📈 利好 / 📉 利空 / ➡️ 中性
- 按领域分组,每条标注话题标签
- 有争议内容保持中立,标注多方视角
- 统计总条数和预估阅读时长(每条约30秒)
输出格式:
📰 今日早报 · dateStr | 10条 · 阅读约5分钟
━━━━━━━━━━━━━━━━━━━━━━━
🔥 头条
[头条新闻 — 标题加粗,3-4句详细摘要,含影响分析]
━━━━━━━━━━━━━━━━━━━━━━━
🔴 重要
[新闻条目]
💰 财经 (每条含 📈/📉/➡️ 评级)
[新闻条目]
🌍 国际
[新闻条目]
💻 科技
[新闻条目]
🏙️ 社会
[新闻条目]
━━━━━━━━━━━━━━━━━━━━━━━
💡 回复序号深读 · 回复"热榜"查看实时热搜 · 晚报20:00见`);
} else {
const WEEKDAYS_EN = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
const MONTHS_EN = ['January','February','March','April','May','June','July','August','September','October','November','December'];
const dateStr = `WEEKDAYS_EN[now.getDay()], MONTHS_EN[now.getMonth()] now.getDate(), now.getFullYear()`;
const topicHint = topicList.length
? `\nUser's priority topics: topicList.join(', ') — prefer these categories.`
: '';
console.log(`Please generate today's personalized morning news briefing. Date: dateStrtopicHint
Sources (in priority order):
1. [WebSearch main]
- Search "top news today dateISO"
- Search "international news today dateISO"
- Search "financial news today dateISO"
2. [RSS supplement] Run node scripts/rss-fetch.js --lang en for RSS feed list
Requirements:
- After deduplication, select 10 stories covering different categories
- Story #1 is the [Lead Story]: highest-importance single item, with headline, source, time, 3-4 sentence detailed summary, and impact analysis
- Remaining 9 are standard entries: headline, source, publish time, 2-sentence summary
- Finance entries must include an impact tag: 📈 bullish / 📉 bearish / ➡️ neutral
- Group by category, tag each story
- Present disputed topics neutrally with multiple perspectives
- Count total stories and estimate reading time (~30 sec per story)
Output format:
📰 Morning Briefing · dateStr | 10 stories · ~5 min read
━━━━━━━━━━━━━━━━━━━━━━━
🔥 Lead Story
[Top story — bold headline, 3-4 sentence detailed summary with impact analysis]
━━━━━━━━━━━━━━━━━━━━━━━
🔴 Top Stories
[entries]
💰 Finance (each with 📈/📉/➡️ rating)
[entries]
🌍 International
[entries]
💻 Tech
[entries]
🏙️ Society
[entries]
━━━━━━━━━━━━━━━━━━━━━━━
💡 Reply a number to deep-read · Reply "trending" for hot topics · Evening recap at 8 PM`);
}
FILE:scripts/preference.js
#!/usr/bin/env node
/**
* NewsToday — 话题偏好管理
*
* 用法:
* node preference.js show <userId> 查看当前偏好
* node preference.js set <userId> <话题> <权重> 设置话题权重(0.0 - 1.0)
* node preference.js reset <userId> 重置为默认偏好
*
* 话题: 科技 财经 国际 社会 娱乐 体育
* 权重: 0.0(不感兴趣)~ 1.0(最感兴趣)
*/
const fs = require('fs');
const path = require('path');
const USERS_DIR = path.join(__dirname, '../data/users');
const ALLOWED_TOPICS = ['科技', '财经', '国际', '社会', '娱乐', '体育'];
const TOPIC_MAP = { tech: '科技', finance: '财经', international: '国际', society: '社会', entertainment: '娱乐', sports: '体育' };
const DEFAULT_TOPICS = { 科技: 0.8, 财经: 0.8, 国际: 0.7, 社会: 0.6, 娱乐: 0.3, 体育: 0.3 };
function sanitizeId(value) {
if (typeof value !== 'string' || !/^[a-zA-Z0-9_-]{1,128}$/.test(value)) {
console.error('❌ 无效的 userId');
process.exit(1);
}
return value;
}
function safeUserPath(userId) {
const resolved = path.resolve(USERS_DIR, `userId.json`);
if (!resolved.startsWith(path.resolve(USERS_DIR) + path.sep)) {
console.error('❌ 非法路径');
process.exit(1);
}
return resolved;
}
function loadUser(userId) {
const f = safeUserPath(userId);
if (!fs.existsSync(f)) {
console.error(`❌ 未找到用户 userId,请先运行: node register.js userId`);
process.exit(1);
}
return JSON.parse(fs.readFileSync(f, 'utf8'));
}
function saveUser(userId, data) {
fs.writeFileSync(safeUserPath(userId), JSON.stringify(data, null, 2), 'utf8');
}
function bar(weight) {
const filled = Math.round(weight * 10);
return '█'.repeat(filled) + '░'.repeat(10 - filled);
}
const args = process.argv.slice(2);
const command = args[0];
const userId = args[1] ? sanitizeId(args[1]) : null;
if (!command || !userId) {
console.log(`用法:
node preference.js show <userId>
node preference.js set <userId> <话题> <权重0-1>
node preference.js reset <userId>`);
process.exit(1);
}
switch (command) {
case 'show': {
const user = loadUser(userId);
const topics = user.topics || DEFAULT_TOPICS;
console.log(`\n📌 话题偏好 — userId\n'━'.repeat(30)`);
for (const t of ALLOWED_TOPICS) {
const w = topics[t] ?? 0.5;
console.log(` t.padEnd(4) bar(w) (w * 10).toFixed(0)/10`);
}
console.log('━'.repeat(30));
console.log(`语言:'中文' 渠道:user.channel || 'telegram'`);
break;
}
case 'set': {
const rawTopic = args[2];
const rawWeight = args[3];
if (!rawTopic || rawWeight === undefined) {
console.error('用法: node preference.js set <userId> <话题> <权重0-1>');
process.exit(1);
}
const topic = TOPIC_MAP[rawTopic] || rawTopic;
if (!ALLOWED_TOPICS.includes(topic)) {
console.error(`❌ 无效话题:rawTopic。可用:ALLOWED_TOPICS.join(', ')`);
process.exit(1);
}
const weight = parseFloat(rawWeight);
if (isNaN(weight) || weight < 0 || weight > 1) {
console.error('❌ 权重须在 0.0 ~ 1.0 之间');
process.exit(1);
}
const user = loadUser(userId);
user.topics = user.topics || { ...DEFAULT_TOPICS };
user.topics[topic] = weight;
user.updatedAt = new Date().toISOString();
saveUser(userId, user);
console.log(`✅ 已设置「topic」权重为 weight.toFixed(1)`);
break;
}
case 'reset': {
const user = loadUser(userId);
user.topics = { ...DEFAULT_TOPICS };
user.updatedAt = new Date().toISOString();
saveUser(userId, user);
console.log(`✅ 话题偏好已重置为默认值`);
break;
}
default:
console.error(`❌ 未知命令: command`);
process.exit(1);
}
FILE:scripts/push-toggle.js
#!/usr/bin/env node
/**
* NewsToday — 推送开关
*
* 用法:
* node push-toggle.js on <userId> 开启推送
* node push-toggle.js off <userId> 关闭推送
* node push-toggle.js status <userId> 查看状态
*
* 选项:
* --morning HH:MM 早报时间(默认 08:00)
* --evening HH:MM 晚报时间(默认 20:00)
* --channel <name> 推送渠道(默认 telegram)
*/
const fs = require('fs');
const path = require('path');
const USERS_DIR = path.join(__dirname, '../data/users');
// 只允许字母、数字、连字符、下划线,最长 128 字符
function sanitizeId(value, label) {
if (typeof value !== 'string' || !/^[a-zA-Z0-9_-]{1,128}$/.test(value)) {
console.error(`❌ 无效的 label:只允许字母、数字、- 和 _,长度 1-128`);
process.exit(1);
}
return value;
}
// 校验 HH:MM 格式,返回 { h, m } 整数
function sanitizeTime(value, label) {
if (typeof value !== 'string' || !/^\d{1,2}:\d{2}$/.test(value)) {
console.error(`❌ 无效的 label:格式应为 HH:MM,如 08:00`);
process.exit(1);
}
const [h, m] = value.split(':').map(Number);
if (h < 0 || h > 23 || m < 0 || m > 59) {
console.error(`❌ 无效的 label:小时 0-23,分钟 0-59`);
process.exit(1);
}
return { h, m };
}
// 验证文件路径确实在 USERS_DIR 内(防路径穿越)
function safeUserPath(userId) {
const resolved = path.resolve(USERS_DIR, `userId.json`);
if (!resolved.startsWith(path.resolve(USERS_DIR) + path.sep)) {
console.error('❌ 非法路径');
process.exit(1);
}
return resolved;
}
function loadUser(userId) {
const f = safeUserPath(userId);
if (!fs.existsSync(f)) return null;
return JSON.parse(fs.readFileSync(f, 'utf8'));
}
function saveUser(userId, data) {
fs.mkdirSync(USERS_DIR, { recursive: true });
fs.writeFileSync(safeUserPath(userId), JSON.stringify(data, null, 2), 'utf8');
}
const ALLOWED_CHANNELS = new Set(['telegram', 'feishu', 'slack', 'discord']);
function enablePush(userId, opts = {}) {
userId = sanitizeId(userId, 'userId');
// 一次性读取用户档案,仅提取原始标量值用于构建 cron 命令
// push 脚本本身不读文件,偏好通过 CLI 参数传入
const profile = loadUser(userId);
const defaultChannel = profile?.channel || 'telegram';
const defaultTz = 'Asia/Shanghai'; // 不从文件读取 tz,避免不可信数据进入 cron 配置
// 从档案中提取 lang(仅允许 zh/en)和 topics(仅白名单话题名)
const ALLOWED_TOPICS = new Set(['科技','财经','国际','社会','娱乐','体育']);
const profileLang = profile?.language === 'en' ? 'en' : 'zh';
const profileTopics = profile?.topics
? Object.entries(profile.topics)
.filter(([t, w]) => ALLOWED_TOPICS.has(t) && typeof w === 'number' && w >= 0.7)
.map(([t]) => t)
.join(',')
: '';
// 构建 push 脚本的 CLI 参数(lang 和 topics 均经过白名单过滤)
const pushArgs = `--lang profileLangprofileTopics ? ` --topics ${profileTopics` : ''}`;
const { h: mh, m: mm } = sanitizeTime(opts.morning || '08:00', 'morning');
const { h: eh, m: em } = sanitizeTime(opts.evening || '20:00', 'evening');
const rawChannel = opts.channel || defaultChannel;
if (!ALLOWED_CHANNELS.has(rawChannel)) {
console.error(`❌ 不支持的渠道:rawChannel。支持:[...ALLOWED_CHANNELS].join(', ')`);
process.exit(1);
}
const channel = rawChannel;
const morningCron = `mm mh * * *`;
const eveningCron = `em eh * * *`;
const sessionKey = `agent:main:channel:direct:userId`;
// 早报 cron(lang/topics 已嵌入命令,push 脚本无需再读文件)
const morningConfig = {
name: `newstoday-morning-userId`,
cronExpr: morningCron,
tz: defaultTz,
session: 'isolated',
sessionKey,
channel,
to: userId,
announce: true,
timeoutSeconds: 120,
message: `node path.join(__dirname, 'morning-push.js') pushArgs`
};
console.log(`__OPENCLAW_CRON_ADD__:JSON.stringify(morningConfig)`);
// 晚报 cron
const eveningConfig = {
name: `newstoday-evening-userId`,
cronExpr: eveningCron,
tz: defaultTz,
session: 'isolated',
sessionKey,
channel,
to: userId,
announce: true,
timeoutSeconds: 120,
message: `node path.join(__dirname, 'evening-push.js') pushArgs`
};
console.log(`__OPENCLAW_CRON_ADD__:JSON.stringify(eveningConfig)`);
// 突发新闻检测 cron(每2小时,08:00-22:00)
const breakingConfig = {
name: `newstoday-breaking-userId`,
cronExpr: '0 8,10,12,14,16,18,20,22 * * *',
tz: defaultTz,
session: 'isolated',
sessionKey,
channel,
to: userId,
announce: false,
timeoutSeconds: 60,
message: `node path.join(__dirname, 'breaking-alert.js') pushArgs`
};
console.log(`__OPENCLAW_CRON_ADD__:JSON.stringify(breakingConfig)`);
const morningDisplay = `String(mh).padStart(2,'0'):String(mm).padStart(2,'0')`;
const eveningDisplay = `String(eh).padStart(2,'0'):String(em).padStart(2,'0')`;
// 更新用户档案中的推送状态(合并,不覆盖话题偏好等字段)
const updated = {
...(profile || {}),
userId,
channel,
push: { enabled: true, morningTime: morningDisplay, eveningTime: eveningDisplay, enabledAt: new Date().toISOString() },
updatedAt: new Date().toISOString()
};
saveUser(userId, updated);
console.log(`
✅ 每日推送已开启
⏰ 早报:每天 morningDisplay(个性化10条要闻 + RSS)
🌙 晚报:每天 eveningDisplay(收官 + 明日预告)
🚨 突发:每2小时检测(08:00-22:00,有重大事件才提醒)
📡 渠道:channel
关闭推送:node push-toggle.js off userId`);
}
function disablePush(userId) {
userId = sanitizeId(userId, 'userId');
const user = loadUser(userId);
if (!user) {
console.log(`❌ 未找到用户 userId 的推送记录`);
return;
}
console.log(`__OPENCLAW_CRON_RM__:newstoday-morning-userId`);
console.log(`__OPENCLAW_CRON_RM__:newstoday-evening-userId`);
console.log(`__OPENCLAW_CRON_RM__:newstoday-breaking-userId`);
const updated = { ...user, push: { ...(user.push || {}), enabled: false, disabledAt: new Date().toISOString() }, updatedAt: new Date().toISOString() };
saveUser(userId, updated);
console.log(`✅ 推送已关闭`);
}
function showStatus(userId) {
userId = sanitizeId(userId, 'userId');
const user = loadUser(userId);
if (!user) {
console.log(`❌ 未找到用户 userId 的推送记录`);
return;
}
const push = user.push || {};
const topTopics = Object.entries(user.topics || {})
.filter(([,w]) => w >= 0.7).map(([t]) => t).join('、') || '默认';
console.log(`
📡 推送状态 — userId
━━━━━━━━━━━━━━━━━━━━━━━
状态:'❌ 已关闭'
早报:00'
晚报:00'
突发:每2小时检测(重大事件才提醒)
渠道:user.channel || 'telegram'
语言:'中文'
重点话题:topTopics
开启于:'未知'
━━━━━━━━━━━━━━━━━━━━━━━`);
}
module.exports = { enablePush, disablePush, showStatus };
if (require.main !== module) return;
const args = process.argv.slice(2);
const command = args[0];
const userId = args[1];
if (!command || !userId) {
console.log(`用法:
node push-toggle.js on <userId> [--morning 08:00] [--evening 20:00] [--channel telegram]
node push-toggle.js off <userId>
node push-toggle.js status <userId>`);
process.exit(1);
}
const opts = {};
const mi = args.indexOf('--morning');
if (mi !== -1) opts.morning = args[mi + 1];
const ei = args.indexOf('--evening');
if (ei !== -1) opts.evening = args[ei + 1];
const ci = args.indexOf('--channel');
if (ci !== -1) opts.channel = args[ci + 1];
switch (command) {
case 'on': enablePush(userId, opts); break;
case 'off': disablePush(userId); break;
case 'status': showStatus(userId); break;
default:
console.log(`❌ 未知命令: command`);
process.exit(1);
}
FILE:scripts/register.js
#!/usr/bin/env node
/**
* NewsToday — 用户注册 / 偏好设置
*
* 用法:
* node register.js <userId> [language] [topics] [channel]
*
* 参数:
* userId 必填,字母/数字/-/_,1-128 字符
* language 可选,zh(默认)或 en
* topics 可选,逗号分隔的偏好话题(如 科技,财经,国际)
* 可选值: 科技 财经 国际 社会 娱乐 体育
* channel 可选,telegram/feishu/slack/discord(默认 telegram)
*
* 示例:
* node register.js alice
* node register.js bob zh 科技,财经,国际
* node register.js carol en tech,finance,international telegram
*/
const fs = require('fs');
const path = require('path');
const USERS_DIR = path.join(__dirname, '../data/users');
const ALLOWED_TOPICS_ZH = ['科技', '财经', '国际', '社会', '娱乐', '体育'];
const ALLOWED_TOPICS_EN = ['tech', 'finance', 'international', 'society', 'entertainment', 'sports'];
const TOPIC_MAP = { tech: '科技', finance: '财经', international: '国际', society: '社会', entertainment: '娱乐', sports: '体育' };
const ALLOWED_CHANNELS = new Set(['telegram', 'feishu', 'slack', 'discord']);
function sanitizeId(value) {
if (typeof value !== 'string' || !/^[a-zA-Z0-9_-]{1,128}$/.test(value)) {
console.error('❌ 无效的 userId:只允许字母、数字、- 和 _,长度 1-128');
process.exit(1);
}
return value;
}
function safeUserPath(userId) {
const resolved = path.resolve(USERS_DIR, `userId.json`);
if (!resolved.startsWith(path.resolve(USERS_DIR) + path.sep)) {
console.error('❌ 非法路径');
process.exit(1);
}
return resolved;
}
function sanitizeLanguage(value) {
if (value !== 'zh' && value !== 'en') {
console.error('❌ 无效的语言:请使用 zh 或 en');
process.exit(1);
}
return value;
}
function sanitizeTopics(value, language) {
const allowed = language === 'en' ? ALLOWED_TOPICS_EN : ALLOWED_TOPICS_ZH;
const raw = value.split(',').map(t => t.trim()).filter(Boolean);
const weights = {};
for (const t of raw) {
const mapped = TOPIC_MAP[t] || t;
if (!ALLOWED_TOPICS_ZH.includes(mapped)) {
console.error(`❌ 无效的话题:t。可用值:[...ALLOWED_TOPICS_ZH, ...ALLOWED_TOPICS_EN].join(', ')`);
process.exit(1);
}
weights[mapped] = 1.0;
}
// 未指定的话题给默认权重 0.5
for (const t of ALLOWED_TOPICS_ZH) {
if (!(t in weights)) weights[t] = 0.5;
}
return weights;
}
const DEFAULT_TOPICS = { 科技: 0.8, 财经: 0.8, 国际: 0.7, 社会: 0.6, 娱乐: 0.3, 体育: 0.3 };
const args = process.argv.slice(2);
if (!args[0]) {
console.log(`用法:
node register.js <userId> [language] [topics] [channel]
参数:
userId 字母/数字/-/_,1-128 字符
language zh(默认)或 en
topics 逗号分隔偏好话题(如 科技,财经,国际)
channel telegram/feishu/slack/discord(默认 telegram)
示例:
node register.js alice
node register.js bob zh 科技,财经,国际
node register.js carol en tech,finance,international`);
process.exit(1);
}
const userId = sanitizeId(args[0]);
const language = args[1] ? sanitizeLanguage(args[1]) : 'zh';
const topics = args[2] ? sanitizeTopics(args[2], language) : { ...DEFAULT_TOPICS };
const rawCh = args[3] || 'telegram';
if (!ALLOWED_CHANNELS.has(rawCh)) {
console.error(`❌ 无效渠道:rawCh。支持:[...ALLOWED_CHANNELS].join(', ')`);
process.exit(1);
}
const channel = rawCh;
fs.mkdirSync(USERS_DIR, { recursive: true });
const filePath = safeUserPath(userId);
const now = new Date().toISOString();
let existing = null;
if (fs.existsSync(filePath)) {
try { existing = JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch (_) {}
}
const profile = {
userId,
language,
topics,
channel,
push: existing?.push || { enabled: false },
createdAt: existing?.createdAt || now,
updatedAt: now
};
fs.writeFileSync(filePath, JSON.stringify(profile, null, 2), 'utf8');
const topList = Object.entries(topics).filter(([,w]) => w >= 0.7).map(([t]) => t).join('、');
console.log(`
✅ 注册成功
👤 用户:userId
🌐 语言:'English'
📌 重点话题:topList || '默认'
📡 推送渠道:channel
下一步:
调整话题偏好:node scripts/preference.js set userId <话题> <权重0-1>
开启每日推送:node scripts/push-toggle.js on userId
获取今日早报:node scripts/morning-push.js userId`);
FILE:scripts/rss-fetch.js
#!/usr/bin/env node
/**
* NewsToday — RSS 聚合 prompt 生成器
* 纯 CLI 参数输入 → stdout 输出,无任何文件 I/O 或网络调用。
* 所有 URL 均为硬编码公开地址,不受外部输入影响。
*
* 用法:
* node rss-fetch.js [--lang zh|en] [--topics 科技,财经,国际]
*
* 示例:
* node rss-fetch.js --lang zh --topics 科技,财经
* node rss-fetch.js --lang en --topics tech,finance
* node rss-fetch.js # 使用默认值(中文,全部话题)
*/
// 全部 RSS 源均为硬编码公开地址,不受任何外部输入影响
const RSS_SOURCES_ZH = {
综合: [
{ name: '新浪新闻头条', url: 'https://rss.sina.com.cn/news/china/focus15.xml' },
{ name: '澎湃新闻', url: 'https://www.thepaper.cn/rss_promotion.jsp' },
],
科技: [
{ name: '36氪', url: 'https://36kr.com/feed' },
{ name: '少数派', url: 'https://sspai.com/feed' },
],
财经: [
{ name: '华尔街见闻', url: 'https://wallstreetcn.com/rss' },
],
国际: [
{ name: 'BBC中文', url: 'https://feeds.bbci.co.uk/zhongwen/simp/rss.xml' },
{ name: 'Reuters中文', url: 'https://cn.reuters.com/rssFeed/CNTopNews' },
],
};
const RSS_SOURCES_EN = {
general: [
{ name: 'Reuters', url: 'https://feeds.reuters.com/reuters/topNews' },
{ name: 'BBC News', url: 'http://feeds.bbci.co.uk/news/rss.xml' },
{ name: 'AP News', url: 'https://rsshub.app/apnews/topics/apf-topnews' },
],
tech: [
{ name: 'Hacker News', url: 'https://news.ycombinator.com/rss' },
{ name: 'TechCrunch', url: 'https://techcrunch.com/feed/' },
],
finance: [
{ name: 'Financial Times', url: 'https://www.ft.com/rss/home' },
{ name: 'Bloomberg', url: 'https://feeds.bloomberg.com/markets/news.rss' },
],
};
// 允许的话题白名单(防止任意字符串进入输出)
const ALLOWED_ZH = new Set(['综合', '科技', '财经', '国际', '社会', '娱乐', '体育']);
const ALLOWED_EN = new Set(['general', 'tech', 'finance', 'international', 'society', 'entertainment', 'sports']);
const EN_TO_ZH_KEY = { tech: '科技', finance: '财经', international: '国际', general: '综合' };
// --- 解析 CLI 参数(仅接受 --lang 和 --topics)---
const args = process.argv.slice(2);
const langIdx = args.indexOf('--lang');
const rawLang = langIdx !== -1 ? args[langIdx + 1] : null;
const lang = rawLang === 'en' ? 'en' : 'zh'; // 仅允许 zh/en,其余默认 zh
const topicsIdx = args.indexOf('--topics');
const rawTopics = topicsIdx !== -1 ? args[topicsIdx + 1] : null;
// 将 --topics 参数解析为白名单内的话题集合
const allowedSet = lang === 'en' ? ALLOWED_EN : ALLOWED_ZH;
let requestedTopics = null; // null = 全部
if (rawTopics) {
requestedTopics = new Set(
rawTopics.split(',')
.map(t => t.trim())
.filter(t => allowedSet.has(t))
);
}
// --- 从硬编码常量中选取 RSS 源 ---
const sources = lang === 'en' ? RSS_SOURCES_EN : RSS_SOURCES_ZH;
const generalKey = lang === 'en' ? 'general' : '综合';
const selected = [];
for (const [key, feeds] of Object.entries(sources)) {
if (key === generalKey) {
selected.push(...feeds); // 综合/general 始终包含
continue;
}
// 无过滤要求,或该话题在请求列表中
if (!requestedTopics) {
selected.push(...feeds);
} else {
const zhKey = lang === 'en' ? (EN_TO_ZH_KEY[key] ?? key) : key;
if (requestedTopics.has(key) || requestedTopics.has(zhKey)) {
selected.push(...feeds);
}
}
}
// --- 输出 prompt ---
const now = new Date();
const dateISO = `now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
if (lang === 'zh') {
const sourceList = selected.map(s => ` - s.name:s.url`).join('\n');
console.log(`请通过 WebFetch 获取以下 RSS 源的最新内容,汇总今日(dateISO)重要新闻。
RSS 源列表:
sourceList
处理步骤:
1. 依次 WebFetch 以上每个 URL,获取 XML 内容
2. 从每个源提取最新 3-5 条标题和摘要(优先今日内容)
3. 全部去重合并,按新闻价值排序,选取 10 条
4. 每条输出:标题、来源、发布时间、2 句中文摘要
注意:
- 若某 RSS URL 无法访问,跳过并继续其他源
- 去除广告、软文、纯娱乐八卦内容
- 有争议内容保持中立,不做立场判断`);
} else {
const sourceList = selected.map(s => ` - s.name: s.url`).join('\n');
console.log(`Please WebFetch the following RSS feeds and compile today's (dateISO) top news.
RSS sources:
sourceList
Steps:
1. WebFetch each URL above to get the XML content
2. Extract the latest 3–5 headlines and summaries from each (prefer today's content)
3. Deduplicate and merge all results, rank by news value, pick top 10
4. For each item output: headline, source, publish time, 2-sentence English summary
Notes:
- If a URL is unreachable, skip and continue with others
- Filter out ads, sponsored content, and pure celebrity gossip
- Present disputed topics neutrally without taking sides`);
}
Sleep better tonight — personalized wind-down routine, breathing exercises, bedtime stories, and sleep hygiene tips. Daily evening push for better rest.
---
name: daily-sleep
description: "Sleep better tonight — personalized wind-down routine, breathing exercises, bedtime stories, and sleep hygiene tips. Daily evening push for better rest."
keywords:
- 失眠
- 睡不好
- 助眠
- 睡眠
- 睡前放松
- 入睡困难
- 睡眠质量
- 帮我睡觉
- 睡眠问题
- 深度睡眠
- 睡前仪式
- 睡眠卫生
- 午睡
- 亚健康
- 睡眠不足
- 早睡早起
- 睡眠改善
- sleep
- insomnia
- sleep aid
- bedtime routine
- sleep quality
- can't sleep
- deep sleep
- sleep hygiene
- sleep coach
- wind down
- relaxation
- sleep tips
- better sleep
- nap
- wake up routine
- morning routine
metadata:
openclaw:
runtime:
node: ">=18"
---
# 睡眠助手
> 睡眠助手 — 晨间唤醒 · 睡前放松 · 助眠引导 · 失眠改善
## 何时使用
- 用户说"失眠""睡不好""帮我睡觉"
- 用户说"睡前放松""助眠""入睡困难"
- 用户说"sleep""insomnia""can't sleep"
- 用户说"睡眠质量差""怎么改善睡眠"
---
## 推送管理
```bash
node scripts/push-toggle.js on <userId>
node scripts/push-toggle.js on <userId> --morning 07:00 --evening 22:00 --channel feishu
node scripts/push-toggle.js off <userId>
node scripts/push-toggle.js status <userId>
```
支持渠道:`telegram` / `feishu` / `slack` / `discord`
FILE:README.md
# Daily Sleep — Sleep Coach Skill
> Morning wake-up routine + evening wind-down — science-backed tips for better sleep and insomnia relief.
[](https://clawhub.ai/skills/daily-sleep)
[](https://openclaw.ai)
An [OpenClaw](https://openclaw.ai) skill that acts as your personal sleep coach — a morning wake-up routine to start the day energized, and an evening wind-down program to fall asleep naturally. Covers the largest health pain point: insomnia and poor sleep quality.
---
## Features
- **Morning Wake-Up** — Sleep quality self-review, 5-min activation routine, caffeine cutoff reminder
- **Evening Wind-Down** — Pre-sleep 90-min checklist, 4-7-8 breathing, progressive muscle relaxation
- **Sleep Environment Guide** — Temperature, light, noise, phone tips
- **2-Minute Bedtime Meditation** — Guided mindfulness for falling asleep
- **Daily Sleep Tips** — Circadian rhythm advice by day of week
- **Science-based** — Draws from CBT-I, sleep hygiene research, and mindfulness
---
## Daily Push Schedule
| Push | Time | Content |
|------|------|---------|
| Morning | 07:00 | Wake-up routine + sleep quality review + today's sleep tip |
| Evening | 22:00 | Wind-down program + breathing guide + bedtime meditation |
```bash
node scripts/push-toggle.js on <userId>
node scripts/push-toggle.js on <userId> --morning 07:00 --evening 22:00 --channel telegram
node scripts/push-toggle.js off <userId>
node scripts/push-toggle.js status <userId>
```
Supported channels: `telegram` / `feishu` / `slack` / `discord`
---
## Trigger Words
失眠、睡不好、助眠、睡前放松、入睡困难、睡眠质量、sleep、insomnia、sleep aid、bedtime routine、can't sleep、deep sleep、sleep hygiene、wind down
---
*MIT License · OpenClaw Skill*
## Keywords
sleep · insomnia · sleep aid · bedtime routine · daily sleep · sleep coach · 失眠 · 睡不好 · 助眠 · 睡前放松 · 睡眠质量 · 入睡困难
---
Built for [OpenClaw](https://openclaw.ai) · Published on [clawhub.ai](https://clawhub.ai/skills/daily-sleep)
FILE:_meta.json
{
"slug": "daily-sleep",
"version": "1.0.0",
"runtime": {
"node": ">=18"
},
"ownerId": "kn79bebfnwg15sb0g7cj5z5nyd83gxh0"
}
FILE:package.json
{
"name": "daily-sleep",
"version": "1.0.2",
"description": "Sleep better tonight — personalized wind-down routine, breathing exercises, bedtime stories, and sleep hygiene tips. Daily evening push for better rest.",
"engines": {
"node": ">=18"
},
"license": "MIT"
}
FILE:scripts/evening-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('invalid userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('illegal path');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{};}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
console.log(`睡前放松时间🌙(date)。请生成今晚睡眠引导程序:①睡前90分钟清单(该做/不该做各3条)②今晚放松程序(4-7-8呼吸引导+渐进式肌肉放松,3步)③睡眠环境检查(温度/光线/噪音/手机各1条建议)④今晚助眠冥想引导(2分钟正念入睡引导语)⑤明日唤醒预告(提醒明天最重要的1件事,放下,安心入睡)。语气极度舒缓,帮助用户自然入睡。`);
FILE:scripts/morning-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('invalid userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('illegal path');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{};}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
console.log(`早安!今天是weekday(date)。请生成晨间唤醒引导:①昨晚睡眠质量自评引导(3个问题:入睡时间/醒来次数/精神状态)②今日唤醒程序(5分钟晨间激活:呼吸+拉伸+光线建议)③今日睡眠小贴士1条(针对weekday的节律建议)④今日咖啡因截止时间提醒(根据最佳睡眠时间倒推)。语气积极温暖,帮助用户精力满满开始新的一天。`);
FILE:scripts/push-toggle.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const SKILL='daily-sleep',DEFAULT_MORNING='07:00',DEFAULT_EVENING='22:00';
const USERS_DIR=path.join(__dirname,'../data/users');
const ALLOWED_CH=new Set(['telegram','feishu','slack','discord']);
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('invalid userId');process.exit(1);}return v;}
function sanitizeTime(v,l){if(!/^\d{1,2}:\d{2}$/.test(v)){console.error('invalid '+l);process.exit(1);}const[h,m]=v.split(':').map(Number);if(h>23||m>59){console.error('invalid '+l);process.exit(1);}return{h,m};}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('illegal path');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{};}
function saveUser(u,d){fs.mkdirSync(USERS_DIR,{recursive:true});fs.writeFileSync(safeUserPath(u),JSON.stringify(d,null,2),'utf8');}
function enablePush(userId,opts){
userId=sanitizeId(userId);
const user=loadUser(userId);
const mt=opts.morning||user.morningTime||DEFAULT_MORNING;
const et=opts.evening||user.eveningTime||DEFAULT_EVENING;
const{h:mh,m:mm}=sanitizeTime(mt,'--morning');
const{h:eh,m:em}=sanitizeTime(et,'--evening');
const ch=opts.channel||user.channel||'telegram';
if(!ALLOWED_CH.has(ch)){console.error('unsupported channel:'+ch);process.exit(1);}
const sk=`agent:main:ch:direct:userId`;
console.log('__OPENCLAW_CRON_ADD__:'+JSON.stringify({name:`SKILL-morning-userId`,cronExpr:`mm mh * * *`,tz:'Asia/Shanghai',session:'isolated',sessionKey:sk,channel:ch,to:userId,announce:true,timeoutSeconds:180,message:`node path.join(__dirname,'morning-push.js') userId`}));
console.log('__OPENCLAW_CRON_ADD__:'+JSON.stringify({name:`SKILL-evening-userId`,cronExpr:`em eh * * *`,tz:'Asia/Shanghai',session:'isolated',sessionKey:sk,channel:ch,to:userId,announce:true,timeoutSeconds:180,message:`node path.join(__dirname,'evening-push.js') userId`}));
saveUser(userId,{...user,morningTime:mt,eveningTime:et,channel:ch,pushEnabled:true,updatedAt:new Date().toISOString()});
console.log(`\n✅ SKILL 推送已开启\n⏰ 早推: mt 🌙 晚推: et 📡 渠道: ch\n关闭: node push-toggle.js off userId`);
}
function disablePush(userId){
userId=sanitizeId(userId);
console.log(`__OPENCLAW_CRON_RM__:SKILL-morning-userId`);
console.log(`__OPENCLAW_CRON_RM__:SKILL-evening-userId`);
saveUser(userId,{...loadUser(userId),pushEnabled:false,updatedAt:new Date().toISOString()});
console.log(`✅ SKILL 推送已关闭`);
}
function showStatus(userId){
userId=sanitizeId(userId);
const u=loadUser(userId);
console.log(`\n📡 SKILL — userId\n状态: '❌ 关闭' 早推: u.morningTime||DEFAULT_MORNING 晚推: u.eveningTime||DEFAULT_EVENING 渠道: u.channel||'telegram'\n`);
}
if(require.main!==module)return;
const[cmd,uid,...rest]=process.argv.slice(2);
if(!cmd||!uid){console.log('Usage: node push-toggle.js on|off|status <userId>');process.exit(1);}
const opts={};
const mi=rest.indexOf('--morning');if(mi!==-1)opts.morning=rest[mi+1];
const ei=rest.indexOf('--evening');if(ei!==-1)opts.evening=rest[ei+1];
const ci=rest.indexOf('--channel');if(ci!==-1)opts.channel=rest[ci+1];
if(cmd==='on')enablePush(uid,opts);
else if(cmd==='off')disablePush(uid);
else if(cmd==='status')showStatus(uid);
else{console.error('unknown cmd:'+cmd);process.exit(1);}
Daily journaling prompts for meaningful reflection — morning intention-setting and evening review. Build a consistent self-reflection habit one prompt at a t...
---
name: daily-reflect
description: "Daily journaling prompts for meaningful reflection — morning intention-setting and evening review. Build a consistent self-reflection habit one prompt at a time."
keywords:
- 日记
- 写日记
- 今天写什么
- 每日日记
- 日记打卡
- 自我反思
- 情绪日记
- 成长日记
- 感恩日记
- 晨间日记
- 晚间日记
- 日记引导
- 五年日记
- 每日复盘
- journal
- daily journal
- journaling
- writing prompt
- morning journal
- evening reflection
- gratitude journal
- self-reflection
- mindfulness journal
- bullet journal
- diary
- daily writing
- mood journal
- growth journal
metadata:
openclaw:
runtime:
node: ">=18"
---
# 每日日记引导
> 每日日记引导 — 晨间写作 · 晚间反思 · 情绪觉察 · 习惯打卡
## 何时使用
- 用户说"写日记""今天写什么""日记打卡"
- 用户想做情绪记录、自我反思
- 用户说"journal""journaling""writing prompt"
- 用户说"帮我复盘今天""今天有什么值得记录的"
---
## 推送管理
```bash
node scripts/push-toggle.js on <userId>
node scripts/push-toggle.js on <userId> --morning 09:00 --evening 21:00 --channel feishu
node scripts/push-toggle.js off <userId>
node scripts/push-toggle.js status <userId>
```
支持渠道:`telegram` / `feishu` / `slack` / `discord`
FILE:README.md
# Daily Reflect — Daily Journal Prompts Skill
> Morning writing prompts and evening reflections to build a daily journaling habit.
[](https://clawhub.ai/skills/daily-reflect)
[](https://openclaw.ai)
An [OpenClaw](https://openclaw.ai) skill that guides you through daily journaling — a morning writing prompt to set intentions, and an evening reflection to review your day. Designed to build a sustainable journaling habit with the highest user retention of any daily skill type.
---
## Features
- **Morning Prompts** — Theme-based writing guide (7 weekly themes: intention / gratitude / challenge / connection / weekly review / dreams / inner voice)
- **Evening Reflection** — 3-question daily review, emotion check-in, next-day intention
- **Streak Tracking** — Daily check-in motivation
- **Non-judgmental Tone** — Warm, open, inclusive writing guidance
- **Bilingual** — Chinese and English
---
## Daily Push Schedule
| Push | Time | Content |
|------|------|---------|
| Morning | 09:00 | Today's journal theme + 3 guiding questions + writing starter |
| Evening | 21:00 | Day review + emotion score + tomorrow's intention |
```bash
node scripts/push-toggle.js on <userId>
node scripts/push-toggle.js on <userId> --morning 09:00 --evening 21:00 --channel telegram
node scripts/push-toggle.js off <userId>
node scripts/push-toggle.js status <userId>
```
Supported channels: `telegram` / `feishu` / `slack` / `discord`
---
## Trigger Words
日记、写日记、今天写什么、每日日记、日记打卡、自我反思、情绪日记、每日复盘、journal、journaling、writing prompt、morning journal、evening reflection、gratitude journal、self-reflection
---
*MIT License · OpenClaw Skill*
## Keywords
daily journal · journaling · journal prompts · writing prompts · daily reflection · self-awareness · gratitude · 日记 · 写日记 · 每日日记 · 自我反思 · 日记打卡
---
Built for [OpenClaw](https://openclaw.ai) · Published on [clawhub.ai](https://clawhub.ai/skills/daily-reflect)
FILE:_meta.json
{
"slug": "daily-reflect",
"version": "1.0.0",
"runtime": {
"node": ">=18"
},
"ownerId": "kn79bebfnwg15sb0g7cj5z5nyd83gxh0"
}
FILE:package.json
{
"name": "daily-journal",
"version": "1.0.2",
"description": "Daily journaling prompts for meaningful reflection — morning intention-setting and evening review. Build a consistent self-reflection habit one prompt at a time.",
"engines": {
"node": ">=18"
},
"license": "MIT"
}
FILE:scripts/evening-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('invalid userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('illegal path');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{};}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
console.log(`晚间日记时间🌙今天是date。请生成今晚的日记回顾引导:①今日复盘3问(今天发生了什么?有什么感受?有什么想法?)②情绪检测(引导用1-10分为今天打分并说明原因)③明日意图设定(引导写下明天最想完成的1件事)④今日金句(一句适合今晚心情的话,中英双语)。语气轻柔,像陪伴的老朋友。`);
FILE:scripts/morning-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('invalid userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('illegal path');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{};}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
console.log(`早安!今天是weekday(date)。请生成今日日记引导。主题按星期轮换:周一=新的开始与意图、周二=感恩与欣赏、周三=挑战与成长、周四=关系与连接、周五=本周收获复盘、周六=梦想与想象、周日=内心独白。输出:①今日日记主题(标题,中英双语)②引导问题3个(由浅入深)③今日写作提示(具体句子开头)④写作小技巧1条。语气温暖,不评判。`);
FILE:scripts/push-toggle.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const SKILL='daily-journal',DEFAULT_MORNING='09:00',DEFAULT_EVENING='21:00';
const USERS_DIR=path.join(__dirname,'../data/users');
const ALLOWED_CH=new Set(['telegram','feishu','slack','discord']);
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('invalid userId');process.exit(1);}return v;}
function sanitizeTime(v,l){if(!/^\d{1,2}:\d{2}$/.test(v)){console.error('invalid '+l);process.exit(1);}const[h,m]=v.split(':').map(Number);if(h>23||m>59){console.error('invalid '+l);process.exit(1);}return{h,m};}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('illegal path');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{};}
function saveUser(u,d){fs.mkdirSync(USERS_DIR,{recursive:true});fs.writeFileSync(safeUserPath(u),JSON.stringify(d,null,2),'utf8');}
function enablePush(userId,opts){
userId=sanitizeId(userId);
const user=loadUser(userId);
const mt=opts.morning||user.morningTime||DEFAULT_MORNING;
const et=opts.evening||user.eveningTime||DEFAULT_EVENING;
const{h:mh,m:mm}=sanitizeTime(mt,'--morning');
const{h:eh,m:em}=sanitizeTime(et,'--evening');
const ch=opts.channel||user.channel||'telegram';
if(!ALLOWED_CH.has(ch)){console.error('unsupported channel:'+ch);process.exit(1);}
const sk=`agent:main:ch:direct:userId`;
console.log('__OPENCLAW_CRON_ADD__:'+JSON.stringify({name:`SKILL-morning-userId`,cronExpr:`mm mh * * *`,tz:'Asia/Shanghai',session:'isolated',sessionKey:sk,channel:ch,to:userId,announce:true,timeoutSeconds:180,message:`node path.join(__dirname,'morning-push.js') userId`}));
console.log('__OPENCLAW_CRON_ADD__:'+JSON.stringify({name:`SKILL-evening-userId`,cronExpr:`em eh * * *`,tz:'Asia/Shanghai',session:'isolated',sessionKey:sk,channel:ch,to:userId,announce:true,timeoutSeconds:180,message:`node path.join(__dirname,'evening-push.js') userId`}));
saveUser(userId,{...user,morningTime:mt,eveningTime:et,channel:ch,pushEnabled:true,updatedAt:new Date().toISOString()});
console.log(`\n✅ SKILL 推送已开启\n⏰ 早推: mt 🌙 晚推: et 📡 渠道: ch\n关闭: node push-toggle.js off userId`);
}
function disablePush(userId){
userId=sanitizeId(userId);
console.log(`__OPENCLAW_CRON_RM__:SKILL-morning-userId`);
console.log(`__OPENCLAW_CRON_RM__:SKILL-evening-userId`);
saveUser(userId,{...loadUser(userId),pushEnabled:false,updatedAt:new Date().toISOString()});
console.log(`✅ SKILL 推送已关闭`);
}
function showStatus(userId){
userId=sanitizeId(userId);
const u=loadUser(userId);
console.log(`\n📡 SKILL — userId\n状态: '❌ 关闭' 早推: u.morningTime||DEFAULT_MORNING 晚推: u.eveningTime||DEFAULT_EVENING 渠道: u.channel||'telegram'\n`);
}
if(require.main!==module)return;
const[cmd,uid,...rest]=process.argv.slice(2);
if(!cmd||!uid){console.log('Usage: node push-toggle.js on|off|status <userId>');process.exit(1);}
const opts={};
const mi=rest.indexOf('--morning');if(mi!==-1)opts.morning=rest[mi+1];
const ei=rest.indexOf('--evening');if(ei!==-1)opts.evening=rest[ei+1];
const ci=rest.indexOf('--channel');if(ci!==-1)opts.channel=rest[ci+1];
if(cmd==='on')enablePush(uid,opts);
else if(cmd==='off')disablePush(uid);
else if(cmd==='status')showStatus(uid);
else{console.error('unknown cmd:'+cmd);process.exit(1);}
Daily movie & TV recommendation — curated by genre, mood, or theme. One great watch per day with synopsis, streaming platform, and audience ratings.
---
name: daily-movie
description: "Daily movie & TV recommendation — curated by genre, mood, or theme. One great watch per day with synopsis, streaming platform, and audience ratings."
keywords:
- 今天看什么
- 电影推荐
- 看什么电影
- 剧推荐
- 今晚看什么
- 每日电影
- 电影
- 剧集
- 好看的电影
- 值得看的剧
- 豆瓣高分
- IMDb
- Netflix
- 爱奇艺
- 优酷
- 腾讯视频
- 剧情片
- 喜剧
- 悬疑
- 爱情片
- 动作片
- 科幻
- 纪录片
- 动画
- 韩剧
- 美剧
- 日剧
- 国产剧
- 经典电影
- 新片推荐
- movie recommendation
- what to watch
- film of the day
- Netflix recommendation
- series recommendation
- best movies
- top rated
- must watch
- TV show
- binge watch
metadata:
openclaw:
runtime:
node: ">=18"
---
# 每日影视推荐
> 每日影视推荐 — 今日精选 · 评分推荐 · 观影指南 · 中英双语
## 何时使用
- 用户说"今晚看什么""电影推荐""推荐部电影"
- 用户说"what to watch""movie recommendation"
- 用户说"好看的剧""最近有什么新片"
- 用户说"Netflix推荐""豆瓣高分""IMDb top"
---
## 推送管理
```bash
node scripts/push-toggle.js on <userId>
node scripts/push-toggle.js on <userId> --morning 10:00 --evening 19:00 --channel feishu
node scripts/push-toggle.js off <userId>
node scripts/push-toggle.js status <userId>
```
支持渠道:`telegram` / `feishu` / `slack` / `discord`
FILE:README.md
# Daily Movie — Movie & TV Recommendation Skill
> One curated film or series pick every day — synopsis, Douban + IMDb ratings, where to stream. Bilingual EN/CN.
[](https://clawhub.ai/skills/daily-movie)
[](https://openclaw.ai)
An [OpenClaw](https://openclaw.ai) skill that recommends one movie and one TV series every day — with synopsis, Douban/IMDb ratings, platform availability, and a spoiler-free reason to watch. High open rate by design: everyone wants to know what to watch tonight.
---
## Features
- **Daily Pick** — 1 movie + 1 series per day, genre rotates by weekday
- **Ratings** — Douban + IMDb scores
- **Spoiler-Free** — Synopsis without plot reveals
- **Platform Guide** — Where to watch (Netflix, iQiyi, Youku, etc.)
- **Evening Pick** — Lighter recommendation for tonight + weekend preview
- **Bilingual** — Chinese and English
---
## Daily Push Schedule
| Push | Time | Content |
|------|------|---------|
| Morning | 10:00 | Today's movie + series recommendation |
| Evening | 19:00 | Tonight's watch + weekend new releases preview |
```bash
node scripts/push-toggle.js on <userId>
node scripts/push-toggle.js on <userId> --morning 10:00 --evening 19:00 --channel feishu
node scripts/push-toggle.js off <userId>
node scripts/push-toggle.js status <userId>
```
Supported channels: `telegram` / `feishu` / `slack` / `discord`
---
## Weekly Genre Rotation
| Day | Genre |
|-----|-------|
| Mon | 励志 Inspiring |
| Tue | 悬疑惊悚 Thriller |
| Wed | 爱情 Romance |
| Thu | 喜剧 Comedy |
| Fri | 动作科幻 Action/Sci-Fi |
| Sat | 经典名作 Classics |
| Sun | 家庭纪录片 Family/Documentary |
---
*MIT License · OpenClaw Skill*
## Keywords
movie recommendation · what to watch · film recommendation · daily movie · Netflix · Korean drama · 电影推荐 · 今天看什么 · 今晚看什么 · 豆瓣高分 · 剧推荐 · 看什么电影
---
Built for [OpenClaw](https://openclaw.ai) · Published on [clawhub.ai](https://clawhub.ai/skills/daily-movie)
FILE:_meta.json
{
"slug": "daily-movie",
"version": "1.0.0",
"runtime": {
"node": ">=18"
},
"ownerId": "kn79bebfnwg15sb0g7cj5z5nyd83gxh0"
}
FILE:package.json
{
"name": "daily-movie",
"version": "1.0.2",
"description": "Daily movie & TV recommendation — curated by genre, mood, or theme. One great watch per day with synopsis, streaming platform, and audience ratings.",
"engines": {
"node": ">=18"
},
"license": "MIT"
}
FILE:scripts/evening-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('invalid userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('illegal path');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{};}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
console.log(`今晚看什么🎬今天是date。请推荐一部今晚适合的轻松影视:①今晚推荐(1部,标注时长+评分,附3句不剧透理由)②今晚不推荐看的类型(根据睡眠健康:避免恐怖/过于烧脑)③本周末值得期待的新片/新剧预告(搜索本周上映信息)④观影小仪式建议(零食/姿势/音量)。轻松愉快,像私人选片顾问。`);
FILE:scripts/morning-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('invalid userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('illegal path');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{};}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
console.log(`今天是weekday(date),请搜索并推荐今日精选影视。根据星期确定类型(周一励志/周二悬疑/周三爱情/周四喜剧/周五动作科幻/周六经典/周日家庭纪录片)。搜索相关推荐,选1部电影+1部剧集,每部含:①片名(中英)+年份+类型②豆瓣/IMDb评分③一句话吸引你看的理由(不剧透)④剧情简介(100字)⑤适合人群⑥在哪里看(平台)。中英双语,语气像朋友推荐。`);
FILE:scripts/push-toggle.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const SKILL='daily-movie',DEFAULT_MORNING='10:00',DEFAULT_EVENING='19:00';
const USERS_DIR=path.join(__dirname,'../data/users');
const ALLOWED_CH=new Set(['telegram','feishu','slack','discord']);
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('invalid userId');process.exit(1);}return v;}
function sanitizeTime(v,l){if(!/^\d{1,2}:\d{2}$/.test(v)){console.error('invalid '+l);process.exit(1);}const[h,m]=v.split(':').map(Number);if(h>23||m>59){console.error('invalid '+l);process.exit(1);}return{h,m};}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('illegal path');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{};}
function saveUser(u,d){fs.mkdirSync(USERS_DIR,{recursive:true});fs.writeFileSync(safeUserPath(u),JSON.stringify(d,null,2),'utf8');}
function enablePush(userId,opts){
userId=sanitizeId(userId);
const user=loadUser(userId);
const mt=opts.morning||user.morningTime||DEFAULT_MORNING;
const et=opts.evening||user.eveningTime||DEFAULT_EVENING;
const{h:mh,m:mm}=sanitizeTime(mt,'--morning');
const{h:eh,m:em}=sanitizeTime(et,'--evening');
const ch=opts.channel||user.channel||'telegram';
if(!ALLOWED_CH.has(ch)){console.error('unsupported channel:'+ch);process.exit(1);}
const sk=`agent:main:ch:direct:userId`;
console.log('__OPENCLAW_CRON_ADD__:'+JSON.stringify({name:`SKILL-morning-userId`,cronExpr:`mm mh * * *`,tz:'Asia/Shanghai',session:'isolated',sessionKey:sk,channel:ch,to:userId,announce:true,timeoutSeconds:180,message:`node path.join(__dirname,'morning-push.js') userId`}));
console.log('__OPENCLAW_CRON_ADD__:'+JSON.stringify({name:`SKILL-evening-userId`,cronExpr:`em eh * * *`,tz:'Asia/Shanghai',session:'isolated',sessionKey:sk,channel:ch,to:userId,announce:true,timeoutSeconds:180,message:`node path.join(__dirname,'evening-push.js') userId`}));
saveUser(userId,{...user,morningTime:mt,eveningTime:et,channel:ch,pushEnabled:true,updatedAt:new Date().toISOString()});
console.log(`\n✅ SKILL 推送已开启\n⏰ 早推: mt 🌙 晚推: et 📡 渠道: ch\n关闭: node push-toggle.js off userId`);
}
function disablePush(userId){
userId=sanitizeId(userId);
console.log(`__OPENCLAW_CRON_RM__:SKILL-morning-userId`);
console.log(`__OPENCLAW_CRON_RM__:SKILL-evening-userId`);
saveUser(userId,{...loadUser(userId),pushEnabled:false,updatedAt:new Date().toISOString()});
console.log(`✅ SKILL 推送已关闭`);
}
function showStatus(userId){
userId=sanitizeId(userId);
const u=loadUser(userId);
console.log(`\n📡 SKILL — userId\n状态: '❌ 关闭' 早推: u.morningTime||DEFAULT_MORNING 晚推: u.eveningTime||DEFAULT_EVENING 渠道: u.channel||'telegram'\n`);
}
if(require.main!==module)return;
const[cmd,uid,...rest]=process.argv.slice(2);
if(!cmd||!uid){console.log('Usage: node push-toggle.js on|off|status <userId>');process.exit(1);}
const opts={};
const mi=rest.indexOf('--morning');if(mi!==-1)opts.morning=rest[mi+1];
const ei=rest.indexOf('--evening');if(ei!==-1)opts.evening=rest[ei+1];
const ci=rest.indexOf('--channel');if(ci!==-1)opts.channel=rest[ci+1];
if(cmd==='on')enablePush(uid,opts);
else if(cmd==='off')disablePush(uid);
else if(cmd==='status')showStatus(uid);
else{console.error('unknown cmd:'+cmd);process.exit(1);}
Daily Chinese idiom (成语) — origin story, meaning, usage examples, and memory tip. Build Chinese vocabulary and cultural knowledge one 成语 per day.
---
name: daily-idiom
description: "Daily Chinese idiom (成语) — origin story, meaning, usage examples, and memory tip. Build Chinese vocabulary and cultural knowledge one 成语 per day."
keywords:
- 成语
- 每日成语
- 今日成语
- 成语故事
- 俗语
- 歇后语
- 谚语
- 学成语
- 成语打卡
- 中文学习
- 汉语成语
- 四字成语
- 成语典故
- 成语用法
- 成语造句
- 文化成语
- 历史成语
- Chinese idiom
- chengyu
- idiom of the day
- learn Chinese
- daily chengyu
- Chinese culture
- idiom story
- Chinese proverb
- Mandarin learning
- Chinese language
- 成语接龙
metadata:
openclaw:
runtime:
node: ">=18"
---
# 每日成语
> 每日成语 — 典故故事 · 用法例句 · 测验打卡 · 中文文化输出
## 何时使用
- 用户说"成语""今日成语""成语故事"
- 用户学习中文或想了解中国文化
- 用户说"idiom""chengyu""learn Chinese"
- 用户说"成语接龙""猜成语""成语测验"
---
## 推送管理
```bash
node scripts/push-toggle.js on <userId>
node scripts/push-toggle.js on <userId> --morning 08:00 --evening 21:00 --channel feishu
node scripts/push-toggle.js off <userId>
node scripts/push-toggle.js status <userId>
```
支持渠道:`telegram` / `feishu` / `slack` / `discord`
FILE:README.md
# Daily Idiom — Chinese Idiom Learning Skill
> One Chinese idiom (成语) every day — story, usage, examples, quiz. Perfect for Chinese learners.
[](https://clawhub.ai/skills/daily-idiom)
[](https://openclaw.ai)
An [OpenClaw](https://openclaw.ai) skill for learning one Chinese idiom (成语) or proverb every day — with origin story, modern usage, example sentences, and an evening quiz. Perfect for Chinese learners, culture enthusiasts, and anyone who wants to level up their Mandarin.
---
## Features
- **Daily Idiom** — One 成语/俗语/谚语 per day, theme-based by weekday
- **Origin Story** — Historical/literary background (50–80 words)
- **Bilingual** — Chinese original + English translation (meaning, not literal)
- **Usage Guide** — When and how to use it in modern context
- **Example Sentences** — 2 sentences with translations
- **Memory Tip** — Mnemonic or word-root breakdown
- **Evening Quiz** — 3-question review with answers and explanations
---
## Daily Push Schedule
| Push | Time | Content |
|------|------|---------|
| Morning | 08:00 | Today's idiom + story + usage + quiz teaser |
| Evening | 21:00 | Quiz on today's idiom + tomorrow's preview hint |
```bash
node scripts/push-toggle.js on <userId>
node scripts/push-toggle.js on <userId> --morning 08:00 --evening 21:00 --channel feishu
node scripts/push-toggle.js off <userId>
node scripts/push-toggle.js status <userId>
```
Supported channels: `telegram` / `feishu` / `slack` / `discord`
---
## Weekly Themes
| Day | Theme |
|-----|-------|
| Mon | 励志 Motivation |
| Tue | 智慧 Wisdom |
| Wed | 友情 Friendship |
| Thu | 财富 Wealth |
| Fri | 趣味 Fun |
| Sat | 历史 History |
| Sun | 生活 Daily Life |
---
*MIT License · OpenClaw Skill*
## Keywords
Chinese idiom · chengyu · 成语 · daily idiom · Chinese learning · learn Chinese · Chinese proverb · 每日成语 · 成语故事 · 俗语 · 中文学习 · idiom of the day
---
Built for [OpenClaw](https://openclaw.ai) · Published on [clawhub.ai](https://clawhub.ai/skills/daily-idiom)
FILE:_meta.json
{
"slug": "daily-idiom",
"version": "1.0.0",
"runtime": {
"node": ">=18"
},
"ownerId": "kn79bebfnwg15sb0g7cj5z5nyd83gxh0"
}
FILE:package.json
{
"name": "daily-idiom",
"version": "1.0.2",
"description": "Daily Chinese idiom (成语) — origin story, meaning, usage examples, and memory tip. Build Chinese vocabulary and cultural knowledge one 成语 per day.",
"engines": {
"node": ">=18"
},
"license": "MIT"
}
FILE:scripts/evening-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('invalid userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('illegal path');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{};}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
console.log(`成语晚间复习🏮今天是date。请回顾今日成语,出3道复习题:①释义选择(4选1)②填空造句③情景应用判断。给出答案与详解。预告明日成语主题,给出一个悬念提示(不揭晓答案)。轻松有趣,适合睡前温习。`);
FILE:scripts/morning-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('invalid userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('illegal path');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{};}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
console.log(`早安!今天是weekday(date)。请选一个有趣实用的中文成语或俗语(主题轮换:周一励志/周二智慧/周三友情/周四财富/周五趣味/周六历史/周日生活),呈现:①成语原文(大字)+拼音②英文意译③出处典故(50-80字)④现代用法(1-2句)⑤中文例句×2(含翻译)⑥近义词1个+反义词1个⑦记忆口诀。结尾出1道选择题,答案晚间揭晓。`);
FILE:scripts/push-toggle.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const SKILL='daily-idiom',DEFAULT_MORNING='08:00',DEFAULT_EVENING='21:00';
const USERS_DIR=path.join(__dirname,'../data/users');
const ALLOWED_CH=new Set(['telegram','feishu','slack','discord']);
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('invalid userId');process.exit(1);}return v;}
function sanitizeTime(v,l){if(!/^\d{1,2}:\d{2}$/.test(v)){console.error('invalid '+l);process.exit(1);}const[h,m]=v.split(':').map(Number);if(h>23||m>59){console.error('invalid '+l);process.exit(1);}return{h,m};}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('illegal path');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{};}
function saveUser(u,d){fs.mkdirSync(USERS_DIR,{recursive:true});fs.writeFileSync(safeUserPath(u),JSON.stringify(d,null,2),'utf8');}
function enablePush(userId,opts){
userId=sanitizeId(userId);
const user=loadUser(userId);
const mt=opts.morning||user.morningTime||DEFAULT_MORNING;
const et=opts.evening||user.eveningTime||DEFAULT_EVENING;
const{h:mh,m:mm}=sanitizeTime(mt,'--morning');
const{h:eh,m:em}=sanitizeTime(et,'--evening');
const ch=opts.channel||user.channel||'telegram';
if(!ALLOWED_CH.has(ch)){console.error('unsupported channel:'+ch);process.exit(1);}
const sk=`agent:main:ch:direct:userId`;
console.log('__OPENCLAW_CRON_ADD__:'+JSON.stringify({name:`SKILL-morning-userId`,cronExpr:`mm mh * * *`,tz:'Asia/Shanghai',session:'isolated',sessionKey:sk,channel:ch,to:userId,announce:true,timeoutSeconds:180,message:`node path.join(__dirname,'morning-push.js') userId`}));
console.log('__OPENCLAW_CRON_ADD__:'+JSON.stringify({name:`SKILL-evening-userId`,cronExpr:`em eh * * *`,tz:'Asia/Shanghai',session:'isolated',sessionKey:sk,channel:ch,to:userId,announce:true,timeoutSeconds:180,message:`node path.join(__dirname,'evening-push.js') userId`}));
saveUser(userId,{...user,morningTime:mt,eveningTime:et,channel:ch,pushEnabled:true,updatedAt:new Date().toISOString()});
console.log(`\n✅ SKILL 推送已开启\n⏰ 早推: mt 🌙 晚推: et 📡 渠道: ch\n关闭: node push-toggle.js off userId`);
}
function disablePush(userId){
userId=sanitizeId(userId);
console.log(`__OPENCLAW_CRON_RM__:SKILL-morning-userId`);
console.log(`__OPENCLAW_CRON_RM__:SKILL-evening-userId`);
saveUser(userId,{...loadUser(userId),pushEnabled:false,updatedAt:new Date().toISOString()});
console.log(`✅ SKILL 推送已关闭`);
}
function showStatus(userId){
userId=sanitizeId(userId);
const u=loadUser(userId);
console.log(`\n📡 SKILL — userId\n状态: '❌ 关闭' 早推: u.morningTime||DEFAULT_MORNING 晚推: u.eveningTime||DEFAULT_EVENING 渠道: u.channel||'telegram'\n`);
}
if(require.main!==module)return;
const[cmd,uid,...rest]=process.argv.slice(2);
if(!cmd||!uid){console.log('Usage: node push-toggle.js on|off|status <userId>');process.exit(1);}
const opts={};
const mi=rest.indexOf('--morning');if(mi!==-1)opts.morning=rest[mi+1];
const ei=rest.indexOf('--evening');if(ei!==-1)opts.evening=rest[ei+1];
const ci=rest.indexOf('--channel');if(ci!==-1)opts.channel=rest[ci+1];
if(cmd==='on')enablePush(uid,opts);
else if(cmd==='off')disablePush(uid);
else if(cmd==='status')showStatus(uid);
else{console.error('unknown cmd:'+cmd);process.exit(1);}
Daily horoscope for all 12 zodiac signs — love, career & finance scores (1–5★), lucky color/number/direction, daily affirmation. Morning & evening push. Bili...
---
name: daily-astro
description: "Daily horoscope for all 12 zodiac signs — love, career & finance scores (1–5★), lucky color/number/direction, daily affirmation. Morning & evening push. Bilingual EN/CN."
keywords:
- 星座运势
- 今日星座
- 每日星座
- 星座今日
- 白羊座
- 金牛座
- 双子座
- 巨蟹座
- 狮子座
- 处女座
- 天秤座
- 天蝎座
- 射手座
- 摩羯座
- 水瓶座
- 双鱼座
- 星座配对
- 爱情运势
- 事业运势
- 财运
- 幸运颜色
- 幸运数字
- 星座占卜
- 上升星座
- 月亮星座
- horoscope
- zodiac
- daily horoscope
- astrology
- aries
- taurus
- gemini
- cancer
- leo
- virgo
- libra
- scorpio
- sagittarius
- capricorn
- aquarius
- pisces
- love horoscope
- weekly horoscope
- zodiac compatibility
metadata:
openclaw:
runtime:
node: ">=18"
---
# 西方星座运势
> 西方星座运势 — 12星座每日运程 · 爱情事业财运 · 幸运元素 · 中英双语
## 何时使用
- 用户说"今日星座""我的星座运势""白羊座今天怎么样"
- 用户想查爱情运/事业运/财运
- 用户说"horoscope""zodiac""星座配对"
- 用户说"今天适合表白吗""今天适合谈合同吗"
---
## 推送管理
```bash
node scripts/push-toggle.js on <userId>
node scripts/push-toggle.js on <userId> --morning 08:00 --evening 21:00 --channel feishu
node scripts/push-toggle.js off <userId>
node scripts/push-toggle.js status <userId>
```
支持渠道:`telegram` / `feishu` / `slack` / `discord`
FILE:README.md
# Daily Astro — Western Horoscope Skill
> Daily horoscope for all 12 zodiac signs — love, career, finance scores, lucky elements. Bilingual EN/CN.
[](https://clawhub.ai/skills/daily-astro)
[](https://openclaw.ai)
An [OpenClaw](https://openclaw.ai) skill that delivers daily Western horoscope readings for all 12 zodiac signs — love, career, and finance scores, lucky color/number/direction, and a daily affirmation. Pairs naturally with [yunshi](https://github.com/jiajiaoy/yunshi) for a complete Chinese + Western astrology experience.
---
## Features
- **12 Zodiac Signs** — Aries through Pisces, all covered daily
- **Three Fortune Scores** — Love 💕 / Career 💼 / Finance 💰 (1–5 stars each)
- **Lucky Elements** — Color, number, direction for today
- **Daily Affirmation** — One guiding sentence per sign
- **Evening Preview** — Tomorrow's top 3 signs + watch-out signs
- **Bilingual** — Chinese and English output
---
## Daily Push Schedule
| Push | Time | Content |
|------|------|---------|
| Morning | 08:00 | Today's horoscope for all 12 signs |
| Evening | 21:00 | Tomorrow's horoscope preview + lucky tips |
```bash
node scripts/push-toggle.js on <userId>
node scripts/push-toggle.js on <userId> --morning 08:00 --evening 21:00 --channel feishu
node scripts/push-toggle.js off <userId>
node scripts/push-toggle.js status <userId>
```
Supported channels: `telegram` / `feishu` / `slack` / `discord`
---
## Trigger Words
星座运势、今日星座、白羊座、金牛座、双子座、巨蟹座、狮子座、处女座、天秤座、天蝎座、射手座、摩羯座、水瓶座、双鱼座、horoscope、zodiac、daily horoscope、astrology、love horoscope
---
*MIT License · OpenClaw Skill*
## Keywords
horoscope · daily horoscope · zodiac · astrology · western astrology · Aries · Taurus · Gemini · Cancer · Leo · Virgo · Libra · Scorpio · Sagittarius · Capricorn · Aquarius · Pisces · 星座运势 · 今日星座 · 每日运势 · 星座
---
Built for [OpenClaw](https://openclaw.ai) · Published on [clawhub.ai](https://clawhub.ai/skills/daily-astro)
FILE:_meta.json
{
"slug": "daily-astro",
"version": "1.0.0",
"runtime": {
"node": ">=18"
},
"ownerId": "kn79bebfnwg15sb0g7cj5z5nyd83gxh0"
}
FILE:package.json
{
"name": "daily-horoscope-west",
"version": "1.0.2",
"description": "Daily horoscope for all 12 zodiac signs — love, career & finance scores (1–5★), lucky color/number/direction, daily affirmation. Morning & evening push. Bilingual EN/CN.",
"engines": {
"node": ">=18"
},
"license": "MIT"
}
FILE:scripts/evening-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('invalid userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('illegal path');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{};}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
console.log(`晚安星座播报🌟今天是date。请生成明日12星座运势预告:①明日最佳星座TOP3(综合运势最旺)②明日需要注意的星座(有小波折)③明日全体幸运提示1条④一句睡前星座冥想语。简洁有趣,中英双语。`);
FILE:scripts/morning-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('invalid userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('illegal path');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{};}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
console.log(`今天是weekday(date),请为12个西方星座生成今日运势推送。每个星座包含:①今日综合运势简评(2句)②爱情💕/事业💼/财运💰各项指数(1-5颗星)③今日幸运颜色、数字、方位④一句今日箴言。输出格式清晰,中英双语,适合每日推送。星座顺序:白羊/金牛/双子/巨蟹/狮子/处女/天秤/天蝎/射手/摩羯/水瓶/双鱼。`);
FILE:scripts/push-toggle.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const SKILL='daily-astro',DEFAULT_MORNING='08:00',DEFAULT_EVENING='21:00';
const USERS_DIR=path.join(__dirname,'../data/users');
const ALLOWED_CH=new Set(['telegram','feishu','slack','discord']);
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('invalid userId');process.exit(1);}return v;}
function sanitizeTime(v,l){if(!/^\d{1,2}:\d{2}$/.test(v)){console.error('invalid '+l);process.exit(1);}const[h,m]=v.split(':').map(Number);if(h>23||m>59){console.error('invalid '+l);process.exit(1);}return{h,m};}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('illegal path');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{};}
function saveUser(u,d){fs.mkdirSync(USERS_DIR,{recursive:true});fs.writeFileSync(safeUserPath(u),JSON.stringify(d,null,2),'utf8');}
function enablePush(userId,opts){
userId=sanitizeId(userId);
const user=loadUser(userId);
const mt=opts.morning||user.morningTime||DEFAULT_MORNING;
const et=opts.evening||user.eveningTime||DEFAULT_EVENING;
const{h:mh,m:mm}=sanitizeTime(mt,'--morning');
const{h:eh,m:em}=sanitizeTime(et,'--evening');
const ch=opts.channel||user.channel||'telegram';
if(!ALLOWED_CH.has(ch)){console.error('unsupported channel:'+ch);process.exit(1);}
const sk=`agent:main:ch:direct:userId`;
console.log('__OPENCLAW_CRON_ADD__:'+JSON.stringify({name:`SKILL-morning-userId`,cronExpr:`mm mh * * *`,tz:'Asia/Shanghai',session:'isolated',sessionKey:sk,channel:ch,to:userId,announce:true,timeoutSeconds:180,message:`node path.join(__dirname,'morning-push.js') userId`}));
console.log('__OPENCLAW_CRON_ADD__:'+JSON.stringify({name:`SKILL-evening-userId`,cronExpr:`em eh * * *`,tz:'Asia/Shanghai',session:'isolated',sessionKey:sk,channel:ch,to:userId,announce:true,timeoutSeconds:180,message:`node path.join(__dirname,'evening-push.js') userId`}));
saveUser(userId,{...user,morningTime:mt,eveningTime:et,channel:ch,pushEnabled:true,updatedAt:new Date().toISOString()});
console.log(`\n✅ SKILL 推送已开启\n⏰ 早推: mt 🌙 晚推: et 📡 渠道: ch\n关闭: node push-toggle.js off userId`);
}
function disablePush(userId){
userId=sanitizeId(userId);
console.log(`__OPENCLAW_CRON_RM__:SKILL-morning-userId`);
console.log(`__OPENCLAW_CRON_RM__:SKILL-evening-userId`);
saveUser(userId,{...loadUser(userId),pushEnabled:false,updatedAt:new Date().toISOString()});
console.log(`✅ SKILL 推送已关闭`);
}
function showStatus(userId){
userId=sanitizeId(userId);
const u=loadUser(userId);
console.log(`\n📡 SKILL — userId\n状态: '❌ 关闭' 早推: u.morningTime||DEFAULT_MORNING 晚推: u.eveningTime||DEFAULT_EVENING 渠道: u.channel||'telegram'\n`);
}
if(require.main!==module)return;
const[cmd,uid,...rest]=process.argv.slice(2);
if(!cmd||!uid){console.log('Usage: node push-toggle.js on|off|status <userId>');process.exit(1);}
const opts={};
const mi=rest.indexOf('--morning');if(mi!==-1)opts.morning=rest[mi+1];
const ei=rest.indexOf('--evening');if(ei!==-1)opts.evening=rest[ei+1];
const ci=rest.indexOf('--channel');if(ci!==-1)opts.channel=rest[ci+1];
if(cmd==='on')enablePush(uid,opts);
else if(cmd==='off')disablePush(uid);
else if(cmd==='status')showStatus(uid);
else{console.error('unknown cmd:'+cmd);process.exit(1);}
One advanced English word every day — IPA pronunciation, etymology, 3 example sentences, and quiz mode. GRE / SAT / IELTS / C2 vocabulary level.
---
name: daily-vocab
description: "One advanced English word every day — IPA pronunciation, etymology, 3 example sentences, and quiz mode. GRE / SAT / IELTS / C2 vocabulary level."
keywords:
- 每日词汇
- 今日单词
- 学个单词
- 每日一词
- 高级词汇
- GRE词汇
- SAT词汇
- 英语学习
- 词汇量
- 单词记忆
- daily vocab
- daily vocabulary
- word of the day
- vocabulary
- advanced vocabulary
- GRE word
- SAT word
- English learning
- etymology
- flashcard
metadata:
openclaw:
runtime:
node: ">=18"
---
# Daily Vocabulary / 每日词汇
Generate a beautiful vocabulary learning card featuring one advanced English word with complete learning materials.
## Workflow
1. **Get today's date** — Use date to determine word theme. Rotate themes weekly: Mon=emotions, Tue=science, Wed=art/culture, Thu=business, Fri=nature, Sat=philosophy, Sun=daily life.
2. **Select a word** — Choose an interesting, useful but not commonly known English word (GRE/SAT level or above). Use `web_search` to verify meaning and find authentic usage examples. Query: `"[word] definition etymology usage examples"`.
3. **Build the learning card** — Include: word, phonetic transcription, part of speech, definition (EN + CN), etymology/word roots, 3 example sentences (with CN translation), synonyms, antonyms, a memory tip.
4. **Generate the visual** — Create a single-file HTML artifact.
## Visual Design Requirements
Create a flashcard-style learning interface:
- **Layout**: Single large card, centered, with clear sections. Think premium language-learning app aesthetic.
- **Typography**: The word itself in a large, beautiful display font (e.g., Crimson Text, Spectral, Libre Caslon). Phonetics in a monospace font. Body in clean sans-serif.
- **Color scheme**: Soft, focus-friendly palette — muted blues/greens/warm grays. Alternate between light and dark themes based on day of week.
- **Sections**:
- Top: Word + Phonetics + Part of Speech
- Definition block: EN definition, then CN translation
- Etymology: Root breakdown with visual connectors
- Examples: 3 sentences with key word highlighted
- Bottom row: Synonyms | Antonyms | Memory Tip
- **Interactivity**: Click the word to toggle between showing/hiding the Chinese translation (study mode). A "Quiz Me" button that hides the definition and shows it on click.
- **Animation**: Card flips in on load. Sections reveal with stagger.
- **Ad-ready zone**: `<div id="ad-slot-bottom">` below the card (min-height 90px).
- **Footer**: "Powered by ClawCode"
## Word Selection Guidelines
- Prefer words that are: elegant, useful in professional/academic contexts, have interesting etymologies
- Avoid: obscure archaic words nobody uses, basic words everyone knows
- Good examples: "ephemeral", "serendipity", "paradigm", "resilience", "ubiquitous", "cacophony"
## Output
Save as `/mnt/user-data/outputs/daily-vocab.html` and present to user.
---
## 推送管理
```bash
# 开启每日推送(早晚各一次)
node scripts/push-toggle.js on <userId>
# 自定义时间和渠道
node scripts/push-toggle.js on <userId> --morning 08:00 --evening 20:00 --channel feishu
# 关闭推送
node scripts/push-toggle.js off <userId>
# 查看推送状态
node scripts/push-toggle.js status <userId>
```
支持渠道:`telegram` / `feishu` / `slack` / `discord`
FILE:README.md
# Daily Vocab — English Vocabulary Learning Skill
> One advanced English word every day — pronunciation, etymology, examples, and quiz mode.
[](https://clawhub.ai/skills/daily-vocab)
[](https://openclaw.ai)
## What it does
Daily Vocab teaches one high-value English vocabulary word per day — the kind you'd need for GRE, SAT, IELTS, academic writing, or professional communication. Each card includes pronunciation (IPA), word origin, 3 example sentences, and an optional quiz to test retention.
**Daily card** — word, pronunciation, etymology, 3 example sentences
**Quiz mode** — multiple choice or fill-in-the-blank
**Level range** — B2 to C2 / GRE / SAT vocabulary
**Visual** — flash card format for easy review
## Installation
```bash
openclaw install daily-vocab
```
## Usage
```bash
openclaw run daily-vocab morning
```
## Keywords
word of the day · daily vocabulary · GRE vocabulary · SAT words · advanced English · English learning · vocabulary builder · 每日词汇 · 今日单词 · 高级词汇 · 每日一词 · 学英语 · GRE词汇
---
Built for [OpenClaw](https://openclaw.ai) · Published on [clawhub.ai](https://clawhub.ai/skills/daily-vocab)
FILE:scripts/evening-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
const VOCAB_THEMES=["情感词汇", "科学词汇", "艺术文化", "商业词汇", "自然生态", "哲学词汇", "日常生活"];
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('❌ 无效userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('❌ 非法路径');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{}}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const MONTHS_EN=['January','February','March','April','May','June','July','August','September','October','November','December'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const month_en=MONTHS_EN[now.getMonth()];
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
const vocab_theme=VOCAB_THEMES[wd];
const tomorrow_vocab_theme=VOCAB_THEMES[(wd+1)%7];
console.log(`词汇晚间复习🌙今天是date(vocab_theme主题)。请出3道复习题:①词义选择②英文填空③场景应用选择。给出答案与解析。预告明日主题:tomorrow_vocab_theme,并给出1个神秘词汇线索。`);
FILE:scripts/morning-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
const VOCAB_THEMES=["情感词汇", "科学词汇", "艺术文化", "商业词汇", "自然生态", "哲学词汇", "日常生活"];
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('❌ 无效userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('❌ 非法路径');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{}}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const MONTHS_EN=['January','February','March','April','May','June','July','August','September','October','November','December'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const month_en=MONTHS_EN[now.getMonth()];
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
const vocab_theme=VOCAB_THEMES[wd];
const tomorrow_vocab_theme=VOCAB_THEMES[(wd+1)%7];
console.log(`今天是weekday(date),词汇主题:vocab_theme。请选一个该主题下的高级英语词汇(GRE/SAT级别),呈现:单词+音标+词性、中英定义、词源拆解、3个例句(含中译,关键词加粗)、近义词3个/反义词2个、记忆妙招1条。结尾出1道选择题,答案晚间揭晓。`);
FILE:scripts/push-toggle.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const SKILL='daily-vocab',DEFAULT_MORNING='08:00',DEFAULT_EVENING='21:00';
const USERS_DIR=path.join(__dirname,'../data/users');
const ALLOWED_CH=new Set(['telegram','feishu','slack','discord']);
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('❌ 无效userId');process.exit(1);}return v;}
function sanitizeTime(v,l){if(!/^\d{1,2}:\d{2}$/.test(v)){console.error('❌ 无效'+l);process.exit(1);}const[h,m]=v.split(':').map(Number);if(h>23||m>59){console.error('❌ 无效'+l);process.exit(1);}return{h,m};}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('❌ 非法路径');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{}}
function saveUser(u,d){fs.mkdirSync(USERS_DIR,{recursive:true});fs.writeFileSync(safeUserPath(u),JSON.stringify(d,null,2),'utf8');}
function enablePush(userId,opts){
userId=sanitizeId(userId);
const user=loadUser(userId);
const mt=opts.morning||user.morningTime||DEFAULT_MORNING;
const et=opts.evening||user.eveningTime||DEFAULT_EVENING;
const{h:mh,m:mm}=sanitizeTime(mt,'--morning');
const{h:eh,m:em}=sanitizeTime(et,'--evening');
const ch=opts.channel||user.channel||'telegram';
if(!ALLOWED_CH.has(ch)){console.error('❌ 不支持渠道:'+ch);process.exit(1);}
const sk=`agent:main:ch:direct:userId`;
console.log('__OPENCLAW_CRON_ADD__:'+JSON.stringify({name:`SKILL-morning-userId`,cronExpr:`mm mh * * *`,tz:'Asia/Shanghai',session:'isolated',sessionKey:sk,channel:ch,to:userId,announce:true,timeoutSeconds:180,message:`node path.join(__dirname,'morning-push.js') userId`}));
console.log('__OPENCLAW_CRON_ADD__:'+JSON.stringify({name:`SKILL-evening-userId`,cronExpr:`em eh * * *`,tz:'Asia/Shanghai',session:'isolated',sessionKey:sk,channel:ch,to:userId,announce:true,timeoutSeconds:180,message:`node path.join(__dirname,'evening-push.js') userId`}));
saveUser(userId,{...user,morningTime:mt,eveningTime:et,channel:ch,pushEnabled:true,updatedAt:new Date().toISOString()});
console.log(`\n✅ SKILL 推送已开启\n⏰ 早推: mt 🌙 晚推: et 📡 渠道: ch\n关闭: node push-toggle.js off userId`);
}
function disablePush(userId){
userId=sanitizeId(userId);
console.log(`__OPENCLAW_CRON_RM__:SKILL-morning-userId`);
console.log(`__OPENCLAW_CRON_RM__:SKILL-evening-userId`);
const user=loadUser(userId);
saveUser(userId,{...user,pushEnabled:false,updatedAt:new Date().toISOString()});
console.log(`✅ SKILL 推送已关闭`);
}
function showStatus(userId){
userId=sanitizeId(userId);
const u=loadUser(userId);
console.log(`\n📡 SKILL — userId\n状态: '❌ 关闭' 早推: u.morningTime||DEFAULT_MORNING 晚推: u.eveningTime||DEFAULT_EVENING 渠道: u.channel||'telegram'\n`);
}
if(require.main!==module)return;
const[cmd,uid,...rest]=process.argv.slice(2);
if(!cmd||!uid){console.log('用法: node push-toggle.js on|off|status <userId>');process.exit(1);}
const opts={};
const mi=rest.indexOf('--morning');if(mi!==-1)opts.morning=rest[mi+1];
const ei=rest.indexOf('--evening');if(ei!==-1)opts.evening=rest[ei+1];
const ci=rest.indexOf('--channel');if(ci!==-1)opts.channel=rest[ci+1];
if(cmd==='on')enablePush(uid,opts);
else if(cmd==='off')disablePush(uid);
else if(cmd==='status')showStatus(uid);
else{console.error('❌ 未知命令:'+cmd);process.exit(1);}
Daily AI & tech news in 60 seconds — LLM updates, product launches, startup funding, developer tools. Bilingual EN/CN visual card. Morning & evening push.
---
name: dailytech
description: "Daily AI & tech news in 60 seconds — LLM updates, product launches, startup funding, developer tools. Bilingual EN/CN visual card. Morning & evening push."
keywords:
- 科技日报
- 科技新闻
- AI新闻
- 技术日报
- 今日科技
- 最新科技
- 创业新闻
- 人工智能
- 科技动态
- 产品发布
- daily tech
- tech news
- AI news
- tech daily
- startup news
- product launch
- latest tech
- technology briefing
- machine learning
- artificial intelligence
metadata:
openclaw:
runtime:
node: ">=18"
---
# Daily Tech / 科技日报
Generate a beautifully formatted daily tech news briefing focused on AI, startups, and technology.
## Workflow
1. **Search for news** — Use `web_search` to find today's top tech/AI stories. Run 3-4 searches:
- `"AI news today [date]"`
- `"tech startup news today"`
- `"product launch technology today"`
- `"China tech news today"` (for Asian audience relevance)
2. **Curate** — Select 5-6 most significant stories. Prioritize: AI/ML breakthroughs, major product launches, funding rounds > $50M, policy/regulation changes, notable acquisitions.
3. **Write summaries** — Each story: headline (EN + CN), 2-3 sentence summary (EN), 1-2 sentence summary (CN), source name, and a relevance tag.
4. **Generate the visual** — Create a single-file HTML artifact.
## Visual Design Requirements
Create a premium tech-media editorial layout:
- **Layout**: Magazine/newsletter style. Hero story at top (larger card), remaining stories in a 2-column grid below.
- **Typography**: Modern, techy but readable — (e.g., Space Grotesk or JetBrains Mono for headlines, Inter or IBM Plex Sans for body). Monospace accents for tags/categories.
- **Color scheme**: Dark mode default — near-black background (#0a0a0f), with accent colors: electric blue, neon green, or amber for highlights. Very "tech publication" feel.
- **News cards**: Each card has: category tag (AI / Startup / Product / Policy / China Tech), headline, summary, source badge, and a "relevance" indicator (🔥 hot, ⭐ notable, 📌 important).
- **Hero story**: Top story gets a larger card with more detailed summary.
- **Interactive**: Click any card to expand and show the full bilingual summary. Collapse on click again.
- **Stats bar**: At the top — "Today's Briefing: [date] | 6 stories | Reading time: ~3 min"
- **Animation**: Cards slide in from bottom on load with stagger. Category tags have a subtle glow effect.
- **Ad-ready zone**: `<div id="ad-slot-hero">` between hero and grid. `<div id="ad-slot-mid">` between 3rd and 4th story. `<div id="ad-slot-bottom">` at footer.
- **Footer**: "Powered by ClawCode"
## Content Guidelines
- Focus on stories that matter to builders and investors
- Include at least one China/Asia tech story
- Include at least one AI/ML specific story
- Avoid celebrity gossip disguised as tech news
- Source from credible outlets (TechCrunch, The Verge, Wired, 36Kr, etc.)
## Output
Save as `/mnt/user-data/outputs/daily-tech.html` and present to user.
---
## 推送管理
```bash
# 开启每日推送(早晚各一次)
node scripts/push-toggle.js on <userId>
# 自定义时间和渠道
node scripts/push-toggle.js on <userId> --morning 08:00 --evening 20:00 --channel feishu
# 关闭推送
node scripts/push-toggle.js off <userId>
# 查看推送状态
node scripts/push-toggle.js status <userId>
```
支持渠道:`telegram` / `feishu` / `slack` / `discord`
FILE:README.md
# Daily Tech — AI & Tech News Briefing Skill
> Daily AI and tech news in a visual card — product launches, startup news, LLM updates. Bilingual EN/CN.
[](https://clawhub.ai/skills/daily-tech)
[](https://openclaw.ai)
## What it does
Daily Tech delivers a concise tech and AI news briefing every morning — covering the latest in large language models, product launches, startup funding rounds, and developer tools. Output is a clean bilingual visual card you can share directly.
**Morning push** — 5 top tech/AI stories with visual card
**Evening recap** — day's most significant tech development
**Focus areas** — AI/LLM, startups, big tech, developer tools, consumer tech
**Bilingual** — Chinese and English
## Installation
```bash
openclaw install daily-tech
```
## Usage
```bash
openclaw run daily-tech morning
openclaw run daily-tech evening
```
## Keywords
tech news · AI news · daily tech · startup news · LLM news · product launch · developer news · 科技新闻 · AI新闻 · 科技日报 · 大模型 · 今日科技 · 创业新闻
---
Built for [OpenClaw](https://openclaw.ai) · Published on [clawhub.ai](https://clawhub.ai/skills/daily-tech)
FILE:scripts/evening-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
const VOCAB_THEMES=["情感词汇", "科学词汇", "艺术文化", "商业词汇", "自然生态", "哲学词汇", "日常生活"];
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('❌ 无效userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('❌ 非法路径');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{}}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const MONTHS_EN=['January','February','March','April','May','June','July','August','September','October','November','December'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const month_en=MONTHS_EN[now.getMonth()];
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
const vocab_theme=VOCAB_THEMES[wd];
const tomorrow_vocab_theme=VOCAB_THEMES[(wd+1)%7];
console.log(`请搜索今日(date)科技收官动态,生成科技晚报。包含:①今日科技TOP3事件深度复盘②今日数据看点③明日值得关注的科技事件预告。比早报更深度,适合睡前阅读。中英双语。`);
FILE:scripts/morning-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
const VOCAB_THEMES=["情感词汇", "科学词汇", "艺术文化", "商业词汇", "自然生态", "哲学词汇", "日常生活"];
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('❌ 无效userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('❌ 非法路径');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{}}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const MONTHS_EN=['January','February','March','April','May','June','July','August','September','October','November','December'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const month_en=MONTHS_EN[now.getMonth()];
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
const vocab_theme=VOCAB_THEMES[wd];
const tomorrow_vocab_theme=VOCAB_THEMES[(wd+1)%7];
console.log(`请搜索今日(date)最新科技与AI新闻,生成科技早报。搜索AI news today、tech startup news today、China tech news today。精选5-6条(AI/ML突破、重大产品发布、50M+融资、政策、收购),每条含分类标签、标题(中英)、2-3句摘要、热度标记(🔥⭐📌)。`);
FILE:scripts/push-toggle.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const SKILL='daily-tech',DEFAULT_MORNING='08:00',DEFAULT_EVENING='20:00';
const USERS_DIR=path.join(__dirname,'../data/users');
const ALLOWED_CH=new Set(['telegram','feishu','slack','discord']);
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('❌ 无效userId');process.exit(1);}return v;}
function sanitizeTime(v,l){if(!/^\d{1,2}:\d{2}$/.test(v)){console.error('❌ 无效'+l);process.exit(1);}const[h,m]=v.split(':').map(Number);if(h>23||m>59){console.error('❌ 无效'+l);process.exit(1);}return{h,m};}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('❌ 非法路径');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{}}
function saveUser(u,d){fs.mkdirSync(USERS_DIR,{recursive:true});fs.writeFileSync(safeUserPath(u),JSON.stringify(d,null,2),'utf8');}
function enablePush(userId,opts){
userId=sanitizeId(userId);
const user=loadUser(userId);
const mt=opts.morning||user.morningTime||DEFAULT_MORNING;
const et=opts.evening||user.eveningTime||DEFAULT_EVENING;
const{h:mh,m:mm}=sanitizeTime(mt,'--morning');
const{h:eh,m:em}=sanitizeTime(et,'--evening');
const ch=opts.channel||user.channel||'telegram';
if(!ALLOWED_CH.has(ch)){console.error('❌ 不支持渠道:'+ch);process.exit(1);}
const sk=`agent:main:ch:direct:userId`;
console.log('__OPENCLAW_CRON_ADD__:'+JSON.stringify({name:`SKILL-morning-userId`,cronExpr:`mm mh * * *`,tz:'Asia/Shanghai',session:'isolated',sessionKey:sk,channel:ch,to:userId,announce:true,timeoutSeconds:180,message:`node path.join(__dirname,'morning-push.js') userId`}));
console.log('__OPENCLAW_CRON_ADD__:'+JSON.stringify({name:`SKILL-evening-userId`,cronExpr:`em eh * * *`,tz:'Asia/Shanghai',session:'isolated',sessionKey:sk,channel:ch,to:userId,announce:true,timeoutSeconds:180,message:`node path.join(__dirname,'evening-push.js') userId`}));
saveUser(userId,{...user,morningTime:mt,eveningTime:et,channel:ch,pushEnabled:true,updatedAt:new Date().toISOString()});
console.log(`\n✅ SKILL 推送已开启\n⏰ 早推: mt 🌙 晚推: et 📡 渠道: ch\n关闭: node push-toggle.js off userId`);
}
function disablePush(userId){
userId=sanitizeId(userId);
console.log(`__OPENCLAW_CRON_RM__:SKILL-morning-userId`);
console.log(`__OPENCLAW_CRON_RM__:SKILL-evening-userId`);
const user=loadUser(userId);
saveUser(userId,{...user,pushEnabled:false,updatedAt:new Date().toISOString()});
console.log(`✅ SKILL 推送已关闭`);
}
function showStatus(userId){
userId=sanitizeId(userId);
const u=loadUser(userId);
console.log(`\n📡 SKILL — userId\n状态: '❌ 关闭' 早推: u.morningTime||DEFAULT_MORNING 晚推: u.eveningTime||DEFAULT_EVENING 渠道: u.channel||'telegram'\n`);
}
if(require.main!==module)return;
const[cmd,uid,...rest]=process.argv.slice(2);
if(!cmd||!uid){console.log('用法: node push-toggle.js on|off|status <userId>');process.exit(1);}
const opts={};
const mi=rest.indexOf('--morning');if(mi!==-1)opts.morning=rest[mi+1];
const ei=rest.indexOf('--evening');if(ei!==-1)opts.evening=rest[ei+1];
const ci=rest.indexOf('--channel');if(ci!==-1)opts.channel=rest[ci+1];
if(cmd==='on')enablePush(uid,opts);
else if(cmd==='off')disablePush(uid);
else if(cmd==='status')showStatus(uid);
else{console.error('❌ 未知命令:'+cmd);process.exit(1);}
Daily recipe recommendation — Chinese, Western, and fusion cuisines on rotation. Full ingredients, step-by-step instructions, 30–60 min home-cook meals.
---
name: daily-recipe
description: "Daily recipe recommendation — Chinese, Western, and fusion cuisines on rotation. Full ingredients, step-by-step instructions, 30–60 min home-cook meals."
keywords:
- 今日食谱
- 今天吃什么
- 做什么菜
- 食谱推荐
- 每日菜谱
- 晚餐吃什么
- 家常菜
- 菜谱
- 烹饪
- 午餐
- 早餐
- daily recipe
- what to cook
- recipe of the day
- dinner ideas
- lunch recipe
- cooking
- meal idea
- home cooking
metadata:
openclaw:
runtime:
node: ">=18"
---
# Daily Recipe / 今日食谱
Generate a beautiful daily recipe card with bilingual content and step-by-step cooking instructions.
## Workflow
1. **Get today's date** — Use day of week and season to determine cuisine:
- Mon: Chinese home cooking (家常菜)
- Tue: Japanese/Korean
- Wed: Italian/Mediterranean
- Thu: Southeast Asian (Thai/Vietnamese)
- Fri: American/Mexican
- Sat: French/European fine dining (simplified)
- Sun: Brunch/Baking special
- Season affects ingredients: light/cold dishes in summer, warm/hearty in winter.
2. **Select a dish** — Use `web_search` to find a specific recipe that fits today's theme. Query: `"easy [cuisine] recipe [season]"`. Pick something achievable in 30-60 min.
3. **Format the recipe** — Ingredients list, step-by-step instructions, cooking tips. All bilingual.
4. **Generate the visual** — Create a single-file HTML artifact.
## Visual Design Requirements
Create a food-magazine quality recipe page:
- **Layout**: Full-page editorial style. Hero section with dish name and description, then ingredients sidebar + steps main column.
- **Typography**: Warm, editorial fonts (e.g., Playfair Display for titles, Nunito for body). Food should feel inviting.
- **Color scheme**: Warm, appetizing palette — terracotta, olive, cream, warm brown. Or fresh palette for salads/light dishes — mint, white, light yellow. Rotate based on dish type.
- **Recipe header**: Dish name (EN + CN, large), cuisine tag, prep time, cook time, servings, difficulty (1-3 🔥).
- **Ingredients**: Clean list with checkboxes (interactive — click to mark as gathered). Quantities in both metric and imperial.
- **Steps**: Numbered steps with clear formatting. Key actions bolded. Timer suggestions noted.
- **Tips section**: Chef's tips, substitution suggestions, storage advice.
- **Nutrition estimate**: Approximate calories, protein, carbs, fat per serving in a small info box.
- **Ad-ready zone**: `<div id="ad-slot-sidebar">` in the ingredients column area. `<div id="ad-slot-bottom">` after the recipe.
- **Footer**: "Powered by ClawCode"
## Content Guidelines
- Recipes should be achievable by home cooks (not restaurant-level complexity)
- Always include vegetarian substitution where possible
- Chinese dishes should include authentic ingredients with common substitutes noted
- Keep ingredient lists under 15 items
- Steps should be clear and numbered (8-12 steps max)
## Output
Save as `/mnt/user-data/outputs/daily-recipe.html` and present to user.
---
## 推送管理
```bash
# 开启每日推送(早晚各一次)
node scripts/push-toggle.js on <userId>
# 自定义时间和渠道
node scripts/push-toggle.js on <userId> --morning 08:00 --evening 20:00 --channel feishu
# 关闭推送
node scripts/push-toggle.js off <userId>
# 查看推送状态
node scripts/push-toggle.js status <userId>
```
支持渠道:`telegram` / `feishu` / `slack` / `discord`
FILE:README.md
# Daily Recipe — Daily Recipe Recommendation Skill
> One curated recipe every day — Chinese and Western cuisines, with ingredients, steps, and visual card.
[](https://clawhub.ai/skills/daily-recipe)
[](https://openclaw.ai)
## What it does
Daily Recipe solves the "what should I cook today?" question with a fresh recipe recommendation every morning — alternating between Chinese home cooking, Western dishes, and fusion cuisine. Includes full ingredients list, step-by-step instructions, and estimated cook time.
**Morning push** — today's recipe with ingredients and steps
**Variety** — Chinese, Western, and fusion cuisines on rotation
**Practical** — designed for home cooks, 30–60 minute meals
**Visual** — dish card format
## Installation
```bash
openclaw install daily-recipe
```
## Usage
```bash
openclaw run daily-recipe morning
openclaw run daily-recipe evening
```
## Keywords
daily recipe · recipe of the day · what to cook · dinner ideas · home cooking · Chinese cooking · 今日食谱 · 今天吃什么 · 做什么菜 · 食谱推荐 · 晚餐吃什么 · 家常菜
---
Built for [OpenClaw](https://openclaw.ai) · Published on [clawhub.ai](https://clawhub.ai/skills/daily-recipe)
FILE:scripts/evening-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
const VOCAB_THEMES=["情感词汇", "科学词汇", "艺术文化", "商业词汇", "自然生态", "哲学词汇", "日常生活"];
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('❌ 无效userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('❌ 非法路径');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{}}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const MONTHS_EN=['January','February','March','April','May','June','July','August','September','October','November','December'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const month_en=MONTHS_EN[now.getMonth()];
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
const vocab_theme=VOCAB_THEMES[wd];
const tomorrow_vocab_theme=VOCAB_THEMES[(wd+1)%7];
console.log(`晚餐时间到🍽️今天是date。推荐一道30分钟内可完成的快手菜:菜名(中英)、食材(≤8项)、5步极简做法。同时预告明天(tomorrow_weekday)的菜系主题,勾起期待。轻松愉快语气。`);
FILE:scripts/morning-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
const VOCAB_THEMES=["情感词汇", "科学词汇", "艺术文化", "商业词汇", "自然生态", "哲学词汇", "日常生活"];
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('❌ 无效userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('❌ 非法路径');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{}}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const MONTHS_EN=['January','February','March','April','May','June','July','August','September','October','November','December'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const month_en=MONTHS_EN[now.getMonth()];
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
const vocab_theme=VOCAB_THEMES[wd];
const tomorrow_vocab_theme=VOCAB_THEMES[(wd+1)%7];
console.log(`今天是weekday(date),请推荐今日菜谱。轮换菜系(周一中式家常/周二日韩/周三意式/周四东南亚/周五美式/周六法式/周日早午餐)。搜索一道具体菜品,给出:菜名(中英)、难度🔥、时间、食材(≤12项)、步骤(8-10步)、大厨贴士2条、素食替代方案。中英双语。`);
FILE:scripts/push-toggle.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const SKILL='daily-recipe',DEFAULT_MORNING='08:30',DEFAULT_EVENING='17:30';
const USERS_DIR=path.join(__dirname,'../data/users');
const ALLOWED_CH=new Set(['telegram','feishu','slack','discord']);
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('❌ 无效userId');process.exit(1);}return v;}
function sanitizeTime(v,l){if(!/^\d{1,2}:\d{2}$/.test(v)){console.error('❌ 无效'+l);process.exit(1);}const[h,m]=v.split(':').map(Number);if(h>23||m>59){console.error('❌ 无效'+l);process.exit(1);}return{h,m};}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('❌ 非法路径');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{}}
function saveUser(u,d){fs.mkdirSync(USERS_DIR,{recursive:true});fs.writeFileSync(safeUserPath(u),JSON.stringify(d,null,2),'utf8');}
function enablePush(userId,opts){
userId=sanitizeId(userId);
const user=loadUser(userId);
const mt=opts.morning||user.morningTime||DEFAULT_MORNING;
const et=opts.evening||user.eveningTime||DEFAULT_EVENING;
const{h:mh,m:mm}=sanitizeTime(mt,'--morning');
const{h:eh,m:em}=sanitizeTime(et,'--evening');
const ch=opts.channel||user.channel||'telegram';
if(!ALLOWED_CH.has(ch)){console.error('❌ 不支持渠道:'+ch);process.exit(1);}
const sk=`agent:main:ch:direct:userId`;
console.log('__OPENCLAW_CRON_ADD__:'+JSON.stringify({name:`SKILL-morning-userId`,cronExpr:`mm mh * * *`,tz:'Asia/Shanghai',session:'isolated',sessionKey:sk,channel:ch,to:userId,announce:true,timeoutSeconds:180,message:`node path.join(__dirname,'morning-push.js') userId`}));
console.log('__OPENCLAW_CRON_ADD__:'+JSON.stringify({name:`SKILL-evening-userId`,cronExpr:`em eh * * *`,tz:'Asia/Shanghai',session:'isolated',sessionKey:sk,channel:ch,to:userId,announce:true,timeoutSeconds:180,message:`node path.join(__dirname,'evening-push.js') userId`}));
saveUser(userId,{...user,morningTime:mt,eveningTime:et,channel:ch,pushEnabled:true,updatedAt:new Date().toISOString()});
console.log(`\n✅ SKILL 推送已开启\n⏰ 早推: mt 🌙 晚推: et 📡 渠道: ch\n关闭: node push-toggle.js off userId`);
}
function disablePush(userId){
userId=sanitizeId(userId);
console.log(`__OPENCLAW_CRON_RM__:SKILL-morning-userId`);
console.log(`__OPENCLAW_CRON_RM__:SKILL-evening-userId`);
const user=loadUser(userId);
saveUser(userId,{...user,pushEnabled:false,updatedAt:new Date().toISOString()});
console.log(`✅ SKILL 推送已关闭`);
}
function showStatus(userId){
userId=sanitizeId(userId);
const u=loadUser(userId);
console.log(`\n📡 SKILL — userId\n状态: '❌ 关闭' 早推: u.morningTime||DEFAULT_MORNING 晚推: u.eveningTime||DEFAULT_EVENING 渠道: u.channel||'telegram'\n`);
}
if(require.main!==module)return;
const[cmd,uid,...rest]=process.argv.slice(2);
if(!cmd||!uid){console.log('用法: node push-toggle.js on|off|status <userId>');process.exit(1);}
const opts={};
const mi=rest.indexOf('--morning');if(mi!==-1)opts.morning=rest[mi+1];
const ei=rest.indexOf('--evening');if(ei!==-1)opts.evening=rest[ei+1];
const ci=rest.indexOf('--channel');if(ci!==-1)opts.channel=rest[ci+1];
if(cmd==='on')enablePush(uid,opts);
else if(cmd==='off')disablePush(uid);
else if(cmd==='status')showStatus(uid);
else{console.error('❌ 未知命令:'+cmd);process.exit(1);}
Daily curated quote from philosophers, leaders, and thinkers — with author context and why it resonates today. Bilingual EN/CN shareable visual card.
---
name: daily-quote
description: "Daily curated quote from philosophers, leaders, and thinkers — with author context and why it resonates today. Bilingual EN/CN shareable visual card."
keywords:
- 每日金句
- 今日金句
- 名言
- 励志
- 鸡汤
- 早安语录
- 每日名言
- 人生格言
- daily quote
- quote of the day
- motivational quote
- morning motivation
- inspirational quote
- wisdom
- daily inspiration
metadata:
openclaw:
runtime:
node: ">=18"
---
# Daily Quote / 每日金句
Generate a beautiful daily inspirational quote card with bilingual (Chinese + English) presentation.
## Workflow
1. **Get today's date** — Use the current date to seed the quote selection.
2. **Search for a quote** — Use `web_search` to find an inspiring, thought-provoking quote. Search for quotes related to today's date, or a rotating theme (Monday=courage, Tuesday=wisdom, Wednesday=creativity, Thursday=perseverance, Friday=joy, Saturday=love, Sunday=reflection). Query example: `"inspirational quote [theme] famous"`.
3. **Select and translate** — Pick ONE powerful quote. Provide both English original and Chinese translation. Include the author name and brief context (1 line).
4. **Generate the visual card** — Create a single-file HTML artifact (saved to `/mnt/user-data/outputs/daily-quote.html`) with:
## Visual Design Requirements
Create a stunning, full-viewport quote card. Each day should feel different:
- **Layout**: Centered quote with generous whitespace. Author below. Date at top-right corner.
- **Typography**: Use a distinctive serif or display font from Google Fonts (rotate between: Playfair Display, Cormorant Garamond, DM Serif Display, Libre Baskerville). Body text in a clean sans-serif.
- **Background**: Rotating aesthetic — use CSS gradients, subtle patterns, or mesh gradients. Avoid plain white. Examples: warm sunset gradient, deep ocean tones, forest green to black, golden hour warmth.
- **Bilingual display**: English quote prominent, Chinese translation below in slightly smaller size with a different weight.
- **Micro-interactions**: Subtle fade-in animation on load. Quote text should animate in with a gentle reveal.
- **Ad-ready zone**: Include a tasteful, empty `<div id="ad-slot-bottom" style="...">` at the bottom of the card (min-height 90px, centered, with a subtle dashed border in dev mode). This is the future ad placement area.
- **Footer**: Small "Powered by ClawCode" text at the very bottom.
## Content Tone
- Prefer quotes from diverse sources: Eastern and Western philosophers, scientists, writers, leaders
- Avoid overly cliché quotes (no "be the change you wish to see" etc.)
- Each quote should genuinely make someone pause and think
## Output
Save as `/mnt/user-data/outputs/daily-quote.html` and present to user.
---
## 推送管理
```bash
# 开启每日推送(早晚各一次)
node scripts/push-toggle.js on <userId>
# 自定义时间和渠道
node scripts/push-toggle.js on <userId> --morning 08:00 --evening 20:00 --channel feishu
# 关闭推送
node scripts/push-toggle.js off <userId>
# 查看推送状态
node scripts/push-toggle.js status <userId>
```
支持渠道:`telegram` / `feishu` / `slack` / `discord`
FILE:README.md
# Daily Quote — Inspirational Quote Skill
> Daily curated quotes — bilingual EN/CN visual card, ready to share.
[](https://clawhub.ai/skills/daily-quote)
[](https://openclaw.ai)
## What it does
Daily Quote delivers one carefully chosen quote every day — from philosophers, leaders, writers, and thinkers — with a short explanation of its context and why it resonates today. Output is a beautiful bilingual card.
**Morning push** — quote of the day with author and context
**Themes** — wisdom, resilience, creativity, leadership, life
**Bilingual** — Chinese and English
**Visual** — shareable card format
## Installation
```bash
openclaw install daily-quote
```
## Usage
```bash
openclaw run daily-quote morning
```
## Keywords
daily quote · quote of the day · motivational quote · morning motivation · inspirational quote · 每日金句 · 今日金句 · 名言 · 励志 · 早安语录 · 鸡汤
---
Built for [OpenClaw](https://openclaw.ai) · Published on [clawhub.ai](https://clawhub.ai/skills/daily-quote)
FILE:scripts/evening-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
const VOCAB_THEMES=["情感词汇", "科学词汇", "艺术文化", "商业词汇", "自然生态", "哲学词汇", "日常生活"];
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('❌ 无效userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('❌ 非法路径');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{}}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const MONTHS_EN=['January','February','March','April','May','June','July','August','September','October','November','December'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const month_en=MONTHS_EN[now.getMonth()];
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
const vocab_theme=VOCAB_THEMES[wd];
const tomorrow_vocab_theme=VOCAB_THEMES[(wd+1)%7];
console.log(`晚安语录🌙今天是date。请选一句关于结束与新开始、休息、今日与明日主题的晚间金句,来自哲学家、作家或诗人。原文+中文翻译,作者简介,3句睡前冥想引导。语气沉静,适合睡前阅读。`);
FILE:scripts/morning-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
const VOCAB_THEMES=["情感词汇", "科学词汇", "艺术文化", "商业词汇", "自然生态", "哲学词汇", "日常生活"];
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('❌ 无效userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('❌ 非法路径');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{}}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const MONTHS_EN=['January','February','March','April','May','June','July','August','September','October','November','December'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const month_en=MONTHS_EN[now.getMonth()];
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
const vocab_theme=VOCAB_THEMES[wd];
const tomorrow_vocab_theme=VOCAB_THEMES[(wd+1)%7];
console.log(`早安!今天是weekday(date)。根据今日主题(周一勇气/周二智慧/周三创造/周四坚持/周五喜悦/周六爱/周日反思)搜索一句精选名言。原文+中文翻译,作者及简介1句,解读2-3句,附今日行动提示。`);
FILE:scripts/push-toggle.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const SKILL='daily-quote',DEFAULT_MORNING='08:00',DEFAULT_EVENING='21:00';
const USERS_DIR=path.join(__dirname,'../data/users');
const ALLOWED_CH=new Set(['telegram','feishu','slack','discord']);
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('❌ 无效userId');process.exit(1);}return v;}
function sanitizeTime(v,l){if(!/^\d{1,2}:\d{2}$/.test(v)){console.error('❌ 无效'+l);process.exit(1);}const[h,m]=v.split(':').map(Number);if(h>23||m>59){console.error('❌ 无效'+l);process.exit(1);}return{h,m};}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('❌ 非法路径');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{}}
function saveUser(u,d){fs.mkdirSync(USERS_DIR,{recursive:true});fs.writeFileSync(safeUserPath(u),JSON.stringify(d,null,2),'utf8');}
function enablePush(userId,opts){
userId=sanitizeId(userId);
const user=loadUser(userId);
const mt=opts.morning||user.morningTime||DEFAULT_MORNING;
const et=opts.evening||user.eveningTime||DEFAULT_EVENING;
const{h:mh,m:mm}=sanitizeTime(mt,'--morning');
const{h:eh,m:em}=sanitizeTime(et,'--evening');
const ch=opts.channel||user.channel||'telegram';
if(!ALLOWED_CH.has(ch)){console.error('❌ 不支持渠道:'+ch);process.exit(1);}
const sk=`agent:main:ch:direct:userId`;
console.log('__OPENCLAW_CRON_ADD__:'+JSON.stringify({name:`SKILL-morning-userId`,cronExpr:`mm mh * * *`,tz:'Asia/Shanghai',session:'isolated',sessionKey:sk,channel:ch,to:userId,announce:true,timeoutSeconds:180,message:`node path.join(__dirname,'morning-push.js') userId`}));
console.log('__OPENCLAW_CRON_ADD__:'+JSON.stringify({name:`SKILL-evening-userId`,cronExpr:`em eh * * *`,tz:'Asia/Shanghai',session:'isolated',sessionKey:sk,channel:ch,to:userId,announce:true,timeoutSeconds:180,message:`node path.join(__dirname,'evening-push.js') userId`}));
saveUser(userId,{...user,morningTime:mt,eveningTime:et,channel:ch,pushEnabled:true,updatedAt:new Date().toISOString()});
console.log(`\n✅ SKILL 推送已开启\n⏰ 早推: mt 🌙 晚推: et 📡 渠道: ch\n关闭: node push-toggle.js off userId`);
}
function disablePush(userId){
userId=sanitizeId(userId);
console.log(`__OPENCLAW_CRON_RM__:SKILL-morning-userId`);
console.log(`__OPENCLAW_CRON_RM__:SKILL-evening-userId`);
const user=loadUser(userId);
saveUser(userId,{...user,pushEnabled:false,updatedAt:new Date().toISOString()});
console.log(`✅ SKILL 推送已关闭`);
}
function showStatus(userId){
userId=sanitizeId(userId);
const u=loadUser(userId);
console.log(`\n📡 SKILL — userId\n状态: '❌ 关闭' 早推: u.morningTime||DEFAULT_MORNING 晚推: u.eveningTime||DEFAULT_EVENING 渠道: u.channel||'telegram'\n`);
}
if(require.main!==module)return;
const[cmd,uid,...rest]=process.argv.slice(2);
if(!cmd||!uid){console.log('用法: node push-toggle.js on|off|status <userId>');process.exit(1);}
const opts={};
const mi=rest.indexOf('--morning');if(mi!==-1)opts.morning=rest[mi+1];
const ei=rest.indexOf('--evening');if(ei!==-1)opts.evening=rest[ei+1];
const ci=rest.indexOf('--channel');if(ci!==-1)opts.channel=rest[ci+1];
if(cmd==='on')enablePush(uid,opts);
else if(cmd==='off')disablePush(uid);
else if(cmd==='status')showStatus(uid);
else{console.error('❌ 未知命令:'+cmd);process.exit(1);}
Daily mindfulness in 3–5 minutes — morning breathing exercise, guided meditation, and evening reflection prompt. Practical calm and stress relief for busy pe...
---
name: daily-mindful
description: "Daily mindfulness in 3–5 minutes — morning breathing exercise, guided meditation, and evening reflection prompt. Practical calm and stress relief for busy people."
keywords:
- 每日正念
- 冥想
- 正念
- 减压
- 呼吸练习
- 放松
- 焦虑
- 睡前放松
- 心灵鸡汤
- daily mindfulness
- meditation
- stress relief
- breathing exercise
- daily calm
- relax
- anxiety help
- bedtime calm
- mindfulness
- inner peace
- mental health
metadata:
openclaw:
runtime:
node: ">=18"
---
# Daily Mindfulness / 每日正念
Generate a calming daily mindfulness experience with guided breathing, meditation prompts, and reflective content.
## Workflow
1. **Get today's date** — Use day of week for session type:
- Mon: Setting intentions for the week (新周意念)
- Tue: Gratitude practice (感恩练习)
- Wed: Body scan meditation (身体扫描)
- Thu: Breathing exercise (呼吸练习)
- Fri: Letting go practice (放下练习)
- Sat: Nature connection (自然连接)
- Sun: Weekly reflection (周末回顾)
2. **Create content** — Write a short mindfulness guide (3-5 min read/practice), a daily affirmation, and a reflection prompt. All bilingual.
3. **Generate the visual** — Create a single-file HTML artifact with interactive breathing animation.
## Visual Design Requirements
Create a serene, spa-like digital experience:
- **Layout**: Single scrollable page with generous spacing. Minimal, airy, calming.
- **Typography**: Elegant, gentle fonts (e.g., Cormorant, EB Garamond for headings; Karla or DM Sans for body). Light weight. Ample line spacing.
- **Color scheme**: Ultra-calming — soft lavender + cream, or sage green + off-white, or dawn pink + soft gray. Absolutely no harsh or bright colors. Subtle gradient background.
- **Sections**:
1. Today's theme (with date and a calming emoji like 🌿🕊️☁️)
2. Breathing exercise with INTERACTIVE animated circle (expands on inhale, contracts on exhale, with text guidance: "Breathe in... Hold... Breathe out..."). 4-7-8 or box breathing pattern. A start/stop button.
3. Guided mindfulness text (2-3 paragraphs, EN + CN)
4. Daily affirmation (one powerful sentence, beautifully styled)
5. Reflection prompt (a question to journal about)
- **Animation**: Everything fades in very slowly (1.5s+). Breathing circle pulses with smooth CSS animation. Ambient floating dots or gentle wave animation in background.
- **Optional**: Soft ambient sound toggle (use Web Audio API to generate a simple ambient tone/white noise).
- **Ad-ready zone**: `<div id="ad-slot-bottom">` at the very bottom, well separated from content.
- **Footer**: "Powered by ClawCode"
## Content Guidelines
- Tone: Warm, gentle, non-religious, inclusive
- Draw from diverse traditions: Buddhist mindfulness, Stoic philosophy, modern psychology
- Avoid toxic positivity — acknowledge that hard days are OK
- Chinese content should feel natural, not stiffly translated
- Breathing exercises should have clear timing instructions
## Output
Save as `/mnt/user-data/outputs/daily-mindful.html` and present to user.
---
## 推送管理
```bash
# 开启每日推送(早晚各一次)
node scripts/push-toggle.js on <userId>
# 自定义时间和渠道
node scripts/push-toggle.js on <userId> --morning 08:00 --evening 20:00 --channel feishu
# 关闭推送
node scripts/push-toggle.js off <userId>
# 查看推送状态
node scripts/push-toggle.js status <userId>
```
支持渠道:`telegram` / `feishu` / `slack` / `discord`
FILE:README.md
# Daily Mindful — Mindfulness & Meditation Skill
> Daily mindfulness meditation guide, breathing exercises, and reflection — calm and stress relief.
[](https://clawhub.ai/skills/daily-mindful)
[](https://openclaw.ai)
## What it does
Daily Mindful delivers a gentle daily practice of mindfulness — a guided breathing exercise, a short meditation prompt, and an evening reflection question to wind down. Designed for busy people who have 3–5 minutes, not 30.
**Morning** — breathing exercise + intention for the day
**Evening** — reflection prompt + wind-down meditation
**Techniques** — box breathing, body scan, visualization, gratitude
**Stress relief** — practical tools for anxiety and overwhelm
## Installation
```bash
openclaw install daily-mindful
```
## Usage
```bash
openclaw run daily-mindful morning
openclaw run daily-mindful evening
```
## Keywords
mindfulness · meditation · stress relief · breathing exercise · anxiety · daily mindfulness · mental health · calm · 正念 · 冥想 · 减压 · 呼吸练习 · 放松 · 睡前放松
---
Built for [OpenClaw](https://openclaw.ai) · Published on [clawhub.ai](https://clawhub.ai/skills/daily-mindful)
FILE:scripts/evening-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
const VOCAB_THEMES=["情感词汇", "科学词汇", "艺术文化", "商业词汇", "自然生态", "哲学词汇", "日常生活"];
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('❌ 无效userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('❌ 非法路径');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{}}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const MONTHS_EN=['January','February','March','April','May','June','July','August','September','October','November','December'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const month_en=MONTHS_EN[now.getMonth()];
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
const vocab_theme=VOCAB_THEMES[wd];
const tomorrow_vocab_theme=VOCAB_THEMES[(wd+1)%7];
console.log(`晚安冥想时间🌙今天是date。请生成睡前正念引导:①身体扫描放松(从头到脚5步引导)②今日感恩练习(引导写出3件值得感恩的小事)③明日意念种下(一句积极意念)④4-4-6助眠呼吸引导。整体5-8分钟,语气极度舒缓。`);
FILE:scripts/morning-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
const VOCAB_THEMES=["情感词汇", "科学词汇", "艺术文化", "商业词汇", "自然生态", "哲学词汇", "日常生活"];
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('❌ 无效userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('❌ 非法路径');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{}}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const MONTHS_EN=['January','February','March','April','May','June','July','August','September','October','November','December'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const month_en=MONTHS_EN[now.getMonth()];
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
const vocab_theme=VOCAB_THEMES[wd];
const tomorrow_vocab_theme=VOCAB_THEMES[(wd+1)%7];
console.log(`早安!今天是weekday(date)。请生成早间正念引导:①本周主题意念(周一新开始/周二感恩/周三专注/周四呼吸/周五放下/周六自然/周日回顾)②4-7-8呼吸练习引导③今日意念一句话(中英)④今日正念小提示1条。语气温柔平静。`);
FILE:scripts/push-toggle.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const SKILL='daily-mindful',DEFAULT_MORNING='07:30',DEFAULT_EVENING='21:30';
const USERS_DIR=path.join(__dirname,'../data/users');
const ALLOWED_CH=new Set(['telegram','feishu','slack','discord']);
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('❌ 无效userId');process.exit(1);}return v;}
function sanitizeTime(v,l){if(!/^\d{1,2}:\d{2}$/.test(v)){console.error('❌ 无效'+l);process.exit(1);}const[h,m]=v.split(':').map(Number);if(h>23||m>59){console.error('❌ 无效'+l);process.exit(1);}return{h,m};}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('❌ 非法路径');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{}}
function saveUser(u,d){fs.mkdirSync(USERS_DIR,{recursive:true});fs.writeFileSync(safeUserPath(u),JSON.stringify(d,null,2),'utf8');}
function enablePush(userId,opts){
userId=sanitizeId(userId);
const user=loadUser(userId);
const mt=opts.morning||user.morningTime||DEFAULT_MORNING;
const et=opts.evening||user.eveningTime||DEFAULT_EVENING;
const{h:mh,m:mm}=sanitizeTime(mt,'--morning');
const{h:eh,m:em}=sanitizeTime(et,'--evening');
const ch=opts.channel||user.channel||'telegram';
if(!ALLOWED_CH.has(ch)){console.error('❌ 不支持渠道:'+ch);process.exit(1);}
const sk=`agent:main:ch:direct:userId`;
console.log('__OPENCLAW_CRON_ADD__:'+JSON.stringify({name:`SKILL-morning-userId`,cronExpr:`mm mh * * *`,tz:'Asia/Shanghai',session:'isolated',sessionKey:sk,channel:ch,to:userId,announce:true,timeoutSeconds:180,message:`node path.join(__dirname,'morning-push.js') userId`}));
console.log('__OPENCLAW_CRON_ADD__:'+JSON.stringify({name:`SKILL-evening-userId`,cronExpr:`em eh * * *`,tz:'Asia/Shanghai',session:'isolated',sessionKey:sk,channel:ch,to:userId,announce:true,timeoutSeconds:180,message:`node path.join(__dirname,'evening-push.js') userId`}));
saveUser(userId,{...user,morningTime:mt,eveningTime:et,channel:ch,pushEnabled:true,updatedAt:new Date().toISOString()});
console.log(`\n✅ SKILL 推送已开启\n⏰ 早推: mt 🌙 晚推: et 📡 渠道: ch\n关闭: node push-toggle.js off userId`);
}
function disablePush(userId){
userId=sanitizeId(userId);
console.log(`__OPENCLAW_CRON_RM__:SKILL-morning-userId`);
console.log(`__OPENCLAW_CRON_RM__:SKILL-evening-userId`);
const user=loadUser(userId);
saveUser(userId,{...user,pushEnabled:false,updatedAt:new Date().toISOString()});
console.log(`✅ SKILL 推送已关闭`);
}
function showStatus(userId){
userId=sanitizeId(userId);
const u=loadUser(userId);
console.log(`\n📡 SKILL — userId\n状态: '❌ 关闭' 早推: u.morningTime||DEFAULT_MORNING 晚推: u.eveningTime||DEFAULT_EVENING 渠道: u.channel||'telegram'\n`);
}
if(require.main!==module)return;
const[cmd,uid,...rest]=process.argv.slice(2);
if(!cmd||!uid){console.log('用法: node push-toggle.js on|off|status <userId>');process.exit(1);}
const opts={};
const mi=rest.indexOf('--morning');if(mi!==-1)opts.morning=rest[mi+1];
const ei=rest.indexOf('--evening');if(ei!==-1)opts.evening=rest[ei+1];
const ci=rest.indexOf('--channel');if(ci!==-1)opts.channel=rest[ci+1];
if(cmd==='on')enablePush(uid,opts);
else if(cmd==='off')disablePush(uid);
else if(cmd==='status')showStatus(uid);
else{console.error('❌ 未知命令:'+cmd);process.exit(1);}
Daily no-equipment workout — 5–15 min bodyweight routines (HIIT, yoga, core, mobility) with instructions and streak tracking. Works at home, office, or travel.
---
name: daily-fitness
description: "Daily no-equipment workout — 5–15 min bodyweight routines (HIIT, yoga, core, mobility) with instructions and streak tracking. Works at home, office, or travel."
keywords:
- 今日运动
- 每日锻炼
- 运动推荐
- 健身计划
- 快速运动
- 无器材训练
- 拉伸
- 办公室运动
- 瑜伽
- HIIT
- 核心训练
- 有氧运动
- daily fitness
- daily workout
- quick workout
- exercise routine
- morning workout
- no equipment workout
- home workout
- office workout
- stretch
- yoga
- bodyweight workout
metadata:
openclaw:
runtime:
node: ">=18"
---
# Daily Fitness / 今日运动
Generate a daily workout routine with visual exercise cards and a built-in timer.
## Workflow
1. **Get today's date** — Use day of week to determine workout focus:
- Mon: Upper body (push-ups, planks, arm circles)
- Tue: Core (crunches, leg raises, russian twists)
- Wed: Lower body (squats, lunges, calf raises)
- Thu: Cardio (jumping jacks, high knees, burpees)
- Fri: Full body HIIT
- Sat: Yoga/Flexibility
- Sun: Active recovery/Stretching
2. **Design the routine** — Create 5-7 exercises, each with duration/reps, rest periods. Total time 8-12 minutes. No equipment needed.
3. **Generate the visual** — Create a single-file HTML artifact with interactive timer.
## Visual Design Requirements
Create an energetic, app-like workout interface:
- **Layout**: Scrollable card-based layout. Each exercise is a card with: name (EN + CN), duration/reps, brief description of form.
- **Typography**: Bold, sporty fonts (e.g., Rajdhani, Exo 2, Barlow Condensed). High contrast.
- **Color scheme**: Energetic palette — electric blue + neon green, or warm orange + dark gray. Match the workout type (calming blues for yoga, fiery reds for HIIT).
- **Exercise cards**: Each card shows exercise name, duration ("30 seconds" / "12 reps"), form tip in 1 line, and a text-art or emoji representation of the movement.
- **Interactive timer**: A START button at the top that begins a countdown through all exercises with rest intervals. Visual countdown circle. Audio beep (use Web Audio API for a simple tone) at transitions.
- **Progress bar**: Shows overall workout progress.
- **Stats**: Total workout time, estimated calories, difficulty level (⭐⭐⭐).
- **Ad-ready zone**: `<div id="ad-slot-top">` above the workout. `<div id="ad-slot-bottom">` after completion.
- **Footer**: "Powered by ClawCode"
## Content Guidelines
- All exercises should be doable in a small space (apartment/office friendly)
- No equipment required
- Include form tips to prevent injury
- Provide modifications (easier/harder) for at least 2 exercises
- Bilingual exercise names and instructions
## Output
Save as `/mnt/user-data/outputs/daily-fitness.html` and present to user.
---
## 推送管理
```bash
# 开启每日推送(早晚各一次)
node scripts/push-toggle.js on <userId>
# 自定义时间和渠道
node scripts/push-toggle.js on <userId> --morning 08:00 --evening 20:00 --channel feishu
# 关闭推送
node scripts/push-toggle.js off <userId>
# 查看推送状态
node scripts/push-toggle.js status <userId>
```
支持渠道:`telegram` / `feishu` / `slack` / `discord`
FILE:README.md
# Daily Fitness — No-Equipment Daily Workout Skill
> 5–15 minute daily workout with interactive timer — no gym, no equipment needed.
[](https://clawhub.ai/skills/daily-fitness)
[](https://openclaw.ai)
## What it does
Daily Fitness recommends a fresh workout routine every day — bodyweight exercises you can do anywhere, with movement descriptions and a built-in timer. Routines rotate between cardio, strength, flexibility, and recovery so you're never doing the same thing twice in a row.
**Morning push** — today's workout plan with sets, reps, and rest times
**Evening recap** — log completion, track streaks
**Variety** — HIIT, yoga, core, mobility, office stretches
**No equipment** — designed for home, office, or travel
## Installation
```bash
openclaw install daily-fitness
```
## Usage
```bash
openclaw run daily-fitness morning
openclaw run daily-fitness evening
```
## Keywords
daily workout · no equipment workout · home workout · quick workout · HIIT · bodyweight exercises · office workout · 每日锻炼 · 今日运动 · 健身 · 拉伸 · 无器械运动
---
Built for [OpenClaw](https://openclaw.ai) · Published on [clawhub.ai](https://clawhub.ai/skills/daily-fitness)
FILE:scripts/evening-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
const VOCAB_THEMES=["情感词汇", "科学词汇", "艺术文化", "商业词汇", "自然生态", "哲学词汇", "日常生活"];
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('❌ 无效userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('❌ 非法路径');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{}}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const MONTHS_EN=['January','February','March','April','May','June','July','August','September','October','November','December'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const month_en=MONTHS_EN[now.getMonth()];
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
const vocab_theme=VOCAB_THEMES[wd];
const tomorrow_vocab_theme=VOCAB_THEMES[(wd+1)%7];
console.log(`晚上好!今天是date。请生成:①今日运动打卡提醒②明天(tomorrow_weekday)运动预告——明日主题和3个代表动作预览③睡前5分钟拉伸方案(3个动作)。轻松温馨语气。`);
FILE:scripts/morning-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
const VOCAB_THEMES=["情感词汇", "科学词汇", "艺术文化", "商业词汇", "自然生态", "哲学词汇", "日常生活"];
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('❌ 无效userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('❌ 非法路径');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{}}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const MONTHS_EN=['January','February','March','April','May','June','July','August','September','October','November','December'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const month_en=MONTHS_EN[now.getMonth()];
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
const vocab_theme=VOCAB_THEMES[wd];
const tomorrow_vocab_theme=VOCAB_THEMES[(wd+1)%7];
console.log(`今天是weekday(date),请生成今日运动方案。根据星期确定主题(周一上肢/周二核心/周三下肢/周四有氧/周五全身/周六瑜伽/周日恢复),给出5-7个无器材动作,每个含名称(中英)、时长/组数、动作要领、难度选项。总时长8-12分钟。附激励语。`);
FILE:scripts/push-toggle.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const SKILL='daily-fitness',DEFAULT_MORNING='07:00',DEFAULT_EVENING='21:00';
const USERS_DIR=path.join(__dirname,'../data/users');
const ALLOWED_CH=new Set(['telegram','feishu','slack','discord']);
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('❌ 无效userId');process.exit(1);}return v;}
function sanitizeTime(v,l){if(!/^\d{1,2}:\d{2}$/.test(v)){console.error('❌ 无效'+l);process.exit(1);}const[h,m]=v.split(':').map(Number);if(h>23||m>59){console.error('❌ 无效'+l);process.exit(1);}return{h,m};}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('❌ 非法路径');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{}}
function saveUser(u,d){fs.mkdirSync(USERS_DIR,{recursive:true});fs.writeFileSync(safeUserPath(u),JSON.stringify(d,null,2),'utf8');}
function enablePush(userId,opts){
userId=sanitizeId(userId);
const user=loadUser(userId);
const mt=opts.morning||user.morningTime||DEFAULT_MORNING;
const et=opts.evening||user.eveningTime||DEFAULT_EVENING;
const{h:mh,m:mm}=sanitizeTime(mt,'--morning');
const{h:eh,m:em}=sanitizeTime(et,'--evening');
const ch=opts.channel||user.channel||'telegram';
if(!ALLOWED_CH.has(ch)){console.error('❌ 不支持渠道:'+ch);process.exit(1);}
const sk=`agent:main:ch:direct:userId`;
console.log('__OPENCLAW_CRON_ADD__:'+JSON.stringify({name:`SKILL-morning-userId`,cronExpr:`mm mh * * *`,tz:'Asia/Shanghai',session:'isolated',sessionKey:sk,channel:ch,to:userId,announce:true,timeoutSeconds:180,message:`node path.join(__dirname,'morning-push.js') userId`}));
console.log('__OPENCLAW_CRON_ADD__:'+JSON.stringify({name:`SKILL-evening-userId`,cronExpr:`em eh * * *`,tz:'Asia/Shanghai',session:'isolated',sessionKey:sk,channel:ch,to:userId,announce:true,timeoutSeconds:180,message:`node path.join(__dirname,'evening-push.js') userId`}));
saveUser(userId,{...user,morningTime:mt,eveningTime:et,channel:ch,pushEnabled:true,updatedAt:new Date().toISOString()});
console.log(`\n✅ SKILL 推送已开启\n⏰ 早推: mt 🌙 晚推: et 📡 渠道: ch\n关闭: node push-toggle.js off userId`);
}
function disablePush(userId){
userId=sanitizeId(userId);
console.log(`__OPENCLAW_CRON_RM__:SKILL-morning-userId`);
console.log(`__OPENCLAW_CRON_RM__:SKILL-evening-userId`);
const user=loadUser(userId);
saveUser(userId,{...user,pushEnabled:false,updatedAt:new Date().toISOString()});
console.log(`✅ SKILL 推送已关闭`);
}
function showStatus(userId){
userId=sanitizeId(userId);
const u=loadUser(userId);
console.log(`\n📡 SKILL — userId\n状态: '❌ 关闭' 早推: u.morningTime||DEFAULT_MORNING 晚推: u.eveningTime||DEFAULT_EVENING 渠道: u.channel||'telegram'\n`);
}
if(require.main!==module)return;
const[cmd,uid,...rest]=process.argv.slice(2);
if(!cmd||!uid){console.log('用法: node push-toggle.js on|off|status <userId>');process.exit(1);}
const opts={};
const mi=rest.indexOf('--morning');if(mi!==-1)opts.morning=rest[mi+1];
const ei=rest.indexOf('--evening');if(ei!==-1)opts.evening=rest[ei+1];
const ci=rest.indexOf('--channel');if(ci!==-1)opts.channel=rest[ci+1];
if(cmd==='on')enablePush(uid,opts);
else if(cmd==='off')disablePush(uid);
else if(cmd==='status')showStatus(uid);
else{console.error('❌ 未知命令:'+cmd);process.exit(1);}
Daily financial news briefing — stock markets, crypto, macro economy, Fed/PBOC policy, earnings reports. Bilingual EN/CN visual dashboard. Morning push + eve...
---
name: dailyfinance
description: "Daily financial news briefing — stock markets, crypto, macro economy, Fed/PBOC policy, earnings reports. Bilingual EN/CN visual dashboard. Morning push + evening recap."
keywords:
- 今日财经
- 财经新闻
- 股市
- 每日财经
- 股票市场
- 加密货币
- 比特币
- 宏观经济
- 市场动态
- 美联储
- 财报
- 投资资讯
- A股
- 港股
- 纳斯达克
- 道琼斯
- daily finance
- market news
- stock market
- crypto news
- economic news
- market update
- Fed news
- earnings
- investment news
- financial briefing
- S&P 500
- bitcoin
- Nasdaq
- Hang Seng
metadata:
openclaw:
runtime:
node: ">=18"
---
# Daily Finance / 今日财经
Generate a comprehensive daily financial news briefing with market data and bilingual content.
## Workflow
1. **Search for market data and news** — Use `web_search` to gather:
- `"stock market today S&P 500 Dow"` — major indices
- `"bitcoin crypto market today"` — crypto highlights
- `"financial news today economy"` — macro news
- `"China stock market A shares today"` — China markets
2. **Curate** — Select: 2-3 market data points (US indices, crypto, China/HK), 4-5 top financial stories.
3. **Write summaries** — Each story: headline (EN + CN), 2-sentence summary, impact assessment (bullish/bearish/neutral with emoji 📈📉➡️).
4. **Generate the visual** — Create a single-file HTML artifact.
## Visual Design Requirements
Create a Bloomberg/Financial Times inspired dashboard:
- **Layout**: Dashboard-style. Top row = market ticker strip. Below = news cards in clean grid.
- **Typography**: Professional financial fonts (e.g., Roboto Slab for headlines, Source Sans Pro for body, Fira Code for numbers/data). Numbers should be in a monospace font for alignment.
- **Color scheme**: Professional financial palette — dark navy (#1a1a2e) or off-white with gold (#c9a54e) accents. Green (#22c55e) for gains, Red (#ef4444) for losses. Subtle, authoritative.
- **Market ticker**: Horizontal scrolling strip at top showing: S&P 500, Dow, Nasdaq, BTC, ETH, Shanghai Composite, Hang Seng — with price and % change (color-coded green/red).
- **News cards**: Clean, professional cards. Each has: category badge (Macro / Crypto / Equity / Policy / Earnings), headline, summary, impact tag (📈📉➡️).
- **Market overview section**: Simple mini-charts or bar indicators showing daily performance of major indices.
- **Interactive**: Ticker auto-scrolls. Cards expandable for bilingual detail.
- **Disclaimer**: Small text: "This is a news summary, not financial advice. 本内容仅为资讯摘要,不构成投资建议。"
- **Ad-ready zone**: `<div id="ad-slot-ticker">` below ticker. `<div id="ad-slot-mid">` mid-page. `<div id="ad-slot-bottom">` at footer.
- **Footer**: "Powered by ClawCode"
## Content Guidelines
- Data accuracy is critical — always note that data may be delayed
- Include both US and China/HK market coverage
- Crypto section should be proportionate (1-2 items, not dominating)
- Maintain neutral tone — no predictions or advice
- Note the source for each data point
- Always include the financial disclaimer
## Output
Save as `/mnt/user-data/outputs/daily-finance.html` and present to user.
---
## 推送管理
```bash
# 开启每日推送(早晚各一次)
node scripts/push-toggle.js on <userId>
# 自定义时间和渠道
node scripts/push-toggle.js on <userId> --morning 08:00 --evening 20:00 --channel feishu
# 关闭推送
node scripts/push-toggle.js off <userId>
# 查看推送状态
node scripts/push-toggle.js status <userId>
```
支持渠道:`telegram` / `feishu` / `slack` / `discord`
FILE:README.md
# Daily Finance — Global Financial News Briefing Skill
> Daily stock market, crypto, and macro economy news — bilingual EN/CN visual dashboard.
[](https://clawhub.ai/skills/daily-finance)
[](https://openclaw.ai)
## What it does
Daily Finance delivers a curated global financial news briefing every morning — covering equity markets, cryptocurrency, macro economics, central bank policy, and earnings reports. Output is a bilingual EN/CN visual dashboard you can push to Telegram, Slack, Feishu, or Discord.
**Morning briefing** — top financial headlines, market moves, macro signals
**Evening recap** — day's market summary + what to watch tomorrow
**Coverage** — US/China/global markets, crypto (BTC/ETH/alts), Fed/PBOC policy, earnings, FX
## Installation
```bash
openclaw install daily-finance
```
## Usage
```bash
openclaw run daily-finance morning
openclaw run daily-finance evening
```
## Keywords
financial news · stock market · crypto news · market update · daily finance · economics · Fed news · earnings · 财经新闻 · 股市 · 加密货币 · 宏观经济 · 今日财经 · 美联储 · 财报
---
Built for [OpenClaw](https://openclaw.ai) · Published on [clawhub.ai](https://clawhub.ai/skills/daily-finance)
FILE:scripts/evening-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
const VOCAB_THEMES=["情感词汇", "科学词汇", "艺术文化", "商业词汇", "自然生态", "哲学词汇", "日常生活"];
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('❌ 无效userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('❌ 非法路径');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{}}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const MONTHS_EN=['January','February','March','April','May','June','July','August','September','October','November','December'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const month_en=MONTHS_EN[now.getMonth()];
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
const vocab_theme=VOCAB_THEMES[wd];
const tomorrow_vocab_theme=VOCAB_THEMES[(wd+1)%7];
console.log(`请搜索今日(date)全球财经收盘情况,生成晚间财经复盘。包含:美股/A股/港股收盘数据与涨跌、今日最重磅财经事件3-4条复盘、明日重点关注(数据/财报/事件预告)。中英双语。`);
FILE:scripts/morning-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
const VOCAB_THEMES=["情感词汇", "科学词汇", "艺术文化", "商业词汇", "自然生态", "哲学词汇", "日常生活"];
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('❌ 无效userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('❌ 非法路径');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{}}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const MONTHS_EN=['January','February','March','April','May','June','July','August','September','October','November','December'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const month_en=MONTHS_EN[now.getMonth()];
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
const vocab_theme=VOCAB_THEMES[wd];
const tomorrow_vocab_theme=VOCAB_THEMES[(wd+1)%7];
console.log(`请搜索今日(date)全球财经要闻,生成早间财经简报。包含:美股期货/亚太股市开盘情况、隔夜重要财经新闻3-5条(每条含标题+2句摘要+涨跌影响评级📈📉➡️)、今日重点关注事件(财报/央行会议/经济数据)。中英双语,专业简洁。`);
FILE:scripts/push-toggle.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const SKILL='daily-finance',DEFAULT_MORNING='08:00',DEFAULT_EVENING='20:00';
const USERS_DIR=path.join(__dirname,'../data/users');
const ALLOWED_CH=new Set(['telegram','feishu','slack','discord']);
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('❌ 无效userId');process.exit(1);}return v;}
function sanitizeTime(v,l){if(!/^\d{1,2}:\d{2}$/.test(v)){console.error('❌ 无效'+l);process.exit(1);}const[h,m]=v.split(':').map(Number);if(h>23||m>59){console.error('❌ 无效'+l);process.exit(1);}return{h,m};}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('❌ 非法路径');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{}}
function saveUser(u,d){fs.mkdirSync(USERS_DIR,{recursive:true});fs.writeFileSync(safeUserPath(u),JSON.stringify(d,null,2),'utf8');}
function enablePush(userId,opts){
userId=sanitizeId(userId);
const user=loadUser(userId);
const mt=opts.morning||user.morningTime||DEFAULT_MORNING;
const et=opts.evening||user.eveningTime||DEFAULT_EVENING;
const{h:mh,m:mm}=sanitizeTime(mt,'--morning');
const{h:eh,m:em}=sanitizeTime(et,'--evening');
const ch=opts.channel||user.channel||'telegram';
if(!ALLOWED_CH.has(ch)){console.error('❌ 不支持渠道:'+ch);process.exit(1);}
const sk=`agent:main:ch:direct:userId`;
console.log('__OPENCLAW_CRON_ADD__:'+JSON.stringify({name:`SKILL-morning-userId`,cronExpr:`mm mh * * *`,tz:'Asia/Shanghai',session:'isolated',sessionKey:sk,channel:ch,to:userId,announce:true,timeoutSeconds:180,message:`node path.join(__dirname,'morning-push.js') userId`}));
console.log('__OPENCLAW_CRON_ADD__:'+JSON.stringify({name:`SKILL-evening-userId`,cronExpr:`em eh * * *`,tz:'Asia/Shanghai',session:'isolated',sessionKey:sk,channel:ch,to:userId,announce:true,timeoutSeconds:180,message:`node path.join(__dirname,'evening-push.js') userId`}));
saveUser(userId,{...user,morningTime:mt,eveningTime:et,channel:ch,pushEnabled:true,updatedAt:new Date().toISOString()});
console.log(`\n✅ SKILL 推送已开启\n⏰ 早推: mt 🌙 晚推: et 📡 渠道: ch\n关闭: node push-toggle.js off userId`);
}
function disablePush(userId){
userId=sanitizeId(userId);
console.log(`__OPENCLAW_CRON_RM__:SKILL-morning-userId`);
console.log(`__OPENCLAW_CRON_RM__:SKILL-evening-userId`);
const user=loadUser(userId);
saveUser(userId,{...user,pushEnabled:false,updatedAt:new Date().toISOString()});
console.log(`✅ SKILL 推送已关闭`);
}
function showStatus(userId){
userId=sanitizeId(userId);
const u=loadUser(userId);
console.log(`\n📡 SKILL — userId\n状态: '❌ 关闭' 早推: u.morningTime||DEFAULT_MORNING 晚推: u.eveningTime||DEFAULT_EVENING 渠道: u.channel||'telegram'\n`);
}
if(require.main!==module)return;
const[cmd,uid,...rest]=process.argv.slice(2);
if(!cmd||!uid){console.log('用法: node push-toggle.js on|off|status <userId>');process.exit(1);}
const opts={};
const mi=rest.indexOf('--morning');if(mi!==-1)opts.morning=rest[mi+1];
const ei=rest.indexOf('--evening');if(ei!==-1)opts.evening=rest[ei+1];
const ci=rest.indexOf('--channel');if(ci!==-1)opts.channel=rest[ci+1];
if(cmd==='on')enablePush(uid,opts);
else if(cmd==='off')disablePush(uid);
else if(cmd==='status')showStatus(uid);
else{console.error('❌ 未知命令:'+cmd);process.exit(1);}
Today in history — 3–5 major events on this date with context and lasting impact. Bilingual EN/CN timeline card. Daily morning push.
---
name: daily-history
description: "Today in history — 3–5 major events on this date with context and lasting impact. Bilingual EN/CN timeline card. Daily morning push."
keywords:
- 历史上的今天
- 今天发生了什么
- 历史事件
- 大事记
- 历史
- on this day
- today in history
- this day in history
- historical events today
- history timeline
- world history
- China history
metadata:
openclaw:
runtime:
node: ">=18"
---
# Today in History / 历史上的今天
Generate a beautiful visual timeline of significant historical events that happened on today's date.
## Workflow
1. **Get today's date** — Determine the month and day.
2. **Search for events** — Use `web_search` to find 5-6 notable events that happened on this date across different centuries and categories. Query: `"on this day [month] [day] history events"`. Try to cover: science/tech, politics, culture, sports, and notable births/deaths.
3. **Curate and translate** — Select the 5 most interesting/diverse events. Write each as a concise 1-2 sentence description in both English and Chinese.
4. **Generate the visual** — Create a single-file HTML artifact.
## Visual Design Requirements
Create a vertical timeline layout, full-viewport:
- **Layout**: Vertical timeline with alternating left-right event cards. Timeline line runs down the center. Year markers on the timeline.
- **Typography**: Use a distinguished font pair — a bold condensed display font for years (e.g., Oswald, Bebas Neue) and an elegant body font for descriptions (e.g., Source Serif Pro, Lora).
- **Color scheme**: Deep, rich palette — think aged paper tones, or a modern editorial look with dark backgrounds and gold/amber accents. Rotate themes.
- **Event cards**: Each card has: Year (large), Event title (bold), Description (EN + CN), and a category icon (emoji: 🔬 science, 🏛️ politics, 🎨 culture, ⚽ sports, 👤 people).
- **Animation**: Cards should fade and slide in on load with staggered delays. Timeline line draws itself downward.
- **Header**: "历史上的今天 / Today in History" with today's full date (e.g., "April 2 / 4月2日").
- **Ad-ready zone**: `<div id="ad-slot-middle">` between 3rd and 4th event (min-height 90px, centered). `<div id="ad-slot-bottom">` at page bottom.
- **Footer**: "Powered by ClawCode" at bottom.
## Content Guidelines
- Mix different centuries — don't cluster in one era
- Include at least one event relevant to China or Asia
- Include at least one science/technology event
- Keep descriptions concise but vivid — make history feel alive
## Output
Save as `/mnt/user-data/outputs/daily-history.html` and present to user.
---
## 推送管理
```bash
# 开启每日推送(早晚各一次)
node scripts/push-toggle.js on <userId>
# 自定义时间和渠道
node scripts/push-toggle.js on <userId> --morning 08:00 --evening 20:00 --channel feishu
# 关闭推送
node scripts/push-toggle.js off <userId>
# 查看推送状态
node scripts/push-toggle.js status <userId>
```
支持渠道:`telegram` / `feishu` / `slack` / `discord`
FILE:README.md
# Daily History — Today in History Skill
> Significant events that happened on this date in history — bilingual EN/CN visual timeline.
[](https://clawhub.ai/skills/daily-history)
[](https://openclaw.ai)
## What it does
Daily History surfaces the most significant events that occurred on today's date throughout history — wars, discoveries, births, political milestones, natural disasters — presented as a beautiful bilingual timeline.
**Morning push** — 3–5 major historical events for today's date
**Context** — why each event mattered and its lasting impact
**Bilingual** — Chinese and English output
**Visual** — timeline card format for easy sharing
## Installation
```bash
openclaw install daily-history
```
## Usage
```bash
openclaw run daily-history morning
openclaw run daily-history evening
```
## Keywords
today in history · this day in history · on this day · historical events · daily history · history facts · 历史上的今天 · 今天发生了什么 · 大事记 · 历史事件
---
Built for [OpenClaw](https://openclaw.ai) · Published on [clawhub.ai](https://clawhub.ai/skills/daily-history)
FILE:scripts/evening-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
const VOCAB_THEMES=["情感词汇", "科学词汇", "艺术文化", "商业词汇", "自然生态", "哲学词汇", "日常生活"];
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('❌ 无效userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('❌ 非法路径');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{}}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const MONTHS_EN=['January','February','March','April','May','June','July','August','September','October','November','December'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const month_en=MONTHS_EN[now.getMonth()];
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
const vocab_theme=VOCAB_THEMES[wd];
const tomorrow_vocab_theme=VOCAB_THEMES[(wd+1)%7];
console.log(`今日历史深读(month月day日):从今天的历史事件中选1个最值得深挖的,进行深度回顾。搜索详细资料,呈现:事件背景、关键经过、历史影响、现代启示。中英双语,800字以内,叙事生动。`);
FILE:scripts/morning-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
const VOCAB_THEMES=["情感词汇", "科学词汇", "艺术文化", "商业词汇", "自然生态", "哲学词汇", "日常生活"];
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('❌ 无效userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('❌ 非法路径');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{}}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const MONTHS_EN=['January','February','March','April','May','June','July','August','September','October','November','December'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const month_en=MONTHS_EN[now.getMonth()];
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
const vocab_theme=VOCAB_THEMES[wd];
const tomorrow_vocab_theme=VOCAB_THEMES[(wd+1)%7];
console.log(`请搜索历史上的今天 month月day日 相关内容,生成历史上的今天推送。选取5个跨越不同年代、不同领域的事件,每条含年份、标题(中英)、2句描述。至少含1个中国/亚洲事件、1个科技事件。`);
FILE:scripts/push-toggle.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const SKILL='daily-history',DEFAULT_MORNING='08:00',DEFAULT_EVENING='20:00';
const USERS_DIR=path.join(__dirname,'../data/users');
const ALLOWED_CH=new Set(['telegram','feishu','slack','discord']);
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('❌ 无效userId');process.exit(1);}return v;}
function sanitizeTime(v,l){if(!/^\d{1,2}:\d{2}$/.test(v)){console.error('❌ 无效'+l);process.exit(1);}const[h,m]=v.split(':').map(Number);if(h>23||m>59){console.error('❌ 无效'+l);process.exit(1);}return{h,m};}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('❌ 非法路径');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{}}
function saveUser(u,d){fs.mkdirSync(USERS_DIR,{recursive:true});fs.writeFileSync(safeUserPath(u),JSON.stringify(d,null,2),'utf8');}
function enablePush(userId,opts){
userId=sanitizeId(userId);
const user=loadUser(userId);
const mt=opts.morning||user.morningTime||DEFAULT_MORNING;
const et=opts.evening||user.eveningTime||DEFAULT_EVENING;
const{h:mh,m:mm}=sanitizeTime(mt,'--morning');
const{h:eh,m:em}=sanitizeTime(et,'--evening');
const ch=opts.channel||user.channel||'telegram';
if(!ALLOWED_CH.has(ch)){console.error('❌ 不支持渠道:'+ch);process.exit(1);}
const sk=`agent:main:ch:direct:userId`;
console.log('__OPENCLAW_CRON_ADD__:'+JSON.stringify({name:`SKILL-morning-userId`,cronExpr:`mm mh * * *`,tz:'Asia/Shanghai',session:'isolated',sessionKey:sk,channel:ch,to:userId,announce:true,timeoutSeconds:180,message:`node path.join(__dirname,'morning-push.js') userId`}));
console.log('__OPENCLAW_CRON_ADD__:'+JSON.stringify({name:`SKILL-evening-userId`,cronExpr:`em eh * * *`,tz:'Asia/Shanghai',session:'isolated',sessionKey:sk,channel:ch,to:userId,announce:true,timeoutSeconds:180,message:`node path.join(__dirname,'evening-push.js') userId`}));
saveUser(userId,{...user,morningTime:mt,eveningTime:et,channel:ch,pushEnabled:true,updatedAt:new Date().toISOString()});
console.log(`\n✅ SKILL 推送已开启\n⏰ 早推: mt 🌙 晚推: et 📡 渠道: ch\n关闭: node push-toggle.js off userId`);
}
function disablePush(userId){
userId=sanitizeId(userId);
console.log(`__OPENCLAW_CRON_RM__:SKILL-morning-userId`);
console.log(`__OPENCLAW_CRON_RM__:SKILL-evening-userId`);
const user=loadUser(userId);
saveUser(userId,{...user,pushEnabled:false,updatedAt:new Date().toISOString()});
console.log(`✅ SKILL 推送已关闭`);
}
function showStatus(userId){
userId=sanitizeId(userId);
const u=loadUser(userId);
console.log(`\n📡 SKILL — userId\n状态: '❌ 关闭' 早推: u.morningTime||DEFAULT_MORNING 晚推: u.eveningTime||DEFAULT_EVENING 渠道: u.channel||'telegram'\n`);
}
if(require.main!==module)return;
const[cmd,uid,...rest]=process.argv.slice(2);
if(!cmd||!uid){console.log('用法: node push-toggle.js on|off|status <userId>');process.exit(1);}
const opts={};
const mi=rest.indexOf('--morning');if(mi!==-1)opts.morning=rest[mi+1];
const ei=rest.indexOf('--evening');if(ei!==-1)opts.evening=rest[ei+1];
const ci=rest.indexOf('--channel');if(ci!==-1)opts.channel=rest[ci+1];
if(cmd==='on')enablePush(uid,opts);
else if(cmd==='off')disablePush(uid);
else if(cmd==='status')showStatus(uid);
else{console.error('❌ 未知命令:'+cmd);process.exit(1);}
Daily English learning with spaced repetition — built-in A1–B2 word bank, new words daily, quiz mode (MCQ/fill-in/spelling), streak tracking, level progression.
---
name: english-daily
description: "Daily English learning with spaced repetition — built-in A1–B2 word bank, new words daily, quiz mode (MCQ/fill-in/spelling), streak tracking, level progression."
keywords: 学英语, 英语单词, 今日单词, 英语练习, 英语学习, 词汇, 测验, 打卡, 连续学习, 进度, 间隔重复, 每日推送, English learning, vocabulary, daily words, quiz, streak, spaced repetition, English practice, CEFR, word bank
metadata:
openclaw:
runtime:
node: ">=18"
---
# English Daily
> 私人英语学习助手 — 每日单词 · SRS复习 · 测验打卡 · 进度追踪
## 何时使用
- 用户说"学英语""英语单词""今日单词""英语练习"
- 用户想背单词、做填空、做选择题
- 用户说"测验""我的进度""连续打卡""学习报告"
- 用户说"开启推送""每天推英语单词"
---
## 核心命令
```bash
# 注册(首次使用)
node scripts/register.js <userId> <姓名> [等级 A1/A2/B1/B2] [每日目标 1-20]
# 今日学习(每日推送内容)
node scripts/daily-push.js <userId>
# 测验练习
node scripts/quiz.js <userId> [vocab|sentence|mixed]
# 记录测验积分(Claude 在测验完成后调用)
node scripts/quiz.js <userId> --score <正确题数×10>
# 查看进度
node scripts/progress.js <userId>
# 推送管理
node scripts/push-toggle.js on <userId> [--morning 08:00] [--channel telegram]
node scripts/push-toggle.js off <userId>
node scripts/push-toggle.js status <userId>
```
---
## 学习流程
1. **注册** → `register.js` 创建学习档案(等级、每日目标)
2. **每日学习** → `daily-push.js` 输出今日复习词 + 新词列表
3. **测验** → `quiz.js` 生成5题(词义选择或句子填空),Claude 逐题互动
4. **记分** → 测验完成后 Claude 调用 `--score` 记录积分并更新SRS进度
5. **进度** → `progress.js` 显示连续打卡、掌握词数、升级进度
---
## 推送设置
```bash
node scripts/push-toggle.js on <userId> # 默认 08:00
node scripts/push-toggle.js on <userId> --morning 07:30 --channel feishu
node scripts/push-toggle.js off <userId>
```
支持渠道:`telegram` / `feishu` / `slack` / `discord`
---
## 等级体系
| 等级 | 词汇量 | 升级条件(掌握词数) |
|------|--------|---------------------|
| A1 | ~40词 | 掌握40词 → 升A2 |
| A2 | ~50词 | 掌握90词 → 升B1 |
| B1 | ~40词 | 掌握130词 → 升B2 |
| B2 | ~30词 | 最高等级 |
掌握标准:SRS间隔 ≥ 7天(即多次正确复习)
---
## SRS算法说明
采用简化SM-2间隔重复:
- 质量1(遗忘)/ 质量2(困难)→ 明天复习
- 质量3(掌握)→ 间隔 × 1.5
- 质量4(轻松)→ 间隔 × 2.0
- 最大间隔30天
---
## 注意事项
- 数据存储在 `data/users/<userId>.json`,无外部API依赖
- 内置单词库(A1-B2共约160词),`data/wordbank.json`
- 所有脚本仅使用 Node.js 内置模块(fs/path),无需 npm install
- 用户ID仅允许字母、数字、连字符、下划线(防路径穿越)
FILE:README.md
# English Daily — Daily English Learning Skill
> Daily English learning with spaced repetition — built-in A1–B2 vocabulary, quizzes, streaks, and progress tracking. No API needed.
[](https://clawhub.ai/skills/english-daily)
[](https://openclaw.ai)
## What it does
English Daily is a complete daily English learning system — not just a word of the day, but a full spaced repetition (SRS) vocabulary program with quizzes, streaks, and level progression. Built-in A1–B2 word bank with no external API or installation required.
**Daily push** — new words matched to your current level
**Spaced repetition (SRS)** — reviews scheduled based on your retention
**Quiz mode** — multiple choice, fill-in-the-blank, spelling
**Streak tracking** — daily consistency rewards
**Level progression** — A1 → A2 → B1 → B2
**No dependency** — everything runs locally, no API key needed
## Installation
```bash
openclaw install english-daily
```
## Usage
```bash
# Daily word push
openclaw run english-daily morning
# Quiz mode
openclaw run english-daily quiz
# Check progress and streak
openclaw run english-daily progress
```
## Keywords
English learning · daily English · vocabulary · spaced repetition · SRS · IELTS vocabulary · English quiz · word of the day · English streak · 学英语 · 每日英语 · 英语单词 · 英语练习 · 词汇 · 打卡 · 背单词 · IELTS · TOEFL
---
Built for [OpenClaw](https://openclaw.ai) · Published on [clawhub.ai](https://clawhub.ai/skills/english-daily)
FILE:_meta.json
{
"slug": "english-daily",
"version": "1.0.0",
"runtime": { "node": ">=18" }
}
FILE:data/wordbank.json
[
{"w":"hello","p":"/həˈloʊ/","t":"int.","zh":"你好;喂","lv":"A1","tp":["social","basic"],"ex":[["Hello! How are you?","你好!你怎么样?"],["She said hello to everyone.","她向每个人打招呼。"]]},
{"w":"water","p":"/ˈwɔːtər/","t":"n.","zh":"水","lv":"A1","tp":["food","basic"],"ex":[["I drink water every day.","我每天喝水。"],["The water is cold.","这水很凉。"]]},
{"w":"food","p":"/fuːd/","t":"n.","zh":"食物;食品","lv":"A1","tp":["food","basic"],"ex":[["I like Chinese food.","我喜欢中国食物。"],["Food is important for health.","食物对健康很重要。"]]},
{"w":"family","p":"/ˈfæməli/","t":"n.","zh":"家庭;家人","lv":"A1","tp":["family","basic"],"ex":[["My family is very big.","我的家庭很大。"],["Family is the most important thing.","家庭是最重要的事。"]]},
{"w":"house","p":"/haʊs/","t":"n.","zh":"房子;住宅","lv":"A1","tp":["home","basic"],"ex":[["We live in a big house.","我们住在一栋大房子里。"],["The house is near the school.","这房子在学校附近。"]]},
{"w":"school","p":"/skuːl/","t":"n.","zh":"学校","lv":"A1","tp":["education","basic"],"ex":[["I go to school every day.","我每天去上学。"],["The school is very large.","这所学校很大。"]]},
{"w":"work","p":"/wɜːrk/","t":"n./v.","zh":"工作;劳动","lv":"A1","tp":["work","basic"],"ex":[["She works at a hospital.","她在医院工作。"],["Work hard and you will succeed.","努力工作你就会成功。"]]},
{"w":"friend","p":"/frend/","t":"n.","zh":"朋友;友人","lv":"A1","tp":["social","basic"],"ex":[["He is my best friend.","他是我最好的朋友。"],["Make friends with everyone.","和每个人交朋友。"]]},
{"w":"time","p":"/taɪm/","t":"n.","zh":"时间;时刻","lv":"A1","tp":["general","basic"],"ex":[["What time is it?","现在几点了?"],["Time flies quickly.","时间飞逝。"]]},
{"w":"day","p":"/deɪ/","t":"n.","zh":"天;白天","lv":"A1","tp":["general","basic"],"ex":[["Have a nice day!","祝你今天愉快!"],["I work five days a week.","我每周工作五天。"]]},
{"w":"help","p":"/help/","t":"v./n.","zh":"帮助;援助","lv":"A1","tp":["social","basic"],"ex":[["Can you help me?","你能帮我吗?"],["She helped me with my homework.","她帮我做了作业。"]]},
{"w":"love","p":"/lʌv/","t":"v./n.","zh":"爱;热爱","lv":"A1","tp":["emotion","basic"],"ex":[["I love my family.","我爱我的家人。"],["Love is a beautiful feeling.","爱是一种美好的感受。"]]},
{"w":"money","p":"/ˈmʌni/","t":"n.","zh":"钱;金钱","lv":"A1","tp":["finance","basic"],"ex":[["I don't have much money.","我没有很多钱。"],["Money can't buy happiness.","金钱买不来幸福。"]]},
{"w":"book","p":"/bʊk/","t":"n.","zh":"书;图书","lv":"A1","tp":["education","basic"],"ex":[["I read a book every week.","我每周读一本书。"],["This book is very interesting.","这本书很有趣。"]]},
{"w":"phone","p":"/foʊn/","t":"n.","zh":"电话;手机","lv":"A1","tp":["technology","basic"],"ex":[["My phone is new.","我的手机是新的。"],["She called me on the phone.","她给我打了电话。"]]},
{"w":"happy","p":"/ˈhæpi/","t":"adj.","zh":"高兴的;快乐的","lv":"A1","tp":["emotion","basic"],"ex":[["I am very happy today.","我今天非常高兴。"],["A happy life is a good life.","快乐的生活是美好的生活。"]]},
{"w":"sad","p":"/sæd/","t":"adj.","zh":"悲伤的;难过的","lv":"A1","tp":["emotion","basic"],"ex":[["She felt sad when he left.","他离开时她感到悲伤。"],["Don't be sad, things will get better.","别难过,事情会好转的。"]]},
{"w":"big","p":"/bɪɡ/","t":"adj.","zh":"大的;巨大的","lv":"A1","tp":["general","basic"],"ex":[["That is a very big dog.","那是一只非常大的狗。"],["He has big dreams.","他有远大的梦想。"]]},
{"w":"small","p":"/smɔːl/","t":"adj.","zh":"小的;微小的","lv":"A1","tp":["general","basic"],"ex":[["The room is very small.","这个房间很小。"],["Small steps lead to big changes.","小步骤带来大变化。"]]},
{"w":"good","p":"/ɡʊd/","t":"adj.","zh":"好的;优秀的","lv":"A1","tp":["general","basic"],"ex":[["She is a good student.","她是一个好学生。"],["Have a good time!","玩得开心!"]]},
{"w":"bad","p":"/bæd/","t":"adj.","zh":"坏的;糟糕的","lv":"A1","tp":["general","basic"],"ex":[["The weather is bad today.","今天天气很糟糕。"],["It's not as bad as it seems.","没有看起来那么糟。"]]},
{"w":"eat","p":"/iːt/","t":"v.","zh":"吃;进食","lv":"A1","tp":["food","basic"],"ex":[["I eat breakfast every morning.","我每天早上吃早餐。"],["What do you want to eat?","你想吃什么?"]]},
{"w":"drink","p":"/drɪŋk/","t":"v./n.","zh":"喝;饮料","lv":"A1","tp":["food","basic"],"ex":[["She drinks coffee in the morning.","她早上喝咖啡。"],["Would you like a drink?","你想喝点什么吗?"]]},
{"w":"go","p":"/ɡoʊ/","t":"v.","zh":"去;走","lv":"A1","tp":["general","basic"],"ex":[["I go to work by bus.","我乘公共汽车去上班。"],["Let's go to the park.","我们去公园吧。"]]},
{"w":"come","p":"/kʌm/","t":"v.","zh":"来;来到","lv":"A1","tp":["general","basic"],"ex":[["Please come here.","请到这里来。"],["She came home late.","她很晚才回家。"]]},
{"w":"see","p":"/siː/","t":"v.","zh":"看见;理解","lv":"A1","tp":["general","basic"],"ex":[["I can see the mountains.","我能看见山脉。"],["Do you see what I mean?","你明白我的意思吗?"]]},
{"w":"know","p":"/noʊ/","t":"v.","zh":"知道;了解","lv":"A1","tp":["general","basic"],"ex":[["Do you know her name?","你知道她的名字吗?"],["I know how to cook.","我知道怎么做饭。"]]},
{"w":"want","p":"/wɑːnt/","t":"v.","zh":"想要;希望","lv":"A1","tp":["general","basic"],"ex":[["What do you want?","你想要什么?"],["I want to learn English.","我想学英语。"]]},
{"w":"need","p":"/niːd/","t":"v.","zh":"需要;必须","lv":"A1","tp":["general","basic"],"ex":[["I need your help.","我需要你的帮助。"],["You need to rest more.","你需要多休息。"]]},
{"w":"like","p":"/laɪk/","t":"v./prep.","zh":"喜欢;像","lv":"A1","tp":["emotion","basic"],"ex":[["I like reading books.","我喜欢读书。"],["She sings like an angel.","她唱歌像天使一样。"]]},
{"w":"have","p":"/hæv/","t":"v.","zh":"有;拥有","lv":"A1","tp":["general","basic"],"ex":[["I have two brothers.","我有两个兄弟。"],["Do you have a pen?","你有笔吗?"]]},
{"w":"make","p":"/meɪk/","t":"v.","zh":"制作;使得","lv":"A1","tp":["general","basic"],"ex":[["She makes delicious cake.","她做美味的蛋糕。"],["Music makes me happy.","音乐让我快乐。"]]},
{"w":"get","p":"/ɡet/","t":"v.","zh":"得到;变得","lv":"A1","tp":["general","basic"],"ex":[["I get up at 7 every morning.","我每天早上7点起床。"],["Get some rest tonight.","今晚好好休息。"]]},
{"w":"give","p":"/ɡɪv/","t":"v.","zh":"给予;提供","lv":"A1","tp":["general","basic"],"ex":[["Give me a chance.","给我一次机会。"],["She gave me a gift.","她给了我一份礼物。"]]},
{"w":"take","p":"/teɪk/","t":"v.","zh":"拿;带走","lv":"A1","tp":["general","basic"],"ex":[["Take this book with you.","把这本书带走。"],["It takes two hours to drive there.","开车去那里要两个小时。"]]},
{"w":"find","p":"/faɪnd/","t":"v.","zh":"找到;发现","lv":"A1","tp":["general","basic"],"ex":[["I can't find my keys.","我找不到我的钥匙。"],["She found a new job.","她找到了一份新工作。"]]},
{"w":"ask","p":"/æsk/","t":"v.","zh":"问;询问","lv":"A1","tp":["social","basic"],"ex":[["Ask your teacher for help.","向老师寻求帮助。"],["She asked me a question.","她问了我一个问题。"]]},
{"w":"read","p":"/riːd/","t":"v.","zh":"读;阅读","lv":"A1","tp":["education","basic"],"ex":[["I read every night before bed.","我每晚睡前都读书。"],["Can you read this sign?","你能读懂这个标志吗?"]]},
{"w":"write","p":"/raɪt/","t":"v.","zh":"写;书写","lv":"A1","tp":["education","basic"],"ex":[["Please write your name here.","请在这里写上你的名字。"],["She writes in her diary every day.","她每天写日记。"]]},
{"w":"speak","p":"/spiːk/","t":"v.","zh":"说话;演讲","lv":"A1","tp":["communication","basic"],"ex":[["Can you speak English?","你会说英语吗?"],["She spoke clearly and slowly.","她说得清晰而缓慢。"]]},
{"w":"hotel","p":"/hoʊˈtel/","t":"n.","zh":"酒店;旅馆","lv":"A2","tp":["travel","general"],"ex":[["We stayed at a nice hotel.","我们住在一家很好的酒店。"],["The hotel has a swimming pool.","这家酒店有游泳池。"]]},
{"w":"travel","p":"/ˈtrævəl/","t":"v./n.","zh":"旅行;旅游","lv":"A2","tp":["travel","general"],"ex":[["I love to travel abroad.","我喜欢出国旅行。"],["Travel broadens the mind.","旅行开阔视野。"]]},
{"w":"price","p":"/praɪs/","t":"n.","zh":"价格;价钱","lv":"A2","tp":["finance","general"],"ex":[["What is the price of this?","这个多少钱?"],["The price is too high for me.","这个价格对我来说太高了。"]]},
{"w":"weather","p":"/ˈweðər/","t":"n.","zh":"天气;气候","lv":"A2","tp":["general"],"ex":[["The weather is nice today.","今天天气很好。"],["What is the weather like there?","那里天气怎么样?"]]},
{"w":"health","p":"/helθ/","t":"n.","zh":"健康;卫生","lv":"A2","tp":["health","general"],"ex":[["Good health is the greatest wealth.","健康是最大的财富。"],["Exercise is important for health.","锻炼对健康很重要。"]]},
{"w":"sick","p":"/sɪk/","t":"adj.","zh":"生病的;恶心的","lv":"A2","tp":["health","general"],"ex":[["She stayed home because she was sick.","她因为生病在家休息。"],["I feel sick after eating that.","吃了那个后我感到恶心。"]]},
{"w":"tired","p":"/ˈtaɪərd/","t":"adj.","zh":"疲倦的;厌倦的","lv":"A2","tp":["emotion","health"],"ex":[["I'm very tired after work.","工作后我非常疲倦。"],["She is tired of waiting.","她厌倦了等待。"]]},
{"w":"excited","p":"/ɪkˈsaɪtɪd/","t":"adj.","zh":"兴奋的;激动的","lv":"A2","tp":["emotion","general"],"ex":[["The children are excited about the trip.","孩子们对这次旅行感到兴奋。"],["She was excited to start her new job.","她对开始新工作感到兴奋。"]]},
{"w":"worried","p":"/ˈwʌrid/","t":"adj.","zh":"担心的;忧虑的","lv":"A2","tp":["emotion","general"],"ex":[["He was worried about the exam.","他对考试感到担心。"],["Don't be worried, everything will be fine.","别担心,一切都会好的。"]]},
{"w":"office","p":"/ˈɑːfɪs/","t":"n.","zh":"办公室;事务所","lv":"A2","tp":["work","general"],"ex":[["I work in a big office.","我在一个大办公室工作。"],["The office is on the third floor.","办公室在三楼。"]]},
{"w":"meeting","p":"/ˈmiːtɪŋ/","t":"n.","zh":"会议;会面","lv":"A2","tp":["work","general"],"ex":[["The meeting starts at nine.","会议九点开始。"],["I have an important meeting today.","今天我有一个重要的会议。"]]},
{"w":"important","p":"/ɪmˈpɔːrtənt/","t":"adj.","zh":"重要的;重大的","lv":"A2","tp":["general"],"ex":[["Education is very important.","教育非常重要。"],["This is an important decision.","这是一个重要的决定。"]]},
{"w":"busy","p":"/ˈbɪzi/","t":"adj.","zh":"忙碌的;繁忙的","lv":"A2","tp":["work","general"],"ex":[["I am very busy this week.","这周我很忙。"],["The street is busy with traffic.","街道上交通繁忙。"]]},
{"w":"cheap","p":"/tʃiːp/","t":"adj.","zh":"便宜的;廉价的","lv":"A2","tp":["finance","general"],"ex":[["This restaurant is cheap but good.","这家餐厅便宜但很好。"],["I found a cheap flight to Beijing.","我找到了一张去北京的便宜机票。"]]},
{"w":"expensive","p":"/ɪkˈspensɪv/","t":"adj.","zh":"昂贵的;费钱的","lv":"A2","tp":["finance","general"],"ex":[["That car is too expensive for me.","那辆车对我来说太贵了。"],["Good quality is often expensive.","好质量通常是昂贵的。"]]},
{"w":"arrive","p":"/əˈraɪv/","t":"v.","zh":"到达;抵达","lv":"A2","tp":["travel","general"],"ex":[["What time did you arrive?","你什么时候到达的?"],["She arrived late to the meeting.","她开会迟到了。"]]},
{"w":"depart","p":"/dɪˈpɑːrt/","t":"v.","zh":"出发;离开","lv":"A2","tp":["travel","general"],"ex":[["The train departs at eight.","火车八点出发。"],["We will depart early tomorrow.","我们明天早早出发。"]]},
{"w":"understand","p":"/ˌʌndərˈstænd/","t":"v.","zh":"理解;明白","lv":"A2","tp":["communication","education"],"ex":[["I don't understand this question.","我不理解这个问题。"],["Do you understand what I said?","你明白我说的吗?"]]},
{"w":"explain","p":"/ɪkˈspleɪn/","t":"v.","zh":"解释;说明","lv":"A2","tp":["communication","education"],"ex":[["Can you explain that again?","你能再解释一遍吗?"],["She explained the rules clearly.","她清楚地解释了规则。"]]},
{"w":"practice","p":"/ˈpræktɪs/","t":"v./n.","zh":"练习;实践","lv":"A2","tp":["education","general"],"ex":[["Practice makes perfect.","熟能生巧。"],["I practice English every day.","我每天练习英语。"]]},
{"w":"improve","p":"/ɪmˈpruːv/","t":"v.","zh":"改进;提高","lv":"A2","tp":["general","education"],"ex":[["I want to improve my English.","我想提高我的英语水平。"],["Exercise can improve your health.","锻炼可以改善你的健康。"]]},
{"w":"enjoy","p":"/ɪnˈdʒɔɪ/","t":"v.","zh":"享受;欣赏","lv":"A2","tp":["emotion","general"],"ex":[["I enjoy reading in the park.","我喜欢在公园里读书。"],["Enjoy your vacation!","好好享受你的假期!"]]},
{"w":"forget","p":"/fərˈɡet/","t":"v.","zh":"忘记;遗忘","lv":"A2","tp":["general"],"ex":[["Don't forget to call me.","别忘了给我打电话。"],["I forgot her birthday.","我忘记了她的生日。"]]},
{"w":"remember","p":"/rɪˈmembər/","t":"v.","zh":"记得;记住","lv":"A2","tp":["general"],"ex":[["Do you remember our first meeting?","你还记得我们第一次见面吗?"],["Remember to lock the door.","记得锁门。"]]},
{"w":"decide","p":"/dɪˈsaɪd/","t":"v.","zh":"决定;下决心","lv":"A2","tp":["general"],"ex":[["We decided to move to a new city.","我们决定搬到一个新城市。"],["It's hard to decide which one to choose.","很难决定选哪一个。"]]},
{"w":"agree","p":"/əˈɡriː/","t":"v.","zh":"同意;赞成","lv":"A2","tp":["social","communication"],"ex":[["I agree with your idea.","我同意你的想法。"],["They agreed to meet at noon.","他们同意在中午见面。"]]},
{"w":"disagree","p":"/ˌdɪsəˈɡriː/","t":"v.","zh":"不同意;有异议","lv":"A2","tp":["social","communication"],"ex":[["I disagree with that decision.","我不同意那个决定。"],["They disagree about the best solution.","他们对最佳解决方案有异议。"]]},
{"w":"suggest","p":"/səˈdʒest/","t":"v.","zh":"建议;提议","lv":"A2","tp":["communication","general"],"ex":[["I suggest you see a doctor.","我建议你去看医生。"],["She suggested a new approach.","她提出了一种新方法。"]]},
{"w":"choose","p":"/tʃuːz/","t":"v.","zh":"选择;挑选","lv":"A2","tp":["general"],"ex":[["You can choose any color you like.","你可以选择任何你喜欢的颜色。"],["Choose your friends wisely.","明智地选择朋友。"]]},
{"w":"prefer","p":"/prɪˈfɜːr/","t":"v.","zh":"更喜欢;宁愿","lv":"A2","tp":["emotion","general"],"ex":[["I prefer tea to coffee.","我更喜欢茶而不是咖啡。"],["She prefers working alone.","她更喜欢独自工作。"]]},
{"w":"believe","p":"/bɪˈliːv/","t":"v.","zh":"相信;认为","lv":"A2","tp":["general"],"ex":[["I believe in you.","我相信你。"],["Believe in yourself and your abilities.","相信自己和你的能力。"]]},
{"w":"hope","p":"/hoʊp/","t":"v./n.","zh":"希望;盼望","lv":"A2","tp":["emotion","general"],"ex":[["I hope you feel better soon.","我希望你很快好转。"],["There is always hope.","永远都有希望。"]]},
{"w":"wish","p":"/wɪʃ/","t":"v./n.","zh":"希望;愿望","lv":"A2","tp":["emotion","general"],"ex":[["I wish I could travel more.","我希望我能多旅行。"],["Make a wish!","许个愿!"]]},
{"w":"plan","p":"/plæn/","t":"v./n.","zh":"计划;打算","lv":"A2","tp":["work","general"],"ex":[["What is your plan for tomorrow?","你明天有什么计划?"],["We planned a trip to Japan.","我们计划了一次去日本的旅行。"]]},
{"w":"try","p":"/traɪ/","t":"v.","zh":"尝试;努力","lv":"A2","tp":["general"],"ex":[["Always try your best.","永远尽力而为。"],["Try this new restaurant.","试试这家新餐厅。"]]},
{"w":"learn","p":"/lɜːrn/","t":"v.","zh":"学习;学会","lv":"A2","tp":["education","general"],"ex":[["I am learning English.","我正在学英语。"],["You can learn from your mistakes.","你可以从错误中学习。"]]},
{"w":"teach","p":"/tiːtʃ/","t":"v.","zh":"教;教导","lv":"A2","tp":["education","general"],"ex":[["She teaches math at school.","她在学校教数学。"],["He taught me how to cook.","他教我做饭。"]]},
{"w":"study","p":"/ˈstʌdi/","t":"v./n.","zh":"学习;研究","lv":"A2","tp":["education","general"],"ex":[["I study for two hours every night.","我每晚学习两个小时。"],["Her study of history is impressive.","她对历史的研究令人印象深刻。"]]},
{"w":"review","p":"/rɪˈvjuː/","t":"v./n.","zh":"复习;回顾","lv":"A2","tp":["education","general"],"ex":[["Review your notes before the exam.","考试前复习你的笔记。"],["He wrote a positive review.","他写了一篇正面的评论。"]]},
{"w":"difficult","p":"/ˈdɪfɪkəlt/","t":"adj.","zh":"困难的;难以","lv":"A2","tp":["general"],"ex":[["This problem is very difficult.","这个问题非常困难。"],["It is difficult to learn a new language.","学一门新语言很难。"]]},
{"w":"easy","p":"/ˈiːzi/","t":"adj.","zh":"容易的;简单的","lv":"A2","tp":["general"],"ex":[["The test was easy for me.","这次测试对我来说很容易。"],["Take it easy.","放轻松。"]]},
{"w":"beautiful","p":"/ˈbjuːtɪfəl/","t":"adj.","zh":"美丽的;漂亮的","lv":"A2","tp":["general"],"ex":[["The sunset is beautiful.","日落很美丽。"],["She has a beautiful smile.","她有一个美丽的笑容。"]]},
{"w":"interesting","p":"/ˈɪntrəstɪŋ/","t":"adj.","zh":"有趣的;令人感兴趣的","lv":"A2","tp":["general"],"ex":[["This is a very interesting book.","这是一本非常有趣的书。"],["I find history interesting.","我觉得历史很有趣。"]]},
{"w":"boring","p":"/ˈbɔːrɪŋ/","t":"adj.","zh":"无聊的;乏味的","lv":"A2","tp":["emotion","general"],"ex":[["The lecture was very boring.","这场讲座非常无聊。"],["I don't want to do boring work.","我不想做无聊的工作。"]]},
{"w":"funny","p":"/ˈfʌni/","t":"adj.","zh":"有趣的;可笑的","lv":"A2","tp":["emotion","general"],"ex":[["He told a very funny joke.","他讲了一个非常有趣的笑话。"],["That's not funny at all.","那一点也不好笑。"]]},
{"w":"serious","p":"/ˈsɪriəs/","t":"adj.","zh":"严肃的;认真的","lv":"A2","tp":["emotion","general"],"ex":[["She is always serious about her work.","她对工作总是很认真。"],["This is a serious problem.","这是一个严重的问题。"]]},
{"w":"careful","p":"/ˈkerfəl/","t":"adj.","zh":"小心的;谨慎的","lv":"A2","tp":["general"],"ex":[["Be careful when crossing the street.","过马路要小心。"],["She is a careful driver.","她是一个谨慎的司机。"]]},
{"w":"friendly","p":"/ˈfrendli/","t":"adj.","zh":"友好的;亲切的","lv":"A2","tp":["social","general"],"ex":[["The staff here is very friendly.","这里的员工非常友好。"],["She has a friendly personality.","她性格友好。"]]},
{"w":"achieve","p":"/əˈtʃiːv/","t":"v.","zh":"实现;完成","lv":"B1","tp":["work","general"],"ex":[["She achieved her goal at last.","她终于实现了目标。"],["Hard work helps you achieve success.","努力工作帮助你取得成功。"]]},
{"w":"challenge","p":"/ˈtʃælɪndʒ/","t":"n./v.","zh":"挑战;挑战性","lv":"B1","tp":["general"],"ex":[["Every challenge is an opportunity.","每一个挑战都是一个机会。"],["He challenged himself to run every day.","他挑战自己每天跑步。"]]},
{"w":"opportunity","p":"/ˌɑːpərˈtuːnəti/","t":"n.","zh":"机会;时机","lv":"B1","tp":["work","general"],"ex":[["Don't miss this great opportunity.","不要错过这个好机会。"],["She took every opportunity to practice.","她抓住每一个机会练习。"]]},
{"w":"experience","p":"/ɪkˈspɪriəns/","t":"n./v.","zh":"经历;经验","lv":"B1","tp":["general"],"ex":[["That was a life-changing experience.","那是一次改变人生的经历。"],["Experience is the best teacher.","经验是最好的老师。"]]},
{"w":"success","p":"/səkˈses/","t":"n.","zh":"成功;成就","lv":"B1","tp":["work","general"],"ex":[["Success requires patience and effort.","成功需要耐心和努力。"],["Her success inspired many people.","她的成功激励了很多人。"]]},
{"w":"failure","p":"/ˈfeɪljər/","t":"n.","zh":"失败;不成功","lv":"B1","tp":["general"],"ex":[["Failure is the mother of success.","失败是成功之母。"],["Don't fear failure, learn from it.","不要害怕失败,要从中学习。"]]},
{"w":"relationship","p":"/rɪˈleɪʃənʃɪp/","t":"n.","zh":"关系;人际关系","lv":"B1","tp":["social","general"],"ex":[["Their relationship grew stronger.","他们的关系越来越牢固。"],["Good relationships require communication.","良好的关系需要沟通。"]]},
{"w":"communication","p":"/kəˌmjuːnɪˈkeɪʃən/","t":"n.","zh":"沟通;交流","lv":"B1","tp":["communication","general"],"ex":[["Good communication is key to success.","良好的沟通是成功的关键。"],["Communication skills are important at work.","沟通技巧在工作中很重要。"]]},
{"w":"responsibility","p":"/rɪˌspɑːnsəˈbɪləti/","t":"n.","zh":"责任;责任感","lv":"B1","tp":["work","general"],"ex":[["Take responsibility for your actions.","为你的行为承担责任。"],["Leadership comes with great responsibility.","领导力伴随着巨大的责任。"]]},
{"w":"environment","p":"/ɪnˈvaɪrənmənt/","t":"n.","zh":"环境;自然环境","lv":"B1","tp":["general","society"],"ex":[["We must protect the environment.","我们必须保护环境。"],["The work environment affects productivity.","工作环境影响生产效率。"]]},
{"w":"culture","p":"/ˈkʌltʃər/","t":"n.","zh":"文化;文明","lv":"B1","tp":["society","general"],"ex":[["I am fascinated by Chinese culture.","我对中国文化着迷。"],["Different cultures have different values.","不同文化有不同的价值观。"]]},
{"w":"tradition","p":"/trəˈdɪʃən/","t":"n.","zh":"传统;习俗","lv":"B1","tp":["society","general"],"ex":[["This is an old family tradition.","这是一个古老的家族传统。"],["They celebrate many local traditions.","他们庆祝许多当地传统。"]]},
{"w":"society","p":"/səˈsaɪəti/","t":"n.","zh":"社会;社团","lv":"B1","tp":["society","general"],"ex":[["Education plays a big role in society.","教育在社会中扮演重要角色。"],["A fair society benefits everyone.","公平的社会对每个人都有益。"]]},
{"w":"community","p":"/kəˈmjuːnəti/","t":"n.","zh":"社区;社群","lv":"B1","tp":["society","general"],"ex":[["She volunteers in her community.","她在社区做志愿者。"],["A strong community supports everyone.","一个强大的社区支持每个人。"]]},
{"w":"government","p":"/ˈɡʌvərnmənt/","t":"n.","zh":"政府;政体","lv":"B1","tp":["society","general"],"ex":[["The government passed a new law.","政府通过了一项新法律。"],["People need to trust their government.","人们需要信任他们的政府。"]]},
{"w":"education","p":"/ˌedʒuˈkeɪʃən/","t":"n.","zh":"教育;培训","lv":"B1","tp":["education","general"],"ex":[["Education opens doors to the future.","教育为未来打开大门。"],["Quality education should be available to all.","优质教育应该面向所有人。"]]},
{"w":"career","p":"/kəˈrɪr/","t":"n.","zh":"职业;事业","lv":"B1","tp":["work","general"],"ex":[["She built a successful career in medicine.","她在医学领域建立了成功的职业。"],["Choose a career you are passionate about.","选择你热衷的职业。"]]},
{"w":"ambition","p":"/æmˈbɪʃən/","t":"n.","zh":"雄心;抱负","lv":"B1","tp":["work","general"],"ex":[["He has a great ambition to be a doctor.","他有成为医生的远大抱负。"],["Ambition drives people to succeed.","雄心驱使人们走向成功。"]]},
{"w":"confidence","p":"/ˈkɑːnfɪdəns/","t":"n.","zh":"自信;信心","lv":"B1","tp":["emotion","general"],"ex":[["Confidence is key to success.","自信是成功的关键。"],["She spoke with great confidence.","她充满自信地说话。"]]},
{"w":"attitude","p":"/ˈætɪtuːd/","t":"n.","zh":"态度;立场","lv":"B1","tp":["emotion","general"],"ex":[["A positive attitude makes a big difference.","积极的态度能带来巨大的改变。"],["Change your attitude, change your life.","改变你的态度,改变你的生活。"]]},
{"w":"behavior","p":"/bɪˈheɪvjər/","t":"n.","zh":"行为;举止","lv":"B1","tp":["general","social"],"ex":[["Good behavior is expected at school.","在学校需要有良好的行为举止。"],["Your behavior affects those around you.","你的行为影响你周围的人。"]]},
{"w":"creativity","p":"/ˌkriːeɪˈtɪvəti/","t":"n.","zh":"创造力;创意","lv":"B1","tp":["work","general"],"ex":[["Creativity is valued in this company.","创造力在这家公司很受重视。"],["Art encourages creativity in children.","艺术培养孩子的创造力。"]]},
{"w":"curiosity","p":"/ˌkjʊriˈɑːsəti/","t":"n.","zh":"好奇心;求知欲","lv":"B1","tp":["emotion","education"],"ex":[["Curiosity leads to discovery.","好奇心带来发现。"],["Children have natural curiosity.","孩子天生有好奇心。"]]},
{"w":"determination","p":"/dɪˌtɜːrmɪˈneɪʃən/","t":"n.","zh":"决心;决断力","lv":"B1","tp":["emotion","general"],"ex":[["Her determination helped her succeed.","她的决心帮助她成功了。"],["Success requires determination and effort.","成功需要决心和努力。"]]},
{"w":"discipline","p":"/ˈdɪsɪplɪn/","t":"n.","zh":"纪律;自律","lv":"B1","tp":["general","education"],"ex":[["Discipline is the key to achieving goals.","纪律是实现目标的关键。"],["Self-discipline helps you stay focused.","自律帮助你保持专注。"]]},
{"w":"emotion","p":"/ɪˈmoʊʃən/","t":"n.","zh":"情感;感情","lv":"B1","tp":["emotion","general"],"ex":[["She expressed her emotions freely.","她自由地表达了她的情感。"],["Managing emotions is important.","管理情绪很重要。"]]},
{"w":"focus","p":"/ˈfoʊkəs/","t":"v./n.","zh":"专注;焦点","lv":"B1","tp":["work","general"],"ex":[["Focus on what truly matters.","专注于真正重要的事。"],["I need to focus on my studies.","我需要专注于学习。"]]},
{"w":"growth","p":"/ɡroʊθ/","t":"n.","zh":"成长;增长","lv":"B1","tp":["general"],"ex":[["Personal growth is a lifelong journey.","个人成长是一生的旅程。"],["Economic growth creates new opportunities.","经济增长创造新机会。"]]},
{"w":"habit","p":"/ˈhæbɪt/","t":"n.","zh":"习惯;惯例","lv":"B1","tp":["general"],"ex":[["Good habits lead to a better life.","好习惯带来更好的生活。"],["Reading is a habit I want to develop.","读书是我想培养的习惯。"]]},
{"w":"influence","p":"/ˈɪnfluəns/","t":"n./v.","zh":"影响;感化","lv":"B1","tp":["social","general"],"ex":[["Teachers have a great influence on students.","老师对学生有很大的影响。"],["Music can influence your mood.","音乐可以影响你的心情。"]]},
{"w":"integrity","p":"/ɪnˈteɡrəti/","t":"n.","zh":"诚信;正直","lv":"B1","tp":["social","general"],"ex":[["Integrity is the foundation of trust.","诚信是信任的基础。"],["Act with integrity in all situations.","在所有情况下都要诚信行事。"]]},
{"w":"leadership","p":"/ˈliːdərʃɪp/","t":"n.","zh":"领导力;领导地位","lv":"B1","tp":["work","general"],"ex":[["Good leadership inspires others.","良好的领导力激励他人。"],["She showed strong leadership during the crisis.","她在危机中表现出强大的领导力。"]]},
{"w":"motivation","p":"/ˌmoʊtɪˈveɪʃən/","t":"n.","zh":"动力;动机","lv":"B1","tp":["emotion","work"],"ex":[["What is your motivation for learning English?","你学英语的动力是什么?"],["Internal motivation lasts longer.","内在动力持续更久。"]]},
{"w":"patience","p":"/ˈpeɪʃəns/","t":"n.","zh":"耐心;忍耐","lv":"B1","tp":["emotion","general"],"ex":[["Patience is a virtue.","耐心是一种美德。"],["Learning a language requires patience.","学习一门语言需要耐心。"]]},
{"w":"perspective","p":"/pərˈspektɪv/","t":"n.","zh":"观点;视角","lv":"B1","tp":["general"],"ex":[["Try to see things from a different perspective.","尝试从不同的角度看事情。"],["Her perspective changed my mind.","她的观点改变了我的想法。"]]},
{"w":"priority","p":"/praɪˈɔːrəti/","t":"n.","zh":"优先事项;重点","lv":"B1","tp":["work","general"],"ex":[["Health should be your top priority.","健康应该是你的首要任务。"],["Set your priorities and stick to them.","设定你的优先事项并坚持执行。"]]},
{"w":"progress","p":"/ˈprɑːɡres/","t":"n./v.","zh":"进步;发展","lv":"B1","tp":["general","education"],"ex":[["I am making good progress in English.","我的英语取得了很好的进步。"],["Progress requires constant effort.","进步需要持续的努力。"]]},
{"w":"purpose","p":"/ˈpɜːrpəs/","t":"n.","zh":"目的;意义","lv":"B1","tp":["general"],"ex":[["Find your purpose in life.","找到你生命的目的。"],["Every action should have a purpose.","每个行动都应该有目的。"]]},
{"w":"resilience","p":"/rɪˈzɪliəns/","t":"n.","zh":"韧性;复原力","lv":"B1","tp":["emotion","general"],"ex":[["Resilience helps you overcome setbacks.","韧性帮助你克服挫折。"],["Children develop resilience through challenges.","孩子们通过挑战培养韧性。"]]},
{"w":"skill","p":"/skɪl/","t":"n.","zh":"技能;技巧","lv":"B1","tp":["work","education"],"ex":[["Learning new skills takes time.","学习新技能需要时间。"],["Communication is an important skill.","沟通是一项重要的技能。"]]},
{"w":"analyze","p":"/ˈænəlaɪz/","t":"v.","zh":"分析;剖析","lv":"B2","tp":["work","education"],"ex":[["We need to analyze the data carefully.","我们需要仔细分析数据。"],["She analyzed the problem from all angles.","她从各个角度分析了这个问题。"]]},
{"w":"evaluate","p":"/ɪˈvæljueɪt/","t":"v.","zh":"评估;评价","lv":"B2","tp":["work","education"],"ex":[["We need to evaluate the risks first.","我们需要先评估风险。"],["The teacher evaluates students' progress monthly.","老师每月评估学生的进步。"]]},
{"w":"implement","p":"/ˈɪmplɪment/","t":"v.","zh":"实施;执行","lv":"B2","tp":["work","general"],"ex":[["The company will implement new policies.","公司将实施新政策。"],["It's time to implement our plan.","是时候执行我们的计划了。"]]},
{"w":"significant","p":"/sɪɡˈnɪfɪkənt/","t":"adj.","zh":"重大的;显著的","lv":"B2","tp":["general"],"ex":[["This is a significant discovery.","这是一项重大发现。"],["The changes have been significant.","这些变化是显著的。"]]},
{"w":"fundamental","p":"/ˌfʌndəˈmentəl/","t":"adj.","zh":"基本的;根本的","lv":"B2","tp":["general"],"ex":[["Trust is fundamental to any relationship.","信任是任何关系的基础。"],["This is a fundamental problem.","这是一个根本性的问题。"]]},
{"w":"phenomenon","p":"/fɪˈnɑːmɪnən/","t":"n.","zh":"现象;非凡的人","lv":"B2","tp":["general","education"],"ex":[["Climate change is a global phenomenon.","气候变化是一个全球现象。"],["The social media phenomenon changed communication.","社交媒体现象改变了沟通方式。"]]},
{"w":"consequence","p":"/ˈkɑːnsɪkwens/","t":"n.","zh":"后果;结果","lv":"B2","tp":["general"],"ex":[["Consider the consequences of your actions.","考虑你行动的后果。"],["There are serious consequences for breaking rules.","违反规定有严重后果。"]]},
{"w":"assumption","p":"/əˈsʌmpʃən/","t":"n.","zh":"假设;设想","lv":"B2","tp":["general","education"],"ex":[["Don't make assumptions without evidence.","没有证据不要做假设。"],["His assumption was completely wrong.","他的假设完全错误。"]]},
{"w":"innovative","p":"/ˈɪnəveɪtɪv/","t":"adj.","zh":"创新的;革新的","lv":"B2","tp":["work","general"],"ex":[["They found an innovative solution.","他们找到了一个创新的解决方案。"],["Innovative thinking drives progress.","创新思维推动进步。"]]},
{"w":"sustainable","p":"/səˈsteɪnəbəl/","t":"adj.","zh":"可持续的;可维持的","lv":"B2","tp":["general","society"],"ex":[["We need sustainable energy solutions.","我们需要可持续的能源解决方案。"],["Sustainable development protects future generations.","可持续发展保护后代。"]]},
{"w":"comprehensive","p":"/ˌkɑːmprɪˈhensɪv/","t":"adj.","zh":"全面的;综合的","lv":"B2","tp":["general"],"ex":[["We need a comprehensive plan.","我们需要一个全面的计划。"],["She gave a comprehensive review of the topic.","她对这个话题进行了全面的回顾。"]]},
{"w":"elaborate","p":"/ɪˈlæbərɪt/","t":"adj./v.","zh":"详细的;精心制作","lv":"B2","tp":["general"],"ex":[["Can you elaborate on that point?","你能详细说明那一点吗?"],["She made an elaborate plan.","她制定了一个详细的计划。"]]},
{"w":"inevitable","p":"/ɪnˈevɪtəbəl/","t":"adj.","zh":"不可避免的;必然的","lv":"B2","tp":["general"],"ex":[["Change is inevitable in life.","生活中的变化是不可避免的。"],["Conflict was inevitable under those circumstances.","在那种情况下冲突是不可避免的。"]]},
{"w":"sophisticated","p":"/səˈfɪstɪkeɪtɪd/","t":"adj.","zh":"复杂的;有见识的","lv":"B2","tp":["general"],"ex":[["She is a sophisticated thinker.","她是一个思维复杂的人。"],["The system uses sophisticated technology.","该系统使用了复杂的技术。"]]},
{"w":"dilemma","p":"/dɪˈlemə/","t":"n.","zh":"困境;两难局面","lv":"B2","tp":["general"],"ex":[["She faced a moral dilemma.","她面临一个道德困境。"],["There is no easy answer to this dilemma.","这个困境没有简单的答案。"]]},
{"w":"paradox","p":"/ˈpærədɑːks/","t":"n.","zh":"悖论;矛盾","lv":"B2","tp":["general","education"],"ex":[["It's a paradox that more choices can lead to less happiness.","更多的选择会导致更少的幸福,这是一个悖论。"],["The paradox of freedom is that limits can increase it.","自由的悖论是限制可以增加它。"]]},
{"w":"ambiguous","p":"/æmˈbɪɡjuəs/","t":"adj.","zh":"模糊的;含混不清的","lv":"B2","tp":["communication","general"],"ex":[["His answer was ambiguous.","他的回答很模糊。"],["The instructions were ambiguous and confusing.","指示模糊而令人困惑。"]]},
{"w":"diverse","p":"/daɪˈvɜːrs/","t":"adj.","zh":"多样的;多元的","lv":"B2","tp":["society","general"],"ex":[["Our team has diverse backgrounds.","我们的团队有多元化的背景。"],["A diverse society is a rich society.","多元化的社会是丰富的社会。"]]},
{"w":"efficient","p":"/ɪˈfɪʃənt/","t":"adj.","zh":"高效的;有效率的","lv":"B2","tp":["work","general"],"ex":[["We need a more efficient process.","我们需要更高效的流程。"],["She is a very efficient worker.","她是一个非常高效的员工。"]]},
{"w":"impact","p":"/ˈɪmpækt/","t":"n./v.","zh":"影响;冲击","lv":"B2","tp":["general"],"ex":[["Technology has a huge impact on our lives.","技术对我们的生活有巨大影响。"],["The decision impacted the whole company.","这个决定影响了整个公司。"]]},
{"w":"initiative","p":"/ɪˈnɪʃətɪv/","t":"n.","zh":"主动性;倡议","lv":"B2","tp":["work","general"],"ex":[["Take the initiative and make the first move.","主动出击,迈出第一步。"],["The government launched a new initiative.","政府启动了一项新倡议。"]]},
{"w":"strategy","p":"/ˈstræt̮ədʒi/","t":"n.","zh":"策略;战略","lv":"B2","tp":["work","general"],"ex":[["We need a better strategy to compete.","我们需要更好的策略来竞争。"],["Their strategy was successful.","他们的策略成功了。"]]},
{"w":"resource","p":"/ˈriːsɔːrs/","t":"n.","zh":"资源;资料","lv":"B2","tp":["work","general"],"ex":[["Time is our most valuable resource.","时间是我们最宝贵的资源。"],["The company has many resources available.","公司有很多可用资源。"]]},
{"w":"potential","p":"/pəˈtenʃəl/","t":"n./adj.","zh":"潜力;潜在的","lv":"B2","tp":["general"],"ex":[["Every person has great potential.","每个人都有巨大的潜力。"],["This is a potential problem we should address.","这是我们应该解决的潜在问题。"]]},
{"w":"collaborate","p":"/kəˈlæbəreɪt/","t":"v.","zh":"合作;协作","lv":"B2","tp":["work","social"],"ex":[["We collaborate with teams around the world.","我们与世界各地的团队合作。"],["Students collaborate on group projects.","学生们在小组项目中合作。"]]},
{"w":"negotiate","p":"/nɪˈɡoʊʃieɪt/","t":"v.","zh":"谈判;协商","lv":"B2","tp":["work","social"],"ex":[["They negotiated a better deal.","他们谈判达成了更好的协议。"],["You need to negotiate your salary.","你需要谈判薪资。"]]},
{"w":"advocate","p":"/ˈædvəkeɪt/","t":"v./n.","zh":"倡导;提倡者","lv":"B2","tp":["social","general"],"ex":[["She advocates for human rights.","她倡导人权。"],["He is an advocate for environmental protection.","他是环境保护的倡导者。"]]},
{"w":"enhance","p":"/ɪnˈhæns/","t":"v.","zh":"增强;提升","lv":"B2","tp":["general","work"],"ex":[["This course will enhance your skills.","这门课将提升你的技能。"],["Technology can enhance productivity.","技术可以提高生产效率。"]]},
{"w":"facilitate","p":"/fəˈsɪlɪteɪt/","t":"v.","zh":"促进;帮助","lv":"B2","tp":["work","general"],"ex":[["Good management facilitates team success.","良好的管理促进团队成功。"],["Technology facilitates communication.","技术促进了沟通。"]]},
{"w":"demonstrate","p":"/ˈdemənstreɪt/","t":"v.","zh":"展示;证明","lv":"B2","tp":["communication","general"],"ex":[["Please demonstrate how to use the software.","请展示如何使用该软件。"],["She demonstrated great courage.","她展示了极大的勇气。"]]}
]
FILE:package.json
{
"name": "english-daily",
"version": "1.0.2",
"description": "Daily English learning with spaced repetition — built-in A1–B2 word bank, new words daily, quiz mode (MCQ/fill-in/spelling), streak tracking, level progression.",
"keywords": [
"学英语",
"英语单词",
"每日单词",
"英语练习",
"词汇",
"测验",
"打卡",
"间隔重复",
"每日推送",
"English learning",
"vocabulary",
"daily words",
"quiz",
"streak",
"spaced repetition",
"CEFR",
"word bank"
],
"author": "jiajiaoy",
"license": "MIT",
"scripts": {
"register": "node scripts/register.js",
"daily": "node scripts/daily-push.js",
"quiz": "node scripts/quiz.js",
"progress": "node scripts/progress.js",
"push-on": "node scripts/push-toggle.js on",
"push-off": "node scripts/push-toggle.js off",
"push-status": "node scripts/push-toggle.js status"
}
}
FILE:scripts/daily-push.js
#!/usr/bin/env node
/**
* english-daily — 每日学习推送 prompt 生成器
*
* 用法:
* node daily-push.js <userId>
*
* 由 openclaw cron 驱动,每日早晨执行。
* 输出结构化文本,由 Claude 格式化呈现给用户。
*/
'use strict';
const fs = require('fs');
const path = require('path');
const {
getDueWords,
getNewWordsForUser,
todayStr,
addDays
} = require('./wordbank');
const USERS_DIR = path.join(__dirname, '../data/users');
// ── Security helpers ──────────────────────────────────────────────────────────
function sanitizeId(value) {
if (typeof value !== 'string' || !/^[a-zA-Z0-9_-]{1,128}$/.test(value)) {
console.error('❌ 无效的 userId:只允许字母、数字、- 和 _,长度 1-128');
process.exit(1);
}
return value;
}
function safeUserPath(userId) {
const resolved = path.resolve(USERS_DIR, `userId.json`);
if (!resolved.startsWith(path.resolve(USERS_DIR) + path.sep)) {
console.error('❌ 非法路径');
process.exit(1);
}
return resolved;
}
function loadUser(userId) {
const f = safeUserPath(userId);
if (!fs.existsSync(f)) {
console.error(`❌ 未找到用户:userId。请先注册:node register.js userId <姓名>`);
process.exit(1);
}
return JSON.parse(fs.readFileSync(f, 'utf8'));
}
function saveUser(userId, data) {
fs.mkdirSync(USERS_DIR, { recursive: true });
fs.writeFileSync(safeUserPath(userId), JSON.stringify(data, null, 2), 'utf8');
}
// ── Streak update ─────────────────────────────────────────────────────────────
function updateStreak(profile) {
const today = todayStr();
const yesterday = addDays(today, -1);
const last = profile.lastStudyDate;
if (last === today) {
// Already counted today, no change
return profile;
} else if (last === yesterday) {
profile.streak = (profile.streak || 0) + 1;
} else {
// Streak broken (or first study)
profile.streak = 1;
}
if (profile.streak > (profile.longestStreak || 0)) {
profile.longestStreak = profile.streak;
}
profile.lastStudyDate = today;
return profile;
}
// ── Format helpers ────────────────────────────────────────────────────────────
function formatWord(entry) {
const ex = entry.ex && entry.ex[0] ? `entry.ex[0][0] / entry.ex[0][1]` : '';
return `entry.w | entry.p | entry.t | entry.zh''`;
}
function formatWordFull(entry) {
const lines = [`entry.w | entry.p | entry.t | entry.zh`];
if (entry.ex) {
entry.ex.forEach(([en, zh]) => lines.push(` 例: en | zh`));
}
return lines.join('\n');
}
// ── Core function (exportable for cron) ───────────────────────────────────────
function runDailyPush(userId) {
userId = sanitizeId(userId);
const profile = loadUser(userId);
// Update streak
updateStreak(profile);
const today = todayStr();
const dailyGoal = profile.preferences && profile.preferences.dailyGoal ? profile.preferences.dailyGoal : 5;
const dueWords = getDueWords(profile);
const newWords = getNewWordsForUser(profile, dailyGoal);
// Save updated profile (streak + lastStudyDate)
saveUser(userId, profile);
// Format date nicely
const dateObj = new Date(today + 'T12:00:00Z');
const dateDisplay = dateObj.toLocaleDateString('zh-CN', {
year: 'numeric', month: 'long', day: 'numeric', weekday: 'long',
timeZone: 'Asia/Shanghai'
});
// ── Output ─────────────────────────────────────────────────────────────────
console.log(`=== 今日英语学习 · dateDisplay ===`);
console.log(`用户:profile.name | 等级:profile.level | 连续学习:profile.streak天 | 积分:profile.totalPoints || 0`);
console.log('');
if (dueWords.length > 0) {
console.log(`【复习】(dueWords.length个需要复习的单词)`);
dueWords.forEach(w => console.log(formatWord(w)));
console.log('');
} else {
console.log('【复习】今日无需复习的单词 ✅');
console.log('');
}
if (newWords.length > 0) {
console.log(`【今日新词】(目标:dailyGoal个)`);
newWords.forEach(w => console.log(formatWordFull(w)));
console.log('');
} else {
console.log(`【今日新词】当前等级(profile.level)的单词已全部学完!请尝试提升等级。`);
console.log('');
}
console.log('【学习建议】');
console.log('- 先复习旧词,再学新词');
console.log('- 每个单词至少造一个句子');
console.log('- 回复"测验"开始今日练习');
console.log('');
console.log(`📊 查看进度:node scripts/progress.js userId`);
console.log(`📝 开始测验:node scripts/quiz.js userId`);
}
// ── CLI entry ─────────────────────────────────────────────────────────────────
if (require.main === module) {
const args = process.argv.slice(2);
if (!args[0]) {
console.log('用法: node daily-push.js <userId>');
process.exit(1);
}
runDailyPush(args[0]);
}
module.exports = { runDailyPush };
FILE:scripts/evening-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('invalid userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('illegal path');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{};}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
console.log(`英语晚间复习时间🌙(date)。请为用户生成今日词汇测验:node scripts/quiz.js userId mixed。逐题互动,完成后调用 node scripts/quiz.js userId --score <分数> 记录成绩。附今日学习打卡总结和明日预告。`);
FILE:scripts/morning-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('invalid userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('illegal path');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{};}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
console.log(`今天是weekday(date)。请运行今日英语学习推送:node scripts/daily-push.js userId。输出今日复习词汇+新词列表,并附早间学习激励一句话(英文)。`);
FILE:scripts/progress.js
#!/usr/bin/env node
/**
* english-daily — 学习进度查看
*
* 用法:
* node progress.js <userId>
*/
'use strict';
const fs = require('fs');
const path = require('path');
const { getWordStats, loadWordBank, todayStr, addDays } = require('./wordbank');
const USERS_DIR = path.join(__dirname, '../data/users');
// ── Security helpers ──────────────────────────────────────────────────────────
function sanitizeId(value) {
if (typeof value !== 'string' || !/^[a-zA-Z0-9_-]{1,128}$/.test(value)) {
console.error('❌ 无效的 userId:只允许字母、数字、- 和 _,长度 1-128');
process.exit(1);
}
return value;
}
function safeUserPath(userId) {
const resolved = path.resolve(USERS_DIR, `userId.json`);
if (!resolved.startsWith(path.resolve(USERS_DIR) + path.sep)) {
console.error('❌ 非法路径');
process.exit(1);
}
return resolved;
}
function loadUser(userId) {
const f = safeUserPath(userId);
if (!fs.existsSync(f)) {
console.error(`❌ 未找到用户:userId。请先注册:node register.js userId <姓名>`);
process.exit(1);
}
return JSON.parse(fs.readFileSync(f, 'utf8'));
}
function saveUser(userId, data) {
fs.mkdirSync(USERS_DIR, { recursive: true });
fs.writeFileSync(safeUserPath(userId), JSON.stringify(data, null, 2), 'utf8');
}
// ── Level-up thresholds ───────────────────────────────────────────────────────
const LEVEL_UP = {
A1: { wordsNeeded: 40, next: 'A2' },
A2: { wordsNeeded: 90, next: 'B1' },
B1: { wordsNeeded: 130, next: 'B2' },
B2: { wordsNeeded: Infinity, next: null }
};
function checkLevelUp(profile) {
const threshold = LEVEL_UP[profile.level];
if (!threshold || !threshold.next) return false;
const stats = getWordStats(profile);
if (stats.mastered >= threshold.wordsNeeded) {
return threshold.next;
}
return false;
}
// ── Weekly study days ─────────────────────────────────────────────────────────
function getWeeklyDays(profile) {
// Count study days in the last 7 calendar days
// We only track lastStudyDate precisely; use streak as proxy
const streak = profile.streak || 0;
return Math.min(streak, 7);
}
// ── Words to next level ───────────────────────────────────────────────────────
function wordsToNextLevel(profile) {
const threshold = LEVEL_UP[profile.level];
if (!threshold || !threshold.next) return 0;
const stats = getWordStats(profile);
return Math.max(0, threshold.wordsNeeded - stats.mastered);
}
// ── Main ──────────────────────────────────────────────────────────────────────
const args = process.argv.slice(2);
if (!args[0]) {
console.log('用法: node progress.js <userId>');
process.exit(1);
}
const userId = sanitizeId(args[0]);
const profile = loadUser(userId);
// Check for level-up
const newLevel = checkLevelUp(profile);
if (newLevel) {
console.log(`\n🎉 恭喜!你已升级至 newLevel!`);
profile.level = newLevel;
profile.targetLevel = LEVEL_UP[newLevel] ? LEVEL_UP[newLevel].next || newLevel : newLevel;
saveUser(userId, profile);
console.log(`等级已更新:profile.level → 目标 profile.targetLevel\n`);
}
const stats = getWordStats(profile);
const bank = loadWordBank();
const allAtLevel = bank.filter(w => {
const levels = ['A1','A2','B1','B2'];
return levels.indexOf(w.lv) <= levels.indexOf(profile.level);
}).length;
const weeklyDays = getWeeklyDays(profile);
const toNextLevel = wordsToNextLevel(profile);
const threshold = LEVEL_UP[profile.level];
console.log(`
📊 学习进度 — profile.name
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔥 连续学习:profile.streak || 0天(最长:profile.longestStreak || 0天)
⭐ 总积分:profile.totalPoints || 0
📚 已学单词:Object.keys(profile.wordProgress || {).length} / allAtLevel
🎯 当前等级:profile.level → 目标:profile.targetLevel || profile.level
词汇详情:
已掌握(间隔≥7天):stats.mastered个
学习中(间隔<7天):stats.learning个
待复习(今日到期):stats.due个
📅 本周学习:weeklyDays/7天
threshold && threshold.next
? `💡 距离下一等级(${threshold.next):还需掌握 toNextLevel 个单词`
: '🏆 已达到最高等级 B2!'}
stats.due > 0 ? `⚠️ 今日有 ${stats.due 个单词需要复习!` : '✅ 今日无待复习单词'}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
开始今日学习:node scripts/daily-push.js userId
开始测验: node scripts/quiz.js userId
`);
FILE:scripts/push-toggle.js
#!/usr/bin/env node
/**
* english-daily — 每日推送开关
*
* 用法:
* node push-toggle.js on <userId> [--morning HH:MM] [--channel telegram]
* node push-toggle.js off <userId>
* node push-toggle.js status <userId>
*
* 支持渠道:telegram / feishu / slack / discord
*/
'use strict';
const fs = require('fs');
const path = require('path');
const USERS_DIR = path.join(__dirname, '../data/users');
const ALLOWED_CHANNELS = new Set(['telegram', 'feishu', 'slack', 'discord']);
// ── Security helpers ──────────────────────────────────────────────────────────
function sanitizeId(value, label) {
if (typeof value !== 'string' || !/^[a-zA-Z0-9_-]{1,128}$/.test(value)) {
console.error(`❌ 无效的 label:只允许字母、数字、- 和 _,长度 1-128`);
process.exit(1);
}
return value;
}
function sanitizeTime(value, label) {
if (typeof value !== 'string' || !/^\d{1,2}:\d{2}$/.test(value)) {
console.error(`❌ 无效的 label:格式应为 HH:MM,如 08:00`);
process.exit(1);
}
const [h, m] = value.split(':').map(Number);
if (h < 0 || h > 23 || m < 0 || m > 59) {
console.error(`❌ 无效的 label:小时 0-23,分钟 0-59`);
process.exit(1);
}
return { h, m };
}
function safeUserPath(userId) {
const resolved = path.resolve(USERS_DIR, `userId.json`);
if (!resolved.startsWith(path.resolve(USERS_DIR) + path.sep)) {
console.error('❌ 非法路径');
process.exit(1);
}
return resolved;
}
function loadUser(userId) {
const f = safeUserPath(userId);
if (!fs.existsSync(f)) return null;
return JSON.parse(fs.readFileSync(f, 'utf8'));
}
function saveUser(userId, data) {
fs.mkdirSync(USERS_DIR, { recursive: true });
fs.writeFileSync(safeUserPath(userId), JSON.stringify(data, null, 2), 'utf8');
}
// ── Commands ──────────────────────────────────────────────────────────────────
function enablePush(userId, opts = {}) {
userId = sanitizeId(userId, 'userId');
const { h: mh, m: mm } = sanitizeTime(opts.morning || '08:00', 'morning');
const morningDisplay = `String(mh).padStart(2,'0'):String(mm).padStart(2,'0')`;
const morningCron = `mm mh * * *`;
const rawChannel = opts.channel || 'telegram';
if (!ALLOWED_CHANNELS.has(rawChannel)) {
console.error(`❌ 不支持的渠道:rawChannel。支持:[...ALLOWED_CHANNELS].join(', ')`);
process.exit(1);
}
const channel = rawChannel;
const sessionKey = `agent:main:channel:direct:userId`;
const cronConfig = {
name: `english-daily-morning-userId`,
cronExpr: morningCron,
tz: 'Asia/Shanghai',
session: 'isolated',
sessionKey,
channel,
to: userId,
announce: true,
timeoutSeconds: 120,
message: `node path.join(__dirname, 'daily-push.js') userId`
};
console.log(`__OPENCLAW_CRON_ADD__:JSON.stringify(cronConfig)`);
// Update user preferences
const user = loadUser(userId) || {};
if (user.preferences) {
user.preferences.pushEnabled = true;
user.preferences.morningTime = morningDisplay;
user.preferences.channel = channel;
}
user.pushEnabled = true;
user.morningTime = morningDisplay;
user.channel = channel;
user.pushEnabledAt = new Date().toISOString();
saveUser(userId, user);
console.log(`
✅ 每日英语推送已开启
⏰ 推送时间:每天 morningDisplay(今日单词 + 复习)
📡 推送渠道:channel
关闭推送:node push-toggle.js off userId
查看状态:node push-toggle.js status userId`);
}
function disablePush(userId) {
userId = sanitizeId(userId, 'userId');
const user = loadUser(userId);
if (!user) {
console.log(`❌ 未找到用户 userId 的推送记录`);
return;
}
console.log(`__OPENCLAW_CRON_RM__:english-daily-morning-userId`);
if (user.preferences) user.preferences.pushEnabled = false;
user.pushEnabled = false;
user.pushDisabledAt = new Date().toISOString();
saveUser(userId, user);
console.log(`✅ 每日英语推送已关闭`);
}
function showStatus(userId) {
userId = sanitizeId(userId, 'userId');
const user = loadUser(userId);
if (!user) {
console.log(`❌ 未找到用户 userId 的推送记录(请先注册)`);
return;
}
const enabled = user.pushEnabled || (user.preferences && user.preferences.pushEnabled) || false;
const morningTime = user.morningTime || (user.preferences && user.preferences.morningTime) || '08:00';
const channel = user.channel || (user.preferences && user.preferences.channel) || 'telegram';
const enabledAt = user.pushEnabledAt ? user.pushEnabledAt.split('T')[0] : '未知';
console.log(`
📡 推送状态 — userId(user.name || '')
━━━━━━━━━━━━━━━━━━━━━━━
状态: '❌ 已关闭'
推送时间:morningTime
渠道: channel
''
━━━━━━━━━━━━━━━━━━━━━━━`);
}
module.exports = { enablePush, disablePush, showStatus };
// ── CLI entry ─────────────────────────────────────────────────────────────────
if (require.main !== module) return;
const args = process.argv.slice(2);
const command = args[0];
const userId = args[1];
if (!command || !userId) {
console.log(`用法:
node push-toggle.js on <userId> [--morning 08:00] [--channel telegram]
node push-toggle.js off <userId>
node push-toggle.js status <userId>`);
process.exit(1);
}
const opts = {};
const mi = args.indexOf('--morning');
if (mi !== -1) opts.morning = args[mi + 1];
const ci = args.indexOf('--channel');
if (ci !== -1) opts.channel = args[ci + 1];
switch (command) {
case 'on': enablePush(userId, opts); break;
case 'off': disablePush(userId); break;
case 'status': showStatus(userId); break;
default:
console.error(`❌ 未知命令:command(支持 on/off/status)`);
process.exit(1);
}
FILE:scripts/quiz.js
#!/usr/bin/env node
/**
* english-daily — 测验/练习生成器
*
* 用法:
* node quiz.js <userId> [type]
* node quiz.js <userId> --score <points>
*
* type: vocab | sentence | mixed(默认 mixed)
*
* 生成5道题(含答案),由 Claude 逐题互动呈现。
* 答题完成后 Claude 调用:node quiz.js <userId> --score <points>
*/
'use strict';
const fs = require('fs');
const path = require('path');
const {
getDueWords,
getNewWordsForUser,
updateWordProgress,
loadWordBank,
todayStr
} = require('./wordbank');
const USERS_DIR = path.join(__dirname, '../data/users');
const QUESTIONS_PER_QUIZ = 5;
// ── Security helpers ──────────────────────────────────────────────────────────
function sanitizeId(value) {
if (typeof value !== 'string' || !/^[a-zA-Z0-9_-]{1,128}$/.test(value)) {
console.error('❌ 无效的 userId:只允许字母、数字、- 和 _,长度 1-128');
process.exit(1);
}
return value;
}
function safeUserPath(userId) {
const resolved = path.resolve(USERS_DIR, `userId.json`);
if (!resolved.startsWith(path.resolve(USERS_DIR) + path.sep)) {
console.error('❌ 非法路径');
process.exit(1);
}
return resolved;
}
function loadUser(userId) {
const f = safeUserPath(userId);
if (!fs.existsSync(f)) {
console.error(`❌ 未找到用户:userId。请先注册:node register.js userId <姓名>`);
process.exit(1);
}
return JSON.parse(fs.readFileSync(f, 'utf8'));
}
function saveUser(userId, data) {
fs.mkdirSync(USERS_DIR, { recursive: true });
fs.writeFileSync(safeUserPath(userId), JSON.stringify(data, null, 2), 'utf8');
}
// ── Helpers ───────────────────────────────────────────────────────────────────
/** Simple deterministic pseudo-shuffle based on date + userId seed */
function shuffle(arr, seed) {
const a = arr.slice();
let s = seed;
for (let i = a.length - 1; i > 0; i--) {
s = ((s * 1664525) + 1013904223) & 0xffffffff;
const j = Math.abs(s) % (i + 1);
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
function dateSeed(userId) {
const today = todayStr().replace(/-/g, '');
let hash = 0;
const str = userId + today;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash) + str.charCodeAt(i);
hash |= 0;
}
return Math.abs(hash);
}
/** Get the pool of words a user has studied + due words + new words */
function getQuizPool(profile) {
const bank = loadWordBank();
const progress = profile.wordProgress || {};
// Include: due words + recently studied words + new words (padded)
const studied = bank.filter(w => !!progress[w.w]);
const due = studied.filter(w => {
const p = progress[w.w];
return p && p.nextReview <= todayStr();
});
// If user has less than 5 studied words, also grab new ones to pad
let pool = studied.length >= QUESTIONS_PER_QUIZ
? studied
: [...studied, ...getNewWordsForUser(profile, QUESTIONS_PER_QUIZ - studied.length)];
// Ensure uniqueness
const seen = new Set();
pool = pool.filter(w => {
if (seen.has(w.w)) return false;
seen.add(w.w);
return true;
});
return { pool, due };
}
/** Pick wrong answers for multiple choice (3 distractors) */
function getDistractors(correctEntry, bank, seed) {
const others = bank.filter(w => w.w !== correctEntry.w && w.zh !== correctEntry.zh);
const shuffled = shuffle(others, seed + 1);
return shuffled.slice(0, 3).map(w => w.zh);
}
/** Build a vocab question (word → Chinese, multiple choice) */
function buildVocabQuestion(entry, bank, qNum, seed) {
const distractors = getDistractors(entry, bank, seed + qNum);
const options = shuffle([entry.zh, ...distractors], seed + qNum * 13);
const correctIdx = options.indexOf(entry.zh);
const labels = ['A', 'B', 'C', 'D'];
const optionStr = options.map((o, i) => `labels[i]. o`).join(' ');
const answer = labels[correctIdx];
const example = entry.ex && entry.ex[0] ? entry.ex[0][0] : '';
return {
type: 'vocab',
word: entry.w,
question: `QqNum. What does "entry.w" mean?\nentry.p entry.t\noptionStr`,
answer: `Answer: answer | 解析: entry.w = entry.zh''`
};
}
/** Build a sentence (fill-in-the-blank) question */
function buildSentenceQuestion(entry, qNum) {
// Pick a sentence example if available
const exPair = entry.ex && entry.ex.length > 0 ? entry.ex[0] : null;
if (!exPair) {
// Fallback to simple pattern
return {
type: 'sentence',
word: entry.w,
question: `QqNum. 用 "entry.zh" 填空:_____ is important in life.\n(提示:entry.p entry.t)`,
answer: `Answer: entry.w | 全句: entry.w is important in life.`
};
}
const [enSentence, zhSentence] = exPair;
// Find a good word to blank out — use the target word if present in example
const wordLower = entry.w.toLowerCase();
const regex = new RegExp(`\\bwordLower(?:ed|ing|s|d|es)?\\b`, 'i');
const match = enSentence.match(regex);
if (!match) {
return {
type: 'sentence',
word: entry.w,
question: `QqNum. 翻译成英文(zhSentence)\n(提示:核心词 = entry.zh,entry.p)`,
answer: `Answer: entry.w | 参考译文: enSentence`
};
}
const blanked = enSentence.replace(match[0], '_____');
return {
type: 'sentence',
word: entry.w,
question: `QqNum. 填空 (entry.zh):\nblanked\n中文提示:zhSentence`,
answer: `Answer: match[0] | 全句: enSentence`
};
}
/** Generate quiz questions */
function generateQuiz(profile, type, seed) {
const bank = loadWordBank();
const { pool } = getQuizPool(profile);
if (pool.length < QUESTIONS_PER_QUIZ) {
return null; // Not enough words
}
const words = shuffle(pool, seed).slice(0, QUESTIONS_PER_QUIZ);
const questions = [];
words.forEach((entry, idx) => {
const qNum = idx + 1;
let q;
if (type === 'vocab') {
q = buildVocabQuestion(entry, bank, qNum, seed);
} else if (type === 'sentence') {
q = buildSentenceQuestion(entry, qNum);
} else {
// mixed: alternate
q = idx % 2 === 0
? buildVocabQuestion(entry, bank, qNum, seed)
: buildSentenceQuestion(entry, qNum);
}
questions.push(q);
});
return { words: words.map(w => w.w), questions };
}
// ── Score recording ───────────────────────────────────────────────────────────
function recordScore(userId, rawPoints) {
userId = sanitizeId(userId);
const points = parseInt(rawPoints, 10);
if (isNaN(points) || points < 0 || points > 500) {
console.error('❌ 无效积分值(0-500)');
process.exit(1);
}
const profile = loadUser(userId);
profile.totalPoints = (profile.totalPoints || 0) + points;
const correct = Math.round(points / 10);
// Update word quality for the quiz words (simplified: use quality=3 for correct)
const bank = loadWordBank();
const { pool } = getQuizPool(profile);
const seed = dateSeed(userId);
const quizWords = shuffle(pool, seed).slice(0, QUESTIONS_PER_QUIZ).map(w => w.w);
quizWords.forEach((word, idx) => {
const quality = idx < correct ? 3 : 1;
updateWordProgress(profile, word, quality);
});
// Update wordsLearned count
const newlyLearned = quizWords.filter(w => {
const p = profile.wordProgress[w];
return p && p.repetitions === 0;
});
profile.wordsLearned = Object.keys(profile.wordProgress).length;
saveUser(userId, profile);
console.log(`
✅ 积分已记录!
本次得分:+points分
累计积分:profile.totalPoints分
已掌握单词:profile.wordsLearned个
继续加油!🎉`);
}
// ── CLI entry ─────────────────────────────────────────────────────────────────
if (require.main === module) {
const args = process.argv.slice(2);
if (!args[0]) {
console.log(`用法:
node quiz.js <userId> [type] 生成测验(type: vocab/sentence/mixed)
node quiz.js <userId> --score <pts> 记录本次得分(10分/题)`);
process.exit(1);
}
// --score mode
const scoreIdx = args.indexOf('--score');
if (scoreIdx !== -1) {
const pts = args[scoreIdx + 1];
if (!pts) {
console.error('❌ --score 后需要跟积分数值');
process.exit(1);
}
recordScore(args[0], pts);
process.exit(0);
}
// Generate quiz
const userId = sanitizeId(args[0]);
const rawType = (args[1] || 'mixed').toLowerCase();
const validTypes = ['vocab', 'sentence', 'mixed'];
if (!validTypes.includes(rawType)) {
console.error(`❌ 无效的测验类型:rawType。支持:validTypes.join('/')`);
process.exit(1);
}
const profile = loadUser(userId);
const seed = dateSeed(userId);
const quiz = generateQuiz(profile, rawType, seed);
if (!quiz) {
console.log(`
❌ 单词量不足(需要至少 QUESTIONS_PER_QUIZ 个已学单词或新单词)。
请先完成今日学习:node daily-push.js userId
`);
process.exit(0);
}
const typeNames = { vocab: '词义选择', sentence: '句子填空', mixed: '综合练习' };
console.log(`
📝 英语测验 — profile.name(profile.level)
类型:typeNames[rawType]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
`);
quiz.questions.forEach(q => {
console.log(q.question);
console.log(q.answer);
console.log('');
});
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('【Claude 操作指南】');
console.log('请将以上题目逐题呈现给用户,等待回答后再显示答案。');
console.log('所有题目完成后,根据正确题数运行以下命令记录积分:');
console.log(` node path.join(__dirname, 'quiz.js') userId --score <正确题数×10>`);
console.log('例如答对3题:--score 30');
}
module.exports = { generateQuiz, recordScore };
FILE:scripts/register.js
#!/usr/bin/env node
/**
* english-daily — 用户注册脚本
*
* 用法:
* node register.js <userId> <name> [level] [dailyGoal]
*
* 参数:
* userId - 用户ID(字母/数字/连字符/下划线,1-128字符)
* name - 用户名称
* level - 起始等级 A1/A2/B1/B2(默认 B1)
* dailyGoal - 每日新单词目标 1-20(默认 5)
*
* 示例:
* node register.js 123456 张三
* node register.js 123456 张三 A2 8
*/
'use strict';
const fs = require('fs');
const path = require('path');
const { getWordsByLevel, getNewWordsForUser } = require('./wordbank');
const USERS_DIR = path.join(__dirname, '../data/users');
const VALID_LEVELS = ['A1', 'A2', 'B1', 'B2'];
// ── Security helpers ──────────────────────────────────────────────────────────
function sanitizeId(value) {
if (typeof value !== 'string' || !/^[a-zA-Z0-9_-]{1,128}$/.test(value)) {
console.error('❌ 无效的 userId:只允许字母、数字、- 和 _,长度 1-128');
process.exit(1);
}
return value;
}
function safeUserPath(userId) {
const resolved = path.resolve(USERS_DIR, `userId.json`);
if (!resolved.startsWith(path.resolve(USERS_DIR) + path.sep)) {
console.error('❌ 非法路径');
process.exit(1);
}
return resolved;
}
// ── I/O helpers ───────────────────────────────────────────────────────────────
function saveUser(userId, data) {
fs.mkdirSync(USERS_DIR, { recursive: true });
fs.writeFileSync(safeUserPath(userId), JSON.stringify(data, null, 2), 'utf8');
}
function loadUser(userId) {
const f = safeUserPath(userId);
if (!fs.existsSync(f)) return null;
return JSON.parse(fs.readFileSync(f, 'utf8'));
}
// ── Target level map ──────────────────────────────────────────────────────────
function nextLevel(level) {
const idx = VALID_LEVELS.indexOf(level);
return idx < VALID_LEVELS.length - 1 ? VALID_LEVELS[idx + 1] : level;
}
// ── Main ──────────────────────────────────────────────────────────────────────
const args = process.argv.slice(2);
if (args.length < 2) {
console.log(`
用法:
node register.js <userId> <name> [level] [dailyGoal]
参数:
userId 用户ID(字母/数字/连字符/下划线,1-128字符)
name 用户名称
level 起始等级 A1/A2/B1/B2(默认 B1)
dailyGoal 每日新单词目标 1-20(默认 5)
示例:
node register.js telegram_123 张三
node register.js telegram_123 张三 A2 8
`);
process.exit(1);
}
const rawId = args[0];
const rawName = args[1];
const rawLevel = (args[2] || 'B1').toUpperCase();
const rawGoal = args[3];
// Validate
const userId = sanitizeId(rawId);
if (!rawName || rawName.trim().length === 0) {
console.error('❌ 用户名称不能为空');
process.exit(1);
}
const name = rawName.trim().slice(0, 64);
if (!VALID_LEVELS.includes(rawLevel)) {
console.error(`❌ 无效的等级:rawLevel。支持:VALID_LEVELS.join('/')`);
process.exit(1);
}
const level = rawLevel;
let dailyGoal = 5;
if (rawGoal !== undefined) {
dailyGoal = parseInt(rawGoal, 10);
if (isNaN(dailyGoal) || dailyGoal < 1 || dailyGoal > 20) {
console.error('❌ dailyGoal 必须是 1-20 的整数');
process.exit(1);
}
}
// Check existing
const existing = loadUser(userId);
if (existing) {
console.log(`⚠️ 用户 userId 已存在(existing.name,等级 existing.level)`);
console.log('如需更新,请直接修改 data/users/<userId>.json 或重新注册(将覆盖原有进度)。');
process.exit(0);
}
const now = new Date().toISOString();
const profile = {
userId,
name,
level,
targetLevel: nextLevel(level),
nativeLanguage: 'zh',
streak: 0,
longestStreak: 0,
lastStudyDate: null,
totalPoints: 0,
wordsLearned: 0,
preferences: {
dailyGoal,
pushEnabled: false,
morningTime: '08:00',
channel: 'telegram'
},
wordProgress: {},
createdAt: now
};
saveUser(userId, profile);
// Count available words at their level
const availableWords = getNewWordsForUser(profile, 9999).length;
console.log(`
✅ 注册成功!
用户ID:userId
姓名: name
等级: level(目标:profile.targetLevel)
每日目标:dailyGoal 个新单词
📚 当前等级可学单词:availableWords 个
💡 下一步:
查看今日学习 → node scripts/daily-push.js userId
开始测验 → node scripts/quiz.js userId
查看进度 → node scripts/progress.js userId
开启每日推送 → node scripts/push-toggle.js on userId
`);
module.exports = { saveUser, loadUser, sanitizeId, safeUserPath };
FILE:scripts/wordbank.js
#!/usr/bin/env node
/**
* english-daily — Word Bank Loader + SRS Utilities
* Utility module (not a CLI entry point). Exports helpers for all scripts.
*/
'use strict';
const fs = require('fs');
const path = require('path');
const WORDBANK_PATH = path.join(__dirname, '../data/wordbank.json');
/** Load and return the full word bank array */
function loadWordBank() {
const raw = fs.readFileSync(WORDBANK_PATH, 'utf8');
return JSON.parse(raw);
}
/** Filter words by CEFR level (A1/A2/B1/B2) */
function getWordsByLevel(level) {
return loadWordBank().filter(w => w.lv === level);
}
/** Find a word entry by exact word string */
function getWordByText(word) {
return loadWordBank().find(w => w.w === word) || null;
}
/**
* Return words the user hasn't studied yet, matching their level.
* Falls back to next level up if not enough words remain.
* @param {Object} profile user profile
* @param {number} count how many new words to return
* @returns {Array}
*/
function getNewWordsForUser(profile, count) {
const bank = loadWordBank();
const studied = new Set(Object.keys(profile.wordProgress || {}));
// Levels to draw from, starting at user's level and going up
const levelOrder = ['A1', 'A2', 'B1', 'B2'];
const startIdx = levelOrder.indexOf(profile.level || 'B1');
const levelsToTry = levelOrder.slice(startIdx);
const candidates = [];
for (const lv of levelsToTry) {
const words = bank.filter(w => w.lv === lv && !studied.has(w.w));
candidates.push(...words);
if (candidates.length >= count) break;
}
return candidates.slice(0, count);
}
/**
* Return all words due for SRS review today (nextReview <= today).
* @param {Object} profile
* @returns {Array} array of word bank entries
*/
function getDueWords(profile) {
const bank = loadWordBank();
const today = todayStr();
const progress = profile.wordProgress || {};
return bank.filter(entry => {
const p = progress[entry.w];
if (!p) return false;
return p.nextReview <= today;
});
}
/**
* SM-2 simplified SRS update.
* quality: 1=forgot, 2=hard, 3=ok, 4=easy
* @param {Object} profile (mutated in place)
* @param {string} word
* @param {number} quality 1-4
*/
function updateWordProgress(profile, word, quality) {
if (!profile.wordProgress) profile.wordProgress = {};
const prev = profile.wordProgress[word] || { interval: 1, repetitions: 0, ease: 2.5 };
let { interval, repetitions, ease } = prev;
if (quality <= 2) {
interval = 1;
repetitions = 0;
} else {
const multiplier = quality === 3 ? 1.5 : 2;
interval = Math.min(Math.round(interval * multiplier), 30);
repetitions += 1;
}
// Ease factor adjustment (SM-2 inspired)
ease = Math.max(1.3, ease + 0.1 - (4 - quality) * 0.08);
const nextReview = addDays(todayStr(), interval);
profile.wordProgress[word] = {
interval,
repetitions,
ease: Math.round(ease * 100) / 100,
nextReview,
lastQuality: quality
};
}
/**
* Summary stats for a user's word progress.
* @param {Object} profile
* @returns {{ total: number, due: number, mastered: number, learning: number }}
*/
function getWordStats(profile) {
const bank = loadWordBank();
const total = bank.filter(w => w.lv === profile.level ||
['A1','A2','B1','B2'].indexOf(w.lv) <= ['A1','A2','B1','B2'].indexOf(profile.level)).length;
const today = todayStr();
const progress = profile.wordProgress || {};
let due = 0, mastered = 0, learning = 0;
for (const [word, p] of Object.entries(progress)) {
if (p.nextReview <= today) due++;
if (p.interval >= 7) mastered++;
else learning++;
}
return { total, due, mastered, learning };
}
// ── Helpers ───────────────────────────────────────────────────────────────────
/** Returns today's date as YYYY-MM-DD string (Asia/Shanghai local time) */
function todayStr() {
const now = new Date();
const cst = new Date(now.toLocaleString('en-US', { timeZone: 'Asia/Shanghai' }));
const y = cst.getFullYear();
const m = String(cst.getMonth() + 1).padStart(2, '0');
const d = String(cst.getDate()).padStart(2, '0');
return `y-m-d`;
}
/** Add `days` to a YYYY-MM-DD string, return new YYYY-MM-DD string */
function addDays(dateStr, days) {
const d = new Date(dateStr + 'T12:00:00Z');
d.setUTCDate(d.getUTCDate() + days);
return d.toISOString().slice(0, 10);
}
module.exports = {
loadWordBank,
getWordsByLevel,
getWordByText,
getNewWordsForUser,
getDueWords,
updateWordProgress,
getWordStats,
todayStr,
addDays
};
Daily weather briefing for any city — morning conditions, what to wear, umbrella forecast, evening preview, extreme weather alerts. No API key. Works worldwide.
---
name: weather-daily
description: "Daily weather briefing for any city — morning conditions, what to wear, umbrella forecast, evening preview, extreme weather alerts. No API key. Works worldwide."
keywords:
- 天气
- 今天天气
- 明天天气
- 天气预报
- 本周天气
- 一周天气
- 穿衣建议
- 穿衣指数
- 下雨吗
- 带伞吗
- 空气质量
- AQI
- 空气污染
- 极端天气
- 台风
- 暴雨
- 寒潮
- 高温
- 下雪
- 每日推送
- 天气提醒
- weather
- forecast
- today weather
- tomorrow weather
- daily weather
- weather forecast
- weekly forecast
- weather alert
- air quality
- AQI
- temperature
- rain
- snow
- typhoon
- extreme weather
metadata:
openclaw:
runtime:
node: ">=18"
tags:
- weather
- 天气
- 天气预报
- 推送
- daily
---
# weather-daily
> 私人天气助手 — 早间实况 · 晚间预告 · 一周预报 · 极端预警
## 何时使用
- 用户说"今天天气""天气怎么样""下雨吗""穿什么"
- 用户问"明天天气""明天冷吗""要带伞吗"
- 用户说"本周天气""天气预报""这周有雨吗"
- 用户问"空气质量""AQI""今天适合出门吗"
- 用户说"开启天气推送""订阅天气""每天推天气"
- 用户说"下周天气""这周末出去玩合适吗"
- 用户说"下个月天气""下个月适合旅游吗"
---
## 📋 功能说明
| 指令 | 脚本 | 说明 |
|------|------|------|
| 今日天气 | `morning-push.js <userId>` | 今日温度/湿度/风力/分时预报/穿衣/出行建议 |
| 明日预告 | `evening-push.js <userId>` | 明日天气预告 + 提醒 + 后天预览 |
| 一周预报 | `forecast.js <userId>` | 未来7天逐日天气 + 趋势 + 穿衣建议 |
| 下周周报 | `weekly-push.js <userId>` | 每周六推送下周天气 + 最佳出行日 |
| 月度概况 | `monthly-push.js <userId>` | 每月末推送下月气候 + 分旬预测 |
| 开启推送 | `push-toggle.js on <userId>` | 定时推送(早/晚/周报/月报) |
| 关闭推送 | `push-toggle.js off <userId>` | 停止全部推送 |
| 推送状态 | `push-toggle.js status <userId>` | 查看当前推送配置 |
---
## 🌤️ 早间天气内容
每日早间推送(默认 07:00),包含:
- 今日温度区间、天气状况、湿度、风力风向
- 日出/日落时间
- 分时预报(早晨/上午/下午/夜间)
- 空气质量(AQI + 等级)
- 穿衣建议、出行建议
- 极端天气提醒
---
## 🌙 晚间天气内容
每日晚间推送(默认 21:00),包含:
- 明日温度、天气状况、湿度、风力
- 出行/穿衣/雨伞等具体提醒
- 极端天气预警(如有)
- 后天天气一句话预览
---
## ⚠️ 支持的预警类型
| 类型 | 说明 |
|------|------|
| 🌧️ 降雨 | 中雨/大雨/暴雨预警 |
| ❄️ 降雪 | 小雪/大雪/暴雪预警 |
| 💨 大风 | 6级以上大风预警 |
| 🌀 台风 | 台风路径与影响范围 |
| 🥶 寒潮 | 大幅降温预警 |
| 🔥 高温 | 35°C+ 高温预警 |
| 🌫️ 空气质量 | AQI 重度污染预警 |
---
## 🔧 脚本说明
### 注册用户
```bash
node scripts/register.js <userId> <city> [units] [morningTime] [eveningTime]
# 示例:
node scripts/register.js alice 上海
node scripts/register.js bob Beijing imperial 08:00 22:00
```
### 推送管理
```bash
node scripts/push-toggle.js on <userId> [--morning 07:00] [--evening 21:00] [--channel telegram]
node scripts/push-toggle.js off <userId>
node scripts/push-toggle.js status <userId>
```
支持渠道:`telegram` / `feishu` / `slack` / `discord`
---
## ⚠️ 注意事项
1. 使用前须先注册:`node scripts/register.js <userId> <city>`
2. 无需 API Key,天气数据通过 WebSearch 实时搜索获取
3. 用户偏好存储在 `data/users/<userId>.json`,仅含城市/单位/时间等配置,不含天气内容
4. 搜索结果受 WebSearch 实时性限制,极端天气预警以官方气象部门发布为准
FILE:README.md
# Weather Daily — Daily Weather Push Skill
> Morning conditions, evening preview, weekly forecast, extreme weather alerts — no API key needed.
[](https://clawhub.ai/skills/weather-daily)
[](https://openclaw.ai)
## What it does
Weather Daily delivers a practical daily weather briefing — what to wear this morning, whether to bring an umbrella, what tomorrow looks like, and immediate alerts for extreme conditions. Works for any city worldwide. No API key required — fetches data via real-time web search.
**Morning briefing** — today's conditions, temperature range, UV index, outfit suggestion
**Evening preview** — tomorrow's forecast + weekly summary
**Extreme alerts** — typhoon, heavy rain, heatwave, cold snap
**Global** — works for any city worldwide
**No API key** — no setup, works out of the box
## Installation
```bash
openclaw install weather-daily
```
## Usage
```bash
# Morning weather briefing
openclaw run weather-daily morning --city "Shanghai"
# Evening preview
openclaw run weather-daily evening --city "London"
# Weekly forecast
openclaw run weather-daily weekly --city "New York"
```
## Keywords
weather · daily weather · weather forecast · weather alert · morning weather · weather briefing · 天气 · 今天天气 · 明天天气 · 天气预报 · 本周天气 · 穿什么 · 极端天气预警 · 台风 · 暴雨
---
Built for [OpenClaw](https://openclaw.ai) · Published on [clawhub.ai](https://clawhub.ai/skills/weather-daily)
FILE:_meta.json
{
"slug": "weather-daily",
"version": "1.0.0",
"name": "每日天气推送",
"description": "每日天气推送 Skill - 早间今日天气(温度/穿衣/出行建议),晚间明日预告,支持一周预报、极端天气预警。基于城市设置,无需 API Key。",
"runtime": {
"node": ">=18"
},
"tags": [
"天气",
"天气预报",
"天气推送",
"每日天气",
"穿衣建议",
"空气质量",
"极端天气",
"weather",
"forecast"
],
"author": "jiajiaoy",
"license": "MIT",
"homepage": "https://github.com/jiajiaoy/weather-daily"
}
FILE:package.json
{
"name": "weather-daily",
"version": "1.0.2",
"description": "Daily weather briefing for any city — morning conditions, what to wear, umbrella forecast, evening preview, extreme weather alerts. No API key. Works worldwide.",
"keywords": [
"天气",
"今天天气",
"明天天气",
"天气预报",
"本周天气",
"本周天气预报",
"穿衣建议",
"穿衣指数",
"空气质量",
"AQI",
"空气污染",
"极端天气",
"暴雨",
"台风",
"下雪",
"每日推送",
"天气推送",
"早晨推送",
"晚间推送",
"明日天气",
"weather",
"forecast",
"daily weather",
"weather forecast",
"today weather",
"tomorrow weather",
"weekly forecast",
"weather alert",
"weather push",
"air quality",
"AQI",
"temperature",
"rain",
"snow",
"typhoon"
],
"author": "jiajiaoy",
"license": "MIT",
"scripts": {
"morning": "node scripts/morning-push.js",
"evening": "node scripts/evening-push.js",
"forecast": "node scripts/forecast.js",
"push-on": "node scripts/push-toggle.js on",
"push-off": "node scripts/push-toggle.js off",
"push-status": "node scripts/push-toggle.js status"
}
}
FILE:scripts/evening-push.js
#!/usr/bin/env node
/**
* weather-daily — evening weather push prompt generator (tomorrow preview)
* Driven by openclaw cron; output is fulfilled by Claude via WebSearch
*
* Usage:
* node evening-push.js <userId>
*/
const fs = require('fs');
const path = require('path');
const USERS_DIR = path.join(__dirname, '../data/users');
function sanitizeId(value) {
if (typeof value !== 'string' || !/^[a-zA-Z0-9_-]{1,128}$/.test(value)) {
console.error('❌ Invalid userId');
process.exit(1);
}
return value;
}
function safeUserPath(userId) {
const resolved = path.resolve(USERS_DIR, `userId.json`);
if (!resolved.startsWith(path.resolve(USERS_DIR) + path.sep)) {
console.error('❌ Illegal path');
process.exit(1);
}
return resolved;
}
function loadUser(userId) {
const f = safeUserPath(userId);
if (!fs.existsSync(f)) {
console.error(`❌ User userId not found. Run: node register.js userId <city>`);
process.exit(1);
}
return JSON.parse(fs.readFileSync(f, 'utf8'));
}
const args = process.argv.slice(2);
if (!args[0]) {
console.error('Usage: node evening-push.js <userId>');
process.exit(1);
}
const userId = sanitizeId(args[0]);
const user = loadUser(userId);
const city = user.city || '上海';
const units = user.units || 'metric';
const unit = units === 'metric' ? '°C' : '°F';
const lang = user.language || ((/[\u4e00-\u9fa5]/.test(city)) ? 'zh' : 'en');
const now = new Date();
const tomorrow = new Date(now);
tomorrow.setDate(tomorrow.getDate() + 1);
const tmYear = tomorrow.getFullYear();
const tmMonth = tomorrow.getMonth() + 1;
const tmDay = tomorrow.getDate();
const tomorrowISO = `tmYear-String(tmMonth).padStart(2,'0')-String(tmDay).padStart(2,'0')`;
if (lang === 'zh') {
const WEEKDAYS_ZH = ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const todayDisplay = `now.getFullYear()年now.getMonth()+1月now.getDate()日 WEEKDAYS_ZH[now.getDay()]`;
const tomorrowDisplay = `tmYear年tmMonth月tmDay日 WEEKDAYS_ZH[tomorrow.getDay()]`;
console.log(`请为用户查询 city 明天(tomorrowDisplay)的天气预报,并按以下格式推送晚间天气预告。
搜索步骤:
1. 搜索「city 明天天气 tomorrowISO」
2. 搜索「city 未来天气 是否有极端天气」
输出格式:
🌙 city 明日天气预告 · todayDisplay晚间
━━━━━━━━━━━━━━━━━━━━━━━
📅 明天 tomorrowDisplay
🌡️ 预计温度:低温Xunit ~ 高温Xunit
☁️ 天气:[天气状况]
💧 湿度:X% 🌬️ 风力:X级
⏰ 明日提醒
[根据天气提供出行/穿衣/携带雨伞等具体建议]
⚠️ 重要预警:[如有极端天气则高亮提示,无则省略]
🗓️ 后天预览:[一句话说明后天大致天气]
💡 回复"本周预报"查看7天天气`);
} else {
const WEEKDAYS_EN = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
const MONTHS_EN = ['January','February','March','April','May','June','July','August','September','October','November','December'];
const todayDisplay = `WEEKDAYS_EN[now.getDay()], MONTHS_EN[now.getMonth()] now.getDate(), now.getFullYear()`;
const tomorrowDisplay = `WEEKDAYS_EN[tomorrow.getDay()], MONTHS_EN[tomorrow.getMonth()] tmDay, tmYear`;
console.log(`Please search for tomorrow's weather in city (tomorrowDisplay) and send the evening preview in the following format.
Search steps:
1. Search "city weather tomorrow tomorrowISO"
2. Search "city weather outlook extreme weather warning"
Output format:
🌙 city Tomorrow's Weather Preview · todayDisplay Evening
━━━━━━━━━━━━━━━━━━━━━━━
📅 Tomorrow: tomorrowDisplay
🌡️ Temperature: Low Xunit ~ High Xunit
☁️ Conditions: [weather description]
💧 Humidity: X% 🌬️ Wind: [speed & direction]
⏰ Tomorrow's reminders
[Specific commute / outfit / umbrella advice based on the forecast]
⚠️ Weather alert: [highlight any extreme weather warnings; omit if none]
🗓️ Day after tomorrow: [one-line outlook]
💡 Reply "forecast" for the 7-day outlook`);
}
FILE:scripts/forecast.js
#!/usr/bin/env node
/**
* weather-daily — 7-day forecast prompt generator
* Output is fulfilled by Claude via WebSearch
*
* Usage:
* node forecast.js <userId>
*/
const fs = require('fs');
const path = require('path');
const USERS_DIR = path.join(__dirname, '../data/users');
function sanitizeId(value) {
if (typeof value !== 'string' || !/^[a-zA-Z0-9_-]{1,128}$/.test(value)) {
console.error('❌ Invalid userId');
process.exit(1);
}
return value;
}
function safeUserPath(userId) {
const resolved = path.resolve(USERS_DIR, `userId.json`);
if (!resolved.startsWith(path.resolve(USERS_DIR) + path.sep)) {
console.error('❌ Illegal path');
process.exit(1);
}
return resolved;
}
function loadUser(userId) {
const f = safeUserPath(userId);
if (!fs.existsSync(f)) {
console.error(`❌ User userId not found. Run: node register.js userId <city>`);
process.exit(1);
}
return JSON.parse(fs.readFileSync(f, 'utf8'));
}
const args = process.argv.slice(2);
if (!args[0]) {
console.error('Usage: node forecast.js <userId>');
process.exit(1);
}
const userId = sanitizeId(args[0]);
const user = loadUser(userId);
const city = user.city || '上海';
const units = user.units || 'metric';
const unit = units === 'metric' ? 'C' : 'F';
const lang = user.language || ((/[\u4e00-\u9fa5]/.test(city)) ? 'zh' : 'en');
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth() + 1;
const day = now.getDate();
if (lang === 'zh') {
const WEEKDAYS_ZH = ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const todayDisplay = `year年month月day日(WEEKDAYS_ZH[now.getDay()])`;
console.log(`请为用户查询 city 未来7天的天气预报(从 todayDisplay 起),并按以下格式输出。
搜索步骤:
1. 搜索「city 未来7天天气预报」
2. 搜索「city 本周天气」
输出格式:
📅 city 一周天气预报
━━━━━━━━━━━━━━━━━━━━━━━
从 todayDisplay 起,未来7天:
[每天一行]
{星期} {日期} {天气图标} {天气} 🌡️{低温}~{高温}°unit 💧{湿度}% 🌬️{风力}
━━━━━━━━━━━━━━━━━━━━━━━
📌 本周趋势:[整体天气趋势描述,2-3句]
⚠️ 重要提醒:[本周内任何极端天气预警]
👔 本周穿衣趋势:[整体穿衣建议]
💡 回复"今天天气"获取今日详细天气`);
} else {
const WEEKDAYS_EN = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
const MONTHS_EN = ['January','February','March','April','May','June','July','August','September','October','November','December'];
const todayDisplay = `WEEKDAYS_EN[now.getDay()], MONTHS_EN[now.getMonth()] day, year`;
console.log(`Please search for the 7-day weather forecast for city starting from todayDisplay and present it in the following format.
Search steps:
1. Search "city 7-day weather forecast"
2. Search "city weather this week"
Output format:
📅 city 7-Day Weather Forecast
━━━━━━━━━━━━━━━━━━━━━━━
Starting todayDisplay:
[One line per day]
{Day} {Date} {icon} {conditions} 🌡️{low}~{high}°unit 💧{humidity}% 🌬️{wind}
━━━━━━━━━━━━━━━━━━━━━━━
📌 Weekly trend: [2–3 sentences on overall weather pattern]
⚠️ Alerts: [any extreme weather warnings this week]
👔 What to wear this week: [general outfit guidance]
💡 Reply "today's weather" for today's detailed report`);
}
FILE:scripts/monthly-push.js
#!/usr/bin/env node
/**
* weather-daily — next-month weather overview prompt generator (sent at month end)
* Runs on 28–31 of each month; script checks internally whether today is the last day.
*
* Usage:
* node monthly-push.js <userId>
* node monthly-push.js <userId> --force # skip last-day check
*/
const fs = require('fs');
const path = require('path');
const USERS_DIR = path.join(__dirname, '../data/users');
function sanitizeId(value) {
if (typeof value !== 'string' || !/^[a-zA-Z0-9_-]{1,128}$/.test(value)) {
console.error('❌ Invalid userId');
process.exit(1);
}
return value;
}
function safeUserPath(userId) {
const resolved = path.resolve(USERS_DIR, `userId.json`);
if (!resolved.startsWith(path.resolve(USERS_DIR) + path.sep)) {
console.error('❌ Illegal path');
process.exit(1);
}
return resolved;
}
function loadUser(userId) {
const f = safeUserPath(userId);
if (!fs.existsSync(f)) {
console.error(`❌ User userId not found. Run: node register.js userId <city>`);
process.exit(1);
}
return JSON.parse(fs.readFileSync(f, 'utf8'));
}
function isLastDayOfMonth(date) {
const tomorrow = new Date(date);
tomorrow.setDate(date.getDate() + 1);
return tomorrow.getDate() === 1;
}
const args = process.argv.slice(2);
if (!args[0]) {
console.error('Usage: node monthly-push.js <userId> [--force]');
process.exit(1);
}
const userId = sanitizeId(args[0]);
const force = args.includes('--force');
const now = new Date();
if (!force && !isLastDayOfMonth(now)) {
process.exit(0);
}
const user = loadUser(userId);
const city = user.city || '上海';
const units = user.units || 'metric';
const unit = units === 'metric' ? 'C' : 'F';
const lang = user.language || ((/[\u4e00-\u9fa5]/.test(city)) ? 'zh' : 'en');
const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1);
const nextYear = nextMonth.getFullYear();
const nextMon = nextMonth.getMonth() + 1;
if (lang === 'zh') {
const SEASON = ['冬季','冬季','春季','春季','春季','夏季','夏季','夏季','秋季','秋季','秋季','冬季'];
const season = SEASON[nextMon - 1];
console.log(`请为用户查询 city nextYear年nextMon月的天气概况,并按以下格式推送月度天气预报。
搜索步骤:
1. 搜索「city nextYear年nextMon月天气预报」
2. 搜索「city nextMon月气候特点」
3. 搜索「city nextMon月份season注意事项」
输出格式:
🗓️ city nextYear年nextMon月天气月报
━━━━━━━━━━━━━━━━━━━━━━━
📊 气温概况
· 平均气温:X°unit(历史同期:X°unit)
· 最高气温:X°unit 最低气温:X°unit
· 温差提示:[早晚温差说明]
🌧️ 降水情况
· 降雨概率:X%
· 主要降水时段:[上/中/下旬]
· 是否有梅雨/台风/干旱等气候特征
💨 主要天气特征
[本月主要天气类型及成因,3-5句]
📅 分旬预测
🔹 上旬(1-10日):[天气趋势]
🔹 中旬(11-20日):[天气趋势]
🔹 下旬(21日-月末):[天气趋势]
⚠️ 本月预警
[本月可能出现的极端天气或气候风险,无则省略]
👔 穿衣指南
[本月整体穿衣建议,按上中下旬温度变化说明]
🌿 生活建议
[本月养生、出行、运动等实用建议,2-3条]
💡 回复"本周天气"获取详细一周预报`);
} else {
const MONTHS_EN = ['January','February','March','April','May','June','July','August','September','October','November','December'];
const SEASON_EN = ['Winter','Winter','Spring','Spring','Spring','Summer','Summer','Summer','Autumn','Autumn','Autumn','Winter'];
const monthName = MONTHS_EN[nextMon - 1];
const season = SEASON_EN[nextMon - 1];
console.log(`Please search for the weather overview for city in monthName nextYear and send the monthly weather report in the following format.
Search steps:
1. Search "city weather forecast monthName nextYear"
2. Search "city monthName climate"
3. Search "city season monthName weather tips"
Output format:
🗓️ city monthName nextYear Weather Monthly Report
━━━━━━━━━━━━━━━━━━━━━━━
📊 Temperature Overview
· Average temperature: X°unit (historical average: X°unit)
· High: X°unit Low: X°unit
· Day-night swing: [notes on temperature range]
🌧️ Precipitation
· Rain probability: X%
· Main rainy periods: [early / mid / late month]
· Notable climate patterns: [e.g. monsoon, drought, snow season]
💨 Weather Characteristics
[3–5 sentences on dominant weather types and causes this month]
📅 10-Day Periods
🔹 Early month (1–10): [trend]
🔹 Mid month (11–20): [trend]
🔹 Late month (21–end): [trend]
⚠️ Monthly alerts
[Potential extreme weather or climate risks; omit if none]
👔 What to wear
[General outfit guidance across the month's temperature changes]
🌿 Lifestyle tips
[2–3 practical tips for health, travel, or outdoor activities]
💡 Reply "forecast" for the detailed 7-day outlook`);
}
FILE:scripts/morning-push.js
#!/usr/bin/env node
/**
* weather-daily — morning weather push prompt generator
* Driven by openclaw cron; output is fulfilled by Claude via WebSearch
*
* Usage:
* node morning-push.js <userId>
*/
const fs = require('fs');
const path = require('path');
const USERS_DIR = path.join(__dirname, '../data/users');
function sanitizeId(value) {
if (typeof value !== 'string' || !/^[a-zA-Z0-9_-]{1,128}$/.test(value)) {
console.error('❌ Invalid userId');
process.exit(1);
}
return value;
}
function safeUserPath(userId) {
const resolved = path.resolve(USERS_DIR, `userId.json`);
if (!resolved.startsWith(path.resolve(USERS_DIR) + path.sep)) {
console.error('❌ Illegal path');
process.exit(1);
}
return resolved;
}
function loadUser(userId) {
const f = safeUserPath(userId);
if (!fs.existsSync(f)) {
console.error(`❌ User userId not found. Run: node register.js userId <city>`);
process.exit(1);
}
return JSON.parse(fs.readFileSync(f, 'utf8'));
}
const args = process.argv.slice(2);
if (!args[0]) {
console.error('Usage: node morning-push.js <userId>');
process.exit(1);
}
const userId = sanitizeId(args[0]);
const user = loadUser(userId);
const city = user.city || '上海';
const units = user.units || 'metric';
const unit = units === 'metric' ? '°C' : '°F';
const lang = user.language || ((/[\u4e00-\u9fa5]/.test(city)) ? 'zh' : 'en');
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth() + 1;
const day = now.getDate();
const dateISO = `year-String(month).padStart(2,'0')-String(day).padStart(2,'0')`;
if (lang === 'zh') {
const WEEKDAYS_ZH = ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const weekday = WEEKDAYS_ZH[now.getDay()];
const dateDisplay = `year年month月day日 weekday`;
console.log(`请为用户查询 city 今天(dateDisplay)的天气,并按以下格式推送早间天气报告。
搜索步骤:
1. 搜索「city 今天天气 dateISO」
2. 搜索「city 今日空气质量」
3. 如有极端天气预警,搜索「city 天气预警」
输出格式:
🌤️ city 早间天气 · dateDisplay
━━━━━━━━━━━━━━━━━━━━━━━
🌡️ 温度:低温Xunit ~ 高温Xunit
☁️ 天气:[天气状况]
💧 湿度:X% 🌬️ 风力:X级(风向)
🌅 日出:XX:XX 🌇 日落:XX:XX
📊 分时预报
🌅 早晨(6-9点):[温度+状况]
☀️ 上午(9-12点):[温度+状况]
🌤️ 下午(12-18点):[温度+状况]
🌙 夜间(18-24点):[温度+状况]
🌍 空气质量:AQI X([等级])[提示]
🎽 穿衣建议:[根据温度给出具体建议]
🚗 出行建议:[根据天气给出出行提示]
⚠️ 今日提醒:[重要天气注意事项,无则省略]
💡 查看本周预报:回复"天气预报"`);
} else {
const WEEKDAYS_EN = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
const MONTHS_EN = ['January','February','March','April','May','June','July','August','September','October','November','December'];
const weekday = WEEKDAYS_EN[now.getDay()];
const dateDisplay = `weekday, MONTHS_EN[now.getMonth()] day, year`;
console.log(`Please search for today's weather in city (dateDisplay) and send the morning weather report in the following format.
Search steps:
1. Search "city weather today dateISO"
2. Search "city air quality today"
3. If extreme weather is possible, search "city weather warning"
Output format:
🌤️ city Morning Weather · dateDisplay
━━━━━━━━━━━━━━━━━━━━━━━
🌡️ Temperature: Low Xunit ~ High Xunit
☁️ Conditions: [weather description]
💧 Humidity: X% 🌬️ Wind: [speed & direction]
🌅 Sunrise: XX:XX 🌇 Sunset: XX:XX
📊 Hourly Forecast
🌅 Morning (6–9 AM): [temp + conditions]
☀️ Late Morning (9 AM–12 PM): [temp + conditions]
🌤️ Afternoon (12–6 PM): [temp + conditions]
🌙 Evening (6 PM–midnight): [temp + conditions]
🌍 Air Quality: AQI X ([level]) [note]
🎽 What to wear: [outfit suggestion based on temperature]
🚗 Commute tip: [travel advice based on conditions]
⚠️ Today's alert: [important weather warnings, omit if none]
💡 Reply "forecast" for the weekly outlook`);
}
FILE:scripts/push-toggle.js
#!/usr/bin/env node
/**
* weather-daily — 推送开关
*
* 用法:
* node push-toggle.js on <userId> 开启推送
* node push-toggle.js off <userId> 关闭推送
* node push-toggle.js status <userId> 查看状态
*
* 选项:
* --morning HH:MM 早报时间(覆盖用户设置,默认 07:00)
* --evening HH:MM 晚报时间(覆盖用户设置,默认 21:00)
* --channel <name> 推送渠道(默认 telegram)
*/
const fs = require('fs');
const path = require('path');
const USERS_DIR = path.join(__dirname, '../data/users');
// 只允许字母、数字、连字符、下划线,最长 128 字符
function sanitizeId(value, label) {
if (typeof value !== 'string' || !/^[a-zA-Z0-9_-]{1,128}$/.test(value)) {
console.error(`❌ 无效的 label:只允许字母、数字、- 和 _,长度 1-128`);
process.exit(1);
}
return value;
}
// 校验 HH:MM 格式,返回 { h, m } 整数
function sanitizeTime(value, label) {
if (typeof value !== 'string' || !/^\d{1,2}:\d{2}$/.test(value)) {
console.error(`❌ 无效的 label:格式应为 HH:MM,如 07:00`);
process.exit(1);
}
const [h, m] = value.split(':').map(Number);
if (h < 0 || h > 23 || m < 0 || m > 59) {
console.error(`❌ 无效的 label:小时 0-23,分钟 0-59`);
process.exit(1);
}
return { h, m };
}
// 验证文件路径确实在 USERS_DIR 内(防路径穿越)
function safeUserPath(userId) {
const resolved = path.resolve(USERS_DIR, `userId.json`);
if (!resolved.startsWith(path.resolve(USERS_DIR) + path.sep)) {
console.error('❌ 非法路径');
process.exit(1);
}
return resolved;
}
function loadUser(userId) {
const f = safeUserPath(userId);
if (!fs.existsSync(f)) return null;
return JSON.parse(fs.readFileSync(f, 'utf8'));
}
function saveUser(userId, data) {
fs.mkdirSync(USERS_DIR, { recursive: true });
fs.writeFileSync(safeUserPath(userId), JSON.stringify(data, null, 2), 'utf8');
}
const ALLOWED_CHANNELS = new Set(['telegram', 'feishu', 'slack', 'discord']);
function enablePush(userId, opts = {}) {
userId = sanitizeId(userId, 'userId');
// 加载用户资料获取默认推送时间
const userProfile = loadUser(userId);
const defaultMorning = (userProfile && userProfile.preferences && userProfile.preferences.morningTime) || '07:00';
const defaultEvening = (userProfile && userProfile.preferences && userProfile.preferences.eveningTime) || '21:00';
const { h: mh, m: mm } = sanitizeTime(opts.morning || defaultMorning, 'morning');
const { h: eh, m: em } = sanitizeTime(opts.evening || defaultEvening, 'evening');
const rawChannel = opts.channel || (userProfile && userProfile.preferences && userProfile.preferences.channel) || 'telegram';
if (!ALLOWED_CHANNELS.has(rawChannel)) {
console.error(`❌ 不支持的渠道:rawChannel。支持:[...ALLOWED_CHANNELS].join(', ')`);
process.exit(1);
}
const channel = rawChannel;
const morningCron = `mm mh * * *`;
const eveningCron = `em eh * * *`;
const sessionKey = `agent:main:channel:direct:userId`;
// 早间天气 cron
const morningConfig = {
name: `weather-morning-userId`,
cronExpr: morningCron,
tz: 'Asia/Shanghai',
session: 'isolated',
sessionKey,
channel,
to: userId,
announce: true,
timeoutSeconds: 120,
message: `node path.join(__dirname, 'morning-push.js') userId`
};
console.log(`__OPENCLAW_CRON_ADD__:JSON.stringify(morningConfig)`);
// 晚间预告 cron
const eveningConfig = {
name: `weather-evening-userId`,
cronExpr: eveningCron,
tz: 'Asia/Shanghai',
session: 'isolated',
sessionKey,
channel,
to: userId,
announce: true,
timeoutSeconds: 120,
message: `node path.join(__dirname, 'evening-push.js') userId`
};
console.log(`__OPENCLAW_CRON_ADD__:JSON.stringify(eveningConfig)`);
// 周末下周天气周报 cron(每周六 20:00)
const weeklyConfig = {
name: `weather-weekly-userId`,
cronExpr: '0 20 * * 6',
tz: 'Asia/Shanghai',
session: 'isolated',
sessionKey,
channel,
to: userId,
announce: true,
timeoutSeconds: 120,
message: `node path.join(__dirname, 'weekly-push.js') userId`
};
console.log(`__OPENCLAW_CRON_ADD__:JSON.stringify(weeklyConfig)`);
// 月末下月天气概览 cron(每月 28-31 日 20:00,脚本内判断是否月末)
const monthlyConfig = {
name: `weather-monthly-userId`,
cronExpr: '0 20 28-31 * *',
tz: 'Asia/Shanghai',
session: 'isolated',
sessionKey,
channel,
to: userId,
announce: true,
timeoutSeconds: 120,
message: `node path.join(__dirname, 'monthly-push.js') userId`
};
console.log(`__OPENCLAW_CRON_ADD__:JSON.stringify(monthlyConfig)`);
const morningDisplay = `String(mh).padStart(2,'0'):String(mm).padStart(2,'0')`;
const eveningDisplay = `String(eh).padStart(2,'0'):String(em).padStart(2,'0')`;
// 更新用户资料中的推送设置
const updatedProfile = userProfile ? {
...userProfile,
preferences: {
...userProfile.preferences,
morningTime: morningDisplay,
eveningTime: eveningDisplay,
channel,
pushEnabled: true
},
pushEnabledAt: new Date().toISOString()
} : {
userId,
preferences: {
morningTime: morningDisplay,
eveningTime: eveningDisplay,
channel,
pushEnabled: true
},
pushEnabledAt: new Date().toISOString()
};
saveUser(userId, updatedProfile);
const city = (updatedProfile.city) || '(未设置)';
console.log(`
✅ 天气推送已开启
🌆 城市:city
⏰ 早间推送:每天 morningDisplay(今日天气)
🌙 晚间推送:每天 eveningDisplay(明日预告)
📅 周报推送:每周六 20:00(下周天气)
🗓️ 月报推送:每月末 20:00(下月概况)
📡 渠道:channel
关闭推送:node push-toggle.js off userId`);
}
function disablePush(userId) {
userId = sanitizeId(userId, 'userId');
const user = loadUser(userId);
if (!user) {
console.log(`❌ 未找到用户 userId 的推送记录`);
return;
}
console.log(`__OPENCLAW_CRON_RM__:weather-morning-userId`);
console.log(`__OPENCLAW_CRON_RM__:weather-evening-userId`);
console.log(`__OPENCLAW_CRON_RM__:weather-weekly-userId`);
console.log(`__OPENCLAW_CRON_RM__:weather-monthly-userId`);
const updated = {
...user,
preferences: {
...user.preferences,
pushEnabled: false
},
pushDisabledAt: new Date().toISOString()
};
saveUser(userId, updated);
console.log(`✅ 天气推送已关闭`);
}
function showStatus(userId) {
userId = sanitizeId(userId, 'userId');
const user = loadUser(userId);
if (!user) {
console.log(`❌ 未找到用户 userId 的推送记录`);
return;
}
const prefs = user.preferences || {};
const city = user.city || '(未设置)';
const pushEnabled = prefs.pushEnabled || false;
const morningTime = prefs.morningTime || '07:00';
const eveningTime = prefs.eveningTime || '21:00';
const channel = prefs.channel || 'telegram';
const enabledAt = user.pushEnabledAt ? user.pushEnabledAt.split('T')[0] : '未知';
console.log(`
📡 推送状态 — userId
━━━━━━━━━━━━━━━━━━━━━━━
城市:city
状态:'❌ 已关闭'
早间推送:morningTime(今日天气)
晚间推送:eveningTime(明日预告)
周报推送:每周六 20:00(下周天气)
月报推送:每月末 20:00(下月概况)
渠道:channel
开启于:enabledAt
━━━━━━━━━━━━━━━━━━━━━━━`);
}
module.exports = { enablePush, disablePush, showStatus };
if (require.main !== module) return;
const args = process.argv.slice(2);
const command = args[0];
const userId = args[1];
if (!command || !userId) {
console.log(`用法:
node push-toggle.js on <userId> [--morning 07:00] [--evening 21:00] [--channel telegram]
node push-toggle.js off <userId>
node push-toggle.js status <userId>`);
process.exit(1);
}
const opts = {};
const mi = args.indexOf('--morning');
if (mi !== -1) opts.morning = args[mi + 1];
const ei = args.indexOf('--evening');
if (ei !== -1) opts.evening = args[ei + 1];
const ci = args.indexOf('--channel');
if (ci !== -1) opts.channel = args[ci + 1];
switch (command) {
case 'on': enablePush(userId, opts); break;
case 'off': disablePush(userId); break;
case 'status': showStatus(userId); break;
default:
console.log(`❌ 未知命令: command`);
process.exit(1);
}
FILE:scripts/register.js
#!/usr/bin/env node
/**
* weather-daily — user registration / city setup
*
* Usage:
* node register.js <userId> <city> [units] [morningTime] [eveningTime] [language] [timezone]
*
* Parameters:
* userId required, letters/digits/-/_, 1-128 chars
* city required, 1-50 chars, supports Chinese/English/digits/spaces/hyphens
* units optional, metric (default) or imperial
* morningTime optional, HH:MM format (default 07:00)
* eveningTime optional, HH:MM format (default 21:00)
* language optional, zh or en (auto-detected from city name if omitted)
* timezone optional, IANA timezone (e.g. America/New_York; default: Asia/Shanghai for zh, UTC for en)
*
* Examples:
* node register.js alice 上海
* node register.js bob "New York" imperial 08:00 22:00 en America/New_York
* node register.js carol London metric 07:00 21:00 en Europe/London
*/
const fs = require('fs');
const path = require('path');
const USERS_DIR = path.join(__dirname, '../data/users');
function sanitizeId(value) {
if (typeof value !== 'string' || !/^[a-zA-Z0-9_-]{1,128}$/.test(value)) {
console.error('❌ Invalid userId: only letters, digits, - and _ are allowed (1-128 chars)');
process.exit(1);
}
return value;
}
function sanitizeCity(value) {
if (typeof value !== 'string') {
console.error('❌ Invalid city name');
process.exit(1);
}
const stripped = value.replace(/[^\u4e00-\u9fa5a-zA-Z0-9\s\-]/g, '').trim();
if (!/^[\u4e00-\u9fa5a-zA-Z0-9\s\-]{1,50}$/.test(stripped)) {
console.error('❌ Invalid city name: use Chinese/English/digits/spaces/hyphens, length 1-50');
process.exit(1);
}
return stripped;
}
function sanitizeUnits(value) {
if (value !== 'metric' && value !== 'imperial') {
console.error('❌ Invalid units: use metric or imperial');
process.exit(1);
}
return value;
}
function sanitizeTime(value, label) {
if (typeof value !== 'string' || !/^\d{1,2}:\d{2}$/.test(value)) {
console.error(`❌ Invalid label: format should be HH:MM, e.g. 07:00`);
process.exit(1);
}
const [h, m] = value.split(':').map(Number);
if (h < 0 || h > 23 || m < 0 || m > 59) {
console.error(`❌ Invalid label: hour 0-23, minute 0-59`);
process.exit(1);
}
return `String(h).padStart(2, '0'):String(m).padStart(2, '0')`;
}
function sanitizeLanguage(value) {
if (value !== 'zh' && value !== 'en') {
console.error('❌ Invalid language: use zh or en');
process.exit(1);
}
return value;
}
// Simple IANA timezone format check (not exhaustive, prevents injection)
function sanitizeTimezone(value) {
if (typeof value !== 'string' || !/^[A-Za-z][A-Za-z0-9_+\-\/]{0,49}$/.test(value)) {
console.error('❌ Invalid timezone: use IANA format, e.g. America/New_York');
process.exit(1);
}
return value;
}
function safeUserPath(userId) {
const resolved = path.resolve(USERS_DIR, `userId.json`);
if (!resolved.startsWith(path.resolve(USERS_DIR) + path.sep)) {
console.error('❌ Illegal path');
process.exit(1);
}
return resolved;
}
// Auto-detect language from city name: Chinese chars → zh, else → en
function detectLanguage(city) {
return /[\u4e00-\u9fa5]/.test(city) ? 'zh' : 'en';
}
// --- Main ---
const args = process.argv.slice(2);
if (args.length < 2) {
console.log(`Usage:
node register.js <userId> <city> [units] [morningTime] [eveningTime] [language] [timezone]
Parameters:
userId letters/digits/-/_, 1-128 chars
city city name, supports Chinese/English (e.g. 上海, Beijing, New York)
units metric (default, °C) or imperial (°F)
morningTime HH:MM format, morning push time (default 07:00)
eveningTime HH:MM format, evening push time (default 21:00)
language zh or en (auto-detected from city name if omitted)
timezone IANA timezone (default: Asia/Shanghai for zh, UTC for en)
Examples:
node register.js alice 上海
node register.js bob "New York" imperial 08:00 22:00 en America/New_York
node register.js carol London metric 07:00 21:00 en Europe/London`);
process.exit(1);
}
const userId = sanitizeId(args[0]);
const city = sanitizeCity(args[1]);
const units = sanitizeUnits(args[2] || 'metric');
const morningTime = sanitizeTime(args[3] || '07:00', 'morningTime');
const eveningTime = sanitizeTime(args[4] || '21:00', 'eveningTime');
const language = args[5] ? sanitizeLanguage(args[5]) : detectLanguage(city);
const defaultTz = language === 'zh' ? 'Asia/Shanghai' : 'UTC';
const timezone = args[6] ? sanitizeTimezone(args[6]) : defaultTz;
fs.mkdirSync(USERS_DIR, { recursive: true });
const filePath = safeUserPath(userId);
const now = new Date().toISOString();
let createdAt = now;
if (fs.existsSync(filePath)) {
try {
const existing = JSON.parse(fs.readFileSync(filePath, 'utf8'));
if (existing.createdAt) createdAt = existing.createdAt;
} catch (_) {}
}
const profile = {
userId,
city,
units,
language,
preferences: {
morningTime,
eveningTime,
timezone,
channel: 'telegram',
pushEnabled: false,
alerts: {
rain: true,
snow: true,
wind: true,
extreme: true,
airQuality: true
}
},
createdAt,
updatedAt: now
};
fs.writeFileSync(filePath, JSON.stringify(profile, null, 2), 'utf8');
const unitLabel = units === 'metric' ? '°C / metric' : '°F / imperial';
if (language === 'zh') {
console.log(`
✅ 用户注册成功
👤 用户:userId
🌆 城市:city
🌡️ 单位:unitLabel
🌐 语言:中文
⏰ 早间推送:morningTime(今日天气)
🌙 晚间推送:eveningTime(明日预告)
🕐 时区:timezone
下一步:
开启每日推送:node scripts/push-toggle.js on userId
查看今日天气:node scripts/morning-push.js userId
查看一周预报:node scripts/forecast.js userId
修改城市设置:node scripts/register.js userId <新城市>`);
} else {
console.log(`
✅ Registration successful
👤 User: userId
🌆 City: city
🌡️ Units: unitLabel
🌐 Language: English
⏰ Morning push: morningTime (today's weather)
🌙 Evening push: eveningTime (tomorrow's preview)
🕐 Timezone: timezone
Next steps:
Enable daily push: node scripts/push-toggle.js on userId
Today's weather: node scripts/morning-push.js userId
Weekly forecast: node scripts/forecast.js userId
Change city: node scripts/register.js userId <new-city>`);
}
FILE:scripts/weekly-push.js
#!/usr/bin/env node
/**
* weather-daily — next-week forecast prompt generator (sent every weekend)
* Output is fulfilled by Claude via WebSearch
*
* Usage:
* node weekly-push.js <userId>
*/
const fs = require('fs');
const path = require('path');
const USERS_DIR = path.join(__dirname, '../data/users');
function sanitizeId(value) {
if (typeof value !== 'string' || !/^[a-zA-Z0-9_-]{1,128}$/.test(value)) {
console.error('❌ Invalid userId');
process.exit(1);
}
return value;
}
function safeUserPath(userId) {
const resolved = path.resolve(USERS_DIR, `userId.json`);
if (!resolved.startsWith(path.resolve(USERS_DIR) + path.sep)) {
console.error('❌ Illegal path');
process.exit(1);
}
return resolved;
}
function loadUser(userId) {
const f = safeUserPath(userId);
if (!fs.existsSync(f)) {
console.error(`❌ User userId not found. Run: node register.js userId <city>`);
process.exit(1);
}
return JSON.parse(fs.readFileSync(f, 'utf8'));
}
const args = process.argv.slice(2);
if (!args[0]) {
console.error('Usage: node weekly-push.js <userId>');
process.exit(1);
}
const userId = sanitizeId(args[0]);
const user = loadUser(userId);
const city = user.city || '上海';
const units = user.units || 'metric';
const unit = units === 'metric' ? 'C' : 'F';
const lang = user.language || ((/[\u4e00-\u9fa5]/.test(city)) ? 'zh' : 'en');
const now = new Date();
// Next Monday
const dayOfWeek = now.getDay();
const daysUntilMonday = dayOfWeek === 0 ? 1 : 8 - dayOfWeek;
const nextMonday = new Date(now);
nextMonday.setDate(now.getDate() + daysUntilMonday);
const nextSunday = new Date(nextMonday);
nextSunday.setDate(nextMonday.getDate() + 6);
const year = now.getFullYear();
if (lang === 'zh') {
const MONTHS = ['一月','二月','三月','四月','五月','六月','七月','八月','九月','十月','十一月','十二月'];
const fmtZh = d => `d.getMonth()+1月d.getDate()日`;
const weekRange = `fmtZh(nextMonday)~fmtZh(nextSunday)`;
console.log(`请为用户查询 city 下周(weekRange)的天气预报,并按以下格式推送周报。
搜索步骤:
1. 搜索「city 下周天气预报」
2. 搜索「city year年nextMonday.getMonth()+1月天气」
输出格式:
📅 city 下周天气周报 · weekRange
━━━━━━━━━━━━━━━━━━━━━━━
[每天一行,周一到周日]
{星期}({日期}) {天气图标} {天气} 🌡️{低温}~{高温}°unit {简短提示}
━━━━━━━━━━━━━━━━━━━━━━━
📌 下周天气趋势:[整体天气变化,2-3句]
⚠️ 重要预警:[下周内极端天气,无则省略]
☂️ 本周备雨建议:[哪几天需要带伞]
👔 穿衣趋势:[温差变化与穿衣建议]
📅 最佳出行日:[下周最适合户外活动的1-2天]
💡 回复"今天天气"获取今日实况`);
} else {
const WEEKDAYS_EN = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
const MONTHS_EN = ['January','February','March','April','May','June','July','August','September','October','November','December'];
const fmtEn = d => `MONTHS_EN[d.getMonth()] d.getDate()`;
const weekRange = `fmtEn(nextMonday) – fmtEn(nextSunday)`;
console.log(`Please search for next week's weather forecast for city (weekRange) and send the weekly report in the following format.
Search steps:
1. Search "city next week weather forecast"
2. Search "city MONTHS_EN[nextMonday.getMonth()] year weather"
Output format:
📅 city Next Week Weather · weekRange
━━━━━━━━━━━━━━━━━━━━━━━
[One line per day, Monday to Sunday]
{Day} ({Date}) {icon} {conditions} 🌡️{low}~{high}°unit {brief tip}
━━━━━━━━━━━━━━━━━━━━━━━
📌 Weekly trend: [2–3 sentences on overall pattern]
⚠️ Alerts: [any extreme weather warnings; omit if none]
☂️ Umbrella days: [which days to carry an umbrella]
👔 What to wear: [temperature swings and outfit advice]
📅 Best day out: [1–2 best days for outdoor activities]
💡 Reply "today's weather" for today's detailed report`);
}
NewsToday solves information overload for users who want to stay informed without spending an hour checking scattered sources. Instead of manually browsing W...
---
name: NewsToday
description: |
NewsToday solves information overload for users who want to stay informed without spending an hour checking scattered sources. Instead of manually browsing Weibo, Zhihu, Baidu, and news apps separately, NewsToday aggregates, deduplicates, and summarizes the most important stories into a single readable briefing delivered at the right time.
Every morning, NewsToday pushes a curated briefing of 10 top stories spanning politics, finance, technology, international affairs, and society — pulled from both RSS feeds (Sina News, The Paper, 36Kr, BBC Chinese, Reuters Chinese) and real-time WebSearch, each with a 2-sentence summary and source attribution. Every evening, a recap highlights what developed throughout the day and previews tomorrow's key events. Breaking news alerts fire automatically every 2 hours during daytime whenever a major story breaks — earthquakes, market crashes, political announcements — so users never miss what matters.
Users can tune their experience by setting topic preferences — weighting finance over entertainment, or boosting international coverage — so every briefing reflects what actually matters to them. Supports Chinese and English output. Deliverable via Telegram, Feishu, Slack, or Discord. No registration required for on-demand queries; optional user profile unlocks personalized daily push and breaking alerts.
Trigger words: 早报, 晚报, 今日新闻, 新闻摘要, 热榜, 热搜, 追踪, 最新消息, 突发, 微博热搜, 知乎热榜, X热帖, 科技新闻, 财经新闻, AI早报, AI最新, 人工智能动态, 军事新闻, 军事动态, 头条, 订阅新闻, morning briefing, daily news, news summary, Chinese news, trending, breaking news, news push, hot topics, topic tracking, international news, AI news, military news.
keywords: 新闻推送, 早报, 新闻摘要, 每日新闻, 今日新闻, 热榜, 热搜, 订阅新闻, 晚报, 突发新闻, 微博热搜, 知乎热榜, 百度热搜, X热帖, 头条, 科技新闻, 财经新闻, 娱乐新闻, 体育新闻, 社会新闻, 国际新闻, 军事新闻, 地区冲突, 国防政策, AI早报, AI新闻, 大模型动态, 人工智能, 话题追踪, 最新消息, RSS新闻, 新闻聚合, 资讯, 快讯, 要闻, 每天新闻, 看新闻, 新闻助手, 资讯助手, 新闻机器人, 每日资讯, news push, daily briefing, news summary, Chinese news, morning briefing, evening news, trending, hot topics, breaking news, topic tracking, news aggregator, RSS feeds, personalized news, military news, AI news, news bot, daily news bot, news digest, news alert, China news, top stories, news reader
metadata:
openclaw:
runtime:
node: ">=18"
---
# NewsToday
> 私人新闻助手 — 早报 · 晚报 · RSS聚合 · 突发提醒 · 话题追踪 · 个性化推送
## 何时使用
- 用户说"早报""今天新闻""新闻摘要""今天发生了什么"
- 用户问"热搜""微博热榜""知乎热榜""X热帖"
- 用户说"AI 早报""AI 最新""人工智能动态"
- 用户想看某类新闻:科技、AI、财经、娱乐、体育、社会、国际、军事
- 用户说"追踪 XX""XX 最新消息""XX 怎么样了"
- 用户说"开启推送""订阅早报""每天推新闻"
- 用户说"突发""重大消息""有什么大事"
---
## 🌐 语言规则
- 默认中文;用户英文提问切英文
- 新闻标题保留原文,摘要用回复语言改写
---
## 📋 功能说明
### 早报
从 RSS(新浪/澎湃/36氪/BBC中文/Reuters中文)+ WebSearch 双源聚合,去重后选10条覆盖不同领域,按用户话题偏好加权排序。头部显示今日条数和预估阅读时长。第1条为**头条**(重要性最高,3-4句详细摘要+影响分析),其余9条常规格式(标题、来源、2句摘要)。财经类每条含影响评级:📈 利好 / 📉 利空 / ➡️ 中性。
### 晚报
收官3-5条当日重要新闻 + 1-2条热点最新进展 + 明日日程预告。
### 突发新闻提醒
每2小时检测(08:00-22:00),仅在满足阈值(7级以上地震、市场熔断、重大政策等)时推送,不骚扰用户。
### 热榜聚合
搜索微博热搜 + 知乎热榜 + 百度热搜 + X(Twitter)热帖,去重合并,标注来源,多平台共同热点置顶。X 热帖作为第三方实时信号,补充国内平台之前的舆情风向;若 X 数据不可用则静默降级,不影响其他来源输出。
### 话题追踪
搜索 `{关键词} 最新 {日期}` + `{关键词} 进展` + `{关键词} 官方回应`,时间线倒序输出,含各方反应。
### 深读
用户回复序号或说"详细说说 XX"时,多角度搜索,交叉验证,呈现详细经过、各方反应、延伸阅读。
### AI 早报(独立模式)
用户说"AI 早报""AI 最新""人工智能动态"时触发独立模式:专门搜索 `AI 最新进展 {日期}`、`大模型 新闻`、`OpenAI Anthropic Google DeepMind 动态`,输出 5 条 AI 专项摘要,含产品发布、研究突破、行业动向,与常规早报格式一致但信源更聚焦。
### 分类浏览
| 分类 | 搜索词 |
|------|--------|
| 科技 | 科技新闻 今日、AI新闻 |
| AI | AI 最新进展、大模型 新闻、OpenAI Anthropic 动态 |
| 财经 | 财经新闻 今日、股市 |
| 娱乐 | 娱乐新闻 今日 |
| 体育 | 体育新闻 今日、赛事结果 |
| 社会 | 社会新闻 今日、民生 |
| 国际 | 国际新闻 今日、外交 |
| 军事 | 军事新闻 今日、地区冲突、国防政策、军事演习 |
---
## 🔧 脚本说明
```bash
# 注册(可选,解锁个性化推送)
node scripts/register.js <userId> [language] [topics] [channel]
# 示例:
node scripts/register.js alice zh 科技,财经,国际 telegram
node scripts/register.js bob en tech,finance telegram
# 话题偏好
node scripts/preference.js show <userId>
node scripts/preference.js set <userId> <话题> <权重0-1>
node scripts/preference.js reset <userId>
# 手动触发(不需要注册)
node scripts/morning-push.js [userId]
node scripts/evening-push.js [userId]
node scripts/rss-fetch.js [--lang zh|en] [--topics 科技,财经,国际]
node scripts/breaking-alert.js <userId>
# 推送管理
node scripts/push-toggle.js on <userId> [--morning 08:00] [--evening 20:00] [--channel telegram]
node scripts/push-toggle.js off <userId>
node scripts/push-toggle.js status <userId>
```
支持渠道:`telegram` / `feishu` / `slack` / `discord`
---
## ⚠️ 注意事项
1. 每条新闻必须标注来源媒体
2. 涉及争议内容呈现多方视角,不做立场判断
3. 不注册可直接使用早晚报;注册后可按话题个性化、开启突发提醒
4. 用户数据仅存储推送偏好和话题权重(`data/users/<userId>.json`),不含新闻内容
5. RSS 源无法访问时自动降级为 WebSearch,不影响正常使用
FILE:README.md
# NewsToday
> 私人新闻助手 — 早报 · 晚报 · AI早报 · RSS聚合 · 突发提醒 · 热榜聚合 · 话题追踪 · 军事新闻 · 个性化推送
An [OpenClaw](https://openclaw.ai) skill that aggregates, deduplicates, and summarizes the most important news stories into a single readable briefing — delivered at the right time, via your preferred channel.
## Features
- **Morning Briefing** — 10 top stories across politics, finance, tech, international, and society; pulled from RSS feeds (Sina News, The Paper, 36Kr, BBC Chinese, Reuters Chinese) + real-time WebSearch
- **Evening Recap** — 3–5 key developments of the day + preview of tomorrow's events
- **Breaking News Alerts** — auto-fires every 2 hours (08:00–22:00) when a major story breaks (earthquakes, market crashes, political announcements)
- **Hot List Aggregation** — merges Weibo, Zhihu, Baidu, and X (Twitter) trending lists with deduplication; X falls back silently if unavailable
- **AI Briefing** — dedicated mode for AI/LLM news (trigger: "AI早报", "AI最新", "人工智能动态"); 5 focused stories from AI-specific sources
- **Topic Tracking** — follows a keyword over time with timeline, official responses, and multiple perspectives
- **Personalized Topics** — weight finance over entertainment, boost international coverage, etc.
- **Military News** — dedicated category covering regional conflicts, defense policy, and military exercises
- **Bilingual** — Chinese and English output supported
## Supported Channels
`telegram` / `feishu` / `slack` / `discord`
## Quick Start
```bash
# Optional: register to unlock personalized push & breaking alerts
node scripts/register.js <userId> [language] [topics] [channel]
# e.g.
node scripts/register.js alice zh 科技,财经,国际 telegram
node scripts/register.js bob en tech,finance telegram
# Manual trigger (no registration needed)
node scripts/morning-push.js [userId]
node scripts/evening-push.js [userId]
node scripts/rss-fetch.js [--lang zh|en] [--topics 科技,财经]
node scripts/breaking-alert.js <userId>
# Topic preferences
node scripts/preference.js show <userId>
node scripts/preference.js set <userId> <topic> <weight 0-1>
node scripts/preference.js reset <userId>
# Push management
node scripts/push-toggle.js on <userId> [--morning 08:00] [--evening 20:00] [--channel telegram]
node scripts/push-toggle.js off <userId>
node scripts/push-toggle.js status <userId>
```
## Notes
- No registration required for on-demand morning/evening briefings
- Each story includes source attribution and a 2-sentence summary
- Controversial topics present multiple perspectives without editorial stance
- If RSS feeds are unavailable, the skill automatically falls back to WebSearch
- User data stored in `data/users/<userId>.json` contains only preferences and topic weights — no news content
FILE:_meta.json
{
"slug": "newstoday",
"version": "2.1.0",
"runtime": {
"node": ">=18"
}
}
FILE:package.json
{
"name": "newstoday",
"version": "2.1.0",
"description": "NewsToday — 中文新闻聚合 Skill。头条精读 · 财经影响评级 · 个性化早晚报 · RSS聚合 · 突发提醒 · 热榜追踪。支持中英双语。",
"keywords": [
"新闻推送", "早报", "新闻摘要", "每日新闻", "今日新闻", "热榜", "热搜", "订阅新闻",
"突发新闻", "话题追踪", "微博热搜", "知乎热榜", "百度热搜", "X热帖", "科技新闻",
"财经新闻", "AI早报", "AI新闻", "大模型动态", "人工智能", "军事新闻", "头条",
"新闻聚合", "资讯", "快讯", "要闻", "新闻助手", "新闻机器人", "每日资讯",
"news push", "daily briefing", "news summary", "Chinese news", "trending",
"breaking news", "topic tracking", "morning briefing", "RSS", "AI news", "military news",
"news bot", "news digest", "news alert", "China news", "top stories", "news reader"
],
"author": "jiajiaoy",
"license": "MIT",
"scripts": {
"morning": "node scripts/morning-push.js",
"evening": "node scripts/evening-push.js",
"rss": "node scripts/rss-fetch.js",
"breaking": "node scripts/breaking-alert.js",
"register": "node scripts/register.js",
"preference": "node scripts/preference.js",
"push-on": "node scripts/push-toggle.js on",
"push-off": "node scripts/push-toggle.js off",
"push-status": "node scripts/push-toggle.js status"
}
}
FILE:scripts/breaking-alert.js
#!/usr/bin/env node
/**
* NewsToday — 突发新闻检测 prompt 生成器
* 由 openclaw cron 每 2 小时执行(08:00-22:00)
* 无文件 I/O:所有参数由 push-toggle.js 在设置 cron 时嵌入命令行。
* 只有检测到重大突发新闻时,Claude 才发送提醒(否则静默)。
*
* 用法:
* node breaking-alert.js [--lang zh|en] [--topics 科技,财经,国际]
*/
const ALLOWED_TOPICS = new Set(['科技','财经','国际','社会','娱乐','体育','tech','finance','international','society','entertainment','sports']);
const args = process.argv.slice(2);
const langIdx = args.indexOf('--lang');
const rawLang = langIdx !== -1 ? args[langIdx + 1] : null;
const lang = rawLang === 'en' ? 'en' : 'zh';
const topicsIdx = args.indexOf('--topics');
const rawTopics = topicsIdx !== -1 ? args[topicsIdx + 1] : null;
const topicList = rawTopics
? rawTopics.split(',').map(t => t.trim()).filter(t => ALLOWED_TOPICS.has(t))
: [];
const now = new Date();
const dateISO = `now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const timeStr = `String(now.getHours()).padStart(2,'0'):String(now.getMinutes()).padStart(2,'0')`;
if (lang === 'zh') {
const topicHint = topicList.length
? `用户重点关注领域:topicList.join('、'),优先检测这些领域的突发事件。`
: '';
console.log(`请检测当前是否有重大突发新闻,仅在有真正重要的突发事件时才发送提醒。
当前时间:dateISO timeStr
topicHint
检测步骤:
1. 搜索「突发新闻 dateISO」
2. 搜索「今日重大事件 最新」
3. 如话题包含财经,额外搜索「市场暴跌 OR 股市熔断 dateISO」
4. 如话题包含国际,额外搜索「国际突发 dateISO」
判断标准(满足以下任一条才发送提醒):
- 自然灾害:7级以上地震、大型台风、洪灾
- 重大事故:重大交通/安全事故,伤亡较大
- 金融市场:主要指数单日跌幅 >5%,或熔断
- 政治外交:重大政策发布、外交冲突升级
- 公共卫生:疫情爆发、重大食品安全事件
- 科技事件:重大数据泄露、主流平台大规模宕机
如果没有符合标准的突发事件:输出一个空行,不发送任何内容。
如果有符合标准的突发事件,按以下格式输出:
🚨 突发 · timeStr
━━━━━━━━━━━━━━━━━━━━━━━
[事件标题]
[3-4句描述:什么事、哪里、影响范围、最新进展]
📌 来源:[媒体名称]
💡 回复"追踪"获取持续更新`);
} else {
const topicHint = topicList.length
? `User's priority topics: topicList.join(', '). Focus detection on these areas.`
: '';
console.log(`Check for major breaking news right now. Only send an alert if there is a genuinely significant breaking event.
Current time: dateISO timeStr
topicHint
Detection steps:
1. Search "breaking news dateISO"
2. Search "major event today latest"
3. If topics include finance, also search "market crash OR circuit breaker dateISO"
4. If topics include international, also search "international breaking news dateISO"
Alert criteria (send only if at least one applies):
- Natural disaster: magnitude 7+ earthquake, major hurricane/typhoon, severe flooding
- Major accident: significant casualties in transport or industrial accident
- Financial markets: major index drops >5% in a day, or trading halt triggered
- Politics/diplomacy: major policy announcement, significant escalation
- Public health: disease outbreak, major food safety incident
- Tech: large-scale data breach, major platform outage
If no qualifying breaking event found: output a single blank line. Send nothing.
If a qualifying breaking event is found:
🚨 Breaking · timeStr
━━━━━━━━━━━━━━━━━━━━━━━
[Event headline]
[3–4 sentences: what happened, where, scale, latest update]
📌 Source: [media name]
💡 Reply "track" for continuous updates`);
}
FILE:scripts/evening-push.js
#!/usr/bin/env node
/**
* NewsToday — 晚间推送 prompt 生成器
* 由 openclaw cron 驱动,每日 20:00 执行
* 无文件 I/O:所有个性化参数由 push-toggle.js 在设置 cron 时嵌入命令行。
*
* 用法:
* node evening-push.js [--lang zh|en] [--topics 科技,财经,国际]
*/
const ALLOWED_TOPICS = new Set(['科技','财经','国际','社会','娱乐','体育','tech','finance','international','society','entertainment','sports']);
const args = process.argv.slice(2);
const langIdx = args.indexOf('--lang');
const rawLang = langIdx !== -1 ? args[langIdx + 1] : null;
const lang = rawLang === 'en' ? 'en' : 'zh';
const topicsIdx = args.indexOf('--topics');
const rawTopics = topicsIdx !== -1 ? args[topicsIdx + 1] : null;
const topicList = rawTopics
? rawTopics.split(',').map(t => t.trim()).filter(t => ALLOWED_TOPICS.has(t))
: [];
const now = new Date();
const tomorrow = new Date(now);
tomorrow.setDate(tomorrow.getDate() + 1);
const dateISO = `now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const tomorrowISO = `tomorrow.getFullYear()-String(tomorrow.getMonth()+1).padStart(2,'0')-String(tomorrow.getDate()).padStart(2,'0')`;
if (lang === 'zh') {
const WEEKDAYS = ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const dateStr = `now.getFullYear()年now.getMonth()+1月now.getDate()日 WEEKDAYS[now.getDay()]`;
const tomorrowStr = `tomorrow.getMonth()+1月tomorrow.getDate()日WEEKDAYS[tomorrow.getDay()]`;
const topicHint = topicList.length
? `\n用户重点关注:topicList.join('、'),收官和预告请侧重这些领域。`
: '';
console.log(`请生成今日晚间新闻汇总与明日预告。当前日期:dateStrtopicHint
执行步骤:
1. 搜索「今日晚间重要新闻 dateISO」
2. 搜索「今日热点事件最新进展」
3. 搜索「tomorrowISO 重要日程 财经 政治 体育」
输出格式:
🌙 晚间快报 · dateStr
━━━━━━━━━━━━━━━━━━━━━━━
📋 今日收官(3-5条下午/晚间重要新闻,每条附2句摘要及来源)
🔄 今日热点进展(1-2条今天持续发酵事件的最新动态)
📅 明日预告(tomorrowStr值得关注)
· 重要会议 / 政策发布 / 赛事
· 财经数据公布时间
· 预计有进展的持续事件
━━━━━━━━━━━━━━━━━━━━━━━
💡 回复序号深读 · 明日早报08:00见`);
} else {
const WEEKDAYS_EN = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
const MONTHS_EN = ['January','February','March','April','May','June','July','August','September','October','November','December'];
const dateStr = `WEEKDAYS_EN[now.getDay()], MONTHS_EN[now.getMonth()] now.getDate(), now.getFullYear()`;
const tomorrowStr = `WEEKDAYS_EN[tomorrow.getDay()], MONTHS_EN[tomorrow.getMonth()] tomorrow.getDate()`;
const topicHint = topicList.length
? `\nUser's priority topics: topicList.join(', ') — weight recap and preview toward these.`
: '';
console.log(`Please generate the evening news recap and tomorrow's preview. Date: dateStrtopicHint
Steps:
1. Search "top news this evening dateISO"
2. Search "today's major story latest update"
3. Search "tomorrowISO key events schedule finance politics sports"
Output format:
🌙 Evening Recap · dateStr
━━━━━━━━━━━━━━━━━━━━━━━
📋 Today's wrap-up (3–5 significant afternoon/evening stories, each with 2-sentence summary and source)
🔄 Developing stories (1–2 ongoing stories with latest updates)
📅 Tomorrow's preview (tomorrowStr)
· Key meetings / policy announcements / sports events
· Economic data releases
· Expected developments in ongoing stories
━━━━━━━━━━━━━━━━━━━━━━━
💡 Reply a number to deep-read · Morning briefing tomorrow at 8 AM`);
}
FILE:scripts/morning-push.js
#!/usr/bin/env node
/**
* NewsToday — 早报 prompt 生成器
* 由 openclaw cron 驱动,每日 08:00 执行
* 无文件 I/O:所有个性化参数由 push-toggle.js 在设置 cron 时嵌入命令行。
*
* 用法:
* node morning-push.js [--lang zh|en] [--topics 科技,财经,国际]
*/
// 允许的话题白名单
const ALLOWED_TOPICS = new Set(['科技','财经','国际','社会','娱乐','体育','tech','finance','international','society','entertainment','sports']);
const args = process.argv.slice(2);
const langIdx = args.indexOf('--lang');
const rawLang = langIdx !== -1 ? args[langIdx + 1] : null;
const lang = rawLang === 'en' ? 'en' : 'zh';
const topicsIdx = args.indexOf('--topics');
const rawTopics = topicsIdx !== -1 ? args[topicsIdx + 1] : null;
// 仅保留白名单内的话题,过滤任意外部输入
const topicList = rawTopics
? rawTopics.split(',').map(t => t.trim()).filter(t => ALLOWED_TOPICS.has(t))
: [];
const now = new Date();
const dateISO = `now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
if (lang === 'zh') {
const WEEKDAYS = ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const dateStr = `now.getFullYear()年now.getMonth()+1月now.getDate()日 WEEKDAYS[now.getDay()]`;
const topicHint = topicList.length
? `\n用户重点关注:topicList.join('、')(优先多选这些领域的新闻)。`
: '';
console.log(`请生成今日个性化早报。当前日期:dateStrtopicHint
信息来源(按优先级):
1. 【WebSearch 主源】
- 搜索「今日重要新闻 dateISO」
- 搜索「今日国际新闻 dateISO」
- 搜索「今日财经新闻 dateISO」
2. 【RSS 补充】可运行 node scripts/rss-fetch.js --lang zh 获取 RSS 源列表
处理要求:
- 去重后选取 10 条,覆盖不同领域(重要/财经/国际/科技/社会各至少 1 条)
- 第1条为【头条】:重要性最高的单条新闻,含标题、来源、时间、3-4句详细摘要、影响分析
- 其余9条为常规条目:标题、来源媒体、发布时间、2句摘要
- 财经类每条额外标注影响评级:📈 利好 / 📉 利空 / ➡️ 中性
- 按领域分组,每条标注话题标签
- 有争议内容保持中立,标注多方视角
- 统计总条数和预估阅读时长(每条约30秒)
输出格式:
📰 今日早报 · dateStr | 10条 · 阅读约5分钟
━━━━━━━━━━━━━━━━━━━━━━━
🔥 头条
[头条新闻 — 标题加粗,3-4句详细摘要,含影响分析]
━━━━━━━━━━━━━━━━━━━━━━━
🔴 重要
[新闻条目]
💰 财经 (每条含 📈/📉/➡️ 评级)
[新闻条目]
🌍 国际
[新闻条目]
💻 科技
[新闻条目]
🏙️ 社会
[新闻条目]
━━━━━━━━━━━━━━━━━━━━━━━
💡 回复序号深读 · 回复"热榜"查看实时热搜 · 晚报20:00见`);
} else {
const WEEKDAYS_EN = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
const MONTHS_EN = ['January','February','March','April','May','June','July','August','September','October','November','December'];
const dateStr = `WEEKDAYS_EN[now.getDay()], MONTHS_EN[now.getMonth()] now.getDate(), now.getFullYear()`;
const topicHint = topicList.length
? `\nUser's priority topics: topicList.join(', ') — prefer these categories.`
: '';
console.log(`Please generate today's personalized morning news briefing. Date: dateStrtopicHint
Sources (in priority order):
1. [WebSearch main]
- Search "top news today dateISO"
- Search "international news today dateISO"
- Search "financial news today dateISO"
2. [RSS supplement] Run node scripts/rss-fetch.js --lang en for RSS feed list
Requirements:
- After deduplication, select 10 stories covering different categories
- Story #1 is the [Lead Story]: highest-importance single item, with headline, source, time, 3-4 sentence detailed summary, and impact analysis
- Remaining 9 are standard entries: headline, source, publish time, 2-sentence summary
- Finance entries must include an impact tag: 📈 bullish / 📉 bearish / ➡️ neutral
- Group by category, tag each story
- Present disputed topics neutrally with multiple perspectives
- Count total stories and estimate reading time (~30 sec per story)
Output format:
📰 Morning Briefing · dateStr | 10 stories · ~5 min read
━━━━━━━━━━━━━━━━━━━━━━━
🔥 Lead Story
[Top story — bold headline, 3-4 sentence detailed summary with impact analysis]
━━━━━━━━━━━━━━━━━━━━━━━
🔴 Top Stories
[entries]
💰 Finance (each with 📈/📉/➡️ rating)
[entries]
🌍 International
[entries]
💻 Tech
[entries]
🏙️ Society
[entries]
━━━━━━━━━━━━━━━━━━━━━━━
💡 Reply a number to deep-read · Reply "trending" for hot topics · Evening recap at 8 PM`);
}
FILE:scripts/preference.js
#!/usr/bin/env node
/**
* NewsToday — 话题偏好管理
*
* 用法:
* node preference.js show <userId> 查看当前偏好
* node preference.js set <userId> <话题> <权重> 设置话题权重(0.0 - 1.0)
* node preference.js reset <userId> 重置为默认偏好
*
* 话题: 科技 财经 国际 社会 娱乐 体育
* 权重: 0.0(不感兴趣)~ 1.0(最感兴趣)
*/
const fs = require('fs');
const path = require('path');
const USERS_DIR = path.join(__dirname, '../data/users');
const ALLOWED_TOPICS = ['科技', '财经', '国际', '社会', '娱乐', '体育'];
const TOPIC_MAP = { tech: '科技', finance: '财经', international: '国际', society: '社会', entertainment: '娱乐', sports: '体育' };
const DEFAULT_TOPICS = { 科技: 0.8, 财经: 0.8, 国际: 0.7, 社会: 0.6, 娱乐: 0.3, 体育: 0.3 };
function sanitizeId(value) {
if (typeof value !== 'string' || !/^[a-zA-Z0-9_-]{1,128}$/.test(value)) {
console.error('❌ 无效的 userId');
process.exit(1);
}
return value;
}
function safeUserPath(userId) {
const resolved = path.resolve(USERS_DIR, `userId.json`);
if (!resolved.startsWith(path.resolve(USERS_DIR) + path.sep)) {
console.error('❌ 非法路径');
process.exit(1);
}
return resolved;
}
function loadUser(userId) {
const f = safeUserPath(userId);
if (!fs.existsSync(f)) {
console.error(`❌ 未找到用户 userId,请先运行: node register.js userId`);
process.exit(1);
}
return JSON.parse(fs.readFileSync(f, 'utf8'));
}
function saveUser(userId, data) {
fs.writeFileSync(safeUserPath(userId), JSON.stringify(data, null, 2), 'utf8');
}
function bar(weight) {
const filled = Math.round(weight * 10);
return '█'.repeat(filled) + '░'.repeat(10 - filled);
}
const args = process.argv.slice(2);
const command = args[0];
const userId = args[1] ? sanitizeId(args[1]) : null;
if (!command || !userId) {
console.log(`用法:
node preference.js show <userId>
node preference.js set <userId> <话题> <权重0-1>
node preference.js reset <userId>`);
process.exit(1);
}
switch (command) {
case 'show': {
const user = loadUser(userId);
const topics = user.topics || DEFAULT_TOPICS;
console.log(`\n📌 话题偏好 — userId\n'━'.repeat(30)`);
for (const t of ALLOWED_TOPICS) {
const w = topics[t] ?? 0.5;
console.log(` t.padEnd(4) bar(w) (w * 10).toFixed(0)/10`);
}
console.log('━'.repeat(30));
console.log(`语言:'中文' 渠道:user.channel || 'telegram'`);
break;
}
case 'set': {
const rawTopic = args[2];
const rawWeight = args[3];
if (!rawTopic || rawWeight === undefined) {
console.error('用法: node preference.js set <userId> <话题> <权重0-1>');
process.exit(1);
}
const topic = TOPIC_MAP[rawTopic] || rawTopic;
if (!ALLOWED_TOPICS.includes(topic)) {
console.error(`❌ 无效话题:rawTopic。可用:ALLOWED_TOPICS.join(', ')`);
process.exit(1);
}
const weight = parseFloat(rawWeight);
if (isNaN(weight) || weight < 0 || weight > 1) {
console.error('❌ 权重须在 0.0 ~ 1.0 之间');
process.exit(1);
}
const user = loadUser(userId);
user.topics = user.topics || { ...DEFAULT_TOPICS };
user.topics[topic] = weight;
user.updatedAt = new Date().toISOString();
saveUser(userId, user);
console.log(`✅ 已设置「topic」权重为 weight.toFixed(1)`);
break;
}
case 'reset': {
const user = loadUser(userId);
user.topics = { ...DEFAULT_TOPICS };
user.updatedAt = new Date().toISOString();
saveUser(userId, user);
console.log(`✅ 话题偏好已重置为默认值`);
break;
}
default:
console.error(`❌ 未知命令: command`);
process.exit(1);
}
FILE:scripts/push-toggle.js
#!/usr/bin/env node
/**
* NewsToday — 推送开关
*
* 用法:
* node push-toggle.js on <userId> 开启推送
* node push-toggle.js off <userId> 关闭推送
* node push-toggle.js status <userId> 查看状态
*
* 选项:
* --morning HH:MM 早报时间(默认 08:00)
* --evening HH:MM 晚报时间(默认 20:00)
* --channel <name> 推送渠道(默认 telegram)
*/
const fs = require('fs');
const path = require('path');
const USERS_DIR = path.join(__dirname, '../data/users');
// 只允许字母、数字、连字符、下划线,最长 128 字符
function sanitizeId(value, label) {
if (typeof value !== 'string' || !/^[a-zA-Z0-9_-]{1,128}$/.test(value)) {
console.error(`❌ 无效的 label:只允许字母、数字、- 和 _,长度 1-128`);
process.exit(1);
}
return value;
}
// 校验 HH:MM 格式,返回 { h, m } 整数
function sanitizeTime(value, label) {
if (typeof value !== 'string' || !/^\d{1,2}:\d{2}$/.test(value)) {
console.error(`❌ 无效的 label:格式应为 HH:MM,如 08:00`);
process.exit(1);
}
const [h, m] = value.split(':').map(Number);
if (h < 0 || h > 23 || m < 0 || m > 59) {
console.error(`❌ 无效的 label:小时 0-23,分钟 0-59`);
process.exit(1);
}
return { h, m };
}
// 验证文件路径确实在 USERS_DIR 内(防路径穿越)
function safeUserPath(userId) {
const resolved = path.resolve(USERS_DIR, `userId.json`);
if (!resolved.startsWith(path.resolve(USERS_DIR) + path.sep)) {
console.error('❌ 非法路径');
process.exit(1);
}
return resolved;
}
function loadUser(userId) {
const f = safeUserPath(userId);
if (!fs.existsSync(f)) return null;
return JSON.parse(fs.readFileSync(f, 'utf8'));
}
function saveUser(userId, data) {
fs.mkdirSync(USERS_DIR, { recursive: true });
fs.writeFileSync(safeUserPath(userId), JSON.stringify(data, null, 2), 'utf8');
}
const ALLOWED_CHANNELS = new Set(['telegram', 'feishu', 'slack', 'discord']);
function enablePush(userId, opts = {}) {
userId = sanitizeId(userId, 'userId');
// 一次性读取用户档案,仅提取原始标量值用于构建 cron 命令
// push 脚本本身不读文件,偏好通过 CLI 参数传入
const profile = loadUser(userId);
const defaultChannel = profile?.channel || 'telegram';
const defaultTz = 'Asia/Shanghai'; // 不从文件读取 tz,避免不可信数据进入 cron 配置
// 从档案中提取 lang(仅允许 zh/en)和 topics(仅白名单话题名)
const ALLOWED_TOPICS = new Set(['科技','财经','国际','社会','娱乐','体育']);
const profileLang = profile?.language === 'en' ? 'en' : 'zh';
const profileTopics = profile?.topics
? Object.entries(profile.topics)
.filter(([t, w]) => ALLOWED_TOPICS.has(t) && typeof w === 'number' && w >= 0.7)
.map(([t]) => t)
.join(',')
: '';
// 构建 push 脚本的 CLI 参数(lang 和 topics 均经过白名单过滤)
const pushArgs = `--lang profileLangprofileTopics ? ` --topics ${profileTopics` : ''}`;
const { h: mh, m: mm } = sanitizeTime(opts.morning || '08:00', 'morning');
const { h: eh, m: em } = sanitizeTime(opts.evening || '20:00', 'evening');
const rawChannel = opts.channel || defaultChannel;
if (!ALLOWED_CHANNELS.has(rawChannel)) {
console.error(`❌ 不支持的渠道:rawChannel。支持:[...ALLOWED_CHANNELS].join(', ')`);
process.exit(1);
}
const channel = rawChannel;
const morningCron = `mm mh * * *`;
const eveningCron = `em eh * * *`;
const sessionKey = `agent:main:channel:direct:userId`;
// 早报 cron(lang/topics 已嵌入命令,push 脚本无需再读文件)
const morningConfig = {
name: `newstoday-morning-userId`,
cronExpr: morningCron,
tz: defaultTz,
session: 'isolated',
sessionKey,
channel,
to: userId,
announce: true,
timeoutSeconds: 120,
message: `node path.join(__dirname, 'morning-push.js') pushArgs`
};
console.log(`__OPENCLAW_CRON_ADD__:JSON.stringify(morningConfig)`);
// 晚报 cron
const eveningConfig = {
name: `newstoday-evening-userId`,
cronExpr: eveningCron,
tz: defaultTz,
session: 'isolated',
sessionKey,
channel,
to: userId,
announce: true,
timeoutSeconds: 120,
message: `node path.join(__dirname, 'evening-push.js') pushArgs`
};
console.log(`__OPENCLAW_CRON_ADD__:JSON.stringify(eveningConfig)`);
// 突发新闻检测 cron(每2小时,08:00-22:00)
const breakingConfig = {
name: `newstoday-breaking-userId`,
cronExpr: '0 8,10,12,14,16,18,20,22 * * *',
tz: defaultTz,
session: 'isolated',
sessionKey,
channel,
to: userId,
announce: false,
timeoutSeconds: 60,
message: `node path.join(__dirname, 'breaking-alert.js') pushArgs`
};
console.log(`__OPENCLAW_CRON_ADD__:JSON.stringify(breakingConfig)`);
const morningDisplay = `String(mh).padStart(2,'0'):String(mm).padStart(2,'0')`;
const eveningDisplay = `String(eh).padStart(2,'0'):String(em).padStart(2,'0')`;
// 更新用户档案中的推送状态(合并,不覆盖话题偏好等字段)
const updated = {
...(profile || {}),
userId,
channel,
push: { enabled: true, morningTime: morningDisplay, eveningTime: eveningDisplay, enabledAt: new Date().toISOString() },
updatedAt: new Date().toISOString()
};
saveUser(userId, updated);
console.log(`
✅ 每日推送已开启
⏰ 早报:每天 morningDisplay(个性化10条要闻 + RSS)
🌙 晚报:每天 eveningDisplay(收官 + 明日预告)
🚨 突发:每2小时检测(08:00-22:00,有重大事件才提醒)
📡 渠道:channel
关闭推送:node push-toggle.js off userId`);
}
function disablePush(userId) {
userId = sanitizeId(userId, 'userId');
const user = loadUser(userId);
if (!user) {
console.log(`❌ 未找到用户 userId 的推送记录`);
return;
}
console.log(`__OPENCLAW_CRON_RM__:newstoday-morning-userId`);
console.log(`__OPENCLAW_CRON_RM__:newstoday-evening-userId`);
console.log(`__OPENCLAW_CRON_RM__:newstoday-breaking-userId`);
const updated = { ...user, push: { ...(user.push || {}), enabled: false, disabledAt: new Date().toISOString() }, updatedAt: new Date().toISOString() };
saveUser(userId, updated);
console.log(`✅ 推送已关闭`);
}
function showStatus(userId) {
userId = sanitizeId(userId, 'userId');
const user = loadUser(userId);
if (!user) {
console.log(`❌ 未找到用户 userId 的推送记录`);
return;
}
const push = user.push || {};
const topTopics = Object.entries(user.topics || {})
.filter(([,w]) => w >= 0.7).map(([t]) => t).join('、') || '默认';
console.log(`
📡 推送状态 — userId
━━━━━━━━━━━━━━━━━━━━━━━
状态:'❌ 已关闭'
早报:00'
晚报:00'
突发:每2小时检测(重大事件才提醒)
渠道:user.channel || 'telegram'
语言:'中文'
重点话题:topTopics
开启于:'未知'
━━━━━━━━━━━━━━━━━━━━━━━`);
}
module.exports = { enablePush, disablePush, showStatus };
if (require.main !== module) return;
const args = process.argv.slice(2);
const command = args[0];
const userId = args[1];
if (!command || !userId) {
console.log(`用法:
node push-toggle.js on <userId> [--morning 08:00] [--evening 20:00] [--channel telegram]
node push-toggle.js off <userId>
node push-toggle.js status <userId>`);
process.exit(1);
}
const opts = {};
const mi = args.indexOf('--morning');
if (mi !== -1) opts.morning = args[mi + 1];
const ei = args.indexOf('--evening');
if (ei !== -1) opts.evening = args[ei + 1];
const ci = args.indexOf('--channel');
if (ci !== -1) opts.channel = args[ci + 1];
switch (command) {
case 'on': enablePush(userId, opts); break;
case 'off': disablePush(userId); break;
case 'status': showStatus(userId); break;
default:
console.log(`❌ 未知命令: command`);
process.exit(1);
}
FILE:scripts/register.js
#!/usr/bin/env node
/**
* NewsToday — 用户注册 / 偏好设置
*
* 用法:
* node register.js <userId> [language] [topics] [channel]
*
* 参数:
* userId 必填,字母/数字/-/_,1-128 字符
* language 可选,zh(默认)或 en
* topics 可选,逗号分隔的偏好话题(如 科技,财经,国际)
* 可选值: 科技 财经 国际 社会 娱乐 体育
* channel 可选,telegram/feishu/slack/discord(默认 telegram)
*
* 示例:
* node register.js alice
* node register.js bob zh 科技,财经,国际
* node register.js carol en tech,finance,international telegram
*/
const fs = require('fs');
const path = require('path');
const USERS_DIR = path.join(__dirname, '../data/users');
const ALLOWED_TOPICS_ZH = ['科技', '财经', '国际', '社会', '娱乐', '体育'];
const ALLOWED_TOPICS_EN = ['tech', 'finance', 'international', 'society', 'entertainment', 'sports'];
const TOPIC_MAP = { tech: '科技', finance: '财经', international: '国际', society: '社会', entertainment: '娱乐', sports: '体育' };
const ALLOWED_CHANNELS = new Set(['telegram', 'feishu', 'slack', 'discord']);
function sanitizeId(value) {
if (typeof value !== 'string' || !/^[a-zA-Z0-9_-]{1,128}$/.test(value)) {
console.error('❌ 无效的 userId:只允许字母、数字、- 和 _,长度 1-128');
process.exit(1);
}
return value;
}
function safeUserPath(userId) {
const resolved = path.resolve(USERS_DIR, `userId.json`);
if (!resolved.startsWith(path.resolve(USERS_DIR) + path.sep)) {
console.error('❌ 非法路径');
process.exit(1);
}
return resolved;
}
function sanitizeLanguage(value) {
if (value !== 'zh' && value !== 'en') {
console.error('❌ 无效的语言:请使用 zh 或 en');
process.exit(1);
}
return value;
}
function sanitizeTopics(value, language) {
const allowed = language === 'en' ? ALLOWED_TOPICS_EN : ALLOWED_TOPICS_ZH;
const raw = value.split(',').map(t => t.trim()).filter(Boolean);
const weights = {};
for (const t of raw) {
const mapped = TOPIC_MAP[t] || t;
if (!ALLOWED_TOPICS_ZH.includes(mapped)) {
console.error(`❌ 无效的话题:t。可用值:[...ALLOWED_TOPICS_ZH, ...ALLOWED_TOPICS_EN].join(', ')`);
process.exit(1);
}
weights[mapped] = 1.0;
}
// 未指定的话题给默认权重 0.5
for (const t of ALLOWED_TOPICS_ZH) {
if (!(t in weights)) weights[t] = 0.5;
}
return weights;
}
const DEFAULT_TOPICS = { 科技: 0.8, 财经: 0.8, 国际: 0.7, 社会: 0.6, 娱乐: 0.3, 体育: 0.3 };
const args = process.argv.slice(2);
if (!args[0]) {
console.log(`用法:
node register.js <userId> [language] [topics] [channel]
参数:
userId 字母/数字/-/_,1-128 字符
language zh(默认)或 en
topics 逗号分隔偏好话题(如 科技,财经,国际)
channel telegram/feishu/slack/discord(默认 telegram)
示例:
node register.js alice
node register.js bob zh 科技,财经,国际
node register.js carol en tech,finance,international`);
process.exit(1);
}
const userId = sanitizeId(args[0]);
const language = args[1] ? sanitizeLanguage(args[1]) : 'zh';
const topics = args[2] ? sanitizeTopics(args[2], language) : { ...DEFAULT_TOPICS };
const rawCh = args[3] || 'telegram';
if (!ALLOWED_CHANNELS.has(rawCh)) {
console.error(`❌ 无效渠道:rawCh。支持:[...ALLOWED_CHANNELS].join(', ')`);
process.exit(1);
}
const channel = rawCh;
fs.mkdirSync(USERS_DIR, { recursive: true });
const filePath = safeUserPath(userId);
const now = new Date().toISOString();
let existing = null;
if (fs.existsSync(filePath)) {
try { existing = JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch (_) {}
}
const profile = {
userId,
language,
topics,
channel,
push: existing?.push || { enabled: false },
createdAt: existing?.createdAt || now,
updatedAt: now
};
fs.writeFileSync(filePath, JSON.stringify(profile, null, 2), 'utf8');
const topList = Object.entries(topics).filter(([,w]) => w >= 0.7).map(([t]) => t).join('、');
console.log(`
✅ 注册成功
👤 用户:userId
🌐 语言:'English'
📌 重点话题:topList || '默认'
📡 推送渠道:channel
下一步:
调整话题偏好:node scripts/preference.js set userId <话题> <权重0-1>
开启每日推送:node scripts/push-toggle.js on userId
获取今日早报:node scripts/morning-push.js userId`);
FILE:scripts/rss-fetch.js
#!/usr/bin/env node
/**
* NewsToday — RSS 聚合 prompt 生成器
* 纯 CLI 参数输入 → stdout 输出,无任何文件 I/O 或网络调用。
* 所有 URL 均为硬编码公开地址,不受外部输入影响。
*
* 用法:
* node rss-fetch.js [--lang zh|en] [--topics 科技,财经,国际]
*
* 示例:
* node rss-fetch.js --lang zh --topics 科技,财经
* node rss-fetch.js --lang en --topics tech,finance
* node rss-fetch.js # 使用默认值(中文,全部话题)
*/
// 全部 RSS 源均为硬编码公开地址,不受任何外部输入影响
const RSS_SOURCES_ZH = {
综合: [
{ name: '新浪新闻头条', url: 'https://rss.sina.com.cn/news/china/focus15.xml' },
{ name: '澎湃新闻', url: 'https://www.thepaper.cn/rss_promotion.jsp' },
],
科技: [
{ name: '36氪', url: 'https://36kr.com/feed' },
{ name: '少数派', url: 'https://sspai.com/feed' },
],
财经: [
{ name: '华尔街见闻', url: 'https://wallstreetcn.com/rss' },
],
国际: [
{ name: 'BBC中文', url: 'https://feeds.bbci.co.uk/zhongwen/simp/rss.xml' },
{ name: 'Reuters中文', url: 'https://cn.reuters.com/rssFeed/CNTopNews' },
],
};
const RSS_SOURCES_EN = {
general: [
{ name: 'Reuters', url: 'https://feeds.reuters.com/reuters/topNews' },
{ name: 'BBC News', url: 'http://feeds.bbci.co.uk/news/rss.xml' },
{ name: 'AP News', url: 'https://rsshub.app/apnews/topics/apf-topnews' },
],
tech: [
{ name: 'Hacker News', url: 'https://news.ycombinator.com/rss' },
{ name: 'TechCrunch', url: 'https://techcrunch.com/feed/' },
],
finance: [
{ name: 'Financial Times', url: 'https://www.ft.com/rss/home' },
{ name: 'Bloomberg', url: 'https://feeds.bloomberg.com/markets/news.rss' },
],
};
// 允许的话题白名单(防止任意字符串进入输出)
const ALLOWED_ZH = new Set(['综合', '科技', '财经', '国际', '社会', '娱乐', '体育']);
const ALLOWED_EN = new Set(['general', 'tech', 'finance', 'international', 'society', 'entertainment', 'sports']);
const EN_TO_ZH_KEY = { tech: '科技', finance: '财经', international: '国际', general: '综合' };
// --- 解析 CLI 参数(仅接受 --lang 和 --topics)---
const args = process.argv.slice(2);
const langIdx = args.indexOf('--lang');
const rawLang = langIdx !== -1 ? args[langIdx + 1] : null;
const lang = rawLang === 'en' ? 'en' : 'zh'; // 仅允许 zh/en,其余默认 zh
const topicsIdx = args.indexOf('--topics');
const rawTopics = topicsIdx !== -1 ? args[topicsIdx + 1] : null;
// 将 --topics 参数解析为白名单内的话题集合
const allowedSet = lang === 'en' ? ALLOWED_EN : ALLOWED_ZH;
let requestedTopics = null; // null = 全部
if (rawTopics) {
requestedTopics = new Set(
rawTopics.split(',')
.map(t => t.trim())
.filter(t => allowedSet.has(t))
);
}
// --- 从硬编码常量中选取 RSS 源 ---
const sources = lang === 'en' ? RSS_SOURCES_EN : RSS_SOURCES_ZH;
const generalKey = lang === 'en' ? 'general' : '综合';
const selected = [];
for (const [key, feeds] of Object.entries(sources)) {
if (key === generalKey) {
selected.push(...feeds); // 综合/general 始终包含
continue;
}
// 无过滤要求,或该话题在请求列表中
if (!requestedTopics) {
selected.push(...feeds);
} else {
const zhKey = lang === 'en' ? (EN_TO_ZH_KEY[key] ?? key) : key;
if (requestedTopics.has(key) || requestedTopics.has(zhKey)) {
selected.push(...feeds);
}
}
}
// --- 输出 prompt ---
const now = new Date();
const dateISO = `now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
if (lang === 'zh') {
const sourceList = selected.map(s => ` - s.name:s.url`).join('\n');
console.log(`请通过 WebFetch 获取以下 RSS 源的最新内容,汇总今日(dateISO)重要新闻。
RSS 源列表:
sourceList
处理步骤:
1. 依次 WebFetch 以上每个 URL,获取 XML 内容
2. 从每个源提取最新 3-5 条标题和摘要(优先今日内容)
3. 全部去重合并,按新闻价值排序,选取 10 条
4. 每条输出:标题、来源、发布时间、2 句中文摘要
注意:
- 若某 RSS URL 无法访问,跳过并继续其他源
- 去除广告、软文、纯娱乐八卦内容
- 有争议内容保持中立,不做立场判断`);
} else {
const sourceList = selected.map(s => ` - s.name: s.url`).join('\n');
console.log(`Please WebFetch the following RSS feeds and compile today's (dateISO) top news.
RSS sources:
sourceList
Steps:
1. WebFetch each URL above to get the XML content
2. Extract the latest 3–5 headlines and summaries from each (prefer today's content)
3. Deduplicate and merge all results, rank by news value, pick top 10
4. For each item output: headline, source, publish time, 2-sentence English summary
Notes:
- If a URL is unreachable, skip and continue with others
- Filter out ads, sponsored content, and pure celebrity gossip
- Present disputed topics neutrally without taking sides`);
}
MingLi is your all-in-one Chinese astrology and fortune-telling companion. It combines six ancient divination systems — BaZi (Four Pillars of Destiny), ZiWei...
---
name: yunshi
description: |
MingLi is your all-in-one Chinese astrology and fortune-telling companion. It combines six ancient divination systems — BaZi (Four Pillars of Destiny), ZiWei DouShu (Purple Star Astrology), QiMen DunJia, I Ching (Meihua Yishu & LiuYao), marriage compatibility analysis, and feng shui — into a single skill, with no external API required.
Every morning MingLi delivers a personalized daily fortune reading covering career, wealth, relationships, and health. Every evening it previews tomorrow's energy and lucky elements. Ask for a full BaZi chart, a ZiWei life-map reading, an I Ching divination, a marriage compatibility report, or a feng shui layout recommendation — MingLi handles them all. Built-in calculation algorithms produce accurate traditional charts instantly.
Supports Chinese and English output. Trigger: fortune telling, BaZi, daily horoscope, ZiWei, QiMen, I Ching, divination, feng shui, marriage compatibility, lucky color, yearly luck, 算命, 八字, 今日运势, 紫微斗数, 占卜, 合婚, 风水.
keywords: BaZi, Chinese astrology, daily horoscope, fortune telling, ZiWei DouShu, four pillars of destiny, I Ching, divination, feng shui, marriage compatibility, QiMen DunJia, daily fortune, horoscope push, lucky color, yearly luck, lucky elements, astrology, Chinese zodiac, fate analysis, life reading, 八字, 算命, 今日运势, 每日运程, 紫微斗数, 奇门遁甲, 梅花易数, 六爻, 占卜, 合婚, 风水, 流年, 大运, 命理, 四柱
metadata:
openclaw:
runtime:
node: ">=18"
install:
- kind: node
package: iztro
env:
- name: OPENCLAW_KNOWLEDGE_DIR
required: false
description: "Optional path to ZiWei pattern knowledge base (.md files). Defaults to ~/.openclaw/workspace/knowledge. Skill degrades gracefully if absent."
---
# 运势 (YunShi)
> 私人命理顾问 — 每日运程推送 · 八字紫微 · 占卜风水
## 何时使用
- 八字/四柱排盘、流年大运分析
- 今日/近期运势(事业/财运/感情/健康)
- 紫微斗数命盘
- 合婚、双方八字相配
- 占卦(梅花易数、六爻、奇门遁甲)
- 风水布局、财位、幸运颜色
- 用户说"算命""看运势""占卜""帮我占一卦"
---
## 🌐 多语言响应规则
1. **语言跟随**:用户语言 → 全程同语言回复
2. **专有术语保留中文**:柱名/星曜/卦名保持中文原字,括号内附译文
- 英文示例:Your Day Pillar is **甲子** (Jiǎ Zǐ — Wood Rat), indicating...
3. **脚本输出翻译**:脚本返回的中文结构由 Agent 解读后以用户语言呈现
4. **注册格式**:非中文用户使用 `Name | Gender(M/F) | BirthDate | BirthTime | BirthPlace`
5. **推送语言**:跟随档案 `language` 字段(默认 `zh`)
---
## 📖 功能列表
### 排盘
| 功能 | 命令 |
|------|------|
| 八字排盘(四柱/日主/用神/神煞) | `八字 1990-05-15 14:30` |
| 紫微斗数(命宫/十二宫/四化) | `紫微 1990-05-15 男` |
| 奇门遁甲 | `奇门 2026-03-24 15:00` |
| 择吉选日 | `择吉 2026-04 开业` |
### 分析
| 功能 | 命令 |
|------|------|
| 流年/大运/事业/财运/婚姻/健康 | `2026年运势` / `未来十年运势` / `财运好不好` |
| 合婚分析 | `合婚 张三 李四` |
| 风水分析 | `风水分析` |
### 占卜
| 功能 | 命令 |
|------|------|
| 梅花易数 | `梅花易数 3 5 2`(数字起卦)或留空时间起卦 |
| 六爻预测 | `六爻占卜` |
| 奇门占卜 | `奇门选时 明天15:00` |
### 每日运程(自动推送)
早晨 07:00 推送今日运势,晚间 20:00 推送明日预告。内容:综合指数、幸运颜色/方位/数字、今日宜忌、风险预警、吉时、每日一言。
| 推送命令 | 说明 |
|---------|------|
| `每日运势开` / `开启运势推送` | 开启 |
| `每日运势关` / `关闭运势推送` | 关闭 |
| `推送状态` | 查看当前状态 |
---
## 📦 环境依赖
- **Node.js >=18**(必须)
- `npm install` 安装 `iztro`(紫微斗数)和 `lunar-typescript`(农历转换)
- `OPENCLAW_KNOWLEDGE_DIR`:可选,紫微格局知识库,不存在时自动降级
- **推送渠道**:`telegram`/`feishu` 由 openclaw 运行时投递,skill 不调用任何渠道 API
- **新闻联动**:由 Agent 的 WebSearch 工具完成,无搜索能力时跳过
- **个人数据**:存储在 `data/profiles/<userId>.json`,含敏感信息,请确认访问权限
---
## 🛠️ 工具脚本
```bash
# 注册 / 档案
node scripts/register.js <userId> <姓名> <性别> <出生日期> <出生时间> [地点]
node scripts/profile.js show <userId>
node scripts/profile.js add <userId> spouse|child <姓名> <出生日期> <性别>
# 排盘
node scripts/ziwei.js <出生日期> <性别> [时辰]
node scripts/qimen.js [日期] [时辰]
node scripts/zhuanshi.js <YYYY-MM> <活动类型> [用户八字]
node scripts/fengshui.js [八字] [年份]
# 运程 / 合婚 / 占卜
node scripts/daily-fortune.js [日期]
node scripts/marriage.js <userId1> <userId2>
node scripts/meihua.js [数字1-3]
node scripts/liuyao.js [010203] [问题]
# 推送管理
node scripts/daily-push.js --dry-run # 模拟推送
node scripts/daily-push.js --test <userId> # 测试推送
node scripts/daily-push.js --list # 查看已开启用户
node scripts/push-toggle.js on|off|status <userId>
# 偏好追踪(每次提问后调用)
node scripts/preference-tracker.js record <userId> <topic> explicit_query|topic_drill
node scripts/preference-tracker.js weights|top <userId> [N]
# topic: 财运|事业|感情|健康|婚姻|子女|官司|出行|风水
```
---
## ⏰ Cron 推送配置
```bash
openclaw cron add "0 7 * * *" "cd ~/.openclaw/workspace/skills/yunshi && node scripts/daily-push.js"
openclaw cron list
openclaw cron delete <任务ID>
```
**子时算法**:`1` = 23:00-23:59 算次日(倪海厦派);`2` = 算当日(传统派)
---
## 📊 交叉验证权重
| 问题类型 | 八字 | 紫微 | 奇门 | 梅花 | 六爻 |
|----------|------|------|------|------|------|
| 终身命格 | 40% | 30% | - | - | - |
| 年度运势 | 40% | 30% | 20% | 10% | - |
| 事业决策 | 30% | 20% | 30% | - | 20% |
| 婚姻感情 | 40% | 30% | - | 10% | 20% |
| 当下问事 | - | - | 30% | 40% | 30% |
---
## ⚠️ 风险预警等级
🔴 严重(立即处理)· 🟡 注意(谨慎处理)· 🟢 提示(一般提醒)
类型:🚨 健康 · 💰 财务 · 💕 感情 · 💼 事业 · ⚖️ 法律
---
## 📁 数据文件
```
data/profiles/{userId}.json # 用户档案(姓名/出生/家庭成员八字)
scripts/ # register, ziwei, qimen, fengshui, profile,
# daily-fortune, marriage, meihua, liuyao,
# zhuanshi, daily-push, push-toggle, preference-tracker
```
---
## ⚠️ 注意事项
1. 用户数据与AI计算冲突时,以用户提供信息为准
2. 命理是参考,不是定数
3. 用户档案仅供个人使用,注意数据隐私
---
*Version: 1.1.0 · Updated: 2026-03-30*
FILE:README.md
# Yunshi — All-in-One Chinese Astrology Skill
> BaZi · ZiWei DouShu · QiMen DunJia · I Ching · Feng Shui · Marriage Compatibility — with daily fortune push. No API required.
[](https://clawhub.ai/skills/yunshi)
[](https://clawhub.ai/skills/yunshi)
[](https://openclaw.ai)
## What it does
Yunshi is the most comprehensive Chinese astrology skill on clawhub — covering all major traditional divination systems in one install. Built-in algorithms for BaZi and ZiWei DouShu calculations; no external API or subscription needed.
**BaZi (八字)** — Four Pillars birth chart, year/month/day/hour pillars, Da Yun major cycles
**ZiWei DouShu (紫微斗数)** — Purple Star Astrology full chart with palace interpretations
**QiMen DunJia (奇门遁甲)** — strategic divination for decision-making
**I Ching (易经)** — 64 hexagram readings with changing lines
**Feng Shui (风水)** — home/office layout analysis and recommendations
**Marriage compatibility (合婚)** — BaZi compatibility analysis for couples
**Daily fortune push** — daily luck ratings across career, wealth, love, health
## Installation
```bash
openclaw install yunshi
```
## Usage
```bash
# Daily fortune
openclaw run yunshi daily --birth "1990-05-15 08:30" --gender male
# BaZi full chart
openclaw run yunshi bazi --birth "1990-05-15 08:30"
# ZiWei DouShu
openclaw run yunshi ziwei --birth "1990-05-15 08:30" --gender male
# Marriage compatibility
openclaw run yunshi marriage --birth1 "1990-05-15" --birth2 "1992-08-22"
# I Ching divination
openclaw run yunshi meihua
# Feng shui advice
openclaw run yunshi fengshui
```
## Keywords
Chinese astrology · BaZi · Four Pillars · ZiWei DouShu · Purple Star Astrology · QiMen DunJia · I Ching · feng shui · fortune telling · daily horoscope · marriage compatibility · Chinese zodiac · 算命 · 八字 · 紫微斗数 · 奇门遁甲 · 今日运势 · 每日运程 · 合婚 · 风水 · 命理 · 占卜
---
Built for [OpenClaw](https://openclaw.ai) · Published on [clawhub.ai](https://clawhub.ai/skills/yunshi)
FILE:_meta.json
{
"ownerId": "kn79bebfnwg15sb0g7cj5z5nyd83gxh0",
"slug": "yunshi",
"version": "1.1.0",
"publishedAt": 1774351732089,
"runtime": {
"node": ">=18"
},
"install": [
{ "kind": "node", "package": "iztro" }
]
}
FILE:data/profiles/8597078097.json
{
"userId": "8597078097",
"name": "Jia",
"language": "zh",
"profile": {
"birthDate": "1978-03-26",
"birthTime": "23:30",
"birthPlace": "北京",
"gender": "男",
"timezone": "Asia/Shanghai",
"trueSolarTime": "23:16",
"trueSolarDate": "1978-03-26",
"solarCorrectionMin": -14,
"solarCorrectionCity": "北京"
},
"bazi": {
"year": "戊午",
"month": "乙卯",
"day": "戊子",
"hour": "壬子",
"dayStem": "戊",
"zodiac": "马",
"sect": "晚子时",
"source": "verified",
"analysis": {
"调候用神": {
"有调候": true,
"主用神": [
"丙",
"甲"
],
"忌神": "壬",
"优先级": "丙先甲后",
"已有用神": [],
"缺用神": [
"丙",
"甲"
],
"调候状态": "调候皆缺",
"说明": "卯月木旺,丙甲并用"
},
"日主强弱": {
"日主": "戊",
"五行": "土",
"强弱等级": "弱",
"总分": 93,
"月令得分": 40,
"比劫得分": 8,
"通根得分": 35,
"印绶得分": 10,
"旺衰": "戊身弱,宜取印比生扶",
"用神方向": "印比扶身"
},
"用神格局": {
"月令": "卯",
"透干用神": "乙",
"格局": "正官",
"格局类型": "正格",
"善用神": false,
"说明": "卯月令,乙透干,取正官格"
},
"阴阳刚柔": {
"日主阴阳": "阳",
"刚柔": "刚",
"盖头截脚": [
"戊子盖头(土克水)"
],
"天干地支关系": {
"年柱": "无直接关系",
"月柱": "比和",
"日柱": "相克",
"时柱": "比和"
},
"说明": "戊为阳干,性刚,有戊子盖头(土克水)"
},
"十神": {
"天干十神": {
"年干": "戊(比肩)",
"月干": "乙(正官)",
"时干": "壬(偏财)"
},
"地支十神": {
"年支": "午(丁正印/己劫财)",
"月支": "卯(乙正官)",
"日支": "子(癸正财/壬偏财)",
"时支": "子(癸正财/壬偏财)"
}
}
}
},
"ziwei": {
"mingGong": "",
"mingZhu": "",
"source": "pending"
},
"family": {
"spouse": {
"name": "配偶",
"profile": {
"birthDate": "待录入",
"birthTime": "待录入",
"birthPlace": "",
"gender": "女",
"lunarBirth": ""
},
"bazi": {
"year": "",
"month": "",
"day": "",
"hour": "",
"source": "pending"
}
},
"father": {
"name": "父亲",
"profile": {
"birthDate": "待录入",
"birthTime": "待录入",
"birthPlace": "",
"gender": "男"
},
"bazi": {
"year": "",
"month": "",
"day": "",
"hour": "",
"source": "pending"
}
},
"mother": {
"name": "母亲",
"profile": {
"birthDate": "待录入",
"birthTime": "待录入",
"birthPlace": "",
"gender": "女"
},
"bazi": {
"year": "",
"month": "",
"day": "",
"hour": "",
"source": "pending"
}
},
"children": []
},
"preferences": {
"pushMorning": true,
"pushEvening": false,
"morningTime": "07:00",
"eveningTime": "20:00",
"channels": [
"telegram"
],
"focusAreas": [
"事业",
"财运",
"健康"
],
"riskTolerance": "中等"
},
"settings": {
"defaultSect": 1,
"lunarCalendar": true,
"notifications": {
"dailyFortune": true,
"riskAlert": true,
"weeklySummary": false
}
},
"createdAt": "2026-04-24",
"updatedAt": "2026-04-24"
}
FILE:data/profiles/template.json
{
"userId": "{{userId}}",
"name": "{{姓名}}",
"profile": {
"birthDate": "{{出生日期,如 1990-01-01}}",
"birthTime": "{{出生时间,如 14:30}}",
"birthPlace": "{{出生地,如 上海}}",
"gender": "{{男/女}}",
"lunarBirth": "",
"timezone": "Asia/Shanghai"
},
"bazi": {
"year": "",
"month": "",
"day": "",
"hour": "",
"dayStem": "",
"zodiac": "",
"dayEmpty": [],
"yearEmpty": [],
"sect": "晚子时",
"source": "pending"
},
"ziwei": {
"mingGong": "",
"mingZhu": "",
"patterns": [],
"source": "pending"
},
"language": "zh",
"preferences": {
"pushMorning": true,
"pushEvening": false,
"morningTime": "07:00",
"eveningTime": "20:00",
"channels": ["telegram"],
"focusAreas": ["事业", "财运", "健康"],
"riskTolerance": "中等"
},
"family": {
"spouse": {
"name": "",
"profile": {
"birthDate": "待录入",
"birthTime": "待录入",
"birthPlace": "",
"gender": "",
"lunarBirth": ""
},
"bazi": {
"year": "",
"month": "",
"day": "",
"hour": "",
"source": "pending"
}
},
"father": {
"name": "父亲",
"profile": {
"birthDate": "待录入",
"birthTime": "待录入",
"birthPlace": "",
"gender": "男"
},
"bazi": {
"year": "",
"month": "",
"day": "",
"hour": "",
"source": "pending"
}
},
"mother": {
"name": "母亲",
"profile": {
"birthDate": "待录入",
"birthTime": "待录入",
"birthPlace": "",
"gender": "女"
},
"bazi": {
"year": "",
"month": "",
"day": "",
"hour": "",
"source": "pending"
}
},
"children": []
},
"settings": {
"defaultSect": 1,
"lunarCalendar": true,
"notifications": {
"dailyFortune": true,
"riskAlert": true,
"weeklySummary": false
}
},
"interactionLog": [],
"lastPushDate": "",
"createdAt": "{{注册日期}}",
"updatedAt": "{{更新日期}}"
}
FILE:data/push-log.json
{
"runs": [
{
"date": "2026-03-24",
"timestamp": "2026-03-24T18:31:11.329Z",
"dryRun": true,
"results": [
{
"userId": "888888",
"name": "测试",
"status": "error",
"error": "zhiElement is not defined"
},
{
"userId": "999888",
"name": "测试",
"status": "error",
"error": "zhiElement is not defined"
},
{
"userId": "test001",
"name": "测试",
"status": "error",
"error": "zhiElement is not defined"
},
{
"userId": "test002",
"name": "测试",
"status": "error",
"error": "zhiElement is not defined"
},
{
"userId": "test003",
"name": "测试",
"status": "error",
"error": "zhiElement is not defined"
}
]
},
{
"date": "2026-03-24",
"timestamp": "2026-03-24T18:31:17.671Z",
"dryRun": true,
"results": [
{
"userId": "888888",
"name": "测试",
"status": "dry-run"
},
{
"userId": "999888",
"name": "测试",
"status": "dry-run"
},
{
"userId": "test001",
"name": "测试",
"status": "dry-run"
},
{
"userId": "test002",
"name": "测试",
"status": "dry-run"
},
{
"userId": "test003",
"name": "测试",
"status": "dry-run"
}
]
},
{
"date": "2026-03-24",
"timestamp": "2026-03-24T18:31:46.352Z",
"dryRun": true,
"results": [
{
"userId": "888888",
"name": "测试",
"status": "dry-run"
},
{
"userId": "999888",
"name": "测试",
"status": "dry-run"
},
{
"userId": "pushtest",
"name": "推送测试员",
"status": "dry-run"
},
{
"userId": "test001",
"name": "测试",
"status": "dry-run"
},
{
"userId": "test002",
"name": "测试",
"status": "dry-run"
},
{
"userId": "test003",
"name": "测试",
"status": "dry-run"
}
]
},
{
"date": "2026-03-30",
"timestamp": "2026-03-30T09:20:09.315Z",
"dryRun": true,
"results": [
{
"userId": "test001",
"name": "张三",
"status": "dry-run"
},
{
"userId": "test002",
"name": "测试",
"status": "dry-run"
},
{
"userId": "test003",
"name": "测试",
"status": "dry-run"
},
{
"userId": "user001",
"name": "测试用户",
"status": "dry-run"
}
]
}
]
}
FILE:docs/注册流程.md
# 用户注册流程
## 概述
新用户通过与机器人对话,完成注册并开始使用"命理私人导师"服务。
---
## 注册流程
```
用户 → 发起注册 → 引导录入 → 确认信息 → 完成
```
---
## 详细步骤
### 第一步:发起注册
```
用户: 你好,我想注册
助手: 欢迎使用命理私人导师!我是你的私人命理顾问。
为了给你提供准确的运势分析,请告诉我以下信息:
📝 基本信息
1. 姓名:__________
2. 性别:男/女
3. 出生日期:YYYY-MM-DD
4. 出生时间:HH:MM(精确到分钟)
5. 出生地点:__________
请依次告诉我~
```
---
### 第二步:录入信息
#### 2.1 必填信息
| 信息 | 说明 | 验证 |
|------|------|------|
| 姓名 | 用户称呼 | 非空 |
| 性别 | 男/女 | 男或女 |
| 出生日期 | 阳历生日 | 有效日期 |
| 出生时间 | 24小时制 | HH:MM格式 |
| 出生地点 | 省市即可 | 非空 |
#### 2.2 引导话术
```
📅 出生日期
请告诉我你的阳历出生日期,例如:1990-05-15
⏰ 出生时间
请告诉我精确的出生时间,例如:14:30
注意:如果是23:00之后出生,请务必如实告知(会影响八字准确性)
📍 出生地点
请告诉我出生省份/城市,例如:上海
```
---
### 第三步:确认信息
```
📋 信息确认
请确认以下信息是否正确:
姓名:XXX
性别:X
出生日期:XXXX年XX月XX日
出生时间:XX:XX
出生地点:XXXX
子时计算:晚子时(23:00后算次日)
如信息正确,请回复"确认"
如有修改,请告诉我需要修改的内容
```
---
### 第四步:八字计算
用户确认后,系统自动计算八字:
```
🧮 正在为您计算八字...
✅ 八字排盘完成!
┌─────────────────┐
│ 年柱:戊午 │
│ 月柱:乙卯 │
│ 日柱:戊子 │
│ 时柱:壬子 │
├─────────────────┤
│ 日主:戊土 │
│ 生肖:马 │
│ 日空:午未 │
│ 年空:子丑 │
└─────────────────┘
是否需要详细解读?
回复"解读"获取完整命理分析
回复"跳过"稍后再说
```
---
### 第五步:家庭成员(可选)
```
👪 家庭成员(可选)
您可以添加家庭成员的档案,用于合盘分析:
• 配偶 - 合婚分析
• 父母 - 家族运势
• 子女 - 子女缘分
示例:
- "添加配偶:1990-05-15 女"
- "添加父亲:1950-03-15"
- "添加儿子:2020-01-01 男"
回复"跳过"继续
```
---
### 第六步:推送设置
```
⏰ 推送设置
您希望收到每日运程推送吗?
📬 推送时间(可选1-2个):
• 早晨版 - 07:00 发送当日完整运势
• 傍晚版 - 20:00 发送次日预告
📱 推送渠道:
• Telegram ✅
• 飞书 ✅
• WhatsApp ✅
示例回复:
- "只要早晨版"
- "早晨+傍晚,发送到飞书"
```
---
### 第七步:完成注册
```
🎉 注册完成!
欢迎,XXX!你的私人命理顾问已就位。
📊 你的命盘
八字:XX柱 XX柱 XX柱 XX柱
日主:XX
生肖:XX
🔔 每日推送
已开启:早晨07:00
💬 你可以这样问我:
• "今日运势如何"
• "算算2026年的事业"
• "帮我占一卦"
• "我和配偶的八字合吗"
祝你好运!🍀
```
---
## 快速注册命令
用户也可以用快捷命令一次完成注册:
```
/注册 姓名|性别|出生日期|出生时间|出生地点
示例:
/注册 张三|男|1990-05-15|14:30|上海
```
---
## 注册字段映射
| 用户输入 | 保存字段 | 说明 |
|----------|----------|------|
| 姓名 | name | 称呼 |
| 性别 | profile.gender | 男/女 |
| 出生日期 | profile.birthDate | YYYY-MM-DD |
| 出生时间 | profile.birthTime | HH:MM |
| 出生地点 | profile.birthPlace | 省市 |
| 子时 | bazi.sect | 晚子时 |
---
## 错误处理
### 信息不完整
```
抱歉,信息不完整,请补充:
缺少:出生日期
请告诉我你的出生日期,例如:1990-05-15
```
### 日期格式错误
```
日期格式有误,请重新输入:
正确格式:YYYY-MM-DD
示例:1990-05-15
请重新告诉我出生日期~
```
### 时间模糊
```
您只说了"早上"出生,请尽量精确:
• 上午6点前 → 建议问家长确认
• 无法确认 → 我会按早子时(当日)计算
请告诉我更精确的时间,或者回复"不确定"
```
---
## 隐私说明
```
🔒 隐私说明
您的个人信息仅用于命理分析:
• 出生信息用于八字/紫微排盘
• 不会被分享给第三方
• 可随时要求删除档案
继续即表示同意以上条款。
```
---
*Version: 1.1.0*
*Created: 2026-03-24*
*Updated: 2026-03-24*
FILE:package-lock.json
{
"name": "yunshi",
"version": "1.0.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "yunshi",
"version": "1.0.9",
"license": "MIT",
"dependencies": {
"iztro": "^2.5.8",
"lunar-typescript": "^1.8.6"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/dayjs": {
"version": "1.11.20",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz",
"integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==",
"license": "MIT"
},
"node_modules/i18next": {
"version": "23.16.8",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.8.tgz",
"integrity": "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
{
"type": "individual",
"url": "https://locize.com/i18next.html"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/iztro": {
"version": "2.5.8",
"resolved": "https://registry.npmjs.org/iztro/-/iztro-2.5.8.tgz",
"integrity": "sha512-kgyyvxdSEvgJxi6zvHpvzGbXZLGXCdhTHYK2Pe/sRdBIQ7RfCArvupmg2ChUMQCSQGomW7XCI0gWwUuKJwPENg==",
"license": "MIT",
"dependencies": {
"dayjs": "^1.11.10",
"i18next": "^23.5.1",
"lunar-lite": "^0.2.8",
"lunar-typescript": "^1.7.8"
}
},
"node_modules/lunar-lite": {
"version": "0.2.8",
"resolved": "https://registry.npmjs.org/lunar-lite/-/lunar-lite-0.2.8.tgz",
"integrity": "sha512-Y4tba4RaIFI0ikImJhgoEsyqtDE64lJIM3yFwRX01dbmagCDq7rNmpDQFrSFFy4WXeuywdRVFpIBoT1GGCEizw==",
"license": "MIT",
"dependencies": {
"lunar-typescript": "^1.8.6"
}
},
"node_modules/lunar-typescript": {
"version": "1.8.6",
"resolved": "https://registry.npmjs.org/lunar-typescript/-/lunar-typescript-1.8.6.tgz",
"integrity": "sha512-5Eo4T/cnuXfrgO4k5LCpOGHIUOuz5hCF/IfNv0T29WY2shR36Hiz+ecN9WjnUuxUKhql9gbOkPaQoqLFKtPRNA==",
"license": "MIT"
}
}
}
FILE:package.json
{
"name": "yunshi",
"version": "1.2.2",
"description": "All-in-one Chinese astrology — BaZi, ZiWei DouShu, QiMen DunJia, I Ching, feng shui, marriage compatibility, and daily fortune push. No API required.",
"keywords": [
"BaZi",
"Chinese astrology",
"daily horoscope",
"fortune telling",
"ZiWei DouShu",
"four pillars",
"I Ching",
"divination",
"feng shui",
"marriage compatibility",
"QiMen DunJia",
"daily fortune",
"lucky color",
"yearly luck",
"Chinese zodiac",
"astrology",
"fate reading",
"算命",
"八字",
"今日运势",
"每日运程",
"紫微斗数",
"奇门遁甲",
"梅花易数",
"六爻",
"占卜",
"合婚",
"风水",
"命理",
"流年",
"大运"
],
"author": "MingLi Mentor Team",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
},
"scripts": {
"daily": "node scripts/daily-fortune.js",
"register": "node scripts/register.js",
"ziwei": "node scripts/ziwei.js",
"marriage": "node scripts/marriage.js",
"meihua": "node scripts/meihua.js",
"liuyao": "node scripts/liuyao.js",
"qimen": "node scripts/qimen.js",
"fengshui": "node scripts/fengshui.js",
"zhuanshi": "node scripts/zhuanshi.js",
"profile": "node scripts/profile.js"
},
"directories": {
"scripts": "scripts",
"data": "data",
"docs": "docs"
},
"dependencies": {
"iztro": "^2.5.8"
}
}
FILE:scripts/bazi-analysis.js
#!/usr/bin/env node
/**
* 八字深度分析模块
* 基于《穷通宝鉴》《子平真诠》《滴天髓》三大经典
* 实现:调候用神、日主强弱、用神格局、十神、阴阳刚柔
*/
// ─────────────────────────────────────────────
// 基础数据表
// ─────────────────────────────────────────────
const STEM_ELEMENT = {
'甲': '木', '乙': '木', '丙': '火', '丁': '火', '戊': '土',
'己': '土', '庚': '金', '辛': '金', '壬': '水', '癸': '水'
};
const STEM_YINYANG = {
'甲': '阳', '乙': '阴', '丙': '阳', '丁': '阴', '戊': '阳',
'己': '阴', '庚': '阳', '辛': '阴', '壬': '阳', '癸': '阴'
};
const BRANCH_ELEMENT = {
'子': '水', '丑': '土', '寅': '木', '卯': '木',
'辰': '土', '巳': '火', '午': '火', '未': '土',
'申': '金', '酉': '金', '戌': '土', '亥': '水'
};
const ELEMENT_PRODUCES = { '木': '火', '火': '土', '土': '金', '金': '水', '水': '木' };
const ELEMENT_RESTRAIN = { '木': '土', '火': '金', '土': '水', '金': '木', '水': '火' };
const ELEMENT_GENERATED_BY = { '木': '水', '火': '木', '土': '火', '金': '土', '水': '金' };
// 地支藏干(主气/中气/余气)
const BRANCH_HIDDEN = {
'子': { 主气: '癸', 中气: '壬', 余气: null },
'丑': { 主气: '己', 中气: '辛', 余气: '癸' },
'寅': { 主气: '甲', 中气: '丙', 余气: '戊' },
'卯': { 主气: '乙', 中气: null, 余气: null },
'辰': { 主气: '戊', 中气: '乙', 余气: '癸' },
'巳': { 主气: '丙', 中气: '庚', 余气: '戊' },
'午': { 主气: '丁', 中气: '己', 余气: null },
'未': { 主气: '己', 中气: '丁', 余气: '乙' },
'申': { 主气: '庚', 中气: '壬', 余气: '戊' },
'酉': { 主气: '辛', 中气: null, 余气: null },
'戌': { 主气: '戊', 中气: '辛', 余气: '丁' },
'亥': { 主气: '壬', 中气: '甲', 余气: '戊' },
};
// ─────────────────────────────────────────────
// 十神表(以日主为基准)
// ─────────────────────────────────────────────
const TEN_GODS_TABLE = {
'甲': { '甲': '比肩', '乙': '劫财', '丙': '食神', '丁': '伤官', '戊': '偏财', '己': '正财', '庚': '七杀', '辛': '正官', '壬': '偏印', '癸': '正印' },
'乙': { '甲': '劫财', '乙': '比肩', '丙': '伤官', '丁': '食神', '戊': '正财', '己': '偏财', '庚': '正官', '辛': '七杀', '壬': '正印', '癸': '偏印' },
'丙': { '甲': '偏印', '乙': '正印', '丙': '比肩', '丁': '劫财', '戊': '食神', '己': '伤官', '庚': '偏财', '辛': '正财', '壬': '七杀', '癸': '正官' },
'丁': { '甲': '正印', '乙': '偏印', '丙': '劫财', '丁': '比肩', '戊': '伤官', '己': '食神', '庚': '正财', '辛': '偏财', '壬': '正官', '癸': '七杀' },
'戊': { '甲': '七杀', '乙': '正官', '丙': '偏印', '丁': '正印', '戊': '比肩', '己': '劫财', '庚': '食神', '辛': '伤官', '壬': '偏财', '癸': '正财' },
'己': { '甲': '正官', '乙': '七杀', '丙': '正印', '丁': '偏印', '戊': '劫财', '己': '比肩', '庚': '伤官', '辛': '食神', '壬': '正财', '癸': '偏财' },
'庚': { '甲': '偏财', '乙': '正财', '丙': '七杀', '丁': '正官', '戊': '偏印', '己': '正印', '庚': '比肩', '辛': '劫财', '壬': '食神', '癸': '伤官' },
'辛': { '甲': '正财', '乙': '偏财', '丙': '正官', '丁': '七杀', '戊': '正印', '己': '偏印', '庚': '劫财', '辛': '比肩', '壬': '伤官', '癸': '食神' },
'壬': { '甲': '食神', '乙': '伤官', '丙': '偏财', '丁': '正财', '戊': '七杀', '己': '正官', '庚': '偏印', '辛': '正印', '壬': '比肩', '癸': '劫财' },
'癸': { '甲': '伤官', '乙': '食神', '丙': '正财', '丁': '偏财', '戊': '正官', '己': '七杀', '庚': '正印', '辛': '偏印', '壬': '劫财', '癸': '比肩' },
};
// ─────────────────────────────────────────────
// 穷通宝鉴调候用神表(120条)
// ─────────────────────────────────────────────
const TIAO_HOU_TABLE = {
// 甲木
'甲寅': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '庚', 说明: '寅月木寒,丙火为君,癸水为佐' },
'甲卯': { 主用神: ['丁', '丙'], 优先级: '丁先', 忌神: '庚', 说明: '卯月木旺,丁火泄秀,忌金' },
'甲辰': { 主用神: ['庚', '丁'], 优先级: '庚先丁后', 忌神: '癸', 说明: '辰月土旺,先庚后丁' },
'甲巳': { 主用神: ['癸', '丁'], 优先级: '癸先丁后', 忌神: '庚', 说明: '巳月火旺,癸水调候' },
'甲午': { 主用神: ['癸', '壬'], 优先级: '癸先', 忌神: '丁', 说明: '午月火旺,水为调候' },
'甲未': { 主用神: ['丁', '庚'], 优先级: '丁先', 忌神: '癸', 说明: '未月土月,用丁庚' },
'甲申': { 主用神: ['庚', '丁'], 优先级: '庚先丁后', 忌神: '癸', 说明: '申月金旺,庚劈甲引丁' },
'甲酉': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '庚', 说明: '酉月金旺,丁火制金' },
'甲戌': { 主用神: ['庚', '丁'], 优先级: '庚先丁后', 忌神: '癸', 说明: '戌月金土,用庚丁' },
'甲亥': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '庚', 说明: '亥月水冷,丙火调候' },
'甲子': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '庚', 说明: '子月水寒,丙戊并用' },
'甲丑': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '辛', 说明: '丑月寒湿,丁火暖局' },
// 乙木
'乙寅': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '辛', 说明: '寅月木寒,丙癸双清' },
'乙卯': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '辛', 说明: '卯月木旺,丙癸调候' },
'乙辰': { 主用神: ['癸', '丙'], 优先级: '癸先丙后', 忌神: '乙', 说明: '辰月湿土,癸水润乙' },
'乙巳': { 主用神: ['癸', '丙'], 优先级: '癸先丙后', 忌神: '辛', 说明: '巳月火旺,癸水调候' },
'乙午': { 主用神: ['癸', '壬'], 优先级: '癸先', 忌神: '丙', 说明: '午月火旺,癸水制火' },
'乙未': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '乙', 说明: '未月土月,丙癸并用' },
'乙申': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '辛', 说明: '申月金旺,丙癸并用' },
'乙酉': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '辛', 说明: '酉月金旺,丙火制金' },
'乙戌': { 主用神: ['癸', '辛'], 优先级: '癸先辛后', 忌神: '丙', 说明: '戌月燥土,癸水润局' },
'乙亥': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '辛', 说明: '亥月水冷,丙戊暖局' },
'乙子': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '辛', 说明: '子月水寒,丙戊调候' },
'乙丑': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '辛', 说明: '丑月寒湿,丙丁暖局' },
// 丙火
'丙寅': { 主用神: ['壬', '庚'], 优先级: '壬先庚后', 忌神: '癸', 说明: '寅月木火,壬水通月令' },
'丙卯': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '甲', 说明: '卯月木旺,壬癸制火' },
'丙辰': { 主用神: ['壬', '庚'], 优先级: '壬先庚后', 忌神: '戊', 说明: '辰月湿土,壬水通根' },
'丙巳': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '戊', 说明: '巳月火旺,壬水为用' },
'丙午': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丙', 说明: '午月火旺极,壬水调候' },
'丙未': { 主用神: ['壬', '庚'], 优先级: '壬先庚后', 忌神: '己', 说明: '未月土月,壬庚并用' },
'丙申': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '庚', 说明: '申月金水,壬水通根' },
'丙酉': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '辛', 说明: '酉月金旺,壬癸制火' },
'丙戌': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '丁', 说明: '戌月土金,壬甲并用' },
'丙亥': { 主用神: ['甲', '壬'], 优先级: '甲先壬后', 忌神: '辛', 说明: '亥月水冷,甲木生火' },
'丙子': { 主用神: ['甲', '壬'], 优先级: '甲先壬后', 忌神: '癸', 说明: '子月水旺,甲木生丙' },
'丙丑': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '己', 说明: '丑月寒湿,壬甲暖局' },
// 丁火
'丁寅': { 主用神: ['甲', '丙'], 优先级: '甲先丙后', 忌神: '壬', 说明: '寅月木旺,甲木生丁' },
'丁卯': { 主用神: ['甲', '丙'], 优先级: '甲先丙后', 忌神: '癸', 说明: '卯月木旺,甲丙生丁' },
'丁辰': { 主用神: ['甲', '庚'], 优先级: '甲先庚后', 忌神: '癸', 说明: '辰月土月,甲庚并用' },
'丁巳': { 主用神: ['甲', '庚'], 优先级: '甲先庚后', 忌神: '戊', 说明: '巳月火旺,甲庚制火' },
'丁午': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丁', 说明: '午月火旺,壬癸调候' },
'丁未': { 主用神: ['甲', '庚'], 优先级: '甲先庚后', 忌神: '丁', 说明: '未月土月,甲庚并用' },
'丁申': { 主用神: ['甲', '丙'], 优先级: '甲先丙后', 忌神: '壬', 说明: '申月金旺,甲丙生丁' },
'丁酉': { 主用神: ['甲', '丙'], 优先级: '甲先丙后', 忌神: '癸', 说明: '酉月金旺,甲丙生丁' },
'丁戌': { 主用神: ['甲', '壬'], 优先级: '甲先壬后', 忌神: '丁', 说明: '戌月燥土,壬水润局' },
'丁亥': { 主用神: ['甲', '庚'], 优先级: '甲先庚后', 忌神: '壬', 说明: '亥月水冷,甲庚暖局' },
'丁子': { 主用神: ['甲', '庚'], 优先级: '甲先庚后', 忌神: '癸', 说明: '子月水寒,甲庚暖局' },
'丁丑': { 主用神: ['甲', '庚'], 优先级: '甲先庚后', 忌神: '癸', 说明: '丑月寒湿,甲庚暖局' },
// 戊土
'戊寅': { 主用神: ['丙', '甲'], 优先级: '丙先甲后', 忌神: '壬', 说明: '寅月木旺,丙甲并用' },
'戊卯': { 主用神: ['丙', '甲'], 优先级: '丙先甲后', 忌神: '壬', 说明: '卯月木旺,丙甲并用' },
'戊辰': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '甲', 说明: '辰月湿土,丙癸调候' },
'戊巳': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '甲', 说明: '巳月火旺,丙癸并用' },
'戊午': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丙', 说明: '午月火旺极,壬癸调候' },
'戊未': { 主用神: ['癸', '丙'], 优先级: '癸先丙后', 忌神: '己', 说明: '未月土月,癸水润局' },
'戊申': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '壬', 说明: '申月金旺,丙丁暖局' },
'戊酉': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '癸', 说明: '酉月金旺,丙丁暖局' },
'戊戌': { 主用神: ['甲', '丁'], 优先级: '甲先丁后', 忌神: '壬', 说明: '戌月燥土,甲丁调候' },
'戊亥': { 主用神: ['丙', '甲'], 优先级: '丙先甲后', 忌神: '壬', 说明: '亥月水冷,丙甲暖局' },
'戊子': { 主用神: ['丙', '甲'], 优先级: '丙先甲后', 忌神: '壬', 说明: '子月水寒,丙甲暖局' },
'戊丑': { 主用神: ['丙', '甲'], 优先级: '丙先甲后', 忌神: '癸', 说明: '丑月寒湿,丙甲暖局' },
// 己土
'己寅': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '甲', 说明: '寅月木旺,丙癸暖局' },
'己卯': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '甲', 说明: '卯月木旺,丙癸暖局' },
'己辰': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '乙', 说明: '辰月湿土,丙癸调候' },
'己巳': { 主用神: ['癸', '丙'], 优先级: '癸先丙后', 忌神: '甲', 说明: '巳月火旺,癸水润局' },
'己午': { 主用神: ['癸', '壬'], 优先级: '癸先壬后', 忌神: '丙', 说明: '午月火旺,癸壬调候' },
'己未': { 主用神: ['癸', '丙'], 优先级: '癸先丙后', 忌神: '己', 说明: '未月土月,癸水润局' },
'己申': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '壬', 说明: '申月金旺,丙癸暖局' },
'己酉': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '辛', 说明: '酉月金旺,丙癸暖局' },
'己戌': { 主用神: ['癸', '辛'], 优先级: '癸先辛后', 忌神: '丙', 说明: '戌月燥土,癸水润燥' },
'己亥': { 主用神: ['丙', '辛'], 优先级: '丙先辛后', 忌神: '壬', 说明: '亥月水冷,丙辛暖局' },
'己子': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '癸', 说明: '子月水寒,丙丁暖局' },
'己丑': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '癸', 说明: '丑月寒湿,丙丁暖局' },
// 庚金
'庚寅': { 主用神: ['丁', '甲'], 优先级: '丁先甲后', 忌神: '壬', 说明: '寅月木旺,丁甲并用' },
'庚卯': { 主用神: ['丁', '甲'], 优先级: '丁先甲后', 忌神: '癸', 说明: '卯月木旺,丁甲制木' },
'庚辰': { 主用神: ['丁', '甲'], 优先级: '丁先甲后', 忌神: '壬', 说明: '辰月土月,丁甲并用' },
'庚巳': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丙', 说明: '巳月火旺,壬癸制火' },
'庚午': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丁', 说明: '午月火旺,壬癸调候' },
'庚未': { 主用神: ['丁', '甲'], 优先级: '丁先甲后', 忌神: '己', 说明: '未月土月,丁甲暖局' },
'庚申': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '壬', 说明: '申月金旺,丁丙制金' },
'庚酉': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '壬', 说明: '酉月金旺,丁丙制金' },
'庚戌': { 主用神: ['丁', '甲'], 优先级: '丁先甲后', 忌神: '辛', 说明: '戌月燥土,丁甲调候' },
'庚亥': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '壬', 说明: '亥月水冷,丁丙暖局' },
'庚子': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '癸', 说明: '子月水寒,丁丙暖局' },
'庚丑': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '癸', 说明: '丑月寒湿,丙丁暖局' },
// 辛金
'辛寅': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '丙', 说明: '寅月木旺,壬水化木' },
'辛卯': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '丙', 说明: '卯月木旺,壬甲并用' },
'辛辰': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '乙', 说明: '辰月土月,壬甲暖局' },
'辛巳': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丙', 说明: '巳月火旺,壬癸制火' },
'辛午': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丁', 说明: '午月火旺,壬癸调候' },
'辛未': { 主用神: ['丁', '甲'], 优先级: '丁先甲后', 忌神: '己', 说明: '未月土月,丁甲暖局' },
'辛申': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '庚', 说明: '申月金旺,壬水洗金' },
'辛酉': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '庚', 说明: '酉月金旺,壬水洗金' },
'辛戌': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '辛', 说明: '戌月燥土,丁丙暖局' },
'辛亥': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '壬', 说明: '亥月水冷,丙戊暖局' },
'辛子': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '丙', 说明: '子月水寒,壬甲暖局' },
'辛丑': { 主用神: ['壬', '庚'], 优先级: '壬先庚后', 忌神: '己', 说明: '丑月寒湿,壬庚暖局' },
// 壬水
'壬寅': { 主用神: ['庚', '戊'], 优先级: '庚先戊后', 忌神: '丙', 说明: '寅月木旺,庚金生水' },
'壬卯': { 主用神: ['庚', '辛'], 优先级: '庚先辛后', 忌神: '丙', 说明: '卯月木旺,庚辛生水' },
'壬辰': { 主用神: ['庚', '丙'], 优先级: '庚先丙后', 忌神: '甲', 说明: '辰月土月,庚丙并用' },
'壬巳': { 主用神: ['辛', '庚'], 优先级: '辛先庚后', 忌神: '戊', 说明: '巳月火旺,辛金化火' },
'壬午': { 主用神: ['辛', '癸'], 优先级: '辛先癸后', 忌神: '丁', 说明: '午月火旺,辛癸调候' },
'壬未': { 主用神: ['庚', '辛'], 优先级: '庚先辛后', 忌神: '己', 说明: '未月土月,庚辛生水' },
'壬申': { 主用神: ['戊', '丁'], 优先级: '戊先丁后', 忌神: '丙', 说明: '申月金旺,戊丁暖局' },
'壬酉': { 主用神: ['戊', '丁'], 优先级: '戊先丁后', 忌神: '丙', 说明: '酉月金旺,戊丁暖局' },
'壬戌': { 主用神: ['辛', '丙'], 优先级: '辛先丙后', 忌神: '甲', 说明: '戌月燥土,辛丙调候' },
'壬亥': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '庚', 说明: '亥月水冷,丙戊暖局' },
'壬子': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '庚', 说明: '子月水寒,丙戊暖局' },
'壬丑': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '己', 说明: '丑月寒湿,丙丁暖局' },
// 癸水
'癸寅': { 主用神: ['辛', '丙'], 优先级: '辛先丙后', 忌神: '壬', 说明: '寅月木旺,辛丙暖局' },
'癸卯': { 主用神: ['庚', '辛'], 优先级: '庚先辛后', 忌神: '壬', 说明: '卯月木旺,庚辛生水' },
'癸辰': { 主用神: ['辛', '丙'], 优先级: '辛先丙后', 忌神: '乙', 说明: '辰月湿土,辛丙暖局' },
'癸巳': { 主用神: ['辛', '壬'], 优先级: '辛先壬后', 忌神: '戊', 说明: '巳月火旺,辛壬调候' },
'癸午': { 主用神: ['癸', '壬'], 优先级: '癸先壬后', 忌神: '丁', 说明: '午月火旺,癸壬制火' },
'癸未': { 主用神: ['庚', '辛'], 优先级: '庚先辛后', 忌神: '己', 说明: '未月土月,庚辛生水' },
'癸申': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '壬', 说明: '申月金旺,丁丙暖局' },
'癸酉': { 主用神: ['辛', '丁'], 优先级: '辛先丁后', 忌神: '壬', 说明: '酉月金旺,辛金生水' },
'癸戌': { 主用神: ['辛', '壬'], 优先级: '辛先壬后', 忌神: '丙', 说明: '戌月燥土,辛壬润局' },
'癸亥': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '庚', 说明: '亥月水冷,丙戊暖局' },
'癸子': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '庚', 说明: '子月水寒,丙丁暖局' },
'癸丑': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '己', 说明: '丑月寒湿,丙丁暖局' },
};
// ─────────────────────────────────────────────
// 月令旺衰分值表(子平真诠)
// ─────────────────────────────────────────────
const MONTH_STRENGTH = {
'寅': { '甲': 100, '乙': 80, '丙': 70, '丁': 60, '戊': 50, '己': 40, '庚': 30, '辛': 20, '壬': 10, '癸': 0 },
'卯': { '甲': 80, '乙': 100, '丙': 60, '丁': 70, '戊': 40, '己': 50, '庚': 20, '辛': 30, '壬': 10, '癸': 0 },
'辰': { '甲': 60, '乙': 70, '丙': 70, '丁': 80, '戊': 70, '己': 80, '庚': 50, '辛': 60, '壬': 40, '癸': 50 },
'巳': { '甲': 30, '乙': 40, '丙': 100,'丁': 80, '戊': 60, '己': 50, '庚': 40, '辛': 30, '壬': 10, '癸': 0 },
'午': { '甲': 20, '乙': 30, '丙': 80, '丁': 100,'戊': 50, '己': 60, '庚': 30, '辛': 40, '壬': 0, '癸': 10 },
'未': { '甲': 50, '乙': 60, '丙': 60, '丁': 70, '戊': 70, '己': 80, '庚': 50, '辛': 60, '壬': 20, '癸': 30 },
'申': { '甲': 20, '乙': 10, '丙': 30, '丁': 40, '戊': 50, '己': 60, '庚': 100,'辛': 80, '壬': 70, '癸': 50 },
'酉': { '甲': 10, '乙': 20, '丙': 20, '丁': 30, '戊': 40, '己': 50, '庚': 80, '辛': 100,'壬': 50, '癸': 70 },
'戌': { '甲': 50, '乙': 60, '丙': 70, '丁': 80, '戊': 70, '己': 80, '庚': 50, '辛': 60, '壬': 40, '癸': 50 },
'亥': { '甲': 70, '乙': 60, '丙': 20, '丁': 30, '戊': 30, '己': 40, '庚': 10, '辛': 20, '壬': 100,'癸': 80 },
'子': { '甲': 50, '乙': 40, '丙': 10, '丁': 20, '戊': 20, '己': 30, '庚': 0, '辛': 10, '壬': 80, '癸': 100},
'丑': { '甲': 40, '乙': 50, '丙': 50, '丁': 60, '戊': 60, '己': 70, '庚': 50, '辛': 60, '壬': 50, '癸': 60 },
};
// 通根加分表
const TONGGEEN_BONUS = {
'甲': { '寅': 50, '卯': 40, '亥': 20, '子': 0, '辰': 10, '未': 10, '戌': 10, '丑': 5, '巳': 0, '午': 0, '申': 0, '酉': 0 },
'乙': { '卯': 50, '寅': 30, '亥': 10, '子': 20, '辰': 10, '未': 15, '戌': 10, '丑': 10, '巳': 0, '午': 0, '申': 0, '酉': 0 },
'丙': { '巳': 50, '午': 40, '寅': 20, '卯': 10, '申': 0, '酉': 0, '辰': 5, '戌': 10, '丑': 5, '亥': 0, '子': 0, '未': 10 },
'丁': { '午': 50, '巳': 30, '未': 15, '戌': 10, '寅': 10, '酉': 0, '申': 0, '辰': 5, '丑': 5, '亥': 0, '子': 0, '卯': 5 },
'戊': { '辰': 40, '戌': 40, '丑': 30, '未': 30, '巳': 20, '午': 30, '寅': 5, '卯': 5, '申': 0, '酉': 0, '亥': 0, '子': 0 },
'己': { '丑': 40, '未': 40, '辰': 30, '戌': 30, '午': 20, '巳': 10, '寅': 5, '卯': 5, '申': 0, '酉': 0, '亥': 5, '子': 5 },
'庚': { '申': 50, '酉': 40, '辰': 15, '戌': 15, '丑': 20, '未': 15, '寅': 0, '卯': 0, '巳': 0, '午': 0, '亥': 0, '子': 0 },
'辛': { '酉': 50, '申': 30, '辰': 10, '戌': 10, '丑': 15, '未': 10, '寅': 0, '卯': 0, '巳': 0, '午': 0, '亥': 0, '子': 0 },
'壬': { '亥': 50, '子': 40, '申': 20, '酉': 10, '辰': 10, '戌': 10, '丑': 15, '寅': 0, '卯': 0, '巳': 0, '午': 0, '未': 5 },
'癸': { '子': 50, '亥': 40, '丑': 20, '辰': 10, '戌': 10, '申': 5, '酉': 5, '寅': 0, '卯': 0, '巳': 0, '午': 0, '未': 5 },
};
// ─────────────────────────────────────────────
// 辅助函数
// ─────────────────────────────────────────────
function getBranchHiddenStems(branch) {
const h = BRANCH_HIDDEN[branch] || {};
return [h.主气, h.中气, h.余气].filter(Boolean);
}
function getTenGod(dayStem, stem) {
return (TEN_GODS_TABLE[dayStem] || {})[stem] || '未知';
}
// ─────────────────────────────────────────────
// 1. 调候用神(穷通宝鉴)
// ─────────────────────────────────────────────
function analyzeTiaoHou(dayStem, monthBranch, allStems) {
const rule = TIAO_HOU_TABLE[dayStem + monthBranch];
if (!rule) return { 有调候: false, 说明: '此月令无特别调候' };
const present = rule.主用神.filter(g => allStems.includes(g));
const absent = rule.主用神.filter(g => !allStems.includes(g));
const state = present.length === rule.主用神.length ? '调候俱全'
: present.length > 0 ? '调候不全' : '调候皆缺';
return {
有调候: true,
主用神: rule.主用神,
忌神: rule.忌神,
优先级: rule.优先级,
已有用神: present,
缺用神: absent,
调候状态: state,
说明: rule.说明
};
}
// ─────────────────────────────────────────────
// 2. 日主强弱(子平真诠)
// ─────────────────────────────────────────────
function analyzeDayMasterStrength(dayStem, monthBranch, allStemsList, allBranchList) {
const myElement = STEM_ELEMENT[dayStem];
const genByElement = ELEMENT_GENERATED_BY[myElement]; // 生我者(印)
// 月令分
const monthScore = (MONTH_STRENGTH[monthBranch] || {})[dayStem] || 0;
// 比劫分(天干同五行)
let biJieScore = 0;
allStemsList.forEach(s => {
if (s !== dayStem && STEM_ELEMENT[s] === myElement) biJieScore += 20;
});
allBranchList.forEach(z => {
const hidden = BRANCH_HIDDEN[z] || {};
const weight = { 主气: 15, 中气: 8, 余气: 5 };
['主气', '中气', '余气'].forEach(k => {
if (hidden[k] && STEM_ELEMENT[hidden[k]] === myElement) biJieScore += weight[k];
});
});
// 通根分
let tonggenScore = 0;
allBranchList.forEach(z => {
tonggenScore += ((TONGGEEN_BONUS[dayStem] || {})[z] || 0);
});
// 印绶分(生我者天干/地支中)
let yinShouScore = 0;
allStemsList.forEach(s => {
if (STEM_ELEMENT[s] === genByElement) yinShouScore += 15;
});
allBranchList.forEach(z => {
const hidden = BRANCH_HIDDEN[z] || {};
const weight = { 主气: 10, 中气: 5, 余气: 3 };
['主气', '中气', '余气'].forEach(k => {
if (hidden[k] && STEM_ELEMENT[hidden[k]] === genByElement) yinShouScore += weight[k];
});
});
const total = monthScore + biJieScore + tonggenScore + yinShouScore;
let level;
if (total < 80) level = '极弱';
else if (total < 150) level = '弱';
else if (total < 220) level = '偏弱';
else if (total < 300) level = '中和';
else if (total < 380) level = '偏强';
else if (total < 450) level = '强';
else level = '极强';
const isWeak = ['极弱', '弱', '偏弱'].includes(level);
const isStrong = ['偏强', '强', '极强'].includes(level);
return {
日主: dayStem,
五行: myElement,
强弱等级: level,
总分: total,
月令得分: monthScore,
比劫得分: biJieScore,
通根得分: tonggenScore,
印绶得分: yinShouScore,
旺衰: isWeak ? `dayStem身level,宜取印比生扶` : isStrong ? `dayStem身level,宜取官杀财食伤克泄` : `dayStem身中和,综合取用`,
用神方向: isWeak ? '印比扶身' : isStrong ? '官杀财泄身' : '综合平衡'
};
}
// ─────────────────────────────────────────────
// 3. 用神格局(子平真诠)
// ─────────────────────────────────────────────
function analyzeYongShen(dayStem, monthBranch, monthStem, strengthResult) {
const hidden = BRANCH_HIDDEN[monthBranch] || {};
const tenGodsMap = TEN_GODS_TABLE[dayStem] || {};
// 月令本气透干则取透干,否则取本气
const yongshen = (monthStem === hidden.主气 || monthStem === hidden.中气) ? monthStem : hidden.主气;
const pattern = tenGodsMap[yongshen] || '比肩';
// 判断格局类型
let patternType = '正格';
const { 总分: score, 强弱等级: level } = strengthResult;
if (score < 80 && ['七杀', '正官', '偏财', '正财', '食神', '伤官'].includes(pattern)) patternType = '从格';
else if (score > 450 && ['比肩', '劫财'].includes(pattern)) patternType = '专旺格';
// 善用神判断
// 从格/专旺格:顺从格局主气,月令本气即为用神(isGood = true)
// 正格:按身强/身弱扶抑法判断
const isStrong = ['偏强', '强', '极强'].includes(level);
const isWeak = ['极弱', '弱', '偏弱'].includes(level);
let isGood = true;
if (patternType === '正格') {
if (isStrong && ['比肩', '劫财', '正印', '偏印'].includes(pattern)) isGood = false;
if (isWeak && ['七杀', '正官', '偏财', '正财', '食神', '伤官'].includes(pattern)) isGood = false;
}
return {
月令: monthBranch,
透干用神: yongshen,
格局: pattern,
格局类型: patternType,
善用神: isGood,
说明: `monthBranch月令,yongshen'本气',取pattern格`
};
}
// ─────────────────────────────────────────────
// 4. 阴阳刚柔(滴天髓)
// ─────────────────────────────────────────────
function analyzeYinYang(pillars, dayStem) {
const issues = [];
const relations = {};
pillars.forEach((p, i) => {
const label = ['年柱', '月柱', '日柱', '时柱'][i];
const se = STEM_ELEMENT[p.stem];
const be = BRANCH_ELEMENT[p.branch];
if (!se || !be) return;
if (ELEMENT_RESTRAIN[be] === se) {
issues.push(`p.stemp.branch截脚(be耗se)`);
}
if (ELEMENT_RESTRAIN[se] === be) {
issues.push(`p.stemp.branch盖头(se克be)`);
}
if (se === be) relations[label] = '比和';
else if (ELEMENT_PRODUCES[se] === be) relations[label] = '相生';
else if (ELEMENT_RESTRAIN[se] === be) relations[label] = '相克';
else relations[label] = '无直接关系';
});
const yy = STEM_YINYANG[dayStem] || '阳';
return {
日主阴阳: yy,
刚柔: yy === '阳' ? '刚' : '柔',
盖头截脚: issues,
天干地支关系: relations,
说明: `dayStem为yy干,性'柔','无截脚盖头'`
};
}
// ─────────────────────────────────────────────
// 5. 十神分析
// ─────────────────────────────────────────────
function analyzeTenGods(dayStem, stems, branches) {
const result = { 天干十神: {}, 地支十神: {} };
const labels = ['年', '月', '日', '时'];
stems.forEach((s, i) => {
if (i === 2) return; // 日干本身
result.天干十神[labels[i] + '干'] = `s(getTenGod(dayStem, s))`;
});
branches.forEach((z, i) => {
const hidden = BRANCH_HIDDEN[z] || {};
const parts = [hidden.主气, hidden.中气, hidden.余气].filter(Boolean)
.map(h => `hgetTenGod(dayStem, h)`).join('/');
result.地支十神[labels[i] + '支'] = `z(parts)`;
});
return result;
}
// ─────────────────────────────────────────────
// 综合分析入口
// ─────────────────────────────────────────────
/**
* @param {object} bazi - { year, month, day, hour, dayStem }
* e.g. { year:'丙寅', month:'己亥', day:'乙丑', hour:'乙酉', dayStem:'乙' }
*/
function runFullAnalysis(bazi) {
const yearStem = bazi.year[0], yearBranch = bazi.year[1];
const monthStem = bazi.month[0], monthBranch = bazi.month[1];
const dayStem = bazi.dayStem || bazi.day[0];
const dayBranch = bazi.day[1];
const hourStem = bazi.hour[0], hourBranch = bazi.hour[1];
const allStems = [yearStem, monthStem, dayStem, hourStem];
const allBranches = [yearBranch, monthBranch, dayBranch, hourBranch];
const allHiddenStems = allBranches.flatMap(getBranchHiddenStems);
const pillars = [
{ stem: yearStem, branch: yearBranch },
{ stem: monthStem, branch: monthBranch },
{ stem: dayStem, branch: dayBranch },
{ stem: hourStem, branch: hourBranch },
];
const tiaoHou = analyzeTiaoHou(dayStem, monthBranch, [...allStems, ...allHiddenStems]);
const strength = analyzeDayMasterStrength(dayStem, monthBranch, allStems, allBranches);
const yongShen = analyzeYongShen(dayStem, monthBranch, monthStem, strength);
const yinYang = analyzeYinYang(pillars, dayStem);
const tenGods = analyzeTenGods(dayStem, allStems, allBranches);
return { 调候用神: tiaoHou, 日主强弱: strength, 用神格局: yongShen, 阴阳刚柔: yinYang, 十神: tenGods };
}
/**
* 格式化输出分析报告
*/
function formatAnalysisReport(bazi) {
const a = runFullAnalysis(bazi);
const lines = [];
lines.push(`━━ 八字深度分析 ━━`);
lines.push(`四柱:bazi.year bazi.month bazi.day bazi.hour`);
lines.push(`日主:a.日主强弱.日主(a.日主强弱.五行)`);
lines.push('');
// 强弱
lines.push(`▶ 日主强弱(子平真诠)`);
lines.push(` 强弱等级:a.日主强弱.强弱等级(a.日主强弱.总分分)`);
lines.push(` 月令a.日主强弱.月令得分 + 比劫a.日主强弱.比劫得分 + 通根a.日主强弱.通根得分 + 印绶a.日主强弱.印绶得分`);
lines.push(` → a.日主强弱.旺衰`);
lines.push('');
// 格局
lines.push(`▶ 用神格局(子平真诠)`);
lines.push(` 格局:a.用神格局.格局(a.用神格局.格局类型)`);
lines.push(` a.用神格局.说明`);
lines.push(` 善用神:'❌ 否(用神有害)'`);
lines.push('');
// 调候
lines.push(`▶ 调候用神(穷通宝鉴)`);
if (a.调候用神.有调候) {
lines.push(` 用神:a.调候用神.主用神.join('、')(a.调候用神.优先级)`);
lines.push(` 忌神:a.调候用神.忌神 状态:a.调候用神.调候状态`);
lines.push(` a.调候用神.说明`);
if (a.调候用神.缺用神.length > 0) lines.push(` ⚠ 八字缺:a.调候用神.缺用神.join('、')`);
} else {
lines.push(` a.调候用神.说明`);
}
lines.push('');
// 阴阳
lines.push(`▶ 阴阳刚柔(滴天髓)`);
lines.push(` a.阴阳刚柔.说明`);
if (a.阴阳刚柔.盖头截脚.length > 0) {
a.阴阳刚柔.盖头截脚.forEach(s => lines.push(` ⚠ s`));
}
lines.push('');
// 十神
lines.push(`▶ 十神`);
Object.entries(a.十神.天干十神).forEach(([k, v]) => lines.push(` k: v`));
Object.entries(a.十神.地支十神).forEach(([k, v]) => lines.push(` k: v`));
return lines.join('\n');
}
module.exports = { runFullAnalysis, formatAnalysisReport, analyzeTiaoHou, analyzeDayMasterStrength, analyzeYongShen, analyzeYinYang, analyzeTenGods, TEN_GODS_TABLE, BRANCH_HIDDEN, TIAO_HOU_TABLE };
// 命令行调用
if (require.main === module) {
const args = process.argv.slice(2);
if (args.length < 4) {
console.log('用法: node bazi-analysis.js <年柱> <月柱> <日柱> <时柱>');
console.log('示例: node bazi-analysis.js 丙寅 己亥 乙丑 乙酉');
process.exit(1);
}
const [year, month, day, hour] = args;
const dayStem = day[0];
console.log(formatAnalysisReport({ year, month, day, hour, dayStem }));
}
FILE:scripts/daily-fortune.js
#!/usr/bin/env node
/**
* 每日运程生成脚本
* 生成当日运程报告:综合指数、穿衣颜色、宜忌、风险提示、吉时
*/
const dayMap = ['日', '一', '二', '三', '四', '五', '六'];
const monthMap = ['正', '二', '三', '四', '五', '六', '七', '八', '九', '十', '冬', '腊'];
// 五行颜色对应
const fiveElements = {
'木': { color: '绿色、青色', direction: '东方', lucky: '招贵人运' },
'火': { color: '红色、紫色', direction: '南方', lucky: '增事业运' },
'土': { color: '黄色、棕色', direction: '中央', lucky: '稳财运' },
'金': { color: '白色、金色', direction: '西方', lucky: '旺事业' },
'水': { color: '黑色、蓝色', direction: '北方', lucky: '防水逆' }
};
// 地支对应五行
const zhiElement = {
'子': '水', '丑': '土', '寅': '木', '卯': '木',
'辰': '土', '巳': '火', '午': '火', '未': '土',
'申': '金', '酉': '金', '戌': '土', '亥': '水'
};
//时辰信息
const hourInfo = {
'子': { range: '23-01', element: '水', tip: '整理思考' },
'丑': { range: '01-03', element: '土', tip: '睡眠休息' },
'寅': { range: '03-05', element: '木', tip: '计划准备' },
'卯': { range: '05-07', element: '木', tip: '晨间运动' },
'辰': { range: '07-09', element: '土', tip: '贵人运佳' },
'巳': { range: '09-11', element: '火', tip: '事业高峰' },
'午': { range: '11-13', element: '火', tip: '财运旺盛' },
'未': { range: '13-15', element: '土', tip: '平稳行事' },
'申': { range: '15-17', element: '金', tip: '财运佳' },
'酉': { range: '17-19', element: '金', tip: '收整理' },
'戌': { range: '19-21', element: '土', tip: '社交应酬' },
'亥': { range: '21-23', element: '水', tip: '学习思考' }
};
// 宜忌(简化版)
const yiJi = {
'木': { yi: ['出行', '学习', '交友', '谈判'], ji: ['冒险', '投资', '手术'] },
'火': { yi: ['表白', '签约', '创新', '表演'], ji: ['安葬', '搬家', '诉讼'] },
'土': { yi: ['种植', '装修', '求职', '上任'], ji: ['动土', '开业', '破土'] },
'金': { yi: ['上任', '洽谈', '收款', '装修'], ji: ['安葬', '破土', '开业'] },
'水': { yi: ['出行', '考试', '推广', '流动'], ji: ['搬家', '动土', '投资'] }
};
/**
* 计算当日天干地支
*/
function getDayGanZhi(date = new Date()) {
// 以2024年1月1日=甲子日为基准(取正午避免夏令时边界问题)
const baseDate = new Date('2024-01-01T12:00:00');
const diffDays = Math.round((date - baseDate) / (1000 * 60 * 60 * 24));
const ganIndex = ((diffDays % 10) + 10) % 10; // 甲=0
const zhiIndex = ((diffDays % 12) + 12) % 12; // 子=0
const tianGan = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
const diZhi = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
return tianGan[ganIndex] + diZhi[zhiIndex];
}
/**
* 获取五行信息
*/
function getElementInfo(ganZhi) {
const zhi = ganZhi[1];
const element = zhiElement[zhi] || '土';
return fiveElements[element] || fiveElements['土'];
}
/**
* 生成宜忌列表
*/
function getYiJiList(element) {
const info = yiJi[element] || yiJi['土'];
return {
yi: info.yi.slice(0, 4),
ji: info.ji.slice(0, 4)
};
}
/**
* 获取吉时
*/
function getLuckyHours(ganZhi) {
const zhi = ganZhi[1];
const element = zhiElement[zhi] || '土';
// 根据五行找出当日旺的时辰
const luckyZhi = Object.entries(zhiElement)
.filter(([_, el]) => el === element)
.map(([z]) => z);
return luckyZhi.slice(0, 2).map(z => ({
zhi: z,
...hourInfo[z]
}));
}
/**
* 生成风险提示
*/
function getRiskWarnings(ganZhi, dayOfWeek) {
const warnings = [];
const zhi = ganZhi[1];
// 驿马星(地支对冲)
const yimaZhi = ['申', '亥', '寅', '巳'];
if (yimaZhi.includes(zhi)) {
warnings.push({
level: '🟡',
type: '出行',
msg: '今日驿马星动,出行注意安全'
});
}
// 破日(地支相破)
const poZhi = ['子', '午', '卯', '酉', '辰', '丑', '寅', '亥', '巳', '申', '戌', '未'];
const poPairs = [['子', '丑'], ['寅', '亥'], ['卯', '戌'], ['辰', '酉'], ['巳', '申'], ['午', '未']];
// 简单的风险判断
if (dayOfWeek === 1 || dayOfWeek === 5) {
warnings.push({
level: '🟢',
type: '综合',
msg: '今日诸事顺遂,宜积极行动'
});
}
return warnings;
}
/**
* 生成运势评分
*/
function getFortuneScores(ganZhi) {
const zhi = ganZhi[1];
const element = zhiElement[zhi] || '土';
// 根据五行生克简单评分
const baseScores = {
'木': { career: 4, wealth: 3, love: 4, health: 3 },
'火': { career: 5, wealth: 4, love: 3, health: 3 },
'土': { career: 3, wealth: 4, love: 3, health: 4 },
'金': { career: 4, wealth: 5, love: 3, health: 3 },
'水': { career: 3, wealth: 3, love: 4, health: 4 }
};
const scores = baseScores[element] || baseScores['土'];
// 随机微调(±0.5)
const jitter = () => (Math.random() - 0.5);
return {
career: Math.min(5, Math.max(1, scores.career + jitter())).toFixed(1),
wealth: Math.min(5, Math.max(1, scores.wealth + jitter())).toFixed(1),
love: Math.min(5, Math.max(1, scores.love + jitter())).toFixed(1),
health: Math.min(5, Math.max(1, scores.health + jitter())).toFixed(1)
};
}
/**
* 格式化星级
*/
function formatStars(score) {
const num = parseFloat(score);
const full = Math.floor(num);
const half = num - full >= 0.5 ? 1 : 0;
const empty = 5 - full - half;
return '★'.repeat(full) + '☆'.repeat(half) + '☆'.repeat(empty);
}
/**
* 生成完整运程报告
*/
function generateDailyFortune(date = new Date()) {
const ganZhi = getDayGanZhi(date);
const elementInfo = getElementInfo(ganZhi);
const element = zhiElement[ganZhi[1]] || '土';
const yiJiInfo = getYiJiList(element);
const luckyHours = getLuckyHours(ganZhi);
const warnings = getRiskWarnings(ganZhi, date.getDay());
const scores = getFortuneScores(ganZhi);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const weekDay = dayMap[date.getDay()];
const monthName = monthMap[month - 1];
// 生成运势语
const fortuneQuotes = [
'顺势而为,伺机而动',
'稳中求进,步步为营',
'阳光总在风雨后',
'把握当下,展望未来',
'积跬步以至千里'
];
const quote = fortuneQuotes[date.getDate() % fortuneQuotes.length];
// 组装报告
const report = `
🌅 【私人命理顾问】year年month月day日(周weekDay)
📊 今日综合指数
事业:formatStars(scores.career) scores.career
财运:formatStars(scores.wealth) scores.wealth
感情:formatStars(scores.love) scores.love
健康:formatStars(scores.health) scores.health
🎨 幸运色:elementInfo.color(利elementInfo.lucky)
幸运方位:elementInfo.direction
💼 今日宜忌
✅ 宜:yiJiInfo.yi.join('、')
❌ 忌:yiJiInfo.ji.join('、')
⚠️ 风险提示
warnings.length > 0 ? warnings.map(w => ` ${w.level 【w.type】w.msg`).join('\n') : ' 🟢 今日总体平稳,无明显风险'}
⏰ 吉时
luckyHours.map(h => ` • ${h.zhi时(h.range点)- h.tip`).join('\n')}
💡 今日一句
「quote」
📅 干支:ganZhi(elementInfo.direction.charAt(0)气旺)
`;
return report;
}
// 主入口
const args = process.argv.slice(2);
let date = new Date();
if (args[0]) {
try {
date = new Date(args[0]);
if (isNaN(date.getTime())) {
console.error('日期格式无效,请使用 YYYY-MM-DD');
process.exit(1);
}
} catch (e) {
console.error('日期解析错误:', e.message);
process.exit(1);
}
}
console.log(generateDailyFortune(date));
FILE:scripts/daily-push.js
#!/usr/bin/env node
/**
* 每日运势自动推送脚本
* 读取所有已开启推送的用户,生成定制化运程并通过 OpenClaw 发送
*
* 用法:
* node daily-push.js # 推送今日运势给所有已开启的用户
* node daily-push.js --dry-run # 模拟推送(不实际发送)
* node daily-push.js --test <userId> # 测试推送指定用户
* node daily-push.js --list # 列出所有已开启推送的用户
*/
const fs = require('fs');
const path = require('path');
const PROFILES_DIR = path.join(__dirname, '../data/profiles');
const LOG_FILE = path.join(__dirname, '../data/push-log.json');
// ============================================================
// 八字/紫微 核心分析(内嵌,避免外部依赖)
// ============================================================
const GAN = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
const ZHI = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
const SHENGXIAO = ['鼠', '牛', '虎', '兔', '龙', '蛇', '马', '羊', '猴', '鸡', '狗', '猪'];
const ZHI_ELEMENT = {
'子': '水', '丑': '土', '寅': '木', '卯': '木',
'辰': '土', '巳': '火', '午': '火', '未': '土',
'申': '金', '酉': '金', '戌': '土', '亥': '水'
};
const ELEMENT_COLOR = {
'木': { color: '绿色、青色', direction: '东方', emoji: '🌿' },
'火': { color: '红色、紫色', direction: '南方', emoji: '🔥' },
'土': { color: '黄色、棕色', direction: '中央', emoji: '🌍' },
'金': { color: '白色、银色', direction: '西方', emoji: '⚪' },
'水': { color: '黑色、蓝色', direction: '北方', emoji: '🌊' }
};
const LUCKY_NUMBERS = {
'木': [3, 8], '火': [2, 7], '土': [5, 10], '金': [4, 9], '水': [1, 6]
};
const DAY_MAP = ['日', '一', '二', '三', '四', '五', '六'];
const MONTH_MAP = ['正', '二', '三', '四', '五', '六', '七', '八', '九', '十', '冬', '腊'];
const HOUR_INFO = {
'子': { range: '23-01', tip: '整理思考', stars: '☽ 阴性星' },
'丑': { range: '01-03', tip: '睡眠休息', stars: '☆ 平常' },
'寅': { range: '03-05', tip: '计划准备', stars: '🌟 小吉' },
'卯': { range: '05-07', tip: '晨间运动', stars: '🌟 小吉' },
'辰': { range: '07-09', tip: '贵人运佳', stars: '★★ 吉祥' },
'巳': { range: '09-11', tip: '事业高峰', stars: '★★ 大吉' },
'午': { range: '11-13', tip: '财运旺盛', stars: '★★ 大吉' },
'未': { range: '13-15', tip: '平稳行事', stars: '★☆ 一般' },
'申': { range: '15-17', tip: '财运佳', stars: '★★ 吉祥' },
'酉': { range: '17-19', tip: '收整理', stars: '★☆ 一般' },
'戌': { range: '19-21', tip: '社交应酬', stars: '★★ 吉祥' },
'亥': { range: '21-23', tip: '学习思考', stars: '☆ 平常' }
};
// ============================================================
// 命理核心算法
// ============================================================
function getDayGanZhi(date = new Date()) {
const baseDate = new Date('2024-01-01T12:00:00');
const diffDays = Math.round((date - baseDate) / (1000 * 60 * 60 * 24));
const ganIndex = ((diffDays % 10) + 10) % 10;
const zhiIndex = ((diffDays % 12) + 12) % 12;
return GAN[ganIndex] + ZHI[zhiIndex];
}
function getYearGanZhi(year) {
const baseYear = 1984; // 甲子年
const offset = year - baseYear;
return GAN[((offset % 10) + 10) % 10] + ZHI[((offset % 12) + 12) % 12];
}
function getLunarMonth(month) {
return MONTH_MAP[month - 1] + '月';
}
function getElementInfo(ganZhi) {
const zhi = ganZhi[1];
const element = ZHI_ELEMENT[zhi] || '土';
return { element, ...ELEMENT_COLOR[element] };
}
function getLuckyNumbers(element) {
const nums = LUCKY_NUMBERS[element] || [5, 10];
const allNums = [];
for (let i = 0; i < 5; i++) allNums.push(nums[i % nums.length]);
return allNums.slice(0, 5);
}
// ============================================================
// 八字用神计算
// ============================================================
function calculateBaziYongshen(bazi) {
if (!bazi || !bazi.dayStem) return { primary: '木', secondary: ['火', '水'], details: [] };
const dayStem = bazi.dayStem;
const monthZhi = bazi.month ? bazi.month[1] : '寅';
const dayWuxing = { '甲': '木', '乙': '木', '丙': '火', '丁': '火', '戊': '土', '己': '土', '庚': '金', '辛': '金', '壬': '水', '癸': '水' }[dayStem] || '木';
const monthWuxing = ZHI_ELEMENT[monthZhi] || '木';
const sheng = { '木': '火', '火': '土', '土': '金', '金': '水', '水': '木' };
const ke = { '木': '土', '火': '金', '土': '水', '金': '木', '水': '火' };
const results = [];
// 调候用神
const tiaohouTable = {
'甲': { '寅': '丙', '卯': '丙', '辰': '癸', '巳': '壬', '午': '壬', '未': '癸', '申': '丁', '酉': '丁', '戌': '辛', '亥': '丙', '子': '庚', '丑': '辛' },
'乙': { '寅': '丙', '卯': '丙', '辰': '癸', '巳': '壬', '午': '癸', '未': '丙', '申': '丁', '酉': '丁', '戌': '辛', '亥': '丙', '子': '庚', '丑': '辛' }
};
const t = tiaohouTable[dayStem]?.[monthZhi];
if (t) results.push({ type: '调候', value: t, desc: '寒木喜火暖局' });
// 扶抑用神
results.push({ type: '扶抑', value: sheng[dayWuxing], desc: `日主dayWuxing,喜生助` });
results.push({ type: '忌', value: ke[dayWuxing], desc: `日主dayWuxing,宜避` });
const primary = results[0]?.value || dayWuxing;
const secondary = [...new Set(results.filter(r => r.type === '扶抑').map(r => r.value))];
return { primary, secondary: secondary.slice(0, 2), details: results };
}
// ============================================================
// 运势评分(结合八字五行 + 当日干支)
// ============================================================
function generatePersonalizedScores(bazi, dayGanZhi) {
const dayElement = ZHI_ELEMENT[dayGanZhi[1]] || '土';
const dayWuxing = dayElement;
// 八字日主五行
const dayStem = bazi?.dayStem || '甲';
const dayStemWuxing = { '甲': '木', '乙': '木', '丙': '火', '丁': '火', '戊': '土', '己': '土', '庚': '金', '辛': '金', '壬': '水', '癸': '水' }[dayStem] || '木';
// 日主与当日五行的关系
const sheng = { '木': '火', '火': '土', '土': '金', '金': '水', '水': '木' };
const ke = { '木': '土', '火': '金', '土': '水', '金': '木', '水': '火' };
const bi = { '木': '金', '火': '水', '土': '木', '金': '火', '水': '土' };
// 生助日主 = 吉
const dayHelpsDay = dayWuxing === dayStemWuxing || sheng[dayStemWuxing] === dayWuxing;
// 克耗日主 = 压力
const dayStressesDay = ke[dayStemWuxing] === dayWuxing || bi[dayStemWuxing] === dayWuxing;
// 事业:与用神同气 + 当日吉
let career = 3 + Math.random() * 1.5;
let wealth = 3 + Math.random() * 1.5;
let love = 3 + Math.random() * 1.5;
let health = 3 + Math.random() * 1.5;
if (dayHelpsDay) {
career += 0.5;
wealth += 0.5;
health += 0.3;
}
if (dayStressesDay) {
career -= 0.3;
wealth -= 0.3;
}
// 基于用神微调
const yongshen = calculateBaziYongshen(bazi);
if (yongshen.primary === dayElement) { career += 0.5; wealth += 0.3; }
// 加入日期随机性(确保每天有变化)
const date = new Date();
const dayFactor = (date.getDate() % 3) * 0.3 - 0.3;
career += dayFactor;
wealth += dayFactor * 0.8;
love += dayFactor * 0.6;
health += dayFactor * 0.4;
const scores = {
career: Math.min(5, Math.max(1, career)).toFixed(1),
wealth: Math.min(5, Math.max(1, wealth)).toFixed(1),
love: Math.min(5, Math.max(1, love)).toFixed(1),
health: Math.min(5, Math.max(1, health)).toFixed(1)
};
return scores;
}
function formatStars(score) {
const num = parseFloat(score);
const full = Math.floor(num);
const half = num - full >= 0.5 ? 1 : 0;
return '★'.repeat(full) + '☆'.repeat(5 - full - half);
}
// ============================================================
// 宜忌生成(基于八字用神 + 当日干支)
// ============================================================
function generateYiJi(bazi, dayGanZhi) {
const dayElement = ZHI_ELEMENT[dayGanZhi[1]] || '土';
const yongshen = calculateBaziYongshen(bazi);
const primaryElement = yongshen.primary;
const YI_JI = {
'木': {
yi: ['出行', '学习', '交友', '谈判', '签约', '求职'],
ji: ['冒险', '投资', '手术', '安葬', '破土']
},
'火': {
yi: ['表白', '签约', '创新', '表演', '开业', '上任'],
ji: ['安葬', '搬家', '诉讼', '动土', '破土']
},
'土': {
yi: ['种植', '装修', '求职', '上任', '签约', '装修'],
ji: ['动土', '开业', '破土', '安葬', '投资']
},
'金': {
yi: ['上任', '洽谈', '收款', '装修', '签约', '投资'],
ji: ['安葬', '破土', '开业', '动土', '搬家']
},
'水': {
yi: ['出行', '考试', '推广', '流动', '求职', '开业'],
ji: ['搬家', '动土', '投资', '安葬', '破土']
}
};
// 优先用神,其次当日五行
const element = primaryElement || dayElement;
const info = YI_JI[element] || YI_JI['土'];
return {
yi: info.yi.slice(0, 4),
ji: info.ji.slice(0, 4)
};
}
// ============================================================
// 吉凶判断
// ============================================================
function getDayFortuneLevel(dayGanZhi) {
const zhi = dayGanZhi[1];
// 天恩 吉日
const tianEnZhi = ['丑', '寅', '卯', '辰', '午', '未', '亥'];
// 天贵 吉时
const tianGuiZhi = ['辰', '巳', '午', '未', '申'];
// 驿马 变动
const yimaZhi = ['申', '亥', '寅', '巳'];
let level = '平常';
let desc = '今日诸事平稳';
if (tianEnZhi.includes(zhi)) {
level = '吉祥';
desc = '天恩降临,贵人相助';
}
if (yimaZhi.includes(zhi)) {
if (level === '吉祥') {
level = '小吉';
desc = '有变动,宜把握机遇';
} else {
level = '平常';
desc = '驿马星动,出行奔波';
}
}
// 检查是否破日(相破)
const poPairs = [['子','丑'], ['寅','亥'], ['卯','戌'], ['辰','酉'], ['巳','申'], ['午','未']];
for (const [a, b] of poPairs) {
if (zhi === a || zhi === b) {
level = '平常';
desc = '今日有小损耗,宜守成';
break;
}
}
return { level, desc };
}
// ============================================================
// 风险预警
// ============================================================
function generateWarnings(bazi, dayGanZhi) {
const warnings = [];
const zhi = dayGanZhi[1];
// 驿马星
const yimaZhi = ['申', '亥', '寅', '巳'];
if (yimaZhi.includes(zhi)) {
warnings.push({ level: '🟡', type: '出行', msg: '今日驿马星动,出行注意安全,提前出门' });
}
// 五黄煞(简易判断:基于地支)
const wuhuang = ['子', '卯', '午', '酉'];
if (wuhuang.includes(zhi)) {
warnings.push({ level: '🟡', type: '健康', msg: '注意脾胃保养,饮食清淡' });
}
// 八字日主与当日关系
const dayStem = bazi?.dayStem || '甲';
const dayStemWuxing = { '甲': '木', '乙': '木', '丙': '火', '丁': '火', '戊': '土', '己': '土', '庚': '金', '辛': '金', '壬': '水', '癸': '水' }[dayStem] || '木';
const dayWuxing = ZHI_ELEMENT[zhi] || '土';
const ke = { '木': '土', '火': '金', '土': '水', '金': '木', '水': '火' };
if (ke[dayStemWuxing] === dayWuxing) {
warnings.push({ level: '🔴', type: '破财', msg: '今日财星受克,谨慎投资,避免大额花费' });
}
if (warnings.length === 0) {
warnings.push({ level: '🟢', type: '综合', msg: '今日总体顺遂,无明显风险' });
}
return warnings;
}
// ============================================================
// 吉时计算
// ============================================================
function getLuckyHours(dayGanZhi) {
const zhi = dayGanZhi[1];
const dayElement = ZHI_ELEMENT[zhi] || '土';
// 找与当日同气的时辰(旺相)
const sameElementZhi = Object.entries(ZHI_ELEMENT)
.filter(([_, el]) => el === dayElement)
.map(([z]) => z);
// 找生助当日五行的时辰
const sheng = { '木': '火', '火': '土', '土': '金', '金': '水', '水': '木' };
const helpfulZhi = Object.entries(ZHI_ELEMENT)
.filter(([_, el]) => el === sheng[dayElement])
.map(([z]) => z);
const allLucky = [...sameElementZhi, ...helpfulZhi];
const unique = [...new Set(allLucky)].slice(0, 4);
return unique.map(z => ({
zhi: z,
...(HOUR_INFO[z] || { range: '--', tip: '平常', stars: '☆' })
}));
}
// ============================================================
// 流年/流月提示(简化版,基于八字和大运)
// ============================================================
function getYearMonthTips(bazi) {
const currentYear = new Date().getFullYear();
const currentMonth = new Date().getMonth() + 1;
const yearGanZhi = getYearGanZhi(currentYear);
const yearElement = ZHI_ELEMENT[yearGanZhi[1]] || '土';
const tips = [];
// 流年提示
const yearTips = {
'木': '今年木气旺盛,利事业拓展,春季尤佳',
'火': '今年火气当令,利创新突破,夏季事业运佳',
'土': '今年土气稳重,利积累沉淀,秋季财运回升',
'金': '今年金气肃杀,利变革调整,秋季利财运',
'水': '今年水气流动,利流通传播,冬季人脉广'
};
tips.push({ period: '流年', msg: yearTips[yearElement] || '今年运势平稳' });
// 流月提示(简化)
const monthTips = [
'正月开门红,二月稳中求进,三月事业上升',
'四月注意小人,五月财运上佳,六月桃花旺盛',
'七月健康注意,八月事业转折,九月贵人相助',
'十月财运爆发,十一月感情升温,十二月总结规划'
];
const monthIdx = Math.floor((currentMonth - 1) / 2);
tips.push({ period: '本月', msg: monthTips[monthIdx] || '本月运势良好' });
return tips;
}
// ============================================================
// 每日一言
// ============================================================
function getDailyQuote(dayGanZhi) {
const quotes = [
{ element: '木', text: '木秀于林,风必摧之;堆出于岸,流必湍之。' },
{ element: '木', text: '顺势而为,不与天争;待时而动,方成大事。' },
{ element: '火', text: '火焰熊熊,照亮前路;热情如火,无坚不摧。' },
{ element: '火', text: '烈火炼真金,逆境显本色。' },
{ element: '土', text: '厚德载物,稳如泰山;静以修身,俭以养德。' },
{ element: '土', text: '土能生金,稳中求进;深根固本,方可长久。' },
{ element: '金', text: '金以刚为体,人以正为尊;锋芒内敛,大业可成。' },
{ element: '金', text: '金戈铁马,气吞万里如虎。' },
{ element: '水', text: '上善若水,水善利万物而不争。' },
{ element: '水', text: '水能载舟,亦能覆舟;顺势而行,方得始终。' },
{ element: '通用', text: '命里有时终须有,命里无时莫强求。' },
{ element: '通用', text: '三分天注定,七分靠打拼。' }
];
const dayElement = ZHI_ELEMENT[dayGanZhi[1]] || '土';
const dayQuotes = quotes.filter(q => q.element === dayElement);
const fallback = quotes.filter(q => q.element === '通用');
const pool = dayQuotes.length > 0 ? dayQuotes : fallback;
const idx = new Date().getDate() % pool.length;
return pool[idx]?.text || '积善之家,必有余庆。';
}
// ============================================================
// 生成完整个性化运程报告
// ============================================================
function generatePersonalizedFortune(profile, date = new Date()) {
const { bazi } = profile;
const dayGanZhi = getDayGanZhi(date);
const elementInfo = getElementInfo(dayGanZhi);
const luckyNumbers = getLuckyNumbers(elementInfo.element);
const scores = generatePersonalizedScores(bazi, dayGanZhi);
const fortuneLevel = getDayFortuneLevel(dayGanZhi);
const yiJi = generateYiJi(bazi, dayGanZhi);
const warnings = generateWarnings(bazi, dayGanZhi);
const luckyHours = getLuckyHours(dayGanZhi);
const yearMonthTips = getYearMonthTips(bazi);
const quote = getDailyQuote(dayGanZhi);
const yongshen = calculateBaziYongshen(bazi);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const weekDay = DAY_MAP[date.getDay()];
const lunarMonth = getLunarMonth(month);
// 用户基本信息
const userName = profile.name || '你';
const gender = profile.profile?.gender === '男' ? '先生' : '女士';
const zodiac = bazi?.zodiac || '';
// 构建报告
const fortuneEmoji = fortuneLevel.level === '大吉' ? '🌟' :
fortuneLevel.level === '吉祥' ? '✨' :
fortuneLevel.level === '小吉' ? '🌤️' : '🌙';
let report = `fortuneEmoji 【userNamegender】year年month月day日(周weekDay)
━━━━━━━━━━━━━━━━━━━━━━
📊 今日综合指数
事业 formatStars(scores.career) scores.career分
财运 formatStars(scores.wealth) scores.wealth分
感情 formatStars(scores.love) scores.love分
健康 formatStars(scores.health) scores.health分
━━━━━━━━━━━━━━━━━━━━━━
🎨 幸运属性
颜色:elementInfo.color
方位:elementInfo.direction
数字:luckyNumbers.join('、')
幸运物:elementInfo.emoji elementInfo.element元素相关
💮 今日吉凶
fortuneLevel.level — fortuneLevel.desc
💼 今日宜忌
✅ 宜:yiJi.yi.join('、')
❌ 忌:yiJi.ji.join('、')
⚠️ 风险提示
warnings.map(w => ` ${w.level【w.type】w.msg`).join('\n')}
⏰ 吉时
luckyHours.slice(0, 3).map(h => ` • ${h.zhi时(h.range点)- h.tip`).join('\n')}
luckyHours.length > 3 ? ` • ...等 ${luckyHours.length 个吉时` : ''}
📅 流年流月
yearMonthTips.map(t => ` 【${t.period】t.msg`).join('\n')}
💡 今日一言
「quote」
🧮 八字用神:yongshen.primary(主)yongshen.secondary.join('、')(辅)
今日干支:dayGanZhi(elementInfo.element气'得令')
`;
return report;
}
// ============================================================
// 用户档案管理
// ============================================================
function loadAllProfiles() {
if (!fs.existsSync(PROFILES_DIR)) return [];
const files = fs.readdirSync(PROFILES_DIR).filter(f => f.endsWith('.json'));
const profiles = [];
for (const file of files) {
try {
const userId = file.replace('.json', '');
const data = JSON.parse(fs.readFileSync(path.join(PROFILES_DIR, file), 'utf8'));
profiles.push({ userId, ...data });
} catch (e) {
console.warn(`⚠️ 加载档案失败: file`, e.message);
}
}
return profiles;
}
function getUsersWithPushEnabled(profiles) {
return profiles.filter(p => {
// 新字段:preferences.pushEnabled(优先)或 legacy 字段
const enabled = p.preferences?.pushEnabled ?? p.preferences?.pushMorning ?? false;
const hasBazi = p.bazi && p.bazi.day && p.bazi.dayStem;
return enabled && hasBazi;
});
}
function updateLastPushDate(userId) {
const filePath = path.join(PROFILES_DIR, `userId.json`);
if (!fs.existsSync(filePath)) return;
try {
const profile = JSON.parse(fs.readFileSync(filePath, 'utf8'));
profile.lastPushDate = new Date().toISOString().split('T')[0];
profile.updatedAt = new Date().toISOString().split('T')[0];
fs.writeFileSync(filePath, JSON.stringify(profile, null, 2), 'utf8');
} catch (e) {
console.warn(`⚠️ 更新推送日期失败: userId`, e.message);
}
}
// ============================================================
// OpenClaw 消息发送(通过 IPC / openclaw 工具接口)
// ============================================================
async function sendMessage(userId, message) {
// 在 openclaw cron 环境中,stdout 内容由运行时自动发送给用户
console.log(message);
return true;
}
// ============================================================
// 日志记录
// ============================================================
function loadLog() {
if (!fs.existsSync(LOG_FILE)) return { runs: [] };
try {
return JSON.parse(fs.readFileSync(LOG_FILE, 'utf8'));
} catch (e) {
return { runs: [] };
}
}
function appendLog(entry) {
const log = loadLog();
log.runs.push(entry);
// 只保留最近100条
if (log.runs.length > 100) log.runs = log.runs.slice(-100);
fs.writeFileSync(LOG_FILE, JSON.stringify(log, null, 2), 'utf8');
}
// ============================================================
// 主推送流程
// ============================================================
async function runPush({ dryRun = false, testUserId = null } = {}) {
const date = new Date();
const dateStr = date.toISOString().split('T')[0];
const logEntry = {
date: dateStr,
timestamp: new Date().toISOString(),
dryRun,
results: []
};
console.log(`\n🚀 每日运势推送开始 - dateStr\n`);
console.log(` 模式: '📨 正式推送'\n`);
const allProfiles = loadAllProfiles();
let targets = getUsersWithPushEnabled(allProfiles);
if (testUserId) {
const testProfile = allProfiles.find(p => p.userId === testUserId);
if (testProfile) {
targets = [testProfile];
console.log(` 📋 测试模式: 仅推送给 testUserId\n`);
} else {
console.log(` ❌ 测试用户不存在: testUserId`);
return;
}
}
console.log(` 📋 共 targets.length 个用户开启推送\n`);
console.log(' ' + '─'.repeat(50));
let successCount = 0;
let failCount = 0;
for (const profile of targets) {
const { userId, name } = profile;
process.stdout.write(` 🔄 name || userId (userId)... `);
try {
const fortune = generatePersonalizedFortune(profile, date);
if (dryRun) {
console.log('\n' + fortune.split('\n').map(l => ' ' + l).join('\n'));
console.log(' ' + '─'.repeat(50));
successCount++;
} else {
const sent = await sendMessage(userId, fortune);
if (sent) {
updateLastPushDate(userId);
console.log('✅');
successCount++;
} else {
console.log('⚠️ (发送失败,已记录)');
failCount++;
}
}
logEntry.results.push({
userId,
name,
status: dryRun ? 'dry-run' : (sent ? 'success' : 'failed')
});
} catch (e) {
console.log(`❌ e.message`);
failCount++;
logEntry.results.push({
userId,
name,
status: 'error',
error: e.message
});
}
}
console.log(' ' + '─'.repeat(50));
console.log(`\n ✅ 推送完成: successCount 成功failCount > 0 ? `, ${failCount 失败` : ''}\n`);
appendLog(logEntry);
return { successCount, failCount };
}
// ============================================================
// 列出开启推送的用户
// ============================================================
function listPushUsers() {
const profiles = loadAllProfiles();
const targets = getUsersWithPushEnabled(profiles);
console.log('\n📋 已开启每日运势推送的用户:\n');
if (targets.length === 0) {
console.log(' (暂无用户开启推送)\n');
return;
}
for (const p of targets) {
const lastPush = p.lastPushDate || '从未推送';
const channels = (p.preferences?.channels || ['telegram']).join(', ');
console.log(` 👤 p.name (p.userId)`);
console.log(` 八字: p.bazi?.year p.bazi?.month p.bazi?.day p.bazi?.hour`);
console.log(` 推送时间: 00' | 渠道: channels`);
console.log(` 最后推送: lastPush`);
console.log('');
}
}
// ============================================================
// 命令行入口
// ============================================================
async function main() {
const args = process.argv.slice(2);
if (args.includes('--list') || args.includes('-l')) {
listPushUsers();
return;
}
if (args.includes('--dry-run') || args.includes('-d')) {
await runPush({ dryRun: true });
return;
}
const testIdx = args.indexOf('--test');
if (testIdx !== -1 && args[testIdx + 1]) {
await runPush({ testUserId: args[testIdx + 1] });
return;
}
if (args.length === 0) {
await runPush({ dryRun: false });
return;
}
// 帮助
console.log(`
🌅 每日运势自动推送脚本
用法:
node daily-push.js 推送给所有已开启的用户
node daily-push.js --dry-run 模拟推送(显示内容,不发送)
node daily-push.js --test <userId> 测试推送指定用户
node daily-push.js --list 列出已开启推送的用户
配置:
- 用户的 preferences.pushEnabled 需为 true
- 用户的 preferences.morningTime 决定推送时间(默认07:00)
- 渠道由 preferences.channels 指定(telegram/feishu)
- 用户需有完整的八字信息(bazi.dayStem 不为空)
OpenClaw Cron 配置:
openclaw cron add "0 7 * * *" "cd <skill-dir> && node scripts/daily-push.js"
`);
}
main().catch(e => {
console.error('❌ 推送脚本出错:', e);
process.exit(1);
});
FILE:scripts/evening-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('invalid userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('illegal path');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{};}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
console.log(`晚间运势复盘🌙(date)。请生成明日运势预告:①明日五行旺衰②明日最佳行动方向(事业/感情/财务各1条)③明日需要注意的风险⑤一句引导睡前反思的命理箴言。结合今日运势变化趋势,简洁有深度,中文输出。`);
FILE:scripts/fengshui.js
#!/usr/bin/env node
/**
* 风水分析脚本
* 支持:阳宅风水、办公室风水、颜色风水
*/
const fs = require('fs');
const path = require('path');
// 八卦方位对应
const baGuaDirection = {
'乾': { direction: '西北', number: 6, element: '金', color: '白色', trait: '领导、权威' },
'坤': { direction: '西南', number: 2, element: '土', color: '黄色', trait: '柔顺、包容' },
'震': { direction: '东', number: 3, element: '木', color: '绿色', trait: '震动、创新' },
'巽': { direction: '东南', number: 4, element: '木', color: '绿色', trait: '进入、柔和' },
'坎': { direction: '北', number: 1, element: '水', color: '黑色', trait: '险陷、智慧' },
'离': { direction: '南', number: 9, element: '火', color: '红色', trait: '明亮、热情' },
'艮': { direction: '东北', number: 8, element: '土', color: '黄色', trait: '停止、稳定' },
'兑': { direction: '西', number: 7, element: '金', color: '白色', trait: '喜悦、口才' }
};
// 九宫飞星宫位序列(洛书飞布次序)
const PALACE_SEQ = ['中宫', '乾', '兑', '艮', '离', '坎', '坤', '震', '巽'];
const STAR_NAMES = ['一白', '二黑', '三碧', '四绿', '五黄', '六白', '七赤', '八白', '九紫'];
// 财位寻找
const caiWei = {
'明财位': '大门对角线位置',
'暗财位': '住宅中心点',
'流年财位': '每年变化,见流年飞星',
'固定财位': '根据主人八字喜忌定'
};
// 吉凶方位
const jiXiongDirections = {
'乾': { wealth: '吉', health: '平', career: '吉', love: '平' },
'坤': { wealth: '平', health: '吉', career: '平', love: '吉' },
'震': { wealth: '平', health: '平', career: '平', love: '吉' },
'巽': { wealth: '吉', health: '平', career: '吉', love: '平' },
'坎': { wealth: '平', health: '凶', career: '凶', love: '平' },
'离': { wealth: '吉', health: '吉', career: '平', love: '平' },
'艮': { wealth: '吉', health: '吉', career: '平', love: '平' },
'兑': { wealth: '平', health: '平', career: '凶', love: '吉' }
};
/**
* 获取流年飞星(动态计算,支持任意年份)
* 基准:2024年中宫=一白,每年中宫星顺序+3(3年一轮:一白→四绿→七赤)
*/
function getFlyingStars(year = new Date().getFullYear()) {
// yearOffset: 0→中宫一白, 1→中宫四绿, 2→中宫七赤
const yearOffset = ((year - 2024) % 3 + 3) % 3;
const result = {};
for (let i = 0; i < 9; i++) {
const starIdx = (i + yearOffset * 3) % 9;
result[STAR_NAMES[starIdx]] = PALACE_SEQ[i];
}
return result;
}
/**
* 分析大门风水
*/
function analyzeMainDoor(direction) {
const info = baGuaDirection[direction];
if (!info) {
return { error: '方向不明确' };
}
let analysis = '';
let suggestions = [];
// 大门朝向来判断
if (['乾', '兑'].includes(direction)) {
analysis = '大门朝西或西北,金气旺盛';
suggestions.push('宜摆放金属装饰增强运势');
suggestions.push('可放白色或金色地毯');
} else if (['震', '巽'].includes(direction)) {
analysis = '大门朝东或东南,木气旺盛';
suggestions.push('宜摆放绿植招贵人');
suggestions.push('保持空间明亮通风');
} else if (['坎'].includes(direction)) {
analysis = '大门朝北,水气旺盛';
suggestions.push('宜用蓝色或黑色装饰');
suggestions.push('注意防潮防湿');
} else if (['离'].includes(direction)) {
analysis = '大门朝南,火气旺盛';
suggestions.push('宜用红色或紫色装饰');
suggestions.push('注意防火安全');
} else if (['坤', '艮'].includes(direction)) {
analysis = '大门朝西南或东北,土气旺盛';
suggestions.push('宜用黄色或棕色装饰');
suggestions.push('保持空间稳重踏实');
}
return {
direction: info.direction,
element: info.element,
analysis,
suggestions
};
}
/**
* 分析财位
*/
function analyzeWealthPosition(bazi, year = new Date().getFullYear()) {
const stars = getFlyingStars(year);
const baziDayStem = bazi?.charAt(0) || '甲';
// 找出财位
const positions = [];
// 明财位
positions.push({
type: '明财位',
location: '大门对角线(进门后左右角落)',
description: '气流入处,聚气纳财',
direction: '根据实际大门位置定'
});
// 流年财位
const yiMaPosition = Object.entries(stars).find(([name]) => name === '一白贪狼')?.[1] || '坎';
positions.push({
type: '流年财位(2026)',
location: `baGuaDirection[yiMaPosition]?.direction || '北'`,
description: '一白贪狼星所在,利财运',
stars: stars
});
// 日主喜用(简化)
const dayElements = {
'甲': '木', '乙': '木', '丙': '火', '丁': '火',
'戊': '土', '己': '土', '庚': '金', '辛': '金',
'壬': '水', '癸': '水'
};
const dayElement = dayElements[baziDayStem] || '土';
// 根据日主五行找财位
const wealthDirections = {
'木': '东方、东南',
'火': '南方',
'土': '西南、东北',
'金': '西方、西北',
'水': '北方'
};
positions.push({
type: '八字喜用财位',
location: wealthDirections[dayElement] || '根据八字定',
description: `日主baziDayStem,喜dayElement,财位在wealthDirections[dayElement]`
});
return positions;
}
/**
* 分析卧室风水
*/
function analyzeBedroom(direction) {
const info = baGuaDirection[direction];
if (!info) {
return { error: '方向不明确' };
}
const bedroomAnalysis = {
'乾': {
score: 85,
pros: ['领导力增强', '事业运势提升'],
cons: ['过于刚硬', '需柔和装饰平衡'],
tips: ['放置圆润家具', '用红色点缀']
},
'坤': {
score: 80,
pros: ['睡眠安稳', '感情和睦'],
cons: ['行动力减弱', '需适当运动'],
tips: ['保持整洁', '放置陶瓷饰品']
},
'震': {
score: 75,
pros: ['事业突破', '贵人运强'],
cons: ['情绪波动大', '需静心'],
tips: ['放置绿植', '避免尖锐物品']
},
'巽': {
score: 78,
pros: ['思维活跃', '学习运佳'],
cons: ['易犹豫不决', '需果断'],
tips: ['保持空气流通', '放置文昌塔']
},
'坎': {
score: 70,
pros: ['智慧提升', '财运渐佳'],
cons: ['健康需注意', '多运动'],
tips: ['保持干燥', '放置属火物品']
},
'离': {
score: 82,
pros: ['名气提升', '桃花运佳'],
cons: ['情绪波动', '需平和'],
tips: ['避免强光直射', '放置水景装饰']
},
'艮': {
score: 88,
pros: ['健康安稳', '守财能力强'],
cons: ['过于保守', '需突破'],
tips: ['放置金属装饰', '适当变动布局']
},
'兑': {
score: 76,
pros: ['口才提升', '社交运佳'],
cons: ['易起争执', '需忍让'],
tips: ['放置鲜花', '保持明亮']
}
};
return bedroomAnalysis[direction] || bedroomAnalysis['艮'];
}
/**
* 分析办公室风水
*/
function analyzeOffice() {
return {
desk: {
ideal: '背靠实墙,面朝门口或窗户',
avoid: '背窗坐、横梁压顶',
direction: '坐北朝南或坐东朝西'
},
wealth: {
position: '大门对角线',
tips: ['放置貔貅或金蟾', '保持整洁', '不放垃圾桶']
},
career: {
position: '东或东南',
tips: ['放置绿植', '文昌位放毛笔或书籍']
},
avoid: [
'横梁压顶',
'背后有窗户',
'正对厕所门',
'灯光直射头顶'
]
};
}
/**
* 生成颜色建议
*/
function generateColorAdvice(bazi) {
const dayStem = bazi?.charAt(0) || '甲';
const dayElements = {
'甲': '木', '乙': '木', '丙': '火', '丁': '火',
'戊': '土', '己': '土', '庚': '金', '辛': '金',
'壬': '水', '癸': '水'
};
const element = dayElements[dayStem] || '土';
const colorMap = {
'木': { lucky: ['绿色', '青色', '蓝色'], avoid: ['白色', '金色'], reason: '木生火,绿色助木' },
'火': { lucky: ['红色', '紫色', '绿色'], avoid: ['黑色', '蓝色'], reason: '火生土,红紫色助火' },
'土': { lucky: ['黄色', '棕色', '红色'], avoid: ['绿色', '青色'], reason: '土生金,黄色助土' },
'金': { lucky: ['白色', '金色', '黄色'], avoid: ['红色', '紫色'], reason: '金生水,白金色助金' },
'水': { lucky: ['黑色', '蓝色', '白色'], avoid: ['黄色', '棕色'], reason: '水生木,蓝色助水' }
};
return colorMap[element] || colorMap['土'];
}
/**
* 生成完整风水报告
*/
function generateFengShuiReport(bazi, year = new Date().getFullYear()) {
const stars = getFlyingStars(year);
const dayStem = bazi?.charAt(0) || '甲';
// 流年分析
let report = `
🏠 【风水分析报告】
━━━━━━━━━━━━━━━━━━━━
📅 流年:year年
🧮 八字:bazi || '未提供'
━━━━━━━━━━━━━━━━━━━━
✨ 流年飞星(year年)
`;
for (const [star, position] of Object.entries(stars)) {
// 中宫特殊处理
if (position === '中宫' || position === '中') {
report += ` star → 中宫\n`;
report += ` 方位:中 | 五行:土\n`;
report += ` 财:平 | 健康:吉 | 事业:平\n\n`;
continue;
}
const info = baGuaDirection[position] || {};
const jiXiong = jiXiongDirections[position] || {};
report += ` star → info.direction || position\n`;
report += ` 方位:info.direction | 五行:info.element\n`;
report += ` 财:jiXiong.wealth | 健康:jiXiong.health | 事业:jiXiong.career\n\n`;
}
// 财位分析
const wealthPositions = analyzeWealthPosition(bazi, year);
report += `
💰 财位分析
`;
wealthPositions.forEach(pos => {
report += `【pos.type】\n`;
report += ` 位置:pos.location\n`;
report += ` 说明:pos.description\n\n`;
});
// 颜色建议
const colorAdvice = generateColorAdvice(dayStem);
report += `
🎨 幸运颜色
幸运色:colorAdvice.lucky.join('、')
忌用色:colorAdvice.avoid.join('、')
原因:colorAdvice.reason
`;
// 方位分析
report += `
🧭 各方位吉凶
`;
for (const [gua, info] of Object.entries(baGuaDirection)) {
const jx = jiXiongDirections[gua];
if (jx) {
report += `【info.direction】gua\n`;
report += ` 财:jx.wealth | 健康:jx.health | 事业:jx.career | 感情:jx.love\n`;
report += ` 布置建议:info.trait\n\n`;
}
}
// 办公室风水
const office = analyzeOffice();
report += `
💼 办公室风水
理想工位:office.desk.ideal
宜朝向:office.desk.direction
忌讳:office.desk.avoid
财位布置:office.wealth.tips.join('、')
事业位:office.career.tips.join('、')
`;
// 综合建议
report += `
💡 综合建议
1. 财位保持整洁,避免堆放杂物
2. 门口保持畅通,气流流通
3. 每日开窗通风,引入新鲜气场
4. 根据今日幸运色选择穿着或装饰
5. 流年不利方位可用水景或绿植化解
`;
return report;
}
// 主入口
const args = process.argv.slice(2);
if (args[0] === '--help' || args[0] === '-h') {
console.log(`
🏠 风水分析
用法:
node fengshui.js # 基础分析
node fengshui.js <八字> # 带八字分析
node fengshui.js <八字> <年份> # 指定年份
示例:
node fengshui.js
node fengshui.js 戊子
node fengshui.js 戊子 2026
`);
} else {
const bazi = args[0] || '';
const year = parseInt(args[1]) || new Date().getFullYear();
console.log(generateFengShuiReport(bazi, year));
}
module.exports = {
analyzeWealthPosition,
generateColorAdvice,
getFlyingStars,
analyzeOffice
};
FILE:scripts/jieqi.js
#!/usr/bin/env node
/**
* 节气精确计算模块
* 基于太阳黄道经度(简化 VSOP87),精度 ±15 分钟,对应到日期误差 < 1 天
*/
const DEG = Math.PI / 180;
/**
* 计算给定儒略日的太阳黄道经度(度)
* 来源:Jean Meeus《Astronomical Algorithms》第27章
*/
function sunLongitude(jd) {
const T = (jd - 2451545.0) / 36525;
const M = (357.52911 + 35999.05029 * T - 0.0001537 * T * T) * DEG;
const L0 = 280.46646 + 36000.76983 * T + 0.0003032 * T * T;
const C =
(1.914602 - 0.004817 * T - 0.000014 * T * T) * Math.sin(M) +
(0.019993 - 0.000101 * T) * Math.sin(2 * M) +
0.000289 * Math.sin(3 * M);
return ((L0 + C) % 360 + 360) % 360;
}
/**
* 牛顿迭代:求太阳黄道经度恰好为 targetLon 时的儒略日
*/
function jdeAtLongitude(year, targetLon) {
// 初始估算:以平均运动推算
let jd = 2451545.0 + (year - 2000 + ((targetLon - 280.46 + 360) % 360) / 360) * 365.2422;
for (let i = 0; i < 50; i++) {
let diff = ((targetLon - sunLongitude(jd) + 540) % 360) - 180;
if (Math.abs(diff) < 1e-6) break;
jd += (diff / 360) * 365.2422;
}
return jd;
}
/**
* 儒略日 → 北京时间(UTC+8)日期
*/
function jdToCST(jde) {
const jd = jde + 8 / 24; // 转 UTC+8
const z = Math.floor(jd + 0.5);
let a = z;
if (z >= 2299161) {
const alpha = Math.floor((z - 1867216.25) / 36524.25);
a = z + 1 + alpha - Math.floor(alpha / 4);
}
const b = a + 1524;
const c = Math.floor((b - 122.1) / 365.25);
const d = Math.floor(365.25 * c);
const e = Math.floor((b - d) / 30.6001);
const day = b - d - Math.floor(30.6001 * e);
const month = e < 14 ? e - 1 : e - 13;
const yr = month > 2 ? c - 4716 : c - 4715;
return { year: yr, month, day };
}
// ── 12 个月建节气(定义月柱边界)─────────────────────────────────────────
// 名称 黄道经度 对应日历月
const MONTH_JIEQI = [
{ name: '小寒', lon: 285, calMonth: 1 },
{ name: '立春', lon: 315, calMonth: 2 },
{ name: '惊蛰', lon: 345, calMonth: 3 },
{ name: '清明', lon: 15, calMonth: 4 },
{ name: '立夏', lon: 45, calMonth: 5 },
{ name: '芒种', lon: 75, calMonth: 6 },
{ name: '小暑', lon: 105, calMonth: 7 },
{ name: '立秋', lon: 135, calMonth: 8 },
{ name: '白露', lon: 165, calMonth: 9 },
{ name: '寒露', lon: 195, calMonth: 10 },
{ name: '立冬', lon: 225, calMonth: 11 },
{ name: '大雪', lon: 255, calMonth: 12 },
];
// 未过当月节气 → 属于上个节气月(寅月=1, 卯月=2, ..., 丑月=12)
const LUNAR_BEFORE = [11, 12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// 已过当月节气 → 属于当月节气月
const LUNAR_AFTER = [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
// 简单 LRU 缓存,避免重复计算
const _cache = new Map();
function cachedJdeAtLon(year, lon) {
const key = `year:lon`;
if (!_cache.has(key)) _cache.set(key, jdToCST(jdeAtLongitude(year, lon)));
return _cache.get(key);
}
/**
* 给定阳历日期,返回八字月柱的节气月序号
* 1 = 寅月(正月,立春后)
* 2 = 卯月(惊蛰后)
* ...
* 12 = 丑月(小寒后)
*/
function getLunarMonth(year, month, day) {
const jq = MONTH_JIEQI[month - 1];
const { day: jqDay } = cachedJdeAtLon(year, jq.lon);
return day >= jqDay ? LUNAR_AFTER[month - 1] : LUNAR_BEFORE[month - 1];
}
/**
* 判断给定日期是否已过立春(用于年柱计算)
* 立春 = 黄道 315°
*/
function isAfterLiChun(year, month, day) {
if (month > 2) return true;
if (month < 2) return false;
const { day: liChunDay } = cachedJdeAtLon(year, 315);
return day >= liChunDay;
}
/**
* 获取指定年份某节气的北京时间日期(供外部查询)
* @param {number} year
* @param {string} name 节气名称,如 '立春'
* @returns {{ year, month, day }}
*/
function getJieQiDate(year, name) {
const jq = MONTH_JIEQI.find(j => j.name === name);
if (!jq) throw new Error(`未知节气: name`);
return cachedJdeAtLon(year, jq.lon);
}
// ── 命令行调试模式 ───────────────────────────────────────────────────────
if (require.main === module) {
const year = parseInt(process.argv[2]) || new Date().getFullYear();
console.log(`\nyear年 十二月建节气(北京时间)\n'─'.repeat(30)`);
for (const jq of MONTH_JIEQI) {
const d = cachedJdeAtLon(year, jq.lon);
console.log(` jq.name d.year-String(d.month).padStart(2,'0')-String(d.day).padStart(2,'0')`);
}
}
module.exports = { getLunarMonth, isAfterLiChun, getJieQiDate };
FILE:scripts/liuyao.js
#!/usr/bin/env node
/**
* 六爻预测脚本
* 模拟三枚铜钱摇六次成卦
* 输入:6组阴阳信息(每组3个0或1,1=阳面)
*/
const yaoNames = ['初爻', '二爻', '三爻', '四爻', '五爻', '上爻'];
// 八卦对应
const baGua = {
'乾': '☰', '兑': '☱', '离': '☲', '震': '☳',
'巽': '☴', '坎': '☵', '艮': '☶', '坤': '☷'
};
// 八卦属性
const baGuaInfo = {
'乾': { element: '金', direction: '西北', trait: '刚健' },
'兑': { element: '金', direction: '西', trait: '喜悦' },
'离': { element: '火', direction: '南', trait: '明亮' },
'震': { element: '木', direction: '东', trait: '震动' },
'巽': { element: '木', direction: '东南', trait: '入' },
'坎': { element: '水', direction: '北', trait: '险陷' },
'艮': { element: '土', direction: '东北', trait: '止' },
'坤': { element: '土', direction: '西南', trait: '柔顺' }
};
// 六亲
const liuQin = ['父母', '兄弟', '子孙', '妻财', '官鬼', '子孙', '妻财', '官鬼'];
// 地支藏干(简化)
const zangGan = {
'子': ['癸'], '丑': ['己', '癸', '辛'], '寅': ['甲', '丙', '戊'],
'卯': ['乙'], '辰': ['戊', '乙', '癸'], '巳': ['丙', '庚', '戊'],
'午': ['丁', '己'], '未': ['己', '丁', '乙'], '申': ['庚', '壬', '戊'],
'酉': ['辛'], '戌': ['戊', '辛', '丁'], '亥': ['壬', '甲']
};
/**
* 抛铜钱模拟
* 3个铜钱,正面=阳,背面=阴
* 3正=老阳(变阴),2正1背=少阳(阳)
* 3背=老阴(变阳),2背1正=少阴(阴)
*/
function tossCoin() {
// 三枚铜钱各自独立:正面(1)=阳,背面(0)=阴
const heads = [0, 1, 2].reduce((sum) => sum + (Math.random() < 0.5 ? 1 : 0), 0);
if (heads === 3) return '阳动'; // 老阳(三正,变爻)
if (heads === 2) return '阴'; // 少阴(二正一背,不变)
if (heads === 1) return '阳'; // 少阳(一正二背,不变)
return '阴动'; // 老阴(三背,变爻)
}
/**
* 模拟完整六次摇卦
*/
function simulateCoins() {
const results = [];
for (let i = 0; i < 6; i++) {
results.push(tossCoin());
}
return results;
}
/**
* 从输入解析卦象
* 输入格式:六个0/1/2/3的数字
* 0=少阳(阳不动),1=少阴(阴不动),2=老阳(动),3=老阴(动)
*/
function parseCoins(input) {
const results = [];
for (const char of input) {
const num = parseInt(char);
if (num === 0 || num === 1) {
// 不动爻
results.push(num === 0 ? '阳' : '阴');
} else if (num === 2) {
// 老阳变阴
results.push('阳动');
} else if (num === 3) {
// 老阴变阳
results.push('阴动');
}
}
return results;
}
/**
* 根据地支找六亲
*/
function findLiuQin(zhi, riGan) {
const riEl = getElement(riGan);
const zhiEl = getElement(zhi);
// 五行生克
if (riEl === '木') {
if (zhiEl === '木') return '比肩';
if (zhiEl === '火') return '食神';
if (zhiEl === '土') return '偏财';
if (zhiEl === '金') return '官鬼';
if (zhiEl === '水') return '印绶';
} else if (riEl === '火') {
if (zhiEl === '火') return '比肩';
if (zhiEl === '土') return '食神';
if (zhiEl === '金') return '偏财';
if (zhiEl === '水') return '官鬼';
if (zhiEl === '木') return '印绶';
} else if (riEl === '土') {
if (zhiEl === '土') return '比肩';
if (zhiEl === '金') return '食神';
if (zhiEl === '水') return '偏财';
if (zhiEl === '木') return '官鬼';
if (zhiEl === '火') return '印绶';
} else if (riEl === '金') {
if (zhiEl === '金') return '比肩';
if (zhiEl === '水') return '食神';
if (zhiEl === '木') return '偏财';
if (zhiEl === '火') return '官鬼';
if (zhiEl === '土') return '印绶';
} else if (riEl === '水') {
if (zhiEl === '水') return '比肩';
if (zhiEl === '木') return '食神';
if (zhiEl === '火') return '偏财';
if (zhiEl === '土') return '官鬼';
if (zhiEl === '金') return '印绶';
}
return '无';
}
/**
* 获取五行
*/
function getElement(zhi) {
const elements = {
'子': '水', '丑': '土', '寅': '木', '卯': '木',
'辰': '土', '巳': '火', '午': '火', '未': '土',
'申': '金', '酉': '金', '戌': '土', '亥': '水'
};
return elements[zhi] || '土';
}
/**
* 生成六爻卦象
*/
function generateLiuYao(coins, riGan, riZhi) {
// 用时间或给定信息确定卦
const date = new Date();
const hour = date.getHours();
// 简化:以下卦+上卦
const gua64 = ['乾', '坤', '屯', '蒙', '需', '讼', '师', '比',
'小畜', '履', '泰', '否', '同人', '大有', '谦', '豫',
'随', '蛊', '临', '观', '噬嗑', '贲', '剥', '复',
'无妄', '大畜', '颐', '大过', '坎', '离', '咸', '恒',
'遁', '大壮', '晋', '明夷', '家人', '睽', '蹇', '解',
'损', '益', '夬', '姤', '萃', '升', '困', '井',
'革', '鼎', '震', '艮', '渐', '归妹', '丰', '旅',
'巽', '兑', '涣', '节', '中孚', '小过', '既济', '未济'];
// 动爻组合确定卦
const dongCount = coins.filter(c => c.includes('动')).length;
const guaIndex = (dongCount * 6 + coins.filter(c => c === '阳' || c === '阳动').length) % 64;
const guaName = gua64[guaIndex];
// 世应关系(简化)
const shiYing = {
'乾': { shi: '五爻', ying: '二爻' },
'坤': { shi: '二爻', ying: '五爻' },
'屯': { shi: '初爻', ying: '四爻' },
'蒙': { shi: '三爻', ying: '上爻' },
'default': { shi: '三爻', ying: '上爻' }
};
const sy = shiYing[guaName] || shiYing['default'];
// 构建爻列表
const yaoList = coins.map((c, i) => {
const isYang = c === '阳' || c === '阳动';
const isDong = c.includes('动');
const zhi = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'][i];
const gan = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'][i];
const liuqin = findLiuQin(zhi, riGan);
return {
name: yaoNames[i],
yinYang: isYang ? '阳' : '阴',
dong: isDong ? '动' : '',
zhi,
gan,
liuqin
};
});
return {
guaName,
yaoList,
shiYing: sy,
dongCount
};
}
/**
* 判断吉凶
*/
function judgeFortune(hexagram) {
const { dongCount, yaoList } = hexagram;
let result;
if (dongCount === 0) {
result = '静卦 - 事情稳定,需等待时机';
} else if (dongCount === 1) {
result = '独发 - 一件事主导,专注可成';
} else if (dongCount === 2) {
result = '双动 - 两件事关联,需协调';
} else if (dongCount >= 3) {
result = '多动 - 变数较多,谨慎行事';
}
// 检查动爻的六亲
const dongLiuQin = yaoList.filter(y => y.dong === '动').map(y => y.liuqin);
if (dongLiuQin.includes('官鬼')) {
result += '\n⚠️ 动爻带官鬼 - 谨防小人、压力';
}
if (dongLiuQin.includes('妻财')) {
result += '\n💰 动爻带妻财 - 财运显现或破耗';
}
if (dongLiuQin.includes('子孙')) {
result += '\n🌟 动爻带子孙 - 好事发生,贵人运';
}
if (dongLiuQin.includes('父母')) {
result += '\n📚 动爻带父母 - 文书、合同事宜';
}
return result;
}
/**
* 生成报告
*/
function generateReport(hexagram, question = '占卜事宜') {
const { guaName, yaoList, shiYing, dongCount } = hexagram;
const guaInfo = baGuaInfo[guaName[0]] || baGuaInfo['乾'];
const fortune = judgeFortune(hexagram);
let report = `
🔮 【六爻预测】
📋 占卜信息
事项:question
动爻数:dongCount个
🎴 卦象
卦名:guaName
卦性:guaInfo.trait
📊 世应关系
世爻:shiYing.shi
应爻:shiYing.ying
📜 六爻排列(从下至上)
`;
yaoList.reverse().forEach((y, i) => {
const symbol = y.yinYang === '阳' ? '━━' : ' ━ ';
const dongSymbol = y.dong === '动' ? ' ◯' : ' ';
report += ` y.name symboldongSymbol y.zhiy.gan y.liuqin\n`;
});
report += `
⚖️ 吉凶判断
fortune
💡 建议
dongCount === 1 ? '专注一事,把握独发之机' :
'多事并行,量力而行'
`;
return report;
}
// 主入口
const args = process.argv.slice(2);
if (args[0] === '--help' || args[0] === '-h') {
console.log(`
六爻预测
用法:
node liuyao.js # 模拟摇卦
node liuyao.js 010203 # 指定6个爻(0=阳不动,1=阴不动,2=阳动,3=阴动)
node liuyao.js 010203 婚姻 # 指定爻+问题
示例:
node liuyao.js
node liuyao.js 012013 事业
`);
} else if (args.length >= 1 && /^\d{6}$/.test(args[0])) {
// 指定爻象
const coins = parseCoins(args[0]);
const question = args[1] || '占卜事宜';
const riGan = '戊'; // 简化处理
const riZhi = '子';
const hexagram = generateLiuYao(coins, riGan, riZhi);
console.log(generateReport(hexagram, question));
} else if (args.length >= 2 && /^\d{6}$/.test(args[0])) {
// 爻象 + 问题
const coins = parseCoins(args[0]);
const question = args.slice(1).join(' ');
const riGan = '戊';
const riZhi = '子';
const hexagram = generateLiuYao(coins, riGan, riZhi);
console.log(generateReport(hexagram, question));
} else {
// 模拟摇卦
console.log('🎲 摇卦中...\n');
const coins = simulateCoins();
const question = args.join(' ') || '占卜事宜';
const riGan = '戊';
const riZhi = '子';
const hexagram = generateLiuYao(coins, riGan, riZhi);
console.log(`📍 摇得:[coins.join(' ')]\n`);
console.log(generateReport(hexagram, question));
}
FILE:scripts/marriage.js
#!/usr/bin/env node
/**
* 合婚分析脚本
* 根据八字分析两个人的婚姻适配度
*/
const fs = require('fs');
const path = require('path');
const PROFILES_DIR = path.join(__dirname, '../data/profiles');
/**
* 加载用户档案
*/
function loadProfile(userId) {
const filePath = path.join(PROFILES_DIR, `userId.json`);
if (!fs.existsSync(filePath)) {
return null;
}
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
/**
* 天干信息
*/
const tianGan = {
'甲': { element: '木', yin: '阳' },
'乙': { element: '木', yin: '阴' },
'丙': { element: '火', yin: '阳' },
'丁': { element: '火', yin: '阴' },
'戊': { element: '土', yin: '阳' },
'己': { element: '土', yin: '阴' },
'庚': { element: '金', yin: '阳' },
'辛': { element: '金', yin: '阴' },
'壬': { element: '水', yin: '阳' },
'癸': { element: '水', yin: '阴' }
};
/**
* 地支信息
*/
const diZhi = {
'子': { element: '水', animal: '鼠' },
'丑': { element: '土', animal: '牛' },
'寅': { element: '木', animal: '虎' },
'卯': { element: '木', animal: '兔' },
'辰': { element: '土', animal: '龙' },
'巳': { element: '火', animal: '蛇' },
'午': { element: '火', animal: '马' },
'未': { element: '土', animal: '羊' },
'申': { element: '金', animal: '猴' },
'酉': { element: '金', animal: '鸡' },
'戌': { element: '土', animal: '狗' },
'亥': { element: '水', animal: '猪' }
};
/**
* 五行生克关系
*/
const wuXingRelations = {
'木': { sheng: '火', ke: '土' },
'火': { sheng: '土', ke: '金' },
'土': { sheng: '金', ke: '水' },
'金': { sheng: '水', ke: '木' },
'水': { sheng: '木', ke: '火' }
};
/**
* 日主五行关系分析
*/
function analyzeDayMasterCompatibility(bazi1, bazi2) {
const day1 = bazi1.charAt(0);
const day2 = bazi2.charAt(0);
const el1 = tianGan[day1]?.element;
const el2 = tianGan[day2]?.element;
if (!el1 || !el2) return { score: 0, reason: '日主五行无法确定' };
let relation = '';
let score = 50;
if (wuXingRelations[el1]?.sheng === el2) {
relation = `day1(el1)生 day2(el2)`;
score = 75;
} else if (wuXingRelations[el1]?.ke === el2) {
relation = `day1(el1)克 day2(el2)`;
score = 45;
} else if (wuXingRelations[el2]?.sheng === el1) {
relation = `day2(el2)生 day1(el1)`;
score = 70;
} else if (wuXingRelations[el2]?.ke === el1) {
relation = `day2(el2)克 day1(el1)`;
score = 40;
} else if (el1 === el2) {
relation = `日主同属el1,比和`;
score = 60;
}
return { score, relation, el1, el2, day1, day2 };
}
/**
* 纳音五行分析
*/
function analyzeNaYinCompatibility(bazi1, bazi2) {
// 从八字中提取年柱
const year1 = bazi1.split(' ')[0] || '';
const year2 = bazi2.split(' ')[0] || '';
const naYinMap = {
'甲子': '海中金', '乙丑': '海中金',
'丙寅': '炉中火', '丁卯': '炉中火',
'戊辰': '大林木', '己巳': '大林木',
'庚午': '路旁土', '辛未': '路旁土',
'壬申': '剑锋金', '癸酉': '剑锋金',
'甲戌': '山头火', '乙亥': '山头火',
'丙子': '漳下水', '丁丑': '漳下水',
'戊寅': '城头土', '己卯': '城头土',
'庚辰': '白蜡金', '辛巳': '白蜡金',
'壬午': '杨柳木', '癸未': '杨柳木',
'甲申': '井泉水', '乙酉': '井泉水',
'丙戌': '屋上土', '丁亥': '屋上土',
'戊子': '霹雳火', '己丑': '霹雳火',
'庚寅': '松柏木', '辛卯': '松柏木',
'壬辰': '长流水', '癸巳': '长流水',
'甲午': '沙中金', '乙未': '沙中金',
'丙申': '山下火', '丁酉': '山下火',
'戊戌': '平地木', '己亥': '平地木',
'庚子': '壁上土', '辛丑': '壁上土',
'壬寅': '金箔金', '癸卯': '金箔金',
'甲辰': '覆灯火', '乙巳': '覆灯火',
'丙午': '天河水', '丁未': '天河水',
'戊申': '大驿土', '己酉': '大驿土',
'庚戌': '钗钏金', '辛亥': '钗钏金',
'壬子': '桑柘木', '癸丑': '桑柘木',
'甲寅': '大溪水', '乙卯': '大溪水',
'丙辰': '沙中土', '丁巳': '沙中土',
'戊午': '天上火', '己未': '天上火',
'庚申': '石榴木', '辛酉': '石榴木',
'壬戌': '大海水', '癸亥': '大海水'
};
const ny1 = naYinMap[year1] || '未知';
const ny2 = naYinMap[year2] || '未知';
return { ny1, ny2 };
}
/**
* 地支合冲分析
*/
function analyzeDiZhiRelation(zhi1, zhi2) {
const heMap = {
'子丑': '六合', '寅亥': '六合', '卯戌': '六合',
'辰酉': '六合', '巳申': '六合', '午未': '六合',
'寅午': '三合', '午戌': '三合', '子辰': '三合',
'申子': '三合', '巳酉': '三合', '丑亥': '三合',
'卯卯': '比和', '午午': '比和', '酉酉': '比和'
};
const chongMap = {
'子午': '子午相冲', '丑未': '丑未相冲',
'寅申': '寅申相冲', '卯酉': '卯酉相冲',
'辰戌': '辰戌相冲', '巳亥': '巳亥相冲'
};
const key1 = zhi1 + zhi2;
const key2 = zhi2 + zhi1;
let result = '';
if (heMap[key1]) result = heMap[key1];
else if (heMap[key2]) result = heMap[key2];
else if (chongMap[key1]) result = chongMap[key1];
else if (chongMap[key2]) result = chongMap[key2];
else result = '无特殊合冲';
return result;
}
/**
* 天干合分析
*/
function analyzeTianGanHe(bazi1, bazi2) {
const day1 = bazi1.charAt(0);
const day2 = bazi2.charAt(0);
const heTian = {
'甲己': '甲己合(中正之合)', '乙庚': '乙庚合(仁义之合)',
'丙辛': '丙辛合(威制之合)', '丁壬': '丁壬合(淫昵之合)',
'戊癸': '戊癸合(无情之合)'
};
const key = day1 + day2;
const key2 = day2 + day1;
return heTian[key] || heTian[key2] || '日主无天干相合';
}
/**
* 计算综合评分
*/
function calculateOverallScore(dayScore, heResult, dzResult) {
let score = dayScore;
if (dzResult.includes('六合')) score += 15;
else if (dzResult.includes('三合')) score += 10;
else if (dzResult.includes('比和')) score += 5;
else if (dzResult.includes('相冲')) score -= 15;
if (heResult.includes('中正') || heResult.includes('仁义')) score += 10;
else if (heResult.includes('淫昵')) score -= 5;
else if (heResult.includes('无情')) score -= 10;
return Math.max(0, Math.min(100, score));
}
/**
* 生成评价
*/
function getEvaluation(score) {
if (score >= 85) return { grade: '★★★★★', level: '极佳', desc: '天作之合,百年好合' };
if (score >= 70) return { grade: '★★★★☆', level: '优秀', desc: '缘分深厚,婚配吉祥' };
if (score >= 55) return { grade: '★★★☆☆', level: '中等', desc: '缘分平平,需要磨合' };
if (score >= 40) return { grade: '★★☆☆☆', level: '偏低', desc: '需要多沟通包容' };
return { grade: '★☆☆☆☆', level: '较差', desc: '婚配欠佳,需谨慎' };
}
/**
* 生成分析报告
*/
function generateReport(name1, bazi1, name2, bazi2) {
const dayAnalysis = analyzeDayMasterCompatibility(bazi1, bazi2);
const naYin = analyzeNaYinCompatibility(bazi1, bazi2);
// 解析八字(格式:"庚午 辛巳 庚辰 癸未")
const parseBazi = (bazi) => {
const parts = bazi.split(' ');
return {
year: parts[0] || '',
month: parts[1] || '',
day: parts[2] || '',
hour: parts[3] || '',
yearZhi: (parts[0] || '').charAt(1),
monthZhi: (parts[1] || '').charAt(1),
dayZhi: (parts[2] || '').charAt(1),
hourZhi: (parts[3] || '').charAt(1)
};
};
const p1 = parseBazi(bazi1);
const p2 = parseBazi(bazi2);
const zhiPairs = [
{ name: '年柱', z1: p1.yearZhi, z2: p2.yearZhi },
{ name: '月柱', z1: p1.monthZhi, z2: p2.monthZhi },
{ name: '日柱', z1: p1.dayZhi, z2: p2.dayZhi },
{ name: '时柱', z1: p1.hourZhi, z2: p2.hourZhi }
];
const heResult = analyzeTianGanHe(bazi1, bazi2);
const dzResults = zhiPairs.map(p => ({
name: p.name,
z1: p.z1,
z2: p.z2,
relation: analyzeDiZhiRelation(p.z1, p.z2)
}));
const overallScore = calculateOverallScore(dayAnalysis.score, heResult, dzResults[0].relation);
const evaluation = getEvaluation(overallScore);
let report = `
💕 【合婚分析报告】
━━━━━━━━━━━━━━━━━━━━
👤 男方:name1
八字:bazi1
日主:dayAnalysis.day1(dayAnalysis.el1)
👤 女方:name2
八字:bazi2
日主:dayAnalysis.day2(dayAnalysis.el2)
━━━━━━━━━━━━━━━━━━━━
📊 合婚评分
evaluation.grade evaluation.level
综合得分:overallScore分(满分100)
━━━━━━━━━━━━━━━━━━━━
🔮 详细分析
【日主关系】
dayAnalysis.relation
overallScore >= 40 ? '⚠️ 日主关系一般' : '❌ 日主关系欠佳'
【纳音五行】
男:naYin.ny1
女:naYin.ny2
'📝 纳音不同,需注意调和'
【天干相合】
heResult
【地支关系】
`;
dzResults.forEach(p => {
report += ` p.name(p.z1 vs p.z2):p.relation\n`;
});
report += `
━━━━━━━━━━━━━━━━━━━━
💡 综合建议
`;
if (overallScore >= 70) {
report += `
🎉 恭喜!你们的八字非常相配。
• 日主关系和谐
• '可多培养共同兴趣'
• '虽有冲克,但可化解'
💕 婚姻展望:
婚后生活较和谐稳定,双方能互相理解支持。
`;
} else if (overallScore >= 50) {
report += `
📝 中等缘分,需要用心经营。
• 日主关系'需加强'
• 建议多沟通,了解彼此需求
• 注意dzResults.find(p => p.relation.includes('冲'))?.name || '相关'地支的影响
💡 婚姻建议:
婚后需要双方共同努力,多包容理解。
`;
} else {
report += `
⚠️ 缘分较弱,需要谨慎对待。
• 存在一定婚配障碍
• 建议深入了解后再做结婚决定
• 如坚持在一起,需要更多磨合
⚠️ 注意事项:
重点关注事业、感情沟通方面。
`;
}
return report;
}
// 主入口
const args = process.argv.slice(2);
if (args.length < 2) {
console.log(`
💕 合婚分析
用法:
node marriage.js <userId1> <userId2>
node marriage.js <name1> <bazi1> <name2> <bazi2>
示例:
node marriage.js 111111 222222
node marriage.js 张三 "甲子 乙丑 丙寅 丁卯" 李四 "庚午 辛巳 庚辰 癸未"
`);
process.exit(1);
}
let name1, bazi1, name2, bazi2;
// 判断输入模式:2个参数=从档案加载,4个参数=直接输入
if (args.length === 2) {
const profile1 = loadProfile(args[0]);
const profile2 = loadProfile(args[1]);
if (!profile1 || !profile2) {
console.log('❌ 未找到用户档案');
process.exit(1);
}
name1 = profile1.name;
name2 = profile2.name;
bazi1 = profile1.bazi.year + ' ' + profile1.bazi.month + ' ' + profile1.bazi.day + ' ' + profile1.bazi.hour;
bazi2 = profile2.bazi.year + ' ' + profile2.bazi.month + ' ' + profile2.bazi.day + ' ' + profile2.bazi.hour;
console.log('📋 从档案加载\n');
} else {
name1 = args[0];
bazi1 = args[1];
name2 = args[2];
bazi2 = args[3];
}
console.log(generateReport(name1, bazi1, name2, bazi2));
module.exports = { generateReport, analyzeDayMasterCompatibility };
FILE:scripts/meihua.js
#!/usr/bin/env node
/**
* 梅花易数占卜脚本
* 支持:报数起卦、时间起卦、方位起卦
*/
const tianGan = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
const diZhi = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
// 先天八卦数
const baGuaNum = {
'乾': 1, '兑': 2, '离': 3, '震': 4,
'巽': 5, '坎': 6, '艮': 7, '坤': 8
};
// 八卦属性
const baGuaInfo = {
'乾': { element: '金', symbol: '☰', trait: '刚健', color: '白色', direction: '西北' },
'兑': { element: '金', symbol: '☱', trait: '喜悦', color: '白色', direction: '西' },
'离': { element: '火', symbol: '☲', trait: '明亮', color: '红色', direction: '南' },
'震': { element: '木', symbol: '☳', trait: '震动', color: '青色', direction: '东' },
'巽': { element: '木', symbol: '☴', trait: '入', color: '绿色', direction: '东南' },
'坎': { element: '水', symbol: '☵', trait: '险陷', color: '黑色', direction: '北' },
'艮': { element: '土', symbol: '☶', trait: '阻止', color: '黄色', direction: '东北' },
'坤': { element: '土', symbol: '☷', trait: '柔顺', color: '黄色', direction: '西南' }
};
// 64卦象(简化版)
const hexagrams = {
'11': { name: '乾为天', gua: '乾', trait: '元亨利贞', meaning: '大吉大利' },
'12': { name: '天地否', gua: '否', trait: '不交不通', meaning: '诸事不顺' },
'13': { name: '天风姤', gua: '姤', trait: '遇也', meaning: '偶遇机缘' },
'21': { name: '地天泰', gua: '泰', trait: '天地交泰', meaning: '万事亨通' },
'22': { name: '坤为地', gua: '坤', trait: '元亨利牝马之贞', meaning: '柔顺有利' },
'31': { name: '火天大有', gua: '大有', trait: '元亨', meaning: '收获丰富' },
'32': { name: '火地晋', gua: '晋', trait: '康候用锡马', meaning: '晋升发展' },
'33': { name: '雷天大壮', gua: '大壮', trait: '利贞', meaning: '气势正盛' },
'41': { name: '风地观', gua: '观', trait: '盥而不荐', meaning: '观察时机' },
'42': { name: '风雷益', gua: '益', trait: '利有攸往', meaning: '受益匪浅' },
'51': { name: '水地比', gua: '比', trait: '吉原筮元永贞', meaning: '亲和友善' },
'52': { name: '水山蹇', gua: '蹇', trait: '利西南不利东北', meaning: '艰难险阻' },
'61': { name: '山地剥', gua: '剥', trait: '不利有攸往', meaning: '需要隐忍' },
'62': { name: '山地艮', gua: '艮', trait: '止也', meaning: '适可而止' },
'71': { name: '泽地萃', gua: '萃', trait: '利见大人', meaning: '人才汇聚' },
'72': { name: '泽山咸', gua: '咸', trait: '亨利贞', meaning: '感情顺利' },
'81': { name: '雷地豫', gua: '豫', trait: '利建侯行师', meaning: '安乐祥和' },
'82': { name: '雷风恒', gua: '恒', trait: '亨无咎利贞', meaning: '恒久稳定' }
};
// 简化版64卦映射(取常见卦)
const simpleHexagrams = {
'11': '乾为天', '12': '天雷无妄', '13': '天风姤', '14': '天山遁',
'15': '天地否', '16': '风地观', '17': '山地剥', '18': '坤为地',
'21': '地天泰', '22': '地雷复', '23': '地泽临', '24': '地天决',
'25': '地风升', '26': '地火明夷', '27': '地山谦', '28': '地水师',
'31': '火天大有', '32': '火雷噬嗑', '33': '火风鼎', '34': '火水未济',
'35': '火山旅', '36': '风水涣', '37': '山火贲', '38': '山水蒙',
'41': '风天小畜', '42': '风雷益', '43': '风泽中孚', '44': '风山渐',
'45': '风地观', '46': '风火家人', '47': '风雷恒', '48': '风水困',
'51': '水天需', '52': '水雷屯', '53': '水泽节', '54': '水山蹇',
'55': '水地比', '56': '水火既济', '57': '水风井', '58': '水火未济',
'61': '山天大畜', '62': '山雷颐', '63': '山泽损', '64': '山风蛊',
'71': '泽天夬', '72': '泽雷随', '73': '泽火革', '74': '泽水困',
'75': '泽地萃', '76': '泽山咸', '77': '雷天大壮', '78': '雷泽归妹',
'81': '雷地豫', '82': '雷水解', '83': '雷风恒', '84': '雷山小过',
'85': '雷火丰', '86': '雷电噬嗑', '87': '雷风恒', '88': '雷山小过'
};
/**
* 报数起卦
* 报3个数字,分别对应上卦、下卦、动爻
*/
function numbersToHexagram(nums) {
if (nums.length < 3) {
throw new Error('请报3个数字');
}
const num1 = parseInt(nums[0]) % 8 || 8;
const num2 = parseInt(nums[1]) % 8 || 8;
const dongYao = parseInt(nums[2]) % 6 || 6;
const guaNames = ['乾', '兑', '离', '震', '巽', '坎', '艮', '坤'];
const shangGua = guaNames[(num1 - 1) % 8];
const xiaGua = guaNames[(num2 - 1) % 8];
return {
shangGua,
xiaGua,
dongYao,
hexagram: simpleHexagrams[`num1num2`] || `shangGuaxiaGua`,
num1, num2, dongYao
};
}
/**
* 时间起卦
*/
function timeToHexagram(date = new Date()) {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const hour = date.getHours();
// 报数法:年月日时相加
const num1 = (year + month + day) % 8 || 8;
const num2 = (month + day + hour) % 8 || 8;
const num3 = (year + month + day + hour) % 6 || 6;
return numbersToHexagram([num1, num2, num3]);
}
/**
* 方位起卦
* 东南西北对应 1-4
*/
function directionToHexagram(directions) {
if (directions.length < 2) {
throw new Error('请提供2个方位');
}
const dirMap = {
'东': 4, '南': 9, '西': 2, '北': 6,
'东南': 5, '东北': 7, '西南': 8, '西北': 1
};
const num1 = dirMap[directions[0]] || 1;
const num2 = dirMap[directions[1]] || 2;
return numbersToHexagram([num1, num2, 3]);
}
/**
* 判断体用关系
*/
function getTiYong(hexagram, shangGua, xiaGua) {
// 先天八卦:乾1兑2离3震4巽5坎6艮7坤8
// 数大为用,数小为体
const num1 = baGuaNum[shangGua];
const num2 = baGuaNum[xiaGua];
// 上卦数 > 下卦数 → 上为用,下为体
// 上卦数 < 下卦数 → 下为用,上为体
let tiYong;
if (num1 > num2) {
tiYong = { ti: xiaGua, yong: shangGua, gua: '上卦为用,下卦为体' };
} else if (num1 < num2) {
tiYong = { ti: shangGua, yong: xiaGua, gua: '下卦为用,上卦为体' };
} else {
tiYong = { ti: shangGua, yong: xiaGua, gua: '体用比和' };
}
return tiYong;
}
/**
* 五行生克判断
*/
function getElementRelation(ti, yong) {
const elements = {
'乾': '金', '兑': '金', '离': '火', '震': '木',
'巽': '木', '坎': '水', '艮': '土', '坤': '土'
};
const tiEl = elements[ti];
const yongEl = elements[yong];
// 五行相生:木→火→土→金→水→木
// 五行相克:木→土→水→火→金→木
const relations = {
// 用生体 → 大吉(得生助)
'火木': '用生体 → 大吉', '土火': '用生体 → 大吉', '金土': '用生体 → 大吉',
'水金': '用生体 → 大吉', '木水': '用生体 → 大吉',
// 体生用 → 泄气(有耗散)
'木火': '体生用 → 泄气', '火土': '体生用 → 泄气', '土金': '体生用 → 泄气',
'金水': '体生用 → 泄气', '水木': '体生用 → 泄气',
// 体克用 → 有利(我制对方)
'木土': '体克用 → 有利', '土水': '体克用 → 有利', '水火': '体克用 → 有利',
'火金': '体克用 → 有利', '金木': '体克用 → 有利',
// 用克体 → 凶(对方克我)
'土木': '用克体 → 凶', '水土': '用克体 → 凶', '火水': '用克体 → 凶',
'金火': '用克体 → 凶', '木金': '用克体 → 凶'
};
const key = tiEl + yongEl;
let result;
if (tiEl === yongEl) {
result = '体用比和 → 平稳';
} else {
result = relations[key] || '体用相合 → 平稳';
}
return { tiEl, yongEl, result };
}
/**
* 动爻分析
*/
function analyzeDongYao(dongYao) {
const yaoNames = ['初爻', '二爻', '三爻', '四爻', '五爻', '上爻'];
return {
position: yaoNames[dongYao - 1] || '上爻',
note: dongYao <= 3 ? '下卦动' : '上卦动'
};
}
/**
* 生成占卜报告
*/
function generateDivinationReport(hexagramData, type, inputData) {
const { shangGua, xiaGua, dongYao } = hexagramData;
const tiYong = getTiYong(hexagramData.hexagram, shangGua, xiaGua);
const elements = getElementRelation(tiYong.ti, tiYong.yong);
const yaoInfo = analyzeDongYao(dongYao);
const shangInfo = baGuaInfo[shangGua] || baGuaInfo['乾'];
const xiaInfo = baGuaInfo[xiaGua] || baGuaInfo['坤'];
// 判断吉凶
let jiXiong;
if (elements.result.includes('大吉') || elements.result.includes('比和')) {
jiXiong = '✅ 吉利';
} else if (elements.result.includes('凶')) {
jiXiong = '❌ 需谨慎';
} else {
jiXiong = '⚠️ 中平';
}
const report = `
🔮 【梅花易数占卜】
📋 占卜信息
类型:type
输入:inputData
🎴 卦象
上卦:shangGua shangInfo.symbol(shangInfo.trait)
下卦:xiaGua xiaInfo.symbol(xiaInfo.trait)
动爻:yaoInfo.position(yaoInfo.note)
📊 卦名
hexagramData.hexagram
⚖️ 体用分析
体卦:tiYong.ti(elements.tiEl气)
用卦:tiYong.yong(elements.yongEl气)
关系:elements.result
jiXiong
🎯 综合判断
shangInfo.direction方向shangInfo.trait,xiaInfo.direction方向xiaInfo.trait
今日shangInfo.color色、xiaInfo.color色利于增强运势
💡 建议
elements.result.includes('凶')
? '宜静不宜动,谨慎行事'
: '循序渐进,稳扎稳打'
`;
return report;
}
// 主入口
const args = process.argv.slice(2);
if (args.length === 0) {
// 默认时间起卦
const result = timeToHexagram(new Date());
console.log(generateDivinationReport(result, '时间起卦', '当前时间'));
} else if (args[0] === '--help' || args[0] === '-h') {
console.log(`
梅花易数占卜
用法:
node meihua.js # 时间起卦
node meihua.js 数字1 数字2 数字3 # 报数起卦
node meihua.js 东 南 # 方位起卦
示例:
node meihua.js 3 5 2
node meihua.js 东 南
`);
} else if (args.length === 1 && ['东', '南', '西', '北', '东南', '东北', '西南', '西北'].includes(args[0])) {
// 单方位 → 用另一个默认方位
const result = directionToHexagram([args[0], '中']);
console.log(generateDivinationReport(result, '方位起卦', args[0]));
} else if (args.length === 2 && ['东', '南', '西', '北', '东南', '东北', '西南', '西北'].includes(args[0])) {
// 两个方位
const result = directionToHexagram(args);
console.log(generateDivinationReport(result, '方位起卦', args.join('-')));
} else if (!isNaN(args[0])) {
// 报数起卦
try {
const result = numbersToHexagram(args);
console.log(generateDivinationReport(result, '报数起卦', args.join('-')));
} catch (e) {
console.error('错误:', e.message);
process.exit(1);
}
} else {
// 时间起卦
const result = timeToHexagram(new Date());
console.log(generateDivinationReport(result, '时间起卦', '当前时间'));
}
FILE:scripts/morning-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('invalid userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('illegal path');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{};}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
console.log(`今天是weekday(date)。请运行今日运势推送:node scripts/daily-fortune.js date。输出今日综合运势(事业/财运/感情/健康)、幸运颜色/数字/方位、今日宜忌、吉时,以及每日命理一言。中文输出,简洁精准。`);
FILE:scripts/preference-tracker.js
#!/usr/bin/env node
/**
* 用户偏好学习系统
* 基于用户互动记录,动态调整关注领域权重
*
* 用法(供 Agent 调用):
* node preference-tracker.js record <userId> <topic> [context]
* node preference-tracker.js weights <userId>
* node preference-tracker.js top <userId> [n]
*/
const fs = require('fs');
const path = require('path');
const PROFILES_DIR = path.join(__dirname, '../data/profiles');
// 支持的关注领域
const TOPICS = ['财运', '事业', '感情', '健康', '婚姻', '子女', '官司', '出行', '风水'];
// 互动来源权重倍率
const CONTEXT_MULTIPLIERS = {
explicit_query: 2.0, // 用户主动提问
topic_drill: 1.5, // 用户追问同一话题
morning_push: 0.8, // 晨间推送被消费
evening_push: 0.8, // 晚间推送被消费
};
const DECAY_LAMBDA = 0.05; // 衰减系数,约14天半衰期
const MAX_LOG_SIZE = 500; // 最大记录条数
const MIN_WEIGHT = 0.5; // 进入 focusAreas 的最低权重
const DEFAULT_FOCUS = ['事业', '财运', '健康'];
// ─────────────────────────────────────────────
// 文件 I/O
// ─────────────────────────────────────────────
function loadProfile(userId) {
const fp = path.join(PROFILES_DIR, `userId.json`);
if (!fs.existsSync(fp)) return null;
return JSON.parse(fs.readFileSync(fp, 'utf8'));
}
function saveProfile(userId, profile) {
const fp = path.join(PROFILES_DIR, `userId.json`);
profile.updatedAt = new Date().toISOString().split('T')[0];
fs.writeFileSync(fp, JSON.stringify(profile, null, 2), 'utf8');
}
// ─────────────────────────────────────────────
// 核心算法:指数衰减加权
// ─────────────────────────────────────────────
function _computeWeights(log) {
const now = Date.now();
const totals = {};
TOPICS.forEach(t => { totals[t] = 0; });
for (const entry of (log || [])) {
if (!TOPICS.includes(entry.topic)) continue;
if (!entry.ts || typeof entry.ts !== 'number') continue;
const daysDelta = (now - entry.ts) / 86400000;
const multiplier = CONTEXT_MULTIPLIERS[entry.context] || 1.0;
totals[entry.topic] += multiplier * Math.exp(-DECAY_LAMBDA * daysDelta);
}
return totals;
}
function _normalizeWeights(raw) {
const max = Math.max(...Object.values(raw), 0.001);
const result = {};
for (const [t, w] of Object.entries(raw)) {
result[t] = parseFloat((w / max).toFixed(3));
}
return result;
}
function _sortedTopics(weights) {
return Object.entries(weights)
.sort((a, b) => b[1] - a[1])
.map(([topic, weight]) => ({ topic, weight }));
}
// ─────────────────────────────────────────────
// 公开 API
// ─────────────────────────────────────────────
/**
* 记录一次互动
*/
function recordInteraction(userId, topic, context = 'explicit_query') {
const profile = loadProfile(userId);
if (!profile) return false;
if (!TOPICS.includes(topic)) return false;
if (!profile.interactionLog) profile.interactionLog = [];
profile.interactionLog.push({ ts: Date.now(), topic, context });
// 超出上限时删除最旧的记录
if (profile.interactionLog.length > MAX_LOG_SIZE) {
profile.interactionLog = profile.interactionLog.slice(-MAX_LOG_SIZE);
}
// 重新计算并更新 focusAreas
const raw = _computeWeights(profile.interactionLog);
const normalized = _normalizeWeights(raw);
const top = _sortedTopics(normalized)
.filter(({ weight }) => weight >= MIN_WEIGHT)
.slice(0, 3)
.map(({ topic }) => topic);
if (!profile.preferences) profile.preferences = {};
profile.preferences.focusAreas = top.length > 0 ? top : DEFAULT_FOCUS;
saveProfile(userId, profile);
return true;
}
/**
* 获取所有领域权重(归一化,降序)
*/
function getWeights(userId) {
const profile = loadProfile(userId);
if (!profile) return [];
const raw = _computeWeights(profile.interactionLog || []);
const normalized = _normalizeWeights(raw);
return _sortedTopics(normalized);
}
/**
* 获取 top-n 关注领域(有互动记录用计算结果,否则用 profile 默认值)
*/
function getTopTopics(userId, n = 3) {
const profile = loadProfile(userId);
if (!profile) return DEFAULT_FOCUS.slice(0, n);
const log = profile.interactionLog || [];
if (log.length === 0) {
return (profile.preferences?.focusAreas || DEFAULT_FOCUS).slice(0, n);
}
const raw = _computeWeights(log);
const normalized = _normalizeWeights(raw);
return _sortedTopics(normalized)
.slice(0, n)
.map(({ topic }) => topic);
}
module.exports = { recordInteraction, getWeights, getTopTopics, TOPICS };
// ─────────────────────────────────────────────
// 命令行入口(供 Agent 调用)
// ─────────────────────────────────────────────
if (require.main === module) {
const [cmd, userId, ...rest] = process.argv.slice(2);
if (!cmd || !userId) {
console.log(`
🧠 用户偏好追踪器
用法:
node preference-tracker.js record <userId> <topic> [context]
node preference-tracker.js weights <userId>
node preference-tracker.js top <userId> [n]
topic 可选: TOPICS.join(' | ')
context 可选: explicit_query | topic_drill | morning_push | evening_push
示例:
node preference-tracker.js record 123456 财运 explicit_query
node preference-tracker.js weights 123456
node preference-tracker.js top 123456 3
`);
process.exit(1);
}
switch (cmd) {
case 'record': {
const [topic, context = 'explicit_query'] = rest;
if (!topic) { console.error('缺少 topic 参数'); process.exit(1); }
const ok = recordInteraction(userId, topic, context);
console.log(JSON.stringify({ success: ok, userId, topic, context }));
break;
}
case 'weights': {
const weights = getWeights(userId);
console.log(JSON.stringify({ userId, weights }));
break;
}
case 'top': {
const n = parseInt(rest[0] || '3');
const topics = getTopTopics(userId, n);
console.log(JSON.stringify({ userId, topTopics: topics }));
break;
}
default:
console.error(`未知命令: cmd`);
process.exit(1);
}
}
FILE:scripts/profile.js
#!/usr/bin/env node
/**
* 用户档案管理脚本 - 支持家庭成员
* 保存/读取用户命理数据及家庭成员
*/
const fs = require('fs');
const path = require('path');
const PROFILES_DIR = path.join(__dirname, '../data/profiles');
// 确保目录存在
if (!fs.existsSync(PROFILES_DIR)) {
fs.mkdirSync(PROFILES_DIR, { recursive: true });
}
/**
* 获取用户档案路径
*/
function getProfilePath(userId) {
return path.join(PROFILES_DIR, `userId.json`);
}
/**
* 读取用户档案
*/
function loadProfile(userId) {
const filePath = getProfilePath(userId);
if (!fs.existsSync(filePath)) {
return null;
}
const data = fs.readFileSync(filePath, 'utf8');
return JSON.parse(data);
}
/**
* 保存用户档案
*/
function saveProfile(userId, data) {
const filePath = getProfilePath(userId);
const profile = {
...data,
userId,
updatedAt: new Date().toISOString().split('T')[0]
};
fs.writeFileSync(filePath, JSON.stringify(profile, null, 2), 'utf8');
return profile;
}
/**
* 更新档案字段
*/
function updateProfile(userId, field, value) {
const profile = loadProfile(userId) || {};
// 处理嵌套字段如 "family.spouse.name"
const fields = field.split('.');
let current = profile;
for (let i = 0; i < fields.length - 1; i++) {
if (!current[fields[i]]) {
current[fields[i]] = {};
}
current = current[fields[i]];
}
current[fields[fields.length - 1]] = value;
saveProfile(userId, profile);
console.log(`✅ 已更新: field = value`);
}
/**
* 添加家庭成员
*/
function addFamilyMember(userId, type, name, data = {}) {
const profile = loadProfile(userId);
if (!profile) {
console.log(`❌ 用户档案不存在: userId`);
return;
}
if (!profile.family) {
profile.family = {};
}
const memberData = {
name,
profile: {
birthDate: data.birthDate || '待录入',
birthTime: data.birthTime || '待录入',
birthPlace: data.birthPlace || '',
gender: data.gender || '',
lunarBirth: data.lunarBirth || ''
},
bazi: {
year: data.year || '',
month: data.month || '',
day: data.day || '',
hour: data.hour || '',
dayStem: data.dayStem || '',
zodiac: data.zodiac || '',
sect: data.sect || '晚子时',
source: 'pending'
},
relationship: type,
addedAt: new Date().toISOString().split('T')[0]
};
if (type === 'children') {
if (!profile.family.children) {
profile.family.children = [];
}
profile.family.children.push(memberData);
console.log(`✅ 已添加子女: name`);
} else {
profile.family[type] = memberData;
console.log(`✅ 已添加type: name`);
}
saveProfile(userId, profile);
}
/**
* 添加子女
*/
function addChild(userId, name, birthDate, gender) {
const profile = loadProfile(userId);
if (!profile) {
console.log(`❌ 用户档案不存在: userId`);
return;
}
const child = {
name,
profile: {
birthDate: birthDate || '待录入',
birthTime: '待录入',
birthPlace: '',
gender: gender || '',
lunarBirth: ''
},
bazi: {
year: '',
month: '',
day: '',
hour: '',
source: 'pending'
},
relationship: '子女',
addedAt: new Date().toISOString().split('T')[0]
};
if (!profile.family) profile.family = {};
if (!profile.family.children) profile.family.children = [];
profile.family.children.push(child);
saveProfile(userId, profile);
console.log(`✅ 已添加子女: name (gender || '待定')`);
}
/**
* 列出家庭成员
*/
function listFamilyMembers(userId) {
const profile = loadProfile(userId);
if (!profile) {
console.log(`❌ 用户档案不存在: userId`);
return;
}
console.log(`\n👪 家庭成员列表 (profile.name)\n`);
const { family } = profile;
if (family?.spouse?.name && family.spouse.name !== '配偶') {
console.log(` 👫 配偶: family.spouse.name`);
console.log(` 八字: family.spouse.bazi?.year || '?' family.spouse.bazi?.month || '' family.spouse.bazi?.day || '' family.spouse.bazi?.hour || ''`);
}
if (family?.father?.name && family.father.name !== '父亲') {
console.log(` 👨 父亲: family.father.name`);
console.log(` 八字: family.father.bazi?.year || '?' family.father.bazi?.month || '' family.father.bazi?.day || '' family.father.bazi?.hour || ''`);
}
if (family?.mother?.name && family.mother.name !== '母亲') {
console.log(` 👩 母亲: family.mother.name`);
console.log(` 八字: family.mother.bazi?.year || '?' family.mother.bazi?.month || '' family.mother.bazi?.day || '' family.mother.bazi?.hour || ''`);
}
if (family?.children?.length > 0) {
console.log(` 👶 子女 (family.children.length):`);
family.children.forEach((child, i) => {
console.log(` i + 1. child.name (child.profile?.gender || '待定')`);
console.log(` 出生: child.profile?.birthDate || '待录入'`);
console.log(` 八字: child.bazi?.year || '?' child.bazi?.month || '' child.bazi?.day || '' child.bazi?.hour || ''`);
});
}
if (!family?.spouse && !family?.father && !family?.mother && (!family?.children || family.children.length === 0)) {
console.log(` (暂无家庭成员记录)`);
}
console.log('');
}
/**
* 显示完整档案
*/
function showProfile(userId) {
const profile = loadProfile(userId);
if (!profile) {
console.log(`❌ 用户档案不存在: userId`);
return;
}
console.log('\n📋 用户档案\n');
console.log(`ID: profile.userId`);
console.log(`姓名: profile.name`);
console.log(`出生: profile.profile?.birthDate profile.profile?.birthTime`);
console.log(`地点: profile.profile?.birthPlace`);
console.log(`性别: profile.profile?.gender`);
console.log('\n🧮 八字');
console.log(` profile.bazi?.year profile.bazi?.month profile.bazi?.day profile.bazi?.hour`);
console.log(` 日主: profile.bazi?.dayStem`);
console.log(` 生肖: profile.bazi?.zodiac`);
if (profile.ziwei) {
console.log('\n✨ 紫微');
console.log(` 命宫: profile.ziwei.mingGong`);
console.log(` 命主: profile.ziwei.mingZhu`);
}
listFamilyMembers(userId);
}
/**
* 列出所有用户
*/
function listProfiles() {
const files = fs.readdirSync(PROFILES_DIR).filter(f => f.endsWith('.json'));
console.log('\n📋 用户列表\n');
files.forEach(f => {
const userId = f.replace('.json', '');
const data = loadProfile(userId);
console.log(` userId | data?.name || '未知' | data?.profile?.birthDate || '未知'`);
});
console.log(`\n共 files.length 个用户\n`);
}
/**
* 删除档案
*/
function deleteProfile(userId) {
const filePath = getProfilePath(userId);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
console.log(`✅ 已删除: userId`);
} else {
console.log(`❌ 档案不存在: userId`);
}
}
// 主入口
const args = process.argv.slice(2);
const command = args[0];
switch (command) {
case 'show':
case 'load':
if (args[1]) {
showProfile(args[1]);
} else {
console.log('用法: node profile.js show <userId>');
}
break;
case 'list':
listProfiles();
break;
case 'save':
if (args.length < 4) {
console.log('用法: node profile.js save <userId> <field> <value>');
console.log('示例: node profile.js save 123 name 张三');
console.log(' node profile.js save 123 bazi.day 戊子');
} else {
updateProfile(args[1], args[2], args[3]);
}
break;
case 'add':
// node profile.js add <userId> <type> <name> [birthDate] [gender]
if (args.length < 4) {
console.log('用法:');
console.log(' node profile.js add <userId> spouse <name> [出生日期] [性别]');
console.log(' node profile.js add <userId> father <name> [出生日期]');
console.log(' node profile.js add <userId> mother <name> [出生日期]');
console.log(' node profile.js add <userId> child <name> <出生日期> <性别>');
console.log('');
console.log('示例:');
console.log(' node profile.js add 123 spouse 李四 1990-05-15 女');
console.log(' node profile.js add 123 child 子女姓名 2020-01-01 男');
} else {
const userId = args[1];
const type = args[2];
const name = args[3];
if (type === 'child') {
const birthDate = args[4];
const gender = args[5];
addChild(userId, name, birthDate, gender);
} else {
addFamilyMember(userId, type, name, {
birthDate: args[4],
gender: type === 'spouse' ? (args[5] || '女') : (args[4] ? '男' : '')
});
}
}
break;
case 'family':
if (args[1]) {
listFamilyMembers(args[1]);
} else {
console.log('用法: node profile.js family <userId>');
}
break;
case 'delete':
if (args[1]) {
deleteProfile(args[1]);
} else {
console.log('用法: node profile.js delete <userId>');
}
break;
default:
console.log(`
🗂️ 用户档案管理 (支持家庭成员)
用法:
node profile.js show <userId> 显示完整档案
node profile.js list 列出所有用户
node profile.js save <userId> <field> <value> 保存字段
node profile.js add <userId> <type> <name> [参数] 添加家庭成员
node profile.js family <userId> 显示家庭成员
node profile.js delete <userId> 删除档案
家庭成员类型:
spouse - 配偶
father - 父亲
mother - 母亲
child - 子女
示例:
# 查看档案
node profile.js show 123456
# 添加配偶
node profile.js add 123456 spouse 配偶姓名 1990-05-15 女
# 添加子女
node profile.js add 123456 child 子女姓名 2020-01-01 男
# 添加父亲
node profile.js add 123456 father 父亲姓名 1950-03-15
# 查看家庭成员
node profile.js family 123456
# 保存八字
node profile.js save 123456 family.spouse.bazi.year 庚午
`);
}
module.exports = { loadProfile, saveProfile, updateProfile, addFamilyMember, addChild, listFamilyMembers, showProfile };
FILE:scripts/push-toggle.js
#!/usr/bin/env node
/**
* 每日运势推送开关
* 开启时自动创建用户专属 cron job,关闭时删除
*
* 用法:
* node push-toggle.js on <userId> 开启推送(默认早8点+晚8点)
* node push-toggle.js off <userId> 关闭推送(删除 cron)
* node push-toggle.js status <userId> 查看状态
* node push-toggle.js on <userId> --morning 08:00 --evening 20:00
*/
const fs = require('fs');
const path = require('path');
const { getTopTopics } = require('./preference-tracker');
const PROFILES_DIR = path.join(__dirname, '../data/profiles');
// 各领域深度分析模板
const TOPIC_EXPANDED = {
'财运': `💰 财运深析(重点关注):
- 今日财星状态与格局分析
- 投资/支出/收款建议
- 结合今日金融/市场新闻的财运影响与风险`,
'事业': `💼 事业深析(重点关注):
- 今日官禄宫能量与事业星状态
- 职场关键决策与行动建议
- 结合今日政策/商业新闻的机遇与风险`,
'感情': `💕 感情深析(重点关注):
- 今日桃花星与夫妻宫状态
- 感情互动与表达建议
- 今日社会/情感类新闻的命理启示`,
'健康': `🏥 健康深析(重点关注):
- 今日五行对应脏腑的能量状态
- 饮食、作息、运动建议
- 结合今日天气/环境/公共卫生新闻`,
'婚姻': `💍 婚姻深析(重点关注):
- 今日夫妻宫能量与刑冲状态
- 婚姻经营与沟通建议
- 今日家庭/社会新闻的婚姻启示`,
'子女': `👶 子女深析(重点关注):
- 今日子女宫状态
- 亲子关系与教育建议`,
'官司': `⚖️ 官司/是非深析(重点关注):
- 今日官星与白虎星分析
- 法律/合同/是非风险提示
- 结合今日司法/社会冲突新闻`,
'出行': `✈️ 出行深析(重点关注):
- 今日驿马星与方位吉凶
- 出行时机、方向与交通建议
- 结合今日天气/灾害/交通新闻`,
'风水': `🏠 风水深析(重点关注):
- 今日飞星方位吉凶
- 家居/办公能量调整建议`,
};
// 新闻到命理领域映射规则(嵌入 prompt,供 Agent 识别)
const NEWS_FORTUNE_MAPPING = `新闻与命理映射规则(识别今日新闻后按此对应):
- 市场波动/股债汇变动/降息加息 → 财运风险或机遇信号
- 政策出台/经济刺激/行业利好 → 事业机遇信号
- 监管收紧/行业整顿/合规要求 → 事业风险,行事低调
- 自然灾害/台风暴雪/恶劣天气 → 出行风险 + 健康警示
- 公共卫生/食品安全/空气质量 → 健康领域警示
- 社会冲突/司法/法律法规变动 → 官司/是非风险
- 科技突破/国际贸易/地缘政治 → 事业/财运双向影响分析`;
function loadProfile(userId) {
const filePath = path.join(PROFILES_DIR, `userId.json`);
if (!fs.existsSync(filePath)) return null;
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
function saveProfile(userId, profile) {
const filePath = path.join(PROFILES_DIR, `userId.json`);
profile.updatedAt = new Date().toISOString().split('T')[0];
fs.writeFileSync(filePath, JSON.stringify(profile, null, 2), 'utf8');
}
/**
* 创建用户专属 cron job
* 返回 cron job id,失败返回 null
*/
function createCronJob(userId, name, cronExpr, message, channel) {
const sessionKey = `agent:main:channel:direct:userId`;
const args = [
'cron', 'add',
'--name', name,
'--cron', cronExpr,
'--tz', 'Asia/Shanghai',
'--session', 'isolated',
'--session-key', sessionKey,
'--channel', channel,
'--to', userId,
'--announce',
'--timeout-seconds', '120',
'--message', message,
'--json'
];
try {
// openclaw cron 由运行时管理,输出配置供运行时处理
const cronConfig = { name, cronExpr, tz: 'Asia/Shanghai', session: 'isolated', sessionKey, channel, to: userId, announce: true, timeoutSeconds: 120, message };
console.log(`__OPENCLAW_CRON_ADD__:JSON.stringify(cronConfig)`);
return `cron:name:userId`;
} catch (e) {
console.error('创建 cron 失败:', e.message);
return null;
}
}
/**
* 删除 cron job
*/
function removeCronJob(cronId) {
try {
console.log(`__OPENCLAW_CRON_RM__:cronId`);
return true;
} catch (e) {
console.error(`删除 cron cronId 失败:`, e.message);
return false;
}
}
/**
* 构建早晨推送 prompt(今日运程)— 含偏好权重 + 新闻整合
*/
function buildMorningMessage(profile, topTopics) {
const bazi = profile.bazi || {};
const baziStr = `bazi.year bazi.month bazi.day bazi.hour`;
const name = profile.name || '用户';
const userId = profile.userId;
const top1 = topTopics[0] || '事业';
const top2 = topTopics[1] || '财运';
const top3 = topTopics[2] || '健康';
const expandedSection = TOPIC_EXPANDED[top1] || '';
return `请为name生成今日命理运程报告。
用户八字:baziStr,日主:bazi.dayStem
用户重点关注(按偏好排序):top1 > top2 > top3
步骤:
1) 运行 node scripts/daily-fortune.js 获取今日干支基础运程
2) 搜索今日重要新闻(财经、政策、社会、国际各一条)
NEWS_FORTUNE_MAPPING
3) 结合八字与新闻做个性化分析,重点展开【top1】领域深度分析
4) 完成后运行:node scripts/preference-tracker.js record userId top1 morning_push
输出格式:
🌅 【私人命理顾问】今日完整日期(含星期)
📊 今日综合指数
事业:★★★★☆ 财运:★★★☆☆ 感情:★★★☆☆ 健康:★★★★☆
🎨 幸运色:xxx(结合今日干支五行)
expandedSection
💼 今日宜忌
✅ 宜:xxx、xxx、xxx
❌ 忌:xxx、xxx
⚠️ 风险提示(结合命理+今日新闻背景,如无则省略)
📰 命理与时事(1-2句:将今日1条重要新闻与运势联系)
⏰ 今日三吉时:时辰(时间段)宜做xxx
💡 今日一句(命理格言或人生启示)`;
}
/**
* 构建晚间推送 prompt(明日预告)— 含偏好权重 + 新闻整合
*/
function buildEveningMessage(profile, topTopics) {
const bazi = profile.bazi || {};
const baziStr = `bazi.year bazi.month bazi.day bazi.hour`;
const name = profile.name || '用户';
const userId = profile.userId;
const top1 = topTopics[0] || '事业';
const top2 = topTopics[1] || '财运';
const expandedSection = TOPIC_EXPANDED[top1] || '';
return `请为name生成明日命理预告(今晚提前推送明日运势)。
用户八字:baziStr,日主:bazi.dayStem
用户重点关注(按偏好排序):top1 > top2
步骤:
1) 运行 node scripts/daily-fortune.js 获取明日(今日+1天)干支运程
2) 搜索今日晚间重要新闻,预判对明日的影响
NEWS_FORTUNE_MAPPING
3) 重点展开【top1】明日深度预告
4) 完成后运行:node scripts/preference-tracker.js record userId top1 evening_push
输出格式:
🌙 【明日预告】明日完整日期(含星期)
📊 明日综合指数
事业:★★★★☆ 财运:★★★☆☆ 感情:★★★☆☆ 健康:★★★★☆
🎨 明日幸运色:xxx
expandedSection.replace('今日', '明日')
💼 明日宜忌
✅ 宜:xxx、xxx
❌ 忌:xxx、xxx
⚠️ 明日风险预警(结合命理+今晚新闻动向,如无则省略)
📰 时事预判(今晚新闻对明日命理的影响,1句)
⏰ 明日三吉时
💡 今晚一句`;
}
// ─────────────────────────────────────────────
function enablePush(userId, options = {}) {
const profile = loadProfile(userId);
if (!profile) {
console.log(`❌ 用户档案不存在: userId,请先注册`);
return false;
}
const morningTime = options.morning || '08:00';
const eveningTime = options.evening || '20:00';
const channel = options.channel || (profile.preferences?.channels?.[0]) || 'telegram';
const [mHour, mMin] = morningTime.split(':');
const [eHour, eMin] = eveningTime.split(':');
const morningCron = `mMin mHour * * *`;
const eveningCron = `eMin eHour * * *`;
console.log(`\n⏳ 正在为 profile.name(userId) 创建推送计划...\n`);
// 读取用户偏好权重
const topTopics = getTopTopics(userId, 3);
console.log(` 关注领域:topTopics.join(' > ')`);
// 如果已有 cron,先删除旧的
const existing = profile.push?.cronIds || {};
if (existing.morning) { removeCronJob(existing.morning); }
if (existing.evening) { removeCronJob(existing.evening); }
// 创建早晨 cron
const morningId = createCronJob(
userId,
`yunshi-morning-userId`,
morningCron,
buildMorningMessage(profile, topTopics),
channel
);
// 创建晚间 cron
const eveningId = createCronJob(
userId,
`yunshi-evening-userId`,
eveningCron,
buildEveningMessage(profile, topTopics),
channel
);
// 保存到档案
if (!profile.preferences) profile.preferences = {};
profile.preferences.pushEnabled = true;
profile.preferences.pushMorning = true;
profile.preferences.pushEvening = true;
profile.preferences.morningTime = morningTime;
profile.preferences.eveningTime = eveningTime;
profile.preferences.channels = [channel];
profile.push = {
cronIds: {
morning: morningId,
evening: eveningId
},
createdAt: new Date().toISOString()
};
saveProfile(userId, profile);
console.log(`✅ 推送已开启!\n`);
console.log(` 用户: profile.name (userId)`);
console.log(` 渠道: channel`);
console.log(` 🌅 早晨运程: 每天 morningTime ${morningId)` : '⚠️ 创建失败'}`);
console.log(` 🌙 晚间预告: 每天 eveningTime ${eveningId)` : '⚠️ 创建失败'}`);
console.log('');
return true;
}
function disablePush(userId) {
const profile = loadProfile(userId);
if (!profile) {
console.log(`❌ 用户档案不存在: userId`);
return false;
}
// 删除 cron job
const cronIds = profile.push?.cronIds || {};
let removed = 0;
if (cronIds.morning) { if (removeCronJob(cronIds.morning)) removed++; }
if (cronIds.evening) { if (removeCronJob(cronIds.evening)) removed++; }
if (!profile.preferences) profile.preferences = {};
profile.preferences.pushEnabled = false;
profile.preferences.pushMorning = false;
profile.preferences.pushEvening = false;
profile.push = { cronIds: {}, disabledAt: new Date().toISOString() };
saveProfile(userId, profile);
console.log(`\n✅ 推送已关闭(删除了 removed 个定时任务)\n`);
return true;
}
function showStatus(userId) {
const profile = loadProfile(userId);
if (!profile) {
console.log(`❌ 用户档案不存在: userId`);
return;
}
const pref = profile.preferences || {};
const enabled = pref.pushEnabled ?? pref.pushMorning ?? false;
const cronIds = profile.push?.cronIds || {};
console.log(`
👤 用户: profile.name (userId)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🧮 八字: profile.bazi?.year profile.bazi?.month profile.bazi?.day profile.bazi?.hour
📅 出生: profile.profile?.birthDate profile.profile?.birthTime
🔔 推送: '❌ 已关闭'
⏰ 早晨: 00' ${cronIds.morning)` : ''}
🌙 晚间: 00' ${cronIds.evening)` : ''}
📡 渠道: (pref.channels || ['telegram']).join(', ')
📆 推送创建: profile.push?.createdAt?.split('T')[0] || '未设置'
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
`);
}
module.exports = { enablePush, disablePush, showStatus };
// ─────────────────────────────────────────────
// 命令行入口
// ─────────────────────────────────────────────
if (require.main !== module) return;
const args = process.argv.slice(2);
const command = args[0];
const userId = args[1];
if (!userId) {
console.log(`
🔔 每日运势推送管理
用法:
node push-toggle.js on <userId> 开启推送(早8点+晚8点)
node push-toggle.js off <userId> 关闭推送
node push-toggle.js status <userId> 查看状态
node push-toggle.js on <userId> --morning 08:00 --evening 20:00
node push-toggle.js on <userId> --channel feishu
说明:
开启后自动创建两个定时任务:
- 每天早晨推送当日运程(默认 08:00)
- 每天晚间推送明日预告(默认 20:00)
`);
process.exit(1);
}
const options = {};
const morningIdx = args.indexOf('--morning');
if (morningIdx !== -1 && args[morningIdx + 1]) options.morning = args[morningIdx + 1];
const eveningIdx = args.indexOf('--evening');
if (eveningIdx !== -1 && args[eveningIdx + 1]) options.evening = args[eveningIdx + 1];
const channelIdx = args.indexOf('--channel');
if (channelIdx !== -1 && args[channelIdx + 1]) options.channel = args[channelIdx + 1];
switch (command) {
case 'on': enablePush(userId, options); break;
case 'off': disablePush(userId); break;
case 'status': showStatus(userId); break;
default:
console.log(`❌ 未知命令: command`);
process.exit(1);
}
FILE:scripts/qimen.js
#!/usr/bin/env node
/**
* 奇门遁甲排盘脚本
* 支持:时间起局、择日选时
*/
// 地支
const diZhi = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
// 九星
const nineStars = [
{ name: '天蓬', symbol: '⭐', element: '水', trait: '凶星', position: 1 },
{ name: '天任', symbol: '⭐', element: '土', trait: '凶星', position: 8 },
{ name: '天冲', symbol: '⭐', element: '木', trait: '吉星', position: 3 },
{ name: '天辅', symbol: '⭐', element: '木', trait: '吉星', position: 4 },
{ name: '天英', symbol: '⭐', element: '火', trait: '凶星', position: 9 },
{ name: '天芮', symbol: '⭐', element: '土', trait: '凶星', position: 2 },
{ name: '天柱', symbol: '⭐', element: '金', trait: '凶星', position: 7 },
{ name: '天心', symbol: '⭐', element: '金', trait: '吉星', position: 6 },
{ name: '天禽', symbol: '⭐', element: '土', trait: '大吉', position: 5 }
];
// 八门
const eightDoors = [
{ name: '休门', symbol: '🏠', element: '水', trait: '休息、平稳', position: 1 },
{ name: '生门', symbol: '🌱', element: '土', trait: '生长、财运', position: 8 },
{ name: '伤门', symbol: '💔', element: '木', trait: '受伤、变动', position: 3 },
{ name: '杜门', symbol: '🔒', element: '木', trait: '阻碍、保密', position: 4 },
{ name: '景门', symbol: '🔥', element: '火', trait: '文化、虚假', position: 9 },
{ name: '死门', symbol: '💀', element: '土', trait: '死亡、凶险', position: 2 },
{ name: '惊门', symbol: '😱', element: '金', trait: '惊恐、口舌', position: 7 },
{ name: '开门', symbol: '🚪', element: '金', trait: '开创、顺利', position: 6 }
];
// 三奇
const sanQi = ['乙', '丙', '丁'];
// 六仪
const liuYi = ['戊', '己', '庚', '辛', '壬', '癸'];
// 九宫(后天八卦方位)
const ninePalaces = [
{ num: 9, gua: '离', zhi: '午', direction: '南' },
{ num: 4, gua: '巽', zhi: '卯', direction: '东南' },
{ num: 2, gua: '坤', zhi: '未', direction: '西南' },
{ num: 3, gua: '震', zhi: '卯', direction: '东' },
{ num: 5, gua: '中', zhi: '戌', direction: '中' },
{ num: 1, gua: '坎', zhi: '子', direction: '北' },
{ num: 7, gua: '兑', zhi: '酉', direction: '西' },
{ num: 8, gua: '艮', zhi: '丑', direction: '东北' },
{ num: 6, gua: '乾', zhi: '戌', direction: '西北' }
];
/**
* 判断阴遁还是阳遁
* 冬至 → 夏至:阳遁
* 夏至 → 冬至:阴遁
*/
function isYangDun(date = new Date()) {
const month = date.getMonth() + 1;
const day = date.getDate();
// 节气粗略判断
// 夏至在6月21日,冬至在12月22日
const yearDay = date.getMonth() * 30 + day;
const summerSolstice = 5 * 30 + 21; // 约6月21日
const winterSolstice = 11 * 30 + 22; // 约12月22日
if (yearDay < summerSolstice || yearDay > winterSolstice) {
return true; // 阳遁(春夏)
}
return false; // 阴遁(秋冬)
}
/**
* 计算值符星(以2024-01-01为基准,按时辰连续推算)
*/
function getZhiFu(date, isYang) {
const baseDate = new Date('2024-01-01T00:00:00');
const diffDays = Math.floor((date - baseDate) / (1000 * 60 * 60 * 24));
const hour = date.getHours();
const shichen = Math.floor((hour + 1) / 2) % 12; // 当前时辰序号
const idx = ((diffDays * 12 + shichen) % 9 + 9) % 9;
// 阳遁顺布,阴遁逆布
return isYang ? nineStars[idx] : nineStars[(9 - idx) % 9];
}
/**
* 计算值使门(以2024-01-01为基准,按时辰连续推算)
*/
function getZhiShi(date, isYang) {
const baseDate = new Date('2024-01-01T00:00:00');
const diffDays = Math.floor((date - baseDate) / (1000 * 60 * 60 * 24));
const hour = date.getHours();
const shichen = Math.floor((hour + 1) / 2) % 12;
const idx = ((diffDays * 12 + shichen) % 8 + 8) % 8;
return isYang ? eightDoors[idx] : eightDoors[(8 - idx) % 8];
}
/**
* 排布九宫
*/
function arrangePalaces(isYang, zhiFu) {
const palaces = [];
// 阳遁顺布,阴遁逆布
const order = isYang ? [1, 2, 3, 4, 5, 6, 7, 8, 9] : [9, 8, 7, 6, 5, 4, 3, 2, 1];
// 九宫对应
const palaceMap = {
1: { gua: '坎', zhi: '子', direction: '北' },
2: { gua: '坤', zhi: '未', direction: '西南' },
3: { gua: '震', zhi: '卯', direction: '东' },
4: { gua: '巽', zhi: '辰', direction: '东南' },
5: { gua: '中', zhi: '戌', direction: '中' },
6: { gua: '乾', zhi: '戌', direction: '西北' },
7: { gua: '兑', zhi: '酉', direction: '西' },
8: { gua: '艮', zhi: '丑', direction: '东北' },
9: { gua: '离', zhi: '午', direction: '南' }
};
return order.map((num, index) => ({
position: index + 1,
num,
...palaceMap[num]
}));
}
/**
* 安九星到九宫
*/
function arrangeStars(palaces, isYang, zhiFu) {
const starIndex = nineStars.findIndex(s => s.name === zhiFu.name);
const result = palaces.map((palace, i) => {
const offset = isYang ? i : (8 - i);
const starIdx = (starIndex + offset) % 9;
const star = nineStars[starIdx];
// 天禽永远在中五宫
if (palace.num === 5) {
return { ...palace, star: nineStars[4] };
}
return { ...palace, star };
});
return result;
}
/**
* 安八门到九宫
*/
function arrangeDoors(palaces, isYang, zhiShi) {
const doorIndex = eightDoors.findIndex(d => d.name === zhiShi.name);
const result = palaces.map((palace, i) => {
const offset = isYang ? i : (8 - i);
const doorIdx = (doorIndex + offset) % 8;
const door = eightDoors[doorIdx];
// 死门永远在坤二宫
if (palace.num === 2) {
return { ...palace, door: eightDoors[5] };
}
return { ...palace, door };
});
return result;
}
/**
* 找三奇方位
*/
function findSanQi(palaces) {
const results = [];
palaces.forEach(p => {
if (p.star && p.door) {
const starName = p.star.name;
// 乙奇在坎、离;丙奇在乾、兑;丁奇在震、巽
if (starName === '天任' || starName === '天英') {
results.push({ qi: '乙', palace: p });
} else if (starName === '天心' || starName === '天柱') {
results.push({ qi: '丙', palace: p });
} else if (starName === '天冲' || starName === '天辅') {
results.push({ qi: '丁', palace: p });
}
}
});
return results;
}
/**
* 判断吉凶
*/
function judgeFortune(palace, sanQiCount) {
const star = palace.star;
const door = palace.door;
if (!star || !door) return '未知';
const starGood = ['天冲', '天辅', '天心', '天禽'].includes(star.name);
const doorGood = ['生门', '休门', '开门', '景门'].includes(door.name);
if (starGood && doorGood) return '大吉';
if (starGood || doorGood) return '中吉';
if (door.name === '死门' || door.name === '惊门') return '大凶';
return '凶';
}
/**
* 生成报告
*/
function generateReport(date, palaces, zhiFu, zhiShi, isYang, sanQiList) {
const hour = date.getHours();
const hourZhi = diZhi[Math.floor((hour + 1) / 2) % 12];
const hourElement = { '子': '水', '丑': '土', '寅': '木', '卯': '木', '辰': '土', '巳': '火', '午': '火', '未': '土', '申': '金', '酉': '金', '戌': '土', '亥': '水' }[hourZhi];
let report = `
🎴 【奇门遁甲盘】
📋 基本信息
日期:date.toLocaleDateString('zh-CN')
时辰:hourZhi时
遁局:'阴遁'('夏至→冬至')
值符:zhiFu.name
值使:zhiShi.name
📊 九宫排布
`;
// 按洛书顺序展示
const luoshu = [4, 9, 2, 3, 5, 1, 7, 8, 6]; // 巽4 离9 坤2 震3 中5 坎1 兑7 艮8 乾6
report += '\n 【东南】【南】【西南】\n';
report += ' ';
for (let i = 0; i < 9; i++) {
const row = Math.floor(i / 3);
const col = i % 3;
const palace = palaces.find(p => p.num === luoshu[i]);
if (palace) {
const starSymbol = palace.star ? palace.star.name.substring(1, 3) : ' ';
const doorSymbol = palace.door ? palace.door.name.charAt(0) : ' ';
report += `starSymboldoorSymbol `;
} else {
report += ' ';
}
if (col === 2 && row === 0) report += '\n 【东】【中】【西】\n ';
if (col === 2 && row === 1) report += '\n 【东北】【北】【西北】\n ';
}
// 详细宫位信息
report += '\n\n📍 各宫位详情\n';
report += '━━━━━━━━━━━━━━━━━━━━\n';
palaces.forEach(p => {
const guaInfo = ninePalaces.find(n => n.num === p.num) || {};
const fortune = judgeFortune(p, 0);
const fortuneSymbol = fortune.includes('吉') ? '✅' : fortune.includes('凶') ? '❌' : '⚠️';
report += `\n【p.num宫】p.direction || guaInfo.direction || '' guaInfo.gua || '' guaInfo.zhi || ''\n`;
if (p.star) report += ` 九星:p.star.name(p.star.element,p.star.trait)\n`;
if (p.door) report += ` 八门:p.door.name(p.door.trait)\n`;
report += ` 吉凶:fortuneSymbol fortune\n`;
});
// 三奇位置
if (sanQiList.length > 0) {
report += '\n✨ 三奇方位\n';
sanQiList.forEach(sq => {
report += ` sq.qi奇在sq.palace.directionsq.palace.num宫\n`;
});
}
// 最佳方位
const goodPalaces = palaces.filter(p => judgeFortune(p, 0).includes('吉'));
if (goodPalaces.length > 0) {
report += '\n🌟 最佳方位\n';
goodPalaces.forEach(p => {
report += ` p.direction(p.num宫)- p.star?.name || ''p.door?.name || ''\n`;
});
}
// 值符使跟随
report += `\n⚡ 值符zhiFu.name运行,值使zhiShi.name值事\n`;
report += `
💡 综合建议
'阴遁宜退,防守为主'
值符zhiFu.name为核心,zhiShi.name为动向
goodPalaces.length > 0 ? `吉利方位:${goodPalaces.map(p => p.direction).join('、')` : '宜静不宜动'}
`;
return report;
}
// 主入口
const args = process.argv.slice(2);
if (args[0] === '--help' || args[0] === '-h') {
console.log(`
奇门遁甲排盘
用法:
node qimen.js # 当前时间起局
node qimen.js 2026-03-24 # 指定日期(默认当前时辰)
node qimen.js 2026-03-24 15 # 指定日期和时辰
示例:
node qimen.js
node qimen.js 2026-03-24
node qimen.js 2026-03-24 15
`);
} else {
let date;
let hour;
if (args.length === 0) {
date = new Date();
} else if (args.length === 1) {
date = new Date(args[0]);
if (isNaN(date.getTime())) {
console.error('日期格式无效');
process.exit(1);
}
} else if (args.length >= 2) {
date = new Date(args[0]);
if (isNaN(date.getTime())) {
console.error('日期格式无效');
process.exit(1);
}
hour = parseInt(args[1]);
if (hour >= 0 && hour <= 23) {
date.setHours(hour);
}
}
const isYang = isYangDun(date);
const zhiFu = getZhiFu(date, isYang);
const zhiShi = getZhiShi(date, isYang);
const palaces = arrangePalaces(isYang, zhiFu);
const palacesWithStars = arrangeStars(palaces, isYang, zhiFu);
const palacesWithAll = arrangeDoors(palacesWithStars, isYang, zhiShi);
const sanQiList = findSanQi(palacesWithAll);
console.log(generateReport(date, palacesWithAll, zhiFu, zhiShi, isYang, sanQiList));
}
FILE:scripts/register.js
#!/usr/bin/env node
/**
* 快速注册脚本
* 用于命令行快速注册新用户
*/
const fs = require('fs');
const path = require('path');
const { getLunarMonth, isAfterLiChun } = require('./jieqi');
const { runFullAnalysis } = require('./bazi-analysis');
// 天干地支
const tianGan = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
const diZhi = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
const zodiacMap = { '子': '鼠', '丑': '牛', '寅': '虎', '卯': '兔', '辰': '龙', '巳': '蛇', '午': '马', '未': '羊', '申': '猴', '酉': '鸡', '戌': '狗', '亥': '猪' };
// ============================================================
// 真太阳时修正
// ============================================================
/** 主要城市经度表(东经度) */
const CITY_LONGITUDE = {
'上海': 121.47, '北京': 116.40, '广州': 113.26, '深圳': 114.06,
'杭州': 120.15, '南京': 118.80, '成都': 104.07, '重庆': 106.55,
'武汉': 114.30, '西安': 108.93, '沈阳': 123.43, '哈尔滨': 126.68,
'长春': 125.32, '大连': 121.62, '天津': 117.19, '济南': 117.00,
'青岛': 120.38, '郑州': 113.65, '石家庄': 114.51, '太原': 112.55,
'呼和浩特': 111.76, '乌鲁木齐': 87.62, '拉萨': 91.11, '昆明': 102.68,
'贵阳': 106.63, '南宁': 108.37, '海口': 110.33, '福州': 119.30,
'厦门': 118.08, '南昌': 115.89, '合肥': 117.27, '长沙': 112.98,
'兰州': 103.82, '西宁': 101.74, '银川': 106.23, '昭通': 103.72,
'曲靖': 103.80, '丽江': 100.22, '大理': 100.27, '玉溪': 102.55,
'保山': 99.16, '普洱': 100.97, '临沧': 100.08, '香港': 114.17,
'澳门': 113.55, '台北': 121.53, '苏州': 120.62, '无锡': 120.30,
'宁波': 121.55, '温州': 120.67, '济宁': 116.59, '烟台': 121.39,
'徐州': 117.18, '洛阳': 112.45, '唐山': 118.18, '秦皇岛': 119.60
};
/**
* 均时差(分钟):地球椭圆公转导致的时差,精度约 ±1 分钟
*/
function getEquationOfTime(date) {
const startOfYear = new Date(date.getFullYear(), 0, 1);
const doy = Math.floor((date - startOfYear) / 86400000) + 1;
const B = (2 * Math.PI * (doy - 1)) / 365;
return 9.87 * Math.sin(2 * B) - 7.53 * Math.cos(B) - 1.5 * Math.sin(B);
}
/**
* 计算真太阳时,返回修正后的日期和时间
* @param {string} birthDate YYYY-MM-DD
* @param {string} birthTime HH:MM
* @param {string} birthPlace 出生地(城市名)
* @returns {{ date: string, time: string, offsetMinutes: number, city: string|null }}
*/
function getTrueSolarTime(birthDate, birthTime, birthPlace) {
// 匹配城市经度(支持"上海市"、"上海浦东"等写法)
let longitude = null;
let matchedCity = null;
if (birthPlace) {
for (const [city, lng] of Object.entries(CITY_LONGITUDE)) {
if (birthPlace.includes(city)) {
longitude = lng;
matchedCity = city;
break;
}
}
}
if (longitude === null) {
// 未知城市,不做修正
return { date: birthDate, time: birthTime, offsetMinutes: 0, city: null };
}
const date = new Date(`birthDateT12:00:00+08:00`);
const geoOffset = (longitude - 120) * 4; // 地理时差(分钟)
const eot = getEquationOfTime(date); // 均时差(分钟)
const totalOffset = Math.round(geoOffset + eot); // 总修正量(分钟,四舍五入)
const [h, m] = birthTime.split(':').map(Number);
let totalMinutes = h * 60 + m + totalOffset;
// 处理跨日
let correctedDate = birthDate;
if (totalMinutes < 0) {
const d = new Date(`birthDateT12:00:00+08:00`);
d.setDate(d.getDate() - 1);
correctedDate = d.toISOString().slice(0, 10);
totalMinutes += 1440;
} else if (totalMinutes >= 1440) {
const d = new Date(`birthDateT12:00:00+08:00`);
d.setDate(d.getDate() + 1);
correctedDate = d.toISOString().slice(0, 10);
totalMinutes -= 1440;
}
const ch = String(Math.floor(totalMinutes / 60)).padStart(2, '0');
const cm = String(totalMinutes % 60).padStart(2, '0');
return {
date: correctedDate,
time: `ch:cm`,
offsetMinutes: totalOffset,
city: matchedCity,
geoOffsetMin: Math.round(geoOffset),
eotMin: Math.round(eot)
};
}
/**
* 内置八字计算(使用精确节气算法)
*/
function calculateBazi(birthDate, birthTime, gender, sect = 1) {
const [year, month, day] = birthDate.split('-').map(Number);
const [hour] = birthTime.split(':').map(Number);
// 年柱(以立春精确时刻为界)
const calcYear = isAfterLiChun(year, month, day) ? year : year - 1;
const yearGanIndex = ((calcYear - 4) % 10 + 10) % 10;
const yearZhiIndex = ((calcYear - 4) % 12 + 12) % 12;
// 月柱(以精确节气为界)
const lunarMonth = getLunarMonth(year, month, day);
const monthZhiIndex = (lunarMonth + 1) % 12;
const monthGanBases = [2, 4, 6, 8, 0]; // 甲己起丙,乙庚起戊,丙辛起庚,丁壬起壬,戊癸起甲
const monthGanIndex = (monthGanBases[yearGanIndex % 5] + lunarMonth - 1) % 10;
// 日柱(以2024-01-01甲子日为基准)
let calcDate = new Date(`birthDateT12:00:00`);
if (sect === 1 && hour === 23) calcDate.setDate(calcDate.getDate() + 1); // 晚子时算次日
const baseDate = new Date('2024-01-01T12:00:00');
const diffDays = Math.round((calcDate - baseDate) / (1000 * 60 * 60 * 24));
const dayGanIndex = (diffDays % 10 + 10) % 10; // 2024-01-01=甲子(甲=0)
const dayZhiIndex = (diffDays % 12 + 12) % 12; // 2024-01-01=甲子(子=0)
// 时柱(五鼠遁日)
const hourZhiIndex = (sect === 1 && hour === 23) ? 0 : Math.floor((hour + 1) / 2) % 12;
const hourGanBases = [0, 2, 4, 6, 8]; // 甲己起甲,乙庚起丙,丙辛起戊,丁壬起庚,戊癸起壬
const hourGanIndex = (hourGanBases[dayGanIndex % 5] + hourZhiIndex) % 10;
return {
year: tianGan[yearGanIndex] + diZhi[yearZhiIndex],
month: tianGan[monthGanIndex] + diZhi[monthZhiIndex],
day: tianGan[dayGanIndex] + diZhi[dayZhiIndex],
hour: tianGan[hourGanIndex] + diZhi[hourZhiIndex],
dayStem: tianGan[dayGanIndex],
zodiac: zodiacMap[diZhi[yearZhiIndex]]
};
}
/**
* 生成初始档案
*/
function createProfile(userId, name, gender, birthDate, birthTime, birthPlace, sect = 1) {
// 真太阳时修正
const solar = getTrueSolarTime(birthDate, birthTime, birthPlace);
// 用真太阳时计算八字
const bazi = calculateBazi(solar.date, solar.time, gender === '男' ? 1 : 0, sect);
const profile = {
userId,
name,
language: 'zh',
profile: {
birthDate,
birthTime,
birthPlace,
gender,
timezone: 'Asia/Shanghai',
trueSolarTime: solar.time,
trueSolarDate: solar.date,
solarCorrectionMin: solar.offsetMinutes,
solarCorrectionCity: solar.city
},
bazi: {
year: bazi?.year || '',
month: bazi?.month || '',
day: bazi?.day || '',
hour: bazi?.hour || '',
dayStem: bazi?.dayStem || '',
zodiac: bazi?.zodiac || '',
sect: sect === 1 ? '晚子时' : '早子时',
source: 'verified',
analysis: bazi ? runFullAnalysis(bazi) : null
},
ziwei: {
mingGong: '',
mingZhu: '',
source: 'pending'
},
family: {
spouse: {
name: '配偶',
profile: {
birthDate: '待录入',
birthTime: '待录入',
birthPlace: '',
gender: gender === '男' ? '女' : '男',
lunarBirth: ''
},
bazi: {
year: '',
month: '',
day: '',
hour: '',
source: 'pending'
}
},
father: {
name: '父亲',
profile: {
birthDate: '待录入',
birthTime: '待录入',
birthPlace: '',
gender: '男'
},
bazi: {
year: '',
month: '',
day: '',
hour: '',
source: 'pending'
}
},
mother: {
name: '母亲',
profile: {
birthDate: '待录入',
birthTime: '待录入',
birthPlace: '',
gender: '女'
},
bazi: {
year: '',
month: '',
day: '',
hour: '',
source: 'pending'
}
},
children: []
},
preferences: {
pushMorning: true,
pushEvening: false,
morningTime: '07:00',
eveningTime: '20:00',
channels: ['telegram'],
focusAreas: ['事业', '财运', '健康'],
riskTolerance: '中等'
},
settings: {
defaultSect: sect,
lunarCalendar: true,
notifications: {
dailyFortune: true,
riskAlert: true,
weeklySummary: false
}
},
createdAt: new Date().toISOString().split('T')[0],
updatedAt: new Date().toISOString().split('T')[0]
};
return profile;
}
/**
* 保存档案
*/
function saveProfile(userId, profile) {
const dir = path.join(__dirname, '../data/profiles');
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const filePath = path.join(dir, `userId.json`);
fs.writeFileSync(filePath, JSON.stringify(profile, null, 2), 'utf8');
return filePath;
}
// 主入口
const args = process.argv.slice(2);
if (args.length < 5) {
console.log(`
📝 快速注册用户
用法:
node register.js <userId> <姓名> <性别> <出生日期> <出生时间> [出生地点] [子时]
参数:
userId - 用户ID(telegram id或其他唯一标识)
姓名 - 用户姓名
性别 - 男 或 女
出生日期 - YYYY-MM-DD
出生时间 - HH:MM(24小时制)
出生地点 - 省市(可选,默认上海)
子时 - 1=晚子时(23点后算次日),2=早子时(可选,默认1)
示例:
node register.js 123456 张三 男 1990-05-15 14:30 上海
node register.js 123456 李四 女 1995-08-20 23:45 北京 1
说明:
子时(23:00-01:00)出生需要特别注意:
- 晚子时(1): 23:00后算次日日柱
- 早子时(2): 23:00后算当日日柱
`);
process.exit(1);
}
const userId = args[0];
const name = args[1];
const gender = args[2];
const birthDate = args[3];
const birthTime = args[4];
const birthPlace = args[5] || '上海';
const sect = parseInt(args[6] || '1');
// 验证
if (!['男', '女'].includes(gender)) {
console.error('性别必须是"男"或"女"');
process.exit(1);
}
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
if (!dateRegex.test(birthDate)) {
console.error('出生日期格式错误,请使用 YYYY-MM-DD');
process.exit(1);
}
const timeRegex = /^\d{2}:\d{2}$/;
if (!timeRegex.test(birthTime)) {
console.error('出生时间格式错误,请使用 HH:MM');
process.exit(1);
}
console.log('\n📝 正在注册用户...\n');
console.log(` 用户ID: userId`);
console.log(` 姓名: name`);
console.log(` 性别: gender`);
console.log(` 出生: birthDate birthTime`);
console.log(` 地点: birthPlace`);
console.log(` 子时: '早子时(23点后算当日)'`);
console.log('');
// 创建档案
const profile = createProfile(userId, name, gender, birthDate, birthTime, birthPlace, sect);
// 保存
const filePath = saveProfile(userId, profile);
console.log('✅ 注册成功!\n');
// 真太阳时提示
const sc = profile.profile.solarCorrectionMin;
if (sc !== 0 && profile.profile.solarCorrectionCity) {
const sign = sc > 0 ? '+' : '';
console.log(`🌞 真太阳时修正(profile.profile.solarCorrectionCity)`);
console.log(` 北京时间: birthDate birthTime`);
console.log(` 真太阳时: profile.profile.trueSolarDate profile.profile.trueSolarTime (signsc分钟)`);
console.log(` 八字时柱以真太阳时计算`);
console.log('');
} else if (!profile.profile.solarCorrectionCity) {
console.log(`🌞 真太阳时:未识别城市"birthPlace",以北京时间计算(如需精确请使用主要城市名)`);
console.log('');
}
console.log('📊 八字信息');
console.log(` 年柱: profile.bazi.year`);
console.log(` 月柱: profile.bazi.month`);
console.log(` 日柱: profile.bazi.day`);
console.log(` 时柱: profile.bazi.hour`);
console.log(` 日主: profile.bazi.dayStem (profile.bazi.zodiac)`);
console.log('');
console.log(`📁 档案已保存: filePath`);
console.log('');
// 自动开启推送(如果指定了 --push 参数)
const pushIdx = args.indexOf('--push');
if (pushIdx !== -1) {
const channel = args[args.indexOf('--channel') + 1] || 'telegram';
const morning = args[args.indexOf('--morning') + 1] || '08:00';
const evening = args[args.indexOf('--evening') + 1] || '20:00';
console.log('⏳ 正在开启每日推送...');
try {
const { enablePush } = require('./push-toggle');
enablePush(userId, { morning, evening, channel });
} catch (e) {
console.error('推送开启失败:', e.message);
}
} else {
console.log('💡 提示:运行以下命令开启每日运程推送:');
console.log(` node scripts/push-toggle.js on userId`);
console.log('');
}
module.exports = { createProfile, saveProfile };
FILE:scripts/zhuanshi.js
#!/usr/bin/env node
/**
* 择吉选日脚本
* 帮助用户选择:黄道吉日、开业、搬家、签约、订婚、装修、出行等好日子
*
* 基于:紫微斗数、奇门遁甲、黄历建除十二神、彭祖百忌
*/
// ============================================================
// 内置农历/黄历算法(替代 lunar-typescript,无外部依赖)
// ============================================================
const _GAN = ['甲','乙','丙','丁','戊','己','庚','辛','壬','癸'];
const _ZHI = ['子','丑','寅','卯','辰','巳','午','未','申','酉','戌','亥'];
const _CHONG = { '子':'午','丑':'未','寅':'申','卯':'酉','辰':'戌','巳':'亥','午':'子','未':'丑','申':'寅','酉':'卯','戌':'辰','亥':'巳' };
const _JIAN_CHU = ['建','除','满','平','定','执','破','危','成','收','开','闭'];
const _JIAN_CHU_YI_JI = {
'建': { yi: ['出行','上任','祭祀','求财'], ji: ['嫁娶','动土','破土','安葬'] },
'除': { yi: ['扫除','解除','移徙','沐浴'], ji: ['嫁娶','破土','安葬','入殓'] },
'满': { yi: ['嫁娶','开业','纳财','入宅'], ji: ['出行','动土','破土','诉讼'] },
'平': { yi: ['出行','移徙','求医','上任'], ji: ['嫁娶','安葬','破土'] },
'定': { yi: ['嫁娶','开业','签约','求财'], ji: ['出行','诉讼','动土'] },
'执': { yi: ['祭祀','纳财','捕猎','捉贼'], ji: ['开业','嫁娶','移徙','出行'] },
'破': { yi: [], ji: ['开业','嫁娶','出行','移徙','动土','签约'] },
'危': { yi: ['祭祀'], ji: ['出行','登高','嫁娶','开业'] },
'成': { yi: ['开业','嫁娶','移徙','上任','出行'], ji: ['诉讼','破土'] },
'收': { yi: ['纳财','收获','祭祀'], ji: ['出行','嫁娶','动土','开业'] },
'开': { yi: ['开业','嫁娶','出行','求财','移徙'], ji: ['入殓','安葬','破土'] },
'闭': { yi: ['入殓','安葬','封穴'], ji: ['开业','嫁娶','出行','动土'] }
};
const _PENG_ZU_GAN = {
'甲':'甲不开仓财物耗散','乙':'乙不栽植千株不长','丙':'丙不修灶必见灾殃',
'丁':'丁不剃头头必生疮','戊':'戊不受田田主不祥','己':'己不破券二比并亡',
'庚':'庚不经络织机虚张','辛':'辛不合酱主人不尝','壬':'壬不决水更难提防',
'癸':'癸不词讼理弱敌强'
};
function _getDayGanZhi(date) {
const base = new Date('2024-01-01T12:00:00');
const diff = Math.round((date - base) / 86400000);
return _GAN[((diff % 10) + 10) % 10] + _ZHI[((diff % 12) + 12) % 12];
}
function _getZhiXing(date) {
const m = date.getMonth() + 1; // solar month 1-12
const monthZhiIdx = m % 12; // 1→丑(1), 2→寅(2)…12→子(0)
const gz = _getDayGanZhi(date);
const dayZhiIdx = _ZHI.indexOf(gz[1]);
return _JIAN_CHU[((dayZhiIdx - monthZhiIdx) + 12) % 12];
}
/** 模拟 lunar-typescript Lunar 对象 */
function createLunarDate(date) {
const gz = _getDayGanZhi(date);
const dayGan = gz[0];
const dayZhi = gz[1];
const zhiXing = _getZhiXing(date);
const chongZhi = _CHONG[dayZhi] || '';
const yiji = _JIAN_CHU_YI_JI[zhiXing] || { yi: [], ji: [] };
return {
getDayInGanZhi: () => gz,
getDayGan: () => dayGan,
getDayZhi: () => dayZhi,
getZhiXing: () => zhiXing,
getDayYi: () => yiji.yi,
getDayJi: () => yiji.ji,
getPengZuGan: () => _PENG_ZU_GAN[dayGan] || '',
getChong: () => chongZhi,
getChongDesc: () => `冲chongZhi`
};
}
// ============================================
// 常量定义
// ============================================
// 天干地支
const TIAN_GAN = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
const DI_ZHI = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
// 地支对应五行
const ZHI_ELEMENT = {
'子': '水', '丑': '土', '寅': '木', '卯': '木',
'辰': '土', '巳': '火', '午': '火', '未': '土',
'申': '金', '酉': '金', '戌': '土', '亥': '水'
};
// 天干对应五行
const GAN_ELEMENT = {
'甲': '木', '乙': '木', '丙': '火', '丁': '火',
'戊': '土', '己': '土', '庚': '金', '辛': '金',
'壬': '水', '癸': '水'
};
// 五行颜色
const ELEMENT_COLOR = {
'木': { color: '绿色、青色', direction: '东方' },
'火': { color: '红色、紫色', direction: '南方' },
'土': { color: '黄色、棕色', direction: '中央' },
'金': { color: '白色、金色', direction: '西方' },
'水': { color: '黑色、蓝色', direction: '北方' }
};
// 建除十二神序列
const JIAN_CHU = ['建', '除', '满', '平', '定', '执', '破', '危', '成', '收', '开', '闭'];
// 奇门遁甲九星
const NINE_STARS = [
{ name: '天蓬', element: '水', trait: '凶星', position: 1, good: false },
{ name: '天任', element: '土', trait: '凶星', position: 8, good: false },
{ name: '天冲', element: '木', trait: '吉星', position: 3, good: true },
{ name: '天辅', element: '木', trait: '吉星', position: 4, good: true },
{ name: '天英', element: '火', trait: '凶星', position: 9, good: false },
{ name: '天芮', element: '土', trait: '凶星', position: 2, good: false },
{ name: '天柱', element: '金', trait: '凶星', position: 7, good: false },
{ name: '天心', element: '金', trait: '吉星', position: 6, good: true },
{ name: '天禽', element: '土', trait: '大吉', position: 5, good: true }
];
// 奇门遁甲八门
const EIGHT_DOORS = [
{ name: '休门', element: '水', trait: '休息、平稳', good: true },
{ name: '生门', element: '土', trait: '生长、财运', good: true },
{ name: '伤门', element: '木', trait: '受伤、变动', good: false },
{ name: '杜门', element: '木', trait: '阻碍、保密', good: false },
{ name: '景门', element: '火', trait: '文化、虚假', good: false },
{ name: '死门', element: '土', trait: '死亡、凶险', good: false },
{ name: '惊门', element: '金', trait: '惊恐、口舌', good: false },
{ name: '开门', element: '金', trait: '开创、顺利', good: true }
];
// 时辰信息
const HOUR_INFO = {
'子': { range: '23-01', element: '水', tip: '整理思考' },
'丑': { range: '01-03', element: '土', tip: '睡眠休息' },
'寅': { range: '03-05', element: '木', tip: '计划准备' },
'卯': { range: '05-07', element: '木', tip: '晨间运动' },
'辰': { range: '07-09', element: '土', tip: '贵人运佳' },
'巳': { range: '09-11', element: '火', tip: '事业高峰' },
'午': { range: '11-13', element: '火', tip: '财运旺盛' },
'未': { range: '13-15', element: '土', tip: '平稳行事' },
'申': { range: '15-17', element: '金', tip: '财运佳' },
'酉': { range: '17-19', element: '金', tip: '收整理' },
'戌': { range: '19-21', element: '土', tip: '社交应酬' },
'亥': { range: '21-23', element: '水', tip: '学习思考' }
};
// 活动类型与宜忌配合
const ACTIVITIES = {
'开业': { good: ['开', '满', '定', '成', '收'], bad: ['闭', '破', '危', '建'] },
'搬家': { good: ['满', '定', '平', '成', '收'], bad: ['破', '危', '闭', '建'] },
'签约': { good: ['开', '定', '成', '满', '收'], bad: ['闭', '破', '危', '建'] },
'订婚': { good: ['合', '定', '满', '成', '开'], bad: ['冲', '刑', '破', '危'] },
'装修': { good: ['平', '满', '定', '成', '收'], bad: ['破', '冲', '危', '闭'] },
'出行': { good: ['开', '定', '成', '满'], bad: ['闭', '破', '危', '建'] },
'结婚': { good: ['合', '定', '满', '成', '开'], bad: ['冲', '刑', '破', '危'] },
'祭祀': { good: ['建', '除', '满', '平'], bad: ['破', '闭'] },
'求财': { good: ['开', '生', '满', '成', '收'], bad: ['闭', '破', '危'] },
'上任': { good: ['开', '定', '成', '满'], bad: ['破', '危', '闭'] }
};
// ============================================
// 核心算法
// ============================================
/**
* 获取指定月份的所有日期
*/
function getDatesInMonth(year, month) {
const dates = [];
const daysInMonth = new Date(year, month, 0).getDate();
for (let d = 1; d <= daysInMonth; d++) {
dates.push(new Date(year, month - 1, d));
}
return dates;
}
/**
* 计算某日的奇门遁甲信息
*/
function calculateQimen(date) {
const isYang = isYangDun(date);
const zhiFu = getZhiFuStar(date, isYang);
const zhiShi = getZhiShiDoor(date, isYang);
return {
isYang,
zhiFu,
zhiShi,
goodStars: NINE_STARS.filter(s => s.good).map(s => s.name),
goodDoors: EIGHT_DOORS.filter(d => d.good).map(d => d.name)
};
}
/**
* 判断阴遁还是阳遁
*/
function isYangDun(date = new Date()) {
const month = date.getMonth() + 1;
const day = date.getDate();
const yearDay = date.getMonth() * 30 + day;
const summerSolstice = 5 * 30 + 21;
const winterSolstice = 11 * 30 + 22;
return yearDay < summerSolstice || yearDay > winterSolstice;
}
/**
* 计算值符星
*/
function getZhiFuStar(date, isYang) {
const baseDate = new Date('2024-01-01T12:00:00');
const diffDays = Math.round((date - baseDate) / (1000 * 60 * 60 * 24));
const hour = date.getHours();
const shichen = Math.floor((hour + 1) / 2) % 12;
const idx = ((diffDays * 12 + shichen) % 9 + 9) % 9;
return isYang ? NINE_STARS[idx] : NINE_STARS[(9 - idx) % 9];
}
/**
* 计算值使门
*/
function getZhiShiDoor(date, isYang) {
const baseDate = new Date('2024-01-01T12:00:00');
const diffDays = Math.round((date - baseDate) / (1000 * 60 * 60 * 24));
const hour = date.getHours();
const shichen = Math.floor((hour + 1) / 2) % 12;
const idx = ((diffDays * 12 + shichen) % 8 + 8) % 8;
return isYang ? EIGHT_DOORS[idx] : EIGHT_DOORS[(8 - idx) % 8];
}
/**
* 获取某日吉时(基于五行)
*/
function getLuckyHoursForDate(date) {
const lunarDate = createLunarDate(date);
const ganZhi = lunarDate.getDayInGanZhi();
const dayZhi = ganZhi[1];
const dayElement = ZHI_ELEMENT[dayZhi] || '土';
// 找出与日主五行相同或相生的时辰
const luckyHours = [];
for (const [zhi, elem] of Object.entries(ZHI_ELEMENT)) {
if (elem === dayElement || (isSupportingElement(dayElement, elem))) {
if (HOUR_INFO[zhi]) {
luckyHours.push({ zhi, ...HOUR_INFO[zhi] });
}
}
}
return luckyHours.slice(0, 4);
}
/**
* 判断是否相生
*/
function isSupportingElement(main, support) {
const supportMap = { '木': '火', '火': '土', '土': '金', '金': '水', '水': '木' };
return supportMap[main] === support;
}
/**
* 评分日期
*/
function scoreDate(date, activityType, userDayStem = null) {
const lunarDate = createLunarDate(date);
const activity = ACTIVITIES[activityType] || ACTIVITIES['开业'];
let score = 50; // 基础分
const factors = [];
// 1. 建除十二神评分
const zhiXing = lunarDate.getZhiXing();
const jianChuIndex = JIAN_CHU.indexOf(zhiXing);
if (activity.good.includes(zhiXing)) {
score += 20;
factors.push({ name: '建除', value: `zhiXing日`, bonus: 20, good: true });
} else if (activity.bad.includes(zhiXing)) {
score -= 25;
factors.push({ name: '建除', value: `zhiXing日`, bonus: -25, good: false });
} else {
score += 5;
factors.push({ name: '建除', value: `zhiXing日`, bonus: 5, good: null });
}
// 2. 宜忌配合
const dayYi = lunarDate.getDayYi() || [];
const dayJi = lunarDate.getDayJi() || [];
const yiMatch = activity.good.some(a => dayYi.some(y => y.includes(a)));
const jiMatch = activity.bad.some(a => dayJi.some(j => j.includes(a)));
if (yiMatch) score += 15;
if (jiMatch) score -= 15;
factors.push({ name: '宜忌', value: yiMatch ? '配合较好' : '一般', bonus: yiMatch ? 15 : (jiMatch ? -15 : 0), good: yiMatch ? true : (jiMatch ? false : null) });
// 3. 彭祖百忌(检查是否与日干相冲)
const pengZuGan = lunarDate.getPengZuGan();
const dayGan = lunarDate.getDayGan();
if (pengZuGan && pengZuGan.includes(dayGan)) {
score -= 5;
}
// 4. 日冲评分
const chong = lunarDate.getChong();
const dayZhi = lunarDate.getDayZhi();
if (userDayStem) {
const userElement = GAN_ELEMENT[userDayStem] || '';
const chongElement = ZHI_ELEMENT[chong] || '';
if (isSameElement(userElement, chongElement)) {
score -= 20;
factors.push({ name: '日冲', value: `chong(lunarDate.getChongDesc())`, bonus: -20, good: false });
} else {
factors.push({ name: '日冲', value: `chong(lunarDate.getChongDesc())`, bonus: 0, good: null });
}
} else {
factors.push({ name: '日冲', value: `chong(lunarDate.getChongDesc())`, bonus: 0, good: null });
}
// 5. 奇门遁甲吉凶
const qimen = calculateQimen(date);
if (qimen.zhiFu.good) {
score += 10;
factors.push({ name: '值符', value: qimen.zhiFu.name, bonus: 10, good: true });
} else {
factors.push({ name: '值符', value: qimen.zhiFu.name, bonus: 0, good: false });
}
if (qimen.zhiShi.good) {
score += 10;
factors.push({ name: '值使', value: qimen.zhiShi.name, bonus: 10, good: true });
} else {
factors.push({ name: '值使', value: qimen.zhiShi.name, bonus: 0, good: false });
}
// 6. 五行配合(如果提供了用户日干)
if (userDayStem) {
const userElement = GAN_ELEMENT[userDayStem];
const dayElement = GAN_ELEMENT[dayGan];
if (isSupportingElement(userElement, dayElement)) {
score += 15;
factors.push({ name: '五行', value: `日主dayElement生助我userElement`, bonus: 15, good: true });
} else if (isSupportingElement(dayElement, userElement)) {
score += 10;
factors.push({ name: '五行', value: `我userElement生日主dayElement`, bonus: 10, good: true });
} else if (userElement === dayElement) {
score += 5;
factors.push({ name: '五行', value: `比和dayElement`, bonus: 5, good: true });
} else {
score -= 10;
factors.push({ name: '五行', value: `相克`, bonus: -10, good: false });
}
}
// 限制分数范围
score = Math.max(0, Math.min(100, score));
return {
score,
factors,
zhiXing,
dayGanZhi: lunarDate.getDayInGanZhi(),
dayGan,
dayZhi,
pengZuGan: lunarDate.getPengZuGan(),
chong: lunarDate.getChong(),
chongDesc: lunarDate.getChongDesc(),
dayYi,
dayJi,
qimen,
luckyHours: getLuckyHoursForDate(date)
};
}
/**
* 判断是否同元素
*/
function isSameElement(elem1, elem2) {
return elem1 && elem2 && elem1 === elem2;
}
/**
* 获取八字日主
*/
function getDayStemFromBazi(bazi) {
if (!bazi) return null;
// bazi 格式: "庚午 辛巳 庚辰 辛巳" (年 月 日 时)
const parts = bazi.split(/\s+/);
if (parts.length >= 3) {
const dayGanZhi = parts[2];
return dayGanZhi[0]; // 取天干
}
return null;
}
/**
* 格式化星级
*/
function formatStars(score) {
const stars = Math.round(score / 20);
return '⭐'.repeat(stars) + '☆'.repeat(5 - stars);
}
// ============================================
// 报告生成
// ============================================
/**
* 生成择日报告
*/
function generateReport(year, month, activityType, userBazi = null) {
const userDayStem = userBazi ? getDayStemFromBazi(userBazi) : null;
const dates = getDatesInMonth(year, month);
const dayMap = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const results = dates.map(date => {
const scoreResult = scoreDate(date, activityType, userDayStem);
return {
date,
dateStr: `month月date.getDate()日`,
weekDay: dayMap[date.getDay()],
...scoreResult
};
});
// 排序:按分数降序
results.sort((a, b) => b.score - a.score);
// 分类
const avoidDates = results.filter(r => r.score < 30);
const goodDates = results.filter(r => r.score >= 70).slice(0, 5);
const mediumDates = results.filter(r => r.score >= 50 && r.score < 70).slice(0, 5);
// 生成报告
let report = `
🎯 year年month月 最佳吉日(activityType)
━━━━━━━━━━━━━━━━━━━━
`;
if (goodDates.length > 0) {
const best = goodDates[0];
report += `
🏆 综合最优(activityType)
best.dateStr(best.weekDay)formatStars(best.score) best.score分
吉时:best.luckyHours.map(h => `${h.range点(h.zhi时)`).join('、')}
干支:best.dayGanZhi
建除:best.zhiXing日
冲:best.chongbest.chongDesc
彭祖:best.pengZuGan
宜:best.dayYi.slice(0, 4).join('、')
忌:best.dayJi.slice(0, 3).join('、')
`;
// 奇门信息
report += `
【奇门遁甲】
遁局:'阴遁'
值符:best.qimen.zhiFu.name(best.qimen.zhiFu.trait)
值使:best.qimen.zhiShi.name(best.qimen.zhiShi.trait)
`;
}
if (mediumDates.length > 0) {
report += `
━━━━━━━━━━━━━━━━━━━━
📅 其他推荐
`;
mediumDates.forEach(d => {
report += `
d.dateStr(d.weekDay)formatStars(d.score) d.score分
干支:d.dayGanZhi | 建除:d.zhiXing日 | 冲:d.chongd.chongDesc
`;
});
}
if (avoidDates.length > 0) {
report += `
━━━━━━━━━━━━━━━━━━━━
⚠️ 避免日期
`;
avoidDates.slice(0, 3).forEach(d => {
report += `
d.dateStr(d.weekDay)❌ d.score分
原因:d.factors.filter(f => f.bonus < 0).map(f => `${f.namef.value`).join('、')}
冲:d.chongd.chongDesc
`;
});
}
report += `
━━━━━━━━━━━━━━━━━━━━
💡 评分说明
• 分数范围:0-100分
• ⭐⭐⭐⭐⭐ = 80-100分(极佳)
• ⭐⭐⭐⭐ = 60-79分(良好)
• ⭐⭐⭐ = 40-59分(一般)
• ⭐⭐ = 20-39分(欠佳)
• ⭐ = 0-19分(避免)
评分因素:建除十二神(±25)、宜忌配合(±15)、
日冲(±20)、值符值使(±20)、五行生克(±15)
`;
if (userBazi) {
report += `
用户日主:userDayStem(GAN_ELEMENT[userDayStem])
`;
}
return report;
}
/**
* 查找最佳日期
*/
function findBestDate(year, month, activityType, userBazi = null) {
const userDayStem = userBazi ? getDayStemFromBazi(userBazi) : null;
const dates = getDatesInMonth(year, month);
let bestDate = null;
let bestScore = -1;
for (const date of dates) {
const result = scoreDate(date, activityType, userDayStem);
if (result.score > bestScore) {
bestScore = result.score;
bestDate = { date, ...result };
}
}
return bestDate;
}
// ============================================
// 主入口
// ============================================
const args = process.argv.slice(2);
function showHelp() {
console.log(`
📅 择吉选日脚本
用法:
node zhuanshi.js <YYYY-MM> <活动类型> [用户八字]
node zhuanshi.js best <YYYY-MM> <活动类型> [用户八字]
活动类型:
开业、搬家、签约、订婚、装修、出行、结婚、祭祀、求财、上任
示例:
node zhuanshi.js 2026-04 开业
node zhuanshi.js 2026-04 签约 "庚午 辛巳 庚辰 辛巳"
node zhuanshi.js best 2026-04 搬家
`);
}
if (args[0] === '--help' || args[0] === '-h') {
showHelp();
process.exit(0);
}
// 解析参数
if (args.length < 2) {
console.error('参数不足');
showHelp();
process.exit(1);
}
let year, month, activityType, userBazi;
let findBest = false;
if (args[0] === 'best') {
findBest = true;
const dateMatch = args[1].match(/^(\d{4})-(\d{2})$/);
if (!dateMatch) {
console.error('日期格式错误,请使用 YYYY-MM');
process.exit(1);
}
year = parseInt(dateMatch[1]);
month = parseInt(dateMatch[2]);
activityType = args[2] || '开业';
userBazi = args[3] || null;
} else {
const dateMatch = args[0].match(/^(\d{4})-(\d{2})$/);
if (!dateMatch) {
console.error('日期格式错误,请使用 YYYY-MM');
process.exit(1);
}
year = parseInt(dateMatch[1]);
month = parseInt(dateMatch[2]);
activityType = args[1] || '开业';
userBazi = args[2] || null;
}
if (month < 1 || month > 12) {
console.error('月份无效,请使用 1-12');
process.exit(1);
}
if (!Object.keys(ACTIVITIES).includes(activityType)) {
console.warn(`警告:未知的活动类型 "activityType",使用默认值"开业"`);
activityType = '开业';
}
console.log(`
╭──────────────────────────────────────╮
│ 🔮 择吉选日分析中... │
╰──────────────────────────────────────╯
`);
console.log(`📋 分析条件`);
console.log(` 日期:year年month月`);
console.log(` 活动:activityType`);
if (userBazi) console.log(` 用户八字:userBazi`);
console.log('');
if (findBest) {
const best = findBestDate(year, month, activityType, userBazi);
const dayMap = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const stars = formatStars(best.score);
console.log(`
🏆 year年month月最佳吉日
best.date.getMonth() + 1月best.date.getDate()日(dayMap[best.date.getDay()])stars best.score分
📊 详细信息
干支:best.dayGanZhi
建除:best.zhiXing日
冲:best.chongbest.chongDesc
彭祖:best.pengZuGan
⏰ 吉时
best.luckyHours.map(h => ` • ${h.zhi时(h.range点)- h.tip`).join('\n')}
✅ 宜
best.dayYi.slice(0, 5).join('、')
❌ 忌
best.dayJi.slice(0, 4).join('、')
【奇门遁甲】
遁局:'阴遁'
值符:best.qimen.zhiFu.name(best.qimen.zhiFu.element,best.qimen.zhiFu.trait)
值使:best.qimen.zhiShi.name(best.qimen.zhiShi.element,best.qimen.zhiShi.trait)
`);
} else {
console.log(generateReport(year, month, activityType, userBazi));
}
FILE:scripts/ziwei.js
#!/usr/bin/env node
/**
* 紫微斗数命盘分析 v4 - 知识库驱动格局 + 大运流年 + 八字用神
* 使用 iztro 库(中文紫微斗数)
*/
const fs = require('fs');
const path = require('path');
const { astro } = require('iztro');
// ============================================================
// 知识库格局匹配系统
// ============================================================
const KNOWLEDGE_DIR = process.env.OPENCLAW_KNOWLEDGE_DIR
|| (process.env.HOME ? path.join(process.env.HOME, '.openclaw/workspace/knowledge') : '');
/**
* 解析知识库中的格局文件,构建模式检测规则
*/
function buildPatternRules() {
if (!fs.existsSync(KNOWLEDGE_DIR)) return [];
const files = fs.readdirSync(KNOWLEDGE_DIR).filter(f => f.endsWith('.md'));
const rules = [];
const skipNames = [
'倪海厦', '渊海子平', '滴天髓', '命理交叉', '排盘不准',
'命运解读', '算卦', 'Jia-八字', '紫微斗数格局', '四化表',
'紫微斗数基本术语', '紫微斗数与奇门遁甲', '星平会海',
'渊海子平-学习', '滴天髓-子平真诠', '命理交叉验证系统'
];
for (const file of files) {
if (skipNames.some(n => file.includes(n))) continue;
const filePath = path.join(KNOWLEDGE_DIR, file);
const content = fs.readFileSync(filePath, 'utf-8');
const name = file.replace('.md', '');
const rule = parsePatternFile(name, content);
if (rule) rules.push(rule);
}
return rules;
}
/**
* 解析单个格局文件,提取检测条件
*/
function parsePatternFile(name, content) {
// 提取吉星加会条件
const luckyStars = [];
if (content.includes('禄存')) luckyStars.push('禄存');
if (content.includes('科权禄') || content.includes('化禄') && content.includes('化权') && content.includes('化科')) {
luckyStars.push('科', '权', '科权禄');
}
if (content.includes('左右')) { luckyStars.push('左辅', '右弼'); }
if (content.includes('昌曲') || content.includes('文昌') || content.includes('文曲')) {
luckyStars.push('文昌', '文曲');
}
if (content.includes('魁钺') || content.includes('天魁') || content.includes('天钺')) {
luckyStars.push('天魁', '天钺');
}
// 判断格局等级
let level = '平';
if (content.includes('大富大贵') || content.includes('极美') || content.includes('极贵')) level = '贵';
else if (content.includes('富贵') || content.includes('大富') || content.includes('大贵')) level = '富';
else if (content.includes('凶') || content.includes('刑') || content.includes('破格')) level = '凶';
else if (content.includes('平常') || content.includes('普通')) level = '平';
// 提取星曜条件
const mainStars = [];
const starMatches = content.match(/[\u4e00-\u9fa5]{2,4}(?:星|门|府|相|杀|狼|军|曲|昌|梁|机|阴|阳|同|贞|府|微)/g);
if (starMatches) {
const uniqueStars = [...new Set(starMatches.map(s => s.slice(0, 2)))];
const knownStars = ['紫微','天机','太阳','武曲','天同','廉贞','天府','贪狼','巨门','太阴','天相','天梁','七杀','破军',
'文昌','文曲','左辅','右弼','天魁','天钺','禄存','天马','擎羊','陀罗','火星','铃星','地空','地劫','解神','天虚','天喜','红鸾'];
for (const s of uniqueStars) {
if (knownStars.includes(s)) mainStars.push(s);
}
}
// 提取宫位条件
const branches = [];
const branchKeywords = ['子','午','寅','申','卯','酉','辰','戌','丑','未','巳','亥','寅申','子午','辰戌','丑未','卯酉','巳亥'];
for (const kw of branchKeywords) {
if (content.includes(kw) && kw.length >= 1) {
if (kw.length === 1) branches.push(kw);
else branches.push(kw);
}
}
// 提取年干条件
const yearStems = [];
if (content.includes('甲年') || content.includes('甲年生')) yearStems.push('甲');
if (content.includes('乙年') || content.includes('乙年生')) yearStems.push('乙');
if (content.includes('丙年') || content.includes('丙年生')) yearStems.push('丙');
if (content.includes('丁年') || content.includes('丁年生')) yearStems.push('丁');
if (content.includes('戊年') || content.includes('戊年生')) yearStems.push('戊');
if (content.includes('己年') || content.includes('己年生')) yearStems.push('己');
if (content.includes('庚年') || content.includes('庚年生')) yearStems.push('庚');
if (content.includes('辛年') || content.includes('辛年生')) yearStems.push('辛');
if (content.includes('壬年') || content.includes('壬年生')) yearStems.push('壬');
if (content.includes('癸年') || content.includes('癸年生')) yearStems.push('癸');
// 提取四化条件
const mutagens = [];
if (content.includes('化禄')) mutagens.push('禄');
if (content.includes('化权')) mutagens.push('权');
if (content.includes('化科')) mutagens.push('科');
if (content.includes('化忌')) mutagens.push('忌');
// 提取三方四正条件
const sanfang = content.includes('三方四正') || content.includes('三合');
// 提取夹的条件(邻宫)
const adjacent = content.includes('夹命') || content.includes('相夹');
// 提取凶格标志
const isJiong = content.includes('凶格') || content.includes('刑') || content.includes('破格');
// 提取描述
let desc = '';
const lines = content.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('*') && !trimmed.startsWith('---') && trimmed.length > 5 && trimmed.length < 100) {
if (trimmed.includes('大富大贵') || trimmed.includes('福寿') || trimmed.includes('贵气') ||
trimmed.includes('少年') || trimmed.includes('劳碌') || trimmed.includes('平常') ||
trimmed.includes('大富') || trimmed.includes('大贵') || trimmed.includes('先贫后富')) {
desc = trimmed.replace(/[#*]/g, '').trim();
break;
}
}
}
if (!desc) {
for (const line of lines) {
const trimmed = line.replace(/[#*]/g, '').trim();
if (trimmed.length > 5 && trimmed.length < 80 && !trimmed.startsWith('-') && !trimmed.startsWith('|')) {
desc = trimmed;
break;
}
}
}
// 判断宫位条件类型
let palaceCondition = 'ming'; // 默认命宫
if (content.includes('命宫三方') || content.includes('三方') || sanfang) palaceCondition = 'sanfang';
if (content.includes('命宫') && content.includes('邻宫') && adjacent) palaceCondition = 'adjacent';
if (content.includes('命身宫入丑未') || content.includes('命身宫')) palaceCondition = 'mingBody';
return {
name,
level,
stars: mainStars,
branches,
yearStems,
mutagens,
luckyStars,
palaceCondition, // ming | sanfang | adjacent | mingBody
isJiong,
desc: desc.substring(0, 80)
};
}
/**
* 使用知识库规则检测命盘格局
*/
function checkPatternsFromKnowledge(palaces, mingIdx, transforms, yearStem) {
const rules = buildPatternRules();
const results = [];
// 构建命宫、三方四正、邻宫数据
const mingPalace = palaces[mingIdx];
const mingBranch = mingPalace?.earthlyBranch || '';
const mingStars = mingPalace?.majorStars?.map(s => s.name) || [];
const mingMinor = mingPalace?.minorStars?.map(s => s.name) || [];
const mingAdj = mingPalace?.adjectiveStars?.map(s => s.name) || [];
const allMingStars = [...mingStars, ...mingMinor, ...mingAdj];
// 三方四正
const opposite = (mingIdx + 6) % 12;
const tri1 = (mingIdx + 4) % 12;
const tri2 = (mingIdx + 8) % 12;
const sanfangPalaces = [palaces[opposite], palaces[tri1], palaces[tri2]];
const sanfangStars = sanfangPalaces.flatMap(p => [
...(p?.majorStars?.map(s => s.name) || []),
...(p?.minorStars?.map(s => s.name) || []),
...(p?.adjectiveStars?.map(s => s.name) || [])
]);
const allSanfangStars = [...allMingStars, ...sanfangStars];
// 邻宫
const prevIdx = (mingIdx - 1 + 12) % 12;
const nextIdx = (mingIdx + 1) % 12;
const prevStars = [
...(palaces[prevIdx]?.majorStars?.map(s => s.name) || []),
...(palaces[prevIdx]?.minorStars?.map(s => s.name) || [])
];
const nextStars = [
...(palaces[nextIdx]?.majorStars?.map(s => s.name) || []),
...(palaces[nextIdx]?.minorStars?.map(s => s.name) || [])
];
// 四化星
const transformMap = {};
transforms.forEach(t => { transformMap[t.star] = t.hua; });
for (const rule of rules) {
try {
if (!rule.stars || rule.stars.length === 0) continue;
let matched = false;
let matchType = '';
// 主星检查
const requiredStars = rule.stars.filter(s => {
const main14 = ['紫微','天机','太阳','武曲','天同','廉贞','天府','贪狼','巨门','太阴','天相','天梁','七杀','破军',
'文昌','文曲','左辅','右弼','天魁','天钺','禄存','天马','擎羊','陀罗','火星','铃星','地空','地劫'];
return main14.includes(s);
});
if (requiredStars.length === 0) continue;
if (rule.palaceCondition === 'ming' || rule.palaceCondition === 'mingBody') {
// 命宫/命身宫检查
if (requiredStars.every(s => allMingStars.includes(s))) {
matched = true;
matchType = '命宫';
}
} else if (rule.palaceCondition === 'sanfang') {
// 三方四正检查
if (requiredStars.every(s => allSanfangStars.includes(s))) {
matched = true;
matchType = '三方四正';
}
} else if (rule.palaceCondition === 'adjacent') {
// 邻宫夹命检查
const prevHas = requiredStars.some(s => prevStars.includes(s));
const nextHas = requiredStars.some(s => nextStars.includes(s));
if (prevHas && nextHas) {
matched = true;
matchType = '邻宫夹命';
}
} else {
// 默认:命宫优先,三方四正次之
if (requiredStars.every(s => allMingStars.includes(s))) {
matched = true;
matchType = '命宫';
} else if (requiredStars.every(s => allSanfangStars.includes(s))) {
matched = true;
matchType = '三方四正';
}
}
// 年干条件
if (matched && rule.yearStems && rule.yearStems.length > 0) {
if (!rule.yearStems.includes(yearStem)) {
matched = false;
}
}
// 宫位地支条件
if (matched && rule.branches && rule.branches.length > 0) {
const validBranch = rule.branches.some(b => {
if (b.length === 1) return mingBranch === b;
// 处理双地支如寅申
return b.split('').some(c => mingBranch === c);
});
if (!validBranch) matched = false;
}
// 吉星加会条件
if (matched && rule.luckyStars && rule.luckyStars.length > 0) {
const hasLucky = rule.luckyStars.every(s => allSanfangStars.includes(s));
if (!hasLucky) {
// 降级:记录为弱匹配
matchType += '(吉星不足)';
}
}
if (matched) {
results.push({
name: rule.name,
level: rule.level,
desc: rule.desc,
matchType
});
}
} catch (e) {
// Skip failed rules silently
}
}
return results;
}
// ============================================================
// 十四主星、六煞星等定义
// ============================================================
const MAIN_STARS = ['紫微','天机','太阳','武曲','天同','廉贞','天府','贪狼','巨门','太阴','天相','天梁','七杀','破军'];
const LUCKY_STARS = ['左辅','右弼','天魁','天钺','文昌','文曲','禄存','天马'];
const UNLUCKY_STARS = ['擎羊','陀罗','火星','铃星','地空','地劫'];
const PEACH_STARS = ['贪狼','廉贞','红鸾','天喜','桃花','天姚'];
const WEALTH_STARS = ['武曲','太阴','天府','禄存','紫微','天相'];
// ============================================================
// 八字用神核心算法(穷通宝鉴 + 子平真诠)
// ============================================================
// 天干五行
const STEM_ELEMENT = { '甲': '木', '乙': '木', '丙': '火', '丁': '火', '戊': '土', '己': '土', '庚': '金', '辛': '金', '壬': '水', '癸': '水' };
const STEM_YINYANG = { '甲': '阳', '乙': '阴', '丙': '阳', '丁': '阴', '戊': '阳', '己': '阴', '庚': '阳', '辛': '阴', '壬': '阳', '癸': '阴' };
const ELEMENT_PRODUCES = { '木': '水', '火': '木', '土': '火', '金': '土', '水': '金' };
const ELEMENT_RESTRAINS = { '木': '金', '火': '水', '土': '木', '金': '火', '水': '土' };
const ELEMENT_SHENG = { '木': '火', '火': '土', '土': '金', '金': '水', '水': '木' };
const ELEMENT_KE = { '木': '土', '火': '金', '土': '水', '金': '木', '水': '火' };
const ELEMENT_BI = { '木': '金', '火': '水', '土': '木', '金': '火', '水': '土' };
// 地支藏干(主气、中气、余气)
const BRANCH_HIDDEN = {
'子': { '主气': '癸', '中气': '壬', '余气': '辛' },
'丑': { '主气': '己', '中气': '辛', '余气': '癸' },
'寅': { '主气': '甲', '中气': '丙', '余气': '戊' },
'卯': { '主气': '乙', '中气': '甲', '余气': '壬' },
'辰': { '主气': '戊', '中气': '乙', '余气': '癸' },
'巳': { '主气': '丙', '中气': '庚', '余气': '戊' },
'午': { '主气': '丁', '中气': '己', '余气': '乙' },
'未': { '主气': '己', '中气': '丁', '余气': '乙' },
'申': { '主气': '庚', '中气': '壬', '余气': '戊' },
'酉': { '主气': '辛', '中气': '庚', '余气': '丁' },
'戌': { '主气': '戊', '中气': '辛', '余气': '丁' },
'亥': { '主气': '壬', '中气': '甲', '余气': '戊' }
};
const HIDDEN_WEIGHT = { '主气': 1.0, '中气': 0.5, '余气': 0.3 };
// 地支藏干旺衰权重
const BRANCH_HIDDEN_WEIGHT = { '主气': 1.0, '中气': 0.5, '余气': 0.3 };
// 月令旺衰表(子平真诠)
const MONTH_STRENGTH = {
'寅': { '甲': 100, '乙': 80, '丙': 70, '丁': 60, '戊': 50, '己': 40, '庚': 30, '辛': 20, '壬': 10, '癸': 0 },
'卯': { '甲': 80, '乙': 100, '丙': 60, '丁': 70, '戊': 40, '己': 50, '庚': 20, '辛': 30, '壬': 10, '癸': 0 },
'辰': { '甲': 60, '乙': 70, '丙': 70, '丁': 80, '戊': 70, '己': 80, '庚': 50, '辛': 60, '壬': 40, '癸': 50 },
'巳': { '甲': 30, '乙': 40, '丙': 100, '丁': 80, '戊': 60, '己': 50, '庚': 40, '辛': 30, '壬': 10, '癸': 0 },
'午': { '甲': 20, '乙': 30, '丙': 80, '丁': 100, '戊': 50, '己': 60, '庚': 30, '辛': 40, '壬': 0, '癸': 10 },
'未': { '甲': 50, '乙': 60, '丙': 60, '丁': 70, '戊': 70, '己': 80, '庚': 50, '辛': 60, '壬': 20, '癸': 30 },
'申': { '甲': 20, '乙': 10, '丙': 30, '丁': 40, '戊': 50, '己': 60, '庚': 100, '辛': 80, '壬': 70, '癸': 50 },
'酉': { '甲': 10, '乙': 20, '丙': 20, '丁': 30, '戊': 40, '己': 50, '庚': 80, '辛': 100, '壬': 50, '癸': 70 },
'戌': { '甲': 50, '乙': 60, '丙': 70, '丁': 80, '戊': 70, '己': 80, '庚': 50, '辛': 60, '壬': 40, '癸': 50 },
'亥': { '甲': 70, '乙': 60, '丙': 20, '丁': 30, '戊': 30, '己': 40, '庚': 10, '辛': 20, '壬': 100, '癸': 80 },
'子': { '甲': 50, '乙': 40, '丙': 10, '丁': 20, '戊': 20, '己': 30, '庚': 0, '辛': 10, '壬': 80, '癸': 100 },
'丑': { '甲': 40, '乙': 50, '丙': 50, '丁': 60, '戊': 60, '己': 70, '庚': 50, '辛': 60, '壬': 50, '癸': 60 }
};
// 通根加分表
const TONGGEN_BONUS = {
'甲': { '寅': 50, '卯': 40, '亥': 20, '子': 0, '辰': 10, '未': 10, '戌': 10, '丑': 5 },
'乙': { '卯': 50, '寅': 30, '亥': 10, '子': 20, '辰': 10, '未': 15, '戌': 10, '丑': 10 },
'丙': { '巳': 50, '午': 40, '寅': 20, '卯': 10, '申': 0, '酉': 0, '辰': 5, '戌': 10, '丑': 5 },
'丁': { '午': 50, '巳': 30, '未': 15, '戌': 10, '寅': 10, '酉': 0, '申': 0, '辰': 5, '丑': 5 },
'戊': { '巳': 20, '午': 30, '辰': 40, '戌': 40, '丑': 30, '寅': 5, '卯': 5, '申': 0, '酉': 0, '亥': 0, '子': 0 },
'己': { '午': 20, '巳': 10, '辰': 30, '戌': 30, '丑': 40, '寅': 5, '卯': 5, '申': 0, '酉': 0, '亥': 5, '子': 5 },
'庚': { '申': 50, '酉': 40, '辰': 15, '戌': 15, '丑': 20, '寅': 0, '卯': 0, '巳': 0, '午': 0, '亥': 0, '子': 0 },
'辛': { '酉': 50, '申': 30, '辰': 10, '戌': 10, '丑': 15, '寅': 0, '卯': 0, '巳': 0, '午': 0, '亥': 0, '子': 0 },
'壬': { '亥': 50, '子': 40, '申': 20, '酉': 10, '辰': 10, '戌': 10, '丑': 15, '寅': 0, '卯': 0, '巳': 0, '午': 0 },
'癸': { '子': 50, '亥': 40, '丑': 20, '辰': 10, '戌': 10, '申': 5, '酉': 5, '寅': 0, '卯': 0, '巳': 0, '午': 0 }
};
// 穷通宝鉴调候用神表(完整版)
const TIAO_HOU_TABLE = {
// === 甲木日主 ===
'甲寅': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '庚', 说明: '寅月木寒,丙火为君,癸水为佐' },
'甲卯': { 主用神: ['丁', '丙'], 优先级: '丁先', 忌神: '庚', 说明: '卯月木旺,丁火泄秀,忌金' },
'甲辰': { 主用神: ['庚', '丁'], 优先级: '庚先丁后', 忌神: '癸', 说明: '辰月土旺,先庚后丁' },
'甲巳': { 主用神: ['癸', '丁'], 优先级: '癸先丁后', 忌神: '庚', 说明: '巳月火旺,癸水调候' },
'甲午': { 主用神: ['癸', '壬'], 优先级: '癸先', 忌神: '丁', 说明: '午月火旺,水为调候' },
'甲未': { 主用神: ['丁', '庚'], 优先级: '丁先', 忌神: '癸', 说明: '未月土月,用丁庚' },
'甲申': { 主用神: ['庚', '丁'], 优先级: '庚先丁后', 忌神: '癸', 说明: '申月金旺,庚劈甲引丁' },
'甲酉': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '庚', 说明: '酉月金旺,丁火制金' },
'甲戌': { 主用神: ['庚', '丁'], 优先级: '庚先丁后', 忌神: '癸', 说明: '戌月金土,用庚丁' },
'甲亥': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '庚', 说明: '亥月水冷,丙火调候' },
'甲子': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '庚', 说明: '子月水寒,丙戊并用' },
'甲丑': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '辛', 说明: '丑月寒湿,丁火暖局' },
// === 乙木日主 ===
'乙寅': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '辛', 说明: '寅月木寒,丙癸双清' },
'乙卯': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '辛', 说明: '卯月木旺,丙癸调候' },
'乙辰': { 主用神: ['癸', '丙'], 优先级: '癸先丙后', 忌神: '乙', 说明: '辰月湿土,癸水润乙' },
'乙巳': { 主用神: ['癸', '丙'], 优先级: '癸先丙后', 忌神: '辛', 说明: '巳月火旺,癸水调候' },
'乙午': { 主用神: ['癸', '壬'], 优先级: '癸先', 忌神: '丙', 说明: '午月火旺,癸水制火' },
'乙未': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '乙', 说明: '未月土月,丙癸并用' },
'乙申': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '辛', 说明: '申月金旺,丙癸并用' },
'乙酉': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '辛', 说明: '酉月金旺,丙火制金' },
'乙戌': { 主用神: ['癸', '辛'], 优先级: '癸先辛后', 忌神: '丙', 说明: '戌月燥土,癸水润局' },
'乙亥': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '辛', 说明: '亥月水冷,丙戊暖局' },
'乙子': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '辛', 说明: '子月水寒,丙戊调候' },
'乙丑': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '辛', 说明: '丑月寒湿,丙丁暖局' },
// === 丙火日主 ===
'丙寅': { 主用神: ['壬', '庚'], 优先级: '壬先庚后', 忌神: '癸', 说明: '寅月木火,壬水通月令' },
'丙卯': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '甲', 说明: '卯月木旺,壬癸制火' },
'丙辰': { 主用神: ['壬', '庚'], 优先级: '壬先庚后', 忌神: '戊', 说明: '辰月湿土,壬水通根' },
'丙巳': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '戊', 说明: '巳月火旺,壬水为用' },
'丙午': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丙', 说明: '午月火旺极,壬水调候' },
'丙未': { 主用神: ['壬', '庚'], 优先级: '壬先庚后', 忌神: '己', 说明: '未月土月,壬庚并用' },
'丙申': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '庚', 说明: '申月金水,壬水通根' },
'丙酉': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '辛', 说明: '酉月金旺,壬癸制火' },
'丙戌': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '丁', 说明: '戌月土金,壬甲并用' },
'丙亥': { 主用神: ['甲', '壬'], 优先级: '甲先壬后', 忌神: '辛', 说明: '亥月水冷,甲木生火' },
'丙子': { 主用神: ['甲', '壬'], 优先级: '甲先壬后', 忌神: '癸', 说明: '子月水旺,甲木生丙' },
'丙丑': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '己', 说明: '丑月寒湿,壬甲暖局' },
// === 丁火日主 ===
'丁寅': { 主用神: ['甲', '丙'], 优先级: '甲先丙后', 忌神: '壬', 说明: '寅月木旺,甲木生丁' },
'丁卯': { 主用神: ['甲', '丙'], 优先级: '甲先丙后', 忌神: '癸', 说明: '卯月木旺,甲丙生丁' },
'丁辰': { 主用神: ['甲', '庚'], 优先级: '甲先庚后', 忌神: '癸', 说明: '辰月土月,甲庚并用' },
'丁巳': { 主用神: ['甲', '庚'], 优先级: '甲先庚后', 忌神: '戊', 说明: '巳月火旺,甲庚制火' },
'丁午': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丁', 说明: '午月火旺,壬癸调候' },
'丁未': { 主用神: ['甲', '庚'], 优先级: '甲先庚后', 忌神: '丁', 说明: '未月土月,甲庚并用' },
'丁申': { 主用神: ['甲', '丙'], 优先级: '甲先丙后', 忌神: '壬', 说明: '申月金旺,甲丙生丁' },
'丁酉': { 主用神: ['甲', '丙'], 优先级: '甲先丙后', 忌神: '癸', 说明: '酉月金旺,甲丙生丁' },
'丁戌': { 主用神: ['甲', '壬'], 优先级: '甲先壬后', 忌神: '丁', 说明: '戌月燥土,壬水润局' },
'丁亥': { 主用神: ['甲', '庚'], 优先级: '甲先庚后', 忌神: '壬', 说明: '亥月水冷,甲庚暖局' },
'丁子': { 主用神: ['甲', '庚'], 优先级: '甲先庚后', 忌神: '癸', 说明: '子月水寒,甲庚暖局' },
'丁丑': { 主用神: ['甲', '庚'], 优先级: '甲先庚后', 忌神: '癸', 说明: '丑月寒湿,甲庚暖局' },
// === 戊土日主 ===
'戊寅': { 主用神: ['丙', '甲'], 优先级: '丙先甲后', 忌神: '壬', 说明: '寅月木旺,丙甲并用' },
'戊卯': { 主用神: ['丙', '甲'], 优先级: '丙先甲后', 忌神: '壬', 说明: '卯月木旺,丙甲并用' },
'戊辰': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '甲', 说明: '辰月湿土,丙癸调候' },
'戊巳': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '甲', 说明: '巳月火旺,丙癸并用' },
'戊午': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丙', 说明: '午月火旺极,壬癸调候' },
'戊未': { 主用神: ['癸', '丙'], 优先级: '癸先丙后', 忌神: '己', 说明: '未月土月,癸水润局' },
'戊申': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '壬', 说明: '申月金旺,丙丁暖局' },
'戊酉': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '癸', 说明: '酉月金旺,丙丁暖局' },
'戊戌': { 主用神: ['甲', '丁'], 优先级: '甲先丁后', 忌神: '壬', 说明: '戌月燥土,甲丁调候' },
'戊亥': { 主用神: ['丙', '甲'], 优先级: '丙先甲后', 忌神: '壬', 说明: '亥月水冷,丙甲暖局' },
'戊子': { 主用神: ['丙', '甲'], 优先级: '丙先甲后', 忌神: '壬', 说明: '子月水寒,丙甲暖局' },
'戊丑': { 主用神: ['丙', '甲'], 优先级: '丙先甲后', 忌神: '癸', 说明: '丑月寒湿,丙甲暖局' },
// === 己土日主 ===
'己寅': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '甲', 说明: '寅月木旺,丙癸暖局' },
'己卯': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '甲', 说明: '卯月木旺,丙癸暖局' },
'己辰': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '乙', 说明: '辰月湿土,丙癸调候' },
'己巳': { 主用神: ['癸', '丙'], 优先级: '癸先丙后', 忌神: '甲', 说明: '巳月火旺,癸水润局' },
'己午': { 主用神: ['癸', '壬'], 优先级: '癸先壬后', 忌神: '丙', 说明: '午月火旺,癸壬调候' },
'己未': { 主用神: ['癸', '丙'], 优先级: '癸先丙后', 忌神: '己', 说明: '未月土月,癸水润局' },
'己申': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '壬', 说明: '申月金旺,丙癸暖局' },
'己酉': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '辛', 说明: '酉月金旺,丙癸暖局' },
'己戌': { 主用神: ['癸', '辛'], 优先级: '癸先辛后', 忌神: '丙', 说明: '戌月燥土,癸水润燥' },
'己亥': { 主用神: ['丙', '辛'], 优先级: '丙先辛后', 忌神: '壬', 说明: '亥月水冷,丙辛暖局' },
'己子': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '癸', 说明: '子月水寒,丙丁暖局' },
'己丑': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '癸', 说明: '丑月寒湿,丙丁暖局' },
// === 庚金日主 ===
'庚寅': { 主用神: ['丁', '甲'], 优先级: '丁先甲后', 忌神: '壬', 说明: '寅月木旺,丁甲并用' },
'庚卯': { 主用神: ['丁', '甲'], 优先级: '丁先甲后', 忌神: '癸', 说明: '卯月木旺,丁甲制木' },
'庚辰': { 主用神: ['丁', '甲'], 优先级: '丁先甲后', 忌神: '壬', 说明: '辰月土月,丁甲并用' },
'庚巳': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丙', 说明: '巳月火旺,壬癸制火' },
'庚午': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丁', 说明: '午月火旺,壬癸调候' },
'庚未': { 主用神: ['丁', '甲'], 优先级: '丁先甲后', 忌神: '己', 说明: '未月土月,丁甲暖局' },
'庚申': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '壬', 说明: '申月金旺,丁丙制金' },
'庚酉': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '壬', 说明: '酉月金旺,丁丙制金' },
'庚戌': { 主用神: ['丁', '甲'], 优先级: '丁先甲后', 忌神: '辛', 说明: '戌月燥土,丁甲调候' },
'庚亥': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '壬', 说明: '亥月水冷,丁丙暖局' },
'庚子': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '癸', 说明: '子月水寒,丁丙暖局' },
'庚丑': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '癸', 说明: '丑月寒湿,丙丁暖局' },
// === 辛金日主 ===
'辛寅': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '丙', 说明: '寅月木旺,壬水化木' },
'辛卯': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '丙', 说明: '卯月木旺,壬甲并用' },
'辛辰': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '乙', 说明: '辰月土月,壬甲暖局' },
'辛巳': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丙', 说明: '巳月火旺,壬癸制火' },
'辛午': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丁', 说明: '午月火旺,壬癸调候' },
'辛未': { 主用神: ['丁', '甲'], 优先级: '丁先甲后', 忌神: '己', 说明: '未月土月,丁甲暖局' },
'辛申': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '庚', 说明: '申月金旺,壬水洗金' },
'辛酉': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '庚', 说明: '酉月金旺,壬水洗金' },
'辛戌': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '辛', 说明: '戌月燥土,丁丙暖局' },
'辛亥': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '壬', 说明: '亥月水冷,丙戊暖局' },
'辛子': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '丙', 说明: '子月水寒,壬甲暖局' },
'辛丑': { 主用神: ['壬', '庚'], 优先级: '壬先庚后', 忌神: '己', 说明: '丑月寒湿,壬庚暖局' },
// === 壬水日主 ===
'壬寅': { 主用神: ['庚', '戊'], 优先级: '庚先戊后', 忌神: '丙', 说明: '寅月木旺,庚金生水' },
'壬卯': { 主用神: ['庚', '辛'], 优先级: '庚先辛后', 忌神: '丙', 说明: '卯月木旺,庚辛生水' },
'壬辰': { 主用神: ['庚', '丙'], 优先级: '庚先丙后', 忌神: '甲', 说明: '辰月土月,庚丙并用' },
'壬巳': { 主用神: ['辛', '庚'], 优先级: '辛先庚后', 忌神: '戊', 说明: '巳月火旺,辛金化火' },
'壬午': { 主用神: ['辛', '癸'], 优先级: '辛先癸后', 忌神: '丁', 说明: '午月火旺,辛癸调候' },
'壬未': { 主用神: ['庚', '辛'], 优先级: '庚先辛后', 忌神: '己', 说明: '未月土月,庚辛生水' },
'壬申': { 主用神: ['戊', '丁'], 优先级: '戊先丁后', 忌神: '丙', 说明: '申月金旺,戊丁暖局' },
'壬酉': { 主用神: ['戊', '丁'], 优先级: '戊先丁后', 忌神: '丙', 说明: '酉月金旺,戊丁暖局' },
'壬戌': { 主用神: ['辛', '丙'], 优先级: '辛先丙后', 忌神: '甲', 说明: '戌月燥土,辛丙调候' },
'壬亥': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '庚', 说明: '亥月水冷,丙戊暖局' },
'壬子': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '庚', 说明: '子月水寒,丙戊暖局' },
'壬丑': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '己', 说明: '丑月寒湿,丙丁暖局' },
// === 癸水日主 ===
'癸寅': { 主用神: ['辛', '丙'], 优先级: '辛先丙后', 忌神: '壬', 说明: '寅月木旺,辛丙暖局' },
'癸卯': { 主用神: ['庚', '辛'], 优先级: '庚先辛后', 忌神: '壬', 说明: '卯月木旺,庚辛生水' },
'癸辰': { 主用神: ['辛', '丙'], 优先级: '辛先丙后', 忌神: '乙', 说明: '辰月湿土,辛丙暖局' },
'癸巳': { 主用神: ['辛', '壬'], 优先级: '辛先壬后', 忌神: '戊', 说明: '巳月火旺,辛壬调候' },
'癸午': { 主用神: ['癸', '壬'], 优先级: '癸先壬后', 忌神: '丁', 说明: '午月火旺,癸壬制火' },
'癸未': { 主用神: ['庚', '辛'], 优先级: '庚先辛后', 忌神: '己', 说明: '未月土月,庚辛生水' },
'癸申': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '壬', 说明: '申月金旺,丁丙暖局' },
'癸酉': { 主用神: ['辛', '丁'], 优先级: '辛先丁后', 忌神: '壬', 说明: '酉月金旺,辛金生水' },
'癸戌': { 主用神: ['辛', '壬'], 优先级: '辛先壬后', 忌神: '丙', 说明: '戌月燥土,辛壬润局' },
'癸亥': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '庚', 说明: '亥月水冷,丙戊暖局' },
'癸子': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '庚', 说明: '子月水寒,丙丁暖局' },
'癸丑': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '己', 说明: '丑月寒湿,丙丁暖局' },
};
// ============================================================
// 八字用神算法 - 增强版(穷通宝鉴 + 子平真诠)
// ============================================================
// 天干→十神映射(以日主为基准)
function stemToTenGods(dayMaster) {
return {
'甲': { '甲': '比肩', '乙': '劫财', '丙': '食神', '丁': '伤官', '戊': '偏财', '己': '正财', '庚': '七杀', '辛': '正官', '壬': '偏印', '癸': '正印' },
'乙': { '甲': '劫财', '乙': '比肩', '丙': '伤官', '丁': '食神', '戊': '正财', '己': '偏财', '庚': '正官', '辛': '七杀', '壬': '正印', '癸': '偏印' },
'丙': { '甲': '偏印', '乙': '正印', '丙': '比肩', '丁': '劫财', '戊': '食神', '己': '伤官', '庚': '偏财', '辛': '正财', '壬': '七杀', '癸': '正官' },
'丁': { '甲': '正印', '乙': '偏印', '丙': '劫财', '丁': '比肩', '戊': '伤官', '己': '食神', '庚': '正财', '辛': '偏财', '壬': '正官', '癸': '七杀' },
'戊': { '甲': '七杀', '乙': '正官', '丙': '偏印', '丁': '正印', '戊': '比肩', '己': '劫财', '庚': '食神', '辛': '伤官', '壬': '偏财', '癸': '正财' },
'己': { '甲': '正官', '乙': '七杀', '丙': '正印', '丁': '偏印', '戊': '劫财', '己': '比肩', '庚': '伤官', '辛': '食神', '壬': '正财', '癸': '偏财' },
'庚': { '甲': '偏财', '乙': '正财', '丙': '七杀', '丁': '正官', '戊': '偏印', '己': '正印', '庚': '比肩', '辛': '劫财', '壬': '食神', '癸': '伤官' },
'辛': { '甲': '正财', '乙': '偏财', '丙': '正官', '丁': '七杀', '戊': '正印', '己': '偏印', '庚': '劫财', '辛': '比肩', '壬': '伤官', '癸': '食神' },
'壬': { '甲': '食神', '乙': '伤官', '丙': '正财', '丁': '偏财', '戊': '七杀', '己': '正官', '庚': '偏印', '辛': '正印', '壬': '比肩', '癸': '劫财' },
'癸': { '甲': '伤官', '乙': '食神', '丙': '偏财', '丁': '正财', '戊': '正官', '己': '七杀', '庚': '正印', '辛': '偏印', '壬': '劫财', '癸': '比肩' },
}[dayMaster];
}
// 获取八字中所有天干和地支藏干
function getAllStemsAndHidden(palaces) {
const allStems = [];
const allHidden = []; // { stem, weight }
for (const p of palaces) {
if (p.stem) allStems.push(p.stem);
const hidden = BRANCH_HIDDEN[p.branch];
if (hidden) {
for (const [pos, stem] of Object.entries(hidden)) {
if (pos === '主气' || pos === '中气' || pos === '余气') {
allHidden.push({ stem, weight: BRANCH_HIDDEN_WEIGHT[pos] || 0 });
}
}
}
}
return { allStems, allHidden };
}
// 判断是否需要调候
function needsTiaoHou(monthBranch) {
const coldMonths = ['子', '丑', '亥'];
const hotMonths = ['巳', '午', '未'];
return { isCold: coldMonths.includes(monthBranch), isHot: hotMonths.includes(monthBranch) };
}
// 穷通宝鉴调候用神查询
function getTiaoHouByTable(dayStem, monthBranch) {
return TIAO_HOU_TABLE[`dayStemmonthBranch`] || null;
}
// 子平真诠格局判断
function calculatePattern(dayStem, monthBranch, monthStem, strengthScore) {
const me = dayStem;
const hidden = BRANCH_HIDDEN[monthBranch];
const tenGodsMap = stemToTenGods(me);
// Step 1: 子平真诠月令取用
// 规则:月令本气透干以透出为用,否则取本气
let yongshenStem = hidden['主气'];
if (monthStem && monthStem !== '' && [hidden['主气'], hidden['中气'], hidden['余气']].includes(monthStem)) {
// 月干透出,以透出为用
if (monthStem === hidden['主气'] || monthStem === hidden['中气']) {
yongshenStem = monthStem;
}
}
const tenGod = tenGodsMap[yongshenStem] || '比肩';
// Step 2: 判断格局类型
let patternType = '正格';
const isStrong = strengthScore >= 220;
const isWeak = strengthScore < 150;
// 从格判断:日主极弱时
if (isWeak) {
if (tenGod === '正官' || tenGod === '七杀') patternType = '从官杀格';
else if (tenGod === '正财' || tenGod === '偏财') patternType = '从财格';
else if (tenGod === '食神' || tenGod === '伤官') patternType = '从食伤格';
else patternType = '正格';
}
// 专旺格:日主极强时
else if (strengthScore >= 380) {
patternType = '专旺格';
}
// Step 3: 善用神判断
// 身旺用官杀/财/食伤为善;身弱用印比为善
let isGood = true;
if (isStrong) {
if (['比肩', '劫财', '偏印', '正印'].includes(tenGod)) isGood = false;
} else if (isWeak) {
if (['七杀', '正官', '偏财', '正财', '食神', '伤官'].includes(tenGod)) isGood = false;
}
// Step 4: 格局名称
let patternName = tenGod;
if (patternType !== '正格') {
patternName = patternType;
}
// Step 5: 格局用神
let patternYongshen = '';
if (patternType === '正格') {
patternYongshen = yongshenStem;
// 身旺:取克泄;身弱:取生助
if (isStrong) {
const ke = ELEMENT_KE[STEM_ELEMENT[me]];
const bi = ELEMENT_BI[STEM_ELEMENT[me]];
const sheng = ELEMENT_SHENG[STEM_ELEMENT[me]];
// 官杀、财、食伤皆可用
patternYongshen = `yongshenStem(可辅以ke、bi)`;
} else if (isWeak) {
const sheng = ELEMENT_SHENG[STEM_ELEMENT[me]];
const wuxing = STEM_ELEMENT[me];
patternYongshen = `yongshenStem(可辅以sheng、wuxing)`;
}
}
return {
patternName,
patternType,
tenGod,
yongshenStem,
patternYongshen,
isGood,
desc: `monthBranch月令,yongshenStem为用,取tenGod(patternType)`
};
}
// 子平真诠日主强弱判断(增强版)
function calculateStrengthEnhanced(dayMaster, monthBranch, palaces) {
const me = dayMaster;
const myElement = STEM_ELEMENT[me];
// 1. 得令分(月令旺衰)
const monthScore = MONTH_STRENGTH[monthBranch]?.[me] ?? 0;
// 2. 得地分(地支根气 - 通根)
let tonggenScore = 0;
for (const p of palaces) {
const bonus = TONGGEN_BONUS[me]?.[p.branch] ?? 0;
if (bonus > 0) tonggenScore += bonus;
}
// 3. 得助分(天干印比帮身)
let bizhuScore = 0;
let yinScore = 0;
for (const p of palaces) {
const stem = p.stem;
const stemElement = STEM_ELEMENT[stem];
if (stemElement === myElement && stem !== me) {
bizhuScore += 20; // 比肩/劫财
}
if (ELEMENT_PRODUCES[myElement] === stemElement) {
yinScore += 15; // 印星
}
}
// 4. 地支藏干中的印比
for (const p of palaces) {
const hidden = BRANCH_HIDDEN[p.branch];
if (hidden) {
for (const [pos, stem] of Object.entries(hidden)) {
if (pos === '主气' || pos === '中气' || pos === '余气') {
const stemElement = STEM_ELEMENT[stem];
const weight = BRANCH_HIDDEN_WEIGHT[pos] || 0;
if (stemElement === myElement) {
bizhuScore += 15 * weight;
}
if (ELEMENT_PRODUCES[myElement] === stemElement) {
yinScore += 10 * weight;
}
}
}
}
}
const totalScore = monthScore + tonggenScore + bizhuScore + yinScore;
// 5. 等级判断
let level = '中和';
if (totalScore < 80) level = '极弱';
else if (totalScore < 150) level = '弱';
else if (totalScore < 220) level = '偏弱';
else if (totalScore < 300) level = '中和';
else if (totalScore < 380) level = '偏强';
else if (totalScore < 450) level = '强';
else level = '极强';
// 6. 用神方向
let direction = '中和难取';
let directionDesc = '';
if (level.includes('弱')) {
direction = '扶抑-扶';
directionDesc = '宜取印比生扶';
} else if (level.includes('强')) {
direction = '扶抑-抑';
directionDesc = '宜取官杀财食克泄';
}
return {
level,
score: Math.round(totalScore),
monthScore: Math.round(monthScore),
tonggenScore: Math.round(tonggenScore),
bizhuScore: Math.round(bizhuScore),
yinScore: Math.round(yinScore),
totalScore,
direction,
directionDesc,
weightBreakdown: `月令monthScore分 + 通根tonggenScore分 + 比劫bizhuScore分 + 印绶yinScore分`
};
}
// 综合用神计算(穷通宝鉴 + 子平真诠)
// 参数:dayMaster{stem, wuxing}, monthBranch, monthStem, palaces, strength{level, score, direction}
function calculateYongshenEnhanced(dayMaster, monthBranch, monthStem, palaces, strength) {
const me = dayMaster.originalStem;
const myElement = dayMaster.wuxing;
const results = [];
const { allStems, allHidden } = getAllStemsAndHidden(palaces);
// === 1. 调候用神(穷通宝鉴) ===
const tiaohouRule = getTiaoHouByTable(me, monthBranch);
if (tiaohouRule) {
const tiaohouPresent = tiaohouRule['主用神'].filter(g => allStems.includes(g));
const tiaohouAbsent = tiaohouRule['主用神'].filter(g => !allStems.includes(g));
const status = tiaohouPresent.length === tiaohouRule['主用神'].length ? '调候俱全' :
tiaohouPresent.length > 0 ? '调候不全' : '调候皆缺';
results.push({
type: '调候',
values: tiaohouPresent.length > 0 ? tiaohouPresent : tiaohouAbsent,
primary: tiaohouRule['主用神'][0],
present: tiaohouPresent,
absent: tiaohouAbsent,
status,
priority: tiaohouRule['优先级'],
avoid: tiaohouRule['忌神'],
desc: tiaohouRule['说明'],
isUrgent: needsTiaoHou(monthBranch).isCold || needsTiaoHou(monthBranch).isHot
});
}
// === 2. 格局用神(子平真诠) ===
const pattern = calculatePattern(me, monthBranch, monthStem, strength.score);
results.push({
type: '格局',
value: pattern.yongshenStem,
patternName: pattern.patternName,
patternType: pattern.patternType,
tenGod: pattern.tenGod,
isGood: pattern.isGood,
desc: pattern.desc,
detail: pattern.patternYongshen
});
// === 3. 通关用神 ===
// 当月令与日主相克时需要通关
const monthElement = STEM_ELEMENT[BRANCH_HIDDEN[monthBranch]?.['主气'] || monthBranch];
const myKe = ELEMENT_KE[myElement]; // 日主所克
const mySheng = ELEMENT_SHENG[myElement]; // 日主所生
// 月令克日主 → 用印通关
if (ELEMENT_RESTRAINS[monthElement] === myElement) {
const mediator = ELEMENT_PRODUCES[monthElement]; // 月令的印星可通关
if (mediator && !results.some(r => r.values?.includes(mediator))) {
results.push({ type: '通关', value: mediator, desc: `monthElement克myElement,以mediator通关`, isUrgent: false });
}
}
// 日主克月令 → 用食伤通关
if (ELEMENT_RESTRAINS[myElement] === monthElement) {
const biElement = ELEMENT_BI[myElement]; // 日主所泄(食伤)
if (biElement && !results.some(r => r.values?.includes(biElement))) {
results.push({ type: '通关', value: biElement, desc: `myElement克monthElement,以biElement通关`, isUrgent: false });
}
}
// === 4. 扶抑用神 ===
if (strength.direction === '扶抑-扶') {
results.push({
type: '扶抑',
values: [myElement, ELEMENT_SHENG[myElement]],
desc: `身strength.level,宜取myElement、ELEMENT_SHENG[myElement]生助`
});
} else if (strength.direction === '扶抑-抑') {
results.push({
type: '扶抑',
values: [ELEMENT_KE[myElement], ELEMENT_BI[myElement]],
desc: `身strength.level,宜取ELEMENT_KE[myElement]、ELEMENT_BI[myElement]克泄`
});
}
// === 综合排序 ===
// 优先级:调候(紧急时)> 格局 > 通关 > 扶抑
// 调候在寒月(亥子丑)和热月(巳午未)为急
const tiaohou = results.find(r => r.type === '调候');
const isUrgentTiaohou = tiaohou?.isUrgent;
// 构建最终用神列表
const finalDetails = [];
if (isUrgentTiaohou && tiaohou) {
finalDetails.push({ type: '调候(急)', value: tiaohou.primary, desc: tiaohou.desc });
}
const patternResult = results.find(r => r.type === '格局');
if (patternResult) {
finalDetails.push({ type: '格局', value: patternResult.value, desc: patternResult.desc });
}
const touguanResult = results.find(r => r.type === '通关');
if (touguanResult) {
finalDetails.push({ type: '通关', value: touguanResult.value, desc: touguanResult.desc });
}
const fuyiResult = results.find(r => r.type === '扶抑');
if (fuyiResult) {
for (const v of fuyiResult.values || []) {
if (!finalDetails.some(d => d.value === v)) {
finalDetails.push({ type: '扶抑', value: v, desc: fuyiResult.desc });
}
}
}
// 非紧急的调候也加入
if (!isUrgentTiaohou && tiaohou && tiaohou.values) {
for (const v of tiaohou.values) {
if (!finalDetails.some(d => d.value === v)) {
finalDetails.push({ type: '调候', value: v, desc: tiaohou.desc });
}
}
}
// 去重
const seen = new Set();
const uniqueDetails = finalDetails.filter(d => {
if (seen.has(d.value)) return false;
seen.add(d.value);
return true;
});
const primary = uniqueDetails[0]?.value || me;
const secondary = uniqueDetails.slice(1, 4).map(d => d.value);
let tiaohouSummary = '无调候';
if (tiaohou) {
const urgent = isUrgentTiaohou ? '(急)' : '';
// status如"调候俱全",去掉前缀"调候"再拼
const statusPart = (tiaohou.status || '').replace(/^调候/, '');
tiaohouSummary = `调候urgentstatusPart`;
}
let summary = `tiaohouSummary,格局patternResult?.patternName || '待定'`;
if (touguanResult) summary += `,需touguanResult.value通关`;
return {
primary,
secondary,
details: uniqueDetails.slice(0, 5),
summary,
tiaohouStatus: tiaohou ? { present: tiaohou.present, absent: tiaohou.absent, status: tiaohou.status, avoid: tiaohou.avoid } : null,
pattern: patternResult ? { name: patternResult.patternName, type: patternResult.patternType, isGood: patternResult.isGood, tenGod: patternResult.tenGod } : null,
strengthDirection: strength.direction,
fullAnalysis: results
};
}
// 旧版兼容函数 - 保留接口兼容(内部调用增强版)
function getTiaohouYongshen(wuxing, monthBranch) {
// 兼容旧接口:monthBranch可以是地支或月令对象
const branch = typeof monthBranch === 'string' ? monthBranch : (monthBranch?.zhi || monthBranch?.branch || '寅');
// 遍历找主用神
for (const [key, rule] of Object.entries(TIAO_HOU_TABLE)) {
const dayStem = key[0];
const mz = key.slice(1);
if (mz === branch) {
return rule['主用神'][0];
}
}
return null;
}
function getTiaohouDesc(dayStem, monthBranch) {
const rule = getTiaoHouByTable(dayStem, monthBranch);
if (!rule) return '';
const { isCold, isHot } = needsTiaoHou(monthBranch);
let season = '';
if (isCold) season = '寒月';
else if (isHot) season = '热月';
return `''rule['说明']`;
}
// ============================================================
// 核心排盘分析
// ============================================================
function analyzePlate(year, month, day, hour, minute, sex) {
const dateStr = minute > 0
? `year-month-day hour:String(minute).padStart(2, '0')`
: `year-month-day hour`;
const gender = sex === 1 ? 1 : 0;
const astrolabe = astro.bySolar(dateStr, gender, true, 'zh-CN');
// 收集十二宫数据
const palaces = astrolabe.palaces.map((p, idx) => ({
index: idx,
name: p.name,
duty: p.name,
stem: p.heavenlyStem,
branch: p.earthlyBranch,
majorStars: p.majorStars || [],
minorStars: p.minorStars || [],
adjectiveStars: p.adjectiveStars || [],
changsheng12: p.changsheng12 || '',
boshi12: p.boshi12 || '',
jiangqian12: p.jiangqian12 || '',
suiqian12: p.suiqian12 || '',
decadal: p.decadal || {}
}));
let mingIdx = -1;
palaces.forEach((p, idx) => {
if (p.name === '命宫') mingIdx = idx;
});
// 四化信息
const transforms = [];
for (const starName of MAIN_STARS) {
try {
const star = astrolabe.star(starName);
if (star && star.mutagen) {
transforms.push({ star: starName, hua: star.mutagen });
}
} catch (e) { /* skip */ }
}
// 八字信息
const eightChar = astrolabe.chineseDate.split(' ');
// eightChar = ['乙亥', '甲申', '戊寅', '壬子'] -> [年柱, 月柱, 日柱, 时柱]
const yearStem = eightChar[0]?.[0] || '甲'; // 年干 = 乙
const monthStem = eightChar[1]?.[0] || '甲'; // 月干 = 甲
const dayStem = eightChar[2]?.[0] || '甲'; // 日干 = 戊
const monthBranch = eightChar[1]?.[1] || '寅'; // 月支 = 申
// 日主信息
const dayMaster = getDayMaster(dayStem);
const monthInfo = getMonthInfo(monthBranch);
// 计算强弱(子平真诠增强版)
const strength = calculateStrengthEnhanced(dayStem, monthBranch, palaces);
// 构建兼容旧接口的strength对象
const strengthCompat = {
helpScore: strength.bizhuScore + strength.yinScore,
stressScore: 0,
total: strength.score,
strength: strength.level,
needSupport: strength.direction === '扶抑-抑' ? [ELEMENT_KE[dayMaster.wuxing]] : [ELEMENT_SHENG[dayMaster.wuxing], dayMaster.wuxing],
needAvoid: strength.direction === '扶抑-抑' ? [dayMaster.wuxing, ELEMENT_SHENG[dayMaster.wuxing]] : [ELEMENT_KE[dayMaster.wuxing]],
level: strength.level,
score: strength.score,
monthScore: strength.monthScore,
tonggenScore: strength.tonggenScore,
bizhuScore: strength.bizhuScore,
yinScore: strength.yinScore,
direction: strength.direction,
directionDesc: strength.directionDesc,
weightBreakdown: strength.weightBreakdown
};
// 用神计算(穷通宝鉴 + 子平真诠增强版)
const yongshen = calculateYongshenEnhanced(dayMaster, monthBranch, monthStem, palaces, strength);
// 格局检测(知识库驱动)
const knowledgePatterns = checkPatternsFromKnowledge(palaces, mingIdx, transforms, yearStem);
// 传统格局检测(补充)
const traditionalPatterns = checkTraditionalPatterns(palaces, mingIdx, transforms);
// 合并格局
const allPatterns = mergePatterns(knowledgePatterns, traditionalPatterns);
// 大运分析
const decadalAnalysis = analyzeDecadal(astrolabe, year, month, day, gender, mingIdx, palaces);
// 流年分析
const yearlyAnalysis = analyzeYearly(astrolabe, year, month, day, gender, palaces);
return {
basic: {
year, month, day, hour, minute,
sex: sex === 1 ? '男' : '女',
chineseDate: astrolabe.chineseDate,
fiveElements: astrolabe.fiveElementsClass,
soul: astrolabe.soul,
body: astrolabe.body,
zodiac: astrolabe.zodiac,
sign: astrolabe.sign,
palaces,
mingIdx,
transforms,
yearStem,
dayStem,
monthBranch,
astrolabe
},
analysis: {
dayStem,
dayMaster,
monthZhi: monthBranch,
monthInfo,
...strengthCompat,
yongshen
},
patterns: allPatterns,
decadal: decadalAnalysis,
yearly: yearlyAnalysis
};
}
// ============================================================
// 日主与月令
// ============================================================
function getDayMaster(stem) {
const masters = {
'甲': { name: '甲木', wuxing: '木', stem: '阳木', originalStem: '甲' },
'乙': { name: '乙木', wuxing: '木', stem: '阴木', originalStem: '乙' },
'丙': { name: '丙火', wuxing: '火', stem: '阳火', originalStem: '丙' },
'丁': { name: '丁火', wuxing: '火', stem: '阴火', originalStem: '丁' },
'戊': { name: '戊土', wuxing: '土', stem: '阳土', originalStem: '戊' },
'己': { name: '己土', wuxing: '土', stem: '阴土', originalStem: '己' },
'庚': { name: '庚金', wuxing: '金', stem: '阳金', originalStem: '庚' },
'辛': { name: '辛金', wuxing: '金', stem: '阴金', originalStem: '辛' },
'壬': { name: '壬水', wuxing: '水', stem: '阳水', originalStem: '壬' },
'癸': { name: '癸水', wuxing: '水', stem: '阴水', originalStem: '癸' }
};
return masters[stem] || masters['甲'];
}
function getMonthInfo(zhi) {
const infos = {
'寅': { element: '木', strength: '旺', score: 3, season: '春' },
'卯': { element: '木', strength: '旺', score: 3, season: '春' },
'辰': { element: '木', strength: '墓', score: 0, season: '春' },
'巳': { element: '火', strength: '相', score: 2, season: '夏' },
'午': { element: '火', strength: '旺', score: 3, season: '夏' },
'未': { element: '火', strength: '墓', score: 0, season: '夏' },
'申': { element: '金', strength: '旺', score: 3, season: '秋' },
'酉': { element: '金', strength: '旺', score: 3, season: '秋' },
'戌': { element: '金', strength: '墓', score: 0, season: '秋' },
'亥': { element: '水', strength: '旺', score: 3, season: '冬' },
'子': { element: '水', strength: '旺', score: 3, season: '冬' },
'丑': { element: '土', strength: '旺', score: 3, season: '冬' }
};
return infos[zhi] || { element: '土', strength: '平', score: 1, season: '四季' };
}
function getWuxing(stem) {
const map = { '甲': '木', '乙': '木', '丙': '火', '丁': '火', '戊': '土', '己': '土', '庚': '金', '辛': '金', '壬': '水', '癸': '水' };
return map[stem] || '土';
}
// ============================================================
// 命盘强弱分析(旧版兼容wrapper,调用增强版)
// ============================================================
function calculateStrength(dayMaster, monthInfo, palaces, mingIdx) {
// 调用增强版算法
const monthBranch = monthInfo?.zhi || monthInfo?.branch || monthInfo?.branch || '寅';
const enhanced = calculateStrengthEnhanced(dayMaster.originalStem, monthBranch, palaces);
// 兼容旧接口
const wuxing = dayMaster.wuxing;
let needSupport = [], needAvoid = [];
if (enhanced.direction === '扶抑-抑') {
needSupport = [ELEMENT_KE[wuxing]];
needAvoid = [wuxing, ELEMENT_SHENG[wuxing]];
} else if (enhanced.direction === '扶抑-扶') {
needSupport = [ELEMENT_SHENG[wuxing], wuxing];
needAvoid = [ELEMENT_KE[wuxing]];
}
return {
helpScore: enhanced.bizhuScore + enhanced.yinScore,
stressScore: 0,
total: enhanced.score,
strength: enhanced.level,
needSupport,
needAvoid,
// 增强版额外字段
level: enhanced.level,
score: enhanced.score,
monthScore: enhanced.monthScore,
tonggenScore: enhanced.tonggenScore,
bizhuScore: enhanced.bizhuScore,
yinScore: enhanced.yinScore,
direction: enhanced.direction,
directionDesc: enhanced.directionDesc,
weightBreakdown: enhanced.weightBreakdown
};
}
// ============================================================
// 八字用神计算(旧版兼容wrapper,调用增强版)
// ============================================================
function calculateYongshen(dayMaster, monthInfo, palaces, mingIdx, strength) {
const dayStem = dayMaster.stem;
const monthBranch = monthInfo?.zhi || monthInfo?.branch || '寅';
const monthStem = monthInfo?.stem || '';
return calculateYongshenEnhanced(dayMaster, monthBranch, monthStem, palaces, strength);
}
function findWeakestLink(palaces, mingIdx, wuxing) {
const counts = {};
MAIN_STARS.forEach(s => counts[s] = 0);
for (const p of palaces) {
for (const s of p.majorStars || []) {
if (MAIN_STARS.includes(s.name)) counts[s.name]++;
}
}
// 检查是否有某主星完全缺失
const missing = Object.entries(counts).filter(([k, v]) => v === 0).map(([k]) => k);
if (missing.length > 2) {
return { remedy: missing[0], desc: `命局缺missing[0]` };
}
return null;
}
// ============================================================
// 传统格局检测(补充知识库)
// ============================================================
function checkTraditionalPatterns(palaces, mingIdx, transforms) {
const patterns = [];
const mingPalace = palaces[mingIdx];
const mingStars = mingPalace?.majorStars?.map(s => s.name) || [];
const mingBranch = mingPalace?.branch || '';
const allMingStars = [
...mingStars,
...(mingPalace?.minorStars?.map(s => s.name) || []),
...(mingPalace?.adjectiveStars?.map(s => s.name) || [])
];
const getSanfang = () => {
const opp = (mingIdx + 6) % 12;
const t1 = (mingIdx + 4) % 12;
const t2 = (mingIdx + 8) % 12;
return [
...(palaces[opp]?.majorStars?.map(s => s.name) || []),
...(palaces[t1]?.majorStars?.map(s => s.name) || []),
...(palaces[t2]?.majorStars?.map(s => s.name) || [])
];
};
const sanfang = getSanfang();
const allSanfang = [...allMingStars, ...sanfang];
const has = (stars, names) => names.every(n => stars.includes(n));
const hasAny = (stars, names) => names.some(n => stars.includes(n));
const prevIdx = (mingIdx - 1 + 12) % 12;
const nextIdx = (mingIdx + 1) % 12;
const prevStars = palaces[prevIdx]?.minorStars?.map(s => s.name) || [];
const nextStars = palaces[nextIdx]?.minorStars?.map(s => s.name) || [];
// 紫府同宫
if (has(mingStars, ['紫微', '天府']) && ['寅','申'].includes(mingBranch)) {
patterns.push({ name: '紫府同宫', level: '贵', desc: '最吉之格,富贵双全', source: 'traditional' });
}
// 杀破狼
if (['贪狼','七杀','破军'].filter(s => sanfang.includes(s)).length >= 2) {
patterns.push({ name: '杀破狼', level: '变', desc: '动荡变革,破旧立新', source: 'traditional' });
}
// 机月同梁
if (['天机','太阴','天同','天梁'].filter(s => sanfang.includes(s)).length >= 3) {
patterns.push({ name: '机月同梁', level: '富', desc: '善谋稳定,公职之命', source: 'traditional' });
}
// 七杀朝斗
if (has(mingStars, ['七杀']) && ['子','午','寅','申'].includes(mingBranch)) {
patterns.push({ name: '七杀朝斗', level: '贵', desc: '威镇边疆,将相之才', source: 'traditional' });
}
// 石中隐
// 左右同宫
if (has(mingStars, ['左辅', '右弼'])) {
patterns.push({ name: '左右同宫', level: '贵', desc: '辅助有力,秉性宽厚', source: 'traditional' });
}
// 魁钺相遇
if (hasAny(mingStars, ['天魁', '天钺'])) {
if (has(mingStars, ['天魁', '天钺'])) {
patterns.push({ name: '魁钺相遇', level: '贵', desc: '贵人相助,文武双全', source: 'traditional' });
}
}
// 天乙拱命
if (hasAny(sanfang, ['天魁', '天钺'])) {
patterns.push({ name: '天乙拱命', level: '贵', desc: '多贵人助,学识出众', source: 'traditional' });
}
// 羊陀夹命
if ((prevStars.includes('擎羊') && nextStars.includes('陀罗')) ||
(prevStars.includes('陀罗') && nextStars.includes('擎羊'))) {
patterns.push({ name: '羊陀夹命', level: '凶', desc: '守财奴,钱财难聚', source: 'traditional' });
}
// 火铃夹命
if ((prevStars.includes('火星') && nextStars.includes('铃星')) ||
(prevStars.includes('铃星') && nextStars.includes('火星'))) {
patterns.push({ name: '火铃夹命', level: '凶', desc: '叛逆冲动,易惹祸端', source: 'traditional' });
}
// 空劫夹命
if ((prevStars.includes('地空') && nextStars.includes('地劫')) ||
(prevStars.includes('地劫') && nextStars.includes('地空'))) {
patterns.push({ name: '空劫夹命', level: '凶', desc: '精神孤独,钱难聚', source: 'traditional' });
}
// 命无正曜
if (mingStars.length === 0) {
patterns.push({ name: '命无正曜', level: '平', desc: '可塑性高,运势受环境影响大', source: 'traditional' });
}
// 日月同宫
if (has(mingStars, ['太阳', '太阴'])) {
patterns.push({ name: '日月同宫', level: '中', desc: '贵富,妨弟兄', source: 'traditional' });
}
// 贪武同行
if (has(mingStars, ['贪狼', '武曲'])) {
patterns.push({ name: '贪武同行', level: '富', desc: '大富,奔波后成', source: 'traditional' });
}
// 三奇加会
const transNames = transforms.map(t => t.hua);
if (transNames.includes('禄') && transNames.includes('权') && transNames.includes('科')) {
patterns.push({ name: '三奇加会', level: '贵', desc: '志向远大,运气极佳', source: 'traditional' });
}
// 明珠出海
const yiPalace = palaces.find(p => p.name === '迁移');
if (yiPalace && ['太阳','太阴'].some(s => yiPalace.majorStars?.map(x => x.name).includes(s))) {
patterns.push({ name: '明珠出海', level: '富', desc: '远行得名,利学术', source: 'traditional' });
}
return patterns;
}
// ============================================================
// 合并格局(去重,知识库优先)
// ============================================================
function mergePatterns(knowledgePatterns, traditionalPatterns) {
const map = new Map();
for (const p of traditionalPatterns) {
if (!map.has(p.name)) map.set(p.name, p);
}
for (const p of knowledgePatterns) {
if (!map.has(p.name)) {
map.set(p.name, { ...p, source: 'knowledge' });
}
}
const all = Array.from(map.values());
// 按等级排序
const levelOrder = { '贵': 1, '富': 2, '中': 3, '平': 4, '变': 5, '凶': 6 };
all.sort((a, b) => (levelOrder[a.level] || 9) - (levelOrder[b.level] || 9));
return all;
}
// ============================================================
// 大运分析
// ============================================================
function analyzeDecadal(astrolabe, birthYear, birthMonth, birthDay, gender, mingIdx, palaces) {
const results = [];
const currentYear = new Date().getFullYear();
const currentAge = currentYear - birthYear;
// 计算每步大运
// 大运从命宫开始,每步大运10年
// 大运地支顺序:寅→卯→辰→巳→午→未→申→酉→戌→亥→子→丑
const branchOrder = ['寅','卯','辰','巳','午','未','申','酉','戌','亥','子','丑'];
const stemOrder = ['甲','乙','丙','丁','戊','己','庚','辛','壬','癸'];
// 命宫地支
const mingBranch = palaces[mingIdx]?.branch || '寅';
const mingBranchIdx = branchOrder.indexOf(mingBranch);
// 五虎遁起月干(简化版)
const tigerRule = { '甲': '丙', '乙': '戊', '丙': '庚', '丁': '辛', '戊': '壬', '己': '甲', '庚': '丙', '辛': '戊', '壬': '庚', '癸': '壬' };
// 计算命宫天干
// iztro的算法:命宫天干 = 五虎遁(年干)
// 这里用简化:年干对应的五虎遁月干,再结合命宫地支推算
// 获取出生年干
const yearStem = astrolabe.chineseDate.split(' ')[1]?.[0] || '甲';
const startStem = tigerRule[yearStem] || '丙';
const startStemIdx = stemOrder.indexOf(startStem);
// 命宫天干索引
const mingStemIdx = (startStemIdx + mingBranchIdx) % 10;
for (let i = 0; i < 12; i++) {
const branchIdx = (mingBranchIdx + i) % 12;
const stemIdx = (mingStemIdx + i) % 10;
const ageStart = i * 10;
const ageEnd = ageStart + 9;
const midAge = ageStart + 5;
// 检查是否当前大运
const isCurrent = currentAge >= ageStart && currentAge <= ageEnd;
// 获取大运星曜(通过horoscope)
let decadalStars = [];
let mutagen = [];
if (isCurrent) {
try {
const today = new Date();
const h = astrolabe.horoscope(today);
decadalStars = h.decadal?.stars || [];
mutagen = h.decadal?.mutagen || [];
} catch (e) { /* skip */ }
}
// 大运宫名
const palaceIdx = (mingIdx + i) % 12;
const palaceName = palaces[palaceIdx]?.name || '命宫';
const palaceBranch = palaces[palaceIdx]?.branch || branchOrder[branchIdx];
// 大运运势评估
const luckScore = evaluateDecadalLuck(palaces[palaceIdx], decadalStars, mutagen);
results.push({
index: i,
ageStart,
ageEnd,
stem: stemOrder[stemIdx],
branch: branchOrder[branchIdx],
palaceName,
palaceBranch,
isCurrent,
stars: decadalStars,
mutagen,
luck: luckScore
});
}
return results;
}
function evaluateDecadalLuck(palace, decadalStars, mutagen) {
let score = 0;
const allStars = [
...(palace?.majorStars?.map(s => s.name) || []),
...(palace?.minorStars?.map(s => s.name) || [])
];
for (const star of allStars) {
if (LUCKY_STARS.includes(star)) score += 2;
if (UNLUCKY_STARS.includes(star)) score -= 1;
}
for (const star of decadalStars) {
if (star.type === 'soft' || star.type === 'flower' || star.type === 'lucun') score += 1;
if (star.type === 'tough') score -= 0.5;
}
let level = '平常';
if (score >= 4) level = '大吉';
else if (score >= 2) level = '吉祥';
else if (score >= 0) level = '平稳';
else if (score >= -2) level = '小逆';
else level = '不顺';
return { score: +score.toFixed(1), level };
}
// ============================================================
// 流年分析
// ============================================================
function analyzeYearly(astrolabe, birthYear, birthMonth, birthDay, gender, palaces) {
const currentYear = new Date().getFullYear();
const currentMonth = new Date().getMonth() + 1;
try {
const today = new Date();
const h = astrolabe.horoscope(today);
const yearly = h.yearly;
const age = h.age;
// 流年命宫位置
const yearlyPalaceIdx = yearly?.index ?? 0;
const yearlyPalaceName = yearly?.palaceNames?.[yearlyPalaceIdx] || '命宫';
const yearlyStem = yearly?.heavenlyStem || '甲';
const yearlyBranch = yearly?.earthlyBranch || '子';
// 流年星
const yearlyStars = yearly?.stars || [];
// 流年四化
const yearlyMutagen = yearly?.mutagen || [];
// 小限
const agePalaceIdx = age?.index ?? 0;
const agePalaceName = age?.palaceNames?.[agePalaceIdx] || '命宫';
const ageStem = age?.heavenlyStem || '甲';
const ageBranch = age?.earthlyBranch || '子';
// 评估流年
const yearlyScore = evaluateYearlyLuck(yearlyStars, yearlyMutagen, palaces[yearlyPalaceIdx]);
// 未来5年流年简览
const nextYears = [];
for (let i = 0; i < 5; i++) {
const yr = currentYear + i;
try {
const date = new Date(yr + '-08-15');
const hy = astrolabe.horoscope(date);
nextYears.push({
year: yr,
stem: hy.yearly?.heavenlyStem || '',
branch: hy.yearly?.earthlyBranch || '',
palaceIdx: hy.yearly?.index || 0,
palaceName: hy.yearly?.palaceNames?.[hy.yearly?.index || 0] || ''
});
} catch (e) {
nextYears.push({ year: yr, stem: '', branch: '', palaceName: '(计算)' });
}
}
return {
current: {
year: currentYear,
stem: yearlyStem,
branch: yearlyBranch,
palaceName: yearlyPalaceName,
palaceIdx: yearlyPalaceIdx,
stars: yearlyStars,
mutagen: yearlyMutagen,
score: yearlyScore
},
age: {
nominalAge: age?.nominalAge || currentYear - birthYear,
stem: ageStem,
branch: ageBranch,
palaceName: agePalaceName,
palaceIdx: agePalaceIdx
},
nextYears
};
} catch (e) {
return { error: e.message, current: null, nextYears: [] };
}
}
function evaluateYearlyLuck(stars, mutagen, palace) {
let score = 0;
for (const star of stars) {
if (star.type === 'soft' || star.type === 'flower' || star.type === 'lucun') score += 1;
if (star.type === 'tough') score -= 1;
}
for (const m of mutagen) {
if (['禄','权','科'].includes(m)) score += 1;
if (m === '忌') score -= 1;
}
let level = '平常';
if (score >= 3) level = '大吉';
else if (score >= 1) level = '吉祥';
else if (score >= -1) level = '平稳';
else if (score >= -3) level = '小逆';
else level = '不顺';
return { score: +score.toFixed(1), level };
}
// ============================================================
// 格式输出
// ============================================================
function formatOutput(result) {
const { basic, analysis, patterns, decadal, yearly } = result;
const palaces = basic.palaces;
const mingIdx = basic.mingIdx;
const mingPalace = palaces[mingIdx];
const mingMainStars = mingPalace?.majorStars?.map(s => s.name) || [];
// 地支生肖
const branchNames = {
'子':'鼠','丑':'牛','寅':'虎','卯':'兔','辰':'龙','巳':'蛇',
'午':'马','未':'羊','申':'猴','酉':'鸡','戌':'狗','亥':'猪'
};
// 四化
const transformStr = basic.transforms.map(t => `t.star化t.hua`).join(' ');
let out = `
✨ ═══════════════════════════════════════
紫微斗数命盘 v4 · 知识库增强版
══════════════════════════════════════ ✨
📋 基本信息
出生:basic.year年basic.month月basic.day日 basic.hour:String(basic.minute).padStart(2,'0')
性别:basic.sex
生肖:basic.zodiac
八字:basic.chineseDate
五行局:basic.fiveElements
命主:basic.soul | 身主:basic.body
星座:basic.sign
🌟 命宫
位置:第mingIdx + 1宫「mingPalace?.name」
干支:mingPalace?.stemmingPalace?.branch
主星:mingMainStars.join('、') || '空宫'
长生:mingPalace?.changsheng12 || '-' | 博士:mingPalace?.boshi12 || '-'
擎羊:mingPalace?.jiangqian12 || '-' | 岁前:mingPalace?.suiqian12 || '-'
🔮 日主分析
日主:analysis.dayMaster.name
月令:analysis.monthZhi月(analysis.monthInfo.strength)
助力:analysis.helpScore分 | 压力:analysis.stressScore分
综合:analysis.strength(analysis.total分)
💊 八字用神(增强算法)
主用神:analysis.yongshen.primary
辅用神:analysis.yongshen.secondary.join('、')
说明:analysis.yongshen.summary
`;
if (analysis.yongshen.details.length > 0) {
out += ` 用神详情:\n`;
analysis.yongshen.details.forEach(d => {
out += ` · d.type:d.value — d.desc\n`;
});
}
out += `
🎯 用神喜忌
宜补:analysis.needSupport.join('、')(身analysis.strength宜)
宜避:analysis.needAvoid.join('、')
`;
if (basic.transforms.length > 0) {
out += `
🔄 四化(basic.yearStem年)
transformStr
`;
}
if (patterns.length > 0) {
out += `
🎴 命盘格局(共patterns.length个)
`;
patterns.slice(0, 15).forEach(p => {
const src = p.source === 'knowledge' ? '📚' : '📖';
out += ` src p.name(p.level)p.desc\n`;
});
if (patterns.length > 15) out += ` ...另有patterns.length - 15个格局\n`;
}
// 大运
if (decadal.length > 0) {
out += `
🔁 大运流年
当前:decadal.filter(d => d.isCurrent).map(d =>
`${d.stemd.branch(d.palaceName)d.stars.flat().filter(s=>s.name).map(s=>s.name).join('、')`
).join(' | ') || '(计算中)'}
`;
out += ` 大运一览(basic.year年起)\n`;
decadal.forEach(d => {
const cur = d.isCurrent ? '👉' : ' ';
out += ` cur d.stemd.branch · d.ageStart-d.ageEnd岁 · d.palaceName · d.luck.level(d.luck.score)\n`;
});
}
// 流年
if (yearly && yearly.current) {
out += `
📅 流年(yearly.current.year年)
干支:yearly.current.stemyearly.current.branch
流年命宫:yearly.current.palaceName
流年星:yearly.current.stars.flat().filter(s=>s.name).map(s=>'流'+s.name.replace('流','')).join('、') || '(无明显吉凶)'
流年运势:yearly.current.score.level(yearly.current.score.score)
小限:yearly.age.stemyearly.age.branch · yearly.age.palaceName(yearly.age.nominalAge岁)
`;
if (yearly.nextYears.length > 0) {
out += ` 未来五年:`;
out += yearly.nextYears.map(n => `n.year年n.stemn.branchn.palaceName`).join(' → ');
out += '\n';
}
}
out += `
📜 十二宫
`;
palaces.forEach((p, i) => {
const stars = [
...(p.majorStars?.map(s => s.name) || []),
...(p.minorStars?.map(s => s.name) || []),
...(p.adjectiveStars?.map(s => s.name) || [])
].join('、');
const isMing = i === mingIdx;
const cur = isMing ? '👉' : ' ';
const empty = stars ? '' : '(空)';
out += `cur String(i+1).padStart(2,'0').p.name p.stemp.branch starsempty\n`;
});
out += `\n═══════════════════════════════════════\n`;
return out;
}
// ============================================================
// 主入口
// ============================================================
function main() {
const args = process.argv.slice(2);
if (args.length < 2) {
console.log(`
✨ 紫微斗数命盘分析 v4(知识库增强版)
用法:
node ziwei.js <出生日期> <性别> [时间]
参数:
出生日期 YYYY-MM-DD
性别 男=1 或 女=0
时间 HH:MM(可选,默认12:00)
示例:
node ziwei.js 1995-08-15 0 12:00
node ziwei.js 1984-05-18 1
node ziwei.js 1990-05-15 0 14:30
`);
return;
}
const dateStr = args[0];
const sexArg = args[1];
const sex = sexArg === '男' ? 1 : sexArg === '女' ? 0 : parseInt(sexArg);
const timeStr = args[2] || '12:00';
const [year, month, day] = dateStr.split('-').map(Number);
const SHICHEN_MAP = {'子':0,'丑':2,'寅':4,'卯':6,'辰':8,'巳':10,'午':12,'未':14,'申':16,'酉':18,'戌':20,'亥':22};
let hour, minute;
if (SHICHEN_MAP[timeStr] !== undefined) {
hour = SHICHEN_MAP[timeStr];
minute = 0;
} else {
[hour, minute = 0] = timeStr.split(':').map(Number);
}
try {
const result = analyzePlate(year, month, day, hour, minute, sex);
console.log(formatOutput(result));
} catch (e) {
console.error('分析失败:', e.message);
console.error(e.stack);
}
}
main();
MingLi is your all-in-one Chinese astrology and fortune-telling companion. It combines six ancient divination systems — BaZi (Four Pillars of Destiny), ZiWei...
---
name: yunshi
description: |
MingLi is your all-in-one Chinese astrology and fortune-telling companion. It combines six ancient divination systems — BaZi (Four Pillars of Destiny), ZiWei DouShu (Purple Star Astrology), QiMen DunJia, I Ching (Meihua Yishu & LiuYao), marriage compatibility analysis, and feng shui — into a single skill, with no external API required.
Every morning MingLi delivers a personalized daily fortune reading covering career, wealth, relationships, and health. Every evening it previews tomorrow's energy and lucky elements. Ask for a full BaZi chart, a ZiWei life-map reading, an I Ching divination, a marriage compatibility report, or a feng shui layout recommendation — MingLi handles them all. Built-in calculation algorithms produce accurate traditional charts instantly.
Supports Chinese and English output. Trigger: fortune telling, BaZi, daily horoscope, ZiWei, QiMen, I Ching, divination, feng shui, marriage compatibility, lucky color, yearly luck, 算命, 八字, 今日运势, 紫微斗数, 占卜, 合婚, 风水.
keywords: BaZi, Chinese astrology, daily horoscope, fortune telling, ZiWei DouShu, four pillars of destiny, I Ching, divination, feng shui, marriage compatibility, QiMen DunJia, daily fortune, horoscope push, lucky color, yearly luck, lucky elements, astrology, Chinese zodiac, fate analysis, life reading, 八字, 算命, 今日运势, 每日运程, 紫微斗数, 奇门遁甲, 梅花易数, 六爻, 占卜, 合婚, 风水, 流年, 大运, 命理, 四柱
metadata:
openclaw:
runtime:
node: ">=18"
install:
- kind: node
package: iztro
env:
- name: OPENCLAW_KNOWLEDGE_DIR
required: false
description: "Optional path to ZiWei pattern knowledge base (.md files). Defaults to ~/.openclaw/workspace/knowledge. Skill degrades gracefully if absent."
---
# 运势 (YunShi)
> 私人命理顾问 — 每日运程推送 · 八字紫微 · 占卜风水
## 何时使用
- 八字/四柱排盘、流年大运分析
- 今日/近期运势(事业/财运/感情/健康)
- 紫微斗数命盘
- 合婚、双方八字相配
- 占卦(梅花易数、六爻、奇门遁甲)
- 风水布局、财位、幸运颜色
- 用户说"算命""看运势""占卜""帮我占一卦"
---
## 🌐 多语言响应规则
1. **语言跟随**:用户语言 → 全程同语言回复
2. **专有术语保留中文**:柱名/星曜/卦名保持中文原字,括号内附译文
- 英文示例:Your Day Pillar is **甲子** (Jiǎ Zǐ — Wood Rat), indicating...
3. **脚本输出翻译**:脚本返回的中文结构由 Agent 解读后以用户语言呈现
4. **注册格式**:非中文用户使用 `Name | Gender(M/F) | BirthDate | BirthTime | BirthPlace`
5. **推送语言**:跟随档案 `language` 字段(默认 `zh`)
---
## 📖 功能列表
### 排盘
| 功能 | 命令 |
|------|------|
| 八字排盘(四柱/日主/用神/神煞) | `八字 1990-05-15 14:30` |
| 紫微斗数(命宫/十二宫/四化) | `紫微 1990-05-15 男` |
| 奇门遁甲 | `奇门 2026-03-24 15:00` |
| 择吉选日 | `择吉 2026-04 开业` |
### 分析
| 功能 | 命令 |
|------|------|
| 流年/大运/事业/财运/婚姻/健康 | `2026年运势` / `未来十年运势` / `财运好不好` |
| 合婚分析 | `合婚 张三 李四` |
| 风水分析 | `风水分析` |
### 占卜
| 功能 | 命令 |
|------|------|
| 梅花易数 | `梅花易数 3 5 2`(数字起卦)或留空时间起卦 |
| 六爻预测 | `六爻占卜` |
| 奇门占卜 | `奇门选时 明天15:00` |
### 每日运程(自动推送)
早晨 07:00 推送今日运势,晚间 20:00 推送明日预告。内容:综合指数、幸运颜色/方位/数字、今日宜忌、风险预警、吉时、每日一言。
| 推送命令 | 说明 |
|---------|------|
| `每日运势开` / `开启运势推送` | 开启 |
| `每日运势关` / `关闭运势推送` | 关闭 |
| `推送状态` | 查看当前状态 |
---
## 📦 环境依赖
- **Node.js >=18**(必须)
- `npm install` 安装 `iztro`(紫微斗数)和 `lunar-typescript`(农历转换)
- `OPENCLAW_KNOWLEDGE_DIR`:可选,紫微格局知识库,不存在时自动降级
- **推送渠道**:`telegram`/`feishu` 由 openclaw 运行时投递,skill 不调用任何渠道 API
- **新闻联动**:由 Agent 的 WebSearch 工具完成,无搜索能力时跳过
- **个人数据**:存储在 `data/profiles/<userId>.json`,含敏感信息,请确认访问权限
---
## 🛠️ 工具脚本
```bash
# 注册 / 档案
node scripts/register.js <userId> <姓名> <性别> <出生日期> <出生时间> [地点]
node scripts/profile.js show <userId>
node scripts/profile.js add <userId> spouse|child <姓名> <出生日期> <性别>
# 排盘
node scripts/ziwei.js <出生日期> <性别> [时辰]
node scripts/qimen.js [日期] [时辰]
node scripts/zhuanshi.js <YYYY-MM> <活动类型> [用户八字]
node scripts/fengshui.js [八字] [年份]
# 运程 / 合婚 / 占卜
node scripts/daily-fortune.js [日期]
node scripts/marriage.js <userId1> <userId2>
node scripts/meihua.js [数字1-3]
node scripts/liuyao.js [010203] [问题]
# 推送管理
node scripts/daily-push.js --dry-run # 模拟推送
node scripts/daily-push.js --test <userId> # 测试推送
node scripts/daily-push.js --list # 查看已开启用户
node scripts/push-toggle.js on|off|status <userId>
# 偏好追踪(每次提问后调用)
node scripts/preference-tracker.js record <userId> <topic> explicit_query|topic_drill
node scripts/preference-tracker.js weights|top <userId> [N]
# topic: 财运|事业|感情|健康|婚姻|子女|官司|出行|风水
```
---
## ⏰ Cron 推送配置
```bash
openclaw cron add "0 7 * * *" "cd ~/.openclaw/workspace/skills/yunshi && node scripts/daily-push.js"
openclaw cron list
openclaw cron delete <任务ID>
```
**子时算法**:`1` = 23:00-23:59 算次日(倪海厦派);`2` = 算当日(传统派)
---
## 📊 交叉验证权重
| 问题类型 | 八字 | 紫微 | 奇门 | 梅花 | 六爻 |
|----------|------|------|------|------|------|
| 终身命格 | 40% | 30% | - | - | - |
| 年度运势 | 40% | 30% | 20% | 10% | - |
| 事业决策 | 30% | 20% | 30% | - | 20% |
| 婚姻感情 | 40% | 30% | - | 10% | 20% |
| 当下问事 | - | - | 30% | 40% | 30% |
---
## ⚠️ 风险预警等级
🔴 严重(立即处理)· 🟡 注意(谨慎处理)· 🟢 提示(一般提醒)
类型:🚨 健康 · 💰 财务 · 💕 感情 · 💼 事业 · ⚖️ 法律
---
## 📁 数据文件
```
data/profiles/{userId}.json # 用户档案(姓名/出生/家庭成员八字)
scripts/ # register, ziwei, qimen, fengshui, profile,
# daily-fortune, marriage, meihua, liuyao,
# zhuanshi, daily-push, push-toggle, preference-tracker
```
---
## ⚠️ 注意事项
1. 用户数据与AI计算冲突时,以用户提供信息为准
2. 命理是参考,不是定数
3. 用户档案仅供个人使用,注意数据隐私
---
*Version: 1.1.0 · Updated: 2026-03-30*
FILE:README.md
# 运势 (YunShi)
> 私人命理顾问 — 每日运程推送 · 八字紫微 · 占卜风水
An [OpenClaw](https://openclaw.ai) skill that brings traditional Chinese astrology into your chat. Covers BaZi (Four Pillars), ZiWei DouShu, QiMen DunJia, I Ching divination, marriage compatibility, and feng shui — with built-in algorithms and no external API dependencies.
---
## Features
### Chart Reading
| Feature | Trigger |
|---------|---------|
| BaZi (Four Pillars, Day Master, Useful God, Spirits) | `八字 1990-05-15 14:30` |
| ZiWei DouShu (Life Palace, 12 Palaces, Four Transformations) | `紫微 1990-05-15 男` |
| QiMen DunJia | `奇门 2026-03-24 15:00` |
| Auspicious Date Selection | `择吉 2026-04 开业` |
### Fortune Analysis
| Feature | Trigger |
|---------|---------|
| Annual / Decade Fortune | `2026年运势` / `未来十年运势` |
| Career / Wealth / Romance / Health | `财运好不好` |
| Marriage Compatibility | `合婚 张三 李四` |
| Feng Shui Layout | `风水分析` |
### Divination
| Feature | Trigger |
|---------|---------|
| Meihua Yi Shu (I Ching) | `梅花易数 3 5 2` |
| Liu Yao (Six Lines) | `六爻占卜` |
| QiMen Timing | `奇门选时 明天15:00` |
### Daily Fortune Push
Automatic morning push at **07:00** (today's fortune) and evening push at **20:00** (tomorrow's preview).
Each push includes: overall index, lucky color / direction / number, daily do's & don'ts, risk warnings, auspicious hours, and a daily quote.
```
每日运势开 / 开启运势推送 → Enable
每日运势关 / 关闭运势推送 → Disable
推送状态 → Check status
```
---
## Cross-Validation Weights
| Question Type | BaZi | ZiWei | QiMen | Meihua | LiuYao |
|---------------|------|-------|-------|--------|--------|
| Lifetime Chart | 40% | 30% | — | — | — |
| Annual Fortune | 40% | 30% | 20% | 10% | — |
| Career Decision | 30% | 20% | 30% | — | 20% |
| Romance / Marriage | 40% | 30% | — | 10% | 20% |
| Immediate Question | — | — | 30% | 40% | 30% |
---
## Requirements
- Node.js >= 18
- Run `npm install` to install `iztro` (ZiWei DouShu) and `lunar-typescript` (lunar calendar)
- `OPENCLAW_KNOWLEDGE_DIR` *(optional)*: path to ZiWei pattern knowledge base (`.md` files). Skill degrades gracefully if absent.
---
## Installation
```bash
# Clone into your openclaw skills directory
cd ~/.openclaw/workspace/skills
git clone https://github.com/jiajiaoy/yunshi.git
# Install dependencies
cd yunshi && npm install
```
---
## Scripts
```bash
# Register / Profile
node scripts/register.js <userId> <name> <gender> <birthDate> <birthTime> [place]
node scripts/profile.js show <userId>
node scripts/profile.js add <userId> spouse|child <name> <birthDate> <gender>
# Chart Reading
node scripts/ziwei.js <birthDate> <gender> [hour]
node scripts/qimen.js [date] [hour]
node scripts/zhuanshi.js <YYYY-MM> <activityType> [userBaZi]
node scripts/fengshui.js [bazi] [year]
# Fortune / Compatibility / Divination
node scripts/daily-fortune.js [date]
node scripts/marriage.js <userId1> <userId2>
node scripts/meihua.js [num1-3]
node scripts/liuyao.js [010203] [question]
# Push Management
node scripts/daily-push.js --dry-run # Simulate push
node scripts/daily-push.js --test <userId> # Test push
node scripts/daily-push.js --list # List active users
node scripts/push-toggle.js on|off|status <userId>
# Preference Tracking (call after each query)
node scripts/preference-tracker.js record <userId> <topic> explicit_query|topic_drill
node scripts/preference-tracker.js weights|top <userId> [N]
# topic: 财运|事业|感情|健康|婚姻|子女|官司|出行|风水
```
---
## Cron Push Setup
```bash
openclaw cron add "0 7 * * *" "cd ~/.openclaw/workspace/skills/yunshi && node scripts/daily-push.js"
openclaw cron list
openclaw cron delete <jobId>
```
> **子时算法**: Method `1` = 23:00–23:59 counts as next day (Ni Haisha school); Method `2` = counts as current day (traditional school)
---
## Supported Channels
`telegram` / `feishu` / `slack` / `discord`
---
## Risk Alert Levels
🔴 Critical (act immediately) · 🟡 Caution (handle carefully) · 🟢 Note (general reminder)
Categories: 🚨 Health · 💰 Finance · 💕 Romance · 💼 Career · ⚖️ Legal
---
## Privacy
User profiles are stored in `data/profiles/<userId>.json` and contain birth date, time, place, and family member data. Keep this directory private. Data is used only for personal astrology calculations.
---
## Notes
- When user-provided data conflicts with AI calculations, user data takes precedence
- Astrology is a reference, not a certainty
- Bilingual support: responds in the user's language; Chinese astrology terms are preserved with translations in parentheses
---
*Version 1.1.0 · Updated 2026-03-30 · MIT License*
FILE:_meta.json
{
"ownerId": "kn79bebfnwg15sb0g7cj5z5nyd83gxh0",
"slug": "yunshi",
"version": "1.1.0",
"publishedAt": 1774351732089,
"runtime": {
"node": ">=18"
},
"install": [
{ "kind": "node", "package": "iztro" }
]
}
FILE:data/profiles/template.json
{
"userId": "{{userId}}",
"name": "{{姓名}}",
"profile": {
"birthDate": "{{出生日期,如 1990-01-01}}",
"birthTime": "{{出生时间,如 14:30}}",
"birthPlace": "{{出生地,如 上海}}",
"gender": "{{男/女}}",
"lunarBirth": "",
"timezone": "Asia/Shanghai"
},
"bazi": {
"year": "",
"month": "",
"day": "",
"hour": "",
"dayStem": "",
"zodiac": "",
"dayEmpty": [],
"yearEmpty": [],
"sect": "晚子时",
"source": "pending"
},
"ziwei": {
"mingGong": "",
"mingZhu": "",
"patterns": [],
"source": "pending"
},
"language": "zh",
"preferences": {
"pushMorning": true,
"pushEvening": false,
"morningTime": "07:00",
"eveningTime": "20:00",
"channels": ["telegram"],
"focusAreas": ["事业", "财运", "健康"],
"riskTolerance": "中等"
},
"family": {
"spouse": {
"name": "",
"profile": {
"birthDate": "待录入",
"birthTime": "待录入",
"birthPlace": "",
"gender": "",
"lunarBirth": ""
},
"bazi": {
"year": "",
"month": "",
"day": "",
"hour": "",
"source": "pending"
}
},
"father": {
"name": "父亲",
"profile": {
"birthDate": "待录入",
"birthTime": "待录入",
"birthPlace": "",
"gender": "男"
},
"bazi": {
"year": "",
"month": "",
"day": "",
"hour": "",
"source": "pending"
}
},
"mother": {
"name": "母亲",
"profile": {
"birthDate": "待录入",
"birthTime": "待录入",
"birthPlace": "",
"gender": "女"
},
"bazi": {
"year": "",
"month": "",
"day": "",
"hour": "",
"source": "pending"
}
},
"children": []
},
"settings": {
"defaultSect": 1,
"lunarCalendar": true,
"notifications": {
"dailyFortune": true,
"riskAlert": true,
"weeklySummary": false
}
},
"interactionLog": [],
"lastPushDate": "",
"createdAt": "{{注册日期}}",
"updatedAt": "{{更新日期}}"
}
FILE:data/push-log.json
{
"runs": [
{
"date": "2026-03-24",
"timestamp": "2026-03-24T18:31:11.329Z",
"dryRun": true,
"results": [
{
"userId": "888888",
"name": "测试",
"status": "error",
"error": "zhiElement is not defined"
},
{
"userId": "999888",
"name": "测试",
"status": "error",
"error": "zhiElement is not defined"
},
{
"userId": "test001",
"name": "测试",
"status": "error",
"error": "zhiElement is not defined"
},
{
"userId": "test002",
"name": "测试",
"status": "error",
"error": "zhiElement is not defined"
},
{
"userId": "test003",
"name": "测试",
"status": "error",
"error": "zhiElement is not defined"
}
]
},
{
"date": "2026-03-24",
"timestamp": "2026-03-24T18:31:17.671Z",
"dryRun": true,
"results": [
{
"userId": "888888",
"name": "测试",
"status": "dry-run"
},
{
"userId": "999888",
"name": "测试",
"status": "dry-run"
},
{
"userId": "test001",
"name": "测试",
"status": "dry-run"
},
{
"userId": "test002",
"name": "测试",
"status": "dry-run"
},
{
"userId": "test003",
"name": "测试",
"status": "dry-run"
}
]
},
{
"date": "2026-03-24",
"timestamp": "2026-03-24T18:31:46.352Z",
"dryRun": true,
"results": [
{
"userId": "888888",
"name": "测试",
"status": "dry-run"
},
{
"userId": "999888",
"name": "测试",
"status": "dry-run"
},
{
"userId": "pushtest",
"name": "推送测试员",
"status": "dry-run"
},
{
"userId": "test001",
"name": "测试",
"status": "dry-run"
},
{
"userId": "test002",
"name": "测试",
"status": "dry-run"
},
{
"userId": "test003",
"name": "测试",
"status": "dry-run"
}
]
},
{
"date": "2026-03-30",
"timestamp": "2026-03-30T09:20:09.315Z",
"dryRun": true,
"results": [
{
"userId": "test001",
"name": "张三",
"status": "dry-run"
},
{
"userId": "test002",
"name": "测试",
"status": "dry-run"
},
{
"userId": "test003",
"name": "测试",
"status": "dry-run"
},
{
"userId": "user001",
"name": "测试用户",
"status": "dry-run"
}
]
}
]
}
FILE:docs/注册流程.md
# 用户注册流程
## 概述
新用户通过与机器人对话,完成注册并开始使用"命理私人导师"服务。
---
## 注册流程
```
用户 → 发起注册 → 引导录入 → 确认信息 → 完成
```
---
## 详细步骤
### 第一步:发起注册
```
用户: 你好,我想注册
助手: 欢迎使用命理私人导师!我是你的私人命理顾问。
为了给你提供准确的运势分析,请告诉我以下信息:
📝 基本信息
1. 姓名:__________
2. 性别:男/女
3. 出生日期:YYYY-MM-DD
4. 出生时间:HH:MM(精确到分钟)
5. 出生地点:__________
请依次告诉我~
```
---
### 第二步:录入信息
#### 2.1 必填信息
| 信息 | 说明 | 验证 |
|------|------|------|
| 姓名 | 用户称呼 | 非空 |
| 性别 | 男/女 | 男或女 |
| 出生日期 | 阳历生日 | 有效日期 |
| 出生时间 | 24小时制 | HH:MM格式 |
| 出生地点 | 省市即可 | 非空 |
#### 2.2 引导话术
```
📅 出生日期
请告诉我你的阳历出生日期,例如:1990-05-15
⏰ 出生时间
请告诉我精确的出生时间,例如:14:30
注意:如果是23:00之后出生,请务必如实告知(会影响八字准确性)
📍 出生地点
请告诉我出生省份/城市,例如:上海
```
---
### 第三步:确认信息
```
📋 信息确认
请确认以下信息是否正确:
姓名:XXX
性别:X
出生日期:XXXX年XX月XX日
出生时间:XX:XX
出生地点:XXXX
子时计算:晚子时(23:00后算次日)
如信息正确,请回复"确认"
如有修改,请告诉我需要修改的内容
```
---
### 第四步:八字计算
用户确认后,系统自动计算八字:
```
🧮 正在为您计算八字...
✅ 八字排盘完成!
┌─────────────────┐
│ 年柱:戊午 │
│ 月柱:乙卯 │
│ 日柱:戊子 │
│ 时柱:壬子 │
├─────────────────┤
│ 日主:戊土 │
│ 生肖:马 │
│ 日空:午未 │
│ 年空:子丑 │
└─────────────────┘
是否需要详细解读?
回复"解读"获取完整命理分析
回复"跳过"稍后再说
```
---
### 第五步:家庭成员(可选)
```
👪 家庭成员(可选)
您可以添加家庭成员的档案,用于合盘分析:
• 配偶 - 合婚分析
• 父母 - 家族运势
• 子女 - 子女缘分
示例:
- "添加配偶:1990-05-15 女"
- "添加父亲:1950-03-15"
- "添加儿子:2020-01-01 男"
回复"跳过"继续
```
---
### 第六步:推送设置
```
⏰ 推送设置
您希望收到每日运程推送吗?
📬 推送时间(可选1-2个):
• 早晨版 - 07:00 发送当日完整运势
• 傍晚版 - 20:00 发送次日预告
📱 推送渠道:
• Telegram ✅
• 飞书 ✅
• WhatsApp ✅
示例回复:
- "只要早晨版"
- "早晨+傍晚,发送到飞书"
```
---
### 第七步:完成注册
```
🎉 注册完成!
欢迎,XXX!你的私人命理顾问已就位。
📊 你的命盘
八字:XX柱 XX柱 XX柱 XX柱
日主:XX
生肖:XX
🔔 每日推送
已开启:早晨07:00
💬 你可以这样问我:
• "今日运势如何"
• "算算2026年的事业"
• "帮我占一卦"
• "我和配偶的八字合吗"
祝你好运!🍀
```
---
## 快速注册命令
用户也可以用快捷命令一次完成注册:
```
/注册 姓名|性别|出生日期|出生时间|出生地点
示例:
/注册 张三|男|1990-05-15|14:30|上海
```
---
## 注册字段映射
| 用户输入 | 保存字段 | 说明 |
|----------|----------|------|
| 姓名 | name | 称呼 |
| 性别 | profile.gender | 男/女 |
| 出生日期 | profile.birthDate | YYYY-MM-DD |
| 出生时间 | profile.birthTime | HH:MM |
| 出生地点 | profile.birthPlace | 省市 |
| 子时 | bazi.sect | 晚子时 |
---
## 错误处理
### 信息不完整
```
抱歉,信息不完整,请补充:
缺少:出生日期
请告诉我你的出生日期,例如:1990-05-15
```
### 日期格式错误
```
日期格式有误,请重新输入:
正确格式:YYYY-MM-DD
示例:1990-05-15
请重新告诉我出生日期~
```
### 时间模糊
```
您只说了"早上"出生,请尽量精确:
• 上午6点前 → 建议问家长确认
• 无法确认 → 我会按早子时(当日)计算
请告诉我更精确的时间,或者回复"不确定"
```
---
## 隐私说明
```
🔒 隐私说明
您的个人信息仅用于命理分析:
• 出生信息用于八字/紫微排盘
• 不会被分享给第三方
• 可随时要求删除档案
继续即表示同意以上条款。
```
---
*Version: 1.1.0*
*Created: 2026-03-24*
*Updated: 2026-03-24*
FILE:package-lock.json
{
"name": "yunshi",
"version": "1.0.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "yunshi",
"version": "1.0.9",
"license": "MIT",
"dependencies": {
"iztro": "^2.5.8",
"lunar-typescript": "^1.8.6"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/dayjs": {
"version": "1.11.20",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz",
"integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==",
"license": "MIT"
},
"node_modules/i18next": {
"version": "23.16.8",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.8.tgz",
"integrity": "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
{
"type": "individual",
"url": "https://locize.com/i18next.html"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/iztro": {
"version": "2.5.8",
"resolved": "https://registry.npmjs.org/iztro/-/iztro-2.5.8.tgz",
"integrity": "sha512-kgyyvxdSEvgJxi6zvHpvzGbXZLGXCdhTHYK2Pe/sRdBIQ7RfCArvupmg2ChUMQCSQGomW7XCI0gWwUuKJwPENg==",
"license": "MIT",
"dependencies": {
"dayjs": "^1.11.10",
"i18next": "^23.5.1",
"lunar-lite": "^0.2.8",
"lunar-typescript": "^1.7.8"
}
},
"node_modules/lunar-lite": {
"version": "0.2.8",
"resolved": "https://registry.npmjs.org/lunar-lite/-/lunar-lite-0.2.8.tgz",
"integrity": "sha512-Y4tba4RaIFI0ikImJhgoEsyqtDE64lJIM3yFwRX01dbmagCDq7rNmpDQFrSFFy4WXeuywdRVFpIBoT1GGCEizw==",
"license": "MIT",
"dependencies": {
"lunar-typescript": "^1.8.6"
}
},
"node_modules/lunar-typescript": {
"version": "1.8.6",
"resolved": "https://registry.npmjs.org/lunar-typescript/-/lunar-typescript-1.8.6.tgz",
"integrity": "sha512-5Eo4T/cnuXfrgO4k5LCpOGHIUOuz5hCF/IfNv0T29WY2shR36Hiz+ecN9WjnUuxUKhql9gbOkPaQoqLFKtPRNA==",
"license": "MIT"
}
}
}
FILE:package.json
{
"name": "yunshi",
"version": "1.1.0",
"description": "All-in-one Chinese astrology skill: BaZi, ZiWei DouShu, QiMen DunJia, I Ching, feng shui, marriage compatibility — with daily fortune push. No API required.",
"keywords": [
"BaZi", "Chinese astrology", "daily horoscope", "fortune telling",
"ZiWei DouShu", "four pillars", "I Ching", "divination", "feng shui",
"marriage compatibility", "QiMen DunJia", "daily fortune", "lucky color",
"yearly luck", "Chinese zodiac", "astrology", "fate reading",
"算命", "八字", "今日运势", "每日运程", "紫微斗数", "奇门遁甲",
"梅花易数", "六爻", "占卜", "合婚", "风水", "命理", "流年", "大运"
],
"author": "MingLi Mentor Team",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
},
"scripts": {
"daily": "node scripts/daily-fortune.js",
"register": "node scripts/register.js",
"ziwei": "node scripts/ziwei.js",
"marriage": "node scripts/marriage.js",
"meihua": "node scripts/meihua.js",
"liuyao": "node scripts/liuyao.js",
"qimen": "node scripts/qimen.js",
"fengshui": "node scripts/fengshui.js",
"zhuanshi": "node scripts/zhuanshi.js",
"profile": "node scripts/profile.js"
},
"directories": {
"scripts": "scripts",
"data": "data",
"docs": "docs"
},
"dependencies": {
"iztro": "^2.5.8"
}
}
FILE:scripts/bazi-analysis.js
#!/usr/bin/env node
/**
* 八字深度分析模块
* 基于《穷通宝鉴》《子平真诠》《滴天髓》三大经典
* 实现:调候用神、日主强弱、用神格局、十神、阴阳刚柔
*/
// ─────────────────────────────────────────────
// 基础数据表
// ─────────────────────────────────────────────
const STEM_ELEMENT = {
'甲': '木', '乙': '木', '丙': '火', '丁': '火', '戊': '土',
'己': '土', '庚': '金', '辛': '金', '壬': '水', '癸': '水'
};
const STEM_YINYANG = {
'甲': '阳', '乙': '阴', '丙': '阳', '丁': '阴', '戊': '阳',
'己': '阴', '庚': '阳', '辛': '阴', '壬': '阳', '癸': '阴'
};
const BRANCH_ELEMENT = {
'子': '水', '丑': '土', '寅': '木', '卯': '木',
'辰': '土', '巳': '火', '午': '火', '未': '土',
'申': '金', '酉': '金', '戌': '土', '亥': '水'
};
const ELEMENT_PRODUCES = { '木': '火', '火': '土', '土': '金', '金': '水', '水': '木' };
const ELEMENT_RESTRAIN = { '木': '土', '火': '金', '土': '水', '金': '木', '水': '火' };
const ELEMENT_GENERATED_BY = { '木': '水', '火': '木', '土': '火', '金': '土', '水': '金' };
// 地支藏干(主气/中气/余气)
const BRANCH_HIDDEN = {
'子': { 主气: '癸', 中气: '壬', 余气: null },
'丑': { 主气: '己', 中气: '辛', 余气: '癸' },
'寅': { 主气: '甲', 中气: '丙', 余气: '戊' },
'卯': { 主气: '乙', 中气: null, 余气: null },
'辰': { 主气: '戊', 中气: '乙', 余气: '癸' },
'巳': { 主气: '丙', 中气: '庚', 余气: '戊' },
'午': { 主气: '丁', 中气: '己', 余气: null },
'未': { 主气: '己', 中气: '丁', 余气: '乙' },
'申': { 主气: '庚', 中气: '壬', 余气: '戊' },
'酉': { 主气: '辛', 中气: null, 余气: null },
'戌': { 主气: '戊', 中气: '辛', 余气: '丁' },
'亥': { 主气: '壬', 中气: '甲', 余气: '戊' },
};
// ─────────────────────────────────────────────
// 十神表(以日主为基准)
// ─────────────────────────────────────────────
const TEN_GODS_TABLE = {
'甲': { '甲': '比肩', '乙': '劫财', '丙': '食神', '丁': '伤官', '戊': '偏财', '己': '正财', '庚': '七杀', '辛': '正官', '壬': '偏印', '癸': '正印' },
'乙': { '甲': '劫财', '乙': '比肩', '丙': '伤官', '丁': '食神', '戊': '正财', '己': '偏财', '庚': '正官', '辛': '七杀', '壬': '正印', '癸': '偏印' },
'丙': { '甲': '偏印', '乙': '正印', '丙': '比肩', '丁': '劫财', '戊': '食神', '己': '伤官', '庚': '偏财', '辛': '正财', '壬': '七杀', '癸': '正官' },
'丁': { '甲': '正印', '乙': '偏印', '丙': '劫财', '丁': '比肩', '戊': '伤官', '己': '食神', '庚': '正财', '辛': '偏财', '壬': '正官', '癸': '七杀' },
'戊': { '甲': '七杀', '乙': '正官', '丙': '偏印', '丁': '正印', '戊': '比肩', '己': '劫财', '庚': '食神', '辛': '伤官', '壬': '偏财', '癸': '正财' },
'己': { '甲': '正官', '乙': '七杀', '丙': '正印', '丁': '偏印', '戊': '劫财', '己': '比肩', '庚': '伤官', '辛': '食神', '壬': '正财', '癸': '偏财' },
'庚': { '甲': '偏财', '乙': '正财', '丙': '七杀', '丁': '正官', '戊': '偏印', '己': '正印', '庚': '比肩', '辛': '劫财', '壬': '食神', '癸': '伤官' },
'辛': { '甲': '正财', '乙': '偏财', '丙': '正官', '丁': '七杀', '戊': '正印', '己': '偏印', '庚': '劫财', '辛': '比肩', '壬': '伤官', '癸': '食神' },
'壬': { '甲': '食神', '乙': '伤官', '丙': '偏财', '丁': '正财', '戊': '七杀', '己': '正官', '庚': '偏印', '辛': '正印', '壬': '比肩', '癸': '劫财' },
'癸': { '甲': '伤官', '乙': '食神', '丙': '正财', '丁': '偏财', '戊': '正官', '己': '七杀', '庚': '正印', '辛': '偏印', '壬': '劫财', '癸': '比肩' },
};
// ─────────────────────────────────────────────
// 穷通宝鉴调候用神表(120条)
// ─────────────────────────────────────────────
const TIAO_HOU_TABLE = {
// 甲木
'甲寅': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '庚', 说明: '寅月木寒,丙火为君,癸水为佐' },
'甲卯': { 主用神: ['丁', '丙'], 优先级: '丁先', 忌神: '庚', 说明: '卯月木旺,丁火泄秀,忌金' },
'甲辰': { 主用神: ['庚', '丁'], 优先级: '庚先丁后', 忌神: '癸', 说明: '辰月土旺,先庚后丁' },
'甲巳': { 主用神: ['癸', '丁'], 优先级: '癸先丁后', 忌神: '庚', 说明: '巳月火旺,癸水调候' },
'甲午': { 主用神: ['癸', '壬'], 优先级: '癸先', 忌神: '丁', 说明: '午月火旺,水为调候' },
'甲未': { 主用神: ['丁', '庚'], 优先级: '丁先', 忌神: '癸', 说明: '未月土月,用丁庚' },
'甲申': { 主用神: ['庚', '丁'], 优先级: '庚先丁后', 忌神: '癸', 说明: '申月金旺,庚劈甲引丁' },
'甲酉': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '庚', 说明: '酉月金旺,丁火制金' },
'甲戌': { 主用神: ['庚', '丁'], 优先级: '庚先丁后', 忌神: '癸', 说明: '戌月金土,用庚丁' },
'甲亥': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '庚', 说明: '亥月水冷,丙火调候' },
'甲子': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '庚', 说明: '子月水寒,丙戊并用' },
'甲丑': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '辛', 说明: '丑月寒湿,丁火暖局' },
// 乙木
'乙寅': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '辛', 说明: '寅月木寒,丙癸双清' },
'乙卯': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '辛', 说明: '卯月木旺,丙癸调候' },
'乙辰': { 主用神: ['癸', '丙'], 优先级: '癸先丙后', 忌神: '乙', 说明: '辰月湿土,癸水润乙' },
'乙巳': { 主用神: ['癸', '丙'], 优先级: '癸先丙后', 忌神: '辛', 说明: '巳月火旺,癸水调候' },
'乙午': { 主用神: ['癸', '壬'], 优先级: '癸先', 忌神: '丙', 说明: '午月火旺,癸水制火' },
'乙未': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '乙', 说明: '未月土月,丙癸并用' },
'乙申': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '辛', 说明: '申月金旺,丙癸并用' },
'乙酉': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '辛', 说明: '酉月金旺,丙火制金' },
'乙戌': { 主用神: ['癸', '辛'], 优先级: '癸先辛后', 忌神: '丙', 说明: '戌月燥土,癸水润局' },
'乙亥': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '辛', 说明: '亥月水冷,丙戊暖局' },
'乙子': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '辛', 说明: '子月水寒,丙戊调候' },
'乙丑': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '辛', 说明: '丑月寒湿,丙丁暖局' },
// 丙火
'丙寅': { 主用神: ['壬', '庚'], 优先级: '壬先庚后', 忌神: '癸', 说明: '寅月木火,壬水通月令' },
'丙卯': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '甲', 说明: '卯月木旺,壬癸制火' },
'丙辰': { 主用神: ['壬', '庚'], 优先级: '壬先庚后', 忌神: '戊', 说明: '辰月湿土,壬水通根' },
'丙巳': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '戊', 说明: '巳月火旺,壬水为用' },
'丙午': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丙', 说明: '午月火旺极,壬水调候' },
'丙未': { 主用神: ['壬', '庚'], 优先级: '壬先庚后', 忌神: '己', 说明: '未月土月,壬庚并用' },
'丙申': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '庚', 说明: '申月金水,壬水通根' },
'丙酉': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '辛', 说明: '酉月金旺,壬癸制火' },
'丙戌': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '丁', 说明: '戌月土金,壬甲并用' },
'丙亥': { 主用神: ['甲', '壬'], 优先级: '甲先壬后', 忌神: '辛', 说明: '亥月水冷,甲木生火' },
'丙子': { 主用神: ['甲', '壬'], 优先级: '甲先壬后', 忌神: '癸', 说明: '子月水旺,甲木生丙' },
'丙丑': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '己', 说明: '丑月寒湿,壬甲暖局' },
// 丁火
'丁寅': { 主用神: ['甲', '丙'], 优先级: '甲先丙后', 忌神: '壬', 说明: '寅月木旺,甲木生丁' },
'丁卯': { 主用神: ['甲', '丙'], 优先级: '甲先丙后', 忌神: '癸', 说明: '卯月木旺,甲丙生丁' },
'丁辰': { 主用神: ['甲', '庚'], 优先级: '甲先庚后', 忌神: '癸', 说明: '辰月土月,甲庚并用' },
'丁巳': { 主用神: ['甲', '庚'], 优先级: '甲先庚后', 忌神: '戊', 说明: '巳月火旺,甲庚制火' },
'丁午': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丁', 说明: '午月火旺,壬癸调候' },
'丁未': { 主用神: ['甲', '庚'], 优先级: '甲先庚后', 忌神: '丁', 说明: '未月土月,甲庚并用' },
'丁申': { 主用神: ['甲', '丙'], 优先级: '甲先丙后', 忌神: '壬', 说明: '申月金旺,甲丙生丁' },
'丁酉': { 主用神: ['甲', '丙'], 优先级: '甲先丙后', 忌神: '癸', 说明: '酉月金旺,甲丙生丁' },
'丁戌': { 主用神: ['甲', '壬'], 优先级: '甲先壬后', 忌神: '丁', 说明: '戌月燥土,壬水润局' },
'丁亥': { 主用神: ['甲', '庚'], 优先级: '甲先庚后', 忌神: '壬', 说明: '亥月水冷,甲庚暖局' },
'丁子': { 主用神: ['甲', '庚'], 优先级: '甲先庚后', 忌神: '癸', 说明: '子月水寒,甲庚暖局' },
'丁丑': { 主用神: ['甲', '庚'], 优先级: '甲先庚后', 忌神: '癸', 说明: '丑月寒湿,甲庚暖局' },
// 戊土
'戊寅': { 主用神: ['丙', '甲'], 优先级: '丙先甲后', 忌神: '壬', 说明: '寅月木旺,丙甲并用' },
'戊卯': { 主用神: ['丙', '甲'], 优先级: '丙先甲后', 忌神: '壬', 说明: '卯月木旺,丙甲并用' },
'戊辰': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '甲', 说明: '辰月湿土,丙癸调候' },
'戊巳': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '甲', 说明: '巳月火旺,丙癸并用' },
'戊午': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丙', 说明: '午月火旺极,壬癸调候' },
'戊未': { 主用神: ['癸', '丙'], 优先级: '癸先丙后', 忌神: '己', 说明: '未月土月,癸水润局' },
'戊申': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '壬', 说明: '申月金旺,丙丁暖局' },
'戊酉': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '癸', 说明: '酉月金旺,丙丁暖局' },
'戊戌': { 主用神: ['甲', '丁'], 优先级: '甲先丁后', 忌神: '壬', 说明: '戌月燥土,甲丁调候' },
'戊亥': { 主用神: ['丙', '甲'], 优先级: '丙先甲后', 忌神: '壬', 说明: '亥月水冷,丙甲暖局' },
'戊子': { 主用神: ['丙', '甲'], 优先级: '丙先甲后', 忌神: '壬', 说明: '子月水寒,丙甲暖局' },
'戊丑': { 主用神: ['丙', '甲'], 优先级: '丙先甲后', 忌神: '癸', 说明: '丑月寒湿,丙甲暖局' },
// 己土
'己寅': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '甲', 说明: '寅月木旺,丙癸暖局' },
'己卯': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '甲', 说明: '卯月木旺,丙癸暖局' },
'己辰': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '乙', 说明: '辰月湿土,丙癸调候' },
'己巳': { 主用神: ['癸', '丙'], 优先级: '癸先丙后', 忌神: '甲', 说明: '巳月火旺,癸水润局' },
'己午': { 主用神: ['癸', '壬'], 优先级: '癸先壬后', 忌神: '丙', 说明: '午月火旺,癸壬调候' },
'己未': { 主用神: ['癸', '丙'], 优先级: '癸先丙后', 忌神: '己', 说明: '未月土月,癸水润局' },
'己申': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '壬', 说明: '申月金旺,丙癸暖局' },
'己酉': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '辛', 说明: '酉月金旺,丙癸暖局' },
'己戌': { 主用神: ['癸', '辛'], 优先级: '癸先辛后', 忌神: '丙', 说明: '戌月燥土,癸水润燥' },
'己亥': { 主用神: ['丙', '辛'], 优先级: '丙先辛后', 忌神: '壬', 说明: '亥月水冷,丙辛暖局' },
'己子': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '癸', 说明: '子月水寒,丙丁暖局' },
'己丑': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '癸', 说明: '丑月寒湿,丙丁暖局' },
// 庚金
'庚寅': { 主用神: ['丁', '甲'], 优先级: '丁先甲后', 忌神: '壬', 说明: '寅月木旺,丁甲并用' },
'庚卯': { 主用神: ['丁', '甲'], 优先级: '丁先甲后', 忌神: '癸', 说明: '卯月木旺,丁甲制木' },
'庚辰': { 主用神: ['丁', '甲'], 优先级: '丁先甲后', 忌神: '壬', 说明: '辰月土月,丁甲并用' },
'庚巳': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丙', 说明: '巳月火旺,壬癸制火' },
'庚午': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丁', 说明: '午月火旺,壬癸调候' },
'庚未': { 主用神: ['丁', '甲'], 优先级: '丁先甲后', 忌神: '己', 说明: '未月土月,丁甲暖局' },
'庚申': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '壬', 说明: '申月金旺,丁丙制金' },
'庚酉': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '壬', 说明: '酉月金旺,丁丙制金' },
'庚戌': { 主用神: ['丁', '甲'], 优先级: '丁先甲后', 忌神: '辛', 说明: '戌月燥土,丁甲调候' },
'庚亥': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '壬', 说明: '亥月水冷,丁丙暖局' },
'庚子': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '癸', 说明: '子月水寒,丁丙暖局' },
'庚丑': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '癸', 说明: '丑月寒湿,丙丁暖局' },
// 辛金
'辛寅': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '丙', 说明: '寅月木旺,壬水化木' },
'辛卯': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '丙', 说明: '卯月木旺,壬甲并用' },
'辛辰': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '乙', 说明: '辰月土月,壬甲暖局' },
'辛巳': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丙', 说明: '巳月火旺,壬癸制火' },
'辛午': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丁', 说明: '午月火旺,壬癸调候' },
'辛未': { 主用神: ['丁', '甲'], 优先级: '丁先甲后', 忌神: '己', 说明: '未月土月,丁甲暖局' },
'辛申': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '庚', 说明: '申月金旺,壬水洗金' },
'辛酉': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '庚', 说明: '酉月金旺,壬水洗金' },
'辛戌': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '辛', 说明: '戌月燥土,丁丙暖局' },
'辛亥': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '壬', 说明: '亥月水冷,丙戊暖局' },
'辛子': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '丙', 说明: '子月水寒,壬甲暖局' },
'辛丑': { 主用神: ['壬', '庚'], 优先级: '壬先庚后', 忌神: '己', 说明: '丑月寒湿,壬庚暖局' },
// 壬水
'壬寅': { 主用神: ['庚', '戊'], 优先级: '庚先戊后', 忌神: '丙', 说明: '寅月木旺,庚金生水' },
'壬卯': { 主用神: ['庚', '辛'], 优先级: '庚先辛后', 忌神: '丙', 说明: '卯月木旺,庚辛生水' },
'壬辰': { 主用神: ['庚', '丙'], 优先级: '庚先丙后', 忌神: '甲', 说明: '辰月土月,庚丙并用' },
'壬巳': { 主用神: ['辛', '庚'], 优先级: '辛先庚后', 忌神: '戊', 说明: '巳月火旺,辛金化火' },
'壬午': { 主用神: ['辛', '癸'], 优先级: '辛先癸后', 忌神: '丁', 说明: '午月火旺,辛癸调候' },
'壬未': { 主用神: ['庚', '辛'], 优先级: '庚先辛后', 忌神: '己', 说明: '未月土月,庚辛生水' },
'壬申': { 主用神: ['戊', '丁'], 优先级: '戊先丁后', 忌神: '丙', 说明: '申月金旺,戊丁暖局' },
'壬酉': { 主用神: ['戊', '丁'], 优先级: '戊先丁后', 忌神: '丙', 说明: '酉月金旺,戊丁暖局' },
'壬戌': { 主用神: ['辛', '丙'], 优先级: '辛先丙后', 忌神: '甲', 说明: '戌月燥土,辛丙调候' },
'壬亥': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '庚', 说明: '亥月水冷,丙戊暖局' },
'壬子': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '庚', 说明: '子月水寒,丙戊暖局' },
'壬丑': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '己', 说明: '丑月寒湿,丙丁暖局' },
// 癸水
'癸寅': { 主用神: ['辛', '丙'], 优先级: '辛先丙后', 忌神: '壬', 说明: '寅月木旺,辛丙暖局' },
'癸卯': { 主用神: ['庚', '辛'], 优先级: '庚先辛后', 忌神: '壬', 说明: '卯月木旺,庚辛生水' },
'癸辰': { 主用神: ['辛', '丙'], 优先级: '辛先丙后', 忌神: '乙', 说明: '辰月湿土,辛丙暖局' },
'癸巳': { 主用神: ['辛', '壬'], 优先级: '辛先壬后', 忌神: '戊', 说明: '巳月火旺,辛壬调候' },
'癸午': { 主用神: ['癸', '壬'], 优先级: '癸先壬后', 忌神: '丁', 说明: '午月火旺,癸壬制火' },
'癸未': { 主用神: ['庚', '辛'], 优先级: '庚先辛后', 忌神: '己', 说明: '未月土月,庚辛生水' },
'癸申': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '壬', 说明: '申月金旺,丁丙暖局' },
'癸酉': { 主用神: ['辛', '丁'], 优先级: '辛先丁后', 忌神: '壬', 说明: '酉月金旺,辛金生水' },
'癸戌': { 主用神: ['辛', '壬'], 优先级: '辛先壬后', 忌神: '丙', 说明: '戌月燥土,辛壬润局' },
'癸亥': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '庚', 说明: '亥月水冷,丙戊暖局' },
'癸子': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '庚', 说明: '子月水寒,丙丁暖局' },
'癸丑': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '己', 说明: '丑月寒湿,丙丁暖局' },
};
// ─────────────────────────────────────────────
// 月令旺衰分值表(子平真诠)
// ─────────────────────────────────────────────
const MONTH_STRENGTH = {
'寅': { '甲': 100, '乙': 80, '丙': 70, '丁': 60, '戊': 50, '己': 40, '庚': 30, '辛': 20, '壬': 10, '癸': 0 },
'卯': { '甲': 80, '乙': 100, '丙': 60, '丁': 70, '戊': 40, '己': 50, '庚': 20, '辛': 30, '壬': 10, '癸': 0 },
'辰': { '甲': 60, '乙': 70, '丙': 70, '丁': 80, '戊': 70, '己': 80, '庚': 50, '辛': 60, '壬': 40, '癸': 50 },
'巳': { '甲': 30, '乙': 40, '丙': 100,'丁': 80, '戊': 60, '己': 50, '庚': 40, '辛': 30, '壬': 10, '癸': 0 },
'午': { '甲': 20, '乙': 30, '丙': 80, '丁': 100,'戊': 50, '己': 60, '庚': 30, '辛': 40, '壬': 0, '癸': 10 },
'未': { '甲': 50, '乙': 60, '丙': 60, '丁': 70, '戊': 70, '己': 80, '庚': 50, '辛': 60, '壬': 20, '癸': 30 },
'申': { '甲': 20, '乙': 10, '丙': 30, '丁': 40, '戊': 50, '己': 60, '庚': 100,'辛': 80, '壬': 70, '癸': 50 },
'酉': { '甲': 10, '乙': 20, '丙': 20, '丁': 30, '戊': 40, '己': 50, '庚': 80, '辛': 100,'壬': 50, '癸': 70 },
'戌': { '甲': 50, '乙': 60, '丙': 70, '丁': 80, '戊': 70, '己': 80, '庚': 50, '辛': 60, '壬': 40, '癸': 50 },
'亥': { '甲': 70, '乙': 60, '丙': 20, '丁': 30, '戊': 30, '己': 40, '庚': 10, '辛': 20, '壬': 100,'癸': 80 },
'子': { '甲': 50, '乙': 40, '丙': 10, '丁': 20, '戊': 20, '己': 30, '庚': 0, '辛': 10, '壬': 80, '癸': 100},
'丑': { '甲': 40, '乙': 50, '丙': 50, '丁': 60, '戊': 60, '己': 70, '庚': 50, '辛': 60, '壬': 50, '癸': 60 },
};
// 通根加分表
const TONGGEEN_BONUS = {
'甲': { '寅': 50, '卯': 40, '亥': 20, '子': 0, '辰': 10, '未': 10, '戌': 10, '丑': 5, '巳': 0, '午': 0, '申': 0, '酉': 0 },
'乙': { '卯': 50, '寅': 30, '亥': 10, '子': 20, '辰': 10, '未': 15, '戌': 10, '丑': 10, '巳': 0, '午': 0, '申': 0, '酉': 0 },
'丙': { '巳': 50, '午': 40, '寅': 20, '卯': 10, '申': 0, '酉': 0, '辰': 5, '戌': 10, '丑': 5, '亥': 0, '子': 0, '未': 10 },
'丁': { '午': 50, '巳': 30, '未': 15, '戌': 10, '寅': 10, '酉': 0, '申': 0, '辰': 5, '丑': 5, '亥': 0, '子': 0, '卯': 5 },
'戊': { '辰': 40, '戌': 40, '丑': 30, '未': 30, '巳': 20, '午': 30, '寅': 5, '卯': 5, '申': 0, '酉': 0, '亥': 0, '子': 0 },
'己': { '丑': 40, '未': 40, '辰': 30, '戌': 30, '午': 20, '巳': 10, '寅': 5, '卯': 5, '申': 0, '酉': 0, '亥': 5, '子': 5 },
'庚': { '申': 50, '酉': 40, '辰': 15, '戌': 15, '丑': 20, '未': 15, '寅': 0, '卯': 0, '巳': 0, '午': 0, '亥': 0, '子': 0 },
'辛': { '酉': 50, '申': 30, '辰': 10, '戌': 10, '丑': 15, '未': 10, '寅': 0, '卯': 0, '巳': 0, '午': 0, '亥': 0, '子': 0 },
'壬': { '亥': 50, '子': 40, '申': 20, '酉': 10, '辰': 10, '戌': 10, '丑': 15, '寅': 0, '卯': 0, '巳': 0, '午': 0, '未': 5 },
'癸': { '子': 50, '亥': 40, '丑': 20, '辰': 10, '戌': 10, '申': 5, '酉': 5, '寅': 0, '卯': 0, '巳': 0, '午': 0, '未': 5 },
};
// ─────────────────────────────────────────────
// 辅助函数
// ─────────────────────────────────────────────
function getBranchHiddenStems(branch) {
const h = BRANCH_HIDDEN[branch] || {};
return [h.主气, h.中气, h.余气].filter(Boolean);
}
function getTenGod(dayStem, stem) {
return (TEN_GODS_TABLE[dayStem] || {})[stem] || '未知';
}
// ─────────────────────────────────────────────
// 1. 调候用神(穷通宝鉴)
// ─────────────────────────────────────────────
function analyzeTiaoHou(dayStem, monthBranch, allStems) {
const rule = TIAO_HOU_TABLE[dayStem + monthBranch];
if (!rule) return { 有调候: false, 说明: '此月令无特别调候' };
const present = rule.主用神.filter(g => allStems.includes(g));
const absent = rule.主用神.filter(g => !allStems.includes(g));
const state = present.length === rule.主用神.length ? '调候俱全'
: present.length > 0 ? '调候不全' : '调候皆缺';
return {
有调候: true,
主用神: rule.主用神,
忌神: rule.忌神,
优先级: rule.优先级,
已有用神: present,
缺用神: absent,
调候状态: state,
说明: rule.说明
};
}
// ─────────────────────────────────────────────
// 2. 日主强弱(子平真诠)
// ─────────────────────────────────────────────
function analyzeDayMasterStrength(dayStem, monthBranch, allStemsList, allBranchList) {
const myElement = STEM_ELEMENT[dayStem];
const genByElement = ELEMENT_GENERATED_BY[myElement]; // 生我者(印)
// 月令分
const monthScore = (MONTH_STRENGTH[monthBranch] || {})[dayStem] || 0;
// 比劫分(天干同五行)
let biJieScore = 0;
allStemsList.forEach(s => {
if (s !== dayStem && STEM_ELEMENT[s] === myElement) biJieScore += 20;
});
allBranchList.forEach(z => {
const hidden = BRANCH_HIDDEN[z] || {};
const weight = { 主气: 15, 中气: 8, 余气: 5 };
['主气', '中气', '余气'].forEach(k => {
if (hidden[k] && STEM_ELEMENT[hidden[k]] === myElement) biJieScore += weight[k];
});
});
// 通根分
let tonggenScore = 0;
allBranchList.forEach(z => {
tonggenScore += ((TONGGEEN_BONUS[dayStem] || {})[z] || 0);
});
// 印绶分(生我者天干/地支中)
let yinShouScore = 0;
allStemsList.forEach(s => {
if (STEM_ELEMENT[s] === genByElement) yinShouScore += 15;
});
allBranchList.forEach(z => {
const hidden = BRANCH_HIDDEN[z] || {};
const weight = { 主气: 10, 中气: 5, 余气: 3 };
['主气', '中气', '余气'].forEach(k => {
if (hidden[k] && STEM_ELEMENT[hidden[k]] === genByElement) yinShouScore += weight[k];
});
});
const total = monthScore + biJieScore + tonggenScore + yinShouScore;
let level;
if (total < 80) level = '极弱';
else if (total < 150) level = '弱';
else if (total < 220) level = '偏弱';
else if (total < 300) level = '中和';
else if (total < 380) level = '偏强';
else if (total < 450) level = '强';
else level = '极强';
const isWeak = ['极弱', '弱', '偏弱'].includes(level);
const isStrong = ['偏强', '强', '极强'].includes(level);
return {
日主: dayStem,
五行: myElement,
强弱等级: level,
总分: total,
月令得分: monthScore,
比劫得分: biJieScore,
通根得分: tonggenScore,
印绶得分: yinShouScore,
旺衰: isWeak ? `dayStem身level,宜取印比生扶` : isStrong ? `dayStem身level,宜取官杀财食伤克泄` : `dayStem身中和,综合取用`,
用神方向: isWeak ? '印比扶身' : isStrong ? '官杀财泄身' : '综合平衡'
};
}
// ─────────────────────────────────────────────
// 3. 用神格局(子平真诠)
// ─────────────────────────────────────────────
function analyzeYongShen(dayStem, monthBranch, monthStem, strengthResult) {
const hidden = BRANCH_HIDDEN[monthBranch] || {};
const tenGodsMap = TEN_GODS_TABLE[dayStem] || {};
// 月令本气透干则取透干,否则取本气
const yongshen = (monthStem === hidden.主气 || monthStem === hidden.中气) ? monthStem : hidden.主气;
const pattern = tenGodsMap[yongshen] || '比肩';
// 判断格局类型
let patternType = '正格';
const { 总分: score, 强弱等级: level } = strengthResult;
if (score < 80 && ['七杀', '正官', '偏财', '正财', '食神', '伤官'].includes(pattern)) patternType = '从格';
else if (score > 450 && ['比肩', '劫财'].includes(pattern)) patternType = '专旺格';
// 善用神判断
// 从格/专旺格:顺从格局主气,月令本气即为用神(isGood = true)
// 正格:按身强/身弱扶抑法判断
const isStrong = ['偏强', '强', '极强'].includes(level);
const isWeak = ['极弱', '弱', '偏弱'].includes(level);
let isGood = true;
if (patternType === '正格') {
if (isStrong && ['比肩', '劫财', '正印', '偏印'].includes(pattern)) isGood = false;
if (isWeak && ['七杀', '正官', '偏财', '正财', '食神', '伤官'].includes(pattern)) isGood = false;
}
return {
月令: monthBranch,
透干用神: yongshen,
格局: pattern,
格局类型: patternType,
善用神: isGood,
说明: `monthBranch月令,yongshen'本气',取pattern格`
};
}
// ─────────────────────────────────────────────
// 4. 阴阳刚柔(滴天髓)
// ─────────────────────────────────────────────
function analyzeYinYang(pillars, dayStem) {
const issues = [];
const relations = {};
pillars.forEach((p, i) => {
const label = ['年柱', '月柱', '日柱', '时柱'][i];
const se = STEM_ELEMENT[p.stem];
const be = BRANCH_ELEMENT[p.branch];
if (!se || !be) return;
if (ELEMENT_RESTRAIN[be] === se) {
issues.push(`p.stemp.branch截脚(be耗se)`);
}
if (ELEMENT_RESTRAIN[se] === be) {
issues.push(`p.stemp.branch盖头(se克be)`);
}
if (se === be) relations[label] = '比和';
else if (ELEMENT_PRODUCES[se] === be) relations[label] = '相生';
else if (ELEMENT_RESTRAIN[se] === be) relations[label] = '相克';
else relations[label] = '无直接关系';
});
const yy = STEM_YINYANG[dayStem] || '阳';
return {
日主阴阳: yy,
刚柔: yy === '阳' ? '刚' : '柔',
盖头截脚: issues,
天干地支关系: relations,
说明: `dayStem为yy干,性'柔','无截脚盖头'`
};
}
// ─────────────────────────────────────────────
// 5. 十神分析
// ─────────────────────────────────────────────
function analyzeTenGods(dayStem, stems, branches) {
const result = { 天干十神: {}, 地支十神: {} };
const labels = ['年', '月', '日', '时'];
stems.forEach((s, i) => {
if (i === 2) return; // 日干本身
result.天干十神[labels[i] + '干'] = `s(getTenGod(dayStem, s))`;
});
branches.forEach((z, i) => {
const hidden = BRANCH_HIDDEN[z] || {};
const parts = [hidden.主气, hidden.中气, hidden.余气].filter(Boolean)
.map(h => `hgetTenGod(dayStem, h)`).join('/');
result.地支十神[labels[i] + '支'] = `z(parts)`;
});
return result;
}
// ─────────────────────────────────────────────
// 综合分析入口
// ─────────────────────────────────────────────
/**
* @param {object} bazi - { year, month, day, hour, dayStem }
* e.g. { year:'丙寅', month:'己亥', day:'乙丑', hour:'乙酉', dayStem:'乙' }
*/
function runFullAnalysis(bazi) {
const yearStem = bazi.year[0], yearBranch = bazi.year[1];
const monthStem = bazi.month[0], monthBranch = bazi.month[1];
const dayStem = bazi.dayStem || bazi.day[0];
const dayBranch = bazi.day[1];
const hourStem = bazi.hour[0], hourBranch = bazi.hour[1];
const allStems = [yearStem, monthStem, dayStem, hourStem];
const allBranches = [yearBranch, monthBranch, dayBranch, hourBranch];
const allHiddenStems = allBranches.flatMap(getBranchHiddenStems);
const pillars = [
{ stem: yearStem, branch: yearBranch },
{ stem: monthStem, branch: monthBranch },
{ stem: dayStem, branch: dayBranch },
{ stem: hourStem, branch: hourBranch },
];
const tiaoHou = analyzeTiaoHou(dayStem, monthBranch, [...allStems, ...allHiddenStems]);
const strength = analyzeDayMasterStrength(dayStem, monthBranch, allStems, allBranches);
const yongShen = analyzeYongShen(dayStem, monthBranch, monthStem, strength);
const yinYang = analyzeYinYang(pillars, dayStem);
const tenGods = analyzeTenGods(dayStem, allStems, allBranches);
return { 调候用神: tiaoHou, 日主强弱: strength, 用神格局: yongShen, 阴阳刚柔: yinYang, 十神: tenGods };
}
/**
* 格式化输出分析报告
*/
function formatAnalysisReport(bazi) {
const a = runFullAnalysis(bazi);
const lines = [];
lines.push(`━━ 八字深度分析 ━━`);
lines.push(`四柱:bazi.year bazi.month bazi.day bazi.hour`);
lines.push(`日主:a.日主强弱.日主(a.日主强弱.五行)`);
lines.push('');
// 强弱
lines.push(`▶ 日主强弱(子平真诠)`);
lines.push(` 强弱等级:a.日主强弱.强弱等级(a.日主强弱.总分分)`);
lines.push(` 月令a.日主强弱.月令得分 + 比劫a.日主强弱.比劫得分 + 通根a.日主强弱.通根得分 + 印绶a.日主强弱.印绶得分`);
lines.push(` → a.日主强弱.旺衰`);
lines.push('');
// 格局
lines.push(`▶ 用神格局(子平真诠)`);
lines.push(` 格局:a.用神格局.格局(a.用神格局.格局类型)`);
lines.push(` a.用神格局.说明`);
lines.push(` 善用神:'❌ 否(用神有害)'`);
lines.push('');
// 调候
lines.push(`▶ 调候用神(穷通宝鉴)`);
if (a.调候用神.有调候) {
lines.push(` 用神:a.调候用神.主用神.join('、')(a.调候用神.优先级)`);
lines.push(` 忌神:a.调候用神.忌神 状态:a.调候用神.调候状态`);
lines.push(` a.调候用神.说明`);
if (a.调候用神.缺用神.length > 0) lines.push(` ⚠ 八字缺:a.调候用神.缺用神.join('、')`);
} else {
lines.push(` a.调候用神.说明`);
}
lines.push('');
// 阴阳
lines.push(`▶ 阴阳刚柔(滴天髓)`);
lines.push(` a.阴阳刚柔.说明`);
if (a.阴阳刚柔.盖头截脚.length > 0) {
a.阴阳刚柔.盖头截脚.forEach(s => lines.push(` ⚠ s`));
}
lines.push('');
// 十神
lines.push(`▶ 十神`);
Object.entries(a.十神.天干十神).forEach(([k, v]) => lines.push(` k: v`));
Object.entries(a.十神.地支十神).forEach(([k, v]) => lines.push(` k: v`));
return lines.join('\n');
}
module.exports = { runFullAnalysis, formatAnalysisReport, analyzeTiaoHou, analyzeDayMasterStrength, analyzeYongShen, analyzeYinYang, analyzeTenGods, TEN_GODS_TABLE, BRANCH_HIDDEN, TIAO_HOU_TABLE };
// 命令行调用
if (require.main === module) {
const args = process.argv.slice(2);
if (args.length < 4) {
console.log('用法: node bazi-analysis.js <年柱> <月柱> <日柱> <时柱>');
console.log('示例: node bazi-analysis.js 丙寅 己亥 乙丑 乙酉');
process.exit(1);
}
const [year, month, day, hour] = args;
const dayStem = day[0];
console.log(formatAnalysisReport({ year, month, day, hour, dayStem }));
}
FILE:scripts/daily-fortune.js
#!/usr/bin/env node
/**
* 每日运程生成脚本
* 生成当日运程报告:综合指数、穿衣颜色、宜忌、风险提示、吉时
*/
const dayMap = ['日', '一', '二', '三', '四', '五', '六'];
const monthMap = ['正', '二', '三', '四', '五', '六', '七', '八', '九', '十', '冬', '腊'];
// 五行颜色对应
const fiveElements = {
'木': { color: '绿色、青色', direction: '东方', lucky: '招贵人运' },
'火': { color: '红色、紫色', direction: '南方', lucky: '增事业运' },
'土': { color: '黄色、棕色', direction: '中央', lucky: '稳财运' },
'金': { color: '白色、金色', direction: '西方', lucky: '旺事业' },
'水': { color: '黑色、蓝色', direction: '北方', lucky: '防水逆' }
};
// 地支对应五行
const zhiElement = {
'子': '水', '丑': '土', '寅': '木', '卯': '木',
'辰': '土', '巳': '火', '午': '火', '未': '土',
'申': '金', '酉': '金', '戌': '土', '亥': '水'
};
//时辰信息
const hourInfo = {
'子': { range: '23-01', element: '水', tip: '整理思考' },
'丑': { range: '01-03', element: '土', tip: '睡眠休息' },
'寅': { range: '03-05', element: '木', tip: '计划准备' },
'卯': { range: '05-07', element: '木', tip: '晨间运动' },
'辰': { range: '07-09', element: '土', tip: '贵人运佳' },
'巳': { range: '09-11', element: '火', tip: '事业高峰' },
'午': { range: '11-13', element: '火', tip: '财运旺盛' },
'未': { range: '13-15', element: '土', tip: '平稳行事' },
'申': { range: '15-17', element: '金', tip: '财运佳' },
'酉': { range: '17-19', element: '金', tip: '收整理' },
'戌': { range: '19-21', element: '土', tip: '社交应酬' },
'亥': { range: '21-23', element: '水', tip: '学习思考' }
};
// 宜忌(简化版)
const yiJi = {
'木': { yi: ['出行', '学习', '交友', '谈判'], ji: ['冒险', '投资', '手术'] },
'火': { yi: ['表白', '签约', '创新', '表演'], ji: ['安葬', '搬家', '诉讼'] },
'土': { yi: ['种植', '装修', '求职', '上任'], ji: ['动土', '开业', '破土'] },
'金': { yi: ['上任', '洽谈', '收款', '装修'], ji: ['安葬', '破土', '开业'] },
'水': { yi: ['出行', '考试', '推广', '流动'], ji: ['搬家', '动土', '投资'] }
};
/**
* 计算当日天干地支
*/
function getDayGanZhi(date = new Date()) {
// 以2024年1月1日=甲子日为基准(取正午避免夏令时边界问题)
const baseDate = new Date('2024-01-01T12:00:00');
const diffDays = Math.round((date - baseDate) / (1000 * 60 * 60 * 24));
const ganIndex = ((diffDays % 10) + 10) % 10; // 甲=0
const zhiIndex = ((diffDays % 12) + 12) % 12; // 子=0
const tianGan = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
const diZhi = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
return tianGan[ganIndex] + diZhi[zhiIndex];
}
/**
* 获取五行信息
*/
function getElementInfo(ganZhi) {
const zhi = ganZhi[1];
const element = zhiElement[zhi] || '土';
return fiveElements[element] || fiveElements['土'];
}
/**
* 生成宜忌列表
*/
function getYiJiList(element) {
const info = yiJi[element] || yiJi['土'];
return {
yi: info.yi.slice(0, 4),
ji: info.ji.slice(0, 4)
};
}
/**
* 获取吉时
*/
function getLuckyHours(ganZhi) {
const zhi = ganZhi[1];
const element = zhiElement[zhi] || '土';
// 根据五行找出当日旺的时辰
const luckyZhi = Object.entries(zhiElement)
.filter(([_, el]) => el === element)
.map(([z]) => z);
return luckyZhi.slice(0, 2).map(z => ({
zhi: z,
...hourInfo[z]
}));
}
/**
* 生成风险提示
*/
function getRiskWarnings(ganZhi, dayOfWeek) {
const warnings = [];
const zhi = ganZhi[1];
// 驿马星(地支对冲)
const yimaZhi = ['申', '亥', '寅', '巳'];
if (yimaZhi.includes(zhi)) {
warnings.push({
level: '🟡',
type: '出行',
msg: '今日驿马星动,出行注意安全'
});
}
// 破日(地支相破)
const poZhi = ['子', '午', '卯', '酉', '辰', '丑', '寅', '亥', '巳', '申', '戌', '未'];
const poPairs = [['子', '丑'], ['寅', '亥'], ['卯', '戌'], ['辰', '酉'], ['巳', '申'], ['午', '未']];
// 简单的风险判断
if (dayOfWeek === 1 || dayOfWeek === 5) {
warnings.push({
level: '🟢',
type: '综合',
msg: '今日诸事顺遂,宜积极行动'
});
}
return warnings;
}
/**
* 生成运势评分
*/
function getFortuneScores(ganZhi) {
const zhi = ganZhi[1];
const element = zhiElement[zhi] || '土';
// 根据五行生克简单评分
const baseScores = {
'木': { career: 4, wealth: 3, love: 4, health: 3 },
'火': { career: 5, wealth: 4, love: 3, health: 3 },
'土': { career: 3, wealth: 4, love: 3, health: 4 },
'金': { career: 4, wealth: 5, love: 3, health: 3 },
'水': { career: 3, wealth: 3, love: 4, health: 4 }
};
const scores = baseScores[element] || baseScores['土'];
// 随机微调(±0.5)
const jitter = () => (Math.random() - 0.5);
return {
career: Math.min(5, Math.max(1, scores.career + jitter())).toFixed(1),
wealth: Math.min(5, Math.max(1, scores.wealth + jitter())).toFixed(1),
love: Math.min(5, Math.max(1, scores.love + jitter())).toFixed(1),
health: Math.min(5, Math.max(1, scores.health + jitter())).toFixed(1)
};
}
/**
* 格式化星级
*/
function formatStars(score) {
const num = parseFloat(score);
const full = Math.floor(num);
const half = num - full >= 0.5 ? 1 : 0;
const empty = 5 - full - half;
return '★'.repeat(full) + '☆'.repeat(half) + '☆'.repeat(empty);
}
/**
* 生成完整运程报告
*/
function generateDailyFortune(date = new Date()) {
const ganZhi = getDayGanZhi(date);
const elementInfo = getElementInfo(ganZhi);
const element = zhiElement[ganZhi[1]] || '土';
const yiJiInfo = getYiJiList(element);
const luckyHours = getLuckyHours(ganZhi);
const warnings = getRiskWarnings(ganZhi, date.getDay());
const scores = getFortuneScores(ganZhi);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const weekDay = dayMap[date.getDay()];
const monthName = monthMap[month - 1];
// 生成运势语
const fortuneQuotes = [
'顺势而为,伺机而动',
'稳中求进,步步为营',
'阳光总在风雨后',
'把握当下,展望未来',
'积跬步以至千里'
];
const quote = fortuneQuotes[date.getDate() % fortuneQuotes.length];
// 组装报告
const report = `
🌅 【私人命理顾问】year年month月day日(周weekDay)
📊 今日综合指数
事业:formatStars(scores.career) scores.career
财运:formatStars(scores.wealth) scores.wealth
感情:formatStars(scores.love) scores.love
健康:formatStars(scores.health) scores.health
🎨 幸运色:elementInfo.color(利elementInfo.lucky)
幸运方位:elementInfo.direction
💼 今日宜忌
✅ 宜:yiJiInfo.yi.join('、')
❌ 忌:yiJiInfo.ji.join('、')
⚠️ 风险提示
warnings.length > 0 ? warnings.map(w => ` ${w.level 【w.type】w.msg`).join('\n') : ' 🟢 今日总体平稳,无明显风险'}
⏰ 吉时
luckyHours.map(h => ` • ${h.zhi时(h.range点)- h.tip`).join('\n')}
💡 今日一句
「quote」
📅 干支:ganZhi(elementInfo.direction.charAt(0)气旺)
`;
return report;
}
// 主入口
const args = process.argv.slice(2);
let date = new Date();
if (args[0]) {
try {
date = new Date(args[0]);
if (isNaN(date.getTime())) {
console.error('日期格式无效,请使用 YYYY-MM-DD');
process.exit(1);
}
} catch (e) {
console.error('日期解析错误:', e.message);
process.exit(1);
}
}
console.log(generateDailyFortune(date));
FILE:scripts/daily-push.js
#!/usr/bin/env node
/**
* 每日运势自动推送脚本
* 读取所有已开启推送的用户,生成定制化运程并通过 OpenClaw 发送
*
* 用法:
* node daily-push.js # 推送今日运势给所有已开启的用户
* node daily-push.js --dry-run # 模拟推送(不实际发送)
* node daily-push.js --test <userId> # 测试推送指定用户
* node daily-push.js --list # 列出所有已开启推送的用户
*/
const fs = require('fs');
const path = require('path');
const PROFILES_DIR = path.join(__dirname, '../data/profiles');
const LOG_FILE = path.join(__dirname, '../data/push-log.json');
// ============================================================
// 八字/紫微 核心分析(内嵌,避免外部依赖)
// ============================================================
const GAN = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
const ZHI = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
const SHENGXIAO = ['鼠', '牛', '虎', '兔', '龙', '蛇', '马', '羊', '猴', '鸡', '狗', '猪'];
const ZHI_ELEMENT = {
'子': '水', '丑': '土', '寅': '木', '卯': '木',
'辰': '土', '巳': '火', '午': '火', '未': '土',
'申': '金', '酉': '金', '戌': '土', '亥': '水'
};
const ELEMENT_COLOR = {
'木': { color: '绿色、青色', direction: '东方', emoji: '🌿' },
'火': { color: '红色、紫色', direction: '南方', emoji: '🔥' },
'土': { color: '黄色、棕色', direction: '中央', emoji: '🌍' },
'金': { color: '白色、银色', direction: '西方', emoji: '⚪' },
'水': { color: '黑色、蓝色', direction: '北方', emoji: '🌊' }
};
const LUCKY_NUMBERS = {
'木': [3, 8], '火': [2, 7], '土': [5, 10], '金': [4, 9], '水': [1, 6]
};
const DAY_MAP = ['日', '一', '二', '三', '四', '五', '六'];
const MONTH_MAP = ['正', '二', '三', '四', '五', '六', '七', '八', '九', '十', '冬', '腊'];
const HOUR_INFO = {
'子': { range: '23-01', tip: '整理思考', stars: '☽ 阴性星' },
'丑': { range: '01-03', tip: '睡眠休息', stars: '☆ 平常' },
'寅': { range: '03-05', tip: '计划准备', stars: '🌟 小吉' },
'卯': { range: '05-07', tip: '晨间运动', stars: '🌟 小吉' },
'辰': { range: '07-09', tip: '贵人运佳', stars: '★★ 吉祥' },
'巳': { range: '09-11', tip: '事业高峰', stars: '★★ 大吉' },
'午': { range: '11-13', tip: '财运旺盛', stars: '★★ 大吉' },
'未': { range: '13-15', tip: '平稳行事', stars: '★☆ 一般' },
'申': { range: '15-17', tip: '财运佳', stars: '★★ 吉祥' },
'酉': { range: '17-19', tip: '收整理', stars: '★☆ 一般' },
'戌': { range: '19-21', tip: '社交应酬', stars: '★★ 吉祥' },
'亥': { range: '21-23', tip: '学习思考', stars: '☆ 平常' }
};
// ============================================================
// 命理核心算法
// ============================================================
function getDayGanZhi(date = new Date()) {
const baseDate = new Date('2024-01-01T12:00:00');
const diffDays = Math.round((date - baseDate) / (1000 * 60 * 60 * 24));
const ganIndex = ((diffDays % 10) + 10) % 10;
const zhiIndex = ((diffDays % 12) + 12) % 12;
return GAN[ganIndex] + ZHI[zhiIndex];
}
function getYearGanZhi(year) {
const baseYear = 1984; // 甲子年
const offset = year - baseYear;
return GAN[((offset % 10) + 10) % 10] + ZHI[((offset % 12) + 12) % 12];
}
function getLunarMonth(month) {
return MONTH_MAP[month - 1] + '月';
}
function getElementInfo(ganZhi) {
const zhi = ganZhi[1];
const element = ZHI_ELEMENT[zhi] || '土';
return { element, ...ELEMENT_COLOR[element] };
}
function getLuckyNumbers(element) {
const nums = LUCKY_NUMBERS[element] || [5, 10];
const allNums = [];
for (let i = 0; i < 5; i++) allNums.push(nums[i % nums.length]);
return allNums.slice(0, 5);
}
// ============================================================
// 八字用神计算
// ============================================================
function calculateBaziYongshen(bazi) {
if (!bazi || !bazi.dayStem) return { primary: '木', secondary: ['火', '水'], details: [] };
const dayStem = bazi.dayStem;
const monthZhi = bazi.month ? bazi.month[1] : '寅';
const dayWuxing = { '甲': '木', '乙': '木', '丙': '火', '丁': '火', '戊': '土', '己': '土', '庚': '金', '辛': '金', '壬': '水', '癸': '水' }[dayStem] || '木';
const monthWuxing = ZHI_ELEMENT[monthZhi] || '木';
const sheng = { '木': '火', '火': '土', '土': '金', '金': '水', '水': '木' };
const ke = { '木': '土', '火': '金', '土': '水', '金': '木', '水': '火' };
const results = [];
// 调候用神
const tiaohouTable = {
'甲': { '寅': '丙', '卯': '丙', '辰': '癸', '巳': '壬', '午': '壬', '未': '癸', '申': '丁', '酉': '丁', '戌': '辛', '亥': '丙', '子': '庚', '丑': '辛' },
'乙': { '寅': '丙', '卯': '丙', '辰': '癸', '巳': '壬', '午': '癸', '未': '丙', '申': '丁', '酉': '丁', '戌': '辛', '亥': '丙', '子': '庚', '丑': '辛' }
};
const t = tiaohouTable[dayStem]?.[monthZhi];
if (t) results.push({ type: '调候', value: t, desc: '寒木喜火暖局' });
// 扶抑用神
results.push({ type: '扶抑', value: sheng[dayWuxing], desc: `日主dayWuxing,喜生助` });
results.push({ type: '忌', value: ke[dayWuxing], desc: `日主dayWuxing,宜避` });
const primary = results[0]?.value || dayWuxing;
const secondary = [...new Set(results.filter(r => r.type === '扶抑').map(r => r.value))];
return { primary, secondary: secondary.slice(0, 2), details: results };
}
// ============================================================
// 运势评分(结合八字五行 + 当日干支)
// ============================================================
function generatePersonalizedScores(bazi, dayGanZhi) {
const dayElement = ZHI_ELEMENT[dayGanZhi[1]] || '土';
const dayWuxing = dayElement;
// 八字日主五行
const dayStem = bazi?.dayStem || '甲';
const dayStemWuxing = { '甲': '木', '乙': '木', '丙': '火', '丁': '火', '戊': '土', '己': '土', '庚': '金', '辛': '金', '壬': '水', '癸': '水' }[dayStem] || '木';
// 日主与当日五行的关系
const sheng = { '木': '火', '火': '土', '土': '金', '金': '水', '水': '木' };
const ke = { '木': '土', '火': '金', '土': '水', '金': '木', '水': '火' };
const bi = { '木': '金', '火': '水', '土': '木', '金': '火', '水': '土' };
// 生助日主 = 吉
const dayHelpsDay = dayWuxing === dayStemWuxing || sheng[dayStemWuxing] === dayWuxing;
// 克耗日主 = 压力
const dayStressesDay = ke[dayStemWuxing] === dayWuxing || bi[dayStemWuxing] === dayWuxing;
// 事业:与用神同气 + 当日吉
let career = 3 + Math.random() * 1.5;
let wealth = 3 + Math.random() * 1.5;
let love = 3 + Math.random() * 1.5;
let health = 3 + Math.random() * 1.5;
if (dayHelpsDay) {
career += 0.5;
wealth += 0.5;
health += 0.3;
}
if (dayStressesDay) {
career -= 0.3;
wealth -= 0.3;
}
// 基于用神微调
const yongshen = calculateBaziYongshen(bazi);
if (yongshen.primary === dayElement) { career += 0.5; wealth += 0.3; }
// 加入日期随机性(确保每天有变化)
const date = new Date();
const dayFactor = (date.getDate() % 3) * 0.3 - 0.3;
career += dayFactor;
wealth += dayFactor * 0.8;
love += dayFactor * 0.6;
health += dayFactor * 0.4;
const scores = {
career: Math.min(5, Math.max(1, career)).toFixed(1),
wealth: Math.min(5, Math.max(1, wealth)).toFixed(1),
love: Math.min(5, Math.max(1, love)).toFixed(1),
health: Math.min(5, Math.max(1, health)).toFixed(1)
};
return scores;
}
function formatStars(score) {
const num = parseFloat(score);
const full = Math.floor(num);
const half = num - full >= 0.5 ? 1 : 0;
return '★'.repeat(full) + '☆'.repeat(5 - full - half);
}
// ============================================================
// 宜忌生成(基于八字用神 + 当日干支)
// ============================================================
function generateYiJi(bazi, dayGanZhi) {
const dayElement = ZHI_ELEMENT[dayGanZhi[1]] || '土';
const yongshen = calculateBaziYongshen(bazi);
const primaryElement = yongshen.primary;
const YI_JI = {
'木': {
yi: ['出行', '学习', '交友', '谈判', '签约', '求职'],
ji: ['冒险', '投资', '手术', '安葬', '破土']
},
'火': {
yi: ['表白', '签约', '创新', '表演', '开业', '上任'],
ji: ['安葬', '搬家', '诉讼', '动土', '破土']
},
'土': {
yi: ['种植', '装修', '求职', '上任', '签约', '装修'],
ji: ['动土', '开业', '破土', '安葬', '投资']
},
'金': {
yi: ['上任', '洽谈', '收款', '装修', '签约', '投资'],
ji: ['安葬', '破土', '开业', '动土', '搬家']
},
'水': {
yi: ['出行', '考试', '推广', '流动', '求职', '开业'],
ji: ['搬家', '动土', '投资', '安葬', '破土']
}
};
// 优先用神,其次当日五行
const element = primaryElement || dayElement;
const info = YI_JI[element] || YI_JI['土'];
return {
yi: info.yi.slice(0, 4),
ji: info.ji.slice(0, 4)
};
}
// ============================================================
// 吉凶判断
// ============================================================
function getDayFortuneLevel(dayGanZhi) {
const zhi = dayGanZhi[1];
// 天恩 吉日
const tianEnZhi = ['丑', '寅', '卯', '辰', '午', '未', '亥'];
// 天贵 吉时
const tianGuiZhi = ['辰', '巳', '午', '未', '申'];
// 驿马 变动
const yimaZhi = ['申', '亥', '寅', '巳'];
let level = '平常';
let desc = '今日诸事平稳';
if (tianEnZhi.includes(zhi)) {
level = '吉祥';
desc = '天恩降临,贵人相助';
}
if (yimaZhi.includes(zhi)) {
if (level === '吉祥') {
level = '小吉';
desc = '有变动,宜把握机遇';
} else {
level = '平常';
desc = '驿马星动,出行奔波';
}
}
// 检查是否破日(相破)
const poPairs = [['子','丑'], ['寅','亥'], ['卯','戌'], ['辰','酉'], ['巳','申'], ['午','未']];
for (const [a, b] of poPairs) {
if (zhi === a || zhi === b) {
level = '平常';
desc = '今日有小损耗,宜守成';
break;
}
}
return { level, desc };
}
// ============================================================
// 风险预警
// ============================================================
function generateWarnings(bazi, dayGanZhi) {
const warnings = [];
const zhi = dayGanZhi[1];
// 驿马星
const yimaZhi = ['申', '亥', '寅', '巳'];
if (yimaZhi.includes(zhi)) {
warnings.push({ level: '🟡', type: '出行', msg: '今日驿马星动,出行注意安全,提前出门' });
}
// 五黄煞(简易判断:基于地支)
const wuhuang = ['子', '卯', '午', '酉'];
if (wuhuang.includes(zhi)) {
warnings.push({ level: '🟡', type: '健康', msg: '注意脾胃保养,饮食清淡' });
}
// 八字日主与当日关系
const dayStem = bazi?.dayStem || '甲';
const dayStemWuxing = { '甲': '木', '乙': '木', '丙': '火', '丁': '火', '戊': '土', '己': '土', '庚': '金', '辛': '金', '壬': '水', '癸': '水' }[dayStem] || '木';
const dayWuxing = ZHI_ELEMENT[zhi] || '土';
const ke = { '木': '土', '火': '金', '土': '水', '金': '木', '水': '火' };
if (ke[dayStemWuxing] === dayWuxing) {
warnings.push({ level: '🔴', type: '破财', msg: '今日财星受克,谨慎投资,避免大额花费' });
}
if (warnings.length === 0) {
warnings.push({ level: '🟢', type: '综合', msg: '今日总体顺遂,无明显风险' });
}
return warnings;
}
// ============================================================
// 吉时计算
// ============================================================
function getLuckyHours(dayGanZhi) {
const zhi = dayGanZhi[1];
const dayElement = ZHI_ELEMENT[zhi] || '土';
// 找与当日同气的时辰(旺相)
const sameElementZhi = Object.entries(ZHI_ELEMENT)
.filter(([_, el]) => el === dayElement)
.map(([z]) => z);
// 找生助当日五行的时辰
const sheng = { '木': '火', '火': '土', '土': '金', '金': '水', '水': '木' };
const helpfulZhi = Object.entries(ZHI_ELEMENT)
.filter(([_, el]) => el === sheng[dayElement])
.map(([z]) => z);
const allLucky = [...sameElementZhi, ...helpfulZhi];
const unique = [...new Set(allLucky)].slice(0, 4);
return unique.map(z => ({
zhi: z,
...(HOUR_INFO[z] || { range: '--', tip: '平常', stars: '☆' })
}));
}
// ============================================================
// 流年/流月提示(简化版,基于八字和大运)
// ============================================================
function getYearMonthTips(bazi) {
const currentYear = new Date().getFullYear();
const currentMonth = new Date().getMonth() + 1;
const yearGanZhi = getYearGanZhi(currentYear);
const yearElement = ZHI_ELEMENT[yearGanZhi[1]] || '土';
const tips = [];
// 流年提示
const yearTips = {
'木': '今年木气旺盛,利事业拓展,春季尤佳',
'火': '今年火气当令,利创新突破,夏季事业运佳',
'土': '今年土气稳重,利积累沉淀,秋季财运回升',
'金': '今年金气肃杀,利变革调整,秋季利财运',
'水': '今年水气流动,利流通传播,冬季人脉广'
};
tips.push({ period: '流年', msg: yearTips[yearElement] || '今年运势平稳' });
// 流月提示(简化)
const monthTips = [
'正月开门红,二月稳中求进,三月事业上升',
'四月注意小人,五月财运上佳,六月桃花旺盛',
'七月健康注意,八月事业转折,九月贵人相助',
'十月财运爆发,十一月感情升温,十二月总结规划'
];
const monthIdx = Math.floor((currentMonth - 1) / 2);
tips.push({ period: '本月', msg: monthTips[monthIdx] || '本月运势良好' });
return tips;
}
// ============================================================
// 每日一言
// ============================================================
function getDailyQuote(dayGanZhi) {
const quotes = [
{ element: '木', text: '木秀于林,风必摧之;堆出于岸,流必湍之。' },
{ element: '木', text: '顺势而为,不与天争;待时而动,方成大事。' },
{ element: '火', text: '火焰熊熊,照亮前路;热情如火,无坚不摧。' },
{ element: '火', text: '烈火炼真金,逆境显本色。' },
{ element: '土', text: '厚德载物,稳如泰山;静以修身,俭以养德。' },
{ element: '土', text: '土能生金,稳中求进;深根固本,方可长久。' },
{ element: '金', text: '金以刚为体,人以正为尊;锋芒内敛,大业可成。' },
{ element: '金', text: '金戈铁马,气吞万里如虎。' },
{ element: '水', text: '上善若水,水善利万物而不争。' },
{ element: '水', text: '水能载舟,亦能覆舟;顺势而行,方得始终。' },
{ element: '通用', text: '命里有时终须有,命里无时莫强求。' },
{ element: '通用', text: '三分天注定,七分靠打拼。' }
];
const dayElement = ZHI_ELEMENT[dayGanZhi[1]] || '土';
const dayQuotes = quotes.filter(q => q.element === dayElement);
const fallback = quotes.filter(q => q.element === '通用');
const pool = dayQuotes.length > 0 ? dayQuotes : fallback;
const idx = new Date().getDate() % pool.length;
return pool[idx]?.text || '积善之家,必有余庆。';
}
// ============================================================
// 生成完整个性化运程报告
// ============================================================
function generatePersonalizedFortune(profile, date = new Date()) {
const { bazi } = profile;
const dayGanZhi = getDayGanZhi(date);
const elementInfo = getElementInfo(dayGanZhi);
const luckyNumbers = getLuckyNumbers(elementInfo.element);
const scores = generatePersonalizedScores(bazi, dayGanZhi);
const fortuneLevel = getDayFortuneLevel(dayGanZhi);
const yiJi = generateYiJi(bazi, dayGanZhi);
const warnings = generateWarnings(bazi, dayGanZhi);
const luckyHours = getLuckyHours(dayGanZhi);
const yearMonthTips = getYearMonthTips(bazi);
const quote = getDailyQuote(dayGanZhi);
const yongshen = calculateBaziYongshen(bazi);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const weekDay = DAY_MAP[date.getDay()];
const lunarMonth = getLunarMonth(month);
// 用户基本信息
const userName = profile.name || '你';
const gender = profile.profile?.gender === '男' ? '先生' : '女士';
const zodiac = bazi?.zodiac || '';
// 构建报告
const fortuneEmoji = fortuneLevel.level === '大吉' ? '🌟' :
fortuneLevel.level === '吉祥' ? '✨' :
fortuneLevel.level === '小吉' ? '🌤️' : '🌙';
let report = `fortuneEmoji 【userNamegender】year年month月day日(周weekDay)
━━━━━━━━━━━━━━━━━━━━━━
📊 今日综合指数
事业 formatStars(scores.career) scores.career分
财运 formatStars(scores.wealth) scores.wealth分
感情 formatStars(scores.love) scores.love分
健康 formatStars(scores.health) scores.health分
━━━━━━━━━━━━━━━━━━━━━━
🎨 幸运属性
颜色:elementInfo.color
方位:elementInfo.direction
数字:luckyNumbers.join('、')
幸运物:elementInfo.emoji elementInfo.element元素相关
💮 今日吉凶
fortuneLevel.level — fortuneLevel.desc
💼 今日宜忌
✅ 宜:yiJi.yi.join('、')
❌ 忌:yiJi.ji.join('、')
⚠️ 风险提示
warnings.map(w => ` ${w.level【w.type】w.msg`).join('\n')}
⏰ 吉时
luckyHours.slice(0, 3).map(h => ` • ${h.zhi时(h.range点)- h.tip`).join('\n')}
luckyHours.length > 3 ? ` • ...等 ${luckyHours.length 个吉时` : ''}
📅 流年流月
yearMonthTips.map(t => ` 【${t.period】t.msg`).join('\n')}
💡 今日一言
「quote」
🧮 八字用神:yongshen.primary(主)yongshen.secondary.join('、')(辅)
今日干支:dayGanZhi(elementInfo.element气'得令')
`;
return report;
}
// ============================================================
// 用户档案管理
// ============================================================
function loadAllProfiles() {
if (!fs.existsSync(PROFILES_DIR)) return [];
const files = fs.readdirSync(PROFILES_DIR).filter(f => f.endsWith('.json'));
const profiles = [];
for (const file of files) {
try {
const userId = file.replace('.json', '');
const data = JSON.parse(fs.readFileSync(path.join(PROFILES_DIR, file), 'utf8'));
profiles.push({ userId, ...data });
} catch (e) {
console.warn(`⚠️ 加载档案失败: file`, e.message);
}
}
return profiles;
}
function getUsersWithPushEnabled(profiles) {
return profiles.filter(p => {
// 新字段:preferences.pushEnabled(优先)或 legacy 字段
const enabled = p.preferences?.pushEnabled ?? p.preferences?.pushMorning ?? false;
const hasBazi = p.bazi && p.bazi.day && p.bazi.dayStem;
return enabled && hasBazi;
});
}
function updateLastPushDate(userId) {
const filePath = path.join(PROFILES_DIR, `userId.json`);
if (!fs.existsSync(filePath)) return;
try {
const profile = JSON.parse(fs.readFileSync(filePath, 'utf8'));
profile.lastPushDate = new Date().toISOString().split('T')[0];
profile.updatedAt = new Date().toISOString().split('T')[0];
fs.writeFileSync(filePath, JSON.stringify(profile, null, 2), 'utf8');
} catch (e) {
console.warn(`⚠️ 更新推送日期失败: userId`, e.message);
}
}
// ============================================================
// OpenClaw 消息发送(通过 IPC / openclaw 工具接口)
// ============================================================
async function sendMessage(userId, message) {
// 在 openclaw cron 环境中,stdout 内容由运行时自动发送给用户
console.log(message);
return true;
}
// ============================================================
// 日志记录
// ============================================================
function loadLog() {
if (!fs.existsSync(LOG_FILE)) return { runs: [] };
try {
return JSON.parse(fs.readFileSync(LOG_FILE, 'utf8'));
} catch (e) {
return { runs: [] };
}
}
function appendLog(entry) {
const log = loadLog();
log.runs.push(entry);
// 只保留最近100条
if (log.runs.length > 100) log.runs = log.runs.slice(-100);
fs.writeFileSync(LOG_FILE, JSON.stringify(log, null, 2), 'utf8');
}
// ============================================================
// 主推送流程
// ============================================================
async function runPush({ dryRun = false, testUserId = null } = {}) {
const date = new Date();
const dateStr = date.toISOString().split('T')[0];
const logEntry = {
date: dateStr,
timestamp: new Date().toISOString(),
dryRun,
results: []
};
console.log(`\n🚀 每日运势推送开始 - dateStr\n`);
console.log(` 模式: '📨 正式推送'\n`);
const allProfiles = loadAllProfiles();
let targets = getUsersWithPushEnabled(allProfiles);
if (testUserId) {
const testProfile = allProfiles.find(p => p.userId === testUserId);
if (testProfile) {
targets = [testProfile];
console.log(` 📋 测试模式: 仅推送给 testUserId\n`);
} else {
console.log(` ❌ 测试用户不存在: testUserId`);
return;
}
}
console.log(` 📋 共 targets.length 个用户开启推送\n`);
console.log(' ' + '─'.repeat(50));
let successCount = 0;
let failCount = 0;
for (const profile of targets) {
const { userId, name } = profile;
process.stdout.write(` 🔄 name || userId (userId)... `);
try {
const fortune = generatePersonalizedFortune(profile, date);
if (dryRun) {
console.log('\n' + fortune.split('\n').map(l => ' ' + l).join('\n'));
console.log(' ' + '─'.repeat(50));
successCount++;
} else {
const sent = await sendMessage(userId, fortune);
if (sent) {
updateLastPushDate(userId);
console.log('✅');
successCount++;
} else {
console.log('⚠️ (发送失败,已记录)');
failCount++;
}
}
logEntry.results.push({
userId,
name,
status: dryRun ? 'dry-run' : (sent ? 'success' : 'failed')
});
} catch (e) {
console.log(`❌ e.message`);
failCount++;
logEntry.results.push({
userId,
name,
status: 'error',
error: e.message
});
}
}
console.log(' ' + '─'.repeat(50));
console.log(`\n ✅ 推送完成: successCount 成功failCount > 0 ? `, ${failCount 失败` : ''}\n`);
appendLog(logEntry);
return { successCount, failCount };
}
// ============================================================
// 列出开启推送的用户
// ============================================================
function listPushUsers() {
const profiles = loadAllProfiles();
const targets = getUsersWithPushEnabled(profiles);
console.log('\n📋 已开启每日运势推送的用户:\n');
if (targets.length === 0) {
console.log(' (暂无用户开启推送)\n');
return;
}
for (const p of targets) {
const lastPush = p.lastPushDate || '从未推送';
const channels = (p.preferences?.channels || ['telegram']).join(', ');
console.log(` 👤 p.name (p.userId)`);
console.log(` 八字: p.bazi?.year p.bazi?.month p.bazi?.day p.bazi?.hour`);
console.log(` 推送时间: 00' | 渠道: channels`);
console.log(` 最后推送: lastPush`);
console.log('');
}
}
// ============================================================
// 命令行入口
// ============================================================
async function main() {
const args = process.argv.slice(2);
if (args.includes('--list') || args.includes('-l')) {
listPushUsers();
return;
}
if (args.includes('--dry-run') || args.includes('-d')) {
await runPush({ dryRun: true });
return;
}
const testIdx = args.indexOf('--test');
if (testIdx !== -1 && args[testIdx + 1]) {
await runPush({ testUserId: args[testIdx + 1] });
return;
}
if (args.length === 0) {
await runPush({ dryRun: false });
return;
}
// 帮助
console.log(`
🌅 每日运势自动推送脚本
用法:
node daily-push.js 推送给所有已开启的用户
node daily-push.js --dry-run 模拟推送(显示内容,不发送)
node daily-push.js --test <userId> 测试推送指定用户
node daily-push.js --list 列出已开启推送的用户
配置:
- 用户的 preferences.pushEnabled 需为 true
- 用户的 preferences.morningTime 决定推送时间(默认07:00)
- 渠道由 preferences.channels 指定(telegram/feishu)
- 用户需有完整的八字信息(bazi.dayStem 不为空)
OpenClaw Cron 配置:
openclaw cron add "0 7 * * *" "cd <skill-dir> && node scripts/daily-push.js"
`);
}
main().catch(e => {
console.error('❌ 推送脚本出错:', e);
process.exit(1);
});
FILE:scripts/evening-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('invalid userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('illegal path');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{};}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
console.log(`晚间运势复盘🌙(date)。请生成明日运势预告:①明日五行旺衰②明日最佳行动方向(事业/感情/财务各1条)③明日需要注意的风险⑤一句引导睡前反思的命理箴言。结合今日运势变化趋势,简洁有深度,中文输出。`);
FILE:scripts/fengshui.js
#!/usr/bin/env node
/**
* 风水分析脚本
* 支持:阳宅风水、办公室风水、颜色风水
*/
const fs = require('fs');
const path = require('path');
// 八卦方位对应
const baGuaDirection = {
'乾': { direction: '西北', number: 6, element: '金', color: '白色', trait: '领导、权威' },
'坤': { direction: '西南', number: 2, element: '土', color: '黄色', trait: '柔顺、包容' },
'震': { direction: '东', number: 3, element: '木', color: '绿色', trait: '震动、创新' },
'巽': { direction: '东南', number: 4, element: '木', color: '绿色', trait: '进入、柔和' },
'坎': { direction: '北', number: 1, element: '水', color: '黑色', trait: '险陷、智慧' },
'离': { direction: '南', number: 9, element: '火', color: '红色', trait: '明亮、热情' },
'艮': { direction: '东北', number: 8, element: '土', color: '黄色', trait: '停止、稳定' },
'兑': { direction: '西', number: 7, element: '金', color: '白色', trait: '喜悦、口才' }
};
// 九宫飞星宫位序列(洛书飞布次序)
const PALACE_SEQ = ['中宫', '乾', '兑', '艮', '离', '坎', '坤', '震', '巽'];
const STAR_NAMES = ['一白', '二黑', '三碧', '四绿', '五黄', '六白', '七赤', '八白', '九紫'];
// 财位寻找
const caiWei = {
'明财位': '大门对角线位置',
'暗财位': '住宅中心点',
'流年财位': '每年变化,见流年飞星',
'固定财位': '根据主人八字喜忌定'
};
// 吉凶方位
const jiXiongDirections = {
'乾': { wealth: '吉', health: '平', career: '吉', love: '平' },
'坤': { wealth: '平', health: '吉', career: '平', love: '吉' },
'震': { wealth: '平', health: '平', career: '平', love: '吉' },
'巽': { wealth: '吉', health: '平', career: '吉', love: '平' },
'坎': { wealth: '平', health: '凶', career: '凶', love: '平' },
'离': { wealth: '吉', health: '吉', career: '平', love: '平' },
'艮': { wealth: '吉', health: '吉', career: '平', love: '平' },
'兑': { wealth: '平', health: '平', career: '凶', love: '吉' }
};
/**
* 获取流年飞星(动态计算,支持任意年份)
* 基准:2024年中宫=一白,每年中宫星顺序+3(3年一轮:一白→四绿→七赤)
*/
function getFlyingStars(year = new Date().getFullYear()) {
// yearOffset: 0→中宫一白, 1→中宫四绿, 2→中宫七赤
const yearOffset = ((year - 2024) % 3 + 3) % 3;
const result = {};
for (let i = 0; i < 9; i++) {
const starIdx = (i + yearOffset * 3) % 9;
result[STAR_NAMES[starIdx]] = PALACE_SEQ[i];
}
return result;
}
/**
* 分析大门风水
*/
function analyzeMainDoor(direction) {
const info = baGuaDirection[direction];
if (!info) {
return { error: '方向不明确' };
}
let analysis = '';
let suggestions = [];
// 大门朝向来判断
if (['乾', '兑'].includes(direction)) {
analysis = '大门朝西或西北,金气旺盛';
suggestions.push('宜摆放金属装饰增强运势');
suggestions.push('可放白色或金色地毯');
} else if (['震', '巽'].includes(direction)) {
analysis = '大门朝东或东南,木气旺盛';
suggestions.push('宜摆放绿植招贵人');
suggestions.push('保持空间明亮通风');
} else if (['坎'].includes(direction)) {
analysis = '大门朝北,水气旺盛';
suggestions.push('宜用蓝色或黑色装饰');
suggestions.push('注意防潮防湿');
} else if (['离'].includes(direction)) {
analysis = '大门朝南,火气旺盛';
suggestions.push('宜用红色或紫色装饰');
suggestions.push('注意防火安全');
} else if (['坤', '艮'].includes(direction)) {
analysis = '大门朝西南或东北,土气旺盛';
suggestions.push('宜用黄色或棕色装饰');
suggestions.push('保持空间稳重踏实');
}
return {
direction: info.direction,
element: info.element,
analysis,
suggestions
};
}
/**
* 分析财位
*/
function analyzeWealthPosition(bazi, year = new Date().getFullYear()) {
const stars = getFlyingStars(year);
const baziDayStem = bazi?.charAt(0) || '甲';
// 找出财位
const positions = [];
// 明财位
positions.push({
type: '明财位',
location: '大门对角线(进门后左右角落)',
description: '气流入处,聚气纳财',
direction: '根据实际大门位置定'
});
// 流年财位
const yiMaPosition = Object.entries(stars).find(([name]) => name === '一白贪狼')?.[1] || '坎';
positions.push({
type: '流年财位(2026)',
location: `baGuaDirection[yiMaPosition]?.direction || '北'`,
description: '一白贪狼星所在,利财运',
stars: stars
});
// 日主喜用(简化)
const dayElements = {
'甲': '木', '乙': '木', '丙': '火', '丁': '火',
'戊': '土', '己': '土', '庚': '金', '辛': '金',
'壬': '水', '癸': '水'
};
const dayElement = dayElements[baziDayStem] || '土';
// 根据日主五行找财位
const wealthDirections = {
'木': '东方、东南',
'火': '南方',
'土': '西南、东北',
'金': '西方、西北',
'水': '北方'
};
positions.push({
type: '八字喜用财位',
location: wealthDirections[dayElement] || '根据八字定',
description: `日主baziDayStem,喜dayElement,财位在wealthDirections[dayElement]`
});
return positions;
}
/**
* 分析卧室风水
*/
function analyzeBedroom(direction) {
const info = baGuaDirection[direction];
if (!info) {
return { error: '方向不明确' };
}
const bedroomAnalysis = {
'乾': {
score: 85,
pros: ['领导力增强', '事业运势提升'],
cons: ['过于刚硬', '需柔和装饰平衡'],
tips: ['放置圆润家具', '用红色点缀']
},
'坤': {
score: 80,
pros: ['睡眠安稳', '感情和睦'],
cons: ['行动力减弱', '需适当运动'],
tips: ['保持整洁', '放置陶瓷饰品']
},
'震': {
score: 75,
pros: ['事业突破', '贵人运强'],
cons: ['情绪波动大', '需静心'],
tips: ['放置绿植', '避免尖锐物品']
},
'巽': {
score: 78,
pros: ['思维活跃', '学习运佳'],
cons: ['易犹豫不决', '需果断'],
tips: ['保持空气流通', '放置文昌塔']
},
'坎': {
score: 70,
pros: ['智慧提升', '财运渐佳'],
cons: ['健康需注意', '多运动'],
tips: ['保持干燥', '放置属火物品']
},
'离': {
score: 82,
pros: ['名气提升', '桃花运佳'],
cons: ['情绪波动', '需平和'],
tips: ['避免强光直射', '放置水景装饰']
},
'艮': {
score: 88,
pros: ['健康安稳', '守财能力强'],
cons: ['过于保守', '需突破'],
tips: ['放置金属装饰', '适当变动布局']
},
'兑': {
score: 76,
pros: ['口才提升', '社交运佳'],
cons: ['易起争执', '需忍让'],
tips: ['放置鲜花', '保持明亮']
}
};
return bedroomAnalysis[direction] || bedroomAnalysis['艮'];
}
/**
* 分析办公室风水
*/
function analyzeOffice() {
return {
desk: {
ideal: '背靠实墙,面朝门口或窗户',
avoid: '背窗坐、横梁压顶',
direction: '坐北朝南或坐东朝西'
},
wealth: {
position: '大门对角线',
tips: ['放置貔貅或金蟾', '保持整洁', '不放垃圾桶']
},
career: {
position: '东或东南',
tips: ['放置绿植', '文昌位放毛笔或书籍']
},
avoid: [
'横梁压顶',
'背后有窗户',
'正对厕所门',
'灯光直射头顶'
]
};
}
/**
* 生成颜色建议
*/
function generateColorAdvice(bazi) {
const dayStem = bazi?.charAt(0) || '甲';
const dayElements = {
'甲': '木', '乙': '木', '丙': '火', '丁': '火',
'戊': '土', '己': '土', '庚': '金', '辛': '金',
'壬': '水', '癸': '水'
};
const element = dayElements[dayStem] || '土';
const colorMap = {
'木': { lucky: ['绿色', '青色', '蓝色'], avoid: ['白色', '金色'], reason: '木生火,绿色助木' },
'火': { lucky: ['红色', '紫色', '绿色'], avoid: ['黑色', '蓝色'], reason: '火生土,红紫色助火' },
'土': { lucky: ['黄色', '棕色', '红色'], avoid: ['绿色', '青色'], reason: '土生金,黄色助土' },
'金': { lucky: ['白色', '金色', '黄色'], avoid: ['红色', '紫色'], reason: '金生水,白金色助金' },
'水': { lucky: ['黑色', '蓝色', '白色'], avoid: ['黄色', '棕色'], reason: '水生木,蓝色助水' }
};
return colorMap[element] || colorMap['土'];
}
/**
* 生成完整风水报告
*/
function generateFengShuiReport(bazi, year = new Date().getFullYear()) {
const stars = getFlyingStars(year);
const dayStem = bazi?.charAt(0) || '甲';
// 流年分析
let report = `
🏠 【风水分析报告】
━━━━━━━━━━━━━━━━━━━━
📅 流年:year年
🧮 八字:bazi || '未提供'
━━━━━━━━━━━━━━━━━━━━
✨ 流年飞星(year年)
`;
for (const [star, position] of Object.entries(stars)) {
// 中宫特殊处理
if (position === '中宫' || position === '中') {
report += ` star → 中宫\n`;
report += ` 方位:中 | 五行:土\n`;
report += ` 财:平 | 健康:吉 | 事业:平\n\n`;
continue;
}
const info = baGuaDirection[position] || {};
const jiXiong = jiXiongDirections[position] || {};
report += ` star → info.direction || position\n`;
report += ` 方位:info.direction | 五行:info.element\n`;
report += ` 财:jiXiong.wealth | 健康:jiXiong.health | 事业:jiXiong.career\n\n`;
}
// 财位分析
const wealthPositions = analyzeWealthPosition(bazi, year);
report += `
💰 财位分析
`;
wealthPositions.forEach(pos => {
report += `【pos.type】\n`;
report += ` 位置:pos.location\n`;
report += ` 说明:pos.description\n\n`;
});
// 颜色建议
const colorAdvice = generateColorAdvice(dayStem);
report += `
🎨 幸运颜色
幸运色:colorAdvice.lucky.join('、')
忌用色:colorAdvice.avoid.join('、')
原因:colorAdvice.reason
`;
// 方位分析
report += `
🧭 各方位吉凶
`;
for (const [gua, info] of Object.entries(baGuaDirection)) {
const jx = jiXiongDirections[gua];
if (jx) {
report += `【info.direction】gua\n`;
report += ` 财:jx.wealth | 健康:jx.health | 事业:jx.career | 感情:jx.love\n`;
report += ` 布置建议:info.trait\n\n`;
}
}
// 办公室风水
const office = analyzeOffice();
report += `
💼 办公室风水
理想工位:office.desk.ideal
宜朝向:office.desk.direction
忌讳:office.desk.avoid
财位布置:office.wealth.tips.join('、')
事业位:office.career.tips.join('、')
`;
// 综合建议
report += `
💡 综合建议
1. 财位保持整洁,避免堆放杂物
2. 门口保持畅通,气流流通
3. 每日开窗通风,引入新鲜气场
4. 根据今日幸运色选择穿着或装饰
5. 流年不利方位可用水景或绿植化解
`;
return report;
}
// 主入口
const args = process.argv.slice(2);
if (args[0] === '--help' || args[0] === '-h') {
console.log(`
🏠 风水分析
用法:
node fengshui.js # 基础分析
node fengshui.js <八字> # 带八字分析
node fengshui.js <八字> <年份> # 指定年份
示例:
node fengshui.js
node fengshui.js 戊子
node fengshui.js 戊子 2026
`);
} else {
const bazi = args[0] || '';
const year = parseInt(args[1]) || new Date().getFullYear();
console.log(generateFengShuiReport(bazi, year));
}
module.exports = {
analyzeWealthPosition,
generateColorAdvice,
getFlyingStars,
analyzeOffice
};
FILE:scripts/jieqi.js
#!/usr/bin/env node
/**
* 节气精确计算模块
* 基于太阳黄道经度(简化 VSOP87),精度 ±15 分钟,对应到日期误差 < 1 天
*/
const DEG = Math.PI / 180;
/**
* 计算给定儒略日的太阳黄道经度(度)
* 来源:Jean Meeus《Astronomical Algorithms》第27章
*/
function sunLongitude(jd) {
const T = (jd - 2451545.0) / 36525;
const M = (357.52911 + 35999.05029 * T - 0.0001537 * T * T) * DEG;
const L0 = 280.46646 + 36000.76983 * T + 0.0003032 * T * T;
const C =
(1.914602 - 0.004817 * T - 0.000014 * T * T) * Math.sin(M) +
(0.019993 - 0.000101 * T) * Math.sin(2 * M) +
0.000289 * Math.sin(3 * M);
return ((L0 + C) % 360 + 360) % 360;
}
/**
* 牛顿迭代:求太阳黄道经度恰好为 targetLon 时的儒略日
*/
function jdeAtLongitude(year, targetLon) {
// 初始估算:以平均运动推算
let jd = 2451545.0 + (year - 2000 + ((targetLon - 280.46 + 360) % 360) / 360) * 365.2422;
for (let i = 0; i < 50; i++) {
let diff = ((targetLon - sunLongitude(jd) + 540) % 360) - 180;
if (Math.abs(diff) < 1e-6) break;
jd += (diff / 360) * 365.2422;
}
return jd;
}
/**
* 儒略日 → 北京时间(UTC+8)日期
*/
function jdToCST(jde) {
const jd = jde + 8 / 24; // 转 UTC+8
const z = Math.floor(jd + 0.5);
let a = z;
if (z >= 2299161) {
const alpha = Math.floor((z - 1867216.25) / 36524.25);
a = z + 1 + alpha - Math.floor(alpha / 4);
}
const b = a + 1524;
const c = Math.floor((b - 122.1) / 365.25);
const d = Math.floor(365.25 * c);
const e = Math.floor((b - d) / 30.6001);
const day = b - d - Math.floor(30.6001 * e);
const month = e < 14 ? e - 1 : e - 13;
const yr = month > 2 ? c - 4716 : c - 4715;
return { year: yr, month, day };
}
// ── 12 个月建节气(定义月柱边界)─────────────────────────────────────────
// 名称 黄道经度 对应日历月
const MONTH_JIEQI = [
{ name: '小寒', lon: 285, calMonth: 1 },
{ name: '立春', lon: 315, calMonth: 2 },
{ name: '惊蛰', lon: 345, calMonth: 3 },
{ name: '清明', lon: 15, calMonth: 4 },
{ name: '立夏', lon: 45, calMonth: 5 },
{ name: '芒种', lon: 75, calMonth: 6 },
{ name: '小暑', lon: 105, calMonth: 7 },
{ name: '立秋', lon: 135, calMonth: 8 },
{ name: '白露', lon: 165, calMonth: 9 },
{ name: '寒露', lon: 195, calMonth: 10 },
{ name: '立冬', lon: 225, calMonth: 11 },
{ name: '大雪', lon: 255, calMonth: 12 },
];
// 未过当月节气 → 属于上个节气月(寅月=1, 卯月=2, ..., 丑月=12)
const LUNAR_BEFORE = [11, 12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// 已过当月节气 → 属于当月节气月
const LUNAR_AFTER = [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
// 简单 LRU 缓存,避免重复计算
const _cache = new Map();
function cachedJdeAtLon(year, lon) {
const key = `year:lon`;
if (!_cache.has(key)) _cache.set(key, jdToCST(jdeAtLongitude(year, lon)));
return _cache.get(key);
}
/**
* 给定阳历日期,返回八字月柱的节气月序号
* 1 = 寅月(正月,立春后)
* 2 = 卯月(惊蛰后)
* ...
* 12 = 丑月(小寒后)
*/
function getLunarMonth(year, month, day) {
const jq = MONTH_JIEQI[month - 1];
const { day: jqDay } = cachedJdeAtLon(year, jq.lon);
return day >= jqDay ? LUNAR_AFTER[month - 1] : LUNAR_BEFORE[month - 1];
}
/**
* 判断给定日期是否已过立春(用于年柱计算)
* 立春 = 黄道 315°
*/
function isAfterLiChun(year, month, day) {
if (month > 2) return true;
if (month < 2) return false;
const { day: liChunDay } = cachedJdeAtLon(year, 315);
return day >= liChunDay;
}
/**
* 获取指定年份某节气的北京时间日期(供外部查询)
* @param {number} year
* @param {string} name 节气名称,如 '立春'
* @returns {{ year, month, day }}
*/
function getJieQiDate(year, name) {
const jq = MONTH_JIEQI.find(j => j.name === name);
if (!jq) throw new Error(`未知节气: name`);
return cachedJdeAtLon(year, jq.lon);
}
// ── 命令行调试模式 ───────────────────────────────────────────────────────
if (require.main === module) {
const year = parseInt(process.argv[2]) || new Date().getFullYear();
console.log(`\nyear年 十二月建节气(北京时间)\n'─'.repeat(30)`);
for (const jq of MONTH_JIEQI) {
const d = cachedJdeAtLon(year, jq.lon);
console.log(` jq.name d.year-String(d.month).padStart(2,'0')-String(d.day).padStart(2,'0')`);
}
}
module.exports = { getLunarMonth, isAfterLiChun, getJieQiDate };
FILE:scripts/liuyao.js
#!/usr/bin/env node
/**
* 六爻预测脚本
* 模拟三枚铜钱摇六次成卦
* 输入:6组阴阳信息(每组3个0或1,1=阳面)
*/
const yaoNames = ['初爻', '二爻', '三爻', '四爻', '五爻', '上爻'];
// 八卦对应
const baGua = {
'乾': '☰', '兑': '☱', '离': '☲', '震': '☳',
'巽': '☴', '坎': '☵', '艮': '☶', '坤': '☷'
};
// 八卦属性
const baGuaInfo = {
'乾': { element: '金', direction: '西北', trait: '刚健' },
'兑': { element: '金', direction: '西', trait: '喜悦' },
'离': { element: '火', direction: '南', trait: '明亮' },
'震': { element: '木', direction: '东', trait: '震动' },
'巽': { element: '木', direction: '东南', trait: '入' },
'坎': { element: '水', direction: '北', trait: '险陷' },
'艮': { element: '土', direction: '东北', trait: '止' },
'坤': { element: '土', direction: '西南', trait: '柔顺' }
};
// 六亲
const liuQin = ['父母', '兄弟', '子孙', '妻财', '官鬼', '子孙', '妻财', '官鬼'];
// 地支藏干(简化)
const zangGan = {
'子': ['癸'], '丑': ['己', '癸', '辛'], '寅': ['甲', '丙', '戊'],
'卯': ['乙'], '辰': ['戊', '乙', '癸'], '巳': ['丙', '庚', '戊'],
'午': ['丁', '己'], '未': ['己', '丁', '乙'], '申': ['庚', '壬', '戊'],
'酉': ['辛'], '戌': ['戊', '辛', '丁'], '亥': ['壬', '甲']
};
/**
* 抛铜钱模拟
* 3个铜钱,正面=阳,背面=阴
* 3正=老阳(变阴),2正1背=少阳(阳)
* 3背=老阴(变阳),2背1正=少阴(阴)
*/
function tossCoin() {
// 三枚铜钱各自独立:正面(1)=阳,背面(0)=阴
const heads = [0, 1, 2].reduce((sum) => sum + (Math.random() < 0.5 ? 1 : 0), 0);
if (heads === 3) return '阳动'; // 老阳(三正,变爻)
if (heads === 2) return '阴'; // 少阴(二正一背,不变)
if (heads === 1) return '阳'; // 少阳(一正二背,不变)
return '阴动'; // 老阴(三背,变爻)
}
/**
* 模拟完整六次摇卦
*/
function simulateCoins() {
const results = [];
for (let i = 0; i < 6; i++) {
results.push(tossCoin());
}
return results;
}
/**
* 从输入解析卦象
* 输入格式:六个0/1/2/3的数字
* 0=少阳(阳不动),1=少阴(阴不动),2=老阳(动),3=老阴(动)
*/
function parseCoins(input) {
const results = [];
for (const char of input) {
const num = parseInt(char);
if (num === 0 || num === 1) {
// 不动爻
results.push(num === 0 ? '阳' : '阴');
} else if (num === 2) {
// 老阳变阴
results.push('阳动');
} else if (num === 3) {
// 老阴变阳
results.push('阴动');
}
}
return results;
}
/**
* 根据地支找六亲
*/
function findLiuQin(zhi, riGan) {
const riEl = getElement(riGan);
const zhiEl = getElement(zhi);
// 五行生克
if (riEl === '木') {
if (zhiEl === '木') return '比肩';
if (zhiEl === '火') return '食神';
if (zhiEl === '土') return '偏财';
if (zhiEl === '金') return '官鬼';
if (zhiEl === '水') return '印绶';
} else if (riEl === '火') {
if (zhiEl === '火') return '比肩';
if (zhiEl === '土') return '食神';
if (zhiEl === '金') return '偏财';
if (zhiEl === '水') return '官鬼';
if (zhiEl === '木') return '印绶';
} else if (riEl === '土') {
if (zhiEl === '土') return '比肩';
if (zhiEl === '金') return '食神';
if (zhiEl === '水') return '偏财';
if (zhiEl === '木') return '官鬼';
if (zhiEl === '火') return '印绶';
} else if (riEl === '金') {
if (zhiEl === '金') return '比肩';
if (zhiEl === '水') return '食神';
if (zhiEl === '木') return '偏财';
if (zhiEl === '火') return '官鬼';
if (zhiEl === '土') return '印绶';
} else if (riEl === '水') {
if (zhiEl === '水') return '比肩';
if (zhiEl === '木') return '食神';
if (zhiEl === '火') return '偏财';
if (zhiEl === '土') return '官鬼';
if (zhiEl === '金') return '印绶';
}
return '无';
}
/**
* 获取五行
*/
function getElement(zhi) {
const elements = {
'子': '水', '丑': '土', '寅': '木', '卯': '木',
'辰': '土', '巳': '火', '午': '火', '未': '土',
'申': '金', '酉': '金', '戌': '土', '亥': '水'
};
return elements[zhi] || '土';
}
/**
* 生成六爻卦象
*/
function generateLiuYao(coins, riGan, riZhi) {
// 用时间或给定信息确定卦
const date = new Date();
const hour = date.getHours();
// 简化:以下卦+上卦
const gua64 = ['乾', '坤', '屯', '蒙', '需', '讼', '师', '比',
'小畜', '履', '泰', '否', '同人', '大有', '谦', '豫',
'随', '蛊', '临', '观', '噬嗑', '贲', '剥', '复',
'无妄', '大畜', '颐', '大过', '坎', '离', '咸', '恒',
'遁', '大壮', '晋', '明夷', '家人', '睽', '蹇', '解',
'损', '益', '夬', '姤', '萃', '升', '困', '井',
'革', '鼎', '震', '艮', '渐', '归妹', '丰', '旅',
'巽', '兑', '涣', '节', '中孚', '小过', '既济', '未济'];
// 动爻组合确定卦
const dongCount = coins.filter(c => c.includes('动')).length;
const guaIndex = (dongCount * 6 + coins.filter(c => c === '阳' || c === '阳动').length) % 64;
const guaName = gua64[guaIndex];
// 世应关系(简化)
const shiYing = {
'乾': { shi: '五爻', ying: '二爻' },
'坤': { shi: '二爻', ying: '五爻' },
'屯': { shi: '初爻', ying: '四爻' },
'蒙': { shi: '三爻', ying: '上爻' },
'default': { shi: '三爻', ying: '上爻' }
};
const sy = shiYing[guaName] || shiYing['default'];
// 构建爻列表
const yaoList = coins.map((c, i) => {
const isYang = c === '阳' || c === '阳动';
const isDong = c.includes('动');
const zhi = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'][i];
const gan = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'][i];
const liuqin = findLiuQin(zhi, riGan);
return {
name: yaoNames[i],
yinYang: isYang ? '阳' : '阴',
dong: isDong ? '动' : '',
zhi,
gan,
liuqin
};
});
return {
guaName,
yaoList,
shiYing: sy,
dongCount
};
}
/**
* 判断吉凶
*/
function judgeFortune(hexagram) {
const { dongCount, yaoList } = hexagram;
let result;
if (dongCount === 0) {
result = '静卦 - 事情稳定,需等待时机';
} else if (dongCount === 1) {
result = '独发 - 一件事主导,专注可成';
} else if (dongCount === 2) {
result = '双动 - 两件事关联,需协调';
} else if (dongCount >= 3) {
result = '多动 - 变数较多,谨慎行事';
}
// 检查动爻的六亲
const dongLiuQin = yaoList.filter(y => y.dong === '动').map(y => y.liuqin);
if (dongLiuQin.includes('官鬼')) {
result += '\n⚠️ 动爻带官鬼 - 谨防小人、压力';
}
if (dongLiuQin.includes('妻财')) {
result += '\n💰 动爻带妻财 - 财运显现或破耗';
}
if (dongLiuQin.includes('子孙')) {
result += '\n🌟 动爻带子孙 - 好事发生,贵人运';
}
if (dongLiuQin.includes('父母')) {
result += '\n📚 动爻带父母 - 文书、合同事宜';
}
return result;
}
/**
* 生成报告
*/
function generateReport(hexagram, question = '占卜事宜') {
const { guaName, yaoList, shiYing, dongCount } = hexagram;
const guaInfo = baGuaInfo[guaName[0]] || baGuaInfo['乾'];
const fortune = judgeFortune(hexagram);
let report = `
🔮 【六爻预测】
📋 占卜信息
事项:question
动爻数:dongCount个
🎴 卦象
卦名:guaName
卦性:guaInfo.trait
📊 世应关系
世爻:shiYing.shi
应爻:shiYing.ying
📜 六爻排列(从下至上)
`;
yaoList.reverse().forEach((y, i) => {
const symbol = y.yinYang === '阳' ? '━━' : ' ━ ';
const dongSymbol = y.dong === '动' ? ' ◯' : ' ';
report += ` y.name symboldongSymbol y.zhiy.gan y.liuqin\n`;
});
report += `
⚖️ 吉凶判断
fortune
💡 建议
dongCount === 1 ? '专注一事,把握独发之机' :
'多事并行,量力而行'
`;
return report;
}
// 主入口
const args = process.argv.slice(2);
if (args[0] === '--help' || args[0] === '-h') {
console.log(`
六爻预测
用法:
node liuyao.js # 模拟摇卦
node liuyao.js 010203 # 指定6个爻(0=阳不动,1=阴不动,2=阳动,3=阴动)
node liuyao.js 010203 婚姻 # 指定爻+问题
示例:
node liuyao.js
node liuyao.js 012013 事业
`);
} else if (args.length >= 1 && /^\d{6}$/.test(args[0])) {
// 指定爻象
const coins = parseCoins(args[0]);
const question = args[1] || '占卜事宜';
const riGan = '戊'; // 简化处理
const riZhi = '子';
const hexagram = generateLiuYao(coins, riGan, riZhi);
console.log(generateReport(hexagram, question));
} else if (args.length >= 2 && /^\d{6}$/.test(args[0])) {
// 爻象 + 问题
const coins = parseCoins(args[0]);
const question = args.slice(1).join(' ');
const riGan = '戊';
const riZhi = '子';
const hexagram = generateLiuYao(coins, riGan, riZhi);
console.log(generateReport(hexagram, question));
} else {
// 模拟摇卦
console.log('🎲 摇卦中...\n');
const coins = simulateCoins();
const question = args.join(' ') || '占卜事宜';
const riGan = '戊';
const riZhi = '子';
const hexagram = generateLiuYao(coins, riGan, riZhi);
console.log(`📍 摇得:[coins.join(' ')]\n`);
console.log(generateReport(hexagram, question));
}
FILE:scripts/marriage.js
#!/usr/bin/env node
/**
* 合婚分析脚本
* 根据八字分析两个人的婚姻适配度
*/
const fs = require('fs');
const path = require('path');
const PROFILES_DIR = path.join(__dirname, '../data/profiles');
/**
* 加载用户档案
*/
function loadProfile(userId) {
const filePath = path.join(PROFILES_DIR, `userId.json`);
if (!fs.existsSync(filePath)) {
return null;
}
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
/**
* 天干信息
*/
const tianGan = {
'甲': { element: '木', yin: '阳' },
'乙': { element: '木', yin: '阴' },
'丙': { element: '火', yin: '阳' },
'丁': { element: '火', yin: '阴' },
'戊': { element: '土', yin: '阳' },
'己': { element: '土', yin: '阴' },
'庚': { element: '金', yin: '阳' },
'辛': { element: '金', yin: '阴' },
'壬': { element: '水', yin: '阳' },
'癸': { element: '水', yin: '阴' }
};
/**
* 地支信息
*/
const diZhi = {
'子': { element: '水', animal: '鼠' },
'丑': { element: '土', animal: '牛' },
'寅': { element: '木', animal: '虎' },
'卯': { element: '木', animal: '兔' },
'辰': { element: '土', animal: '龙' },
'巳': { element: '火', animal: '蛇' },
'午': { element: '火', animal: '马' },
'未': { element: '土', animal: '羊' },
'申': { element: '金', animal: '猴' },
'酉': { element: '金', animal: '鸡' },
'戌': { element: '土', animal: '狗' },
'亥': { element: '水', animal: '猪' }
};
/**
* 五行生克关系
*/
const wuXingRelations = {
'木': { sheng: '火', ke: '土' },
'火': { sheng: '土', ke: '金' },
'土': { sheng: '金', ke: '水' },
'金': { sheng: '水', ke: '木' },
'水': { sheng: '木', ke: '火' }
};
/**
* 日主五行关系分析
*/
function analyzeDayMasterCompatibility(bazi1, bazi2) {
const day1 = bazi1.charAt(0);
const day2 = bazi2.charAt(0);
const el1 = tianGan[day1]?.element;
const el2 = tianGan[day2]?.element;
if (!el1 || !el2) return { score: 0, reason: '日主五行无法确定' };
let relation = '';
let score = 50;
if (wuXingRelations[el1]?.sheng === el2) {
relation = `day1(el1)生 day2(el2)`;
score = 75;
} else if (wuXingRelations[el1]?.ke === el2) {
relation = `day1(el1)克 day2(el2)`;
score = 45;
} else if (wuXingRelations[el2]?.sheng === el1) {
relation = `day2(el2)生 day1(el1)`;
score = 70;
} else if (wuXingRelations[el2]?.ke === el1) {
relation = `day2(el2)克 day1(el1)`;
score = 40;
} else if (el1 === el2) {
relation = `日主同属el1,比和`;
score = 60;
}
return { score, relation, el1, el2, day1, day2 };
}
/**
* 纳音五行分析
*/
function analyzeNaYinCompatibility(bazi1, bazi2) {
// 从八字中提取年柱
const year1 = bazi1.split(' ')[0] || '';
const year2 = bazi2.split(' ')[0] || '';
const naYinMap = {
'甲子': '海中金', '乙丑': '海中金',
'丙寅': '炉中火', '丁卯': '炉中火',
'戊辰': '大林木', '己巳': '大林木',
'庚午': '路旁土', '辛未': '路旁土',
'壬申': '剑锋金', '癸酉': '剑锋金',
'甲戌': '山头火', '乙亥': '山头火',
'丙子': '漳下水', '丁丑': '漳下水',
'戊寅': '城头土', '己卯': '城头土',
'庚辰': '白蜡金', '辛巳': '白蜡金',
'壬午': '杨柳木', '癸未': '杨柳木',
'甲申': '井泉水', '乙酉': '井泉水',
'丙戌': '屋上土', '丁亥': '屋上土',
'戊子': '霹雳火', '己丑': '霹雳火',
'庚寅': '松柏木', '辛卯': '松柏木',
'壬辰': '长流水', '癸巳': '长流水',
'甲午': '沙中金', '乙未': '沙中金',
'丙申': '山下火', '丁酉': '山下火',
'戊戌': '平地木', '己亥': '平地木',
'庚子': '壁上土', '辛丑': '壁上土',
'壬寅': '金箔金', '癸卯': '金箔金',
'甲辰': '覆灯火', '乙巳': '覆灯火',
'丙午': '天河水', '丁未': '天河水',
'戊申': '大驿土', '己酉': '大驿土',
'庚戌': '钗钏金', '辛亥': '钗钏金',
'壬子': '桑柘木', '癸丑': '桑柘木',
'甲寅': '大溪水', '乙卯': '大溪水',
'丙辰': '沙中土', '丁巳': '沙中土',
'戊午': '天上火', '己未': '天上火',
'庚申': '石榴木', '辛酉': '石榴木',
'壬戌': '大海水', '癸亥': '大海水'
};
const ny1 = naYinMap[year1] || '未知';
const ny2 = naYinMap[year2] || '未知';
return { ny1, ny2 };
}
/**
* 地支合冲分析
*/
function analyzeDiZhiRelation(zhi1, zhi2) {
const heMap = {
'子丑': '六合', '寅亥': '六合', '卯戌': '六合',
'辰酉': '六合', '巳申': '六合', '午未': '六合',
'寅午': '三合', '午戌': '三合', '子辰': '三合',
'申子': '三合', '巳酉': '三合', '丑亥': '三合',
'卯卯': '比和', '午午': '比和', '酉酉': '比和'
};
const chongMap = {
'子午': '子午相冲', '丑未': '丑未相冲',
'寅申': '寅申相冲', '卯酉': '卯酉相冲',
'辰戌': '辰戌相冲', '巳亥': '巳亥相冲'
};
const key1 = zhi1 + zhi2;
const key2 = zhi2 + zhi1;
let result = '';
if (heMap[key1]) result = heMap[key1];
else if (heMap[key2]) result = heMap[key2];
else if (chongMap[key1]) result = chongMap[key1];
else if (chongMap[key2]) result = chongMap[key2];
else result = '无特殊合冲';
return result;
}
/**
* 天干合分析
*/
function analyzeTianGanHe(bazi1, bazi2) {
const day1 = bazi1.charAt(0);
const day2 = bazi2.charAt(0);
const heTian = {
'甲己': '甲己合(中正之合)', '乙庚': '乙庚合(仁义之合)',
'丙辛': '丙辛合(威制之合)', '丁壬': '丁壬合(淫昵之合)',
'戊癸': '戊癸合(无情之合)'
};
const key = day1 + day2;
const key2 = day2 + day1;
return heTian[key] || heTian[key2] || '日主无天干相合';
}
/**
* 计算综合评分
*/
function calculateOverallScore(dayScore, heResult, dzResult) {
let score = dayScore;
if (dzResult.includes('六合')) score += 15;
else if (dzResult.includes('三合')) score += 10;
else if (dzResult.includes('比和')) score += 5;
else if (dzResult.includes('相冲')) score -= 15;
if (heResult.includes('中正') || heResult.includes('仁义')) score += 10;
else if (heResult.includes('淫昵')) score -= 5;
else if (heResult.includes('无情')) score -= 10;
return Math.max(0, Math.min(100, score));
}
/**
* 生成评价
*/
function getEvaluation(score) {
if (score >= 85) return { grade: '★★★★★', level: '极佳', desc: '天作之合,百年好合' };
if (score >= 70) return { grade: '★★★★☆', level: '优秀', desc: '缘分深厚,婚配吉祥' };
if (score >= 55) return { grade: '★★★☆☆', level: '中等', desc: '缘分平平,需要磨合' };
if (score >= 40) return { grade: '★★☆☆☆', level: '偏低', desc: '需要多沟通包容' };
return { grade: '★☆☆☆☆', level: '较差', desc: '婚配欠佳,需谨慎' };
}
/**
* 生成分析报告
*/
function generateReport(name1, bazi1, name2, bazi2) {
const dayAnalysis = analyzeDayMasterCompatibility(bazi1, bazi2);
const naYin = analyzeNaYinCompatibility(bazi1, bazi2);
// 解析八字(格式:"庚午 辛巳 庚辰 癸未")
const parseBazi = (bazi) => {
const parts = bazi.split(' ');
return {
year: parts[0] || '',
month: parts[1] || '',
day: parts[2] || '',
hour: parts[3] || '',
yearZhi: (parts[0] || '').charAt(1),
monthZhi: (parts[1] || '').charAt(1),
dayZhi: (parts[2] || '').charAt(1),
hourZhi: (parts[3] || '').charAt(1)
};
};
const p1 = parseBazi(bazi1);
const p2 = parseBazi(bazi2);
const zhiPairs = [
{ name: '年柱', z1: p1.yearZhi, z2: p2.yearZhi },
{ name: '月柱', z1: p1.monthZhi, z2: p2.monthZhi },
{ name: '日柱', z1: p1.dayZhi, z2: p2.dayZhi },
{ name: '时柱', z1: p1.hourZhi, z2: p2.hourZhi }
];
const heResult = analyzeTianGanHe(bazi1, bazi2);
const dzResults = zhiPairs.map(p => ({
name: p.name,
z1: p.z1,
z2: p.z2,
relation: analyzeDiZhiRelation(p.z1, p.z2)
}));
const overallScore = calculateOverallScore(dayAnalysis.score, heResult, dzResults[0].relation);
const evaluation = getEvaluation(overallScore);
let report = `
💕 【合婚分析报告】
━━━━━━━━━━━━━━━━━━━━
👤 男方:name1
八字:bazi1
日主:dayAnalysis.day1(dayAnalysis.el1)
👤 女方:name2
八字:bazi2
日主:dayAnalysis.day2(dayAnalysis.el2)
━━━━━━━━━━━━━━━━━━━━
📊 合婚评分
evaluation.grade evaluation.level
综合得分:overallScore分(满分100)
━━━━━━━━━━━━━━━━━━━━
🔮 详细分析
【日主关系】
dayAnalysis.relation
overallScore >= 40 ? '⚠️ 日主关系一般' : '❌ 日主关系欠佳'
【纳音五行】
男:naYin.ny1
女:naYin.ny2
'📝 纳音不同,需注意调和'
【天干相合】
heResult
【地支关系】
`;
dzResults.forEach(p => {
report += ` p.name(p.z1 vs p.z2):p.relation\n`;
});
report += `
━━━━━━━━━━━━━━━━━━━━
💡 综合建议
`;
if (overallScore >= 70) {
report += `
🎉 恭喜!你们的八字非常相配。
• 日主关系和谐
• '可多培养共同兴趣'
• '虽有冲克,但可化解'
💕 婚姻展望:
婚后生活较和谐稳定,双方能互相理解支持。
`;
} else if (overallScore >= 50) {
report += `
📝 中等缘分,需要用心经营。
• 日主关系'需加强'
• 建议多沟通,了解彼此需求
• 注意dzResults.find(p => p.relation.includes('冲'))?.name || '相关'地支的影响
💡 婚姻建议:
婚后需要双方共同努力,多包容理解。
`;
} else {
report += `
⚠️ 缘分较弱,需要谨慎对待。
• 存在一定婚配障碍
• 建议深入了解后再做结婚决定
• 如坚持在一起,需要更多磨合
⚠️ 注意事项:
重点关注事业、感情沟通方面。
`;
}
return report;
}
// 主入口
const args = process.argv.slice(2);
if (args.length < 2) {
console.log(`
💕 合婚分析
用法:
node marriage.js <userId1> <userId2>
node marriage.js <name1> <bazi1> <name2> <bazi2>
示例:
node marriage.js 111111 222222
node marriage.js 张三 "甲子 乙丑 丙寅 丁卯" 李四 "庚午 辛巳 庚辰 癸未"
`);
process.exit(1);
}
let name1, bazi1, name2, bazi2;
// 判断输入模式:2个参数=从档案加载,4个参数=直接输入
if (args.length === 2) {
const profile1 = loadProfile(args[0]);
const profile2 = loadProfile(args[1]);
if (!profile1 || !profile2) {
console.log('❌ 未找到用户档案');
process.exit(1);
}
name1 = profile1.name;
name2 = profile2.name;
bazi1 = profile1.bazi.year + ' ' + profile1.bazi.month + ' ' + profile1.bazi.day + ' ' + profile1.bazi.hour;
bazi2 = profile2.bazi.year + ' ' + profile2.bazi.month + ' ' + profile2.bazi.day + ' ' + profile2.bazi.hour;
console.log('📋 从档案加载\n');
} else {
name1 = args[0];
bazi1 = args[1];
name2 = args[2];
bazi2 = args[3];
}
console.log(generateReport(name1, bazi1, name2, bazi2));
module.exports = { generateReport, analyzeDayMasterCompatibility };
FILE:scripts/meihua.js
#!/usr/bin/env node
/**
* 梅花易数占卜脚本
* 支持:报数起卦、时间起卦、方位起卦
*/
const tianGan = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
const diZhi = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
// 先天八卦数
const baGuaNum = {
'乾': 1, '兑': 2, '离': 3, '震': 4,
'巽': 5, '坎': 6, '艮': 7, '坤': 8
};
// 八卦属性
const baGuaInfo = {
'乾': { element: '金', symbol: '☰', trait: '刚健', color: '白色', direction: '西北' },
'兑': { element: '金', symbol: '☱', trait: '喜悦', color: '白色', direction: '西' },
'离': { element: '火', symbol: '☲', trait: '明亮', color: '红色', direction: '南' },
'震': { element: '木', symbol: '☳', trait: '震动', color: '青色', direction: '东' },
'巽': { element: '木', symbol: '☴', trait: '入', color: '绿色', direction: '东南' },
'坎': { element: '水', symbol: '☵', trait: '险陷', color: '黑色', direction: '北' },
'艮': { element: '土', symbol: '☶', trait: '阻止', color: '黄色', direction: '东北' },
'坤': { element: '土', symbol: '☷', trait: '柔顺', color: '黄色', direction: '西南' }
};
// 64卦象(简化版)
const hexagrams = {
'11': { name: '乾为天', gua: '乾', trait: '元亨利贞', meaning: '大吉大利' },
'12': { name: '天地否', gua: '否', trait: '不交不通', meaning: '诸事不顺' },
'13': { name: '天风姤', gua: '姤', trait: '遇也', meaning: '偶遇机缘' },
'21': { name: '地天泰', gua: '泰', trait: '天地交泰', meaning: '万事亨通' },
'22': { name: '坤为地', gua: '坤', trait: '元亨利牝马之贞', meaning: '柔顺有利' },
'31': { name: '火天大有', gua: '大有', trait: '元亨', meaning: '收获丰富' },
'32': { name: '火地晋', gua: '晋', trait: '康候用锡马', meaning: '晋升发展' },
'33': { name: '雷天大壮', gua: '大壮', trait: '利贞', meaning: '气势正盛' },
'41': { name: '风地观', gua: '观', trait: '盥而不荐', meaning: '观察时机' },
'42': { name: '风雷益', gua: '益', trait: '利有攸往', meaning: '受益匪浅' },
'51': { name: '水地比', gua: '比', trait: '吉原筮元永贞', meaning: '亲和友善' },
'52': { name: '水山蹇', gua: '蹇', trait: '利西南不利东北', meaning: '艰难险阻' },
'61': { name: '山地剥', gua: '剥', trait: '不利有攸往', meaning: '需要隐忍' },
'62': { name: '山地艮', gua: '艮', trait: '止也', meaning: '适可而止' },
'71': { name: '泽地萃', gua: '萃', trait: '利见大人', meaning: '人才汇聚' },
'72': { name: '泽山咸', gua: '咸', trait: '亨利贞', meaning: '感情顺利' },
'81': { name: '雷地豫', gua: '豫', trait: '利建侯行师', meaning: '安乐祥和' },
'82': { name: '雷风恒', gua: '恒', trait: '亨无咎利贞', meaning: '恒久稳定' }
};
// 简化版64卦映射(取常见卦)
const simpleHexagrams = {
'11': '乾为天', '12': '天雷无妄', '13': '天风姤', '14': '天山遁',
'15': '天地否', '16': '风地观', '17': '山地剥', '18': '坤为地',
'21': '地天泰', '22': '地雷复', '23': '地泽临', '24': '地天决',
'25': '地风升', '26': '地火明夷', '27': '地山谦', '28': '地水师',
'31': '火天大有', '32': '火雷噬嗑', '33': '火风鼎', '34': '火水未济',
'35': '火山旅', '36': '风水涣', '37': '山火贲', '38': '山水蒙',
'41': '风天小畜', '42': '风雷益', '43': '风泽中孚', '44': '风山渐',
'45': '风地观', '46': '风火家人', '47': '风雷恒', '48': '风水困',
'51': '水天需', '52': '水雷屯', '53': '水泽节', '54': '水山蹇',
'55': '水地比', '56': '水火既济', '57': '水风井', '58': '水火未济',
'61': '山天大畜', '62': '山雷颐', '63': '山泽损', '64': '山风蛊',
'71': '泽天夬', '72': '泽雷随', '73': '泽火革', '74': '泽水困',
'75': '泽地萃', '76': '泽山咸', '77': '雷天大壮', '78': '雷泽归妹',
'81': '雷地豫', '82': '雷水解', '83': '雷风恒', '84': '雷山小过',
'85': '雷火丰', '86': '雷电噬嗑', '87': '雷风恒', '88': '雷山小过'
};
/**
* 报数起卦
* 报3个数字,分别对应上卦、下卦、动爻
*/
function numbersToHexagram(nums) {
if (nums.length < 3) {
throw new Error('请报3个数字');
}
const num1 = parseInt(nums[0]) % 8 || 8;
const num2 = parseInt(nums[1]) % 8 || 8;
const dongYao = parseInt(nums[2]) % 6 || 6;
const guaNames = ['乾', '兑', '离', '震', '巽', '坎', '艮', '坤'];
const shangGua = guaNames[(num1 - 1) % 8];
const xiaGua = guaNames[(num2 - 1) % 8];
return {
shangGua,
xiaGua,
dongYao,
hexagram: simpleHexagrams[`num1num2`] || `shangGuaxiaGua`,
num1, num2, dongYao
};
}
/**
* 时间起卦
*/
function timeToHexagram(date = new Date()) {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const hour = date.getHours();
// 报数法:年月日时相加
const num1 = (year + month + day) % 8 || 8;
const num2 = (month + day + hour) % 8 || 8;
const num3 = (year + month + day + hour) % 6 || 6;
return numbersToHexagram([num1, num2, num3]);
}
/**
* 方位起卦
* 东南西北对应 1-4
*/
function directionToHexagram(directions) {
if (directions.length < 2) {
throw new Error('请提供2个方位');
}
const dirMap = {
'东': 4, '南': 9, '西': 2, '北': 6,
'东南': 5, '东北': 7, '西南': 8, '西北': 1
};
const num1 = dirMap[directions[0]] || 1;
const num2 = dirMap[directions[1]] || 2;
return numbersToHexagram([num1, num2, 3]);
}
/**
* 判断体用关系
*/
function getTiYong(hexagram, shangGua, xiaGua) {
// 先天八卦:乾1兑2离3震4巽5坎6艮7坤8
// 数大为用,数小为体
const num1 = baGuaNum[shangGua];
const num2 = baGuaNum[xiaGua];
// 上卦数 > 下卦数 → 上为用,下为体
// 上卦数 < 下卦数 → 下为用,上为体
let tiYong;
if (num1 > num2) {
tiYong = { ti: xiaGua, yong: shangGua, gua: '上卦为用,下卦为体' };
} else if (num1 < num2) {
tiYong = { ti: shangGua, yong: xiaGua, gua: '下卦为用,上卦为体' };
} else {
tiYong = { ti: shangGua, yong: xiaGua, gua: '体用比和' };
}
return tiYong;
}
/**
* 五行生克判断
*/
function getElementRelation(ti, yong) {
const elements = {
'乾': '金', '兑': '金', '离': '火', '震': '木',
'巽': '木', '坎': '水', '艮': '土', '坤': '土'
};
const tiEl = elements[ti];
const yongEl = elements[yong];
// 五行相生:木→火→土→金→水→木
// 五行相克:木→土→水→火→金→木
const relations = {
// 用生体 → 大吉(得生助)
'火木': '用生体 → 大吉', '土火': '用生体 → 大吉', '金土': '用生体 → 大吉',
'水金': '用生体 → 大吉', '木水': '用生体 → 大吉',
// 体生用 → 泄气(有耗散)
'木火': '体生用 → 泄气', '火土': '体生用 → 泄气', '土金': '体生用 → 泄气',
'金水': '体生用 → 泄气', '水木': '体生用 → 泄气',
// 体克用 → 有利(我制对方)
'木土': '体克用 → 有利', '土水': '体克用 → 有利', '水火': '体克用 → 有利',
'火金': '体克用 → 有利', '金木': '体克用 → 有利',
// 用克体 → 凶(对方克我)
'土木': '用克体 → 凶', '水土': '用克体 → 凶', '火水': '用克体 → 凶',
'金火': '用克体 → 凶', '木金': '用克体 → 凶'
};
const key = tiEl + yongEl;
let result;
if (tiEl === yongEl) {
result = '体用比和 → 平稳';
} else {
result = relations[key] || '体用相合 → 平稳';
}
return { tiEl, yongEl, result };
}
/**
* 动爻分析
*/
function analyzeDongYao(dongYao) {
const yaoNames = ['初爻', '二爻', '三爻', '四爻', '五爻', '上爻'];
return {
position: yaoNames[dongYao - 1] || '上爻',
note: dongYao <= 3 ? '下卦动' : '上卦动'
};
}
/**
* 生成占卜报告
*/
function generateDivinationReport(hexagramData, type, inputData) {
const { shangGua, xiaGua, dongYao } = hexagramData;
const tiYong = getTiYong(hexagramData.hexagram, shangGua, xiaGua);
const elements = getElementRelation(tiYong.ti, tiYong.yong);
const yaoInfo = analyzeDongYao(dongYao);
const shangInfo = baGuaInfo[shangGua] || baGuaInfo['乾'];
const xiaInfo = baGuaInfo[xiaGua] || baGuaInfo['坤'];
// 判断吉凶
let jiXiong;
if (elements.result.includes('大吉') || elements.result.includes('比和')) {
jiXiong = '✅ 吉利';
} else if (elements.result.includes('凶')) {
jiXiong = '❌ 需谨慎';
} else {
jiXiong = '⚠️ 中平';
}
const report = `
🔮 【梅花易数占卜】
📋 占卜信息
类型:type
输入:inputData
🎴 卦象
上卦:shangGua shangInfo.symbol(shangInfo.trait)
下卦:xiaGua xiaInfo.symbol(xiaInfo.trait)
动爻:yaoInfo.position(yaoInfo.note)
📊 卦名
hexagramData.hexagram
⚖️ 体用分析
体卦:tiYong.ti(elements.tiEl气)
用卦:tiYong.yong(elements.yongEl气)
关系:elements.result
jiXiong
🎯 综合判断
shangInfo.direction方向shangInfo.trait,xiaInfo.direction方向xiaInfo.trait
今日shangInfo.color色、xiaInfo.color色利于增强运势
💡 建议
elements.result.includes('凶')
? '宜静不宜动,谨慎行事'
: '循序渐进,稳扎稳打'
`;
return report;
}
// 主入口
const args = process.argv.slice(2);
if (args.length === 0) {
// 默认时间起卦
const result = timeToHexagram(new Date());
console.log(generateDivinationReport(result, '时间起卦', '当前时间'));
} else if (args[0] === '--help' || args[0] === '-h') {
console.log(`
梅花易数占卜
用法:
node meihua.js # 时间起卦
node meihua.js 数字1 数字2 数字3 # 报数起卦
node meihua.js 东 南 # 方位起卦
示例:
node meihua.js 3 5 2
node meihua.js 东 南
`);
} else if (args.length === 1 && ['东', '南', '西', '北', '东南', '东北', '西南', '西北'].includes(args[0])) {
// 单方位 → 用另一个默认方位
const result = directionToHexagram([args[0], '中']);
console.log(generateDivinationReport(result, '方位起卦', args[0]));
} else if (args.length === 2 && ['东', '南', '西', '北', '东南', '东北', '西南', '西北'].includes(args[0])) {
// 两个方位
const result = directionToHexagram(args);
console.log(generateDivinationReport(result, '方位起卦', args.join('-')));
} else if (!isNaN(args[0])) {
// 报数起卦
try {
const result = numbersToHexagram(args);
console.log(generateDivinationReport(result, '报数起卦', args.join('-')));
} catch (e) {
console.error('错误:', e.message);
process.exit(1);
}
} else {
// 时间起卦
const result = timeToHexagram(new Date());
console.log(generateDivinationReport(result, '时间起卦', '当前时间'));
}
FILE:scripts/morning-push.js
#!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('invalid userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('illegal path');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{};}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const wd=now.getDay();
const date=`now.getFullYear()-String(now.getMonth()+1).padStart(2,'0')-String(now.getDate()).padStart(2,'0')`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
console.log(`今天是weekday(date)。请运行今日运势推送:node scripts/daily-fortune.js date。输出今日综合运势(事业/财运/感情/健康)、幸运颜色/数字/方位、今日宜忌、吉时,以及每日命理一言。中文输出,简洁精准。`);
FILE:scripts/preference-tracker.js
#!/usr/bin/env node
/**
* 用户偏好学习系统
* 基于用户互动记录,动态调整关注领域权重
*
* 用法(供 Agent 调用):
* node preference-tracker.js record <userId> <topic> [context]
* node preference-tracker.js weights <userId>
* node preference-tracker.js top <userId> [n]
*/
const fs = require('fs');
const path = require('path');
const PROFILES_DIR = path.join(__dirname, '../data/profiles');
// 支持的关注领域
const TOPICS = ['财运', '事业', '感情', '健康', '婚姻', '子女', '官司', '出行', '风水'];
// 互动来源权重倍率
const CONTEXT_MULTIPLIERS = {
explicit_query: 2.0, // 用户主动提问
topic_drill: 1.5, // 用户追问同一话题
morning_push: 0.8, // 晨间推送被消费
evening_push: 0.8, // 晚间推送被消费
};
const DECAY_LAMBDA = 0.05; // 衰减系数,约14天半衰期
const MAX_LOG_SIZE = 500; // 最大记录条数
const MIN_WEIGHT = 0.5; // 进入 focusAreas 的最低权重
const DEFAULT_FOCUS = ['事业', '财运', '健康'];
// ─────────────────────────────────────────────
// 文件 I/O
// ─────────────────────────────────────────────
function loadProfile(userId) {
const fp = path.join(PROFILES_DIR, `userId.json`);
if (!fs.existsSync(fp)) return null;
return JSON.parse(fs.readFileSync(fp, 'utf8'));
}
function saveProfile(userId, profile) {
const fp = path.join(PROFILES_DIR, `userId.json`);
profile.updatedAt = new Date().toISOString().split('T')[0];
fs.writeFileSync(fp, JSON.stringify(profile, null, 2), 'utf8');
}
// ─────────────────────────────────────────────
// 核心算法:指数衰减加权
// ─────────────────────────────────────────────
function _computeWeights(log) {
const now = Date.now();
const totals = {};
TOPICS.forEach(t => { totals[t] = 0; });
for (const entry of (log || [])) {
if (!TOPICS.includes(entry.topic)) continue;
if (!entry.ts || typeof entry.ts !== 'number') continue;
const daysDelta = (now - entry.ts) / 86400000;
const multiplier = CONTEXT_MULTIPLIERS[entry.context] || 1.0;
totals[entry.topic] += multiplier * Math.exp(-DECAY_LAMBDA * daysDelta);
}
return totals;
}
function _normalizeWeights(raw) {
const max = Math.max(...Object.values(raw), 0.001);
const result = {};
for (const [t, w] of Object.entries(raw)) {
result[t] = parseFloat((w / max).toFixed(3));
}
return result;
}
function _sortedTopics(weights) {
return Object.entries(weights)
.sort((a, b) => b[1] - a[1])
.map(([topic, weight]) => ({ topic, weight }));
}
// ─────────────────────────────────────────────
// 公开 API
// ─────────────────────────────────────────────
/**
* 记录一次互动
*/
function recordInteraction(userId, topic, context = 'explicit_query') {
const profile = loadProfile(userId);
if (!profile) return false;
if (!TOPICS.includes(topic)) return false;
if (!profile.interactionLog) profile.interactionLog = [];
profile.interactionLog.push({ ts: Date.now(), topic, context });
// 超出上限时删除最旧的记录
if (profile.interactionLog.length > MAX_LOG_SIZE) {
profile.interactionLog = profile.interactionLog.slice(-MAX_LOG_SIZE);
}
// 重新计算并更新 focusAreas
const raw = _computeWeights(profile.interactionLog);
const normalized = _normalizeWeights(raw);
const top = _sortedTopics(normalized)
.filter(({ weight }) => weight >= MIN_WEIGHT)
.slice(0, 3)
.map(({ topic }) => topic);
if (!profile.preferences) profile.preferences = {};
profile.preferences.focusAreas = top.length > 0 ? top : DEFAULT_FOCUS;
saveProfile(userId, profile);
return true;
}
/**
* 获取所有领域权重(归一化,降序)
*/
function getWeights(userId) {
const profile = loadProfile(userId);
if (!profile) return [];
const raw = _computeWeights(profile.interactionLog || []);
const normalized = _normalizeWeights(raw);
return _sortedTopics(normalized);
}
/**
* 获取 top-n 关注领域(有互动记录用计算结果,否则用 profile 默认值)
*/
function getTopTopics(userId, n = 3) {
const profile = loadProfile(userId);
if (!profile) return DEFAULT_FOCUS.slice(0, n);
const log = profile.interactionLog || [];
if (log.length === 0) {
return (profile.preferences?.focusAreas || DEFAULT_FOCUS).slice(0, n);
}
const raw = _computeWeights(log);
const normalized = _normalizeWeights(raw);
return _sortedTopics(normalized)
.slice(0, n)
.map(({ topic }) => topic);
}
module.exports = { recordInteraction, getWeights, getTopTopics, TOPICS };
// ─────────────────────────────────────────────
// 命令行入口(供 Agent 调用)
// ─────────────────────────────────────────────
if (require.main === module) {
const [cmd, userId, ...rest] = process.argv.slice(2);
if (!cmd || !userId) {
console.log(`
🧠 用户偏好追踪器
用法:
node preference-tracker.js record <userId> <topic> [context]
node preference-tracker.js weights <userId>
node preference-tracker.js top <userId> [n]
topic 可选: TOPICS.join(' | ')
context 可选: explicit_query | topic_drill | morning_push | evening_push
示例:
node preference-tracker.js record 123456 财运 explicit_query
node preference-tracker.js weights 123456
node preference-tracker.js top 123456 3
`);
process.exit(1);
}
switch (cmd) {
case 'record': {
const [topic, context = 'explicit_query'] = rest;
if (!topic) { console.error('缺少 topic 参数'); process.exit(1); }
const ok = recordInteraction(userId, topic, context);
console.log(JSON.stringify({ success: ok, userId, topic, context }));
break;
}
case 'weights': {
const weights = getWeights(userId);
console.log(JSON.stringify({ userId, weights }));
break;
}
case 'top': {
const n = parseInt(rest[0] || '3');
const topics = getTopTopics(userId, n);
console.log(JSON.stringify({ userId, topTopics: topics }));
break;
}
default:
console.error(`未知命令: cmd`);
process.exit(1);
}
}
FILE:scripts/profile.js
#!/usr/bin/env node
/**
* 用户档案管理脚本 - 支持家庭成员
* 保存/读取用户命理数据及家庭成员
*/
const fs = require('fs');
const path = require('path');
const PROFILES_DIR = path.join(__dirname, '../data/profiles');
// 确保目录存在
if (!fs.existsSync(PROFILES_DIR)) {
fs.mkdirSync(PROFILES_DIR, { recursive: true });
}
/**
* 获取用户档案路径
*/
function getProfilePath(userId) {
return path.join(PROFILES_DIR, `userId.json`);
}
/**
* 读取用户档案
*/
function loadProfile(userId) {
const filePath = getProfilePath(userId);
if (!fs.existsSync(filePath)) {
return null;
}
const data = fs.readFileSync(filePath, 'utf8');
return JSON.parse(data);
}
/**
* 保存用户档案
*/
function saveProfile(userId, data) {
const filePath = getProfilePath(userId);
const profile = {
...data,
userId,
updatedAt: new Date().toISOString().split('T')[0]
};
fs.writeFileSync(filePath, JSON.stringify(profile, null, 2), 'utf8');
return profile;
}
/**
* 更新档案字段
*/
function updateProfile(userId, field, value) {
const profile = loadProfile(userId) || {};
// 处理嵌套字段如 "family.spouse.name"
const fields = field.split('.');
let current = profile;
for (let i = 0; i < fields.length - 1; i++) {
if (!current[fields[i]]) {
current[fields[i]] = {};
}
current = current[fields[i]];
}
current[fields[fields.length - 1]] = value;
saveProfile(userId, profile);
console.log(`✅ 已更新: field = value`);
}
/**
* 添加家庭成员
*/
function addFamilyMember(userId, type, name, data = {}) {
const profile = loadProfile(userId);
if (!profile) {
console.log(`❌ 用户档案不存在: userId`);
return;
}
if (!profile.family) {
profile.family = {};
}
const memberData = {
name,
profile: {
birthDate: data.birthDate || '待录入',
birthTime: data.birthTime || '待录入',
birthPlace: data.birthPlace || '',
gender: data.gender || '',
lunarBirth: data.lunarBirth || ''
},
bazi: {
year: data.year || '',
month: data.month || '',
day: data.day || '',
hour: data.hour || '',
dayStem: data.dayStem || '',
zodiac: data.zodiac || '',
sect: data.sect || '晚子时',
source: 'pending'
},
relationship: type,
addedAt: new Date().toISOString().split('T')[0]
};
if (type === 'children') {
if (!profile.family.children) {
profile.family.children = [];
}
profile.family.children.push(memberData);
console.log(`✅ 已添加子女: name`);
} else {
profile.family[type] = memberData;
console.log(`✅ 已添加type: name`);
}
saveProfile(userId, profile);
}
/**
* 添加子女
*/
function addChild(userId, name, birthDate, gender) {
const profile = loadProfile(userId);
if (!profile) {
console.log(`❌ 用户档案不存在: userId`);
return;
}
const child = {
name,
profile: {
birthDate: birthDate || '待录入',
birthTime: '待录入',
birthPlace: '',
gender: gender || '',
lunarBirth: ''
},
bazi: {
year: '',
month: '',
day: '',
hour: '',
source: 'pending'
},
relationship: '子女',
addedAt: new Date().toISOString().split('T')[0]
};
if (!profile.family) profile.family = {};
if (!profile.family.children) profile.family.children = [];
profile.family.children.push(child);
saveProfile(userId, profile);
console.log(`✅ 已添加子女: name (gender || '待定')`);
}
/**
* 列出家庭成员
*/
function listFamilyMembers(userId) {
const profile = loadProfile(userId);
if (!profile) {
console.log(`❌ 用户档案不存在: userId`);
return;
}
console.log(`\n👪 家庭成员列表 (profile.name)\n`);
const { family } = profile;
if (family?.spouse?.name && family.spouse.name !== '配偶') {
console.log(` 👫 配偶: family.spouse.name`);
console.log(` 八字: family.spouse.bazi?.year || '?' family.spouse.bazi?.month || '' family.spouse.bazi?.day || '' family.spouse.bazi?.hour || ''`);
}
if (family?.father?.name && family.father.name !== '父亲') {
console.log(` 👨 父亲: family.father.name`);
console.log(` 八字: family.father.bazi?.year || '?' family.father.bazi?.month || '' family.father.bazi?.day || '' family.father.bazi?.hour || ''`);
}
if (family?.mother?.name && family.mother.name !== '母亲') {
console.log(` 👩 母亲: family.mother.name`);
console.log(` 八字: family.mother.bazi?.year || '?' family.mother.bazi?.month || '' family.mother.bazi?.day || '' family.mother.bazi?.hour || ''`);
}
if (family?.children?.length > 0) {
console.log(` 👶 子女 (family.children.length):`);
family.children.forEach((child, i) => {
console.log(` i + 1. child.name (child.profile?.gender || '待定')`);
console.log(` 出生: child.profile?.birthDate || '待录入'`);
console.log(` 八字: child.bazi?.year || '?' child.bazi?.month || '' child.bazi?.day || '' child.bazi?.hour || ''`);
});
}
if (!family?.spouse && !family?.father && !family?.mother && (!family?.children || family.children.length === 0)) {
console.log(` (暂无家庭成员记录)`);
}
console.log('');
}
/**
* 显示完整档案
*/
function showProfile(userId) {
const profile = loadProfile(userId);
if (!profile) {
console.log(`❌ 用户档案不存在: userId`);
return;
}
console.log('\n📋 用户档案\n');
console.log(`ID: profile.userId`);
console.log(`姓名: profile.name`);
console.log(`出生: profile.profile?.birthDate profile.profile?.birthTime`);
console.log(`地点: profile.profile?.birthPlace`);
console.log(`性别: profile.profile?.gender`);
console.log('\n🧮 八字');
console.log(` profile.bazi?.year profile.bazi?.month profile.bazi?.day profile.bazi?.hour`);
console.log(` 日主: profile.bazi?.dayStem`);
console.log(` 生肖: profile.bazi?.zodiac`);
if (profile.ziwei) {
console.log('\n✨ 紫微');
console.log(` 命宫: profile.ziwei.mingGong`);
console.log(` 命主: profile.ziwei.mingZhu`);
}
listFamilyMembers(userId);
}
/**
* 列出所有用户
*/
function listProfiles() {
const files = fs.readdirSync(PROFILES_DIR).filter(f => f.endsWith('.json'));
console.log('\n📋 用户列表\n');
files.forEach(f => {
const userId = f.replace('.json', '');
const data = loadProfile(userId);
console.log(` userId | data?.name || '未知' | data?.profile?.birthDate || '未知'`);
});
console.log(`\n共 files.length 个用户\n`);
}
/**
* 删除档案
*/
function deleteProfile(userId) {
const filePath = getProfilePath(userId);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
console.log(`✅ 已删除: userId`);
} else {
console.log(`❌ 档案不存在: userId`);
}
}
// 主入口
const args = process.argv.slice(2);
const command = args[0];
switch (command) {
case 'show':
case 'load':
if (args[1]) {
showProfile(args[1]);
} else {
console.log('用法: node profile.js show <userId>');
}
break;
case 'list':
listProfiles();
break;
case 'save':
if (args.length < 4) {
console.log('用法: node profile.js save <userId> <field> <value>');
console.log('示例: node profile.js save 123 name 张三');
console.log(' node profile.js save 123 bazi.day 戊子');
} else {
updateProfile(args[1], args[2], args[3]);
}
break;
case 'add':
// node profile.js add <userId> <type> <name> [birthDate] [gender]
if (args.length < 4) {
console.log('用法:');
console.log(' node profile.js add <userId> spouse <name> [出生日期] [性别]');
console.log(' node profile.js add <userId> father <name> [出生日期]');
console.log(' node profile.js add <userId> mother <name> [出生日期]');
console.log(' node profile.js add <userId> child <name> <出生日期> <性别>');
console.log('');
console.log('示例:');
console.log(' node profile.js add 123 spouse 李四 1990-05-15 女');
console.log(' node profile.js add 123 child 子女姓名 2020-01-01 男');
} else {
const userId = args[1];
const type = args[2];
const name = args[3];
if (type === 'child') {
const birthDate = args[4];
const gender = args[5];
addChild(userId, name, birthDate, gender);
} else {
addFamilyMember(userId, type, name, {
birthDate: args[4],
gender: type === 'spouse' ? (args[5] || '女') : (args[4] ? '男' : '')
});
}
}
break;
case 'family':
if (args[1]) {
listFamilyMembers(args[1]);
} else {
console.log('用法: node profile.js family <userId>');
}
break;
case 'delete':
if (args[1]) {
deleteProfile(args[1]);
} else {
console.log('用法: node profile.js delete <userId>');
}
break;
default:
console.log(`
🗂️ 用户档案管理 (支持家庭成员)
用法:
node profile.js show <userId> 显示完整档案
node profile.js list 列出所有用户
node profile.js save <userId> <field> <value> 保存字段
node profile.js add <userId> <type> <name> [参数] 添加家庭成员
node profile.js family <userId> 显示家庭成员
node profile.js delete <userId> 删除档案
家庭成员类型:
spouse - 配偶
father - 父亲
mother - 母亲
child - 子女
示例:
# 查看档案
node profile.js show 123456
# 添加配偶
node profile.js add 123456 spouse 配偶姓名 1990-05-15 女
# 添加子女
node profile.js add 123456 child 子女姓名 2020-01-01 男
# 添加父亲
node profile.js add 123456 father 父亲姓名 1950-03-15
# 查看家庭成员
node profile.js family 123456
# 保存八字
node profile.js save 123456 family.spouse.bazi.year 庚午
`);
}
module.exports = { loadProfile, saveProfile, updateProfile, addFamilyMember, addChild, listFamilyMembers, showProfile };
FILE:scripts/push-toggle.js
#!/usr/bin/env node
/**
* 每日运势推送开关
* 开启时自动创建用户专属 cron job,关闭时删除
*
* 用法:
* node push-toggle.js on <userId> 开启推送(默认早8点+晚8点)
* node push-toggle.js off <userId> 关闭推送(删除 cron)
* node push-toggle.js status <userId> 查看状态
* node push-toggle.js on <userId> --morning 08:00 --evening 20:00
*/
const fs = require('fs');
const path = require('path');
const { getTopTopics } = require('./preference-tracker');
const PROFILES_DIR = path.join(__dirname, '../data/profiles');
// 各领域深度分析模板
const TOPIC_EXPANDED = {
'财运': `💰 财运深析(重点关注):
- 今日财星状态与格局分析
- 投资/支出/收款建议
- 结合今日金融/市场新闻的财运影响与风险`,
'事业': `💼 事业深析(重点关注):
- 今日官禄宫能量与事业星状态
- 职场关键决策与行动建议
- 结合今日政策/商业新闻的机遇与风险`,
'感情': `💕 感情深析(重点关注):
- 今日桃花星与夫妻宫状态
- 感情互动与表达建议
- 今日社会/情感类新闻的命理启示`,
'健康': `🏥 健康深析(重点关注):
- 今日五行对应脏腑的能量状态
- 饮食、作息、运动建议
- 结合今日天气/环境/公共卫生新闻`,
'婚姻': `💍 婚姻深析(重点关注):
- 今日夫妻宫能量与刑冲状态
- 婚姻经营与沟通建议
- 今日家庭/社会新闻的婚姻启示`,
'子女': `👶 子女深析(重点关注):
- 今日子女宫状态
- 亲子关系与教育建议`,
'官司': `⚖️ 官司/是非深析(重点关注):
- 今日官星与白虎星分析
- 法律/合同/是非风险提示
- 结合今日司法/社会冲突新闻`,
'出行': `✈️ 出行深析(重点关注):
- 今日驿马星与方位吉凶
- 出行时机、方向与交通建议
- 结合今日天气/灾害/交通新闻`,
'风水': `🏠 风水深析(重点关注):
- 今日飞星方位吉凶
- 家居/办公能量调整建议`,
};
// 新闻到命理领域映射规则(嵌入 prompt,供 Agent 识别)
const NEWS_FORTUNE_MAPPING = `新闻与命理映射规则(识别今日新闻后按此对应):
- 市场波动/股债汇变动/降息加息 → 财运风险或机遇信号
- 政策出台/经济刺激/行业利好 → 事业机遇信号
- 监管收紧/行业整顿/合规要求 → 事业风险,行事低调
- 自然灾害/台风暴雪/恶劣天气 → 出行风险 + 健康警示
- 公共卫生/食品安全/空气质量 → 健康领域警示
- 社会冲突/司法/法律法规变动 → 官司/是非风险
- 科技突破/国际贸易/地缘政治 → 事业/财运双向影响分析`;
function loadProfile(userId) {
const filePath = path.join(PROFILES_DIR, `userId.json`);
if (!fs.existsSync(filePath)) return null;
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
function saveProfile(userId, profile) {
const filePath = path.join(PROFILES_DIR, `userId.json`);
profile.updatedAt = new Date().toISOString().split('T')[0];
fs.writeFileSync(filePath, JSON.stringify(profile, null, 2), 'utf8');
}
/**
* 创建用户专属 cron job
* 返回 cron job id,失败返回 null
*/
function createCronJob(userId, name, cronExpr, message, channel) {
const sessionKey = `agent:main:channel:direct:userId`;
const args = [
'cron', 'add',
'--name', name,
'--cron', cronExpr,
'--tz', 'Asia/Shanghai',
'--session', 'isolated',
'--session-key', sessionKey,
'--channel', channel,
'--to', userId,
'--announce',
'--timeout-seconds', '120',
'--message', message,
'--json'
];
try {
// openclaw cron 由运行时管理,输出配置供运行时处理
const cronConfig = { name, cronExpr, tz: 'Asia/Shanghai', session: 'isolated', sessionKey, channel, to: userId, announce: true, timeoutSeconds: 120, message };
console.log(`__OPENCLAW_CRON_ADD__:JSON.stringify(cronConfig)`);
return `cron:name:userId`;
} catch (e) {
console.error('创建 cron 失败:', e.message);
return null;
}
}
/**
* 删除 cron job
*/
function removeCronJob(cronId) {
try {
console.log(`__OPENCLAW_CRON_RM__:cronId`);
return true;
} catch (e) {
console.error(`删除 cron cronId 失败:`, e.message);
return false;
}
}
/**
* 构建早晨推送 prompt(今日运程)— 含偏好权重 + 新闻整合
*/
function buildMorningMessage(profile, topTopics) {
const bazi = profile.bazi || {};
const baziStr = `bazi.year bazi.month bazi.day bazi.hour`;
const name = profile.name || '用户';
const userId = profile.userId;
const top1 = topTopics[0] || '事业';
const top2 = topTopics[1] || '财运';
const top3 = topTopics[2] || '健康';
const expandedSection = TOPIC_EXPANDED[top1] || '';
return `请为name生成今日命理运程报告。
用户八字:baziStr,日主:bazi.dayStem
用户重点关注(按偏好排序):top1 > top2 > top3
步骤:
1) 运行 node scripts/daily-fortune.js 获取今日干支基础运程
2) 搜索今日重要新闻(财经、政策、社会、国际各一条)
NEWS_FORTUNE_MAPPING
3) 结合八字与新闻做个性化分析,重点展开【top1】领域深度分析
4) 完成后运行:node scripts/preference-tracker.js record userId top1 morning_push
输出格式:
🌅 【私人命理顾问】今日完整日期(含星期)
📊 今日综合指数
事业:★★★★☆ 财运:★★★☆☆ 感情:★★★☆☆ 健康:★★★★☆
🎨 幸运色:xxx(结合今日干支五行)
expandedSection
💼 今日宜忌
✅ 宜:xxx、xxx、xxx
❌ 忌:xxx、xxx
⚠️ 风险提示(结合命理+今日新闻背景,如无则省略)
📰 命理与时事(1-2句:将今日1条重要新闻与运势联系)
⏰ 今日三吉时:时辰(时间段)宜做xxx
💡 今日一句(命理格言或人生启示)`;
}
/**
* 构建晚间推送 prompt(明日预告)— 含偏好权重 + 新闻整合
*/
function buildEveningMessage(profile, topTopics) {
const bazi = profile.bazi || {};
const baziStr = `bazi.year bazi.month bazi.day bazi.hour`;
const name = profile.name || '用户';
const userId = profile.userId;
const top1 = topTopics[0] || '事业';
const top2 = topTopics[1] || '财运';
const expandedSection = TOPIC_EXPANDED[top1] || '';
return `请为name生成明日命理预告(今晚提前推送明日运势)。
用户八字:baziStr,日主:bazi.dayStem
用户重点关注(按偏好排序):top1 > top2
步骤:
1) 运行 node scripts/daily-fortune.js 获取明日(今日+1天)干支运程
2) 搜索今日晚间重要新闻,预判对明日的影响
NEWS_FORTUNE_MAPPING
3) 重点展开【top1】明日深度预告
4) 完成后运行:node scripts/preference-tracker.js record userId top1 evening_push
输出格式:
🌙 【明日预告】明日完整日期(含星期)
📊 明日综合指数
事业:★★★★☆ 财运:★★★☆☆ 感情:★★★☆☆ 健康:★★★★☆
🎨 明日幸运色:xxx
expandedSection.replace('今日', '明日')
💼 明日宜忌
✅ 宜:xxx、xxx
❌ 忌:xxx、xxx
⚠️ 明日风险预警(结合命理+今晚新闻动向,如无则省略)
📰 时事预判(今晚新闻对明日命理的影响,1句)
⏰ 明日三吉时
💡 今晚一句`;
}
// ─────────────────────────────────────────────
function enablePush(userId, options = {}) {
const profile = loadProfile(userId);
if (!profile) {
console.log(`❌ 用户档案不存在: userId,请先注册`);
return false;
}
const morningTime = options.morning || '08:00';
const eveningTime = options.evening || '20:00';
const channel = options.channel || (profile.preferences?.channels?.[0]) || 'telegram';
const [mHour, mMin] = morningTime.split(':');
const [eHour, eMin] = eveningTime.split(':');
const morningCron = `mMin mHour * * *`;
const eveningCron = `eMin eHour * * *`;
console.log(`\n⏳ 正在为 profile.name(userId) 创建推送计划...\n`);
// 读取用户偏好权重
const topTopics = getTopTopics(userId, 3);
console.log(` 关注领域:topTopics.join(' > ')`);
// 如果已有 cron,先删除旧的
const existing = profile.push?.cronIds || {};
if (existing.morning) { removeCronJob(existing.morning); }
if (existing.evening) { removeCronJob(existing.evening); }
// 创建早晨 cron
const morningId = createCronJob(
userId,
`yunshi-morning-userId`,
morningCron,
buildMorningMessage(profile, topTopics),
channel
);
// 创建晚间 cron
const eveningId = createCronJob(
userId,
`yunshi-evening-userId`,
eveningCron,
buildEveningMessage(profile, topTopics),
channel
);
// 保存到档案
if (!profile.preferences) profile.preferences = {};
profile.preferences.pushEnabled = true;
profile.preferences.pushMorning = true;
profile.preferences.pushEvening = true;
profile.preferences.morningTime = morningTime;
profile.preferences.eveningTime = eveningTime;
profile.preferences.channels = [channel];
profile.push = {
cronIds: {
morning: morningId,
evening: eveningId
},
createdAt: new Date().toISOString()
};
saveProfile(userId, profile);
console.log(`✅ 推送已开启!\n`);
console.log(` 用户: profile.name (userId)`);
console.log(` 渠道: channel`);
console.log(` 🌅 早晨运程: 每天 morningTime ${morningId)` : '⚠️ 创建失败'}`);
console.log(` 🌙 晚间预告: 每天 eveningTime ${eveningId)` : '⚠️ 创建失败'}`);
console.log('');
return true;
}
function disablePush(userId) {
const profile = loadProfile(userId);
if (!profile) {
console.log(`❌ 用户档案不存在: userId`);
return false;
}
// 删除 cron job
const cronIds = profile.push?.cronIds || {};
let removed = 0;
if (cronIds.morning) { if (removeCronJob(cronIds.morning)) removed++; }
if (cronIds.evening) { if (removeCronJob(cronIds.evening)) removed++; }
if (!profile.preferences) profile.preferences = {};
profile.preferences.pushEnabled = false;
profile.preferences.pushMorning = false;
profile.preferences.pushEvening = false;
profile.push = { cronIds: {}, disabledAt: new Date().toISOString() };
saveProfile(userId, profile);
console.log(`\n✅ 推送已关闭(删除了 removed 个定时任务)\n`);
return true;
}
function showStatus(userId) {
const profile = loadProfile(userId);
if (!profile) {
console.log(`❌ 用户档案不存在: userId`);
return;
}
const pref = profile.preferences || {};
const enabled = pref.pushEnabled ?? pref.pushMorning ?? false;
const cronIds = profile.push?.cronIds || {};
console.log(`
👤 用户: profile.name (userId)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🧮 八字: profile.bazi?.year profile.bazi?.month profile.bazi?.day profile.bazi?.hour
📅 出生: profile.profile?.birthDate profile.profile?.birthTime
🔔 推送: '❌ 已关闭'
⏰ 早晨: 00' ${cronIds.morning)` : ''}
🌙 晚间: 00' ${cronIds.evening)` : ''}
📡 渠道: (pref.channels || ['telegram']).join(', ')
📆 推送创建: profile.push?.createdAt?.split('T')[0] || '未设置'
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
`);
}
module.exports = { enablePush, disablePush, showStatus };
// ─────────────────────────────────────────────
// 命令行入口
// ─────────────────────────────────────────────
if (require.main !== module) return;
const args = process.argv.slice(2);
const command = args[0];
const userId = args[1];
if (!userId) {
console.log(`
🔔 每日运势推送管理
用法:
node push-toggle.js on <userId> 开启推送(早8点+晚8点)
node push-toggle.js off <userId> 关闭推送
node push-toggle.js status <userId> 查看状态
node push-toggle.js on <userId> --morning 08:00 --evening 20:00
node push-toggle.js on <userId> --channel feishu
说明:
开启后自动创建两个定时任务:
- 每天早晨推送当日运程(默认 08:00)
- 每天晚间推送明日预告(默认 20:00)
`);
process.exit(1);
}
const options = {};
const morningIdx = args.indexOf('--morning');
if (morningIdx !== -1 && args[morningIdx + 1]) options.morning = args[morningIdx + 1];
const eveningIdx = args.indexOf('--evening');
if (eveningIdx !== -1 && args[eveningIdx + 1]) options.evening = args[eveningIdx + 1];
const channelIdx = args.indexOf('--channel');
if (channelIdx !== -1 && args[channelIdx + 1]) options.channel = args[channelIdx + 1];
switch (command) {
case 'on': enablePush(userId, options); break;
case 'off': disablePush(userId); break;
case 'status': showStatus(userId); break;
default:
console.log(`❌ 未知命令: command`);
process.exit(1);
}
FILE:scripts/qimen.js
#!/usr/bin/env node
/**
* 奇门遁甲排盘脚本
* 支持:时间起局、择日选时
*/
// 地支
const diZhi = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
// 九星
const nineStars = [
{ name: '天蓬', symbol: '⭐', element: '水', trait: '凶星', position: 1 },
{ name: '天任', symbol: '⭐', element: '土', trait: '凶星', position: 8 },
{ name: '天冲', symbol: '⭐', element: '木', trait: '吉星', position: 3 },
{ name: '天辅', symbol: '⭐', element: '木', trait: '吉星', position: 4 },
{ name: '天英', symbol: '⭐', element: '火', trait: '凶星', position: 9 },
{ name: '天芮', symbol: '⭐', element: '土', trait: '凶星', position: 2 },
{ name: '天柱', symbol: '⭐', element: '金', trait: '凶星', position: 7 },
{ name: '天心', symbol: '⭐', element: '金', trait: '吉星', position: 6 },
{ name: '天禽', symbol: '⭐', element: '土', trait: '大吉', position: 5 }
];
// 八门
const eightDoors = [
{ name: '休门', symbol: '🏠', element: '水', trait: '休息、平稳', position: 1 },
{ name: '生门', symbol: '🌱', element: '土', trait: '生长、财运', position: 8 },
{ name: '伤门', symbol: '💔', element: '木', trait: '受伤、变动', position: 3 },
{ name: '杜门', symbol: '🔒', element: '木', trait: '阻碍、保密', position: 4 },
{ name: '景门', symbol: '🔥', element: '火', trait: '文化、虚假', position: 9 },
{ name: '死门', symbol: '💀', element: '土', trait: '死亡、凶险', position: 2 },
{ name: '惊门', symbol: '😱', element: '金', trait: '惊恐、口舌', position: 7 },
{ name: '开门', symbol: '🚪', element: '金', trait: '开创、顺利', position: 6 }
];
// 三奇
const sanQi = ['乙', '丙', '丁'];
// 六仪
const liuYi = ['戊', '己', '庚', '辛', '壬', '癸'];
// 九宫(后天八卦方位)
const ninePalaces = [
{ num: 9, gua: '离', zhi: '午', direction: '南' },
{ num: 4, gua: '巽', zhi: '卯', direction: '东南' },
{ num: 2, gua: '坤', zhi: '未', direction: '西南' },
{ num: 3, gua: '震', zhi: '卯', direction: '东' },
{ num: 5, gua: '中', zhi: '戌', direction: '中' },
{ num: 1, gua: '坎', zhi: '子', direction: '北' },
{ num: 7, gua: '兑', zhi: '酉', direction: '西' },
{ num: 8, gua: '艮', zhi: '丑', direction: '东北' },
{ num: 6, gua: '乾', zhi: '戌', direction: '西北' }
];
/**
* 判断阴遁还是阳遁
* 冬至 → 夏至:阳遁
* 夏至 → 冬至:阴遁
*/
function isYangDun(date = new Date()) {
const month = date.getMonth() + 1;
const day = date.getDate();
// 节气粗略判断
// 夏至在6月21日,冬至在12月22日
const yearDay = date.getMonth() * 30 + day;
const summerSolstice = 5 * 30 + 21; // 约6月21日
const winterSolstice = 11 * 30 + 22; // 约12月22日
if (yearDay < summerSolstice || yearDay > winterSolstice) {
return true; // 阳遁(春夏)
}
return false; // 阴遁(秋冬)
}
/**
* 计算值符星(以2024-01-01为基准,按时辰连续推算)
*/
function getZhiFu(date, isYang) {
const baseDate = new Date('2024-01-01T00:00:00');
const diffDays = Math.floor((date - baseDate) / (1000 * 60 * 60 * 24));
const hour = date.getHours();
const shichen = Math.floor((hour + 1) / 2) % 12; // 当前时辰序号
const idx = ((diffDays * 12 + shichen) % 9 + 9) % 9;
// 阳遁顺布,阴遁逆布
return isYang ? nineStars[idx] : nineStars[(9 - idx) % 9];
}
/**
* 计算值使门(以2024-01-01为基准,按时辰连续推算)
*/
function getZhiShi(date, isYang) {
const baseDate = new Date('2024-01-01T00:00:00');
const diffDays = Math.floor((date - baseDate) / (1000 * 60 * 60 * 24));
const hour = date.getHours();
const shichen = Math.floor((hour + 1) / 2) % 12;
const idx = ((diffDays * 12 + shichen) % 8 + 8) % 8;
return isYang ? eightDoors[idx] : eightDoors[(8 - idx) % 8];
}
/**
* 排布九宫
*/
function arrangePalaces(isYang, zhiFu) {
const palaces = [];
// 阳遁顺布,阴遁逆布
const order = isYang ? [1, 2, 3, 4, 5, 6, 7, 8, 9] : [9, 8, 7, 6, 5, 4, 3, 2, 1];
// 九宫对应
const palaceMap = {
1: { gua: '坎', zhi: '子', direction: '北' },
2: { gua: '坤', zhi: '未', direction: '西南' },
3: { gua: '震', zhi: '卯', direction: '东' },
4: { gua: '巽', zhi: '辰', direction: '东南' },
5: { gua: '中', zhi: '戌', direction: '中' },
6: { gua: '乾', zhi: '戌', direction: '西北' },
7: { gua: '兑', zhi: '酉', direction: '西' },
8: { gua: '艮', zhi: '丑', direction: '东北' },
9: { gua: '离', zhi: '午', direction: '南' }
};
return order.map((num, index) => ({
position: index + 1,
num,
...palaceMap[num]
}));
}
/**
* 安九星到九宫
*/
function arrangeStars(palaces, isYang, zhiFu) {
const starIndex = nineStars.findIndex(s => s.name === zhiFu.name);
const result = palaces.map((palace, i) => {
const offset = isYang ? i : (8 - i);
const starIdx = (starIndex + offset) % 9;
const star = nineStars[starIdx];
// 天禽永远在中五宫
if (palace.num === 5) {
return { ...palace, star: nineStars[4] };
}
return { ...palace, star };
});
return result;
}
/**
* 安八门到九宫
*/
function arrangeDoors(palaces, isYang, zhiShi) {
const doorIndex = eightDoors.findIndex(d => d.name === zhiShi.name);
const result = palaces.map((palace, i) => {
const offset = isYang ? i : (8 - i);
const doorIdx = (doorIndex + offset) % 8;
const door = eightDoors[doorIdx];
// 死门永远在坤二宫
if (palace.num === 2) {
return { ...palace, door: eightDoors[5] };
}
return { ...palace, door };
});
return result;
}
/**
* 找三奇方位
*/
function findSanQi(palaces) {
const results = [];
palaces.forEach(p => {
if (p.star && p.door) {
const starName = p.star.name;
// 乙奇在坎、离;丙奇在乾、兑;丁奇在震、巽
if (starName === '天任' || starName === '天英') {
results.push({ qi: '乙', palace: p });
} else if (starName === '天心' || starName === '天柱') {
results.push({ qi: '丙', palace: p });
} else if (starName === '天冲' || starName === '天辅') {
results.push({ qi: '丁', palace: p });
}
}
});
return results;
}
/**
* 判断吉凶
*/
function judgeFortune(palace, sanQiCount) {
const star = palace.star;
const door = palace.door;
if (!star || !door) return '未知';
const starGood = ['天冲', '天辅', '天心', '天禽'].includes(star.name);
const doorGood = ['生门', '休门', '开门', '景门'].includes(door.name);
if (starGood && doorGood) return '大吉';
if (starGood || doorGood) return '中吉';
if (door.name === '死门' || door.name === '惊门') return '大凶';
return '凶';
}
/**
* 生成报告
*/
function generateReport(date, palaces, zhiFu, zhiShi, isYang, sanQiList) {
const hour = date.getHours();
const hourZhi = diZhi[Math.floor((hour + 1) / 2) % 12];
const hourElement = { '子': '水', '丑': '土', '寅': '木', '卯': '木', '辰': '土', '巳': '火', '午': '火', '未': '土', '申': '金', '酉': '金', '戌': '土', '亥': '水' }[hourZhi];
let report = `
🎴 【奇门遁甲盘】
📋 基本信息
日期:date.toLocaleDateString('zh-CN')
时辰:hourZhi时
遁局:'阴遁'('夏至→冬至')
值符:zhiFu.name
值使:zhiShi.name
📊 九宫排布
`;
// 按洛书顺序展示
const luoshu = [4, 9, 2, 3, 5, 1, 7, 8, 6]; // 巽4 离9 坤2 震3 中5 坎1 兑7 艮8 乾6
report += '\n 【东南】【南】【西南】\n';
report += ' ';
for (let i = 0; i < 9; i++) {
const row = Math.floor(i / 3);
const col = i % 3;
const palace = palaces.find(p => p.num === luoshu[i]);
if (palace) {
const starSymbol = palace.star ? palace.star.name.substring(1, 3) : ' ';
const doorSymbol = palace.door ? palace.door.name.charAt(0) : ' ';
report += `starSymboldoorSymbol `;
} else {
report += ' ';
}
if (col === 2 && row === 0) report += '\n 【东】【中】【西】\n ';
if (col === 2 && row === 1) report += '\n 【东北】【北】【西北】\n ';
}
// 详细宫位信息
report += '\n\n📍 各宫位详情\n';
report += '━━━━━━━━━━━━━━━━━━━━\n';
palaces.forEach(p => {
const guaInfo = ninePalaces.find(n => n.num === p.num) || {};
const fortune = judgeFortune(p, 0);
const fortuneSymbol = fortune.includes('吉') ? '✅' : fortune.includes('凶') ? '❌' : '⚠️';
report += `\n【p.num宫】p.direction || guaInfo.direction || '' guaInfo.gua || '' guaInfo.zhi || ''\n`;
if (p.star) report += ` 九星:p.star.name(p.star.element,p.star.trait)\n`;
if (p.door) report += ` 八门:p.door.name(p.door.trait)\n`;
report += ` 吉凶:fortuneSymbol fortune\n`;
});
// 三奇位置
if (sanQiList.length > 0) {
report += '\n✨ 三奇方位\n';
sanQiList.forEach(sq => {
report += ` sq.qi奇在sq.palace.directionsq.palace.num宫\n`;
});
}
// 最佳方位
const goodPalaces = palaces.filter(p => judgeFortune(p, 0).includes('吉'));
if (goodPalaces.length > 0) {
report += '\n🌟 最佳方位\n';
goodPalaces.forEach(p => {
report += ` p.direction(p.num宫)- p.star?.name || ''p.door?.name || ''\n`;
});
}
// 值符使跟随
report += `\n⚡ 值符zhiFu.name运行,值使zhiShi.name值事\n`;
report += `
💡 综合建议
'阴遁宜退,防守为主'
值符zhiFu.name为核心,zhiShi.name为动向
goodPalaces.length > 0 ? `吉利方位:${goodPalaces.map(p => p.direction).join('、')` : '宜静不宜动'}
`;
return report;
}
// 主入口
const args = process.argv.slice(2);
if (args[0] === '--help' || args[0] === '-h') {
console.log(`
奇门遁甲排盘
用法:
node qimen.js # 当前时间起局
node qimen.js 2026-03-24 # 指定日期(默认当前时辰)
node qimen.js 2026-03-24 15 # 指定日期和时辰
示例:
node qimen.js
node qimen.js 2026-03-24
node qimen.js 2026-03-24 15
`);
} else {
let date;
let hour;
if (args.length === 0) {
date = new Date();
} else if (args.length === 1) {
date = new Date(args[0]);
if (isNaN(date.getTime())) {
console.error('日期格式无效');
process.exit(1);
}
} else if (args.length >= 2) {
date = new Date(args[0]);
if (isNaN(date.getTime())) {
console.error('日期格式无效');
process.exit(1);
}
hour = parseInt(args[1]);
if (hour >= 0 && hour <= 23) {
date.setHours(hour);
}
}
const isYang = isYangDun(date);
const zhiFu = getZhiFu(date, isYang);
const zhiShi = getZhiShi(date, isYang);
const palaces = arrangePalaces(isYang, zhiFu);
const palacesWithStars = arrangeStars(palaces, isYang, zhiFu);
const palacesWithAll = arrangeDoors(palacesWithStars, isYang, zhiShi);
const sanQiList = findSanQi(palacesWithAll);
console.log(generateReport(date, palacesWithAll, zhiFu, zhiShi, isYang, sanQiList));
}
FILE:scripts/register.js
#!/usr/bin/env node
/**
* 快速注册脚本
* 用于命令行快速注册新用户
*/
const fs = require('fs');
const path = require('path');
const { getLunarMonth, isAfterLiChun } = require('./jieqi');
const { runFullAnalysis } = require('./bazi-analysis');
// 天干地支
const tianGan = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
const diZhi = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
const zodiacMap = { '子': '鼠', '丑': '牛', '寅': '虎', '卯': '兔', '辰': '龙', '巳': '蛇', '午': '马', '未': '羊', '申': '猴', '酉': '鸡', '戌': '狗', '亥': '猪' };
// ============================================================
// 真太阳时修正
// ============================================================
/** 主要城市经度表(东经度) */
const CITY_LONGITUDE = {
'上海': 121.47, '北京': 116.40, '广州': 113.26, '深圳': 114.06,
'杭州': 120.15, '南京': 118.80, '成都': 104.07, '重庆': 106.55,
'武汉': 114.30, '西安': 108.93, '沈阳': 123.43, '哈尔滨': 126.68,
'长春': 125.32, '大连': 121.62, '天津': 117.19, '济南': 117.00,
'青岛': 120.38, '郑州': 113.65, '石家庄': 114.51, '太原': 112.55,
'呼和浩特': 111.76, '乌鲁木齐': 87.62, '拉萨': 91.11, '昆明': 102.68,
'贵阳': 106.63, '南宁': 108.37, '海口': 110.33, '福州': 119.30,
'厦门': 118.08, '南昌': 115.89, '合肥': 117.27, '长沙': 112.98,
'兰州': 103.82, '西宁': 101.74, '银川': 106.23, '昭通': 103.72,
'曲靖': 103.80, '丽江': 100.22, '大理': 100.27, '玉溪': 102.55,
'保山': 99.16, '普洱': 100.97, '临沧': 100.08, '香港': 114.17,
'澳门': 113.55, '台北': 121.53, '苏州': 120.62, '无锡': 120.30,
'宁波': 121.55, '温州': 120.67, '济宁': 116.59, '烟台': 121.39,
'徐州': 117.18, '洛阳': 112.45, '唐山': 118.18, '秦皇岛': 119.60
};
/**
* 均时差(分钟):地球椭圆公转导致的时差,精度约 ±1 分钟
*/
function getEquationOfTime(date) {
const startOfYear = new Date(date.getFullYear(), 0, 1);
const doy = Math.floor((date - startOfYear) / 86400000) + 1;
const B = (2 * Math.PI * (doy - 1)) / 365;
return 9.87 * Math.sin(2 * B) - 7.53 * Math.cos(B) - 1.5 * Math.sin(B);
}
/**
* 计算真太阳时,返回修正后的日期和时间
* @param {string} birthDate YYYY-MM-DD
* @param {string} birthTime HH:MM
* @param {string} birthPlace 出生地(城市名)
* @returns {{ date: string, time: string, offsetMinutes: number, city: string|null }}
*/
function getTrueSolarTime(birthDate, birthTime, birthPlace) {
// 匹配城市经度(支持"上海市"、"上海浦东"等写法)
let longitude = null;
let matchedCity = null;
if (birthPlace) {
for (const [city, lng] of Object.entries(CITY_LONGITUDE)) {
if (birthPlace.includes(city)) {
longitude = lng;
matchedCity = city;
break;
}
}
}
if (longitude === null) {
// 未知城市,不做修正
return { date: birthDate, time: birthTime, offsetMinutes: 0, city: null };
}
const date = new Date(`birthDateT12:00:00+08:00`);
const geoOffset = (longitude - 120) * 4; // 地理时差(分钟)
const eot = getEquationOfTime(date); // 均时差(分钟)
const totalOffset = Math.round(geoOffset + eot); // 总修正量(分钟,四舍五入)
const [h, m] = birthTime.split(':').map(Number);
let totalMinutes = h * 60 + m + totalOffset;
// 处理跨日
let correctedDate = birthDate;
if (totalMinutes < 0) {
const d = new Date(`birthDateT12:00:00+08:00`);
d.setDate(d.getDate() - 1);
correctedDate = d.toISOString().slice(0, 10);
totalMinutes += 1440;
} else if (totalMinutes >= 1440) {
const d = new Date(`birthDateT12:00:00+08:00`);
d.setDate(d.getDate() + 1);
correctedDate = d.toISOString().slice(0, 10);
totalMinutes -= 1440;
}
const ch = String(Math.floor(totalMinutes / 60)).padStart(2, '0');
const cm = String(totalMinutes % 60).padStart(2, '0');
return {
date: correctedDate,
time: `ch:cm`,
offsetMinutes: totalOffset,
city: matchedCity,
geoOffsetMin: Math.round(geoOffset),
eotMin: Math.round(eot)
};
}
/**
* 内置八字计算(使用精确节气算法)
*/
function calculateBazi(birthDate, birthTime, gender, sect = 1) {
const [year, month, day] = birthDate.split('-').map(Number);
const [hour] = birthTime.split(':').map(Number);
// 年柱(以立春精确时刻为界)
const calcYear = isAfterLiChun(year, month, day) ? year : year - 1;
const yearGanIndex = ((calcYear - 4) % 10 + 10) % 10;
const yearZhiIndex = ((calcYear - 4) % 12 + 12) % 12;
// 月柱(以精确节气为界)
const lunarMonth = getLunarMonth(year, month, day);
const monthZhiIndex = (lunarMonth + 1) % 12;
const monthGanBases = [2, 4, 6, 8, 0]; // 甲己起丙,乙庚起戊,丙辛起庚,丁壬起壬,戊癸起甲
const monthGanIndex = (monthGanBases[yearGanIndex % 5] + lunarMonth - 1) % 10;
// 日柱(以2024-01-01甲子日为基准)
let calcDate = new Date(`birthDateT12:00:00`);
if (sect === 1 && hour === 23) calcDate.setDate(calcDate.getDate() + 1); // 晚子时算次日
const baseDate = new Date('2024-01-01T12:00:00');
const diffDays = Math.round((calcDate - baseDate) / (1000 * 60 * 60 * 24));
const dayGanIndex = (diffDays % 10 + 10) % 10; // 2024-01-01=甲子(甲=0)
const dayZhiIndex = (diffDays % 12 + 12) % 12; // 2024-01-01=甲子(子=0)
// 时柱(五鼠遁日)
const hourZhiIndex = (sect === 1 && hour === 23) ? 0 : Math.floor((hour + 1) / 2) % 12;
const hourGanBases = [0, 2, 4, 6, 8]; // 甲己起甲,乙庚起丙,丙辛起戊,丁壬起庚,戊癸起壬
const hourGanIndex = (hourGanBases[dayGanIndex % 5] + hourZhiIndex) % 10;
return {
year: tianGan[yearGanIndex] + diZhi[yearZhiIndex],
month: tianGan[monthGanIndex] + diZhi[monthZhiIndex],
day: tianGan[dayGanIndex] + diZhi[dayZhiIndex],
hour: tianGan[hourGanIndex] + diZhi[hourZhiIndex],
dayStem: tianGan[dayGanIndex],
zodiac: zodiacMap[diZhi[yearZhiIndex]]
};
}
/**
* 生成初始档案
*/
function createProfile(userId, name, gender, birthDate, birthTime, birthPlace, sect = 1) {
// 真太阳时修正
const solar = getTrueSolarTime(birthDate, birthTime, birthPlace);
// 用真太阳时计算八字
const bazi = calculateBazi(solar.date, solar.time, gender === '男' ? 1 : 0, sect);
const profile = {
userId,
name,
language: 'zh',
profile: {
birthDate,
birthTime,
birthPlace,
gender,
timezone: 'Asia/Shanghai',
trueSolarTime: solar.time,
trueSolarDate: solar.date,
solarCorrectionMin: solar.offsetMinutes,
solarCorrectionCity: solar.city
},
bazi: {
year: bazi?.year || '',
month: bazi?.month || '',
day: bazi?.day || '',
hour: bazi?.hour || '',
dayStem: bazi?.dayStem || '',
zodiac: bazi?.zodiac || '',
sect: sect === 1 ? '晚子时' : '早子时',
source: 'verified',
analysis: bazi ? runFullAnalysis(bazi) : null
},
ziwei: {
mingGong: '',
mingZhu: '',
source: 'pending'
},
family: {
spouse: {
name: '配偶',
profile: {
birthDate: '待录入',
birthTime: '待录入',
birthPlace: '',
gender: gender === '男' ? '女' : '男',
lunarBirth: ''
},
bazi: {
year: '',
month: '',
day: '',
hour: '',
source: 'pending'
}
},
father: {
name: '父亲',
profile: {
birthDate: '待录入',
birthTime: '待录入',
birthPlace: '',
gender: '男'
},
bazi: {
year: '',
month: '',
day: '',
hour: '',
source: 'pending'
}
},
mother: {
name: '母亲',
profile: {
birthDate: '待录入',
birthTime: '待录入',
birthPlace: '',
gender: '女'
},
bazi: {
year: '',
month: '',
day: '',
hour: '',
source: 'pending'
}
},
children: []
},
preferences: {
pushMorning: true,
pushEvening: false,
morningTime: '07:00',
eveningTime: '20:00',
channels: ['telegram'],
focusAreas: ['事业', '财运', '健康'],
riskTolerance: '中等'
},
settings: {
defaultSect: sect,
lunarCalendar: true,
notifications: {
dailyFortune: true,
riskAlert: true,
weeklySummary: false
}
},
createdAt: new Date().toISOString().split('T')[0],
updatedAt: new Date().toISOString().split('T')[0]
};
return profile;
}
/**
* 保存档案
*/
function saveProfile(userId, profile) {
const dir = path.join(__dirname, '../data/profiles');
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const filePath = path.join(dir, `userId.json`);
fs.writeFileSync(filePath, JSON.stringify(profile, null, 2), 'utf8');
return filePath;
}
// 主入口
const args = process.argv.slice(2);
if (args.length < 5) {
console.log(`
📝 快速注册用户
用法:
node register.js <userId> <姓名> <性别> <出生日期> <出生时间> [出生地点] [子时]
参数:
userId - 用户ID(telegram id或其他唯一标识)
姓名 - 用户姓名
性别 - 男 或 女
出生日期 - YYYY-MM-DD
出生时间 - HH:MM(24小时制)
出生地点 - 省市(可选,默认上海)
子时 - 1=晚子时(23点后算次日),2=早子时(可选,默认1)
示例:
node register.js 123456 张三 男 1990-05-15 14:30 上海
node register.js 123456 李四 女 1995-08-20 23:45 北京 1
说明:
子时(23:00-01:00)出生需要特别注意:
- 晚子时(1): 23:00后算次日日柱
- 早子时(2): 23:00后算当日日柱
`);
process.exit(1);
}
const userId = args[0];
const name = args[1];
const gender = args[2];
const birthDate = args[3];
const birthTime = args[4];
const birthPlace = args[5] || '上海';
const sect = parseInt(args[6] || '1');
// 验证
if (!['男', '女'].includes(gender)) {
console.error('性别必须是"男"或"女"');
process.exit(1);
}
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
if (!dateRegex.test(birthDate)) {
console.error('出生日期格式错误,请使用 YYYY-MM-DD');
process.exit(1);
}
const timeRegex = /^\d{2}:\d{2}$/;
if (!timeRegex.test(birthTime)) {
console.error('出生时间格式错误,请使用 HH:MM');
process.exit(1);
}
console.log('\n📝 正在注册用户...\n');
console.log(` 用户ID: userId`);
console.log(` 姓名: name`);
console.log(` 性别: gender`);
console.log(` 出生: birthDate birthTime`);
console.log(` 地点: birthPlace`);
console.log(` 子时: '早子时(23点后算当日)'`);
console.log('');
// 创建档案
const profile = createProfile(userId, name, gender, birthDate, birthTime, birthPlace, sect);
// 保存
const filePath = saveProfile(userId, profile);
console.log('✅ 注册成功!\n');
// 真太阳时提示
const sc = profile.profile.solarCorrectionMin;
if (sc !== 0 && profile.profile.solarCorrectionCity) {
const sign = sc > 0 ? '+' : '';
console.log(`🌞 真太阳时修正(profile.profile.solarCorrectionCity)`);
console.log(` 北京时间: birthDate birthTime`);
console.log(` 真太阳时: profile.profile.trueSolarDate profile.profile.trueSolarTime (signsc分钟)`);
console.log(` 八字时柱以真太阳时计算`);
console.log('');
} else if (!profile.profile.solarCorrectionCity) {
console.log(`🌞 真太阳时:未识别城市"birthPlace",以北京时间计算(如需精确请使用主要城市名)`);
console.log('');
}
console.log('📊 八字信息');
console.log(` 年柱: profile.bazi.year`);
console.log(` 月柱: profile.bazi.month`);
console.log(` 日柱: profile.bazi.day`);
console.log(` 时柱: profile.bazi.hour`);
console.log(` 日主: profile.bazi.dayStem (profile.bazi.zodiac)`);
console.log('');
console.log(`📁 档案已保存: filePath`);
console.log('');
// 自动开启推送(如果指定了 --push 参数)
const pushIdx = args.indexOf('--push');
if (pushIdx !== -1) {
const channel = args[args.indexOf('--channel') + 1] || 'telegram';
const morning = args[args.indexOf('--morning') + 1] || '08:00';
const evening = args[args.indexOf('--evening') + 1] || '20:00';
console.log('⏳ 正在开启每日推送...');
try {
const { enablePush } = require('./push-toggle');
enablePush(userId, { morning, evening, channel });
} catch (e) {
console.error('推送开启失败:', e.message);
}
} else {
console.log('💡 提示:运行以下命令开启每日运程推送:');
console.log(` node scripts/push-toggle.js on userId`);
console.log('');
}
module.exports = { createProfile, saveProfile };
FILE:scripts/zhuanshi.js
#!/usr/bin/env node
/**
* 择吉选日脚本
* 帮助用户选择:黄道吉日、开业、搬家、签约、订婚、装修、出行等好日子
*
* 基于:紫微斗数、奇门遁甲、黄历建除十二神、彭祖百忌
*/
// ============================================================
// 内置农历/黄历算法(替代 lunar-typescript,无外部依赖)
// ============================================================
const _GAN = ['甲','乙','丙','丁','戊','己','庚','辛','壬','癸'];
const _ZHI = ['子','丑','寅','卯','辰','巳','午','未','申','酉','戌','亥'];
const _CHONG = { '子':'午','丑':'未','寅':'申','卯':'酉','辰':'戌','巳':'亥','午':'子','未':'丑','申':'寅','酉':'卯','戌':'辰','亥':'巳' };
const _JIAN_CHU = ['建','除','满','平','定','执','破','危','成','收','开','闭'];
const _JIAN_CHU_YI_JI = {
'建': { yi: ['出行','上任','祭祀','求财'], ji: ['嫁娶','动土','破土','安葬'] },
'除': { yi: ['扫除','解除','移徙','沐浴'], ji: ['嫁娶','破土','安葬','入殓'] },
'满': { yi: ['嫁娶','开业','纳财','入宅'], ji: ['出行','动土','破土','诉讼'] },
'平': { yi: ['出行','移徙','求医','上任'], ji: ['嫁娶','安葬','破土'] },
'定': { yi: ['嫁娶','开业','签约','求财'], ji: ['出行','诉讼','动土'] },
'执': { yi: ['祭祀','纳财','捕猎','捉贼'], ji: ['开业','嫁娶','移徙','出行'] },
'破': { yi: [], ji: ['开业','嫁娶','出行','移徙','动土','签约'] },
'危': { yi: ['祭祀'], ji: ['出行','登高','嫁娶','开业'] },
'成': { yi: ['开业','嫁娶','移徙','上任','出行'], ji: ['诉讼','破土'] },
'收': { yi: ['纳财','收获','祭祀'], ji: ['出行','嫁娶','动土','开业'] },
'开': { yi: ['开业','嫁娶','出行','求财','移徙'], ji: ['入殓','安葬','破土'] },
'闭': { yi: ['入殓','安葬','封穴'], ji: ['开业','嫁娶','出行','动土'] }
};
const _PENG_ZU_GAN = {
'甲':'甲不开仓财物耗散','乙':'乙不栽植千株不长','丙':'丙不修灶必见灾殃',
'丁':'丁不剃头头必生疮','戊':'戊不受田田主不祥','己':'己不破券二比并亡',
'庚':'庚不经络织机虚张','辛':'辛不合酱主人不尝','壬':'壬不决水更难提防',
'癸':'癸不词讼理弱敌强'
};
function _getDayGanZhi(date) {
const base = new Date('2024-01-01T12:00:00');
const diff = Math.round((date - base) / 86400000);
return _GAN[((diff % 10) + 10) % 10] + _ZHI[((diff % 12) + 12) % 12];
}
function _getZhiXing(date) {
const m = date.getMonth() + 1; // solar month 1-12
const monthZhiIdx = m % 12; // 1→丑(1), 2→寅(2)…12→子(0)
const gz = _getDayGanZhi(date);
const dayZhiIdx = _ZHI.indexOf(gz[1]);
return _JIAN_CHU[((dayZhiIdx - monthZhiIdx) + 12) % 12];
}
/** 模拟 lunar-typescript Lunar 对象 */
function createLunarDate(date) {
const gz = _getDayGanZhi(date);
const dayGan = gz[0];
const dayZhi = gz[1];
const zhiXing = _getZhiXing(date);
const chongZhi = _CHONG[dayZhi] || '';
const yiji = _JIAN_CHU_YI_JI[zhiXing] || { yi: [], ji: [] };
return {
getDayInGanZhi: () => gz,
getDayGan: () => dayGan,
getDayZhi: () => dayZhi,
getZhiXing: () => zhiXing,
getDayYi: () => yiji.yi,
getDayJi: () => yiji.ji,
getPengZuGan: () => _PENG_ZU_GAN[dayGan] || '',
getChong: () => chongZhi,
getChongDesc: () => `冲chongZhi`
};
}
// ============================================
// 常量定义
// ============================================
// 天干地支
const TIAN_GAN = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
const DI_ZHI = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
// 地支对应五行
const ZHI_ELEMENT = {
'子': '水', '丑': '土', '寅': '木', '卯': '木',
'辰': '土', '巳': '火', '午': '火', '未': '土',
'申': '金', '酉': '金', '戌': '土', '亥': '水'
};
// 天干对应五行
const GAN_ELEMENT = {
'甲': '木', '乙': '木', '丙': '火', '丁': '火',
'戊': '土', '己': '土', '庚': '金', '辛': '金',
'壬': '水', '癸': '水'
};
// 五行颜色
const ELEMENT_COLOR = {
'木': { color: '绿色、青色', direction: '东方' },
'火': { color: '红色、紫色', direction: '南方' },
'土': { color: '黄色、棕色', direction: '中央' },
'金': { color: '白色、金色', direction: '西方' },
'水': { color: '黑色、蓝色', direction: '北方' }
};
// 建除十二神序列
const JIAN_CHU = ['建', '除', '满', '平', '定', '执', '破', '危', '成', '收', '开', '闭'];
// 奇门遁甲九星
const NINE_STARS = [
{ name: '天蓬', element: '水', trait: '凶星', position: 1, good: false },
{ name: '天任', element: '土', trait: '凶星', position: 8, good: false },
{ name: '天冲', element: '木', trait: '吉星', position: 3, good: true },
{ name: '天辅', element: '木', trait: '吉星', position: 4, good: true },
{ name: '天英', element: '火', trait: '凶星', position: 9, good: false },
{ name: '天芮', element: '土', trait: '凶星', position: 2, good: false },
{ name: '天柱', element: '金', trait: '凶星', position: 7, good: false },
{ name: '天心', element: '金', trait: '吉星', position: 6, good: true },
{ name: '天禽', element: '土', trait: '大吉', position: 5, good: true }
];
// 奇门遁甲八门
const EIGHT_DOORS = [
{ name: '休门', element: '水', trait: '休息、平稳', good: true },
{ name: '生门', element: '土', trait: '生长、财运', good: true },
{ name: '伤门', element: '木', trait: '受伤、变动', good: false },
{ name: '杜门', element: '木', trait: '阻碍、保密', good: false },
{ name: '景门', element: '火', trait: '文化、虚假', good: false },
{ name: '死门', element: '土', trait: '死亡、凶险', good: false },
{ name: '惊门', element: '金', trait: '惊恐、口舌', good: false },
{ name: '开门', element: '金', trait: '开创、顺利', good: true }
];
// 时辰信息
const HOUR_INFO = {
'子': { range: '23-01', element: '水', tip: '整理思考' },
'丑': { range: '01-03', element: '土', tip: '睡眠休息' },
'寅': { range: '03-05', element: '木', tip: '计划准备' },
'卯': { range: '05-07', element: '木', tip: '晨间运动' },
'辰': { range: '07-09', element: '土', tip: '贵人运佳' },
'巳': { range: '09-11', element: '火', tip: '事业高峰' },
'午': { range: '11-13', element: '火', tip: '财运旺盛' },
'未': { range: '13-15', element: '土', tip: '平稳行事' },
'申': { range: '15-17', element: '金', tip: '财运佳' },
'酉': { range: '17-19', element: '金', tip: '收整理' },
'戌': { range: '19-21', element: '土', tip: '社交应酬' },
'亥': { range: '21-23', element: '水', tip: '学习思考' }
};
// 活动类型与宜忌配合
const ACTIVITIES = {
'开业': { good: ['开', '满', '定', '成', '收'], bad: ['闭', '破', '危', '建'] },
'搬家': { good: ['满', '定', '平', '成', '收'], bad: ['破', '危', '闭', '建'] },
'签约': { good: ['开', '定', '成', '满', '收'], bad: ['闭', '破', '危', '建'] },
'订婚': { good: ['合', '定', '满', '成', '开'], bad: ['冲', '刑', '破', '危'] },
'装修': { good: ['平', '满', '定', '成', '收'], bad: ['破', '冲', '危', '闭'] },
'出行': { good: ['开', '定', '成', '满'], bad: ['闭', '破', '危', '建'] },
'结婚': { good: ['合', '定', '满', '成', '开'], bad: ['冲', '刑', '破', '危'] },
'祭祀': { good: ['建', '除', '满', '平'], bad: ['破', '闭'] },
'求财': { good: ['开', '生', '满', '成', '收'], bad: ['闭', '破', '危'] },
'上任': { good: ['开', '定', '成', '满'], bad: ['破', '危', '闭'] }
};
// ============================================
// 核心算法
// ============================================
/**
* 获取指定月份的所有日期
*/
function getDatesInMonth(year, month) {
const dates = [];
const daysInMonth = new Date(year, month, 0).getDate();
for (let d = 1; d <= daysInMonth; d++) {
dates.push(new Date(year, month - 1, d));
}
return dates;
}
/**
* 计算某日的奇门遁甲信息
*/
function calculateQimen(date) {
const isYang = isYangDun(date);
const zhiFu = getZhiFuStar(date, isYang);
const zhiShi = getZhiShiDoor(date, isYang);
return {
isYang,
zhiFu,
zhiShi,
goodStars: NINE_STARS.filter(s => s.good).map(s => s.name),
goodDoors: EIGHT_DOORS.filter(d => d.good).map(d => d.name)
};
}
/**
* 判断阴遁还是阳遁
*/
function isYangDun(date = new Date()) {
const month = date.getMonth() + 1;
const day = date.getDate();
const yearDay = date.getMonth() * 30 + day;
const summerSolstice = 5 * 30 + 21;
const winterSolstice = 11 * 30 + 22;
return yearDay < summerSolstice || yearDay > winterSolstice;
}
/**
* 计算值符星
*/
function getZhiFuStar(date, isYang) {
const baseDate = new Date('2024-01-01T12:00:00');
const diffDays = Math.round((date - baseDate) / (1000 * 60 * 60 * 24));
const hour = date.getHours();
const shichen = Math.floor((hour + 1) / 2) % 12;
const idx = ((diffDays * 12 + shichen) % 9 + 9) % 9;
return isYang ? NINE_STARS[idx] : NINE_STARS[(9 - idx) % 9];
}
/**
* 计算值使门
*/
function getZhiShiDoor(date, isYang) {
const baseDate = new Date('2024-01-01T12:00:00');
const diffDays = Math.round((date - baseDate) / (1000 * 60 * 60 * 24));
const hour = date.getHours();
const shichen = Math.floor((hour + 1) / 2) % 12;
const idx = ((diffDays * 12 + shichen) % 8 + 8) % 8;
return isYang ? EIGHT_DOORS[idx] : EIGHT_DOORS[(8 - idx) % 8];
}
/**
* 获取某日吉时(基于五行)
*/
function getLuckyHoursForDate(date) {
const lunarDate = createLunarDate(date);
const ganZhi = lunarDate.getDayInGanZhi();
const dayZhi = ganZhi[1];
const dayElement = ZHI_ELEMENT[dayZhi] || '土';
// 找出与日主五行相同或相生的时辰
const luckyHours = [];
for (const [zhi, elem] of Object.entries(ZHI_ELEMENT)) {
if (elem === dayElement || (isSupportingElement(dayElement, elem))) {
if (HOUR_INFO[zhi]) {
luckyHours.push({ zhi, ...HOUR_INFO[zhi] });
}
}
}
return luckyHours.slice(0, 4);
}
/**
* 判断是否相生
*/
function isSupportingElement(main, support) {
const supportMap = { '木': '火', '火': '土', '土': '金', '金': '水', '水': '木' };
return supportMap[main] === support;
}
/**
* 评分日期
*/
function scoreDate(date, activityType, userDayStem = null) {
const lunarDate = createLunarDate(date);
const activity = ACTIVITIES[activityType] || ACTIVITIES['开业'];
let score = 50; // 基础分
const factors = [];
// 1. 建除十二神评分
const zhiXing = lunarDate.getZhiXing();
const jianChuIndex = JIAN_CHU.indexOf(zhiXing);
if (activity.good.includes(zhiXing)) {
score += 20;
factors.push({ name: '建除', value: `zhiXing日`, bonus: 20, good: true });
} else if (activity.bad.includes(zhiXing)) {
score -= 25;
factors.push({ name: '建除', value: `zhiXing日`, bonus: -25, good: false });
} else {
score += 5;
factors.push({ name: '建除', value: `zhiXing日`, bonus: 5, good: null });
}
// 2. 宜忌配合
const dayYi = lunarDate.getDayYi() || [];
const dayJi = lunarDate.getDayJi() || [];
const yiMatch = activity.good.some(a => dayYi.some(y => y.includes(a)));
const jiMatch = activity.bad.some(a => dayJi.some(j => j.includes(a)));
if (yiMatch) score += 15;
if (jiMatch) score -= 15;
factors.push({ name: '宜忌', value: yiMatch ? '配合较好' : '一般', bonus: yiMatch ? 15 : (jiMatch ? -15 : 0), good: yiMatch ? true : (jiMatch ? false : null) });
// 3. 彭祖百忌(检查是否与日干相冲)
const pengZuGan = lunarDate.getPengZuGan();
const dayGan = lunarDate.getDayGan();
if (pengZuGan && pengZuGan.includes(dayGan)) {
score -= 5;
}
// 4. 日冲评分
const chong = lunarDate.getChong();
const dayZhi = lunarDate.getDayZhi();
if (userDayStem) {
const userElement = GAN_ELEMENT[userDayStem] || '';
const chongElement = ZHI_ELEMENT[chong] || '';
if (isSameElement(userElement, chongElement)) {
score -= 20;
factors.push({ name: '日冲', value: `chong(lunarDate.getChongDesc())`, bonus: -20, good: false });
} else {
factors.push({ name: '日冲', value: `chong(lunarDate.getChongDesc())`, bonus: 0, good: null });
}
} else {
factors.push({ name: '日冲', value: `chong(lunarDate.getChongDesc())`, bonus: 0, good: null });
}
// 5. 奇门遁甲吉凶
const qimen = calculateQimen(date);
if (qimen.zhiFu.good) {
score += 10;
factors.push({ name: '值符', value: qimen.zhiFu.name, bonus: 10, good: true });
} else {
factors.push({ name: '值符', value: qimen.zhiFu.name, bonus: 0, good: false });
}
if (qimen.zhiShi.good) {
score += 10;
factors.push({ name: '值使', value: qimen.zhiShi.name, bonus: 10, good: true });
} else {
factors.push({ name: '值使', value: qimen.zhiShi.name, bonus: 0, good: false });
}
// 6. 五行配合(如果提供了用户日干)
if (userDayStem) {
const userElement = GAN_ELEMENT[userDayStem];
const dayElement = GAN_ELEMENT[dayGan];
if (isSupportingElement(userElement, dayElement)) {
score += 15;
factors.push({ name: '五行', value: `日主dayElement生助我userElement`, bonus: 15, good: true });
} else if (isSupportingElement(dayElement, userElement)) {
score += 10;
factors.push({ name: '五行', value: `我userElement生日主dayElement`, bonus: 10, good: true });
} else if (userElement === dayElement) {
score += 5;
factors.push({ name: '五行', value: `比和dayElement`, bonus: 5, good: true });
} else {
score -= 10;
factors.push({ name: '五行', value: `相克`, bonus: -10, good: false });
}
}
// 限制分数范围
score = Math.max(0, Math.min(100, score));
return {
score,
factors,
zhiXing,
dayGanZhi: lunarDate.getDayInGanZhi(),
dayGan,
dayZhi,
pengZuGan: lunarDate.getPengZuGan(),
chong: lunarDate.getChong(),
chongDesc: lunarDate.getChongDesc(),
dayYi,
dayJi,
qimen,
luckyHours: getLuckyHoursForDate(date)
};
}
/**
* 判断是否同元素
*/
function isSameElement(elem1, elem2) {
return elem1 && elem2 && elem1 === elem2;
}
/**
* 获取八字日主
*/
function getDayStemFromBazi(bazi) {
if (!bazi) return null;
// bazi 格式: "庚午 辛巳 庚辰 辛巳" (年 月 日 时)
const parts = bazi.split(/\s+/);
if (parts.length >= 3) {
const dayGanZhi = parts[2];
return dayGanZhi[0]; // 取天干
}
return null;
}
/**
* 格式化星级
*/
function formatStars(score) {
const stars = Math.round(score / 20);
return '⭐'.repeat(stars) + '☆'.repeat(5 - stars);
}
// ============================================
// 报告生成
// ============================================
/**
* 生成择日报告
*/
function generateReport(year, month, activityType, userBazi = null) {
const userDayStem = userBazi ? getDayStemFromBazi(userBazi) : null;
const dates = getDatesInMonth(year, month);
const dayMap = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const results = dates.map(date => {
const scoreResult = scoreDate(date, activityType, userDayStem);
return {
date,
dateStr: `month月date.getDate()日`,
weekDay: dayMap[date.getDay()],
...scoreResult
};
});
// 排序:按分数降序
results.sort((a, b) => b.score - a.score);
// 分类
const avoidDates = results.filter(r => r.score < 30);
const goodDates = results.filter(r => r.score >= 70).slice(0, 5);
const mediumDates = results.filter(r => r.score >= 50 && r.score < 70).slice(0, 5);
// 生成报告
let report = `
🎯 year年month月 最佳吉日(activityType)
━━━━━━━━━━━━━━━━━━━━
`;
if (goodDates.length > 0) {
const best = goodDates[0];
report += `
🏆 综合最优(activityType)
best.dateStr(best.weekDay)formatStars(best.score) best.score分
吉时:best.luckyHours.map(h => `${h.range点(h.zhi时)`).join('、')}
干支:best.dayGanZhi
建除:best.zhiXing日
冲:best.chongbest.chongDesc
彭祖:best.pengZuGan
宜:best.dayYi.slice(0, 4).join('、')
忌:best.dayJi.slice(0, 3).join('、')
`;
// 奇门信息
report += `
【奇门遁甲】
遁局:'阴遁'
值符:best.qimen.zhiFu.name(best.qimen.zhiFu.trait)
值使:best.qimen.zhiShi.name(best.qimen.zhiShi.trait)
`;
}
if (mediumDates.length > 0) {
report += `
━━━━━━━━━━━━━━━━━━━━
📅 其他推荐
`;
mediumDates.forEach(d => {
report += `
d.dateStr(d.weekDay)formatStars(d.score) d.score分
干支:d.dayGanZhi | 建除:d.zhiXing日 | 冲:d.chongd.chongDesc
`;
});
}
if (avoidDates.length > 0) {
report += `
━━━━━━━━━━━━━━━━━━━━
⚠️ 避免日期
`;
avoidDates.slice(0, 3).forEach(d => {
report += `
d.dateStr(d.weekDay)❌ d.score分
原因:d.factors.filter(f => f.bonus < 0).map(f => `${f.namef.value`).join('、')}
冲:d.chongd.chongDesc
`;
});
}
report += `
━━━━━━━━━━━━━━━━━━━━
💡 评分说明
• 分数范围:0-100分
• ⭐⭐⭐⭐⭐ = 80-100分(极佳)
• ⭐⭐⭐⭐ = 60-79分(良好)
• ⭐⭐⭐ = 40-59分(一般)
• ⭐⭐ = 20-39分(欠佳)
• ⭐ = 0-19分(避免)
评分因素:建除十二神(±25)、宜忌配合(±15)、
日冲(±20)、值符值使(±20)、五行生克(±15)
`;
if (userBazi) {
report += `
用户日主:userDayStem(GAN_ELEMENT[userDayStem])
`;
}
return report;
}
/**
* 查找最佳日期
*/
function findBestDate(year, month, activityType, userBazi = null) {
const userDayStem = userBazi ? getDayStemFromBazi(userBazi) : null;
const dates = getDatesInMonth(year, month);
let bestDate = null;
let bestScore = -1;
for (const date of dates) {
const result = scoreDate(date, activityType, userDayStem);
if (result.score > bestScore) {
bestScore = result.score;
bestDate = { date, ...result };
}
}
return bestDate;
}
// ============================================
// 主入口
// ============================================
const args = process.argv.slice(2);
function showHelp() {
console.log(`
📅 择吉选日脚本
用法:
node zhuanshi.js <YYYY-MM> <活动类型> [用户八字]
node zhuanshi.js best <YYYY-MM> <活动类型> [用户八字]
活动类型:
开业、搬家、签约、订婚、装修、出行、结婚、祭祀、求财、上任
示例:
node zhuanshi.js 2026-04 开业
node zhuanshi.js 2026-04 签约 "庚午 辛巳 庚辰 辛巳"
node zhuanshi.js best 2026-04 搬家
`);
}
if (args[0] === '--help' || args[0] === '-h') {
showHelp();
process.exit(0);
}
// 解析参数
if (args.length < 2) {
console.error('参数不足');
showHelp();
process.exit(1);
}
let year, month, activityType, userBazi;
let findBest = false;
if (args[0] === 'best') {
findBest = true;
const dateMatch = args[1].match(/^(\d{4})-(\d{2})$/);
if (!dateMatch) {
console.error('日期格式错误,请使用 YYYY-MM');
process.exit(1);
}
year = parseInt(dateMatch[1]);
month = parseInt(dateMatch[2]);
activityType = args[2] || '开业';
userBazi = args[3] || null;
} else {
const dateMatch = args[0].match(/^(\d{4})-(\d{2})$/);
if (!dateMatch) {
console.error('日期格式错误,请使用 YYYY-MM');
process.exit(1);
}
year = parseInt(dateMatch[1]);
month = parseInt(dateMatch[2]);
activityType = args[1] || '开业';
userBazi = args[2] || null;
}
if (month < 1 || month > 12) {
console.error('月份无效,请使用 1-12');
process.exit(1);
}
if (!Object.keys(ACTIVITIES).includes(activityType)) {
console.warn(`警告:未知的活动类型 "activityType",使用默认值"开业"`);
activityType = '开业';
}
console.log(`
╭──────────────────────────────────────╮
│ 🔮 择吉选日分析中... │
╰──────────────────────────────────────╯
`);
console.log(`📋 分析条件`);
console.log(` 日期:year年month月`);
console.log(` 活动:activityType`);
if (userBazi) console.log(` 用户八字:userBazi`);
console.log('');
if (findBest) {
const best = findBestDate(year, month, activityType, userBazi);
const dayMap = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const stars = formatStars(best.score);
console.log(`
🏆 year年month月最佳吉日
best.date.getMonth() + 1月best.date.getDate()日(dayMap[best.date.getDay()])stars best.score分
📊 详细信息
干支:best.dayGanZhi
建除:best.zhiXing日
冲:best.chongbest.chongDesc
彭祖:best.pengZuGan
⏰ 吉时
best.luckyHours.map(h => ` • ${h.zhi时(h.range点)- h.tip`).join('\n')}
✅ 宜
best.dayYi.slice(0, 5).join('、')
❌ 忌
best.dayJi.slice(0, 4).join('、')
【奇门遁甲】
遁局:'阴遁'
值符:best.qimen.zhiFu.name(best.qimen.zhiFu.element,best.qimen.zhiFu.trait)
值使:best.qimen.zhiShi.name(best.qimen.zhiShi.element,best.qimen.zhiShi.trait)
`);
} else {
console.log(generateReport(year, month, activityType, userBazi));
}
FILE:scripts/ziwei.js
#!/usr/bin/env node
/**
* 紫微斗数命盘分析 v4 - 知识库驱动格局 + 大运流年 + 八字用神
* 使用 iztro 库(中文紫微斗数)
*/
const fs = require('fs');
const path = require('path');
const { astro } = require('iztro');
// ============================================================
// 知识库格局匹配系统
// ============================================================
const KNOWLEDGE_DIR = process.env.OPENCLAW_KNOWLEDGE_DIR
|| (process.env.HOME ? path.join(process.env.HOME, '.openclaw/workspace/knowledge') : '');
/**
* 解析知识库中的格局文件,构建模式检测规则
*/
function buildPatternRules() {
if (!fs.existsSync(KNOWLEDGE_DIR)) return [];
const files = fs.readdirSync(KNOWLEDGE_DIR).filter(f => f.endsWith('.md'));
const rules = [];
const skipNames = [
'倪海厦', '渊海子平', '滴天髓', '命理交叉', '排盘不准',
'命运解读', '算卦', 'Jia-八字', '紫微斗数格局', '四化表',
'紫微斗数基本术语', '紫微斗数与奇门遁甲', '星平会海',
'渊海子平-学习', '滴天髓-子平真诠', '命理交叉验证系统'
];
for (const file of files) {
if (skipNames.some(n => file.includes(n))) continue;
const filePath = path.join(KNOWLEDGE_DIR, file);
const content = fs.readFileSync(filePath, 'utf-8');
const name = file.replace('.md', '');
const rule = parsePatternFile(name, content);
if (rule) rules.push(rule);
}
return rules;
}
/**
* 解析单个格局文件,提取检测条件
*/
function parsePatternFile(name, content) {
// 提取吉星加会条件
const luckyStars = [];
if (content.includes('禄存')) luckyStars.push('禄存');
if (content.includes('科权禄') || content.includes('化禄') && content.includes('化权') && content.includes('化科')) {
luckyStars.push('科', '权', '科权禄');
}
if (content.includes('左右')) { luckyStars.push('左辅', '右弼'); }
if (content.includes('昌曲') || content.includes('文昌') || content.includes('文曲')) {
luckyStars.push('文昌', '文曲');
}
if (content.includes('魁钺') || content.includes('天魁') || content.includes('天钺')) {
luckyStars.push('天魁', '天钺');
}
// 判断格局等级
let level = '平';
if (content.includes('大富大贵') || content.includes('极美') || content.includes('极贵')) level = '贵';
else if (content.includes('富贵') || content.includes('大富') || content.includes('大贵')) level = '富';
else if (content.includes('凶') || content.includes('刑') || content.includes('破格')) level = '凶';
else if (content.includes('平常') || content.includes('普通')) level = '平';
// 提取星曜条件
const mainStars = [];
const starMatches = content.match(/[\u4e00-\u9fa5]{2,4}(?:星|门|府|相|杀|狼|军|曲|昌|梁|机|阴|阳|同|贞|府|微)/g);
if (starMatches) {
const uniqueStars = [...new Set(starMatches.map(s => s.slice(0, 2)))];
const knownStars = ['紫微','天机','太阳','武曲','天同','廉贞','天府','贪狼','巨门','太阴','天相','天梁','七杀','破军',
'文昌','文曲','左辅','右弼','天魁','天钺','禄存','天马','擎羊','陀罗','火星','铃星','地空','地劫','解神','天虚','天喜','红鸾'];
for (const s of uniqueStars) {
if (knownStars.includes(s)) mainStars.push(s);
}
}
// 提取宫位条件
const branches = [];
const branchKeywords = ['子','午','寅','申','卯','酉','辰','戌','丑','未','巳','亥','寅申','子午','辰戌','丑未','卯酉','巳亥'];
for (const kw of branchKeywords) {
if (content.includes(kw) && kw.length >= 1) {
if (kw.length === 1) branches.push(kw);
else branches.push(kw);
}
}
// 提取年干条件
const yearStems = [];
if (content.includes('甲年') || content.includes('甲年生')) yearStems.push('甲');
if (content.includes('乙年') || content.includes('乙年生')) yearStems.push('乙');
if (content.includes('丙年') || content.includes('丙年生')) yearStems.push('丙');
if (content.includes('丁年') || content.includes('丁年生')) yearStems.push('丁');
if (content.includes('戊年') || content.includes('戊年生')) yearStems.push('戊');
if (content.includes('己年') || content.includes('己年生')) yearStems.push('己');
if (content.includes('庚年') || content.includes('庚年生')) yearStems.push('庚');
if (content.includes('辛年') || content.includes('辛年生')) yearStems.push('辛');
if (content.includes('壬年') || content.includes('壬年生')) yearStems.push('壬');
if (content.includes('癸年') || content.includes('癸年生')) yearStems.push('癸');
// 提取四化条件
const mutagens = [];
if (content.includes('化禄')) mutagens.push('禄');
if (content.includes('化权')) mutagens.push('权');
if (content.includes('化科')) mutagens.push('科');
if (content.includes('化忌')) mutagens.push('忌');
// 提取三方四正条件
const sanfang = content.includes('三方四正') || content.includes('三合');
// 提取夹的条件(邻宫)
const adjacent = content.includes('夹命') || content.includes('相夹');
// 提取凶格标志
const isJiong = content.includes('凶格') || content.includes('刑') || content.includes('破格');
// 提取描述
let desc = '';
const lines = content.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('*') && !trimmed.startsWith('---') && trimmed.length > 5 && trimmed.length < 100) {
if (trimmed.includes('大富大贵') || trimmed.includes('福寿') || trimmed.includes('贵气') ||
trimmed.includes('少年') || trimmed.includes('劳碌') || trimmed.includes('平常') ||
trimmed.includes('大富') || trimmed.includes('大贵') || trimmed.includes('先贫后富')) {
desc = trimmed.replace(/[#*]/g, '').trim();
break;
}
}
}
if (!desc) {
for (const line of lines) {
const trimmed = line.replace(/[#*]/g, '').trim();
if (trimmed.length > 5 && trimmed.length < 80 && !trimmed.startsWith('-') && !trimmed.startsWith('|')) {
desc = trimmed;
break;
}
}
}
// 判断宫位条件类型
let palaceCondition = 'ming'; // 默认命宫
if (content.includes('命宫三方') || content.includes('三方') || sanfang) palaceCondition = 'sanfang';
if (content.includes('命宫') && content.includes('邻宫') && adjacent) palaceCondition = 'adjacent';
if (content.includes('命身宫入丑未') || content.includes('命身宫')) palaceCondition = 'mingBody';
return {
name,
level,
stars: mainStars,
branches,
yearStems,
mutagens,
luckyStars,
palaceCondition, // ming | sanfang | adjacent | mingBody
isJiong,
desc: desc.substring(0, 80)
};
}
/**
* 使用知识库规则检测命盘格局
*/
function checkPatternsFromKnowledge(palaces, mingIdx, transforms, yearStem) {
const rules = buildPatternRules();
const results = [];
// 构建命宫、三方四正、邻宫数据
const mingPalace = palaces[mingIdx];
const mingBranch = mingPalace?.earthlyBranch || '';
const mingStars = mingPalace?.majorStars?.map(s => s.name) || [];
const mingMinor = mingPalace?.minorStars?.map(s => s.name) || [];
const mingAdj = mingPalace?.adjectiveStars?.map(s => s.name) || [];
const allMingStars = [...mingStars, ...mingMinor, ...mingAdj];
// 三方四正
const opposite = (mingIdx + 6) % 12;
const tri1 = (mingIdx + 4) % 12;
const tri2 = (mingIdx + 8) % 12;
const sanfangPalaces = [palaces[opposite], palaces[tri1], palaces[tri2]];
const sanfangStars = sanfangPalaces.flatMap(p => [
...(p?.majorStars?.map(s => s.name) || []),
...(p?.minorStars?.map(s => s.name) || []),
...(p?.adjectiveStars?.map(s => s.name) || [])
]);
const allSanfangStars = [...allMingStars, ...sanfangStars];
// 邻宫
const prevIdx = (mingIdx - 1 + 12) % 12;
const nextIdx = (mingIdx + 1) % 12;
const prevStars = [
...(palaces[prevIdx]?.majorStars?.map(s => s.name) || []),
...(palaces[prevIdx]?.minorStars?.map(s => s.name) || [])
];
const nextStars = [
...(palaces[nextIdx]?.majorStars?.map(s => s.name) || []),
...(palaces[nextIdx]?.minorStars?.map(s => s.name) || [])
];
// 四化星
const transformMap = {};
transforms.forEach(t => { transformMap[t.star] = t.hua; });
for (const rule of rules) {
try {
if (!rule.stars || rule.stars.length === 0) continue;
let matched = false;
let matchType = '';
// 主星检查
const requiredStars = rule.stars.filter(s => {
const main14 = ['紫微','天机','太阳','武曲','天同','廉贞','天府','贪狼','巨门','太阴','天相','天梁','七杀','破军',
'文昌','文曲','左辅','右弼','天魁','天钺','禄存','天马','擎羊','陀罗','火星','铃星','地空','地劫'];
return main14.includes(s);
});
if (requiredStars.length === 0) continue;
if (rule.palaceCondition === 'ming' || rule.palaceCondition === 'mingBody') {
// 命宫/命身宫检查
if (requiredStars.every(s => allMingStars.includes(s))) {
matched = true;
matchType = '命宫';
}
} else if (rule.palaceCondition === 'sanfang') {
// 三方四正检查
if (requiredStars.every(s => allSanfangStars.includes(s))) {
matched = true;
matchType = '三方四正';
}
} else if (rule.palaceCondition === 'adjacent') {
// 邻宫夹命检查
const prevHas = requiredStars.some(s => prevStars.includes(s));
const nextHas = requiredStars.some(s => nextStars.includes(s));
if (prevHas && nextHas) {
matched = true;
matchType = '邻宫夹命';
}
} else {
// 默认:命宫优先,三方四正次之
if (requiredStars.every(s => allMingStars.includes(s))) {
matched = true;
matchType = '命宫';
} else if (requiredStars.every(s => allSanfangStars.includes(s))) {
matched = true;
matchType = '三方四正';
}
}
// 年干条件
if (matched && rule.yearStems && rule.yearStems.length > 0) {
if (!rule.yearStems.includes(yearStem)) {
matched = false;
}
}
// 宫位地支条件
if (matched && rule.branches && rule.branches.length > 0) {
const validBranch = rule.branches.some(b => {
if (b.length === 1) return mingBranch === b;
// 处理双地支如寅申
return b.split('').some(c => mingBranch === c);
});
if (!validBranch) matched = false;
}
// 吉星加会条件
if (matched && rule.luckyStars && rule.luckyStars.length > 0) {
const hasLucky = rule.luckyStars.every(s => allSanfangStars.includes(s));
if (!hasLucky) {
// 降级:记录为弱匹配
matchType += '(吉星不足)';
}
}
if (matched) {
results.push({
name: rule.name,
level: rule.level,
desc: rule.desc,
matchType
});
}
} catch (e) {
// Skip failed rules silently
}
}
return results;
}
// ============================================================
// 十四主星、六煞星等定义
// ============================================================
const MAIN_STARS = ['紫微','天机','太阳','武曲','天同','廉贞','天府','贪狼','巨门','太阴','天相','天梁','七杀','破军'];
const LUCKY_STARS = ['左辅','右弼','天魁','天钺','文昌','文曲','禄存','天马'];
const UNLUCKY_STARS = ['擎羊','陀罗','火星','铃星','地空','地劫'];
const PEACH_STARS = ['贪狼','廉贞','红鸾','天喜','桃花','天姚'];
const WEALTH_STARS = ['武曲','太阴','天府','禄存','紫微','天相'];
// ============================================================
// 八字用神核心算法(穷通宝鉴 + 子平真诠)
// ============================================================
// 天干五行
const STEM_ELEMENT = { '甲': '木', '乙': '木', '丙': '火', '丁': '火', '戊': '土', '己': '土', '庚': '金', '辛': '金', '壬': '水', '癸': '水' };
const STEM_YINYANG = { '甲': '阳', '乙': '阴', '丙': '阳', '丁': '阴', '戊': '阳', '己': '阴', '庚': '阳', '辛': '阴', '壬': '阳', '癸': '阴' };
const ELEMENT_PRODUCES = { '木': '水', '火': '木', '土': '火', '金': '土', '水': '金' };
const ELEMENT_RESTRAINS = { '木': '金', '火': '水', '土': '木', '金': '火', '水': '土' };
const ELEMENT_SHENG = { '木': '火', '火': '土', '土': '金', '金': '水', '水': '木' };
const ELEMENT_KE = { '木': '土', '火': '金', '土': '水', '金': '木', '水': '火' };
const ELEMENT_BI = { '木': '金', '火': '水', '土': '木', '金': '火', '水': '土' };
// 地支藏干(主气、中气、余气)
const BRANCH_HIDDEN = {
'子': { '主气': '癸', '中气': '壬', '余气': '辛' },
'丑': { '主气': '己', '中气': '辛', '余气': '癸' },
'寅': { '主气': '甲', '中气': '丙', '余气': '戊' },
'卯': { '主气': '乙', '中气': '甲', '余气': '壬' },
'辰': { '主气': '戊', '中气': '乙', '余气': '癸' },
'巳': { '主气': '丙', '中气': '庚', '余气': '戊' },
'午': { '主气': '丁', '中气': '己', '余气': '乙' },
'未': { '主气': '己', '中气': '丁', '余气': '乙' },
'申': { '主气': '庚', '中气': '壬', '余气': '戊' },
'酉': { '主气': '辛', '中气': '庚', '余气': '丁' },
'戌': { '主气': '戊', '中气': '辛', '余气': '丁' },
'亥': { '主气': '壬', '中气': '甲', '余气': '戊' }
};
const HIDDEN_WEIGHT = { '主气': 1.0, '中气': 0.5, '余气': 0.3 };
// 地支藏干旺衰权重
const BRANCH_HIDDEN_WEIGHT = { '主气': 1.0, '中气': 0.5, '余气': 0.3 };
// 月令旺衰表(子平真诠)
const MONTH_STRENGTH = {
'寅': { '甲': 100, '乙': 80, '丙': 70, '丁': 60, '戊': 50, '己': 40, '庚': 30, '辛': 20, '壬': 10, '癸': 0 },
'卯': { '甲': 80, '乙': 100, '丙': 60, '丁': 70, '戊': 40, '己': 50, '庚': 20, '辛': 30, '壬': 10, '癸': 0 },
'辰': { '甲': 60, '乙': 70, '丙': 70, '丁': 80, '戊': 70, '己': 80, '庚': 50, '辛': 60, '壬': 40, '癸': 50 },
'巳': { '甲': 30, '乙': 40, '丙': 100, '丁': 80, '戊': 60, '己': 50, '庚': 40, '辛': 30, '壬': 10, '癸': 0 },
'午': { '甲': 20, '乙': 30, '丙': 80, '丁': 100, '戊': 50, '己': 60, '庚': 30, '辛': 40, '壬': 0, '癸': 10 },
'未': { '甲': 50, '乙': 60, '丙': 60, '丁': 70, '戊': 70, '己': 80, '庚': 50, '辛': 60, '壬': 20, '癸': 30 },
'申': { '甲': 20, '乙': 10, '丙': 30, '丁': 40, '戊': 50, '己': 60, '庚': 100, '辛': 80, '壬': 70, '癸': 50 },
'酉': { '甲': 10, '乙': 20, '丙': 20, '丁': 30, '戊': 40, '己': 50, '庚': 80, '辛': 100, '壬': 50, '癸': 70 },
'戌': { '甲': 50, '乙': 60, '丙': 70, '丁': 80, '戊': 70, '己': 80, '庚': 50, '辛': 60, '壬': 40, '癸': 50 },
'亥': { '甲': 70, '乙': 60, '丙': 20, '丁': 30, '戊': 30, '己': 40, '庚': 10, '辛': 20, '壬': 100, '癸': 80 },
'子': { '甲': 50, '乙': 40, '丙': 10, '丁': 20, '戊': 20, '己': 30, '庚': 0, '辛': 10, '壬': 80, '癸': 100 },
'丑': { '甲': 40, '乙': 50, '丙': 50, '丁': 60, '戊': 60, '己': 70, '庚': 50, '辛': 60, '壬': 50, '癸': 60 }
};
// 通根加分表
const TONGGEN_BONUS = {
'甲': { '寅': 50, '卯': 40, '亥': 20, '子': 0, '辰': 10, '未': 10, '戌': 10, '丑': 5 },
'乙': { '卯': 50, '寅': 30, '亥': 10, '子': 20, '辰': 10, '未': 15, '戌': 10, '丑': 10 },
'丙': { '巳': 50, '午': 40, '寅': 20, '卯': 10, '申': 0, '酉': 0, '辰': 5, '戌': 10, '丑': 5 },
'丁': { '午': 50, '巳': 30, '未': 15, '戌': 10, '寅': 10, '酉': 0, '申': 0, '辰': 5, '丑': 5 },
'戊': { '巳': 20, '午': 30, '辰': 40, '戌': 40, '丑': 30, '寅': 5, '卯': 5, '申': 0, '酉': 0, '亥': 0, '子': 0 },
'己': { '午': 20, '巳': 10, '辰': 30, '戌': 30, '丑': 40, '寅': 5, '卯': 5, '申': 0, '酉': 0, '亥': 5, '子': 5 },
'庚': { '申': 50, '酉': 40, '辰': 15, '戌': 15, '丑': 20, '寅': 0, '卯': 0, '巳': 0, '午': 0, '亥': 0, '子': 0 },
'辛': { '酉': 50, '申': 30, '辰': 10, '戌': 10, '丑': 15, '寅': 0, '卯': 0, '巳': 0, '午': 0, '亥': 0, '子': 0 },
'壬': { '亥': 50, '子': 40, '申': 20, '酉': 10, '辰': 10, '戌': 10, '丑': 15, '寅': 0, '卯': 0, '巳': 0, '午': 0 },
'癸': { '子': 50, '亥': 40, '丑': 20, '辰': 10, '戌': 10, '申': 5, '酉': 5, '寅': 0, '卯': 0, '巳': 0, '午': 0 }
};
// 穷通宝鉴调候用神表(完整版)
const TIAO_HOU_TABLE = {
// === 甲木日主 ===
'甲寅': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '庚', 说明: '寅月木寒,丙火为君,癸水为佐' },
'甲卯': { 主用神: ['丁', '丙'], 优先级: '丁先', 忌神: '庚', 说明: '卯月木旺,丁火泄秀,忌金' },
'甲辰': { 主用神: ['庚', '丁'], 优先级: '庚先丁后', 忌神: '癸', 说明: '辰月土旺,先庚后丁' },
'甲巳': { 主用神: ['癸', '丁'], 优先级: '癸先丁后', 忌神: '庚', 说明: '巳月火旺,癸水调候' },
'甲午': { 主用神: ['癸', '壬'], 优先级: '癸先', 忌神: '丁', 说明: '午月火旺,水为调候' },
'甲未': { 主用神: ['丁', '庚'], 优先级: '丁先', 忌神: '癸', 说明: '未月土月,用丁庚' },
'甲申': { 主用神: ['庚', '丁'], 优先级: '庚先丁后', 忌神: '癸', 说明: '申月金旺,庚劈甲引丁' },
'甲酉': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '庚', 说明: '酉月金旺,丁火制金' },
'甲戌': { 主用神: ['庚', '丁'], 优先级: '庚先丁后', 忌神: '癸', 说明: '戌月金土,用庚丁' },
'甲亥': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '庚', 说明: '亥月水冷,丙火调候' },
'甲子': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '庚', 说明: '子月水寒,丙戊并用' },
'甲丑': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '辛', 说明: '丑月寒湿,丁火暖局' },
// === 乙木日主 ===
'乙寅': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '辛', 说明: '寅月木寒,丙癸双清' },
'乙卯': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '辛', 说明: '卯月木旺,丙癸调候' },
'乙辰': { 主用神: ['癸', '丙'], 优先级: '癸先丙后', 忌神: '乙', 说明: '辰月湿土,癸水润乙' },
'乙巳': { 主用神: ['癸', '丙'], 优先级: '癸先丙后', 忌神: '辛', 说明: '巳月火旺,癸水调候' },
'乙午': { 主用神: ['癸', '壬'], 优先级: '癸先', 忌神: '丙', 说明: '午月火旺,癸水制火' },
'乙未': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '乙', 说明: '未月土月,丙癸并用' },
'乙申': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '辛', 说明: '申月金旺,丙癸并用' },
'乙酉': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '辛', 说明: '酉月金旺,丙火制金' },
'乙戌': { 主用神: ['癸', '辛'], 优先级: '癸先辛后', 忌神: '丙', 说明: '戌月燥土,癸水润局' },
'乙亥': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '辛', 说明: '亥月水冷,丙戊暖局' },
'乙子': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '辛', 说明: '子月水寒,丙戊调候' },
'乙丑': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '辛', 说明: '丑月寒湿,丙丁暖局' },
// === 丙火日主 ===
'丙寅': { 主用神: ['壬', '庚'], 优先级: '壬先庚后', 忌神: '癸', 说明: '寅月木火,壬水通月令' },
'丙卯': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '甲', 说明: '卯月木旺,壬癸制火' },
'丙辰': { 主用神: ['壬', '庚'], 优先级: '壬先庚后', 忌神: '戊', 说明: '辰月湿土,壬水通根' },
'丙巳': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '戊', 说明: '巳月火旺,壬水为用' },
'丙午': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丙', 说明: '午月火旺极,壬水调候' },
'丙未': { 主用神: ['壬', '庚'], 优先级: '壬先庚后', 忌神: '己', 说明: '未月土月,壬庚并用' },
'丙申': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '庚', 说明: '申月金水,壬水通根' },
'丙酉': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '辛', 说明: '酉月金旺,壬癸制火' },
'丙戌': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '丁', 说明: '戌月土金,壬甲并用' },
'丙亥': { 主用神: ['甲', '壬'], 优先级: '甲先壬后', 忌神: '辛', 说明: '亥月水冷,甲木生火' },
'丙子': { 主用神: ['甲', '壬'], 优先级: '甲先壬后', 忌神: '癸', 说明: '子月水旺,甲木生丙' },
'丙丑': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '己', 说明: '丑月寒湿,壬甲暖局' },
// === 丁火日主 ===
'丁寅': { 主用神: ['甲', '丙'], 优先级: '甲先丙后', 忌神: '壬', 说明: '寅月木旺,甲木生丁' },
'丁卯': { 主用神: ['甲', '丙'], 优先级: '甲先丙后', 忌神: '癸', 说明: '卯月木旺,甲丙生丁' },
'丁辰': { 主用神: ['甲', '庚'], 优先级: '甲先庚后', 忌神: '癸', 说明: '辰月土月,甲庚并用' },
'丁巳': { 主用神: ['甲', '庚'], 优先级: '甲先庚后', 忌神: '戊', 说明: '巳月火旺,甲庚制火' },
'丁午': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丁', 说明: '午月火旺,壬癸调候' },
'丁未': { 主用神: ['甲', '庚'], 优先级: '甲先庚后', 忌神: '丁', 说明: '未月土月,甲庚并用' },
'丁申': { 主用神: ['甲', '丙'], 优先级: '甲先丙后', 忌神: '壬', 说明: '申月金旺,甲丙生丁' },
'丁酉': { 主用神: ['甲', '丙'], 优先级: '甲先丙后', 忌神: '癸', 说明: '酉月金旺,甲丙生丁' },
'丁戌': { 主用神: ['甲', '壬'], 优先级: '甲先壬后', 忌神: '丁', 说明: '戌月燥土,壬水润局' },
'丁亥': { 主用神: ['甲', '庚'], 优先级: '甲先庚后', 忌神: '壬', 说明: '亥月水冷,甲庚暖局' },
'丁子': { 主用神: ['甲', '庚'], 优先级: '甲先庚后', 忌神: '癸', 说明: '子月水寒,甲庚暖局' },
'丁丑': { 主用神: ['甲', '庚'], 优先级: '甲先庚后', 忌神: '癸', 说明: '丑月寒湿,甲庚暖局' },
// === 戊土日主 ===
'戊寅': { 主用神: ['丙', '甲'], 优先级: '丙先甲后', 忌神: '壬', 说明: '寅月木旺,丙甲并用' },
'戊卯': { 主用神: ['丙', '甲'], 优先级: '丙先甲后', 忌神: '壬', 说明: '卯月木旺,丙甲并用' },
'戊辰': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '甲', 说明: '辰月湿土,丙癸调候' },
'戊巳': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '甲', 说明: '巳月火旺,丙癸并用' },
'戊午': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丙', 说明: '午月火旺极,壬癸调候' },
'戊未': { 主用神: ['癸', '丙'], 优先级: '癸先丙后', 忌神: '己', 说明: '未月土月,癸水润局' },
'戊申': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '壬', 说明: '申月金旺,丙丁暖局' },
'戊酉': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '癸', 说明: '酉月金旺,丙丁暖局' },
'戊戌': { 主用神: ['甲', '丁'], 优先级: '甲先丁后', 忌神: '壬', 说明: '戌月燥土,甲丁调候' },
'戊亥': { 主用神: ['丙', '甲'], 优先级: '丙先甲后', 忌神: '壬', 说明: '亥月水冷,丙甲暖局' },
'戊子': { 主用神: ['丙', '甲'], 优先级: '丙先甲后', 忌神: '壬', 说明: '子月水寒,丙甲暖局' },
'戊丑': { 主用神: ['丙', '甲'], 优先级: '丙先甲后', 忌神: '癸', 说明: '丑月寒湿,丙甲暖局' },
// === 己土日主 ===
'己寅': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '甲', 说明: '寅月木旺,丙癸暖局' },
'己卯': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '甲', 说明: '卯月木旺,丙癸暖局' },
'己辰': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '乙', 说明: '辰月湿土,丙癸调候' },
'己巳': { 主用神: ['癸', '丙'], 优先级: '癸先丙后', 忌神: '甲', 说明: '巳月火旺,癸水润局' },
'己午': { 主用神: ['癸', '壬'], 优先级: '癸先壬后', 忌神: '丙', 说明: '午月火旺,癸壬调候' },
'己未': { 主用神: ['癸', '丙'], 优先级: '癸先丙后', 忌神: '己', 说明: '未月土月,癸水润局' },
'己申': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '壬', 说明: '申月金旺,丙癸暖局' },
'己酉': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '辛', 说明: '酉月金旺,丙癸暖局' },
'己戌': { 主用神: ['癸', '辛'], 优先级: '癸先辛后', 忌神: '丙', 说明: '戌月燥土,癸水润燥' },
'己亥': { 主用神: ['丙', '辛'], 优先级: '丙先辛后', 忌神: '壬', 说明: '亥月水冷,丙辛暖局' },
'己子': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '癸', 说明: '子月水寒,丙丁暖局' },
'己丑': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '癸', 说明: '丑月寒湿,丙丁暖局' },
// === 庚金日主 ===
'庚寅': { 主用神: ['丁', '甲'], 优先级: '丁先甲后', 忌神: '壬', 说明: '寅月木旺,丁甲并用' },
'庚卯': { 主用神: ['丁', '甲'], 优先级: '丁先甲后', 忌神: '癸', 说明: '卯月木旺,丁甲制木' },
'庚辰': { 主用神: ['丁', '甲'], 优先级: '丁先甲后', 忌神: '壬', 说明: '辰月土月,丁甲并用' },
'庚巳': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丙', 说明: '巳月火旺,壬癸制火' },
'庚午': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丁', 说明: '午月火旺,壬癸调候' },
'庚未': { 主用神: ['丁', '甲'], 优先级: '丁先甲后', 忌神: '己', 说明: '未月土月,丁甲暖局' },
'庚申': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '壬', 说明: '申月金旺,丁丙制金' },
'庚酉': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '壬', 说明: '酉月金旺,丁丙制金' },
'庚戌': { 主用神: ['丁', '甲'], 优先级: '丁先甲后', 忌神: '辛', 说明: '戌月燥土,丁甲调候' },
'庚亥': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '壬', 说明: '亥月水冷,丁丙暖局' },
'庚子': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '癸', 说明: '子月水寒,丁丙暖局' },
'庚丑': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '癸', 说明: '丑月寒湿,丙丁暖局' },
// === 辛金日主 ===
'辛寅': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '丙', 说明: '寅月木旺,壬水化木' },
'辛卯': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '丙', 说明: '卯月木旺,壬甲并用' },
'辛辰': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '乙', 说明: '辰月土月,壬甲暖局' },
'辛巳': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丙', 说明: '巳月火旺,壬癸制火' },
'辛午': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丁', 说明: '午月火旺,壬癸调候' },
'辛未': { 主用神: ['丁', '甲'], 优先级: '丁先甲后', 忌神: '己', 说明: '未月土月,丁甲暖局' },
'辛申': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '庚', 说明: '申月金旺,壬水洗金' },
'辛酉': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '庚', 说明: '酉月金旺,壬水洗金' },
'辛戌': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '辛', 说明: '戌月燥土,丁丙暖局' },
'辛亥': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '壬', 说明: '亥月水冷,丙戊暖局' },
'辛子': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '丙', 说明: '子月水寒,壬甲暖局' },
'辛丑': { 主用神: ['壬', '庚'], 优先级: '壬先庚后', 忌神: '己', 说明: '丑月寒湿,壬庚暖局' },
// === 壬水日主 ===
'壬寅': { 主用神: ['庚', '戊'], 优先级: '庚先戊后', 忌神: '丙', 说明: '寅月木旺,庚金生水' },
'壬卯': { 主用神: ['庚', '辛'], 优先级: '庚先辛后', 忌神: '丙', 说明: '卯月木旺,庚辛生水' },
'壬辰': { 主用神: ['庚', '丙'], 优先级: '庚先丙后', 忌神: '甲', 说明: '辰月土月,庚丙并用' },
'壬巳': { 主用神: ['辛', '庚'], 优先级: '辛先庚后', 忌神: '戊', 说明: '巳月火旺,辛金化火' },
'壬午': { 主用神: ['辛', '癸'], 优先级: '辛先癸后', 忌神: '丁', 说明: '午月火旺,辛癸调候' },
'壬未': { 主用神: ['庚', '辛'], 优先级: '庚先辛后', 忌神: '己', 说明: '未月土月,庚辛生水' },
'壬申': { 主用神: ['戊', '丁'], 优先级: '戊先丁后', 忌神: '丙', 说明: '申月金旺,戊丁暖局' },
'壬酉': { 主用神: ['戊', '丁'], 优先级: '戊先丁后', 忌神: '丙', 说明: '酉月金旺,戊丁暖局' },
'壬戌': { 主用神: ['辛', '丙'], 优先级: '辛先丙后', 忌神: '甲', 说明: '戌月燥土,辛丙调候' },
'壬亥': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '庚', 说明: '亥月水冷,丙戊暖局' },
'壬子': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '庚', 说明: '子月水寒,丙戊暖局' },
'壬丑': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '己', 说明: '丑月寒湿,丙丁暖局' },
// === 癸水日主 ===
'癸寅': { 主用神: ['辛', '丙'], 优先级: '辛先丙后', 忌神: '壬', 说明: '寅月木旺,辛丙暖局' },
'癸卯': { 主用神: ['庚', '辛'], 优先级: '庚先辛后', 忌神: '壬', 说明: '卯月木旺,庚辛生水' },
'癸辰': { 主用神: ['辛', '丙'], 优先级: '辛先丙后', 忌神: '乙', 说明: '辰月湿土,辛丙暖局' },
'癸巳': { 主用神: ['辛', '壬'], 优先级: '辛先壬后', 忌神: '戊', 说明: '巳月火旺,辛壬调候' },
'癸午': { 主用神: ['癸', '壬'], 优先级: '癸先壬后', 忌神: '丁', 说明: '午月火旺,癸壬制火' },
'癸未': { 主用神: ['庚', '辛'], 优先级: '庚先辛后', 忌神: '己', 说明: '未月土月,庚辛生水' },
'癸申': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '壬', 说明: '申月金旺,丁丙暖局' },
'癸酉': { 主用神: ['辛', '丁'], 优先级: '辛先丁后', 忌神: '壬', 说明: '酉月金旺,辛金生水' },
'癸戌': { 主用神: ['辛', '壬'], 优先级: '辛先壬后', 忌神: '丙', 说明: '戌月燥土,辛壬润局' },
'癸亥': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '庚', 说明: '亥月水冷,丙戊暖局' },
'癸子': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '庚', 说明: '子月水寒,丙丁暖局' },
'癸丑': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '己', 说明: '丑月寒湿,丙丁暖局' },
};
// ============================================================
// 八字用神算法 - 增强版(穷通宝鉴 + 子平真诠)
// ============================================================
// 天干→十神映射(以日主为基准)
function stemToTenGods(dayMaster) {
return {
'甲': { '甲': '比肩', '乙': '劫财', '丙': '食神', '丁': '伤官', '戊': '偏财', '己': '正财', '庚': '七杀', '辛': '正官', '壬': '偏印', '癸': '正印' },
'乙': { '甲': '劫财', '乙': '比肩', '丙': '伤官', '丁': '食神', '戊': '正财', '己': '偏财', '庚': '正官', '辛': '七杀', '壬': '正印', '癸': '偏印' },
'丙': { '甲': '偏印', '乙': '正印', '丙': '比肩', '丁': '劫财', '戊': '食神', '己': '伤官', '庚': '偏财', '辛': '正财', '壬': '七杀', '癸': '正官' },
'丁': { '甲': '正印', '乙': '偏印', '丙': '劫财', '丁': '比肩', '戊': '伤官', '己': '食神', '庚': '正财', '辛': '偏财', '壬': '正官', '癸': '七杀' },
'戊': { '甲': '七杀', '乙': '正官', '丙': '偏印', '丁': '正印', '戊': '比肩', '己': '劫财', '庚': '食神', '辛': '伤官', '壬': '偏财', '癸': '正财' },
'己': { '甲': '正官', '乙': '七杀', '丙': '正印', '丁': '偏印', '戊': '劫财', '己': '比肩', '庚': '伤官', '辛': '食神', '壬': '正财', '癸': '偏财' },
'庚': { '甲': '偏财', '乙': '正财', '丙': '七杀', '丁': '正官', '戊': '偏印', '己': '正印', '庚': '比肩', '辛': '劫财', '壬': '食神', '癸': '伤官' },
'辛': { '甲': '正财', '乙': '偏财', '丙': '正官', '丁': '七杀', '戊': '正印', '己': '偏印', '庚': '劫财', '辛': '比肩', '壬': '伤官', '癸': '食神' },
'壬': { '甲': '食神', '乙': '伤官', '丙': '正财', '丁': '偏财', '戊': '七杀', '己': '正官', '庚': '偏印', '辛': '正印', '壬': '比肩', '癸': '劫财' },
'癸': { '甲': '伤官', '乙': '食神', '丙': '偏财', '丁': '正财', '戊': '正官', '己': '七杀', '庚': '正印', '辛': '偏印', '壬': '劫财', '癸': '比肩' },
}[dayMaster];
}
// 获取八字中所有天干和地支藏干
function getAllStemsAndHidden(palaces) {
const allStems = [];
const allHidden = []; // { stem, weight }
for (const p of palaces) {
if (p.stem) allStems.push(p.stem);
const hidden = BRANCH_HIDDEN[p.branch];
if (hidden) {
for (const [pos, stem] of Object.entries(hidden)) {
if (pos === '主气' || pos === '中气' || pos === '余气') {
allHidden.push({ stem, weight: BRANCH_HIDDEN_WEIGHT[pos] || 0 });
}
}
}
}
return { allStems, allHidden };
}
// 判断是否需要调候
function needsTiaoHou(monthBranch) {
const coldMonths = ['子', '丑', '亥'];
const hotMonths = ['巳', '午', '未'];
return { isCold: coldMonths.includes(monthBranch), isHot: hotMonths.includes(monthBranch) };
}
// 穷通宝鉴调候用神查询
function getTiaoHouByTable(dayStem, monthBranch) {
return TIAO_HOU_TABLE[`dayStemmonthBranch`] || null;
}
// 子平真诠格局判断
function calculatePattern(dayStem, monthBranch, monthStem, strengthScore) {
const me = dayStem;
const hidden = BRANCH_HIDDEN[monthBranch];
const tenGodsMap = stemToTenGods(me);
// Step 1: 子平真诠月令取用
// 规则:月令本气透干以透出为用,否则取本气
let yongshenStem = hidden['主气'];
if (monthStem && monthStem !== '' && [hidden['主气'], hidden['中气'], hidden['余气']].includes(monthStem)) {
// 月干透出,以透出为用
if (monthStem === hidden['主气'] || monthStem === hidden['中气']) {
yongshenStem = monthStem;
}
}
const tenGod = tenGodsMap[yongshenStem] || '比肩';
// Step 2: 判断格局类型
let patternType = '正格';
const isStrong = strengthScore >= 220;
const isWeak = strengthScore < 150;
// 从格判断:日主极弱时
if (isWeak) {
if (tenGod === '正官' || tenGod === '七杀') patternType = '从官杀格';
else if (tenGod === '正财' || tenGod === '偏财') patternType = '从财格';
else if (tenGod === '食神' || tenGod === '伤官') patternType = '从食伤格';
else patternType = '正格';
}
// 专旺格:日主极强时
else if (strengthScore >= 380) {
patternType = '专旺格';
}
// Step 3: 善用神判断
// 身旺用官杀/财/食伤为善;身弱用印比为善
let isGood = true;
if (isStrong) {
if (['比肩', '劫财', '偏印', '正印'].includes(tenGod)) isGood = false;
} else if (isWeak) {
if (['七杀', '正官', '偏财', '正财', '食神', '伤官'].includes(tenGod)) isGood = false;
}
// Step 4: 格局名称
let patternName = tenGod;
if (patternType !== '正格') {
patternName = patternType;
}
// Step 5: 格局用神
let patternYongshen = '';
if (patternType === '正格') {
patternYongshen = yongshenStem;
// 身旺:取克泄;身弱:取生助
if (isStrong) {
const ke = ELEMENT_KE[STEM_ELEMENT[me]];
const bi = ELEMENT_BI[STEM_ELEMENT[me]];
const sheng = ELEMENT_SHENG[STEM_ELEMENT[me]];
// 官杀、财、食伤皆可用
patternYongshen = `yongshenStem(可辅以ke、bi)`;
} else if (isWeak) {
const sheng = ELEMENT_SHENG[STEM_ELEMENT[me]];
const wuxing = STEM_ELEMENT[me];
patternYongshen = `yongshenStem(可辅以sheng、wuxing)`;
}
}
return {
patternName,
patternType,
tenGod,
yongshenStem,
patternYongshen,
isGood,
desc: `monthBranch月令,yongshenStem为用,取tenGod(patternType)`
};
}
// 子平真诠日主强弱判断(增强版)
function calculateStrengthEnhanced(dayMaster, monthBranch, palaces) {
const me = dayMaster;
const myElement = STEM_ELEMENT[me];
// 1. 得令分(月令旺衰)
const monthScore = MONTH_STRENGTH[monthBranch]?.[me] ?? 0;
// 2. 得地分(地支根气 - 通根)
let tonggenScore = 0;
for (const p of palaces) {
const bonus = TONGGEN_BONUS[me]?.[p.branch] ?? 0;
if (bonus > 0) tonggenScore += bonus;
}
// 3. 得助分(天干印比帮身)
let bizhuScore = 0;
let yinScore = 0;
for (const p of palaces) {
const stem = p.stem;
const stemElement = STEM_ELEMENT[stem];
if (stemElement === myElement && stem !== me) {
bizhuScore += 20; // 比肩/劫财
}
if (ELEMENT_PRODUCES[myElement] === stemElement) {
yinScore += 15; // 印星
}
}
// 4. 地支藏干中的印比
for (const p of palaces) {
const hidden = BRANCH_HIDDEN[p.branch];
if (hidden) {
for (const [pos, stem] of Object.entries(hidden)) {
if (pos === '主气' || pos === '中气' || pos === '余气') {
const stemElement = STEM_ELEMENT[stem];
const weight = BRANCH_HIDDEN_WEIGHT[pos] || 0;
if (stemElement === myElement) {
bizhuScore += 15 * weight;
}
if (ELEMENT_PRODUCES[myElement] === stemElement) {
yinScore += 10 * weight;
}
}
}
}
}
const totalScore = monthScore + tonggenScore + bizhuScore + yinScore;
// 5. 等级判断
let level = '中和';
if (totalScore < 80) level = '极弱';
else if (totalScore < 150) level = '弱';
else if (totalScore < 220) level = '偏弱';
else if (totalScore < 300) level = '中和';
else if (totalScore < 380) level = '偏强';
else if (totalScore < 450) level = '强';
else level = '极强';
// 6. 用神方向
let direction = '中和难取';
let directionDesc = '';
if (level.includes('弱')) {
direction = '扶抑-扶';
directionDesc = '宜取印比生扶';
} else if (level.includes('强')) {
direction = '扶抑-抑';
directionDesc = '宜取官杀财食克泄';
}
return {
level,
score: Math.round(totalScore),
monthScore: Math.round(monthScore),
tonggenScore: Math.round(tonggenScore),
bizhuScore: Math.round(bizhuScore),
yinScore: Math.round(yinScore),
totalScore,
direction,
directionDesc,
weightBreakdown: `月令monthScore分 + 通根tonggenScore分 + 比劫bizhuScore分 + 印绶yinScore分`
};
}
// 综合用神计算(穷通宝鉴 + 子平真诠)
// 参数:dayMaster{stem, wuxing}, monthBranch, monthStem, palaces, strength{level, score, direction}
function calculateYongshenEnhanced(dayMaster, monthBranch, monthStem, palaces, strength) {
const me = dayMaster.originalStem;
const myElement = dayMaster.wuxing;
const results = [];
const { allStems, allHidden } = getAllStemsAndHidden(palaces);
// === 1. 调候用神(穷通宝鉴) ===
const tiaohouRule = getTiaoHouByTable(me, monthBranch);
if (tiaohouRule) {
const tiaohouPresent = tiaohouRule['主用神'].filter(g => allStems.includes(g));
const tiaohouAbsent = tiaohouRule['主用神'].filter(g => !allStems.includes(g));
const status = tiaohouPresent.length === tiaohouRule['主用神'].length ? '调候俱全' :
tiaohouPresent.length > 0 ? '调候不全' : '调候皆缺';
results.push({
type: '调候',
values: tiaohouPresent.length > 0 ? tiaohouPresent : tiaohouAbsent,
primary: tiaohouRule['主用神'][0],
present: tiaohouPresent,
absent: tiaohouAbsent,
status,
priority: tiaohouRule['优先级'],
avoid: tiaohouRule['忌神'],
desc: tiaohouRule['说明'],
isUrgent: needsTiaoHou(monthBranch).isCold || needsTiaoHou(monthBranch).isHot
});
}
// === 2. 格局用神(子平真诠) ===
const pattern = calculatePattern(me, monthBranch, monthStem, strength.score);
results.push({
type: '格局',
value: pattern.yongshenStem,
patternName: pattern.patternName,
patternType: pattern.patternType,
tenGod: pattern.tenGod,
isGood: pattern.isGood,
desc: pattern.desc,
detail: pattern.patternYongshen
});
// === 3. 通关用神 ===
// 当月令与日主相克时需要通关
const monthElement = STEM_ELEMENT[BRANCH_HIDDEN[monthBranch]?.['主气'] || monthBranch];
const myKe = ELEMENT_KE[myElement]; // 日主所克
const mySheng = ELEMENT_SHENG[myElement]; // 日主所生
// 月令克日主 → 用印通关
if (ELEMENT_RESTRAINS[monthElement] === myElement) {
const mediator = ELEMENT_PRODUCES[monthElement]; // 月令的印星可通关
if (mediator && !results.some(r => r.values?.includes(mediator))) {
results.push({ type: '通关', value: mediator, desc: `monthElement克myElement,以mediator通关`, isUrgent: false });
}
}
// 日主克月令 → 用食伤通关
if (ELEMENT_RESTRAINS[myElement] === monthElement) {
const biElement = ELEMENT_BI[myElement]; // 日主所泄(食伤)
if (biElement && !results.some(r => r.values?.includes(biElement))) {
results.push({ type: '通关', value: biElement, desc: `myElement克monthElement,以biElement通关`, isUrgent: false });
}
}
// === 4. 扶抑用神 ===
if (strength.direction === '扶抑-扶') {
results.push({
type: '扶抑',
values: [myElement, ELEMENT_SHENG[myElement]],
desc: `身strength.level,宜取myElement、ELEMENT_SHENG[myElement]生助`
});
} else if (strength.direction === '扶抑-抑') {
results.push({
type: '扶抑',
values: [ELEMENT_KE[myElement], ELEMENT_BI[myElement]],
desc: `身strength.level,宜取ELEMENT_KE[myElement]、ELEMENT_BI[myElement]克泄`
});
}
// === 综合排序 ===
// 优先级:调候(紧急时)> 格局 > 通关 > 扶抑
// 调候在寒月(亥子丑)和热月(巳午未)为急
const tiaohou = results.find(r => r.type === '调候');
const isUrgentTiaohou = tiaohou?.isUrgent;
// 构建最终用神列表
const finalDetails = [];
if (isUrgentTiaohou && tiaohou) {
finalDetails.push({ type: '调候(急)', value: tiaohou.primary, desc: tiaohou.desc });
}
const patternResult = results.find(r => r.type === '格局');
if (patternResult) {
finalDetails.push({ type: '格局', value: patternResult.value, desc: patternResult.desc });
}
const touguanResult = results.find(r => r.type === '通关');
if (touguanResult) {
finalDetails.push({ type: '通关', value: touguanResult.value, desc: touguanResult.desc });
}
const fuyiResult = results.find(r => r.type === '扶抑');
if (fuyiResult) {
for (const v of fuyiResult.values || []) {
if (!finalDetails.some(d => d.value === v)) {
finalDetails.push({ type: '扶抑', value: v, desc: fuyiResult.desc });
}
}
}
// 非紧急的调候也加入
if (!isUrgentTiaohou && tiaohou && tiaohou.values) {
for (const v of tiaohou.values) {
if (!finalDetails.some(d => d.value === v)) {
finalDetails.push({ type: '调候', value: v, desc: tiaohou.desc });
}
}
}
// 去重
const seen = new Set();
const uniqueDetails = finalDetails.filter(d => {
if (seen.has(d.value)) return false;
seen.add(d.value);
return true;
});
const primary = uniqueDetails[0]?.value || me;
const secondary = uniqueDetails.slice(1, 4).map(d => d.value);
let tiaohouSummary = '无调候';
if (tiaohou) {
const urgent = isUrgentTiaohou ? '(急)' : '';
// status如"调候俱全",去掉前缀"调候"再拼
const statusPart = (tiaohou.status || '').replace(/^调候/, '');
tiaohouSummary = `调候urgentstatusPart`;
}
let summary = `tiaohouSummary,格局patternResult?.patternName || '待定'`;
if (touguanResult) summary += `,需touguanResult.value通关`;
return {
primary,
secondary,
details: uniqueDetails.slice(0, 5),
summary,
tiaohouStatus: tiaohou ? { present: tiaohou.present, absent: tiaohou.absent, status: tiaohou.status, avoid: tiaohou.avoid } : null,
pattern: patternResult ? { name: patternResult.patternName, type: patternResult.patternType, isGood: patternResult.isGood, tenGod: patternResult.tenGod } : null,
strengthDirection: strength.direction,
fullAnalysis: results
};
}
// 旧版兼容函数 - 保留接口兼容(内部调用增强版)
function getTiaohouYongshen(wuxing, monthBranch) {
// 兼容旧接口:monthBranch可以是地支或月令对象
const branch = typeof monthBranch === 'string' ? monthBranch : (monthBranch?.zhi || monthBranch?.branch || '寅');
// 遍历找主用神
for (const [key, rule] of Object.entries(TIAO_HOU_TABLE)) {
const dayStem = key[0];
const mz = key.slice(1);
if (mz === branch) {
return rule['主用神'][0];
}
}
return null;
}
function getTiaohouDesc(dayStem, monthBranch) {
const rule = getTiaoHouByTable(dayStem, monthBranch);
if (!rule) return '';
const { isCold, isHot } = needsTiaoHou(monthBranch);
let season = '';
if (isCold) season = '寒月';
else if (isHot) season = '热月';
return `''rule['说明']`;
}
// ============================================================
// 核心排盘分析
// ============================================================
function analyzePlate(year, month, day, hour, minute, sex) {
const dateStr = minute > 0
? `year-month-day hour:String(minute).padStart(2, '0')`
: `year-month-day hour`;
const gender = sex === 1 ? 1 : 0;
const astrolabe = astro.bySolar(dateStr, gender, true, 'zh-CN');
// 收集十二宫数据
const palaces = astrolabe.palaces.map((p, idx) => ({
index: idx,
name: p.name,
duty: p.name,
stem: p.heavenlyStem,
branch: p.earthlyBranch,
majorStars: p.majorStars || [],
minorStars: p.minorStars || [],
adjectiveStars: p.adjectiveStars || [],
changsheng12: p.changsheng12 || '',
boshi12: p.boshi12 || '',
jiangqian12: p.jiangqian12 || '',
suiqian12: p.suiqian12 || '',
decadal: p.decadal || {}
}));
let mingIdx = -1;
palaces.forEach((p, idx) => {
if (p.name === '命宫') mingIdx = idx;
});
// 四化信息
const transforms = [];
for (const starName of MAIN_STARS) {
try {
const star = astrolabe.star(starName);
if (star && star.mutagen) {
transforms.push({ star: starName, hua: star.mutagen });
}
} catch (e) { /* skip */ }
}
// 八字信息
const eightChar = astrolabe.chineseDate.split(' ');
// eightChar = ['乙亥', '甲申', '戊寅', '壬子'] -> [年柱, 月柱, 日柱, 时柱]
const yearStem = eightChar[0]?.[0] || '甲'; // 年干 = 乙
const monthStem = eightChar[1]?.[0] || '甲'; // 月干 = 甲
const dayStem = eightChar[2]?.[0] || '甲'; // 日干 = 戊
const monthBranch = eightChar[1]?.[1] || '寅'; // 月支 = 申
// 日主信息
const dayMaster = getDayMaster(dayStem);
const monthInfo = getMonthInfo(monthBranch);
// 计算强弱(子平真诠增强版)
const strength = calculateStrengthEnhanced(dayStem, monthBranch, palaces);
// 构建兼容旧接口的strength对象
const strengthCompat = {
helpScore: strength.bizhuScore + strength.yinScore,
stressScore: 0,
total: strength.score,
strength: strength.level,
needSupport: strength.direction === '扶抑-抑' ? [ELEMENT_KE[dayMaster.wuxing]] : [ELEMENT_SHENG[dayMaster.wuxing], dayMaster.wuxing],
needAvoid: strength.direction === '扶抑-抑' ? [dayMaster.wuxing, ELEMENT_SHENG[dayMaster.wuxing]] : [ELEMENT_KE[dayMaster.wuxing]],
level: strength.level,
score: strength.score,
monthScore: strength.monthScore,
tonggenScore: strength.tonggenScore,
bizhuScore: strength.bizhuScore,
yinScore: strength.yinScore,
direction: strength.direction,
directionDesc: strength.directionDesc,
weightBreakdown: strength.weightBreakdown
};
// 用神计算(穷通宝鉴 + 子平真诠增强版)
const yongshen = calculateYongshenEnhanced(dayMaster, monthBranch, monthStem, palaces, strength);
// 格局检测(知识库驱动)
const knowledgePatterns = checkPatternsFromKnowledge(palaces, mingIdx, transforms, yearStem);
// 传统格局检测(补充)
const traditionalPatterns = checkTraditionalPatterns(palaces, mingIdx, transforms);
// 合并格局
const allPatterns = mergePatterns(knowledgePatterns, traditionalPatterns);
// 大运分析
const decadalAnalysis = analyzeDecadal(astrolabe, year, month, day, gender, mingIdx, palaces);
// 流年分析
const yearlyAnalysis = analyzeYearly(astrolabe, year, month, day, gender, palaces);
return {
basic: {
year, month, day, hour, minute,
sex: sex === 1 ? '男' : '女',
chineseDate: astrolabe.chineseDate,
fiveElements: astrolabe.fiveElementsClass,
soul: astrolabe.soul,
body: astrolabe.body,
zodiac: astrolabe.zodiac,
sign: astrolabe.sign,
palaces,
mingIdx,
transforms,
yearStem,
dayStem,
monthBranch,
astrolabe
},
analysis: {
dayStem,
dayMaster,
monthZhi: monthBranch,
monthInfo,
...strengthCompat,
yongshen
},
patterns: allPatterns,
decadal: decadalAnalysis,
yearly: yearlyAnalysis
};
}
// ============================================================
// 日主与月令
// ============================================================
function getDayMaster(stem) {
const masters = {
'甲': { name: '甲木', wuxing: '木', stem: '阳木', originalStem: '甲' },
'乙': { name: '乙木', wuxing: '木', stem: '阴木', originalStem: '乙' },
'丙': { name: '丙火', wuxing: '火', stem: '阳火', originalStem: '丙' },
'丁': { name: '丁火', wuxing: '火', stem: '阴火', originalStem: '丁' },
'戊': { name: '戊土', wuxing: '土', stem: '阳土', originalStem: '戊' },
'己': { name: '己土', wuxing: '土', stem: '阴土', originalStem: '己' },
'庚': { name: '庚金', wuxing: '金', stem: '阳金', originalStem: '庚' },
'辛': { name: '辛金', wuxing: '金', stem: '阴金', originalStem: '辛' },
'壬': { name: '壬水', wuxing: '水', stem: '阳水', originalStem: '壬' },
'癸': { name: '癸水', wuxing: '水', stem: '阴水', originalStem: '癸' }
};
return masters[stem] || masters['甲'];
}
function getMonthInfo(zhi) {
const infos = {
'寅': { element: '木', strength: '旺', score: 3, season: '春' },
'卯': { element: '木', strength: '旺', score: 3, season: '春' },
'辰': { element: '木', strength: '墓', score: 0, season: '春' },
'巳': { element: '火', strength: '相', score: 2, season: '夏' },
'午': { element: '火', strength: '旺', score: 3, season: '夏' },
'未': { element: '火', strength: '墓', score: 0, season: '夏' },
'申': { element: '金', strength: '旺', score: 3, season: '秋' },
'酉': { element: '金', strength: '旺', score: 3, season: '秋' },
'戌': { element: '金', strength: '墓', score: 0, season: '秋' },
'亥': { element: '水', strength: '旺', score: 3, season: '冬' },
'子': { element: '水', strength: '旺', score: 3, season: '冬' },
'丑': { element: '土', strength: '旺', score: 3, season: '冬' }
};
return infos[zhi] || { element: '土', strength: '平', score: 1, season: '四季' };
}
function getWuxing(stem) {
const map = { '甲': '木', '乙': '木', '丙': '火', '丁': '火', '戊': '土', '己': '土', '庚': '金', '辛': '金', '壬': '水', '癸': '水' };
return map[stem] || '土';
}
// ============================================================
// 命盘强弱分析(旧版兼容wrapper,调用增强版)
// ============================================================
function calculateStrength(dayMaster, monthInfo, palaces, mingIdx) {
// 调用增强版算法
const monthBranch = monthInfo?.zhi || monthInfo?.branch || monthInfo?.branch || '寅';
const enhanced = calculateStrengthEnhanced(dayMaster.originalStem, monthBranch, palaces);
// 兼容旧接口
const wuxing = dayMaster.wuxing;
let needSupport = [], needAvoid = [];
if (enhanced.direction === '扶抑-抑') {
needSupport = [ELEMENT_KE[wuxing]];
needAvoid = [wuxing, ELEMENT_SHENG[wuxing]];
} else if (enhanced.direction === '扶抑-扶') {
needSupport = [ELEMENT_SHENG[wuxing], wuxing];
needAvoid = [ELEMENT_KE[wuxing]];
}
return {
helpScore: enhanced.bizhuScore + enhanced.yinScore,
stressScore: 0,
total: enhanced.score,
strength: enhanced.level,
needSupport,
needAvoid,
// 增强版额外字段
level: enhanced.level,
score: enhanced.score,
monthScore: enhanced.monthScore,
tonggenScore: enhanced.tonggenScore,
bizhuScore: enhanced.bizhuScore,
yinScore: enhanced.yinScore,
direction: enhanced.direction,
directionDesc: enhanced.directionDesc,
weightBreakdown: enhanced.weightBreakdown
};
}
// ============================================================
// 八字用神计算(旧版兼容wrapper,调用增强版)
// ============================================================
function calculateYongshen(dayMaster, monthInfo, palaces, mingIdx, strength) {
const dayStem = dayMaster.stem;
const monthBranch = monthInfo?.zhi || monthInfo?.branch || '寅';
const monthStem = monthInfo?.stem || '';
return calculateYongshenEnhanced(dayMaster, monthBranch, monthStem, palaces, strength);
}
function findWeakestLink(palaces, mingIdx, wuxing) {
const counts = {};
MAIN_STARS.forEach(s => counts[s] = 0);
for (const p of palaces) {
for (const s of p.majorStars || []) {
if (MAIN_STARS.includes(s.name)) counts[s.name]++;
}
}
// 检查是否有某主星完全缺失
const missing = Object.entries(counts).filter(([k, v]) => v === 0).map(([k]) => k);
if (missing.length > 2) {
return { remedy: missing[0], desc: `命局缺missing[0]` };
}
return null;
}
// ============================================================
// 传统格局检测(补充知识库)
// ============================================================
function checkTraditionalPatterns(palaces, mingIdx, transforms) {
const patterns = [];
const mingPalace = palaces[mingIdx];
const mingStars = mingPalace?.majorStars?.map(s => s.name) || [];
const mingBranch = mingPalace?.branch || '';
const allMingStars = [
...mingStars,
...(mingPalace?.minorStars?.map(s => s.name) || []),
...(mingPalace?.adjectiveStars?.map(s => s.name) || [])
];
const getSanfang = () => {
const opp = (mingIdx + 6) % 12;
const t1 = (mingIdx + 4) % 12;
const t2 = (mingIdx + 8) % 12;
return [
...(palaces[opp]?.majorStars?.map(s => s.name) || []),
...(palaces[t1]?.majorStars?.map(s => s.name) || []),
...(palaces[t2]?.majorStars?.map(s => s.name) || [])
];
};
const sanfang = getSanfang();
const allSanfang = [...allMingStars, ...sanfang];
const has = (stars, names) => names.every(n => stars.includes(n));
const hasAny = (stars, names) => names.some(n => stars.includes(n));
const prevIdx = (mingIdx - 1 + 12) % 12;
const nextIdx = (mingIdx + 1) % 12;
const prevStars = palaces[prevIdx]?.minorStars?.map(s => s.name) || [];
const nextStars = palaces[nextIdx]?.minorStars?.map(s => s.name) || [];
// 紫府同宫
if (has(mingStars, ['紫微', '天府']) && ['寅','申'].includes(mingBranch)) {
patterns.push({ name: '紫府同宫', level: '贵', desc: '最吉之格,富贵双全', source: 'traditional' });
}
// 杀破狼
if (['贪狼','七杀','破军'].filter(s => sanfang.includes(s)).length >= 2) {
patterns.push({ name: '杀破狼', level: '变', desc: '动荡变革,破旧立新', source: 'traditional' });
}
// 机月同梁
if (['天机','太阴','天同','天梁'].filter(s => sanfang.includes(s)).length >= 3) {
patterns.push({ name: '机月同梁', level: '富', desc: '善谋稳定,公职之命', source: 'traditional' });
}
// 七杀朝斗
if (has(mingStars, ['七杀']) && ['子','午','寅','申'].includes(mingBranch)) {
patterns.push({ name: '七杀朝斗', level: '贵', desc: '威镇边疆,将相之才', source: 'traditional' });
}
// 石中隐
// 左右同宫
if (has(mingStars, ['左辅', '右弼'])) {
patterns.push({ name: '左右同宫', level: '贵', desc: '辅助有力,秉性宽厚', source: 'traditional' });
}
// 魁钺相遇
if (hasAny(mingStars, ['天魁', '天钺'])) {
if (has(mingStars, ['天魁', '天钺'])) {
patterns.push({ name: '魁钺相遇', level: '贵', desc: '贵人相助,文武双全', source: 'traditional' });
}
}
// 天乙拱命
if (hasAny(sanfang, ['天魁', '天钺'])) {
patterns.push({ name: '天乙拱命', level: '贵', desc: '多贵人助,学识出众', source: 'traditional' });
}
// 羊陀夹命
if ((prevStars.includes('擎羊') && nextStars.includes('陀罗')) ||
(prevStars.includes('陀罗') && nextStars.includes('擎羊'))) {
patterns.push({ name: '羊陀夹命', level: '凶', desc: '守财奴,钱财难聚', source: 'traditional' });
}
// 火铃夹命
if ((prevStars.includes('火星') && nextStars.includes('铃星')) ||
(prevStars.includes('铃星') && nextStars.includes('火星'))) {
patterns.push({ name: '火铃夹命', level: '凶', desc: '叛逆冲动,易惹祸端', source: 'traditional' });
}
// 空劫夹命
if ((prevStars.includes('地空') && nextStars.includes('地劫')) ||
(prevStars.includes('地劫') && nextStars.includes('地空'))) {
patterns.push({ name: '空劫夹命', level: '凶', desc: '精神孤独,钱难聚', source: 'traditional' });
}
// 命无正曜
if (mingStars.length === 0) {
patterns.push({ name: '命无正曜', level: '平', desc: '可塑性高,运势受环境影响大', source: 'traditional' });
}
// 日月同宫
if (has(mingStars, ['太阳', '太阴'])) {
patterns.push({ name: '日月同宫', level: '中', desc: '贵富,妨弟兄', source: 'traditional' });
}
// 贪武同行
if (has(mingStars, ['贪狼', '武曲'])) {
patterns.push({ name: '贪武同行', level: '富', desc: '大富,奔波后成', source: 'traditional' });
}
// 三奇加会
const transNames = transforms.map(t => t.hua);
if (transNames.includes('禄') && transNames.includes('权') && transNames.includes('科')) {
patterns.push({ name: '三奇加会', level: '贵', desc: '志向远大,运气极佳', source: 'traditional' });
}
// 明珠出海
const yiPalace = palaces.find(p => p.name === '迁移');
if (yiPalace && ['太阳','太阴'].some(s => yiPalace.majorStars?.map(x => x.name).includes(s))) {
patterns.push({ name: '明珠出海', level: '富', desc: '远行得名,利学术', source: 'traditional' });
}
return patterns;
}
// ============================================================
// 合并格局(去重,知识库优先)
// ============================================================
function mergePatterns(knowledgePatterns, traditionalPatterns) {
const map = new Map();
for (const p of traditionalPatterns) {
if (!map.has(p.name)) map.set(p.name, p);
}
for (const p of knowledgePatterns) {
if (!map.has(p.name)) {
map.set(p.name, { ...p, source: 'knowledge' });
}
}
const all = Array.from(map.values());
// 按等级排序
const levelOrder = { '贵': 1, '富': 2, '中': 3, '平': 4, '变': 5, '凶': 6 };
all.sort((a, b) => (levelOrder[a.level] || 9) - (levelOrder[b.level] || 9));
return all;
}
// ============================================================
// 大运分析
// ============================================================
function analyzeDecadal(astrolabe, birthYear, birthMonth, birthDay, gender, mingIdx, palaces) {
const results = [];
const currentYear = new Date().getFullYear();
const currentAge = currentYear - birthYear;
// 计算每步大运
// 大运从命宫开始,每步大运10年
// 大运地支顺序:寅→卯→辰→巳→午→未→申→酉→戌→亥→子→丑
const branchOrder = ['寅','卯','辰','巳','午','未','申','酉','戌','亥','子','丑'];
const stemOrder = ['甲','乙','丙','丁','戊','己','庚','辛','壬','癸'];
// 命宫地支
const mingBranch = palaces[mingIdx]?.branch || '寅';
const mingBranchIdx = branchOrder.indexOf(mingBranch);
// 五虎遁起月干(简化版)
const tigerRule = { '甲': '丙', '乙': '戊', '丙': '庚', '丁': '辛', '戊': '壬', '己': '甲', '庚': '丙', '辛': '戊', '壬': '庚', '癸': '壬' };
// 计算命宫天干
// iztro的算法:命宫天干 = 五虎遁(年干)
// 这里用简化:年干对应的五虎遁月干,再结合命宫地支推算
// 获取出生年干
const yearStem = astrolabe.chineseDate.split(' ')[1]?.[0] || '甲';
const startStem = tigerRule[yearStem] || '丙';
const startStemIdx = stemOrder.indexOf(startStem);
// 命宫天干索引
const mingStemIdx = (startStemIdx + mingBranchIdx) % 10;
for (let i = 0; i < 12; i++) {
const branchIdx = (mingBranchIdx + i) % 12;
const stemIdx = (mingStemIdx + i) % 10;
const ageStart = i * 10;
const ageEnd = ageStart + 9;
const midAge = ageStart + 5;
// 检查是否当前大运
const isCurrent = currentAge >= ageStart && currentAge <= ageEnd;
// 获取大运星曜(通过horoscope)
let decadalStars = [];
let mutagen = [];
if (isCurrent) {
try {
const today = new Date();
const h = astrolabe.horoscope(today);
decadalStars = h.decadal?.stars || [];
mutagen = h.decadal?.mutagen || [];
} catch (e) { /* skip */ }
}
// 大运宫名
const palaceIdx = (mingIdx + i) % 12;
const palaceName = palaces[palaceIdx]?.name || '命宫';
const palaceBranch = palaces[palaceIdx]?.branch || branchOrder[branchIdx];
// 大运运势评估
const luckScore = evaluateDecadalLuck(palaces[palaceIdx], decadalStars, mutagen);
results.push({
index: i,
ageStart,
ageEnd,
stem: stemOrder[stemIdx],
branch: branchOrder[branchIdx],
palaceName,
palaceBranch,
isCurrent,
stars: decadalStars,
mutagen,
luck: luckScore
});
}
return results;
}
function evaluateDecadalLuck(palace, decadalStars, mutagen) {
let score = 0;
const allStars = [
...(palace?.majorStars?.map(s => s.name) || []),
...(palace?.minorStars?.map(s => s.name) || [])
];
for (const star of allStars) {
if (LUCKY_STARS.includes(star)) score += 2;
if (UNLUCKY_STARS.includes(star)) score -= 1;
}
for (const star of decadalStars) {
if (star.type === 'soft' || star.type === 'flower' || star.type === 'lucun') score += 1;
if (star.type === 'tough') score -= 0.5;
}
let level = '平常';
if (score >= 4) level = '大吉';
else if (score >= 2) level = '吉祥';
else if (score >= 0) level = '平稳';
else if (score >= -2) level = '小逆';
else level = '不顺';
return { score: +score.toFixed(1), level };
}
// ============================================================
// 流年分析
// ============================================================
function analyzeYearly(astrolabe, birthYear, birthMonth, birthDay, gender, palaces) {
const currentYear = new Date().getFullYear();
const currentMonth = new Date().getMonth() + 1;
try {
const today = new Date();
const h = astrolabe.horoscope(today);
const yearly = h.yearly;
const age = h.age;
// 流年命宫位置
const yearlyPalaceIdx = yearly?.index ?? 0;
const yearlyPalaceName = yearly?.palaceNames?.[yearlyPalaceIdx] || '命宫';
const yearlyStem = yearly?.heavenlyStem || '甲';
const yearlyBranch = yearly?.earthlyBranch || '子';
// 流年星
const yearlyStars = yearly?.stars || [];
// 流年四化
const yearlyMutagen = yearly?.mutagen || [];
// 小限
const agePalaceIdx = age?.index ?? 0;
const agePalaceName = age?.palaceNames?.[agePalaceIdx] || '命宫';
const ageStem = age?.heavenlyStem || '甲';
const ageBranch = age?.earthlyBranch || '子';
// 评估流年
const yearlyScore = evaluateYearlyLuck(yearlyStars, yearlyMutagen, palaces[yearlyPalaceIdx]);
// 未来5年流年简览
const nextYears = [];
for (let i = 0; i < 5; i++) {
const yr = currentYear + i;
try {
const date = new Date(yr + '-08-15');
const hy = astrolabe.horoscope(date);
nextYears.push({
year: yr,
stem: hy.yearly?.heavenlyStem || '',
branch: hy.yearly?.earthlyBranch || '',
palaceIdx: hy.yearly?.index || 0,
palaceName: hy.yearly?.palaceNames?.[hy.yearly?.index || 0] || ''
});
} catch (e) {
nextYears.push({ year: yr, stem: '', branch: '', palaceName: '(计算)' });
}
}
return {
current: {
year: currentYear,
stem: yearlyStem,
branch: yearlyBranch,
palaceName: yearlyPalaceName,
palaceIdx: yearlyPalaceIdx,
stars: yearlyStars,
mutagen: yearlyMutagen,
score: yearlyScore
},
age: {
nominalAge: age?.nominalAge || currentYear - birthYear,
stem: ageStem,
branch: ageBranch,
palaceName: agePalaceName,
palaceIdx: agePalaceIdx
},
nextYears
};
} catch (e) {
return { error: e.message, current: null, nextYears: [] };
}
}
function evaluateYearlyLuck(stars, mutagen, palace) {
let score = 0;
for (const star of stars) {
if (star.type === 'soft' || star.type === 'flower' || star.type === 'lucun') score += 1;
if (star.type === 'tough') score -= 1;
}
for (const m of mutagen) {
if (['禄','权','科'].includes(m)) score += 1;
if (m === '忌') score -= 1;
}
let level = '平常';
if (score >= 3) level = '大吉';
else if (score >= 1) level = '吉祥';
else if (score >= -1) level = '平稳';
else if (score >= -3) level = '小逆';
else level = '不顺';
return { score: +score.toFixed(1), level };
}
// ============================================================
// 格式输出
// ============================================================
function formatOutput(result) {
const { basic, analysis, patterns, decadal, yearly } = result;
const palaces = basic.palaces;
const mingIdx = basic.mingIdx;
const mingPalace = palaces[mingIdx];
const mingMainStars = mingPalace?.majorStars?.map(s => s.name) || [];
// 地支生肖
const branchNames = {
'子':'鼠','丑':'牛','寅':'虎','卯':'兔','辰':'龙','巳':'蛇',
'午':'马','未':'羊','申':'猴','酉':'鸡','戌':'狗','亥':'猪'
};
// 四化
const transformStr = basic.transforms.map(t => `t.star化t.hua`).join(' ');
let out = `
✨ ═══════════════════════════════════════
紫微斗数命盘 v4 · 知识库增强版
══════════════════════════════════════ ✨
📋 基本信息
出生:basic.year年basic.month月basic.day日 basic.hour:String(basic.minute).padStart(2,'0')
性别:basic.sex
生肖:basic.zodiac
八字:basic.chineseDate
五行局:basic.fiveElements
命主:basic.soul | 身主:basic.body
星座:basic.sign
🌟 命宫
位置:第mingIdx + 1宫「mingPalace?.name」
干支:mingPalace?.stemmingPalace?.branch
主星:mingMainStars.join('、') || '空宫'
长生:mingPalace?.changsheng12 || '-' | 博士:mingPalace?.boshi12 || '-'
擎羊:mingPalace?.jiangqian12 || '-' | 岁前:mingPalace?.suiqian12 || '-'
🔮 日主分析
日主:analysis.dayMaster.name
月令:analysis.monthZhi月(analysis.monthInfo.strength)
助力:analysis.helpScore分 | 压力:analysis.stressScore分
综合:analysis.strength(analysis.total分)
💊 八字用神(增强算法)
主用神:analysis.yongshen.primary
辅用神:analysis.yongshen.secondary.join('、')
说明:analysis.yongshen.summary
`;
if (analysis.yongshen.details.length > 0) {
out += ` 用神详情:\n`;
analysis.yongshen.details.forEach(d => {
out += ` · d.type:d.value — d.desc\n`;
});
}
out += `
🎯 用神喜忌
宜补:analysis.needSupport.join('、')(身analysis.strength宜)
宜避:analysis.needAvoid.join('、')
`;
if (basic.transforms.length > 0) {
out += `
🔄 四化(basic.yearStem年)
transformStr
`;
}
if (patterns.length > 0) {
out += `
🎴 命盘格局(共patterns.length个)
`;
patterns.slice(0, 15).forEach(p => {
const src = p.source === 'knowledge' ? '📚' : '📖';
out += ` src p.name(p.level)p.desc\n`;
});
if (patterns.length > 15) out += ` ...另有patterns.length - 15个格局\n`;
}
// 大运
if (decadal.length > 0) {
out += `
🔁 大运流年
当前:decadal.filter(d => d.isCurrent).map(d =>
`${d.stemd.branch(d.palaceName)d.stars.flat().filter(s=>s.name).map(s=>s.name).join('、')`
).join(' | ') || '(计算中)'}
`;
out += ` 大运一览(basic.year年起)\n`;
decadal.forEach(d => {
const cur = d.isCurrent ? '👉' : ' ';
out += ` cur d.stemd.branch · d.ageStart-d.ageEnd岁 · d.palaceName · d.luck.level(d.luck.score)\n`;
});
}
// 流年
if (yearly && yearly.current) {
out += `
📅 流年(yearly.current.year年)
干支:yearly.current.stemyearly.current.branch
流年命宫:yearly.current.palaceName
流年星:yearly.current.stars.flat().filter(s=>s.name).map(s=>'流'+s.name.replace('流','')).join('、') || '(无明显吉凶)'
流年运势:yearly.current.score.level(yearly.current.score.score)
小限:yearly.age.stemyearly.age.branch · yearly.age.palaceName(yearly.age.nominalAge岁)
`;
if (yearly.nextYears.length > 0) {
out += ` 未来五年:`;
out += yearly.nextYears.map(n => `n.year年n.stemn.branchn.palaceName`).join(' → ');
out += '\n';
}
}
out += `
📜 十二宫
`;
palaces.forEach((p, i) => {
const stars = [
...(p.majorStars?.map(s => s.name) || []),
...(p.minorStars?.map(s => s.name) || []),
...(p.adjectiveStars?.map(s => s.name) || [])
].join('、');
const isMing = i === mingIdx;
const cur = isMing ? '👉' : ' ';
const empty = stars ? '' : '(空)';
out += `cur String(i+1).padStart(2,'0').p.name p.stemp.branch starsempty\n`;
});
out += `\n═══════════════════════════════════════\n`;
return out;
}
// ============================================================
// 主入口
// ============================================================
function main() {
const args = process.argv.slice(2);
if (args.length < 2) {
console.log(`
✨ 紫微斗数命盘分析 v4(知识库增强版)
用法:
node ziwei.js <出生日期> <性别> [时间]
参数:
出生日期 YYYY-MM-DD
性别 男=1 或 女=0
时间 HH:MM(可选,默认12:00)
示例:
node ziwei.js 1995-08-15 0 12:00
node ziwei.js 1984-05-18 1
node ziwei.js 1990-05-15 0 14:30
`);
return;
}
const dateStr = args[0];
const sexArg = args[1];
const sex = sexArg === '男' ? 1 : sexArg === '女' ? 0 : parseInt(sexArg);
const timeStr = args[2] || '12:00';
const [year, month, day] = dateStr.split('-').map(Number);
const SHICHEN_MAP = {'子':0,'丑':2,'寅':4,'卯':6,'辰':8,'巳':10,'午':12,'未':14,'申':16,'酉':18,'戌':20,'亥':22};
let hour, minute;
if (SHICHEN_MAP[timeStr] !== undefined) {
hour = SHICHEN_MAP[timeStr];
minute = 0;
} else {
[hour, minute = 0] = timeStr.split(':').map(Number);
}
try {
const result = analyzePlate(year, month, day, hour, minute, sex);
console.log(formatOutput(result));
} catch (e) {
console.error('分析失败:', e.message);
console.error(e.stack);
}
}
main();