@zhin.js/plugin-lottery 1.1.9 → 1.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +8 -0
  2. package/commands/lottery/[[game]]/index.js +5 -4
  3. package/commands/lottery/[[game]]/index.ts +5 -4
  4. package/commands/lottery-backtest/[[game]]/index.js +9 -4
  5. package/commands/lottery-backtest/[[game]]/index.ts +8 -4
  6. package/commands/lottery-history/[game]/index.js +6 -4
  7. package/commands/lottery-history/[game]/index.ts +6 -4
  8. package/commands/lottery-stats/[game]/index.js +11 -6
  9. package/commands/lottery-stats/[game]/index.ts +11 -5
  10. package/commands/lottery-today/index.js +8 -4
  11. package/commands/lottery-today/index.ts +8 -3
  12. package/commands/lottery-train/[[game]]/index.js +4 -3
  13. package/commands/lottery-train/[[game]]/index.ts +4 -3
  14. package/lib/command-helpers.d.ts +2 -1
  15. package/lib/command-helpers.d.ts.map +1 -1
  16. package/lib/command-helpers.js +2 -1
  17. package/lib/command-helpers.js.map +1 -1
  18. package/lib/evaluate/backtest.d.ts +3 -2
  19. package/lib/evaluate/backtest.d.ts.map +1 -1
  20. package/lib/evaluate/backtest.js +12 -11
  21. package/lib/evaluate/backtest.js.map +1 -1
  22. package/lib/evaluate/tracker.d.ts +2 -1
  23. package/lib/evaluate/tracker.d.ts.map +1 -1
  24. package/lib/evaluate/tracker.js +9 -8
  25. package/lib/evaluate/tracker.js.map +1 -1
  26. package/lib/evaluate/train-weights.d.ts +3 -2
  27. package/lib/evaluate/train-weights.d.ts.map +1 -1
  28. package/lib/evaluate/train-weights.js +12 -11
  29. package/lib/evaluate/train-weights.js.map +1 -1
  30. package/lib/games/kl8-groups.d.ts +2 -0
  31. package/lib/games/kl8-groups.d.ts.map +1 -1
  32. package/lib/games/kl8-groups.js +2 -1
  33. package/lib/games/kl8-groups.js.map +1 -1
  34. package/lib/i18n.d.ts +2 -0
  35. package/lib/i18n.d.ts.map +1 -0
  36. package/lib/i18n.js +222 -0
  37. package/lib/i18n.js.map +1 -0
  38. package/lib/lottery-tool-handlers.d.ts +4 -3
  39. package/lib/lottery-tool-handlers.d.ts.map +1 -1
  40. package/lib/lottery-tool-handlers.js +12 -7
  41. package/lib/lottery-tool-handlers.js.map +1 -1
  42. package/lib/pipeline.d.ts +3 -1
  43. package/lib/pipeline.d.ts.map +1 -1
  44. package/lib/pipeline.js +9 -7
  45. package/lib/pipeline.js.map +1 -1
  46. package/lib/recommend/game-pick.d.ts +4 -2
  47. package/lib/recommend/game-pick.d.ts.map +1 -1
  48. package/lib/recommend/game-pick.js +39 -28
  49. package/lib/recommend/game-pick.js.map +1 -1
  50. package/lib/recommend/report.d.ts +4 -2
  51. package/lib/recommend/report.d.ts.map +1 -1
  52. package/lib/recommend/report.js +17 -10
  53. package/lib/recommend/report.js.map +1 -1
  54. package/lib/runtime-state.d.ts +2 -0
  55. package/lib/runtime-state.d.ts.map +1 -1
  56. package/lib/runtime-state.js.map +1 -1
  57. package/lib/stats/pro-analysis.d.ts +3 -2
  58. package/lib/stats/pro-analysis.d.ts.map +1 -1
  59. package/lib/stats/pro-analysis.js +10 -9
  60. package/lib/stats/pro-analysis.js.map +1 -1
  61. package/lib/sync/run-sync.d.ts +2 -1
  62. package/lib/sync/run-sync.d.ts.map +1 -1
  63. package/lib/sync/run-sync.js +4 -3
  64. package/lib/sync/run-sync.js.map +1 -1
  65. package/lib/types.d.ts +2 -0
  66. package/lib/types.d.ts.map +1 -1
  67. package/lib/types.js.map +1 -1
  68. package/package.json +8 -8
  69. package/plugin.js +4 -0
  70. package/skills/lottery/tools/compute-recommend/index.js +1 -1
  71. package/skills/lottery/tools/compute-recommend/index.ts +1 -1
  72. package/skills/lottery/tools/history/index.js +2 -1
  73. package/skills/lottery/tools/history/index.ts +2 -1
  74. package/skills/lottery/tools/save-prediction/index.js +1 -1
  75. package/skills/lottery/tools/save-prediction/index.ts +1 -1
  76. package/skills/lottery/tools/stats-snapshot/index.js +1 -1
  77. package/skills/lottery/tools/stats-snapshot/index.ts +1 -1
  78. package/skills/lottery/tools/sync/index.js +1 -1
  79. package/skills/lottery/tools/sync/index.ts +1 -0
  80. package/src/command-helpers.ts +3 -0
  81. package/src/evaluate/backtest.ts +13 -11
  82. package/src/evaluate/tracker.ts +10 -8
  83. package/src/evaluate/train-weights.ts +13 -11
  84. package/src/games/kl8-groups.ts +4 -1
  85. package/src/i18n.ts +222 -0
  86. package/src/lottery-tool-handlers.ts +12 -7
  87. package/src/pipeline.ts +10 -7
  88. package/src/recommend/game-pick.ts +40 -28
  89. package/src/recommend/report.ts +18 -8
  90. package/src/runtime-state.ts +2 -0
  91. package/src/stats/pro-analysis.ts +11 -7
  92. package/src/sync/run-sync.ts +5 -2
  93. package/src/types.ts +2 -0
