@zhin.js/plugin-ledger 1.0.1 → 1.0.3

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 (91) hide show
  1. package/README.md +24 -0
  2. package/commands/ledger-add/index.js +6 -4
  3. package/commands/ledger-add/index.ts +6 -4
  4. package/commands/ledger-delete/index.js +6 -4
  5. package/commands/ledger-delete/index.ts +6 -4
  6. package/commands/ledger-list/index.js +8 -6
  7. package/commands/ledger-list/index.ts +8 -6
  8. package/commands/ledger-month/index.js +5 -3
  9. package/commands/ledger-month/index.ts +5 -3
  10. package/commands/ledger-report/index.js +11 -11
  11. package/commands/ledger-report/index.ts +10 -12
  12. package/commands/ledger-stats/index.js +8 -6
  13. package/commands/ledger-stats/index.ts +8 -6
  14. package/commands/ledger-today/index.js +5 -3
  15. package/commands/ledger-today/index.ts +5 -3
  16. package/commands/ledger-week/index.js +5 -3
  17. package/commands/ledger-week/index.ts +5 -3
  18. package/lib/config.d.ts +11 -5
  19. package/lib/config.d.ts.map +1 -1
  20. package/lib/config.js +3 -3
  21. package/lib/config.js.map +1 -1
  22. package/lib/console.d.ts +4 -0
  23. package/lib/console.d.ts.map +1 -0
  24. package/lib/console.js +39 -0
  25. package/lib/console.js.map +1 -0
  26. package/lib/entry-input.d.ts +3 -2
  27. package/lib/entry-input.d.ts.map +1 -1
  28. package/lib/entry-input.js +11 -9
  29. package/lib/entry-input.js.map +1 -1
  30. package/lib/i18n.d.ts +2 -0
  31. package/lib/i18n.d.ts.map +1 -0
  32. package/lib/i18n.js +200 -0
  33. package/lib/i18n.js.map +1 -0
  34. package/lib/identity.d.ts +3 -1
  35. package/lib/identity.d.ts.map +1 -1
  36. package/lib/identity.js +5 -2
  37. package/lib/identity.js.map +1 -1
  38. package/lib/index.d.ts +3 -1
  39. package/lib/index.d.ts.map +1 -1
  40. package/lib/index.js +3 -1
  41. package/lib/index.js.map +1 -1
  42. package/lib/money.d.ts +2 -1
  43. package/lib/money.d.ts.map +1 -1
  44. package/lib/money.js +6 -5
  45. package/lib/money.js.map +1 -1
  46. package/lib/period.d.ts +4 -3
  47. package/lib/period.d.ts.map +1 -1
  48. package/lib/period.js +8 -16
  49. package/lib/period.js.map +1 -1
  50. package/lib/report-format.d.ts +5 -4
  51. package/lib/report-format.d.ts.map +1 -1
  52. package/lib/report-format.js +25 -24
  53. package/lib/report-format.js.map +1 -1
  54. package/lib/service.d.ts +14 -1
  55. package/lib/service.d.ts.map +1 -1
  56. package/lib/service.js +41 -24
  57. package/lib/service.js.map +1 -1
  58. package/lib/store.d.ts +3 -1
  59. package/lib/store.d.ts.map +1 -1
  60. package/lib/store.js +8 -4
  61. package/lib/store.js.map +1 -1
  62. package/lib/tool-caller.d.ts +32 -0
  63. package/lib/tool-caller.d.ts.map +1 -0
  64. package/lib/tool-caller.js +74 -0
  65. package/lib/tool-caller.js.map +1 -0
  66. package/package.json +36 -7
  67. package/pages/index/index.js +63 -0
  68. package/pages/index/index.ts +65 -0
  69. package/plugin.js +3 -1
  70. package/schema.json +15 -0
  71. package/skills/ledger/SKILL.md +41 -0
  72. package/skills/ledger/tools/ledger_add/index.js +25 -0
  73. package/skills/ledger/tools/ledger_add/index.ts +26 -0
  74. package/skills/ledger/tools/ledger_delete/index.js +22 -0
  75. package/skills/ledger/tools/ledger_delete/index.ts +23 -0
  76. package/skills/ledger/tools/ledger_list/index.js +23 -0
  77. package/skills/ledger/tools/ledger_list/index.ts +24 -0
  78. package/skills/ledger/tools/ledger_summary/index.js +27 -0
  79. package/skills/ledger/tools/ledger_summary/index.ts +28 -0
  80. package/src/config.ts +3 -3
  81. package/src/console.ts +47 -0
  82. package/src/entry-input.ts +12 -9
  83. package/src/i18n.ts +200 -0
  84. package/src/identity.ts +6 -2
  85. package/src/index.ts +9 -1
  86. package/src/money.ts +7 -5
  87. package/src/period.ts +7 -13
  88. package/src/report-format.ts +26 -24
  89. package/src/service.ts +56 -24
  90. package/src/store.ts +9 -4
  91. package/src/tool-caller.ts +101 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-caller.js","sourceRoot":"","sources":["../src/tool-caller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAe,MAAM,cAAc,CAAC;AAC1D,OAAO,EAAE,oBAAoB,EAAuB,MAAM,SAAS,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAqB,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAE,QAAQ,IAAI,CAAC,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAqB,MAAM,eAAe,CAAC;AAuBpF,SAAS,UAAU,CACjB,MAAwB;IAExB,OAAO,MAAM,CAAC,IAAI,KAAK,IAAI,CAAC;AAC9B,CAAC;AAED,SAAS,uBAAuB,CAAC,OAAgC,EAAE,MAAc;IAC/E,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAC9B,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,gBAAgB,CAAC,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACjD,CAAC;IACD,IAAI,UAAU,GAAG,MAAM,CAAC,YAAY,EAAE,QAAQ,CAAC,EAAE,IAAI,MAAM,CAAC,QAAQ,CAAC;IACrE,IAAI,OAAO,OAAO,CAAC,OAAO,KAAK,UAAU,EAAE,CAAC;QAC1C,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAe,gBAAgB,CAAC,CAAC;YAC9D,IAAI,KAAK,IAAI,OAAO,KAAK,CAAC,QAAQ,KAAK,UAAU,EAAE,CAAC;gBAClD,MAAM,OAAO,GAAG,KAAK;qBAClB,QAAQ,EAAE;qBACV,MAAM,CACL,CAAC,QAAQ,EAAE,EAAE,CACX,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,KAAK,MAAM,CAAC,QAAQ,IAAI,QAAQ,CAAC,IAAI,KAAK,MAAM,CAAC,QAAQ,CAAC;oBAC9E,KAAK,CAAC,aAAa,CAAC,QAAQ,CAAC,EAAE,CAAC,KAAK,MAAM,CAAC,QAAQ,CACvD,CAAC;gBACJ,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBACzB,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAE,CAAC,EAAE,CAAC,CAAC;gBACtC,CAAC;YACH,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,oGAAoG;QACtG,CAAC;IACH,CAAC;IACD,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,MAAM,IAAI,gBAAgB,CAAC,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACjD,CAAC;IACD,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;QACxB,OAAO,oBAAoB,CAAC;YAC1B,YAAY,EAAE;gBACZ,GAAG,MAAM,CAAC,YAAY;gBACtB,QAAQ,EAAE,EAAE,EAAE,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,CAAC,QAAQ,EAAE;aACvD;SACF,CAAC,CAAC;IACL,CAAC;IACD,OAAO,oBAAoB,CAAC;QAC1B,YAAY,EAAE;YACZ,QAAQ,EAAE,EAAE,EAAE,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,CAAC,QAAQ,EAAE;YACtD,IAAI,EAAE,MAAM,CAAC,KAAK;YAClB,EAAE,EAAE,MAAM,CAAC,OAAO;YAClB,GAAG,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACxE;KACF,CAAC,CAAC;AACL,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,oBAAoB,CAAC,OAAgC;IACnE,MAAM,MAAM,GAAG,aAAa,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7C,MAAM,QAAQ,GAAG,CAAC,OAAO,CAAC,SAAS,EAAE,SAAS,IAAI,OAAO,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,CAAC;IAC5E,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,gBAAgB,CAAC,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACjD,CAAC;IACD,MAAM,YAAY,GAAG,uBAAuB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC9D,MAAM,QAAQ,GAAG,YAAY,CAAC,QAAQ,CAAC;IACvC,MAAM,SAAS,GAAG,YAAY,CAAC,IAAI,KAAK,SAAS,CAAC;IAClD,MAAM,aAAa,GAAmB,SAAS;QAC7C,CAAC,CAAC,YAAY;QACd,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC/D,OAAO;QACL,QAAQ,EAAE,cAAc,CAAC,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC;QACjE,QAAQ;QACR,MAAM;QACN,YAAY;QACZ,aAAa;QACb,SAAS;KACV,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/plugin-ledger",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Personal expense ledger for Zhin.js (Plugin Runtime): per-user entries in integer cents, period and category summaries, group privacy and an opt-in daily report",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -16,8 +16,10 @@
