feat: IMAP mail sync with thread-to-contact matching (PRD §6.1)

Real IMAP connection via imapflow library:
- Connects to IMAP server (IMAPFlow, TLS on port 993)
- Fetches recent messages (configurable limit, default 20)
- Extracts: from, fromEmail, subject, date, snippet (200 chars)
- Thread-to-contact matching (PRD §6.1):
  1. Match sender email against persons table
  2. If no person match, extract domain and match against company.website
  3. Returns matchedCompany + matchedPerson for each email
- Audit-logged (action: crm.mailsync:imap)
- Zod validated (host, port, user, password, folder, limit)
- 502 on IMAP connection failure with detail
- 400 on validation failure

Tested: invalid creds → 502 'IMAP connection failed', missing host → 400
This commit is contained in:
Z User committed 2026-10-27 22:50:50 +00:00
1 parent 7acc0c5451
commit e45b68bb8b
1089 files changed
+260614

No files matched your search

+226
View File
@@ -0,0 +1,226 @@
/**
* dividend.ts
* 股息分析模块(适配 finance skill)
* 移植原版评分逻辑:安全评分 / CAGR / 连续增长年数 / 派息可持续性
*/
import ZAI from "z-ai-web-dev-sdk";
import { DividendAnalysis, PayoutStatus, IncomeRating } from "./types";
// ── 核心评分逻辑(与原版完全一致)──────────────────────────
function calcPayoutStatus(payoutRatio: number | null): PayoutStatus {
if (payoutRatio === null) return "unknown";
if (payoutRatio < 40) return "safe";
if (payoutRatio < 60) return "moderate";
if (payoutRatio < 80) return "high";
return "unsustainable";
}
function calcSafetyScore(data: {
payoutRatio: number | null;
dividendGrowth5y: number | null;
consecutiveYears: number | null;
dividendYield: number | null;
}): { score: number; factors: string[] } {
let score = 50;
const factors: string[] = [];
// 派息率(±20)
if (data.payoutRatio !== null) {
if (data.payoutRatio < 40) { score += 20; factors.push(`派息率健康(${data.payoutRatio.toFixed(0)}%)`); }
else if (data.payoutRatio < 60) { score += 10; factors.push(`派息率适中(${data.payoutRatio.toFixed(0)}%)`); }
else if (data.payoutRatio < 80) { score -= 10; factors.push(`派息率偏高(${data.payoutRatio.toFixed(0)}%)`); }
else { score -= 20; factors.push(`派息率不可持续(${data.payoutRatio.toFixed(0)}%)`); }
}
// 5年增长率(±15)
if (data.dividendGrowth5y !== null) {
if (data.dividendGrowth5y > 10) { score += 15; factors.push(`股息增长强劲(${data.dividendGrowth5y.toFixed(1)}% CAGR)`); }
else if (data.dividendGrowth5y > 5) { score += 10; factors.push(`股息增长良好(${data.dividendGrowth5y.toFixed(1)}% CAGR)`); }
else if (data.dividendGrowth5y > 0) { score += 5; factors.push(`股息小幅增长(${data.dividendGrowth5y.toFixed(1)}% CAGR)`); }
else { score -= 15; factors.push(`股息下降(${data.dividendGrowth5y.toFixed(1)}% CAGR)`); }
}
// 连续增长年数(±15)
if (data.consecutiveYears !== null) {
if (data.consecutiveYears >= 25) { score += 15; factors.push(`股息贵族(连续${data.consecutiveYears}年增长)`); }
else if (data.consecutiveYears >= 10) { score += 10; factors.push(`长期稳定股息(${data.consecutiveYears}年)`); }
else if (data.consecutiveYears >= 5) { score += 5; factors.push(`股息稳定(${data.consecutiveYears}年)`); }
}
// 高收益率风险(-10)
if (data.dividendYield !== null) {
if (data.dividendYield > 8) { score -= 10; factors.push(`收益率过高(${data.dividendYield.toFixed(1)}%),需核实可持续性`); }
else if (data.dividendYield < 1) { factors.push(`收益率偏低(${data.dividendYield.toFixed(2)}%)`); }
}
return { score: Math.max(0, Math.min(100, score)), factors };
}
function calcIncomeRating(safetyScore: number): IncomeRating {
if (safetyScore >= 80) return "excellent";
if (safetyScore >= 60) return "good";
if (safetyScore >= 40) return "moderate";
return "poor";
}
// ── 通过 finance skill 获取股息数据 ──────────────────────
async function fetchDividendData(ticker: string): Promise<Record<string, any>> {
const zai = await ZAI.create();
const completion = await zai.chat.completions.create({
messages: [{
role: "user",
content: `请查询 ${ticker} 的股息数据,只返回 JSON,包含:
name(公司名称), currentPrice(当前股价),
dividendYield(年化股息率%), annualDividend(年度每股股息),
trailingEps(过去12月每股收益),
exDividendDate(除权日 YYYY-MM-DD 格式),
paymentFrequency("monthly"/"quarterly"/"annual",根据派息频率判断),
dividendHistory(近5年年度股息数组,每项含 year 和 total,从新到旧排序),
consecutiveYears(连续股息增长年数,整数),
dividendGrowth5y(近5年股息 CAGR %)。
缺失字段填 null,不得捏造。`,
}],
thinking: { type: "disabled" },
});
const raw = completion.choices[0]?.message?.content ?? "{}";
return JSON.parse(raw.replace(/```json|```/g, "").trim());
}
// ── 主分析函数 ────────────────────────────────────────────
export async function analyzeDividend(ticker: string): Promise<DividendAnalysis> {
ticker = ticker.toUpperCase();
let raw: Record<string, any> = {};
try {
raw = await fetchDividendData(ticker);
} catch (err: any) {
return {
ticker, name: ticker, currentPrice: null,
dividendYield: null, annualDividend: null,
payoutRatio: null, payoutStatus: "unknown",
dividendGrowth5y: null, consecutiveYears: null,
exDividendDate: null, paymentFrequency: null,
safetyScore: 0, safetyFactors: [`数据获取失败:${err.message}`],
incomeRating: "poor", dividendHistory: [],
summary: `${ticker} 股息数据获取失败。`,
};
}
// 无股息
if (!raw.annualDividend || raw.annualDividend === 0) {
return {
ticker, name: raw.name ?? ticker,
currentPrice: raw.currentPrice ?? null,
dividendYield: null, annualDividend: null,
payoutRatio: null, payoutStatus: "no_dividend",
dividendGrowth5y: null, consecutiveYears: null,
exDividendDate: null, paymentFrequency: null,
safetyScore: 0, safetyFactors: ["该股票不派息"],
incomeRating: "no_dividend", dividendHistory: [],
summary: `${ticker} 目前不派发股息。`,
};
}
// 计算派息率
const payoutRatio = (raw.trailingEps && raw.trailingEps > 0 && raw.annualDividend)
? parseFloat(((raw.annualDividend / raw.trailingEps) * 100).toFixed(1))
: null;
const payoutStatus = calcPayoutStatus(payoutRatio);
const { score: safetyScore, factors: safetyFactors } = calcSafetyScore({
payoutRatio,
dividendGrowth5y: raw.dividendGrowth5y ?? null,
consecutiveYears: raw.consecutiveYears ?? null,
dividendYield: raw.dividendYield ?? null,
});
const incomeRating = calcIncomeRating(safetyScore);
// 生成摘要
const parts: string[] = [];
if (raw.dividendYield) parts.push(`收益率 ${Number(raw.dividendYield).toFixed(2)}%`);
if (payoutRatio) parts.push(`派息率 ${payoutRatio.toFixed(0)}%`);
if (raw.dividendGrowth5y) parts.push(`5年增长 ${Number(raw.dividendGrowth5y) > 0 ? "+" : ""}${Number(raw.dividendGrowth5y).toFixed(1)}%`);
if (raw.consecutiveYears && raw.consecutiveYears >= 5) parts.push(`连续增长 ${raw.consecutiveYears} 年`);
const ratingLabel: Record<IncomeRating, string> = {
excellent: "优秀", good: "良好", moderate: "一般", poor: "较差", no_dividend: "无股息",
};
return {
ticker,
name: raw.name ?? ticker,
currentPrice: raw.currentPrice ?? null,
dividendYield: raw.dividendYield ? Number(Number(raw.dividendYield).toFixed(2)) : null,
annualDividend: raw.annualDividend ?? null,
payoutRatio,
payoutStatus,
dividendGrowth5y: raw.dividendGrowth5y ? Number(Number(raw.dividendGrowth5y).toFixed(2)) : null,
consecutiveYears: raw.consecutiveYears ?? null,
exDividendDate: raw.exDividendDate ?? null,
paymentFrequency: raw.paymentFrequency ?? null,
safetyScore,
safetyFactors,
incomeRating,
dividendHistory: raw.dividendHistory ?? [],
summary: `${ticker}(${raw.name ?? ""}):${parts.join(",")}。评级:${ratingLabel[incomeRating]}`,
};
}
// ── 格式化输出(Markdown 仪表盘)─────────────────────────
export function formatDividendMarkdown(analysis: DividendAnalysis): string {
if (analysis.incomeRating === "no_dividend") {
return `### 💰 股息分析 · ${analysis.ticker}\n\n该股票目前不派发股息。`;
}
const ratingEmoji: Record<IncomeRating, string> = {
excellent: "🏆", good: "✅", moderate: "⚠️", poor: "❌", no_dividend: "—",
};
const payoutLabel: Record<string, string> = {
safe: "✅ 安全", moderate: "⚠️ 适中", high: "⚠️ 偏高", unsustainable: "❌ 不可持续", unknown: "暂缺",
};
let md = `### 💰 股息分析 · ${analysis.ticker}(${analysis.name})
| 指标 | 数值 |
|------|------|
| 股息收益率 | ${analysis.dividendYield ? `${analysis.dividendYield}%` : "暂缺"} |
| 年度每股股息 | ${analysis.annualDividend ? `$${analysis.annualDividend}` : "暂缺"} |
| 派息频率 | ${analysis.paymentFrequency ?? "暂缺"} |
| 除权日 | ${analysis.exDividendDate ?? "暂缺"} |
| 派息率 | ${analysis.payoutRatio ? `${analysis.payoutRatio}%(${payoutLabel[analysis.payoutStatus]})` : "暂缺"} |
| 5年股息增长 | ${analysis.dividendGrowth5y ? `${analysis.dividendGrowth5y > 0 ? "+" : ""}${analysis.dividendGrowth5y}%` : "暂缺"} |
| 连续增长年数 | ${analysis.consecutiveYears ?? "暂缺"} |
**安全评分:${analysis.safetyScore}/100 ${ratingEmoji[analysis.incomeRating]} 收入评级:${analysis.incomeRating.toUpperCase()}**
评分依据:
${analysis.safetyFactors.map((f) => `- ${f}`).join("\n")}
`;
if (analysis.dividendHistory.length > 0) {
md += `\n近年股息历史:\n`;
md += analysis.dividendHistory.slice(0, 5).map((h) => `- ${h.year}年:$${h.total}`).join("\n");
}
return md;
}
// ── 批量分析 ──────────────────────────────────────────────
export async function analyzeDividends(tickers: string[]): Promise<DividendAnalysis[]> {
const results: DividendAnalysis[] = [];
for (const ticker of tickers) {
results.push(await analyzeDividend(ticker));
await new Promise((r) => setTimeout(r, 200));
}
return results;
}