package/src/i18n.ts ADDED
@@ -0,0 +1,222 @@
1
+ import { createMessages } from 'zhin.js/i18n';
2
+
3
+ export const messages = createMessages({
4
+ dbNotReady: { 'zh-CN': '数据库未就绪', en: 'Database is not ready.' },
5
+ syncResult: { 'zh-CN': '同步 {result}', en: 'Sync {result}' },
6
+ reviewTitle: { 'zh-CN': '【彩票推荐复盘】', en: '[Lottery recommendation review]' },
7
+ reviewModel: { 'zh-CN': '■ {game} 模型 {weights}', en: '■ {game} model {weights}' },
8
+ reviewRate: { 'zh-CN': ' 累计 {count} 期 平均命中 {rate}%', en: ' {count} draws reviewed; average hit rate {rate}%' },
9
+ reviewEmpty: { 'zh-CN': ' 尚无复盘数据', en: ' No reviews yet.' },
10
+ reviewDraw: { 'zh-CN': ' 第{issue}期 {detail} ({rate}%)', en: ' Draw {issue}: {detail} ({rate}%)' },
11
+ agentMemory: { 'zh-CN': '【Agent 记忆】', en: '[Agent memory]' },
12
+ pickLine: { 'zh-CN': '■ {label}:{numbers}', en: '■ {label}: {numbers}' },
13
+ noPicks: { 'zh-CN': '暂无推荐号码,请确认已同步开奖数据。', en: 'No picks available. Make sure draw results have been synchronized.' },
14
+ noReport: { 'zh-CN': '今日尚无推荐,请等待定时任务。', en: 'No report for today yet. Wait for the scheduled run.' },
15
+ noReportCommand: { 'zh-CN': '今日尚无推荐,可执行 {command} 或等待定时任务。', en: 'No report for today yet. Run {command} or wait for the scheduled run.' },
16
+ gameRequired: { 'zh-CN': '请指定玩法', en: 'Specify a lottery game.' },
17
+ invalidGame: { 'zh-CN': '无效的玩法', en: 'Invalid lottery game.' },
18
+ noData: { 'zh-CN': '暂无数据', en: 'No data available.' },
19
+ syncRequired: { 'zh-CN': '历史数据不足,请先同步开奖。', en: 'Not enough history. Synchronize draw results first.' },
20
+ syncCommand: { 'zh-CN': '历史数据不足,请先执行 {command} 同步开奖。', en: 'Not enough history. Run {command} to synchronize draw results first.' },
21
+ sample: { 'zh-CN': '样本 {count} 期', en: 'Sample: {count} draws' },
22
+ "zones": {
23
+ "zh-CN": "三区 {zones}",
24
+ "en": "Zones {zones}"
25
+ },
26
+ "oddEven": {
27
+ "zh-CN": "奇偶 {odd}:{even}",
28
+ "en": "Odd/even {odd}:{even}"
29
+ },
30
+ "bigSmall": {
31
+ "zh-CN": "大小 {big}:{small}",
32
+ "en": "High/low {big}:{small}"
33
+ },
34
+ "recentHot": {
35
+ "zh-CN": "近10期热号 {numbers}",
36
+ "en": "Hot numbers in last 10 draws: {numbers}"
37
+ },
38
+ "weights": {
39
+ "zh-CN": "权重 {weights}",
40
+ "en": "Weights {weights}"
41
+ },
42
+ "digitPos": {
43
+ "zh-CN": "第{position}位[{numbers}]",
44
+ "en": "Position {position} [{numbers}]"
45
+ },
46
+ "strategy.balanced": {
47
+ "zh-CN": "均衡",
48
+ "en": "Balanced"
49
+ },
50
+ "strategy.hot": {
51
+ "zh-CN": "热号",
52
+ "en": "Hot"
53
+ },
54
+ "strategy.cold": {
55
+ "zh-CN": "冷号",
56
+ "en": "Cold"
57
+ },
58
+ "strategy.trend": {
59
+ "zh-CN": "趋势",
60
+ "en": "Trend"
61
+ },
62
+ "pickLabel": {
63
+ "zh-CN": "{name}(选{count})",
64
+ "en": "{name} (pick {count})"
65
+ },
66
+ "pickGroupsLabel": {
67
+ "zh-CN": "{name}(选{count}·{groups}组)",
68
+ "en": "{name} (pick {count} · {groups} groups)"
69
+ },
70
+ "basicDetail": {
71
+ "zh-CN": "频率+遗漏+趋势 | 和值{sum} 跨度{span}",
72
+ "en": "Frequency + omission + trend | Sum {sum}, span {span}"
73
+ },
74
+ "multiDetail": {
75
+ "zh-CN": "多策略 {strategies} | 和值{sum} 跨度{span}",
76
+ "en": "Strategies {strategies} | Sum {sum}, span {span}"
77
+ },
78
+ "redDetail": {
79
+ "zh-CN": "红球 {oddEven} {bigSmall} | 蓝 {blue}",
80
+ "en": "Red {oddEven} {bigSmall} | Blue {blue}"
81
+ },
82
+ "frontDetail": {
83
+ "zh-CN": "前区 {oddEven} {zones}",
84
+ "en": "Front {oddEven} {zones}"
85
+ },
86
+ "digitDetail": {
87
+ "zh-CN": "直选 {positions} | 和值{sum}",
88
+ "en": "Straight {positions} | Sum {sum}"
89
+ },
90
+ "groupLine": {
91
+ "zh-CN": " 组{index}·{label}:{numbers}",
92
+ "en": " Group {index} · {label}: {numbers}"
93
+ },
94
+ "redLine": {
95
+ "zh-CN": "■ {label}:红 {red} + 蓝 {blue}",
96
+ "en": "■ {label}: Red {red} + Blue {blue}"
97
+ },
98
+ "frontLine": {
99
+ "zh-CN": "■ {label}:前 {front} + 后 {back}",
100
+ "en": "■ {label}: Front {front} + Back {back}"
101
+ },
102
+ "hot": {
103
+ "zh-CN": "热号 {numbers}",
104
+ "en": "Hot {numbers}"
105
+ },
106
+ "cold": {
107
+ "zh-CN": "冷号 {numbers}",
108
+ "en": "Cold {numbers}"
109
+ },
110
+ "accuracy": {
111
+ "zh-CN": "历史命中 {rate}% ({count}期) 近5期 {recent}",
112
+ "en": "Historical hit rate {rate}% ({count} draws), last 5: {recent}"
113
+ },
114
+ "game.kl8": {
115
+ "zh-CN": "快乐8",
116
+ "en": "Happy 8"
117
+ },
118
+ "game.ssq": {
119
+ "zh-CN": "双色球",
120
+ "en": "Double Color Ball"
121
+ },
122
+ "game.dlt": {
123
+ "zh-CN": "超级大乐透",
124
+ "en": "Super Lotto"
125
+ },
126
+ "game.fc3d": {
127
+ "zh-CN": "福彩3D",
128
+ "en": "Fucai 3D"
129
+ },
130
+ "game.pl3": {
131
+ "zh-CN": "排列3",
132
+ "en": "Pick 3"
133
+ },
134
+ "game.pl5": {
135
+ "zh-CN": "排列5",
136
+ "en": "Pick 5"
137
+ },
138
+ "dailyTitle": {
139
+ "zh-CN": "【彩票每日推荐】{date}",
140
+ "en": "[Daily lottery report] {date}"
141
+ },
142
+ "weightFallback": {
143
+ "zh-CN": "【权重保护】近端 holdout 调优未跑赢随机,已回退默认 F40/O35/T25:{games}",
144
+ "en": "[Weight safeguard] Recent holdout tuning did not beat random; reverted to F40/O35/T25: {games}"
145
+ },
146
+ "accuracyOverview": {
147
+ "zh-CN": "【模型复盘】累计平均命中 {overview}",
148
+ "en": "[Model review] Cumulative average hit rate: {overview}"
149
+ },
150
+ "statsExplanation": {
151
+ "zh-CN": "【统计说明】号码由频率+遗漏+近期趋势综合打分,并根据历史命中自动调权。",
152
+ "en": "[Method] Numbers are scored by frequency, omission and recent trends; weights adapt to historical hits."
153
+ },
154
+ "disclaimer": {
155
+ "zh-CN": "免责声明:本推荐由历史数据统计生成,仅供参考,不构成投注建议,不保证中奖。请理性购彩。",
156
+ "en": "Disclaimer: These picks are generated from historical statistics for reference only, not betting advice or a guarantee of winning. Play responsibly."
157
+ },
158
+ "backtestAdaptive": {
159
+ "zh-CN": "{game}: 调优 {rate}%",
160
+ "en": "{game}: adaptive {rate}%"
161
+ },
162
+ "backtestFixed": {
163
+ "zh-CN": "固定 {rate}%",
164
+ "en": "fixed {rate}%"
165
+ },
166
+ "backtestRandom": {
167
+ "zh-CN": "随机 {rate}%",
168
+ "en": "random {rate}%"
169
+ },
170
+ "backtestDelta": {
171
+ "zh-CN": "(Δ调优-随机 {delta}%, n={count}, 热身{warmup}期)",
172
+ "en": "(Δ adaptive-random {delta}%, n={count}, warmup {warmup} draws)"
173
+ },
174
+ "finalWeights": {
175
+ "zh-CN": "终权重 {weights}",
176
+ "en": "final weights {weights}"
177
+ },
178
+ "backtestTitle": {
179
+ "zh-CN": "【回测可信度】近 N 期 walk-forward 抽样",
180
+ "en": "[Backtest confidence] Walk-forward sampling of the latest N draws"
181
+ },
182
+ "backtestNote": {
183
+ "zh-CN": "说明:Δ 接近 0 为正常;终权重见 lottery_model_weights。",
184
+ "en": "Note: Δ near zero is normal; final weights are stored in lottery_model_weights."
185
+ },
186
+ "backtestFallback": {
187
+ "zh-CN": "推荐时若近端调优输给随机,将自动回退默认 F40/O35/T25(见【权重保护】)。",
188
+ "en": "If recent tuning underperforms random, recommendations revert to F40/O35/T25 (see Weight safeguard)."
189
+ },
190
+ "trainFallback": {
191
+ "zh-CN": " [holdout→默认]",
192
+ "en": " [holdout→default]"
193
+ },
194
+ "trainGame": {
195
+ "zh-CN": "{game}: 训练{steps}期 ({first}→{last}){fallback}",
196
+ "en": "{game}: trained on {steps} draws ({first}→{last}){fallback}"
197
+ },
198
+ "trainHit": {
199
+ "zh-CN": "命中 调优{adaptive}% / 固定{fixed}% / 随机{random}%",
200
+ "en": "Hit rate: adaptive {adaptive}% / fixed {fixed}% / random {random}%"
201
+ },
202
+ "trainDelta": {
203
+ "zh-CN": "Δ调优-随机 {delta}%",
204
+ "en": "Δ adaptive-random {delta}%"
205
+ },
206
+ "trainWeights": {
207
+ "zh-CN": "权重 {initial} → {final}",
208
+ "en": "Weights {initial} → {final}"
209
+ },
210
+ "trainEmpty": {
211
+ "zh-CN": "训练失败:历史数据不足,请先同步开奖。",
212
+ "en": "Training failed: not enough history. Synchronize draw results first."
213
+ },
214
+ "trainTitle": {
215
+ "zh-CN": "【权重训练】全历史 walk-forward:预测→比对→调权,已写入 lottery_model_weights",
216
+ "en": "[Weight training] Full-history walk-forward: predict → compare → tune; saved to lottery_model_weights"
217
+ },
218
+ "trainNote": {
219
+ "zh-CN": "说明:训练≠保证未来更准;终权重供后续推荐使用。",
220
+ "en": "Note: training does not guarantee better future results. Final weights are used for later recommendations."
221
+ }
222
+ });
@@ -1,3 +1,5 @@
1
+ import { resolveLocale, type Locale } from 'zhin.js/i18n';
2
+ import { messages as t } from './i18n.js';
1
3
  import type { GameId } from './types.js';