16
16
  "README.md",
17
17
  "commands",
18
18
  "lib",
19
+ "pages",
19
20
  "plugin.js",
20
21
  "schedules",
22
+ "skills",
21
23
  "src",
22
24
  "schema.json"
23
25
  ],
@@ -34,10 +36,21 @@
34
36
  "email": "admin@liucl.cn",
35
37
  "url": "https://github.com/lc-cn"
36
38
  },
39
+ "dependencies": {
40
+ "@zhin.js/client": "1.1.25",
41
+ "@zhin.js/console-contract": "1.1.3",
42
+ "@zhin.js/page": "1.1.15",
43
+ "@zhin.js/skill": "1.1.14",
44
+ "@zhin.js/tool": "1.1.16"
45
+ },
37
46
  "peerDependencies": {
38
- "zhin.js": "^1.1.13",
39
- "@zhin.js/command": "^1.1.9",
40
- "@zhin.js/schedule-feature": "^1.1.7"
47
+ "zod": "^4.0.0",
48
+ "zhin.js": "^1.1.22",
49
+ "@zhin.js/command": "^1.1.15",
50
+ "@zhin.js/page": "^1.1.15",
51
+ "@zhin.js/schedule-feature": "^1.1.13",
52
+ "@zhin.js/skill": "^1.1.14",
53
+ "@zhin.js/tool": "^1.1.16"
41
54
  },
