@zhin.js/plugin-ledger 0.0.0-stage → 1.0.1

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 (72) hide show
  1. package/README.md +83 -2
  2. package/commands/ledger-add/index.js +17 -0
  3. package/commands/ledger-add/index.ts +17 -0
  4. package/commands/ledger-delete/index.js +17 -0
  5. package/commands/ledger-delete/index.ts +17 -0
  6. package/commands/ledger-list/index.js +21 -0
  7. package/commands/ledger-list/index.ts +20 -0
  8. package/commands/ledger-month/index.js +14 -0
  9. package/commands/ledger-month/index.ts +14 -0
  10. package/commands/ledger-report/index.js +23 -0
  11. package/commands/ledger-report/index.ts +25 -0
  12. package/commands/ledger-stats/index.js +21 -0
  13. package/commands/ledger-stats/index.ts +20 -0
  14. package/commands/ledger-today/index.js +14 -0
  15. package/commands/ledger-today/index.ts +14 -0
  16. package/commands/ledger-week/index.js +14 -0
  17. package/commands/ledger-week/index.ts +14 -0
  18. package/lib/config.d.ts +21 -0
  19. package/lib/config.d.ts.map +1 -0
  20. package/lib/config.js +30 -0
  21. package/lib/config.js.map +1 -0
  22. package/lib/entry-input.d.ts +18 -0
  23. package/lib/entry-input.d.ts.map +1 -0
  24. package/lib/entry-input.js +31 -0
  25. package/lib/entry-input.js.map +1 -0
  26. package/lib/identity.d.ts +22 -0
  27. package/lib/identity.d.ts.map +1 -0
  28. package/lib/identity.js +37 -0
  29. package/lib/identity.js.map +1 -0
  30. package/lib/index.d.ts +10 -0
  31. package/lib/index.d.ts.map +1 -0
  32. package/lib/index.js +10 -0
  33. package/lib/index.js.map +1 -0
  34. package/lib/money.d.ts +20 -0
  35. package/lib/money.d.ts.map +1 -0
  36. package/lib/money.js +48 -0
  37. package/lib/money.js.map +1 -0
  38. package/lib/period.d.ts +21 -0
  39. package/lib/period.d.ts.map +1 -0
  40. package/lib/period.js +98 -0
  41. package/lib/period.js.map +1 -0
  42. package/lib/report-format.d.ts +30 -0
  43. package/lib/report-format.d.ts.map +1 -0
  44. package/lib/report-format.js +98 -0
  45. package/lib/report-format.js.map +1 -0
  46. package/lib/runtime.d.ts +3 -0
  47. package/lib/runtime.d.ts.map +1 -0
  48. package/lib/runtime.js +3 -0
  49. package/lib/runtime.js.map +1 -0
  50. package/lib/service.d.ts +33 -0
  51. package/lib/service.d.ts.map +1 -0
  52. package/lib/service.js +206 -0
  53. package/lib/service.js.map +1 -0
  54. package/lib/store.d.ts +48 -0
  55. package/lib/store.d.ts.map +1 -0
  56. package/lib/store.js +162 -0
  57. package/lib/store.js.map +1 -0
  58. package/package.json +90 -3
  59. package/plugin.js +19 -0
  60. package/schedules/daily-report/index.js +10 -0
  61. package/schedules/daily-report/index.ts +10 -0
  62. package/schema.json +31 -0
  63. package/src/config.ts +39 -0
  64. package/src/entry-input.ts +39 -0
  65. package/src/identity.ts +50 -0
  66. package/src/index.ts +29 -0
  67. package/src/money.ts +53 -0
  68. package/src/period.ts +101 -0
  69. package/src/report-format.ts +125 -0
  70. package/src/runtime.ts +4 -0
  71. package/src/service.ts +217 -0
  72. package/src/store.ts +195 -0