2
4
 
3
5
  import { parseGameId } from './games/registry.js';
@@ -25,14 +27,15 @@ export async function handleGetModelState(runtime: LotteryRuntime, game?: string
25
27
  return JSON.stringify(out);
26
28
  }
27
29
 
28
- export async function handleComputeRecommend(runtime: LotteryRuntime, game: string): Promise<string> {
30
+ export async function handleComputeRecommend(runtime: LotteryRuntime, game: string, locale: Locale = resolveLocale(runtime.locale)): Promise<string> {
29
31
  const gid = parseGameId(game);
30
- if (!gid) return 'invalid game';
32
+ if (!gid) return t(locale, 'invalidGame');
31
33
  const { db, config: cfg } = runtime;
32
34
  const kl8 = lotteryKl8(cfg);
33
35
  const draws = await loadDraws(db, gid, cfg.historyLimit);
34
- if (!draws.length) return 'no data; run the lottery draw synchronization tool first';
36
+ if (!draws.length) return t(locale, 'syncRequired');
35
37
  const pick = recommendGame(gid, draws, {
38
+ locale,
36
39
  pickCount: gid === 'kl8' ? kl8.pickCount : cfg.pickCount,
37
40
  tieSeed: new Date().toISOString().slice(0, 10),
38
41
  weights: await loadGameWeights(db, gid),
@@ -42,13 +45,14 @@ export async function handleComputeRecommend(runtime: LotteryRuntime, game: stri
42
45
  return JSON.stringify(pick);
43
46
  }
44
47
 
45
- export async function handleSavePrediction(runtime: LotteryRuntime, game: string, numbersJson: string): Promise<string> {
48
+ export async function handleSavePrediction(runtime: LotteryRuntime, game: string, numbersJson: string, locale: Locale = resolveLocale(runtime.locale)): Promise<string> {
46
49
  const gid = parseGameId(game);
47
- if (!gid) return 'invalid game';
50
+ if (!gid) return t(locale, 'invalidGame');
48
51
  const { db } = runtime;
49
52
  const numbers = JSON.parse(numbersJson);
50
53
  const weights = await loadGameWeights(db, gid);
51
54
  await savePrediction(db, {
55
+ locale,
52
56
  gameId: gid,
53
57
  label: gid,
54
58
  numbers,
@@ -64,13 +68,14 @@ export async function handleListPending(runtime: LotteryRuntime, game?: string):
64
68
  return JSON.stringify(await listPendingPredictions(db, gid ?? undefined));
65
69
  }
66
70
 
67
- export async function handleStatsSnapshot(runtime: LotteryRuntime, game: string): Promise<string> {
71
+ export async function handleStatsSnapshot(runtime: LotteryRuntime, game: string, locale: Locale = resolveLocale(runtime.locale)): Promise<string> {
68
72
  const gid = parseGameId(game);
69
- if (!gid) return 'invalid game';
73
+ if (!gid) return t(locale, 'invalidGame');
70
74
  const { db, config: cfg } = runtime;
71
75
  const kl8 = lotteryKl8(cfg);
72
76
  const draws = await loadDraws(db, gid, Math.min(30, cfg.historyLimit));
73
77
  const pick = recommendGame(gid, draws, {
78
+ locale,
74
79
  pickCount: gid === 'kl8' ? kl8.pickCount : cfg.pickCount,
75
80
  tieSeed: 'snapshot',
76
81
  weights: await loadGameWeights(db, gid),
package/src/pipeline.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { resolveLocale, type Locale } from 'zhin.js/i18n';
2
+ import { messages as t } from './i18n.js';
1
3
  import type { GameId } from './types.js';
2
4
  import { syncGame, type LotteryDb } from './db.js';
3
5
 
@@ -19,6 +21,7 @@ import { cancelTodayPendingPredictions } from './evaluate/tracker.js';
19
21
  import type { Kl8Config } from './games/kl8-groups.js';
20
22
 
21
23
  export interface PipelineDeps {
24
+ locale?: Locale;
22
25
  db: LotteryDb;
23
26
  enabledGames: () => GameId[];
24
27
  historyLimit: number;
@@ -57,7 +60,7 @@ async function attachBacktestOverview(
57
60
  const summaries = holdoutSummaries.length
58
61
  ? holdoutSummaries
59
62
  : await runBacktestForGames(db, gameIds, backtestOpts(deps));
60
- return formatBacktestSection(summaries);
63
+ return formatBacktestSection(summaries, deps.locale);
61
64
  }
62
65
 
63
66
  export interface PipelineOptions {
@@ -128,11 +131,11 @@ export async function runLotteryPipeline(
128
131
  let recommendText: string;
129
132
 
130
133
  if (!needPredict.length) {
131
- const existing = await resolveTodayReport(db, gameIds);
134
+ const existing = await resolveTodayReport(db, gameIds, new Date(), deps.locale);
132
135
  if (existing) {
133
136
  recommendText = 'reused today picks';
134
137
  existing.weightFallbackGames = weightPlan.fallbacks.length ? weightPlan.fallbacks : undefined;
135
- existing.backtestOverview = await attachBacktestOverview(db, gameIds, deps, holdoutSummaries);
138
+ existing.backtestOverview = await attachBacktestOverview(db, gameIds, { ...deps, locale: resolveLocale(existing.locale) }, holdoutSummaries);
136
139
  reportText = formatDailyReportText(existing, '');
137
140
  } else {
138
141
  recommendText = 'skipped (no picks available)';
@@ -141,7 +144,7 @@ export async function runLotteryPipeline(
141
144
  const report = await buildDailyReport(
142
145
  db,
143
146
  needPredict,
144
- { pickCount: deps.pickCount, historyLimit: deps.historyLimit, kl8: deps.kl8 },
147
+ { pickCount: deps.pickCount, historyLimit: deps.historyLimit, kl8: deps.kl8, locale: deps.locale },
145
148
  new Date(),
146
149
  true,
147
150
  weightPlan.overrides,
@@ -150,7 +153,7 @@ export async function runLotteryPipeline(
150
153
  report.backtestOverview = await attachBacktestOverview(db, gameIds, deps, holdoutSummaries);
151
154
 
152
155
  if (needPredict.length < gameIds.length) {
153
- const existing = await resolveTodayReport(db, gameIds);
156
+ const existing = await resolveTodayReport(db, gameIds, new Date(), deps.locale);
154
157
  if (existing?.picks?.length) {
155
158
  const regen = new Set(needPredict);
156
159
  report.picks = [
@@ -184,7 +187,7 @@ export async function runLotteryPipeline(
184
187
  };
185
188
  }
186
189
 
187
- export function formatPipelineReply(result: PipelineStepResult): string {
190
+ export function formatPipelineReply(result: PipelineStepResult, locale?: Locale): string {
188
191
  if (result.reportText.trim()) return result.reportText.trim();
189
- return '暂无推荐号码,请确认已同步开奖数据。';
192
+ return t(locale, 'noPicks');
190
193
  }
@@ -1,3 +1,5 @@
1
+ import { resolveLocale, type Locale } from 'zhin.js/i18n';
2
+ import { messages as t } from '../i18n.js';
1
3
  import { type DrawNumbers, type GameId, type GamePick, type NormalizedDraw, type ScoreWeights, type StatsSnapshot, type AccuracySnapshot } from '../types.js';
2
4
  import { getGameMeta } from '../games/registry.js';
3
5
  import { recommendKl8Groups, resolveKl8Config, type Kl8Config } from '../games/kl8-groups.js';
@@ -30,6 +32,7 @@ function poolDraws(draws: NormalizedDraw[], extract: (n: DrawNumbers) => number[
30
32
  }
31
33
 
32
34
  export interface RecommendOptions {
35
+ locale?: Locale;
33
36
  pickCount: number;
34
37
  tieSeed: string;
35
38
  weights?: ScoreWeights;
@@ -42,7 +45,9 @@ export function recommendGame(
42
45
  draws: NormalizedDraw[],
43
46
  options: RecommendOptions,
44
47
  ): GamePick {
48
+ const locale = resolveLocale(options.locale);
45
49
  const meta = getGameMeta(gameId);
50
+ const name = locale === 'en' ? t(locale, `game.${gameId}`) : meta.name;
46
51
  const tieSeed = `${options.tieSeed}:${gameId}`;
47
52
  const weights = options.weights ?? { freq: 0.4, omit: 0.35, trend: 0.25 };
48
53
 
@@ -55,10 +60,11 @@ export function recommendGame(
55
60
  const pick = pickTopUnique(scores, kl8.pickCount);
56
61
  const hot = scores.slice(0, 5).map((s) => s.value);
57
62
  const cold = [...scores].reverse().slice(0, 5).map((s) => s.value);
58
- const pro = computeProMetrics(gameId, pick, draws, weights, 1, 80, 40);
63
+ const pro = computeProMetrics(gameId, pick, draws, weights, 1, 80, 40, locale);
59
64
  return {
60
65
  gameId,
61
- label: `${meta.name}(选${pick.length})`,
66
+ locale,
67
+ label: t(locale, 'pickLabel', { name, count: pick.length }),
62
68
  numbers: { main: pick },
63
69
  weights,
64
70
  stats: buildSnapshot(
@@ -66,7 +72,7 @@ export function recommendGame(
66
72
  draws,
67
73
  hot,
68
74
  cold,
69
- `频率+遗漏+趋势 | 和值${pro.sum} 跨度${pro.span}`,
75
+ t(locale, 'basicDetail', { sum: pro.sum, span: pro.span }),
70
76
  pro,
71
77
  options.accuracy,
72
78
  weights,
@@ -79,15 +85,17 @@ export function recommendGame(
79
85
  strategies: kl8.groupStrategies,
80
86
  tieSeed,
81
87
  baseWeights: weights,
88
+ locale,
82
89
  });
83
90
  const primary = multi.groups[0]?.numbers ?? [];
84
91
  const hot = multi.scores.slice(0, 5).map((s) => s.value);
85
92
  const cold = [...multi.scores].reverse().slice(0, 5).map((s) => s.value);
86
- const pro = computeProMetrics(gameId, primary, draws, weights, 1, 80, 40);
93
+ const pro = computeProMetrics(gameId, primary, draws, weights, 1, 80, 40, locale);
87
94
  const strategyNote = multi.groups.map((g) => g.label).join('/');
88
95
  return {
89
96
  gameId,
90
- label: `${meta.name}(选${kl8.pickCount}·${multi.groups.length}组)`,
97
+ locale,
98
+ label: t(locale, 'pickGroupsLabel', { name, count: kl8.pickCount, groups: multi.groups.length }),
91
99
  numbers: { main: primary },
92
100
  weights,
93
101
  kl8Groups: multi.groups,
@@ -96,7 +104,7 @@ export function recommendGame(
96
104
  draws,
97
105
  hot,
98
106
  cold,
99
- `多策略 ${strategyNote} | 和值${pro.sum} 跨度${pro.span}`,
107
+ t(locale, 'multiDetail', { strategies: strategyNote, sum: pro.sum, span: pro.span }),
100
108
  pro,
101
109
  options.accuracy,
102
110
  weights,
@@ -110,10 +118,11 @@ export function recommendGame(
110
118
  const blueScores = scorePool(blueRows, 1, 16, `${tieSeed}:blue`, weights);
111
119
  const red = pickTopUnique(redScores, 6);
112
120
  const blue = pickTopUnique(blueScores, 1);
113
- const pro = computeProMetrics(gameId, red, draws, weights, 1, 33, 17);
121
+ const pro = computeProMetrics(gameId, red, draws, weights, 1, 33, 17, locale);
114
122
  return {
115
123
  gameId,
116
- label: meta.name,
124
+ locale,
125
+ label: name,
117
126
  numbers: { red, blue },
118
127
  weights,
119
128
  stats: buildSnapshot(
@@ -121,7 +130,7 @@ export function recommendGame(
121
130
  draws,
122
131
  redScores.slice(0, 5).map((s) => s.value),
123
132
  redScores.slice(-5).map((s) => s.value),
124
- `红球 ${pro.oddEven} ${pro.bigSmall} | 蓝 ${blue.map(pad2).join(' ')}`,
133
+ t(locale, 'redDetail', { oddEven: pro.oddEven, bigSmall: pro.bigSmall, blue: blue.map(pad2).join(' ') }),
125
134
  pro,
126
135
  options.accuracy,
127
136
  weights,
@@ -135,10 +144,11 @@ export function recommendGame(
135
144
  const backScores = scorePool(backRows, 1, 12, `${tieSeed}:back`, weights);
136
145
  const front = pickTopUnique(frontScores, 5);
137
146
  const back = pickTopUnique(backScores, 2);
138
- const pro = computeProMetrics(gameId, front, draws, weights, 1, 35, 18);
147
+ const pro = computeProMetrics(gameId, front, draws, weights, 1, 35, 18, locale);
139
148
  return {
140
149
  gameId,
141
- label: meta.name,
150
+ locale,
151
+ label: name,
142
152
  numbers: { front, back },
143
153
  weights,
144
154
  stats: buildSnapshot(
@@ -146,7 +156,7 @@ export function recommendGame(
146
156
  draws,
147
157
  frontScores.slice(0, 5).map((s) => s.value),
148
158
  frontScores.slice(-5).map((s) => s.value),
149
- `前区 ${pro.oddEven} ${pro.zones ?? ''}`.trim(),
159
+ t(locale, 'frontDetail', { oddEven: pro.oddEven, zones: pro.zones ?? '' }).trim(),
150
160
  pro,
151
161
  options.accuracy,
152
162
  weights,
@@ -157,10 +167,11 @@ export function recommendGame(
157
167
  case 'pl3': {
158
168
  const rows = poolDraws(draws, (n) => n.digits ?? []);
159
169
  const digits = scoreDigits(rows, 3, tieSeed, weights);
160
- const pro = computeDigitProMetrics(digits, draws, weights);
170
+ const pro = computeDigitProMetrics(digits, draws, weights, locale);
161
171
  return {
162
172
  gameId,
163
- label: meta.name,
173
+ locale,
174
+ label: name,
164
175
  numbers: { digits },
165
176
  weights,
166
177
  stats: buildSnapshot(
@@ -168,7 +179,7 @@ export function recommendGame(
168
179
  draws,
169
180
  digits,
170
181
  [],
171
- `直选 ${pro.digitPos} | 和值${pro.sum}`,
182
+ t(locale, 'digitDetail', { positions: pro.digitPos ?? '', sum: pro.sum }),
172
183
  pro,
173
184
  options.accuracy,
174
185
  weights,
@@ -178,10 +189,11 @@ export function recommendGame(
178
189
  case 'pl5': {
179
190
  const rows = poolDraws(draws, (n) => n.digits ?? []);
180
191
  const digits = scoreDigits(rows, 5, tieSeed, weights);
181
- const pro = computeDigitProMetrics(digits, draws, weights);
192
+ const pro = computeDigitProMetrics(digits, draws, weights, locale);
182
193
  return {
183
194
  gameId,
184
- label: meta.name,
195
+ locale,
196
+ label: name,
185
197
  numbers: { digits },
186
198
  weights,
187
199
  stats: buildSnapshot(
@@ -189,7 +201,7 @@ export function recommendGame(
189
201
  draws,
190
202
  digits,
191
203
  [],
192
- `直选 ${pro.digitPos} | 和值${pro.sum}`,
204
+ t(locale, 'digitDetail', { positions: pro.digitPos ?? '', sum: pro.sum }),
193
205
  pro,
194
206
  options.accuracy,
195
207
  weights,
@@ -201,36 +213,36 @@ export function recommendGame(
201
213
  }
202
214
  }
203
215
 
204
- export function formatPickLine(pick: GamePick): string {
216
+ export function formatPickLine(pick: GamePick, locale: Locale = resolveLocale(pick.locale)): string {
205
217
  const n = pick.numbers;
206
218
  switch (pick.gameId) {
207
219
  case 'kl8': {
208
220
  if (pick.kl8Groups?.length) {
209
221
  const header = `■ ${pick.label}`;
210
222
  const rows = pick.kl8Groups.map(
211
- (g) => ` 组${g.index}·${g.label}:${g.numbers.map(pad2).join(' ')}`,
223
+ (g) => t(locale, 'groupLine', { index: g.index, label: g.label, numbers: g.numbers.map(pad2).join(' ') }),
212
224
  );
213
225
  return [header, ...rows].join('\n');
214
226
  }
215
- return `■ ${pick.label}:${(n.main ?? []).map(pad2).join(' ')}`;
227
+ return t(locale, 'pickLine', { label: pick.label, numbers: (n.main ?? []).map(pad2).join(' ') });
216
228
  }
217
229
  case 'ssq':
218
- return `■ ${pick.label}:红 ${(n.red ?? []).map(pad2).join(' ')} + 蓝 ${(n.blue ?? []).map(pad2).join(' ')}`;
230
+ return t(locale, 'redLine', { label: pick.label, red: (n.red ?? []).map(pad2).join(' '), blue: (n.blue ?? []).map(pad2).join(' ') });
219
231
  case 'dlt':
220
- return `■ ${pick.label}:前 ${(n.front ?? []).map(pad2).join(' ')} + 后 ${(n.back ?? []).map(pad2).join(' ')}`;
232
+ return t(locale, 'frontLine', { label: pick.label, front: (n.front ?? []).map(pad2).join(' '), back: (n.back ?? []).map(pad2).join(' ') });
221
233
  case 'fc3d':
222
234
  case 'pl3':
223
235
  case 'pl5':
224
- return `■ ${pick.label}:${(n.digits ?? []).join(' ')}`;
236
+ return t(locale, 'pickLine', { label: pick.label, numbers: (n.digits ?? []).join(' ') });
225
237
  default:
226
238
  return `■ ${pick.label}`;
227
239
  }
228
240
  }
229
241
 
230
- export function formatPickStats(pick: GamePick): string {
242
+ export function formatPickStats(pick: GamePick, locale: Locale = resolveLocale(pick.locale)): string {
231
243
  const parts: string[] = [];
232
- const hot = pick.stats.hot.length ? `热号 ${pick.stats.hot.join(',')}` : '';
233
- const cold = pick.stats.cold.length ? `冷号 ${pick.stats.cold.join(',')}` : '';
244
+ const hot = pick.stats.hot.length ? t(locale, 'hot', { numbers: pick.stats.hot.join(',') }) : '';
245
+ const cold = pick.stats.cold.length ? t(locale, 'cold', { numbers: pick.stats.cold.join(',') }) : '';
234
246
  if (hot) parts.push(hot);
235
247
  if (cold) parts.push(cold);
236
248
  if (pick.stats.pro) {
@@ -241,7 +253,7 @@ export function formatPickStats(pick: GamePick): string {
241
253
  }
242
254
  if (pick.stats.accuracy) {
243
255
  const a = pick.stats.accuracy;
244
- parts.push(`历史命中 ${(a.avgHitRate * 100).toFixed(1)}% (${a.evalCount}期) 近5期 ${a.recent5}`);
256
+ parts.push(t(locale, 'accuracy', { rate: (a.avgHitRate * 100).toFixed(1), count: a.evalCount, recent: a.recent5 }));
245
257
  }
246
258
  parts.push(pick.stats.detail);
247
259
  return ` ${parts.filter(Boolean).join('\n ')}`;
@@ -1,4 +1,6 @@
1
- import { DEFAULT_WEIGHTS, DISCLAIMER, type DailyReport, type DrawNumbers, type GameId, type GamePick, type ScoreWeights } from '../types.js';
1
+ import { resolveLocale, type Locale } from 'zhin.js/i18n';
2
+ import { messages as t } from '../i18n.js';
3
+ import { DEFAULT_WEIGHTS, type DailyReport, type DrawNumbers, type GameId, type GamePick, type ScoreWeights } from '../types.js';
2
4
 
3
5
  import { recommendGame, formatPickLine, formatPickStats } from './game-pick.js';
4
6
  import { resolveKl8Config, type Kl8Config } from '../games/kl8-groups.js';
@@ -8,6 +10,7 @@ import { loadDraws, REPORTS_TABLE, parseStoredJson, type LotteryDb } from '../db
8
10
  import { listPendingPredictions, loadAccuracySnapshot, loadGameWeights, savePrediction } from '../evaluate/tracker.js';
9
11
 
10
12
  export interface LotteryConfigSlice {
13
+ locale?: Locale;
11
14
  pickCount: number;
12
15
  historyLimit: number;
13
16
  kl8?: Kl8Config;
@@ -31,6 +34,7 @@ export async function buildDailyReport(
31
34
  const accuracy = await loadAccuracySnapshot(db, gameId);
32
35
  const kl8 = config.kl8 ?? resolveKl8Config(config.pickCount);
33
36
  const pick = recommendGame(gameId, draws, {
37
+ locale: config.locale,
34
38
  pickCount: gameId === 'kl8' ? kl8.pickCount : config.pickCount,
35
39
  tieSeed,
36
40
  weights,
@@ -46,28 +50,30 @@ export async function buildDailyReport(
46
50
  .join(' | ');
47
51
  return {
48
52
  date: dateStr,
53
+ locale: resolveLocale(config.locale),
49
54
  picks,
50
- disclaimer: DISCLAIMER,
55
+ disclaimer: t(config.locale, 'disclaimer'),
51
56
  accuracyOverview: accuracyOverview || undefined,
52
57
  };
53
58
  }
54
59
 
55
60
  export function formatDailyReportText(report: DailyReport, aiExplanation?: string): string {
61
+ const locale = resolveLocale(report.locale);
56
62
  if (report.body?.trim()) {
57
63
  const extra = aiExplanation?.trim();
58
64
  return extra ? `${report.body.trim()}\n\n${extra}` : report.body.trim();
59
65
  }
60
- const lines = [`【彩票每日推荐】${report.date}`, ''];
66
+ const lines = [t(locale, 'dailyTitle', { date: report.date }), ''];
61
67
  if (report.backtestOverview?.trim()) {
62
68
  lines.push(report.backtestOverview.trim());
63
69
  lines.push('');
64
70
  }
65
71
  if (report.weightFallbackGames?.length) {
66
- lines.push(`【权重保护】近端 holdout 调优未跑赢随机,已回退默认 F40/O35/T25:${report.weightFallbackGames.join(', ')}`);
72
+ lines.push(t(locale, 'weightFallback', { games: report.weightFallbackGames.join(', ') }));
67
73
  lines.push('');
68
74
  }
69
75
  if (report.accuracyOverview) {
70
- lines.push(`【模型复盘】累计平均命中 ${report.accuracyOverview}`);
76
+ lines.push(t(locale, 'accuracyOverview', { overview: report.accuracyOverview }));
71
77
  lines.push('');
72
78
  }
73
79
  for (const pick of report.picks) {
@@ -79,7 +85,7 @@ export function formatDailyReportText(report: DailyReport, aiExplanation?: strin
79
85
  lines.push(aiExplanation.trim());
80
86
  lines.push('');
81
87
  } else {
82
- lines.push('【统计说明】号码由频率+遗漏+近期趋势综合打分,并根据历史命中自动调权。');
88
+ lines.push(t(locale, 'statsExplanation'));
83
89
  lines.push('');
84
90
  }
85
91
  lines.push(report.disclaimer);
@@ -128,6 +134,7 @@ export async function loadTodayPendingPicks(
128
134
  db: LotteryDb,
129
135
  gameIds: GameId[],
130
136
  date = new Date(),
137
+ locale?: Locale,
131
138
  ): Promise<GamePick[]> {
132
139
  const today = date.toISOString().slice(0, 10);
133
140
  const picks: GamePick[] = [];
@@ -139,6 +146,7 @@ export async function loadTodayPendingPicks(
139
146
  }>;
140
147
  for (const row of pending.filter((p) => p.predict_date === today)) {
141
148
  picks.push({
149
+ locale: resolveLocale(locale),
142
150
  gameId,
143
151
  label: gameId,
144
152
  numbers: parseStoredJson<DrawNumbers>(row.numbers, {}),
@@ -155,15 +163,17 @@ export async function resolveTodayReport(
155
163
  db: LotteryDb,
156
164
  gameIds: GameId[],
157
165
  date = new Date(),
166
+ locale?: Locale,
158
167
  ): Promise<DailyReport | null> {
159
168
  const saved = await loadTodayReport(db, date);
160
169
  if (saved) return saved;
161
- const picks = await loadTodayPendingPicks(db, gameIds, date);
170
+ const picks = await loadTodayPendingPicks(db, gameIds, date, locale);
162
171
  if (!picks.length) return null;
163
172
  return {
164
173
  date: date.toISOString().slice(0, 10),
174
+ locale: resolveLocale(locale),
165
175
  picks,
166
- disclaimer: DISCLAIMER,
176
+ disclaimer: t(locale, 'disclaimer'),
167
177
  };
168
178
  }
169
179