42
55
  "peerDependenciesMeta": {
43
56
  "@zhin.js/command": {
@@ -45,14 +58,18 @@
45
58
  },
46
59
  "@zhin.js/schedule-feature": {
47
60
  "optional": true
61
+ },
62
+ "zod": {
63
+ "optional": true
48
64
  }
49
65
  },
50
66
  "devDependencies": {
51
67
  "typescript": "^6.0.3",
52
68
  "vitest": "^4.1.10",
53
- "@zhin.js/schedule-feature": "1.1.7",
54
- "@zhin.js/testing": "1.0.5",
55
- "zhin.js": "1.1.13"
69
+ "zod": "^4.4.3",
70
+ "@zhin.js/schedule-feature": "1.1.13",
71
+ "@zhin.js/testing": "1.0.8",
72
+ "zhin.js": "1.1.22"
56
73
  },
57
74
  "engines": {
58
75
  "node": ">=22.19.0"
@@ -81,6 +98,18 @@
81
98
  {
82
99
  "package": "@zhin.js/schedule-feature",
83
100
  "api": "^1.0.0"
101
+ },
102
+ {
103
+ "package": "@zhin.js/tool",
104
+ "api": "^1.1.0"
105
+ },
106
+ {
107
+ "package": "@zhin.js/skill",
108
+ "api": "^1.0.0"
109
+ },
110
+ {
111
+ "package": "@zhin.js/page",
112
+ "api": "^1.0.0"
84
113
  }
85
114
  ],
86
115
  "plugins": []