package/lib/money.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Amounts are stored as integer cents (分) and parsed from text without floating point,
3
+ * so 0.1 + 0.2 style rounding errors cannot enter the ledger.
4
+ */
5
+ /** Largest single entry: ¥1,000,000.00, which also keeps sums far below Number.MAX_SAFE_INTEGER. */
6
+ export declare const MAX_ENTRY_CENTS = 100000000;
7
+ export type AmountParseResult = {
8
+ readonly ok: true;
9
+ readonly cents: number;
10
+ } | {
11
+ readonly ok: false;
12
+ readonly reason: string;
13
+ };
14
+ /** Parses `18`, `18.5`, `18.50`, `¥18.5` or `18.5元` into cents; rejects negatives, zero, exponents and more than two decimals. */
15
+ export declare function parseAmountToCents(raw: string): AmountParseResult;
16
+ /** Formats cents as `¥12.50`. */
17
+ export declare function formatCents(cents: number): string;
18
+ /** The text shown instead of an amount where other people can read the reply. */
19
+ export declare const MASKED_AMOUNT = "\u00A5***";
20
+ //# sourceMappingURL=money.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"money.d.ts","sourceRoot":"","sources":["../src/money.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,oGAAoG;AACpG,eAAO,MAAM,eAAe,YAAc,CAAC;AAE3C,MAAM,MAAM,iBAAiB,GACzB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC7C;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAmBpD,kIAAkI;AAClI,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,CAYjE;AAED,iCAAiC;AACjC,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIjD;AAED,iFAAiF;AACjF,eAAO,MAAM,aAAa,cAAS,CAAC"}
package/lib/money.js ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Amounts are stored as integer cents (分) and parsed from text without floating point,
3
+ * so 0.1 + 0.2 style rounding errors cannot enter the ledger.
4
+ */
5
+ /** Largest single entry: ¥1,000,000.00, which also keeps sums far below Number.MAX_SAFE_INTEGER. */
6
+ export const MAX_ENTRY_CENTS = 100_000_000;
7
+ const AMOUNT_PATTERN = /^(\d{1,7})(?:\.(\d{1,2}))?$/u;
8
+ const CURRENCY_PREFIX = /^[¥¥]/u;
9
+ const CURRENCY_SUFFIX = /(?:元|块|rmb|RMB|CNY)$/u;
10
+ /** Turns full-width digits and the full-width dot into ASCII so `12.5` parses like `12.5`. */
11
+ function toHalfWidth(text) {
12
+ return text.replace(/[0-9.]/gu, (character) => {
13
+ if (character === '.')
14
+ return '.';
15
+ const fullWidthZero = '0'.codePointAt(0);
16
+ return String(character.codePointAt(0) - fullWidthZero);
17
+ });
18
+ }
19
+ function invalid(reason) {
20
+ return { ok: false, reason };
21
+ }
22
+ /** Parses `18`, `18.5`, `18.50`, `¥18.5` or `18.5元` into cents; rejects negatives, zero, exponents and more than two decimals. */
23
+ export function parseAmountToCents(raw) {
24
+ const normalized = toHalfWidth(raw.trim());
25
+ const withoutCurrency = normalized.replace(CURRENCY_PREFIX, '').replace(CURRENCY_SUFFIX, '').trim();
26
+ if (withoutCurrency.startsWith('-'))
27
+ return invalid('金额不能为负数');
28
+ const match = AMOUNT_PATTERN.exec(withoutCurrency);
29
+ if (!match)
30
+ return invalid('金额格式不正确,请写成 18 或 18.50(最多两位小数)');
31
+ const wholeYuan = Number(match[1]);
32
+ const fractionDigits = (match[2] ?? '').padEnd(2, '0');
33
+ const cents = wholeYuan * 100 + Number(fractionDigits);
34
+ if (cents === 0)
35
+ return invalid('金额必须大于 0');
36
+ if (cents > MAX_ENTRY_CENTS)
37
+ return invalid('单笔金额不能超过 ¥1000000.00');
38
+ return { ok: true, cents };
39
+ }
40
+ /** Formats cents as `¥12.50`. */
41
+ export function formatCents(cents) {
42
+ const wholeYuan = Math.floor(cents / 100);
43
+ const remainingCents = cents % 100;
44
+ return `¥${wholeYuan}.${String(remainingCents).padStart(2, '0')}`;
45
+ }
46
+ /** The text shown instead of an amount where other people can read the reply. */
47
+ export const MASKED_AMOUNT = '¥***';
48
+ //# sourceMappingURL=money.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"money.js","sourceRoot":"","sources":["../src/money.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,oGAAoG;AACpG,MAAM,CAAC,MAAM,eAAe,GAAG,WAAW,CAAC;AAM3C,MAAM,cAAc,GAAG,8BAA8B,CAAC;AACtD,MAAM,eAAe,GAAG,QAAQ,CAAC;AACjC,MAAM,eAAe,GAAG,uBAAuB,CAAC;AAEhD,8FAA8F;AAC9F,SAAS,WAAW,CAAC,IAAY;IAC/B,OAAO,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,SAAS,EAAE,EAAE;QAC5C,IAAI,SAAS,KAAK,GAAG;YAAE,OAAO,GAAG,CAAC;QAClC,MAAM,aAAa,GAAG,GAAG,CAAC,WAAW,CAAC,CAAC,CAAE,CAAC;QAC1C,OAAO,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC,CAAE,GAAG,aAAa,CAAC,CAAC;IAC3D,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,OAAO,CAAC,MAAc;IAC7B,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;AAC/B,CAAC;AAED,kIAAkI;AAClI,MAAM,UAAU,kBAAkB,CAAC,GAAW;IAC5C,MAAM,UAAU,GAAG,WAAW,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IAC3C,MAAM,eAAe,GAAG,UAAU,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACpG,IAAI,eAAe,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,OAAO,CAAC,SAAS,CAAC,CAAC;IAC/D,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IACnD,IAAI,CAAC,KAAK;QAAE,OAAO,OAAO,CAAC,gCAAgC,CAAC,CAAC;IAC7D,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACnC,MAAM,cAAc,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACvD,MAAM,KAAK,GAAG,SAAS,GAAG,GAAG,GAAG,MAAM,CAAC,cAAc,CAAC,CAAC;IACvD,IAAI,KAAK,KAAK,CAAC;QAAE,OAAO,OAAO,CAAC,UAAU,CAAC,CAAC;IAC5C,IAAI,KAAK,GAAG,eAAe;QAAE,OAAO,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACpE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AAC7B,CAAC;AAED,iCAAiC;AACjC,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,GAAG,CAAC,CAAC;IAC1C,MAAM,cAAc,GAAG,KAAK,GAAG,GAAG,CAAC;IACnC,OAAO,IAAI,SAAS,IAAI,MAAM,CAAC,cAAc,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;AACpE,CAAC;AAED,iFAAiF;AACjF,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Calendar periods evaluated in the configured IANA timezone. Entries are compared by their
3
+ * local calendar date (YYYY-MM-DD), so daylight-saving shifts never move an entry into another day.
4
+ */
5
+ export type LedgerPeriod = 'today' | 'week' | 'month' | 'all';
6
+ export declare function periodLabel(period: LedgerPeriod): string;
7
+ /** Maps a user-typed period word to a period; undefined when the word is not recognised. */
8
+ export declare function parsePeriodWord(word: string | undefined, fallback: LedgerPeriod): LedgerPeriod | undefined;
9
+ /** Local calendar date `YYYY-MM-DD` of an instant in a timezone. */
10
+ export declare function localDateKey(instantMs: number, timeZone: string): string;
11
+ /** Local wall-clock time `HH:MM` of an instant in a timezone. */
12
+ export declare function localTimeKey(instantMs: number, timeZone: string): string;
13
+ /** Monday of the ISO week containing a local date. */
14
+ export declare function weekStartKey(dateKey: string): string;
15
+ /** Sunday that ends the ISO week starting on `weekStart`. */
16
+ export declare function weekEndKey(weekStart: string): string;
17
+ /** Whether an entry made at `entryMs` falls in `period` as seen at `nowMs`. */
18
+ export declare function isInPeriod(entryMs: number, nowMs: number, period: LedgerPeriod, timeZone: string): boolean;
19
+ /** Human-readable range for a period header, e.g. `2026-10-19 ~ 2026-10-25`. */
20
+ export declare function describePeriodRange(nowMs: number, period: LedgerPeriod, timeZone: string): string;
21
+ //# sourceMappingURL=period.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"period.d.ts","sourceRoot":"","sources":["../src/period.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;AAW9D,wBAAgB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAExD;AAED,4FAA4F;AAC5F,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,YAAY,GAAG,YAAY,GAAG,SAAS,CAQ1G;AAiBD,oEAAoE;AACpE,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGxE;AAED,iEAAiE;AACjE,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGxE;AAWD,sDAAsD;AACtD,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAKpD;AAED,6DAA6D;AAC7D,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,+EAA+E;AAC/E,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAO1G;AAED,gFAAgF;AAChF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CASjG"}
package/lib/period.js ADDED
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Calendar periods evaluated in the configured IANA timezone. Entries are compared by their
3
+ * local calendar date (YYYY-MM-DD), so daylight-saving shifts never move an entry into another day.
4
+ */
5
+ const DAY_MS = 86_400_000;
6
+ const PERIOD_LABELS = {
7
+ today: '今日',
8
+ week: '本周',
9
+ month: '本月',
10
+ all: '全部',
11
+ };
12
+ export function periodLabel(period) {
13
+ return PERIOD_LABELS[period];
14
+ }
15
+ /** Maps a user-typed period word to a period; undefined when the word is not recognised. */
16
+ export function parsePeriodWord(word, fallback) {
17
+ if (word === undefined || word.trim() === '')
18
+ return fallback;
19
+ const normalized = word.trim();
20
+ if (normalized === '今日' || normalized === '今天')
21
+ return 'today';
22
+ if (normalized === '本周' || normalized === '这周')
23
+ return 'week';
24
+ if (normalized === '本月' || normalized === '这个月')
25
+ return 'month';
26
+ if (normalized === '全部' || normalized === '所有')
27
+ return 'all';
28
+ return undefined;
29
+ }
30
+ function partsIn(instantMs, timeZone) {
31
+ const formatter = new Intl.DateTimeFormat('en-US', {
32
+ timeZone,
33
+ year: 'numeric',
34
+ month: '2-digit',
35
+ day: '2-digit',
36
+ hour: '2-digit',
37
+ minute: '2-digit',
38
+ hourCycle: 'h23',
39
+ });
40
+ const parts = {};
41
+ for (const part of formatter.formatToParts(instantMs))
42
+ parts[part.type] = part.value;
43
+ return parts;
44
+ }
45
+ /** Local calendar date `YYYY-MM-DD` of an instant in a timezone. */
46
+ export function localDateKey(instantMs, timeZone) {
47
+ const parts = partsIn(instantMs, timeZone);
48
+ return `${parts.year}-${parts.month}-${parts.day}`;
49
+ }
50
+ /** Local wall-clock time `HH:MM` of an instant in a timezone. */
51
+ export function localTimeKey(instantMs, timeZone) {
52
+ const parts = partsIn(instantMs, timeZone);
53
+ return `${parts.hour}:${parts.minute}`;
54
+ }
55
+ function dateKeyToUtcMs(dateKey) {
56
+ const [year, month, day] = dateKey.split('-').map(Number);
57
+ return Date.UTC(year, month - 1, day);
58
+ }
59
+ function utcMsToDateKey(utcMs) {
60
+ return new Date(utcMs).toISOString().slice(0, 10);
61
+ }
62
+ /** Monday of the ISO week containing a local date. */
63
+ export function weekStartKey(dateKey) {
64
+ const utcMs = dateKeyToUtcMs(dateKey);
65
+ const dayOfWeek = new Date(utcMs).getUTCDay();
66
+ const daysSinceMonday = (dayOfWeek + 6) % 7;
67
+ return utcMsToDateKey(utcMs - daysSinceMonday * DAY_MS);
68
+ }
69
+ /** Sunday that ends the ISO week starting on `weekStart`. */
70
+ export function weekEndKey(weekStart) {
71
+ return utcMsToDateKey(dateKeyToUtcMs(weekStart) + 6 * DAY_MS);
72
+ }
73
+ /** Whether an entry made at `entryMs` falls in `period` as seen at `nowMs`. */
74
+ export function isInPeriod(entryMs, nowMs, period, timeZone) {
75
+ if (period === 'all')
76
+ return true;
77
+ const entryDate = localDateKey(entryMs, timeZone);
78
+ const today = localDateKey(nowMs, timeZone);
79
+ if (period === 'today')
80
+ return entryDate === today;
81
+ if (period === 'month')
82
+ return entryDate.slice(0, 7) === today.slice(0, 7);
83
+ return weekStartKey(entryDate) === weekStartKey(today);
84
+ }
85
+ /** Human-readable range for a period header, e.g. `2026-10-19 ~ 2026-10-25`. */
86
+ export function describePeriodRange(nowMs, period, timeZone) {
87
+ const today = localDateKey(nowMs, timeZone);
88
+ if (period === 'today')
89
+ return today;
90
+ if (period === 'month')
91
+ return today.slice(0, 7);
92
+ if (period === 'week') {
93
+ const start = weekStartKey(today);
94
+ return `${start} ~ ${weekEndKey(start)}`;
95
+ }
96
+ return '全部记录';
97
+ }
98
+ //# sourceMappingURL=period.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"period.js","sourceRoot":"","sources":["../src/period.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ import { type LedgerPeriod } from './period.js';
2
+ import type { LedgerEntry } from './store.js';
3
+ /** Total, entry count and share for one category. */
4
+ export interface CategoryTotal {
5
+ readonly category: string;
6
+ readonly totalCents: number;
7
+ readonly count: number;
8
+ readonly largestCents: number;
9
+ }
10
+ export interface PeriodSummary {
11
+ readonly period: LedgerPeriod;
12
+ readonly range: string;
13
+ readonly totalCents: number;
14
+ readonly count: number;
15
+ readonly categories: readonly CategoryTotal[];
16
+ }
17
+ /** A reply in two versions: the full one for the owner, the masked one for public conversations. */
18
+ export interface LedgerReply {
19
+ readonly full: string;
20
+ readonly masked: string;
21
+ }
22
+ /** Groups entries by category, largest total first (ties by name for a stable order). */
23
+ export declare function totalsByCategory(entries: readonly LedgerEntry[]): CategoryTotal[];
24
+ export declare function summarize(entries: readonly LedgerEntry[], period: LedgerPeriod, nowMs: number, timeZone: string): PeriodSummary;
25
+ export declare function formatPeriodSummary(summary: PeriodSummary): LedgerReply;
26
+ export declare function formatCategoryStatistics(summary: PeriodSummary): LedgerReply;
27
+ export declare const MAX_LISTED_ENTRIES = 20;
28
+ /** Newest first, with `#id` so users can find what to delete. */
29
+ export declare function formatEntryList(entries: readonly LedgerEntry[], period: LedgerPeriod, nowMs: number, timeZone: string): LedgerReply;
30
+ //# sourceMappingURL=report-format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report-format.d.ts","sourceRoot":"","sources":["../src/report-format.ts"],"names":[],"mappings":"AACA,OAAO,EAAkD,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAChG,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,qDAAqD;AACrD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,SAAS,aAAa,EAAE,CAAC;CAC/C;AAED,oGAAoG;AACpG,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,yFAAyF;AACzF,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,GAAG,aAAa,EAAE,CAYjF;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,CAU/H;AASD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,WAAW,CAevE;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,aAAa,GAAG,WAAW,CAkB5E;AAED,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC,iEAAiE;AACjE,wBAAgB,eAAe,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,WAAW,CAwBnI"}
@@ -0,0 +1,98 @@
1
+ import { formatCents, MASKED_AMOUNT } from './money.js';
2
+ import { describePeriodRange, localDateKey, periodLabel } from './period.js';
3
+ /** Groups entries by category, largest total first (ties by name for a stable order). */
4
+ export function totalsByCategory(entries) {
5
+ const byCategory = new Map();
6
+ for (const entry of entries) {
7
+ const current = byCategory.get(entry.category) ?? { totalCents: 0, count: 0, largestCents: 0 };
8
+ current.totalCents += entry.amountCents;
9
+ current.count += 1;
10
+ current.largestCents = Math.max(current.largestCents, entry.amountCents);
11
+ byCategory.set(entry.category, current);
12
+ }
13
+ const totals = [...byCategory.entries()].map(([category, value]) => ({ category, ...value }));
14
+ totals.sort((left, right) => right.totalCents - left.totalCents || left.category.localeCompare(right.category));
15
+ return totals;
16
+ }
17
+ export function summarize(entries, period, nowMs, timeZone) {
18
+ let totalCents = 0;
19
+ for (const entry of entries)
20
+ totalCents += entry.amountCents;
21
+ return {
22
+ period,
23
+ range: describePeriodRange(nowMs, period, timeZone),
24
+ totalCents,
25
+ count: entries.length,
26
+ categories: totalsByCategory(entries),
27
+ };
28
+ }
29
+ /** Share of the total with one decimal, computed from integers. */
30
+ function sharePercent(partCents, totalCents) {
31
+ if (totalCents === 0)
32
+ return '0';
33
+ const tenths = Math.round((partCents * 1000) / totalCents);
34
+ return `${Math.floor(tenths / 10)}.${tenths % 10}`;
35
+ }
36
+ export function formatPeriodSummary(summary) {
37
+ const label = periodLabel(summary.period);
38
+ if (summary.count === 0) {
39
+ const empty = `📒 ${label}还没有记账(${summary.range})`;
40
+ return { full: empty, masked: empty };
41
+ }
42
+ const header = `📒 ${label}消费(${summary.range})`;
43
+ const fullLines = [header, `合计 ${formatCents(summary.totalCents)} · ${summary.count} 笔`];
44
+ const maskedLines = [header, `合计 ${MASKED_AMOUNT} · ${summary.count} 笔`];
45
+ for (const category of summary.categories) {
46
+ const share = sharePercent(category.totalCents, summary.totalCents);
47
+ fullLines.push(`· ${category.category} ${formatCents(category.totalCents)}(${category.count} 笔,${share}%)`);
48
+ maskedLines.push(`· ${category.category} ${MASKED_AMOUNT}(${category.count} 笔)`);
49
+ }
50
+ return { full: fullLines.join('\n'), masked: maskedLines.join('\n') };
51
+ }
52
+ export function formatCategoryStatistics(summary) {
53
+ const label = periodLabel(summary.period);
54
+ if (summary.count === 0) {
55
+ const empty = `📊 ${label}没有可统计的记账(${summary.range})`;
56
+ return { full: empty, masked: empty };
57
+ }
58
+ const header = `📊 分类统计 · ${label}(${summary.range})`;
59
+ const fullLines = [header];
60
+ const maskedLines = [header];
61
+ for (const category of summary.categories) {
62
+ const averageCents = Math.round(category.totalCents / category.count);
63
+ const share = sharePercent(category.totalCents, summary.totalCents);
64
+ fullLines.push(`· ${category.category}:合计 ${formatCents(category.totalCents)},${category.count} 笔,平均 ${formatCents(averageCents)},最大 ${formatCents(category.largestCents)},占比 ${share}%`);
65
+ maskedLines.push(`· ${category.category}:${category.count} 笔,金额已隐藏`);
66
+ }
67
+ fullLines.push(`共 ${summary.categories.length} 个类别 · 合计 ${formatCents(summary.totalCents)}`);
68
+ maskedLines.push(`共 ${summary.categories.length} 个类别`);
69
+ return { full: fullLines.join('\n'), masked: maskedLines.join('\n') };
70
+ }
71
+ export const MAX_LISTED_ENTRIES = 20;
72
+ /** Newest first, with `#id` so users can find what to delete. */
73
+ export function formatEntryList(entries, period, nowMs, timeZone) {
74
+ const label = periodLabel(period);
75
+ const range = describePeriodRange(nowMs, period, timeZone);
76
+ if (entries.length === 0) {
77
+ const empty = `🧾 ${label}没有账目(${range})`;
78
+ return { full: empty, masked: empty };
79
+ }
80
+ const newestFirst = [...entries].sort((left, right) => right.createdAt - left.createdAt || right.seq - left.seq);
81
+ const shown = newestFirst.slice(0, MAX_LISTED_ENTRIES);
82
+ const header = `🧾 ${label}账单(${range})`;
83
+ const fullLines = [header];
84
+ const maskedLines = [header];
85
+ for (const entry of shown) {
86
+ const date = localDateKey(entry.createdAt, timeZone).slice(5);
87
+ const note = entry.note ? ` ${entry.note}` : '';
88
+ fullLines.push(`#${entry.seq} ${date} ${entry.category} ${formatCents(entry.amountCents)}${note}`);
89
+ maskedLines.push(`#${entry.seq} ${date} ${entry.category} ${MASKED_AMOUNT}`);
90
+ }
91
+ if (newestFirst.length > shown.length) {
92
+ const more = `……另有 ${newestFirst.length - shown.length} 笔未显示`;
93
+ fullLines.push(more);
94
+ maskedLines.push(more);
95
+ }
96
+ return { full: fullLines.join('\n'), masked: maskedLines.join('\n') };
97
+ }
98
+ //# sourceMappingURL=report-format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report-format.js","sourceRoot":"","sources":["../src/report-format.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { LedgerService } from './service.js';
2
+ export declare const ledgerServiceToken: import("zhin.js").Token<LedgerService>;
3
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,eAAO,MAAM,kBAAkB,wCAA+E,CAAC"}
package/lib/runtime.js ADDED
@@ -0,0 +1,3 @@
1
+ import { createToken } from 'zhin.js';
2
+ export const ledgerServiceToken = createToken('zhin.ledger.service', 'Personal expense ledger');
3
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAGtC,MAAM,CAAC,MAAM,kBAAkB,GAAG,WAAW,CAAgB,qBAAqB,EAAE,yBAAyB,CAAC,CAAC"}
@@ -0,0 +1,33 @@
1
+ import { type OutboundHost, type PluginDatabaseHost } from 'zhin.js';
2
+ import type { ResolvedLedgerConfig } from './config.js';
3
+ import type { LedgerCaller } from './identity.js';
4
+ import { type LedgerPeriod } from './period.js';
5
+ export type DailyReportAction = 'on' | 'off' | 'status';
6
+ export interface LedgerServiceOptions {
7
+ /** Wall clock; tests pass a fake. */
8
+ readonly now?: () => number;
9
+ }
10
+ /**
11
+ * Personal expense ledger. Every operation is scoped to the caller's own `ownerKey`.
12
+ *
13
+ * Group privacy: replies that contain amounts go to the caller's private chat when the platform
14
+ * can deliver there (`OutboundHost.sendTarget` exists and the send succeeds). Otherwise the
15
+ * group gets the masked version. This is a capability check, not a per-platform list.
16
+ */
17
+ export declare class LedgerService {
18
+ #private;
19
+ constructor(database: PluginDatabaseHost, outbound: OutboundHost | undefined, config: ResolvedLedgerConfig, options?: LedgerServiceOptions);
20
+ addEntry(caller: LedgerCaller, text: string): Promise<string>;
21
+ deleteEntry(caller: LedgerCaller, seq: number): Promise<string>;
22
+ periodSummary(caller: LedgerCaller, period: LedgerPeriod): Promise<string>;
23
+ categoryStatistics(caller: LedgerCaller, period: LedgerPeriod): Promise<string>;
24
+ listEntries(caller: LedgerCaller, period: LedgerPeriod): Promise<string>;
25
+ dailyReport(caller: LedgerCaller, action: DailyReportAction): Promise<string>;
26
+ /**
27
+ * Called every minute by the schedule. Once the local time reaches `reportTime`, each subscriber
28
+ * who has not had today's report gets it in their private chat. A failed delivery is logged and
29
+ * not retried that day, so an unreachable user is not messaged every minute.
30
+ */
31
+ runDailyReports(): Promise<number>;
32
+ }
33
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkC,KAAK,YAAY,EAAE,KAAK,kBAAkB,EAAE,MAAM,SAAS,CAAC;AACrG,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAExD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,EAAuD,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAYrG,MAAM,MAAM,iBAAiB,GAAG,IAAI,GAAG,KAAK,GAAG,QAAQ,CAAC;AAExD,MAAM,WAAW,oBAAoB;IACnC,qCAAqC;IACrC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAED;;;;;;GAMG;AACH,qBAAa,aAAa;;gBAUZ,QAAQ,EAAE,kBAAkB,EAAE,QAAQ,EAAE,YAAY,GAAG,SAAS,EAAE,MAAM,EAAE,oBAAoB,EAAE,OAAO,GAAE,oBAAyB;IAwCxI,QAAQ,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAsB7D,WAAW,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAiB/D,aAAa,CAAC,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IAM1E,kBAAkB,CAAC,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IAM/E,WAAW,CAAC,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IA8BxE,WAAW,CAAC,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC;IA0BnF;;;;OAIG;IACG,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC;CAuBzC"}
package/lib/service.js ADDED
@@ -0,0 +1,206 @@
1
+ import { getLogger } from 'zhin.js';
2
+ import { parseEntryInput } from './entry-input.js';
3
+ import { formatCents, MASKED_AMOUNT } from './money.js';
4
+ import { isInPeriod, localDateKey, localTimeKey, periodLabel } from './period.js';
5
+ import { formatCategoryStatistics, formatEntryList, formatPeriodSummary, summarize, } from './report-format.js';
6
+ import { LedgerStore } from './store.js';
7
+ const logger = getLogger('ledger');
8
+ /**
9
+ * Personal expense ledger. Every operation is scoped to the caller's own `ownerKey`.
10
+ *
11
+ * Group privacy: replies that contain amounts go to the caller's private chat when the platform
12
+ * can deliver there (`OutboundHost.sendTarget` exists and the send succeeds). Otherwise the
13
+ * group gets the masked version. This is a capability check, not a per-platform list.
14
+ */
15
+ export class LedgerService {
16
+ #store;
17
+ #outbound;
18
+ #config;
19
+ #now;
20
+ /** Serialises writes per user so two quick `记` commands never share an `#id`. */
21
+ #userQueues = new Map();
22
+ /** Private targets already saved this process, to skip redundant writes. */
23
+ #knownPrivate = new Map();
24
+ constructor(database, outbound, config, options = {}) {
25
+ this.#store = new LedgerStore(database);
26
+ this.#outbound = outbound;
27
+ this.#config = config;
28
+ this.#now = options.now ?? (() => Date.now());
29
+ }
30
+ #withUserLock(ownerKey, operation) {
31
+ const previous = this.#userQueues.get(ownerKey) ?? Promise.resolve();
32
+ const next = previous.then(operation, operation);
33
+ const settled = next.catch(() => undefined);
34
+ this.#userQueues.set(ownerKey, settled);
35
+ void settled.then(() => {
36
+ if (this.#userQueues.get(ownerKey) === settled)
37
+ this.#userQueues.delete(ownerKey);
38
+ });
39
+ return next;
40
+ }
41
+ /**
42
+ * Some platforms (Discord, Lark, Slack, ...) address a private chat by a conversation id that is
43
+ * not the user id. When the user talks to the bot in private we save that real conversation and
44
+ * use it for later private deliveries; until then the user id is the best available guess.
45
+ */
46
+ async #rememberPrivate(caller) {
47
+ if (!caller.isPrivate)
48
+ return;
49
+ const raw = JSON.stringify(caller.conversation);
50
+ if (this.#knownPrivate.get(caller.ownerKey) === raw)
51
+ return;
52
+ try {
53
+ await this.#store.setPrivateTarget(caller.ownerKey, caller.conversation);
54
+ this.#knownPrivate.set(caller.ownerKey, raw);
55
+ }
56
+ catch (error) {
57
+ logger.warn(`saving private target failed: ${error instanceof Error ? error.message : String(error)}`);
58
+ }
59
+ }
60
+ async #privateTargetOf(caller) {
61
+ if (caller.isPrivate)
62
+ return caller.conversation;
63
+ return (await this.#store.getPrivateTarget(caller.ownerKey)) ?? caller.privateTarget;
64
+ }
65
+ async addEntry(caller, text) {
66
+ await this.#rememberPrivate(caller);
67
+ const parsed = parseEntryInput(text);
68
+ if (!parsed.ok)
69
+ return parsed.reason;
70
+ const { category, amountCents, note } = parsed.entry;
71
+ const seq = await this.#withUserLock(caller.ownerKey, async () => {
72
+ const reserved = await this.#store.reserveSequence(caller.ownerKey);
73
+ await this.#store.insertEntry({
74
+ ownerKey: caller.ownerKey,
75
+ seq: reserved,
76
+ category,
77
+ amountCents,
78
+ note,
79
+ createdAt: this.#now(),
80
+ });
81
+ return reserved;
82
+ });
83
+ const amount = caller.isPrivate ? formatCents(amountCents) : MASKED_AMOUNT;
84
+ const noteText = note ? ` · ${note}` : '';
85
+ return `✅ 已记账 #${seq} · ${category} ${amount}${noteText}`;
86
+ }
87
+ async deleteEntry(caller, seq) {
88
+ await this.#rememberPrivate(caller);
89
+ return this.#withUserLock(caller.ownerKey, async () => {
90
+ const entry = await this.#store.findEntry(caller.ownerKey, seq);
91
+ if (!entry)
92
+ return `没有找到你的账目 #${seq}(只能删除自己的记账,可用「账单」查看编号)`;
93
+ await this.#store.deleteEntry(caller.ownerKey, seq);
94
+ const amount = caller.isPrivate ? formatCents(entry.amountCents) : MASKED_AMOUNT;
95
+ return `🗑️ 已删除 #${seq} · ${entry.category} ${amount}`;
96
+ });
97
+ }
98
+ async #entriesIn(ownerKey, period) {
99
+ const nowMs = this.#now();
100
+ const entries = await this.#store.listEntries(ownerKey);
101
+ return entries.filter((entry) => isInPeriod(entry.createdAt, nowMs, period, this.#config.timezone));
102
+ }
103
+ async periodSummary(caller, period) {
104
+ const entries = await this.#entriesIn(caller.ownerKey, period);
105
+ const reply = formatPeriodSummary(summarize(entries, period, this.#now(), this.#config.timezone));
106
+ return this.#deliver(caller, reply, `${periodLabel(period)}消费`);
107
+ }
108
+ async categoryStatistics(caller, period) {
109
+ const entries = await this.#entriesIn(caller.ownerKey, period);
110
+ const reply = formatCategoryStatistics(summarize(entries, period, this.#now(), this.#config.timezone));
111
+ return this.#deliver(caller, reply, '分类统计');
112
+ }
113
+ async listEntries(caller, period) {
114
+ const entries = await this.#entriesIn(caller.ownerKey, period);
115
+ const reply = formatEntryList(entries, period, this.#now(), this.#config.timezone);
116
+ return this.#deliver(caller, reply, `${periodLabel(period)}账单`);
117
+ }
118
+ /** Private chat: full reply. Group/channel: try a private message, else the masked reply. */
119
+ async #deliver(caller, reply, title) {
120
+ if (caller.isPrivate) {
121
+ await this.#rememberPrivate(caller);
122
+ return reply.full;
123
+ }
124
+ if (reply.full === reply.masked)
125
+ return reply.full;
126
+ const delivered = await this.#sendPrivately(caller, reply.full);
127
+ if (delivered)
128
+ return `🔒 ${title}已私信发送给你`;
129
+ return `${reply.masked}\n(群聊中金额已隐藏,私聊我可查看完整金额)`;
130
+ }
131
+ async #sendPrivately(caller, content) {
132
+ const sendTarget = this.#outbound?.sendTarget;
133
+ if (!sendTarget)
134
+ return false;
135
+ try {
136
+ await sendTarget.call(this.#outbound, await this.#privateTargetOf(caller), content);
137
+ return true;
138
+ }
139
+ catch (error) {
140
+ logger.debug(`private delivery unavailable: ${error instanceof Error ? error.message : String(error)}`);
141
+ return false;
142
+ }
143
+ }
144
+ async dailyReport(caller, action) {
145
+ await this.#rememberPrivate(caller);
146
+ const reportTime = this.#config.reportTime;
147
+ const user = await this.#store.getUser(caller.ownerKey);
148
+ if (action === 'status') {
149
+ if (user?.reportEnabled)
150
+ return `记账日报已开启:每天 ${reportTime}(${this.#config.timezone})私信发送当天消费`;
151
+ return '记账日报未开启。发送「记账日报 开启」订阅';
152
+ }
153
+ if (action === 'off') {
154
+ await this.#store.setReport(caller.ownerKey, false, undefined, user?.lastReportDate ?? '');
155
+ return '已关闭记账日报';
156
+ }
157
+ if (!this.#outbound?.sendTarget)
158
+ return '当前环境不支持主动私信,无法开启记账日报';
159
+ const confirmation = `已开启记账日报:每天 ${reportTime}(${this.#config.timezone})私信发送当天消费。发送「记账日报 关闭」取消`;
160
+ if (!caller.isPrivate) {
161
+ const reachable = await this.#sendPrivately(caller, confirmation);
162
+ if (!reachable)
163
+ return '无法私信你,请先私聊我后再发送「记账日报 开启」';
164
+ }
165
+ const nowMs = this.#now();
166
+ const today = localDateKey(nowMs, this.#config.timezone);
167
+ const alreadyPastReportTime = localTimeKey(nowMs, this.#config.timezone) >= reportTime;
168
+ const lastReportDate = alreadyPastReportTime ? today : '';
169
+ await this.#store.setReport(caller.ownerKey, true, await this.#privateTargetOf(caller), lastReportDate);
170
+ return caller.isPrivate ? confirmation : '🔒 已开启记账日报,确认消息已私信发送给你';
171
+ }
172
+ /**
173
+ * Called every minute by the schedule. Once the local time reaches `reportTime`, each subscriber
174
+ * who has not had today's report gets it in their private chat. A failed delivery is logged and
175
+ * not retried that day, so an unreachable user is not messaged every minute.
176
+ */
177
+ async runDailyReports() {
178
+ const nowMs = this.#now();
179
+ const timezone = this.#config.timezone;
180
+ if (localTimeKey(nowMs, timezone) < this.#config.reportTime)
181
+ return 0;
182
+ const today = localDateKey(nowMs, timezone);
183
+ const subscribers = await this.#store.listReportSubscribers();
184
+ let sent = 0;
185
+ for (const subscriber of subscribers) {
186
+ if (subscriber.lastReportDate === today)
187
+ continue;
188
+ await this.#store.markReported(subscriber.ownerKey, today);
189
+ if (!subscriber.reportTarget || !this.#outbound?.sendTarget)
190
+ continue;
191
+ const entries = await this.#entriesIn(subscriber.ownerKey, 'today');
192
+ if (entries.length === 0 && !this.#config.reportWhenEmpty)
193
+ continue;
194
+ const summary = formatPeriodSummary(summarize(entries, 'today', nowMs, timezone));
195
+ try {
196
+ await this.#outbound.sendTarget(subscriber.reportTarget, `🌙 记账日报\n${summary.full}`);
197
+ sent += 1;
198
+ }
199
+ catch (error) {
200
+ logger.warn(`daily report delivery failed: ${error instanceof Error ? error.message : String(error)}`);
201
+ }
202
+ }
203
+ return sent;
204
+ }
205
+ }
206
+ //# sourceMappingURL=service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAmE,MAAM,SAAS,CAAC;AAErG,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,YAAY,EAAE,WAAW,EAAqB,MAAM,aAAa,CAAC;AACrG,OAAO,EACL,wBAAwB,EACxB,eAAe,EACf,mBAAmB,EACnB,SAAS,GAEV,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,WAAW,EAAoB,MAAM,YAAY,CAAC;AAE3D,MAAM,MAAM,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AASnC;;;;;;GAMG;AACH,MAAM,OAAO,aAAa;IACf,MAAM,CAAc;IACpB,SAAS,CAA2B;IACpC,OAAO,CAAuB;IAC9B,IAAI,CAAe;IAC5B,iFAAiF;IACxE,WAAW,GAAG,IAAI,GAAG,EAA4B,CAAC;IAC3D,4EAA4E;IACnE,aAAa,GAAG,IAAI,GAAG,EAAkB,CAAC;IAEnD,YAAY,QAA4B,EAAE,QAAkC,EAAE,MAA4B,EAAE,UAAgC,EAAE;QAC5I,IAAI,CAAC,MAAM,GAAG,IAAI,WAAW,CAAC,QAAQ,CAAC,CAAC;QACxC,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC;QAC1B,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAChD,CAAC;IAED,aAAa,CAAI,QAAgB,EAAE,SAA2B;QAC5D,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QACrE,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QACjD,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAC5C,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QACxC,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE;YACrB,IAAI,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,OAAO;gBAAE,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACpF,CAAC,CAAC,CAAC;QACH,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,gBAAgB,CAAC,MAAoB;QACzC,IAAI,CAAC,MAAM,CAAC,SAAS;YAAE,OAAO;QAC9B,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;QAChD,IAAI,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,GAAG;YAAE,OAAO;QAC5D,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,MAAM,CAAC,gBAAgB,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC;YACzE,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;QAC/C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,IAAI,CAAC,iCAAiC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACzG,CAAC;IACH,CAAC;IAED,KAAK,CAAC,gBAAgB,CAAC,MAAoB;QACzC,IAAI,MAAM,CAAC,SAAS;YAAE,OAAO,MAAM,CAAC,YAAY,CAAC;QACjD,OAAO,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,gBAAgB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,MAAM,CAAC,aAAa,CAAC;IACvF,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,MAAoB,EAAE,IAAY;QAC/C,MAAM,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;QACpC,MAAM,MAAM,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;QACrC,IAAI,CAAC,MAAM,CAAC,EAAE;YAAE,OAAO,MAAM,CAAC,MAAM,CAAC;QACrC,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC;QACrD,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE;YAC/D,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YACpE,MAAM,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC;gBAC5B,QAAQ,EAAE,MAAM,CAAC,QAAQ;gBACzB,GAAG,EAAE,QAAQ;gBACb,QAAQ;gBACR,WAAW;gBACX,IAAI;gBACJ,SAAS,EAAE,IAAI,CAAC,IAAI,EAAE;aACvB,CAAC,CAAC;YACH,OAAO,QAAQ,CAAC;QAClB,CAAC,CAAC,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC;QAC3E,MAAM,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1C,OAAO,UAAU,GAAG,MAAM,QAAQ,IAAI,MAAM,GAAG,QAAQ,EAAE,CAAC;IAC5D,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,MAAoB,EAAE,GAAW;QACjD,MAAM,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;QACpC,OAAO,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE;YACpD,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;YAChE,IAAI,CAAC,KAAK;gBAAE,OAAO,aAAa,GAAG,wBAAwB,CAAC;YAC5D,MAAM,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;YACpD,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC;YACjF,OAAO,YAAY,GAAG,MAAM,KAAK,CAAC,QAAQ,IAAI,MAAM,EAAE,CAAC;QACzD,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,QAAgB,EAAE,MAAoB;QACrD,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC1B,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;QACxD,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;IACtG,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,MAAoB,EAAE,MAAoB;QAC5D,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC/D,MAAM,KAAK,GAAG,mBAAmB,CAAC,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;QAClG,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAClE,CAAC;IAED,KAAK,CAAC,kBAAkB,CAAC,MAAoB,EAAE,MAAoB;QACjE,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC/D,MAAM,KAAK,GAAG,wBAAwB,CAAC,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;QACvG,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;IAC9C,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,MAAoB,EAAE,MAAoB;QAC1D,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC/D,MAAM,KAAK,GAAG,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACnF,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAClE,CAAC;IAED,6FAA6F;IAC7F,KAAK,CAAC,QAAQ,CAAC,MAAoB,EAAE,KAAkB,EAAE,KAAa;QACpE,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;YACrB,MAAM,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;YACpC,OAAO,KAAK,CAAC,IAAI,CAAC;QACpB,CAAC;QACD,IAAI,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC,IAAI,CAAC;QACnD,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QAChE,IAAI,SAAS;YAAE,OAAO,MAAM,KAAK,SAAS,CAAC;QAC3C,OAAO,GAAG,KAAK,CAAC,MAAM,yBAAyB,CAAC;IAClD,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,MAAoB,EAAE,OAAe;QACxD,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC;QAC9C,IAAI,CAAC,UAAU;YAAE,OAAO,KAAK,CAAC;QAC9B,IAAI,CAAC;YACH,MAAM,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC;YACpF,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,KAAK,CAAC,iCAAiC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACxG,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,MAAoB,EAAE,MAAyB;QAC/D,MAAM,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;QACpC,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC;QAC3C,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACxD,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;YACxB,IAAI,IAAI,EAAE,aAAa;gBAAE,OAAO,cAAc,UAAU,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,WAAW,CAAC;YAC7F,OAAO,uBAAuB,CAAC;QACjC,CAAC;QACD,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;YACrB,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,cAAc,IAAI,EAAE,CAAC,CAAC;YAC3F,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,UAAU;YAAE,OAAO,sBAAsB,CAAC;QAC/D,MAAM,YAAY,GAAG,cAAc,UAAU,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,yBAAyB,CAAC;QAChG,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;YACtB,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;YAClE,IAAI,CAAC,SAAS;gBAAE,OAAO,0BAA0B,CAAC;QACpD,CAAC;QACD,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC1B,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACzD,MAAM,qBAAqB,GAAG,YAAY,CAAC,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,UAAU,CAAC;QACvF,MAAM,cAAc,GAAG,qBAAqB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1D,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,EAAE,cAAc,CAAC,CAAC;QACxG,OAAO,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,wBAAwB,CAAC;IACpE,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,eAAe;QACnB,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC1B,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC;QACvC,IAAI,YAAY,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU;YAAE,OAAO,CAAC,CAAC;QACtE,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;QAC5C,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,qBAAqB,EAAE,CAAC;QAC9D,IAAI,IAAI,GAAG,CAAC,CAAC;QACb,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;YACrC,IAAI,UAAU,CAAC,cAAc,KAAK,KAAK;gBAAE,SAAS;YAClD,MAAM,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,UAAU,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;YAC3D,IAAI,CAAC,UAAU,CAAC,YAAY,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,UAAU;gBAAE,SAAS;YACtE,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;YACpE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,eAAe;gBAAE,SAAS;YACpE,MAAM,OAAO,GAAG,mBAAmB,CAAC,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC;YAClF,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,UAAU,CAAC,YAAY,EAAE,YAAY,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;gBACrF,IAAI,IAAI,CAAC,CAAC;YACZ,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,CAAC,IAAI,CAAC,iCAAiC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACzG,CAAC;QACH,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;CACF"}