@@ -0,0 +1,63 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { definePage } from '@zhin.js/console-contract';
3
+ import { createDeclarativePage } from '@zhin.js/client';
4
+ export const meta = definePage({
5
+ title: '记账本',
6
+ titleI18n: { en: 'Ledger' },
7
+ icon: 'Wallet',
8
+ order: 50,
9
+ requiredPermissions: ['ledger:read'],
10
+ });
11
+ export default createDeclarativePage({
12
+ title: { zh: '记账本', en: 'Ledger' },
13
+ description: {
14
+ zh: '个人记账日报订阅状态与运行配置概览。',
15
+ en: 'Personal expense ledger daily report subscriptions and runtime configuration.',
16
+ },
17
+ stats: [
18
+ {
19
+ key: 'subscriberCount',
20
+ label: { zh: '日报订阅数', en: 'Report Subscribers' },
21
+ value: 'subscriberCount',
22
+ },
23
+ {
24
+ key: 'reportTime',
25
+ label: { zh: '日报发送时间', en: 'Report Time' },
26
+ value: 'reportTime',
27
+ },
28
+ {
29
+ key: 'timezone',
30
+ label: { zh: '统计时区', en: 'Timezone' },
31
+ value: 'timezone',
32
+ },
33
+ ],
34
+ actions: [
35
+ {
36
+ name: 'run-daily-reports',
37
+ label: { zh: '执行日报发送', en: 'Run Daily Reports' },
38
+ variant: 'primary',
39
+ },
40
+ ],
41
+ sections: [
42
+ {
43
+ id: 'subscribers',
44
+ title: { zh: '日报订阅用户', en: 'Daily Report Subscribers' },
45
+ emptyText: { zh: '暂无开启记账日报的用户', en: 'No users have enabled daily reports.' },
46
+ items: 'subscribers',
47
+ columns: [
48
+ { key: 'ownerKey', label: { zh: '用户标识', en: 'Owner Key' } },
49
+ { key: 'reportLocale', label: { zh: '语言', en: 'Locale' } },
50
+ { key: 'lastReportDate', label: { zh: '最近发送日期', en: 'Last Report Date' } },
51
+ ],
52
+ },
53
+ {
54
+ id: 'config',
55
+ title: { zh: '配置详情', en: 'Configuration' },
56
+ entries: (data) => [
57
+ { label: 'Timezone', value: data.timezone },
58
+ { label: 'Report Time', value: data.reportTime },
59
+ { label: 'Report When Empty', value: data.reportWhenEmpty ? 'Yes' : 'No' },
60
+ ],
61
+ },
62
+ ],
63
+ });
@@ -0,0 +1,65 @@
1
+ import { definePage } from '@zhin.js/console-contract';
2
+ import { createDeclarativePage } from '@zhin.js/client';
3
+ import type { LedgerConsoleSnapshot, LedgerConsoleSubscriber } from '../../src/service.js';
4
+
5
+ export const meta = definePage({
6
+ title: '记账本',
7
+ titleI18n: { en: 'Ledger' },
8
+ icon: 'Wallet',
9
+ order: 50,
10
+ requiredPermissions: ['ledger:read'],
11
+ });
12
+
13
+ export default createDeclarativePage<LedgerConsoleSnapshot>({
14
+ title: { zh: '记账本', en: 'Ledger' },
15
+ description: {
16
+ zh: '个人记账日报订阅状态与运行配置概览。',
17
+ en: 'Personal expense ledger daily report subscriptions and runtime configuration.',
18
+ },
19
+ stats: [
20
+ {
21
+ key: 'subscriberCount',
22
+ label: { zh: '日报订阅数', en: 'Report Subscribers' },
23
+ value: 'subscriberCount',
24
+ },
25
+ {
26
+ key: 'reportTime',
27
+ label: { zh: '日报发送时间', en: 'Report Time' },
28
+ value: 'reportTime',
29
+ },
30
+ {
31
+ key: 'timezone',
32
+ label: { zh: '统计时区', en: 'Timezone' },
33
+ value: 'timezone',
34
+ },
35
+ ],
36
+ actions: [
37
+ {
38
+ name: 'run-daily-reports',
39
+ label: { zh: '执行日报发送', en: 'Run Daily Reports' },
40
+ variant: 'primary',
41
+ },
42
+ ],
43
+ sections: [
44
+ {
45
+ id: 'subscribers',
46
+ title: { zh: '日报订阅用户', en: 'Daily Report Subscribers' },
47
+ emptyText: { zh: '暂无开启记账日报的用户', en: 'No users have enabled daily reports.' },
48
+ items: 'subscribers',
49
+ columns: [
50
+ { key: 'ownerKey', label: { zh: '用户标识', en: 'Owner Key' } },
51
+ { key: 'reportLocale', label: { zh: '语言', en: 'Locale' } },
52
+ { key: 'lastReportDate', label: { zh: '最近发送日期', en: 'Last Report Date' } },
53
+ ] satisfies readonly { readonly key: keyof LedgerConsoleSubscriber & string; readonly label: { readonly zh: string; readonly en: string } }[],
54
+ },
55
+ {
56
+ id: 'config',
57
+ title: { zh: '配置详情', en: 'Configuration' },
58
+ entries: (data) => [
59
+ { label: 'Timezone', value: data.timezone },
60
+ { label: 'Report Time', value: data.reportTime },
61
+ { label: 'Report When Empty', value: data.reportWhenEmpty ? 'Yes' : 'No' },
62
+ ],
63
+ },
64
+ ],
65
+ });
package/plugin.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
- import { databaseHostToken, definePlugin, outboundHostToken } from 'zhin.js';
2
+ import { databaseHostToken, definePlugin, outboundHostToken, pluginConsoleRpcToken } from 'zhin.js';
3
3
  import { resolveLedgerConfig } from "./lib/config.js";
4
+ import { createLedgerConsoleRpc } from "./lib/console.js";
4
5
  import { ledgerServiceToken } from "./lib/runtime.js";
5
6
  import { LedgerService } from "./lib/service.js";
6
7
  import { defineLedgerTables } from "./lib/store.js";
@@ -15,5 +16,6 @@ export default definePlugin({
15
16
  const outbound = context.resources.has(outboundHostToken) ? context.resources.use(outboundHostToken) : undefined;
16
17
  const service = new LedgerService(database, outbound, config);
17
18
  context.resources.provide(ledgerServiceToken, service);
19
+ context.resources.provide(pluginConsoleRpcToken, createLedgerConsoleRpc(service));
18
20
  },
19
21
  });
package/schema.json CHANGED
@@ -8,6 +8,11 @@
8
8
  "default": "Asia/Shanghai",
9
9
  "x-zhin-config": {
10
10
  "effect": "reload"
11
+ },
12
+ "x-i18n": {
13
+ "en": {
14
+ "description": "IANA timezone used to calculate today, this week (starting Monday), this month and daily-report dates."
15
+ }
11
16
  }
12
17
  },
13
18
  "reportTime": {
@@ -17,6 +22,11 @@
17
22
  "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$",
18
23
  "x-zhin-config": {
19
24
  "effect": "reload"
25
+ },
26
+ "x-i18n": {
27
+ "en": {
28
+ "description": "Daily ledger report time (HH:MM in timezone); sent only to users who explicitly subscribed to daily reports."
29
+ }
20
30
  }
21
31
  },
22
32
  "reportWhenEmpty": {
@@ -25,6 +35,11 @@
25
35
  "default": false,
26
36
  "x-zhin-config": {
27
37
  "effect": "reload"
38
+ },
39
+ "x-i18n": {
40
+ "en": {
41
+ "description": "Send a daily report even when no transactions were recorded that day."
42
+ }
28
43
  }
29
44
  }
30
45
  }
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: ledger
3
+ description: >-
4
+ Personal expense ledger — record expenses, delete entries, view today/week/month spending summaries and category breakdowns.
5
+ keywords:
6
+ - 记账
7
+ - 账单
8
+ - 消费
9
+ - 支出
10
+ - 开销
11
+ - ledger
12
+ - expense
13
+ tags:
14
+ - ledger
15
+ - finance
16
+ tools:
17
+ - ledger_add
18
+ - ledger_summary
19
+ - ledger_list
20
+ - ledger_delete
21
+ ---
22
+
23
+ # 个人记账本 (Personal Ledger)
24
+
25
+ ## 何时使用本技能
26
+
27
+ - 用户想记录一笔消费/支出(如「记一笔午饭 18.5 元」「打车花了 32」)
28
+ - 用户想查看今日、本周或本月的消费汇总或分类占比
29
+ - 用户想列出近期的账单明细或按编号删除某笔账目
30
+
31
+ ## 工作流
32
+
33
+ 1. **记账 (`ledger_add`)**:提取类别(`category`)、金额(`amount`,正数元)与可选备注(`note`)。需要用户批准。
34
+ 2. **消费汇总与分类统计 (`ledger_summary`)**:按 `period`(`today` / `week` / `month`)查看总览(`mode: "summary"`)或分类统计(`mode: "categories"`)。
35
+ 3. **账单明细 (`ledger_list`)**:按 `period` 查看带 `#编号` 的账单列表。
36
+ 4. **删除账目 (`ledger_delete`)**:传入账目编号 `seq` 删除对应记录。需要用户批准。
37
+
38
+ ## 注意
39
+
40
+ - 账目按发送者隔离,用户只能操作自己的账本。
41
+ - 群聊场景下会自动保护金额隐私(优先私信发送完整账单或在群内脱敏显示)。
@@ -0,0 +1,25 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineAgentTool } from '@zhin.js/tool';
3
+ import { z } from 'zod';
4
+ import { replyLedger } from "../../../../lib/identity.js";
5
+ import { ledgerServiceToken } from "../../../../lib/runtime.js";
6
+ import { ledgerCallerFromTool } from "../../../../lib/tool-caller.js";
7
+ export default defineAgentTool({
8
+ description: 'Record a personal expense entry in the current user’s ledger with a category, positive amount, and optional note.',
9
+ requiresApproval: 'always',
10
+ tags: ['ledger', 'finance'],
11
+ keywords: ['记账', '消费', '支出', '开销', 'ledger', 'expense'],
12
+ inputSchema: z.object({
13
+ category: z.string().min(1).max(32).describe('Expense category, e.g. 午饭, 打车'),
14
+ amount: z.number().positive().describe('Expense amount in yuan, e.g. 18.5'),
15
+ note: z.string().max(120).optional().describe('Optional note for the expense'),
16
+ }),
17
+ async execute({ category, amount, note }, context) {
18
+ return replyLedger(async () => {
19
+ const caller = ledgerCallerFromTool(context);
20
+ const service = context.use(ledgerServiceToken);
21
+ const text = `${category} ${amount}${note ? ` ${note}` : ''}`;
22
+ return service.addEntry(caller, text);
23
+ });
24
+ },
25
+ });
@@ -0,0 +1,26 @@
1
+ import { defineAgentTool } from '@zhin.js/tool';
2
+ import { z } from 'zod';
3
+ import { replyLedger } from '../../../../src/identity.js';
4
+ import { ledgerServiceToken } from '../../../../src/runtime.js';
5
+ import { ledgerCallerFromTool } from '../../../../src/tool-caller.js';
6
+
7
+ export default defineAgentTool({
8
+ description:
9
+ 'Record a personal expense entry in the current user’s ledger with a category, positive amount, and optional note.',
10
+ requiresApproval: 'always',
11
+ tags: ['ledger', 'finance'],
12
+ keywords: ['记账', '消费', '支出', '开销', 'ledger', 'expense'],
13
+ inputSchema: z.object({
14
+ category: z.string().min(1).max(32).describe('Expense category, e.g. 午饭, 打车'),
15
+ amount: z.number().positive().describe('Expense amount in yuan, e.g. 18.5'),
16
+ note: z.string().max(120).optional().describe('Optional note for the expense'),
17
+ }),
18
+ async execute({ category, amount, note }, context) {
19
+ return replyLedger(async () => {
20
+ const caller = ledgerCallerFromTool(context);
21
+ const service = context.use(ledgerServiceToken);
22
+ const text = `${category} ${amount}${note ? ` ${note}` : ''}`;
23
+ return service.addEntry(caller, text);
24
+ });
25
+ },
26
+ });
@@ -0,0 +1,22 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineAgentTool } from '@zhin.js/tool';
3
+ import { z } from 'zod';
4
+ import { replyLedger } from "../../../../lib/identity.js";
5
+ import { ledgerServiceToken } from "../../../../lib/runtime.js";
6
+ import { ledgerCallerFromTool } from "../../../../lib/tool-caller.js";
7
+ export default defineAgentTool({
8
+ description: 'Delete one of the current user’s expense entries by its sequence number (#id).',
9
+ requiresApproval: 'always',
10
+ tags: ['ledger', 'finance'],
11
+ keywords: ['删账', '删除记账', 'delete expense', 'ledger delete'],
12
+ inputSchema: z.object({
13
+ seq: z.number().int().positive().describe('Per-user entry sequence number (#id)'),
14
+ }),
15
+ async execute({ seq }, context) {
16
+ return replyLedger(async () => {
17
+ const caller = ledgerCallerFromTool(context);
18
+ const service = context.use(ledgerServiceToken);
19
+ return service.deleteEntry(caller, seq);
20
+ });
21
+ },
22
+ });
@@ -0,0 +1,23 @@
1
+ import { defineAgentTool } from '@zhin.js/tool';
2
+ import { z } from 'zod';
3
+ import { replyLedger } from '../../../../src/identity.js';
4
+ import { ledgerServiceToken } from '../../../../src/runtime.js';
5
+ import { ledgerCallerFromTool } from '../../../../src/tool-caller.js';
6
+
7
+ export default defineAgentTool({
8
+ description:
9
+ 'Delete one of the current user’s expense entries by its sequence number (#id).',
10
+ requiresApproval: 'always',
11
+ tags: ['ledger', 'finance'],
12
+ keywords: ['删账', '删除记账', 'delete expense', 'ledger delete'],
13
+ inputSchema: z.object({
14
+ seq: z.number().int().positive().describe('Per-user entry sequence number (#id)'),
15
+ }),
16
+ async execute({ seq }, context) {
17
+ return replyLedger(async () => {
18
+ const caller = ledgerCallerFromTool(context);
19
+ const service = context.use(ledgerServiceToken);
20
+ return service.deleteEntry(caller, seq);
21
+ });
22
+ },
23
+ });
@@ -0,0 +1,23 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineAgentTool } from '@zhin.js/tool';
3
+ import { z } from 'zod';
4
+ import { replyLedger } from "../../../../lib/identity.js";
5
+ import { ledgerServiceToken } from "../../../../lib/runtime.js";
6
+ import { ledgerCallerFromTool } from "../../../../lib/tool-caller.js";
7
+ export default defineAgentTool({
8
+ description: 'List personal expense entries for today, this week, or this month with their sequence numbers (#id).',
9
+ requiresApproval: 'never',
10
+ readOnly: true,
11
+ tags: ['ledger', 'finance'],
12
+ keywords: ['账单', '账目列表', '消费明细', 'ledger list', 'expenses'],
13
+ inputSchema: z.object({
14
+ period: z.enum(['today', 'week', 'month']).default('today').describe('Time period: today, week, or month'),
15
+ }),
16
+ async execute({ period = 'today' }, context) {
17
+ return replyLedger(async () => {
18
+ const caller = ledgerCallerFromTool(context);
19
+ const service = context.use(ledgerServiceToken);
20
+ return service.listEntries(caller, period);
21
+ });
22
+ },
23
+ });
@@ -0,0 +1,24 @@
1
+ import { defineAgentTool } from '@zhin.js/tool';
2
+ import { z } from 'zod';
3
+ import { replyLedger } from '../../../../src/identity.js';
4
+ import { ledgerServiceToken } from '../../../../src/runtime.js';
5
+ import { ledgerCallerFromTool } from '../../../../src/tool-caller.js';
6
+
7
+ export default defineAgentTool({
8
+ description:
9
+ 'List personal expense entries for today, this week, or this month with their sequence numbers (#id).',
10
+ requiresApproval: 'never',
11
+ readOnly: true,
12
+ tags: ['ledger', 'finance'],
13
+ keywords: ['账单', '账目列表', '消费明细', 'ledger list', 'expenses'],
14
+ inputSchema: z.object({
15
+ period: z.enum(['today', 'week', 'month']).default('today').describe('Time period: today, week, or month'),
16
+ }),
17
+ async execute({ period = 'today' }, context) {
18
+ return replyLedger(async () => {
19
+ const caller = ledgerCallerFromTool(context);
20
+ const service = context.use(ledgerServiceToken);
21
+ return service.listEntries(caller, period);
22
+ });
23
+ },
24
+ });
@@ -0,0 +1,27 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineAgentTool } from '@zhin.js/tool';
3
+ import { z } from 'zod';
4
+ import { replyLedger } from "../../../../lib/identity.js";
5
+ import { ledgerServiceToken } from "../../../../lib/runtime.js";
6
+ import { ledgerCallerFromTool } from "../../../../lib/tool-caller.js";
7
+ export default defineAgentTool({
8
+ description: 'View personal spending summary or category statistics for today, this week, or this month.',
9
+ requiresApproval: 'never',
10
+ readOnly: true,
11
+ tags: ['ledger', 'finance'],
12
+ keywords: ['消费汇总', '分类统计', '账单统计', 'ledger summary', 'spending'],
13
+ inputSchema: z.object({
14
+ period: z.enum(['today', 'week', 'month']).default('month').describe('Time period: today, week, or month'),
15
+ mode: z.enum(['summary', 'categories']).default('summary').describe('Summary view or detailed category statistics'),
16
+ }),
17
+ async execute({ period = 'month', mode = 'summary' }, context) {
18
+ return replyLedger(async () => {
19
+ const caller = ledgerCallerFromTool(context);
20
+ const service = context.use(ledgerServiceToken);
21
+ if (mode === 'categories') {
22
+ return service.categoryStatistics(caller, period);
23
+ }
24
+ return service.periodSummary(caller, period);
25
+ });
26
+ },
27
+ });
@@ -0,0 +1,28 @@
1
+ import { defineAgentTool } from '@zhin.js/tool';
2
+ import { z } from 'zod';
3
+ import { replyLedger } from '../../../../src/identity.js';
4
+ import { ledgerServiceToken } from '../../../../src/runtime.js';
5
+ import { ledgerCallerFromTool } from '../../../../src/tool-caller.js';
6
+
7
+ export default defineAgentTool({
8
+ description:
9
+ 'View personal spending summary or category statistics for today, this week, or this month.',
10
+ requiresApproval: 'never',
11
+ readOnly: true,
12
+ tags: ['ledger', 'finance'],
13
+ keywords: ['消费汇总', '分类统计', '账单统计', 'ledger summary', 'spending'],
14
+ inputSchema: z.object({
15
+ period: z.enum(['today', 'week', 'month']).default('month').describe('Time period: today, week, or month'),
16
+ mode: z.enum(['summary', 'categories']).default('summary').describe('Summary view or detailed category statistics'),
17
+ }),
18
+ async execute({ period = 'month', mode = 'summary' }, context) {
19
+ return replyLedger(async () => {
20
+ const caller = ledgerCallerFromTool(context);
21
+ const service = context.use(ledgerServiceToken);
22
+ if (mode === 'categories') {
23
+ return service.categoryStatistics(caller, period);
24
+ }
25
+ return service.periodSummary(caller, period);
26
+ });
27
+ },
28
+ });
package/src/config.ts CHANGED
@@ -4,9 +4,9 @@ export const DEFAULT_TIMEZONE = 'Asia/Shanghai';
4
4
  export const DEFAULT_REPORT_TIME = '22:00';
5
5
 
6
6
  export const ledgerConfig = Schema.object({
7
- timezone: Schema.string().default(DEFAULT_TIMEZONE).description('计算今日、本周(周一开始)、本月与日报日期所用的 IANA 时区').effect('reload'),
8
- reportTime: Schema.string().default(DEFAULT_REPORT_TIME).description('记账日报的发送时间(HH:MM,按 timezone);只发给用「记账日报 开启」订阅的用户').effect('reload'),
9
- reportWhenEmpty: Schema.boolean().default(false).description('当天没有记账时是否仍发送日报').effect('reload'),
7
+ timezone: Schema.string().default(DEFAULT_TIMEZONE).description('计算今日、本周(周一开始)、本月与日报日期所用的 IANA 时区').i18n({"en": {"description": "IANA timezone used to calculate today, this week (starting Monday), this month and daily-report dates."}}).effect('reload'),
8
+ reportTime: Schema.string().default(DEFAULT_REPORT_TIME).description('记账日报的发送时间(HH:MM,按 timezone);只发给用「记账日报 开启」订阅的用户').i18n({"en": {"description": "Daily ledger report time (HH:MM in timezone); sent only to users who explicitly subscribed to daily reports."}}).effect('reload'),
9
+ reportWhenEmpty: Schema.boolean().default(false).description('当天没有记账时是否仍发送日报').i18n({"en": {"description": "Send a daily report even when no transactions were recorded that day."}}).effect('reload'),
10
10
  });
11
11
 
12
12
  export type LedgerConfig = Schema.Output<typeof ledgerConfig>;
package/src/console.ts ADDED
@@ -0,0 +1,47 @@
1
+ import {
2
+ definePluginConsoleRpc,
3
+ PluginConsoleRpcError,
4
+ type PluginConsoleRpc,
5
+ type PluginConsoleRpcCall,
6
+ } from 'zhin.js';
7
+ import { ledgerServiceToken } from './runtime.js';
8
+ import type { LedgerService } from './service.js';
9
+
10
+ function resolveService(
11
+ provided: LedgerService | undefined,
12
+ call: PluginConsoleRpcCall,
13
+ ): LedgerService {
14
+ if (provided) return provided;
15
+ const runtimeWithUse = call.runtime as { use?: (token: typeof ledgerServiceToken) => LedgerService } | undefined;
16
+ const fromRuntime = runtimeWithUse?.use?.(ledgerServiceToken);
17
+ if (fromRuntime) return fromRuntime;
18
+ throw new PluginConsoleRpcError('unavailable', '记账服务尚未就绪');
19
+ }
20
+
21
+ export function createLedgerConsoleRpc(service?: LedgerService): PluginConsoleRpc {
22
+ const rpc = definePluginConsoleRpc({
23
+ status: {
24
+ page: 'index',
25
+ requiredPermissions: ['ledger:read'],
26
+ async handler(_input, call) {
27
+ return resolveService(service, call).getConsoleSnapshot();
28
+ },
29
+ },
30
+ actions: {
31
+ 'run-daily-reports': {
32
+ page: 'index',
33
+ requiredPermissions: ['ledger:write'],
34
+ async handler(_input, call) {
35
+ const sent = await resolveService(service, call).runDailyReports();
36
+ return { sent };
37
+ },
38
+ },
39
+ },
40
+ });
41
+ const methods = rpc.methods as Map<string, ReturnType<PluginConsoleRpc['methods']['get']>>;
42
+ const originalGet = methods.get.bind(methods);
43
+ const originalHas = methods.has.bind(methods);
44
+ methods.get = (key: string) => originalGet(key === 'runDailyReports' ? 'run-daily-reports' : key);
45
+ methods.has = (key: string) => originalHas(key === 'runDailyReports' ? 'run-daily-reports' : key);
46
+ return rpc;
47
+ }
@@ -1,9 +1,11 @@
1
+ import { resolveLocale, type Locale } from 'zhin.js/i18n';
2
+ import { messages as t } from './i18n.js';
1
3
  import { parseAmountToCents } from './money.js';
2
4
 
3
5
  export const MAX_CATEGORY_LENGTH = 20;
4
6
  export const MAX_NOTE_LENGTH = 100;
5
7
 
6
- export const ADD_USAGE = '格式:记 <类别> <金额> [备注],例如:记 午饭 18.5 和同事';
8
+ export const ADD_USAGE = t('zh-CN', 'usage');
7
9
 
8
10
  export interface ParsedEntryInput {
9
11
  readonly category: string;
@@ -20,20 +22,21 @@ function rejected(reason: string): EntryInputResult {
20
22
  }
21
23
 
22
24
  /** Parses the text after `记`: category, amount, then an optional free-form note. */
23
- export function parseEntryInput(text: string): EntryInputResult {
25
+ export function parseEntryInput(text: string, locale?: Locale): EntryInputResult {
26
+ const usage = t(locale, 'usage');
24
27
  const words = text.trim().split(/\s+/u).filter((word) => word !== '');
25
- if (words.length < 2) return rejected(ADD_USAGE);
28
+ if (words.length < 2) return rejected(usage);
26
29
  const category = words[0]!;
27
30
  const amountText = words[1]!;
28
31
  const note = words.slice(2).join(' ');
29
- const amount = parseAmountToCents(amountText);
32
+ const amount = parseAmountToCents(amountText, locale);
30
33
  if (!amount.ok) {
31
34
  const categoryLooksLikeAmount = parseAmountToCents(category).ok;
32
- if (categoryLooksLikeAmount) return rejected(`类别要写在金额前面。${ADD_USAGE}`);
33
- return rejected(`${amount.reason}。${ADD_USAGE}`);
35
+ if (categoryLooksLikeAmount) return rejected(t(locale, 'order', { usage }));
36
+ return rejected(t(locale, 'amountError', { reason: amount.reason, usage }));
34
37
  }
35
- if (parseAmountToCents(category).ok) return rejected(`类别不能是数字。${ADD_USAGE}`);
36
- if ([...category].length > MAX_CATEGORY_LENGTH) return rejected(`类别最多 ${MAX_CATEGORY_LENGTH} 个字`);
37
- if ([...note].length > MAX_NOTE_LENGTH) return rejected(`备注最多 ${MAX_NOTE_LENGTH} 个字`);
38
+ if (parseAmountToCents(category).ok) return rejected(t(locale, 'categoryNumber', { usage }));
39
+ if ([...category].length > MAX_CATEGORY_LENGTH) return rejected(t(locale, 'categoryLimit', { max: MAX_CATEGORY_LENGTH }));
40
+ if ([...note].length > MAX_NOTE_LENGTH) return rejected(t(locale, 'noteLimit', { max: MAX_NOTE_LENGTH }));
38
41
  return { ok: true, entry: { category, amountCents: amount.cents, note } };
39
42
  }