codex-work-receipt 0.6.1 → 0.7.0
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.
- package/CHANGELOG.md +8 -1
- package/README.en.md +22 -3
- package/README.md +22 -3
- package/assets/codex-pet/ai-work-receipt/pet.json +6 -0
- package/assets/codex-pet/ai-work-receipt/spritesheet.webp +0 -0
- package/docs/cli.en.md +15 -3
- package/docs/cli.md +15 -3
- package/docs/codex-pet.en.md +60 -0
- package/docs/codex-pet.md +64 -0
- package/docs/codex-skill.en.md +9 -1
- package/docs/codex-skill.md +9 -1
- package/docs/data-schema.en.md +1 -1
- package/docs/data-schema.md +1 -1
- package/docs/images/codex-pet-showcase.png +0 -0
- package/package.json +2 -2
- package/skills/ai-work-receipt/SKILL.md +5 -1
- package/skills/ai-work-receipt/agents/openai.yaml +2 -2
- package/src/cli.mjs +42 -7
- package/src/core/args.mjs +33 -3
- package/src/core/fact-identity.mjs +7 -0
- package/src/core/metrics.mjs +13 -3
- package/src/core/pet-installer.mjs +50 -0
- package/src/core/presentation.mjs +9 -0
- package/src/core/range.mjs +40 -2
- package/src/core/receipt-record.mjs +24 -9
- package/src/core/selector.mjs +9 -7
- package/src/renderers/html.mjs +7 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.7.0 - 2026-07-19
|
|
4
|
+
|
|
5
|
+
- 新增 Codex 原生桌宠“票仔 · AI 小票工”,提供夜班绿票风格的核心状态与左右移动动画,并支持通过 `--install-pet`、`--uninstall-pet` 和 `--install-companion` 安装管理。
|
|
6
|
+
- AI 打工小票 Skill 新增“票仔,开今天的票”等自然语言入口。
|
|
7
|
+
- 新增 `--hours <1-168>` 滚动小时统计;“最近几个小时”默认按最近 3 小时处理。
|
|
8
|
+
- 滚动小时小票使用兼容 cwr1 摘要,避免与 cwr2 的“会话 × 自然日”规范事实冲突。
|
|
9
|
+
|
|
10
|
+
## 0.6.1
|
|
4
11
|
|
|
5
12
|
- 将 HTML 小票底部的重复渐变装饰替换为按小票编号生成的真实 Code 128 B 条形码,并保留适合浏览器和长图导出的静区与清晰度。
|
|
6
13
|
- 优化 multipart 扫码交互:单数据码继续与小程序码并排,多分片时先展示小程序码,再自动逐张轮播唯一可见的数据码,避免镜头同时识别多个二维码。
|
package/README.en.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<strong>Turn local Codex activity into a work receipt you can keep and share.</strong><br>
|
|
9
|
-
One command · Local-first ·
|
|
9
|
+
One command · Local-first · Codex pet · Three themes · Mobile QR import
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
@@ -26,7 +26,7 @@ Version `0.6.0` uses the cwr2 protocol to create stable privacy-safe facts for e
|
|
|
26
26
|
|
|
27
27
|
## Quickstart
|
|
28
28
|
|
|
29
|
-
Requires Node.js 20+ and local Codex session records. No clone required; choose today, the last 7 days, this week, or a specific session:
|
|
29
|
+
Requires Node.js 20+ and local Codex session records. No clone required; choose today, the last 3 hours, the last 7 days, this week, or a specific session:
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
32
|
npx codex-work-receipt@latest --lang en
|
|
@@ -38,11 +38,29 @@ Summarize all Codex activity from today:
|
|
|
38
38
|
npx codex-work-receipt@latest --today --lang en
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
Summarize the last 3 hours:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx codex-work-receipt@latest --hours 3 --lang en
|
|
45
|
+
```
|
|
46
|
+
|
|
41
47
|
HTML and structured data are written to `./codex-work-receipt-output/` by default. The generated page can save a high-resolution PNG containing the full receipt and QR import stub. Non-interactive options such as `--latest` and `--today` remain available; see the [CLI guide](docs/cli.en.md).
|
|
42
48
|
|
|
43
49
|
## Ask Codex directly
|
|
44
50
|
|
|
45
|
-
Install the AI Work Receipt skill
|
|
51
|
+
Install the AI Work Receipt skill and Ticket Buddy Codex pet together:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npx codex-work-receipt@latest --install-companion --lang en
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Restart Codex, open `Settings > Pets`, select Refresh, choose “票仔 · AI 小票工” (Ticket Buddy), and use `/pet` to wake it. Then ask:
|
|
58
|
+
|
|
59
|
+
> Ticket Buddy, create today's receipt.
|
|
60
|
+
|
|
61
|
+
> Ticket Buddy, create a receipt for the last three hours.
|
|
62
|
+
|
|
63
|
+
To install only the skill:
|
|
46
64
|
|
|
47
65
|
```bash
|
|
48
66
|
npx codex-work-receipt@latest --install-skill --lang en
|
|
@@ -62,6 +80,7 @@ The desktop page generates one or more privacy-safe data QR codes. The companion
|
|
|
62
80
|
|
|
63
81
|
- [CLI usage and options](docs/cli.en.md)
|
|
64
82
|
- [Codex Skill](docs/codex-skill.en.md)
|
|
83
|
+
- [Ticket Buddy Codex pet](docs/codex-pet.en.md)
|
|
65
84
|
- [Mobile QR import](docs/mobile-import.en.md)
|
|
66
85
|
- [Data schema and QR protocol](docs/data-schema.en.md)
|
|
67
86
|
- [Local data and privacy](docs/privacy.en.md)
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<strong>把本机 Codex 的工作记录,开成一张可以带走的 AI 打工小票。</strong><br>
|
|
9
|
-
一行命令 · 完全本地 ·
|
|
9
|
+
一行命令 · 完全本地 · Codex 桌宠 · 三种主题 · 扫码传到手机
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
## Quickstart
|
|
28
28
|
|
|
29
|
-
需要 Node.js 20 或更高版本,并且本机已经使用过 Codex。无需克隆仓库,运行后选择“今天 / 近 7 日 / 本周 / 指定会话”:
|
|
29
|
+
需要 Node.js 20 或更高版本,并且本机已经使用过 Codex。无需克隆仓库,运行后选择“今天 / 最近 3 小时 / 近 7 日 / 本周 / 指定会话”:
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
32
|
npx codex-work-receipt@latest
|
|
@@ -38,11 +38,29 @@ npx codex-work-receipt@latest
|
|
|
38
38
|
npx codex-work-receipt@latest --today
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
查看最近 3 小时的工作:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx codex-work-receipt@latest --hours 3
|
|
45
|
+
```
|
|
46
|
+
|
|
41
47
|
网页和结构数据默认保存在 `./codex-work-receipt-output/`。生成的网页支持一键保存包含主小票和二维码导入联的高清长图。也可以继续使用 `--latest`、`--today` 等非交互参数,详见 [CLI 使用文档](docs/cli.md)。
|
|
42
48
|
|
|
43
49
|
## 直接跟 Codex 说
|
|
44
50
|
|
|
45
|
-
|
|
51
|
+
一次安装 AI 打工小票 Skill 和“票仔”Codex 桌宠:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npx codex-work-receipt@latest --install-companion
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
重启 Codex 后,在 `Settings > Pets` 中点击 Refresh,选择“票仔 · AI 小票工”,再输入 `/pet` 唤醒。以后可以说:
|
|
58
|
+
|
|
59
|
+
> 票仔,开今天的票。
|
|
60
|
+
|
|
61
|
+
> 票仔,开最近三个小时的票。
|
|
62
|
+
|
|
63
|
+
如果只想安装 Skill:
|
|
46
64
|
|
|
47
65
|
```bash
|
|
48
66
|
npx codex-work-receipt@latest --install-skill
|
|
@@ -62,6 +80,7 @@ Codex 会选择统计范围和主题、执行命令并打开小票。详见 [Cod
|
|
|
62
80
|
|
|
63
81
|
- [CLI 使用与全部参数](docs/cli.md)
|
|
64
82
|
- [Codex Skill](docs/codex-skill.md)
|
|
83
|
+
- [Codex 桌宠“票仔”](docs/codex-pet.md)
|
|
65
84
|
- [手机扫码导入](docs/mobile-import.md)
|
|
66
85
|
- [数据结构与二维码协议](docs/data-schema.md)
|
|
67
86
|
- [本地数据与隐私说明](docs/privacy.md)
|
|
Binary file
|
package/docs/cli.en.md
CHANGED
|
@@ -17,7 +17,7 @@ Run:
|
|
|
17
17
|
npx codex-work-receipt@latest --lang en
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
Choose all activity today, the last 7 calendar days, this week, or a specific recent session. Session choices include their time range, turns, tool calls, and model for identification.
|
|
20
|
+
Choose all activity today, the last 3 hours, the last 7 calendar days, this week, or a specific recent session. Session choices include their time range, turns, tool calls, and model for identification.
|
|
21
21
|
|
|
22
22
|
## Non-interactive ranges
|
|
23
23
|
|
|
@@ -33,6 +33,14 @@ Summarize all Codex activity from today in your local timezone:
|
|
|
33
33
|
npx codex-work-receipt@latest --today --lang en
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
Summarize the last 3 hours:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx codex-work-receipt@latest --hours 3 --lang en
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`--hours` accepts an integer from 1 to 168 and filters exact timestamps, including windows that cross midnight. Rolling-hour receipts use the compatible cwr1 summary protocol and do not create cwr2 session-day canonical facts.
|
|
43
|
+
|
|
36
44
|
Last 7 calendar days, including today:
|
|
37
45
|
|
|
38
46
|
```bash
|
|
@@ -99,7 +107,8 @@ npx codex-work-receipt@latest --latest --lang en --no-open
|
|
|
99
107
|
|
|
100
108
|
| Option | Description |
|
|
101
109
|
| --- | --- |
|
|
102
|
-
| `--range <name>` | `latest`, `today`, `last-7-days`, or `this-week` |
|
|
110
|
+
| `--range <name>` | `latest`, `last-hours`, `today`, `last-7-days`, or `this-week` |
|
|
111
|
+
| `--hours <number>` | Summarize the last 1-168 hours |
|
|
103
112
|
| `--latest` | Summarize the latest active Codex session; default mode |
|
|
104
113
|
| `--today` | Summarize activity from today in the selected timezone |
|
|
105
114
|
| `--session <id>` | Summarize one specific Codex session |
|
|
@@ -109,6 +118,9 @@ npx codex-work-receipt@latest --latest --lang en --no-open
|
|
|
109
118
|
| `--output <file>` | Set the HTML output path |
|
|
110
119
|
| `--data-dir <directory>` | Set the local structured-history directory |
|
|
111
120
|
| `--install-skill` | Install the Codex AI Work Receipt skill |
|
|
121
|
+
| `--install-pet` | Install only the Ticket Buddy Codex pet |
|
|
122
|
+
| `--uninstall-pet` | Remove Ticket Buddy without deleting the skill or receipts |
|
|
123
|
+
| `--install-companion` | Install both the skill and Ticket Buddy Codex pet |
|
|
112
124
|
| `--no-open` | Do not open the generated page |
|
|
113
125
|
|
|
114
126
|
Run the built-in help at any time:
|
|
@@ -133,5 +145,5 @@ See the [data schema and QR protocol](data-schema.en.md).
|
|
|
133
145
|
|
|
134
146
|
- Codex is the only supported data source; Cursor and WorkBuddy are planned
|
|
135
147
|
- Changed-file and line counts are intentionally omitted until they can be measured consistently
|
|
136
|
-
- Calendar ranges filter
|
|
148
|
+
- Calendar ranges filter events by local date, rolling-hour ranges use exact timestamps, and both calculate per-session Token deltas
|
|
137
149
|
- Desktop PNG export is rendered locally in the browser and does not upload receipt data
|
package/docs/cli.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
npx codex-work-receipt@latest
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
命令行会让你选择:今天全部活动、最近 7 个自然日、本周,或最近的某个具体会话。选择具体会话时会展示起止时间、轮次、工具调用和模型,帮助辨认。
|
|
20
|
+
命令行会让你选择:今天全部活动、最近 3 小时、最近 7 个自然日、本周,或最近的某个具体会话。选择具体会话时会展示起止时间、轮次、工具调用和模型,帮助辨认。
|
|
21
21
|
|
|
22
22
|
## 非交互统计范围
|
|
23
23
|
|
|
@@ -33,6 +33,14 @@ npx codex-work-receipt@latest --latest
|
|
|
33
33
|
npx codex-work-receipt@latest --today
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
最近 3 小时:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx codex-work-receipt@latest --hours 3
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`--hours` 支持 1~168 的整数,并按精确时间戳筛选,可以跨越午夜。滚动小时小票使用兼容的 cwr1 摘要协议,不会写入 cwr2 的“会话 × 自然日”规范事实。
|
|
43
|
+
|
|
36
44
|
最近 7 个自然日(含今天):
|
|
37
45
|
|
|
38
46
|
```bash
|
|
@@ -99,7 +107,8 @@ npx codex-work-receipt@latest --latest --no-open
|
|
|
99
107
|
|
|
100
108
|
| 参数 | 说明 |
|
|
101
109
|
| --- | --- |
|
|
102
|
-
| `--range <name>` | `latest`、`today`、`last-7-days` 或 `this-week` |
|
|
110
|
+
| `--range <name>` | `latest`、`last-hours`、`today`、`last-7-days` 或 `this-week` |
|
|
111
|
+
| `--hours <number>` | 统计最近 1~168 小时;等价于 `last-hours` 范围 |
|
|
103
112
|
| `--latest` | 统计最近活跃的 Codex 会话,默认模式 |
|
|
104
113
|
| `--today` | 汇总指定时区今天发生的 Codex 活动 |
|
|
105
114
|
| `--session <id>` | 统计指定的 Codex 会话 |
|
|
@@ -109,6 +118,9 @@ npx codex-work-receipt@latest --latest --no-open
|
|
|
109
118
|
| `--output <file>` | 指定 HTML 输出路径 |
|
|
110
119
|
| `--data-dir <directory>` | 指定本地结构历史目录 |
|
|
111
120
|
| `--install-skill` | 安装 AI 打工小票 Codex Skill |
|
|
121
|
+
| `--install-pet` | 只安装“票仔”Codex 桌宠 |
|
|
122
|
+
| `--uninstall-pet` | 卸载“票仔”,不删除 Skill 或历史小票 |
|
|
123
|
+
| `--install-companion` | 同时安装 Skill 和“票仔”Codex 桌宠 |
|
|
112
124
|
| `--no-open` | 生成后不自动打开浏览器 |
|
|
113
125
|
|
|
114
126
|
也可以随时运行:
|
|
@@ -133,5 +145,5 @@ npx codex-work-receipt@latest --help
|
|
|
133
145
|
|
|
134
146
|
- 当前只支持 Codex;Cursor、WorkBuddy 等数据源仍在规划中
|
|
135
147
|
- 暂不统计修改文件数和代码行数,避免不同工具调用方式产生误导
|
|
136
|
-
-
|
|
148
|
+
- 自然日范围按本地日期筛选,滚动小时范围按精确时间戳筛选,并分别计算每个会话的 Token 区间增量
|
|
137
149
|
- 桌面网页导出 PNG 使用浏览器本地渲染,不上传小票数据
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Ticket Buddy Codex Pet
|
|
2
|
+
|
|
3
|
+
<p><a href="./codex-pet.md">中文</a> · <strong>English</strong> · <a href="../README.en.md">Back to README</a></p>
|
|
4
|
+
|
|
5
|
+
“Ticket Buddy · AI Work Receipt” is a custom companion for Codex's native Pets feature. It is a night-green thermal receipt printer with tiny ox horns and animated idle, working, waiting, ready, failed, and review states.
|
|
6
|
+
|
|
7
|
+
Ticket Buddy only presents Codex activity and provides companionship. It does not collect extra data, generate receipts automatically, or alter the current task. Receipt generation continues through the AI Work Receipt skill.
|
|
8
|
+
|
|
9
|
+
## Install the skill and pet together
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx codex-work-receipt@latest --install-companion --lang en
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
After installation:
|
|
16
|
+
|
|
17
|
+
1. Restart Codex.
|
|
18
|
+
2. Open `Settings > Pets`.
|
|
19
|
+
3. Select Refresh.
|
|
20
|
+
4. Choose “票仔 · AI 小票工” (Ticket Buddy).
|
|
21
|
+
5. Use `/pet` to wake it.
|
|
22
|
+
|
|
23
|
+
Then ask:
|
|
24
|
+
|
|
25
|
+
> Ticket Buddy, create today's receipt.
|
|
26
|
+
|
|
27
|
+
> Ticket Buddy, create a receipt for the latest session.
|
|
28
|
+
|
|
29
|
+
> Ticket Buddy, create a receipt for the last three hours.
|
|
30
|
+
|
|
31
|
+
“The last few hours” defaults to three hours.
|
|
32
|
+
|
|
33
|
+
## Install or remove only the pet
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx codex-work-receipt@latest --install-pet --lang en
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx codex-work-receipt@latest --uninstall-pet --lang en
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Removal only deletes `~/.codex/pets/ai-work-receipt/`. It leaves the skill, saved receipts, and other pets untouched.
|
|
44
|
+
|
|
45
|
+
## States
|
|
46
|
+
|
|
47
|
+
- Idle: quiet breathing and blinking.
|
|
48
|
+
- Running: focused button work shows that Codex is executing a task.
|
|
49
|
+
- Needs input: an open paw and head tilt show that Codex needs approval, a choice, or an answer.
|
|
50
|
+
- Ready: the pet scans the attached receipt to show that the task is complete and waiting for review.
|
|
51
|
+
- Failed or blocked: the pet looks deflated.
|
|
52
|
+
- Movement: separate leftward and rightward walking loops follow its movement direction.
|
|
53
|
+
|
|
54
|
+
Codex automatically controls state transitions and movement direction; a command cannot select an individual action row. A custom pet currently supplies only its name, description, and animation atlas. Selecting the pet returns to Codex or opens activity; it cannot run the receipt CLI directly.
|
|
55
|
+
|
|
56
|
+
## Availability
|
|
57
|
+
|
|
58
|
+
- Codex desktop app: floating companion and activity states.
|
|
59
|
+
- Codex CLI: terminal pets in compatible iTerm2, Kitty Graphics, or Sixel terminals.
|
|
60
|
+
- Codex IDE extension: no pet picker or floating pet overlay currently.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Codex 桌宠“票仔”
|
|
2
|
+
|
|
3
|
+
<p><strong>中文</strong> · <a href="./codex-pet.en.md">English</a> · <a href="../README.md">返回 README</a></p>
|
|
4
|
+
|
|
5
|
+
“票仔 · AI 小票工”是 AI 打工小票为 Codex 原生 Pets 功能制作的自定义桌宠。它是一台长着小牛角的夜班绿热敏打印机,会跟随 Codex 的空闲、工作、等待、完成和失败状态切换动画。
|
|
6
|
+
|
|
7
|
+
票仔只负责状态展示和陪伴,不会自动读取额外数据、生成小票或改变 Codex 的任务。开票仍然通过 AI 打工小票 Skill 完成。
|
|
8
|
+
|
|
9
|
+
## 一次安装 Skill 和票仔
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx codex-work-receipt@latest --install-companion
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
安装完成后:
|
|
16
|
+
|
|
17
|
+
1. 重启 Codex。
|
|
18
|
+
2. 打开 `Settings > Pets`。
|
|
19
|
+
3. 点击 Refresh。
|
|
20
|
+
4. 选择“票仔 · AI 小票工”。
|
|
21
|
+
5. 输入 `/pet` 唤醒票仔。
|
|
22
|
+
|
|
23
|
+
以后可以直接说:
|
|
24
|
+
|
|
25
|
+
> 票仔,开今天的票。
|
|
26
|
+
|
|
27
|
+
> 票仔,开最近一个会话的票。
|
|
28
|
+
|
|
29
|
+
> 票仔,开最近三个小时的票。
|
|
30
|
+
|
|
31
|
+
“最近几个小时”默认表示最近 3 小时。
|
|
32
|
+
|
|
33
|
+
## 只安装或卸载桌宠
|
|
34
|
+
|
|
35
|
+
只安装票仔:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx codex-work-receipt@latest --install-pet
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
卸载票仔:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx codex-work-receipt@latest --uninstall-pet
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
卸载只会删除 `~/.codex/pets/ai-work-receipt/`,不会删除 Skill、历史小票或其他宠物。
|
|
48
|
+
|
|
49
|
+
## 状态
|
|
50
|
+
|
|
51
|
+
- 空闲:安静呼吸和眨眼。
|
|
52
|
+
- 正在打工:专注操作打印按钮,表示 Codex 正在执行任务。
|
|
53
|
+
- 等你确认:抬手并歪头等待,表示 Codex 需要批准、选择或回答。
|
|
54
|
+
- 任务完成:查看票纸并左右扫视,表示任务已经完成、等待你回来查看。
|
|
55
|
+
- 失败或阻塞:小票工露出沮丧表情。
|
|
56
|
+
- 左右移动:根据移动方向播放对应步行动画。
|
|
57
|
+
|
|
58
|
+
Codex 会自动决定状态和移动方向,不能通过命令指定播放某一行动画。当前自定义 Pet 只能提供名称、描述和动画图集;点击票仔会返回 Codex 或打开活动列表,不能改成直接运行 CLI。
|
|
59
|
+
|
|
60
|
+
## 使用范围
|
|
61
|
+
|
|
62
|
+
- Codex 桌面端:支持悬浮桌宠和活动状态。
|
|
63
|
+
- Codex CLI:在支持 iTerm2、Kitty Graphics 或 Sixel 的终端里提供终端宠物。
|
|
64
|
+
- Codex IDE 扩展:当前不提供宠物选择器或悬浮桌宠。
|
package/docs/codex-skill.en.md
CHANGED
|
@@ -26,16 +26,24 @@ After installation, ask Codex:
|
|
|
26
26
|
|
|
27
27
|
> Generate today's AI work receipt with the diner theme.
|
|
28
28
|
|
|
29
|
+
> Ticket Buddy, create a receipt for the last three hours.
|
|
30
|
+
|
|
31
|
+
> Ticket Buddy, create a receipt for the last few hours.
|
|
32
|
+
|
|
29
33
|
> Generate an AI work receipt for the last seven days.
|
|
30
34
|
|
|
31
35
|
> Show how much Codex worked this week.
|
|
32
36
|
|
|
33
37
|
> Create a Chinese receipt for my latest session.
|
|
34
38
|
|
|
35
|
-
Codex will choose a specific session, today, the last seven days, or this week, plus the language and theme, then run the CLI and open the generated page.
|
|
39
|
+
Codex will choose a specific session, the last 1-168 hours, today, the last seven days, or this week, plus the language and theme, then run the CLI and open the generated page. “The last few hours” defaults to three hours.
|
|
40
|
+
|
|
41
|
+
If another Codex task is still running and you want the receipt immediately, start a separate local Codex chat. A message sent to the active chat may steer the current run or wait in its queue, depending on Follow-up behavior.
|
|
36
42
|
|
|
37
43
|
You can also invoke the skill explicitly with `$ai-work-receipt`.
|
|
38
44
|
|
|
45
|
+
See [Ticket Buddy Codex pet](codex-pet.en.md) for the companion setup.
|
|
46
|
+
|
|
39
47
|
## Update
|
|
40
48
|
|
|
41
49
|
Run the installation command again to safely replace an older version:
|
package/docs/codex-skill.md
CHANGED
|
@@ -26,16 +26,24 @@ Skill 会安装到:
|
|
|
26
26
|
|
|
27
27
|
> 生成我今天的 AI 打工小票,使用复古粉票。
|
|
28
28
|
|
|
29
|
+
> 票仔,开最近三个小时的票。
|
|
30
|
+
|
|
31
|
+
> 票仔,开最近几个小时的票。
|
|
32
|
+
|
|
29
33
|
> 生成我最近七天的 AI 打工小票。
|
|
30
34
|
|
|
31
35
|
> 看看本周 Codex 一共打了多少工。
|
|
32
36
|
|
|
33
37
|
> 用英文生成刚刚这次工作的 AI 打工小票。
|
|
34
38
|
|
|
35
|
-
Codex
|
|
39
|
+
Codex 会根据表达选择单次会话、最近 1~168 小时、今天、近七日或本周,以及语言和主题,然后执行命令并打开生成的网页。“最近几个小时”默认统计最近 3 小时。
|
|
40
|
+
|
|
41
|
+
如果当前 Codex 任务仍在运行,而你希望立刻查看小票,建议新开一个本地 Codex 会话再说“开今天的票”;在当前运行会话中发送的消息可能根据 Follow-up behavior 被用于 Steer 或 Queue。
|
|
36
42
|
|
|
37
43
|
也可以通过 `$ai-work-receipt` 显式调用这个 Skill。
|
|
38
44
|
|
|
45
|
+
桌宠安装和使用见 [Codex 桌宠“票仔”](codex-pet.md)。
|
|
46
|
+
|
|
39
47
|
## 更新
|
|
40
48
|
|
|
41
49
|
重新运行安装命令即可安全覆盖旧版本:
|
package/docs/data-schema.en.md
CHANGED
|
@@ -32,7 +32,7 @@ cwr2p.<transferId>.<partIndex>.<partCount>.<totalChecksum>.<partChecksum>.<chunk
|
|
|
32
32
|
|
|
33
33
|
The mini program validates the prefix and checksum before decompressing and parsing the payload. Future schema versions use the compact `v` field for compatibility.
|
|
34
34
|
|
|
35
|
-
Compact field `o` explicitly carries `latest`, `session`, `today`, `last-7-days`, or `this-week`. `d[3]` and `d[4]` carry
|
|
35
|
+
Compact field `o` explicitly carries `latest`, `session`, `last-hours`, `today`, `last-7-days`, or `this-week`. `d[3]` and `d[4]` carry date boundaries. Today, last-seven-days, and this-week receipts continue to use cwr2 canonical facts. `last-hours` is a rolling window, so it uses the compatible cwr1 summary payload to avoid colliding with session-day fact identities. The updated mini program can still import older QR codes.
|
|
36
36
|
|
|
37
37
|
`presentation.compensation` contains playful AI work points, not real API cost. For compatibility with the current Chinese mini program, QR display copy remains Chinese while compact fields `l` and `r` carry the desktop locale and language-neutral role ID. English HTML and local JSON remain fully localized.
|
|
38
38
|
|
package/docs/data-schema.md
CHANGED
|
@@ -32,7 +32,7 @@ cwr2p.<transferId>.<partIndex>.<partCount>.<totalChecksum>.<partChecksum>.<chunk
|
|
|
32
32
|
|
|
33
33
|
小程序应检查前缀和校验值,再解压并解析数据。未来结构升级通过 `v` 字段兼容。
|
|
34
34
|
|
|
35
|
-
精简字段 `o` 显式携带 `latest`、`session`、`today`、`last-7-days` 或 `this-week`;`d[3]` 和 `d[4]`
|
|
35
|
+
精简字段 `o` 显式携带 `latest`、`session`、`last-hours`、`today`、`last-7-days` 或 `this-week`;`d[3]` 和 `d[4]` 携带日期边界。今日、近 7 日和本周继续使用 cwr2 canonical facts。`last-hours` 是滚动时间窗,为避免与“会话 × 自然日”事实发生身份冲突,使用兼容的 cwr1 摘要载荷。旧二维码仍可由新版小程序导入。
|
|
36
36
|
|
|
37
37
|
`presentation.compensation` 是娱乐化 AI 工分,不代表真实 API 费用。为兼容当前中文小程序,二维码展示文案继续使用中文,并通过精简字段 `l` 和 `r` 携带桌面语言及工种语义 ID;英文 HTML 和本地 JSON 不受影响。
|
|
38
38
|
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-work-receipt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Generate a privacy-first Codex work receipt from local session metadata.",
|
|
6
6
|
"author": "a-bai-2026",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"scripts": {
|
|
28
28
|
"receipt": "node src/cli.mjs",
|
|
29
29
|
"test": "node --test",
|
|
30
|
-
"check": "node --check src/cli.mjs && node --check src/core/skill-installer.mjs",
|
|
30
|
+
"check": "node --check src/cli.mjs && node --check src/core/skill-installer.mjs && node --check src/core/pet-installer.mjs && node --check src/core/range.mjs",
|
|
31
31
|
"prepublishOnly": "npm test && npm run check"
|
|
32
32
|
},
|
|
33
33
|
"files": [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ai-work-receipt
|
|
3
|
-
description: Generate and open a privacy-first local AI work receipt from Codex session metadata. Use when the user asks Codex to create, regenerate, open, or locate an “AI
|
|
3
|
+
description: Generate and open a privacy-first local AI work receipt from Codex session metadata. Use when the user asks Codex or 票仔 to create, regenerate, open, or locate an “AI 打工小票”, “工票”, or “AI work receipt” for the latest session, the last few hours, today, the last seven days, or this week, in Chinese or English. Do not use for real invoices, salary calculations, API billing, or non-Codex activity.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# AI 打工小票
|
|
@@ -10,6 +10,9 @@ Use the published `codex-work-receipt` CLI as the only statistics and rendering
|
|
|
10
10
|
## Interpret the request
|
|
11
11
|
|
|
12
12
|
- Use `--latest` for “刚刚”“这次”“最近一次”“上一段工作” or an unspecified request for one receipt.
|
|
13
|
+
- Use `--hours <N>` for “最近 N 小时”“过去 N 小时” or “近 N 小时”. Accept integers from 1 to 168.
|
|
14
|
+
- Use `--hours 3` for an unspecified “最近几个小时”“过去几个小时” request.
|
|
15
|
+
- Use `--hours 12` for “最近半天”.
|
|
13
16
|
- Use `--today` for “今天”“今日”“今天全部工作” or a daily summary.
|
|
14
17
|
- Use `--range last-7-days` for “最近七天”“近 7 日” or a rolling seven-calendar-day summary.
|
|
15
18
|
- Use `--range this-week` for “本周”“这周” or a Monday-to-now summary.
|
|
@@ -21,6 +24,7 @@ Use the published `codex-work-receipt` CLI as the only statistics and rendering
|
|
|
21
24
|
- Use `--lang en` when the user requests English or asks for the receipt in English. Use `--lang zh-CN` otherwise.
|
|
22
25
|
- If the user asks to choose interactively, run the CLI without a range flag so it can show the local selector.
|
|
23
26
|
- If the user asks for an unsupported custom date range, explain the available ranges and do not invent flags.
|
|
27
|
+
- Treat requests addressed to “票仔” the same as direct AI work receipt requests, for example “票仔,开今天的票”.
|
|
24
28
|
|
|
25
29
|
## Execute
|
|
26
30
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
interface:
|
|
2
2
|
display_name: "AI 打工小票"
|
|
3
|
-
short_description: "
|
|
4
|
-
default_prompt: "使用 $ai-work-receipt
|
|
3
|
+
short_description: "让票仔按会话、小时或自然日生成 AI 打工小票"
|
|
4
|
+
default_prompt: "使用 $ai-work-receipt 让票仔给我刚刚这次 Codex 工作生成一张 AI 打工小票。"
|
package/src/cli.mjs
CHANGED
|
@@ -9,12 +9,13 @@ import QRCode from "qrcode";
|
|
|
9
9
|
import { parseArgs, printHelp } from "./core/args.mjs";
|
|
10
10
|
import { buildCanonicalFacts } from "./core/fact-buckets.mjs";
|
|
11
11
|
import { collectMetrics } from "./core/metrics.mjs";
|
|
12
|
-
import {
|
|
12
|
+
import { getScopeLabel } from "./core/presentation.mjs";
|
|
13
13
|
import { encodeReceiptPayloads } from "./core/qr-payload.mjs";
|
|
14
14
|
import { outputSlugForRange, resolveRange } from "./core/range.mjs";
|
|
15
15
|
import { buildReceiptRecord, persistReceiptRecord } from "./core/receipt-record.mjs";
|
|
16
16
|
import { promptForRange } from "./core/selector.mjs";
|
|
17
17
|
import { installCodexSkill } from "./core/skill-installer.mjs";
|
|
18
|
+
import { installCodexPet, uninstallCodexPet } from "./core/pet-installer.mjs";
|
|
18
19
|
import { formatNumber } from "./lib/time.mjs";
|
|
19
20
|
import { listRecentCodexSessions, loadCodexSessions } from "./parsers/codex.mjs";
|
|
20
21
|
import { renderHtml } from "./renderers/html.mjs";
|
|
@@ -64,6 +65,33 @@ async function main() {
|
|
|
64
65
|
}
|
|
65
66
|
return;
|
|
66
67
|
}
|
|
68
|
+
if (options.installPet || options.installCompanion) {
|
|
69
|
+
const installedPet = installCodexPet({ projectDir: PROJECT_DIR });
|
|
70
|
+
const installedSkill = options.installCompanion
|
|
71
|
+
? installCodexSkill({ projectDir: PROJECT_DIR })
|
|
72
|
+
: null;
|
|
73
|
+
if (options.locale === "en") {
|
|
74
|
+
if (installedSkill) console.log(`AI Work Receipt skill installed: ${installedSkill.targetDir}`);
|
|
75
|
+
console.log(`Codex pet installed: ${installedPet.targetDir}`);
|
|
76
|
+
console.log("Restart Codex, open Settings > Pets, select Refresh, then choose 票仔 · AI 小票工 (Ticket Buddy).");
|
|
77
|
+
console.log("Use /pet to wake it. You can then ask: Ticket Buddy, create a receipt for today.");
|
|
78
|
+
} else {
|
|
79
|
+
if (installedSkill) console.log(`AI 打工小票 Skill 已安装:${installedSkill.targetDir}`);
|
|
80
|
+
console.log(`Codex 桌宠已安装:${installedPet.targetDir}`);
|
|
81
|
+
console.log("请重启 Codex,打开 Settings > Pets,点击 Refresh 后选择“票仔 · AI 小票工”。");
|
|
82
|
+
console.log("输入 /pet 唤醒票仔;以后可以说:票仔,开今天的票。");
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (options.uninstallPet) {
|
|
87
|
+
const removed = uninstallCodexPet();
|
|
88
|
+
if (options.locale === "en") {
|
|
89
|
+
console.log(removed.existed ? `Codex pet removed: ${removed.targetDir}` : "Codex pet was not installed.");
|
|
90
|
+
} else {
|
|
91
|
+
console.log(removed.existed ? `Codex 桌宠已卸载:${removed.targetDir}` : "尚未安装 AI 打工小票桌宠。");
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
67
95
|
|
|
68
96
|
if (!options.modeExplicit && process.stdin.isTTY && process.stdout.isTTY) {
|
|
69
97
|
const selected = await promptForRange({
|
|
@@ -73,13 +101,16 @@ async function main() {
|
|
|
73
101
|
});
|
|
74
102
|
options.mode = selected.mode;
|
|
75
103
|
options.sessionId = selected.sessionId;
|
|
104
|
+
options.hours = selected.hours || options.hours;
|
|
76
105
|
}
|
|
77
106
|
|
|
78
|
-
const range = resolveRange(options.mode, options.timezone, new Date(), options.sessionId);
|
|
107
|
+
const range = resolveRange(options.mode, options.timezone, new Date(), options.sessionId, options.hours);
|
|
79
108
|
const sessions = loadCodexSessions(range);
|
|
80
109
|
const metrics = collectMetrics(sessions, range);
|
|
81
110
|
const observedAt = new Date().toISOString();
|
|
82
|
-
const canonical =
|
|
111
|
+
const canonical = range.scope === "last-hours"
|
|
112
|
+
? {}
|
|
113
|
+
: buildCanonicalFacts(sessions, range, { observedAt });
|
|
83
114
|
const record = buildReceiptRecord(metrics, options.theme, options.locale, canonical);
|
|
84
115
|
const qrPayloads = encodeReceiptPayloads(record);
|
|
85
116
|
const dataQrDataUrls = await Promise.all(qrPayloads.map((payload) => QRCode.toDataURL(payload, {
|
|
@@ -109,17 +140,21 @@ async function main() {
|
|
|
109
140
|
console.log(`Generated HTML: ${outputFile}`);
|
|
110
141
|
console.log(`Structured data: ${persisted.companionPath}`);
|
|
111
142
|
console.log(`Local history: ${persisted.receiptPath}`);
|
|
112
|
-
console.log(`Range: ${
|
|
143
|
+
console.log(`Range: ${getScopeLabel(record.source.scope, options.locale, record.source.hours)} · ${record.stats.session_count} session(s)`);
|
|
113
144
|
console.log(`Stats: ${record.stats.completed_turns} turns · ${formatNumber(record.stats.tokens.total_tokens, options.locale)} Tokens · ${record.stats.tool_calls} tool calls`);
|
|
114
|
-
console.log(
|
|
145
|
+
console.log(record.manifest
|
|
146
|
+
? `Data QR: ${qrPayloads.length} code(s) · ${record.manifest.fact_count} canonical fact(s) · schema v${record.schema_version}`
|
|
147
|
+
: `Data QR: ${qrPayloads.length} code(s) · rolling summary · schema v${record.schema_version}`);
|
|
115
148
|
if (!miniProgramCodeDataUrl) console.log("Mini-program code: not configured; using the explicit placeholder");
|
|
116
149
|
} else {
|
|
117
150
|
console.log(`已生成网页:${outputFile}`);
|
|
118
151
|
console.log(`结构数据:${persisted.companionPath}`);
|
|
119
152
|
console.log(`本地历史:${persisted.receiptPath}`);
|
|
120
|
-
console.log(`统计范围:${
|
|
153
|
+
console.log(`统计范围:${getScopeLabel(record.source.scope, options.locale, record.source.hours)} · ${record.stats.session_count} 个会话`);
|
|
121
154
|
console.log(`统计:${record.stats.completed_turns} 轮 · ${formatNumber(record.stats.tokens.total_tokens, options.locale)} Token · ${record.stats.tool_calls} 次工具调用`);
|
|
122
|
-
console.log(
|
|
155
|
+
console.log(record.manifest
|
|
156
|
+
? `数据二维码:${qrPayloads.length} 个 · ${record.manifest.fact_count} 条规范事实 · schema v${record.schema_version}`
|
|
157
|
+
: `数据二维码:${qrPayloads.length} 个 · 滚动摘要 · schema v${record.schema_version}`);
|
|
123
158
|
if (!miniProgramCodeDataUrl) console.log("小程序码:尚未配置,页面使用明确占位符");
|
|
124
159
|
}
|
|
125
160
|
if (options.open) openFile(outputFile);
|
package/src/core/args.mjs
CHANGED
|
@@ -7,13 +7,16 @@ Codex AI Work Receipt
|
|
|
7
7
|
Usage:
|
|
8
8
|
npx codex-work-receipt@latest
|
|
9
9
|
npx codex-work-receipt@latest --latest --lang en
|
|
10
|
+
npx codex-work-receipt@latest --hours 3 --lang en
|
|
10
11
|
npx codex-work-receipt@latest --today --lang en
|
|
11
12
|
npx codex-work-receipt@latest --range last-7-days --lang en
|
|
12
13
|
npx codex-work-receipt@latest --range this-week --lang en
|
|
13
14
|
npx codex-work-receipt@latest --install-skill --lang en
|
|
15
|
+
npx codex-work-receipt@latest --install-companion --lang en
|
|
14
16
|
|
|
15
17
|
Options:
|
|
16
|
-
--range <name> Range: latest, today, last-7-days, this-week
|
|
18
|
+
--range <name> Range: latest, last-hours, today, last-7-days, this-week
|
|
19
|
+
--hours <number> Summarize the last 1-168 hours
|
|
17
20
|
--latest Summarize the latest active Codex session (default)
|
|
18
21
|
--today Summarize all Codex activity from today
|
|
19
22
|
--session <id> Summarize one specific Codex session
|
|
@@ -23,6 +26,9 @@ Options:
|
|
|
23
26
|
--output <file> Set the generated HTML path
|
|
24
27
|
--data-dir <directory> Set the local structured-history directory
|
|
25
28
|
--install-skill Install the natural-language Codex skill
|
|
29
|
+
--install-pet Install the Codex pet only
|
|
30
|
+
--uninstall-pet Remove the installed Codex pet
|
|
31
|
+
--install-companion Install both the skill and Codex pet
|
|
26
32
|
--no-open Do not open the browser after generation
|
|
27
33
|
--help Show help
|
|
28
34
|
` : `
|
|
@@ -31,13 +37,16 @@ Codex AI 打工小票
|
|
|
31
37
|
用法:
|
|
32
38
|
npx codex-work-receipt@latest
|
|
33
39
|
npx codex-work-receipt@latest --latest
|
|
40
|
+
npx codex-work-receipt@latest --hours 3
|
|
34
41
|
npx codex-work-receipt@latest --today
|
|
35
42
|
npx codex-work-receipt@latest --range last-7-days
|
|
36
43
|
npx codex-work-receipt@latest --range this-week
|
|
37
44
|
npx codex-work-receipt@latest --install-skill
|
|
45
|
+
npx codex-work-receipt@latest --install-companion
|
|
38
46
|
|
|
39
47
|
选项:
|
|
40
|
-
--range <name> 统计范围:latest、today、last-7-days、this-week
|
|
48
|
+
--range <name> 统计范围:latest、last-hours、today、last-7-days、this-week
|
|
49
|
+
--hours <number> 统计最近 1~168 小时
|
|
41
50
|
--latest 统计最近活跃的 Codex 会话(默认)
|
|
42
51
|
--today 统计本地时区今天发生的全部 Codex 活动
|
|
43
52
|
--session <id> 统计指定的 Codex 会话
|
|
@@ -47,6 +56,9 @@ Codex AI 打工小票
|
|
|
47
56
|
--output <file> 指定生成的 HTML 文件,默认写入 ./codex-work-receipt-output/
|
|
48
57
|
--data-dir <directory> 指定本地结构数据目录
|
|
49
58
|
--install-skill 安装可通过自然语言调用的 Codex Skill
|
|
59
|
+
--install-pet 只安装 Codex 桌宠
|
|
60
|
+
--uninstall-pet 卸载 AI 打工小票 Codex 桌宠
|
|
61
|
+
--install-companion 同时安装 Skill 和 Codex 桌宠
|
|
50
62
|
--no-open 生成后不自动打开浏览器
|
|
51
63
|
--help 显示帮助
|
|
52
64
|
`);
|
|
@@ -57,12 +69,16 @@ export function parseArgs(argv) {
|
|
|
57
69
|
mode: "latest",
|
|
58
70
|
modeExplicit: false,
|
|
59
71
|
sessionId: null,
|
|
72
|
+
hours: null,
|
|
60
73
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "Asia/Shanghai",
|
|
61
74
|
locale: "zh-CN",
|
|
62
75
|
theme: "classic",
|
|
63
76
|
output: null,
|
|
64
77
|
dataDir: null,
|
|
65
78
|
installSkill: false,
|
|
79
|
+
installPet: false,
|
|
80
|
+
uninstallPet: false,
|
|
81
|
+
installCompanion: false,
|
|
66
82
|
open: true,
|
|
67
83
|
};
|
|
68
84
|
|
|
@@ -73,6 +89,7 @@ export function parseArgs(argv) {
|
|
|
73
89
|
["--output", "output"],
|
|
74
90
|
["--data-dir", "dataDir"],
|
|
75
91
|
["--session", "sessionId"],
|
|
92
|
+
["--hours", "hours"],
|
|
76
93
|
]);
|
|
77
94
|
|
|
78
95
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -83,7 +100,10 @@ export function parseArgs(argv) {
|
|
|
83
100
|
} else if (argument === "--today") {
|
|
84
101
|
result.mode = "today";
|
|
85
102
|
result.modeExplicit = true;
|
|
86
|
-
} else if (argument === "--
|
|
103
|
+
} else if (argument === "--install-pet") result.installPet = true;
|
|
104
|
+
else if (argument === "--uninstall-pet") result.uninstallPet = true;
|
|
105
|
+
else if (argument === "--install-companion") result.installCompanion = true;
|
|
106
|
+
else if (argument === "--range") {
|
|
87
107
|
const value = argv[++index];
|
|
88
108
|
if (!value) throw new Error("--range 需要提供值");
|
|
89
109
|
const scope = normalizeScope(value);
|
|
@@ -101,6 +121,9 @@ export function parseArgs(argv) {
|
|
|
101
121
|
if (key === "sessionId") {
|
|
102
122
|
result.mode = "session";
|
|
103
123
|
result.modeExplicit = true;
|
|
124
|
+
} else if (key === "hours") {
|
|
125
|
+
result.mode = "last-hours";
|
|
126
|
+
result.modeExplicit = true;
|
|
104
127
|
}
|
|
105
128
|
} else throw new Error(`不认识的参数:${argument}`);
|
|
106
129
|
}
|
|
@@ -111,5 +134,12 @@ export function parseArgs(argv) {
|
|
|
111
134
|
if (!new Set(["zh-CN", "en"]).has(result.locale)) {
|
|
112
135
|
throw new Error(`不支持的语言:${result.locale}`);
|
|
113
136
|
}
|
|
137
|
+
if (result.hours !== null) {
|
|
138
|
+
result.hours = Number(result.hours);
|
|
139
|
+
if (!Number.isInteger(result.hours) || result.hours < 1 || result.hours > 168) {
|
|
140
|
+
throw new Error("--hours 需要 1 至 168 之间的整数");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (result.mode === "last-hours" && result.hours === null) result.hours = 3;
|
|
114
144
|
return result;
|
|
115
145
|
}
|
|
@@ -28,12 +28,19 @@ export function buildLogicalReceiptKey(metrics) {
|
|
|
28
28
|
if (metrics.mode === "latest" || metrics.mode === "session") {
|
|
29
29
|
return `${metrics.mode}:${sessionPart}:${metrics.timezone}`;
|
|
30
30
|
}
|
|
31
|
+
if (metrics.mode === "last-hours") {
|
|
32
|
+
return `last-hours:${metrics.windowHours}:${metrics.windowStartAt.toISOString()}:${metrics.windowEndAt.toISOString()}:${metrics.timezone}`;
|
|
33
|
+
}
|
|
31
34
|
if (metrics.mode === "this-week") {
|
|
32
35
|
return `this-week:${metrics.rangeStartDate}:${metrics.timezone}`;
|
|
33
36
|
}
|
|
34
37
|
return `${metrics.mode}:${metrics.rangeStartDate}:${metrics.rangeEndDate}:${metrics.timezone}`;
|
|
35
38
|
}
|
|
36
39
|
|
|
40
|
+
export function buildSummaryReceiptId(logicalReceiptKey) {
|
|
41
|
+
return stableId("cwr", "codex-work-receipt/receipt/v1", [logicalReceiptKey]);
|
|
42
|
+
}
|
|
43
|
+
|
|
37
44
|
export function buildProtocolReceiptId(sourceVersion, logicalReceiptKey) {
|
|
38
45
|
return stableId("cwr2", "codex-work-receipt/receipt/v2", [sourceVersion, logicalReceiptKey]);
|
|
39
46
|
}
|
package/src/core/metrics.mjs
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { dateKey, rowDate } from "../lib/time.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
calendarDayCount,
|
|
4
|
+
isCalendarScope,
|
|
5
|
+
isDateInRange,
|
|
6
|
+
isTimeWindowScope,
|
|
7
|
+
} from "./range.mjs";
|
|
3
8
|
import { selectWorkProfileId } from "./presentation.mjs";
|
|
4
9
|
|
|
5
10
|
function zeroUsage() {
|
|
@@ -28,7 +33,8 @@ function sessionTokenUsage(rows, range) {
|
|
|
28
33
|
for (const row of events) {
|
|
29
34
|
const currentUsage = row.payload.info.total_token_usage;
|
|
30
35
|
const date = rowDate(row);
|
|
31
|
-
const selected = !isCalendarScope(range.scope)
|
|
36
|
+
const selected = (!isCalendarScope(range.scope) && !isTimeWindowScope(range.scope))
|
|
37
|
+
|| isDateInRange(date, range);
|
|
32
38
|
|
|
33
39
|
for (const key of Object.keys(totals)) {
|
|
34
40
|
const current = Math.max(0, Number(currentUsage[key] || 0));
|
|
@@ -56,6 +62,7 @@ function calculateWorkPoints(metrics) {
|
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
function emptyRangeMessage(range) {
|
|
65
|
+
if (range.scope === "last-hours") return `最近 ${range.hours} 小时没有找到 Codex 活动`;
|
|
59
66
|
if (range.scope === "today") return `没有找到 ${range.targetDate} 的 Codex 活动`;
|
|
60
67
|
if (range.scope === "last-7-days") return `没有找到 ${range.startDate} 至 ${range.endDate} 的 Codex 活动`;
|
|
61
68
|
if (range.scope === "this-week") return `本周暂时没有找到 Codex 活动`;
|
|
@@ -78,7 +85,7 @@ export function collectMetrics(sessions, range) {
|
|
|
78
85
|
const models = new Set();
|
|
79
86
|
|
|
80
87
|
for (const session of sessions) {
|
|
81
|
-
const scopedRows = isCalendarScope(range.scope)
|
|
88
|
+
const scopedRows = isCalendarScope(range.scope) || isTimeWindowScope(range.scope)
|
|
82
89
|
? session.rows.filter((row) => isDateInRange(rowDate(row), range))
|
|
83
90
|
: session.rows;
|
|
84
91
|
|
|
@@ -134,6 +141,9 @@ export function collectMetrics(sessions, range) {
|
|
|
134
141
|
targetDate: range.targetDate,
|
|
135
142
|
rangeStartDate,
|
|
136
143
|
rangeEndDate,
|
|
144
|
+
windowStartAt: range.startAt,
|
|
145
|
+
windowEndAt: range.endAt,
|
|
146
|
+
windowHours: range.hours,
|
|
137
147
|
calendarDayCount: isCalendarScope(range.scope) ? calendarDayCount(range) : 1,
|
|
138
148
|
activeDayCount: Math.max(1, activeDateKeys.size),
|
|
139
149
|
sessionIds,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export const CODEX_PET_NAME = "ai-work-receipt";
|
|
6
|
+
|
|
7
|
+
export function getCodexPetHome({ homeDir = os.homedir(), codexHome = null } = {}) {
|
|
8
|
+
return path.resolve(codexHome || process.env.CODEX_HOME || path.join(homeDir, ".codex"));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getCodexPetInstallPath(options = {}) {
|
|
12
|
+
return path.join(getCodexPetHome(options), "pets", CODEX_PET_NAME);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function installCodexPet({ projectDir, homeDir = os.homedir(), codexHome = null } = {}) {
|
|
16
|
+
if (!projectDir) throw new Error("安装 Pet 时缺少项目目录");
|
|
17
|
+
|
|
18
|
+
const sourceDir = path.join(projectDir, "assets", "codex-pet", CODEX_PET_NAME);
|
|
19
|
+
const sourceManifest = path.join(sourceDir, "pet.json");
|
|
20
|
+
const sourceSpritesheet = path.join(sourceDir, "spritesheet.webp");
|
|
21
|
+
if (!fs.existsSync(sourceManifest) || !fs.existsSync(sourceSpritesheet)) {
|
|
22
|
+
throw new Error(`发布包中缺少 Codex Pet:${sourceDir}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const targetDir = getCodexPetInstallPath({ homeDir, codexHome });
|
|
26
|
+
const targetParent = path.dirname(targetDir);
|
|
27
|
+
const stagingDir = path.join(
|
|
28
|
+
targetParent,
|
|
29
|
+
`.${CODEX_PET_NAME}.install-${process.pid}-${Date.now()}`,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
fs.mkdirSync(targetParent, { recursive: true });
|
|
33
|
+
try {
|
|
34
|
+
fs.cpSync(sourceDir, stagingDir, { recursive: true });
|
|
35
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
36
|
+
fs.renameSync(stagingDir, targetDir);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { sourceDir, targetDir };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function uninstallCodexPet({ homeDir = os.homedir(), codexHome = null } = {}) {
|
|
46
|
+
const targetDir = getCodexPetInstallPath({ homeDir, codexHome });
|
|
47
|
+
const existed = fs.existsSync(targetDir);
|
|
48
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
49
|
+
return { targetDir, existed };
|
|
50
|
+
}
|
|
@@ -93,6 +93,7 @@ const RECEIPT_COPY = {
|
|
|
93
93
|
scope: {
|
|
94
94
|
latest: "最近一次会话",
|
|
95
95
|
session: "指定会话",
|
|
96
|
+
"last-hours": "最近 {hours} 小时",
|
|
96
97
|
today: "今日全部会话",
|
|
97
98
|
"last-7-days": "最近 7 个自然日",
|
|
98
99
|
"this-week": "本周全部会话",
|
|
@@ -165,6 +166,7 @@ const RECEIPT_COPY = {
|
|
|
165
166
|
scope: {
|
|
166
167
|
latest: "Latest session",
|
|
167
168
|
session: "Selected session",
|
|
169
|
+
"last-hours": "Last {hours} hours",
|
|
168
170
|
today: "All sessions today",
|
|
169
171
|
"last-7-days": "Last 7 calendar days",
|
|
170
172
|
"this-week": "All sessions this week",
|
|
@@ -225,6 +227,7 @@ const COMPENSATION_COPY = {
|
|
|
225
227
|
"zh-CN": {
|
|
226
228
|
latest: "本单工资",
|
|
227
229
|
session: "本单工资",
|
|
230
|
+
"last-hours": "本段工资",
|
|
228
231
|
today: "本日工资",
|
|
229
232
|
"last-7-days": "近七日工资",
|
|
230
233
|
"this-week": "本周工资",
|
|
@@ -234,6 +237,7 @@ const COMPENSATION_COPY = {
|
|
|
234
237
|
en: {
|
|
235
238
|
latest: "SHIFT PAY",
|
|
236
239
|
session: "SHIFT PAY",
|
|
240
|
+
"last-hours": "WINDOW PAY",
|
|
237
241
|
today: "TODAY'S PAY",
|
|
238
242
|
"last-7-days": "7-DAY PAY",
|
|
239
243
|
"this-week": "THIS WEEK'S PAY",
|
|
@@ -262,6 +266,11 @@ export function getReceiptCopy(locale = DEFAULT_LOCALE) {
|
|
|
262
266
|
return RECEIPT_COPY[SUPPORTED_LOCALES.has(locale) ? locale : DEFAULT_LOCALE];
|
|
263
267
|
}
|
|
264
268
|
|
|
269
|
+
export function getScopeLabel(scope, locale = DEFAULT_LOCALE, hours = null) {
|
|
270
|
+
const template = getReceiptCopy(locale).scope[scope] || getReceiptCopy(locale).scope.latest;
|
|
271
|
+
return String(template).replaceAll("{hours}", String(hours || 3));
|
|
272
|
+
}
|
|
273
|
+
|
|
265
274
|
export function buildCompensation(scope, amount, locale = DEFAULT_LOCALE) {
|
|
266
275
|
const copy = COMPENSATION_COPY[SUPPORTED_LOCALES.has(locale) ? locale : DEFAULT_LOCALE];
|
|
267
276
|
return {
|
package/src/core/range.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { dateKey } from "../lib/time.mjs";
|
|
|
3
3
|
export const RECEIPT_SCOPES = new Set([
|
|
4
4
|
"latest",
|
|
5
5
|
"session",
|
|
6
|
+
"last-hours",
|
|
6
7
|
"today",
|
|
7
8
|
"last-7-days",
|
|
8
9
|
"this-week",
|
|
@@ -11,6 +12,8 @@ export const RECEIPT_SCOPES = new Set([
|
|
|
11
12
|
const SCOPE_ALIASES = new Map([
|
|
12
13
|
["latest", "latest"],
|
|
13
14
|
["session", "session"],
|
|
15
|
+
["hours", "last-hours"],
|
|
16
|
+
["last-hours", "last-hours"],
|
|
14
17
|
["today", "today"],
|
|
15
18
|
["7d", "last-7-days"],
|
|
16
19
|
["last-7-days", "last-7-days"],
|
|
@@ -38,7 +41,13 @@ function mondayDateKey(value) {
|
|
|
38
41
|
return shiftDateKey(value, -daysSinceMonday);
|
|
39
42
|
}
|
|
40
43
|
|
|
41
|
-
|
|
44
|
+
function floorToMinute(value) {
|
|
45
|
+
const date = new Date(value);
|
|
46
|
+
date.setUTCSeconds(0, 0);
|
|
47
|
+
return date;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function resolveRange(scope, timezone, now = new Date(), sessionId = null, hours = null) {
|
|
42
51
|
const normalizedScope = normalizeScope(scope);
|
|
43
52
|
if (!normalizedScope || !RECEIPT_SCOPES.has(normalizedScope)) {
|
|
44
53
|
throw new Error(`不支持的统计范围:${scope}`);
|
|
@@ -47,8 +56,20 @@ export function resolveRange(scope, timezone, now = new Date(), sessionId = null
|
|
|
47
56
|
const targetDate = dateKey(now, timezone);
|
|
48
57
|
let startDate = null;
|
|
49
58
|
let endDate = null;
|
|
59
|
+
let startAt = null;
|
|
60
|
+
let endAt = null;
|
|
61
|
+
let windowHours = null;
|
|
50
62
|
|
|
51
|
-
if (normalizedScope === "
|
|
63
|
+
if (normalizedScope === "last-hours") {
|
|
64
|
+
windowHours = Number(hours ?? 3);
|
|
65
|
+
if (!Number.isInteger(windowHours) || windowHours < 1 || windowHours > 168) {
|
|
66
|
+
throw new Error("最近小时数必须是 1 至 168 之间的整数");
|
|
67
|
+
}
|
|
68
|
+
endAt = floorToMinute(now);
|
|
69
|
+
startAt = new Date(endAt.getTime() - windowHours * 60 * 60 * 1000);
|
|
70
|
+
startDate = dateKey(startAt, timezone);
|
|
71
|
+
endDate = dateKey(endAt, timezone);
|
|
72
|
+
} else if (normalizedScope === "today") {
|
|
52
73
|
startDate = targetDate;
|
|
53
74
|
endDate = targetDate;
|
|
54
75
|
} else if (normalizedScope === "last-7-days") {
|
|
@@ -65,6 +86,9 @@ export function resolveRange(scope, timezone, now = new Date(), sessionId = null
|
|
|
65
86
|
targetDate,
|
|
66
87
|
startDate,
|
|
67
88
|
endDate,
|
|
89
|
+
startAt,
|
|
90
|
+
endAt,
|
|
91
|
+
hours: windowHours,
|
|
68
92
|
sessionId: sessionId || null,
|
|
69
93
|
};
|
|
70
94
|
}
|
|
@@ -73,8 +97,15 @@ export function isCalendarScope(scope) {
|
|
|
73
97
|
return scope === "today" || scope === "last-7-days" || scope === "this-week";
|
|
74
98
|
}
|
|
75
99
|
|
|
100
|
+
export function isTimeWindowScope(scope) {
|
|
101
|
+
return scope === "last-hours";
|
|
102
|
+
}
|
|
103
|
+
|
|
76
104
|
export function isDateInRange(date, range) {
|
|
77
105
|
if (!date) return false;
|
|
106
|
+
if (isTimeWindowScope(range.scope)) {
|
|
107
|
+
return date >= range.startAt && date <= range.endAt;
|
|
108
|
+
}
|
|
78
109
|
if (!isCalendarScope(range.scope)) return true;
|
|
79
110
|
const key = dateKey(date, range.timezone);
|
|
80
111
|
return key >= range.startDate && key <= range.endDate;
|
|
@@ -107,6 +138,13 @@ export function outputSlugForRange(range, receiptId = "") {
|
|
|
107
138
|
const endDate = range?.endDate || range?.targetDate || startDate;
|
|
108
139
|
|
|
109
140
|
if (scope === "today") return `today-${safeSlugSegment(endDate, "today")}`;
|
|
141
|
+
if (scope === "last-hours") {
|
|
142
|
+
const endAt = range?.endAt instanceof Date ? range.endAt : new Date(range?.endAt || 0);
|
|
143
|
+
const endStamp = Number.isNaN(endAt.getTime())
|
|
144
|
+
? safeSlugSegment(endDate, "window")
|
|
145
|
+
: endAt.toISOString().replace(/[-:]/g, "").slice(0, 13);
|
|
146
|
+
return `last-${Number(range?.hours || 3)}-hours-${safeSlugSegment(endStamp, "window")}`;
|
|
147
|
+
}
|
|
110
148
|
if (scope === "last-7-days" || scope === "this-week") {
|
|
111
149
|
const dateSpan = startDate === endDate ? endDate : `${startDate}-to-${endDate}`;
|
|
112
150
|
return `${scope}-${safeSlugSegment(dateSpan, scope)}`;
|
|
@@ -4,7 +4,11 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
|
|
6
6
|
import { canonicalStringify, sha256Hex } from "./canonical.mjs";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
buildLogicalReceiptKey,
|
|
9
|
+
buildProtocolReceiptId,
|
|
10
|
+
buildSummaryReceiptId,
|
|
11
|
+
} from "./fact-identity.mjs";
|
|
8
12
|
import {
|
|
9
13
|
buildCompensation,
|
|
10
14
|
DEFAULT_LOCALE,
|
|
@@ -22,7 +26,12 @@ function fingerprintSessionIds(sessionIds) {
|
|
|
22
26
|
export function buildReceiptRecord(metrics, defaultTheme = "classic", locale = DEFAULT_LOCALE, canonical = {}) {
|
|
23
27
|
const sessionFingerprint = fingerprintSessionIds(metrics.sessionIds);
|
|
24
28
|
const logicalKey = buildLogicalReceiptKey(metrics);
|
|
25
|
-
const
|
|
29
|
+
const summaryOnly = metrics.mode === "last-hours";
|
|
30
|
+
const schemaVersion = summaryOnly ? 1 : SCHEMA_VERSION;
|
|
31
|
+
const sourceVersion = summaryOnly ? "cwr1" : SOURCE_VERSION;
|
|
32
|
+
const id = summaryOnly
|
|
33
|
+
? buildSummaryReceiptId(logicalKey)
|
|
34
|
+
: buildProtocolReceiptId(SOURCE_VERSION, logicalKey);
|
|
26
35
|
const snapshotHash = sha256Hex({
|
|
27
36
|
start: metrics.startAt.toISOString(),
|
|
28
37
|
end: metrics.endAt.toISOString(),
|
|
@@ -33,6 +42,8 @@ export function buildReceiptRecord(metrics, defaultTheme = "classic", locale = D
|
|
|
33
42
|
scope: metrics.mode,
|
|
34
43
|
rangeStartDate: metrics.rangeStartDate,
|
|
35
44
|
rangeEndDate: metrics.rangeEndDate,
|
|
45
|
+
windowStartAt: metrics.windowStartAt?.toISOString() || null,
|
|
46
|
+
windowEndAt: metrics.windowEndAt?.toISOString() || null,
|
|
36
47
|
});
|
|
37
48
|
const workProfileId = metrics.workProfileId || "temporary-hire";
|
|
38
49
|
const workProfile = getWorkProfileCopy(workProfileId, locale);
|
|
@@ -59,23 +70,24 @@ export function buildReceiptRecord(metrics, defaultTheme = "classic", locale = D
|
|
|
59
70
|
manifest_hash: sha256Hex(canonicalStringify(manifestCore)),
|
|
60
71
|
};
|
|
61
72
|
|
|
62
|
-
|
|
63
|
-
schema_version:
|
|
73
|
+
const record = {
|
|
74
|
+
schema_version: schemaVersion,
|
|
64
75
|
locale,
|
|
65
76
|
id,
|
|
66
77
|
generated_at: new Date().toISOString(),
|
|
67
78
|
source: {
|
|
68
79
|
type: "codex",
|
|
69
|
-
version:
|
|
80
|
+
version: sourceVersion,
|
|
70
81
|
scope: metrics.mode,
|
|
82
|
+
hours: metrics.windowHours || null,
|
|
71
83
|
collector_version: COLLECTOR_VERSION,
|
|
72
84
|
logical_key: logicalKey,
|
|
73
85
|
session_fingerprint: sessionFingerprint,
|
|
74
86
|
snapshot_hash: snapshotHash,
|
|
75
87
|
},
|
|
76
88
|
period: {
|
|
77
|
-
start_at: metrics.startAt.toISOString(),
|
|
78
|
-
end_at: metrics.endAt.toISOString(),
|
|
89
|
+
start_at: (metrics.windowStartAt || metrics.startAt).toISOString(),
|
|
90
|
+
end_at: (metrics.windowEndAt || metrics.endAt).toISOString(),
|
|
79
91
|
timezone: metrics.timezone,
|
|
80
92
|
range_start_date: metrics.rangeStartDate,
|
|
81
93
|
range_end_date: metrics.rangeEndDate,
|
|
@@ -107,9 +119,12 @@ export function buildReceiptRecord(metrics, defaultTheme = "classic", locale = D
|
|
|
107
119
|
contains_paths: false,
|
|
108
120
|
contains_filenames: false,
|
|
109
121
|
},
|
|
110
|
-
manifest,
|
|
111
|
-
facts,
|
|
112
122
|
};
|
|
123
|
+
if (!summaryOnly) {
|
|
124
|
+
record.manifest = manifest;
|
|
125
|
+
record.facts = facts;
|
|
126
|
+
}
|
|
127
|
+
return record;
|
|
113
128
|
}
|
|
114
129
|
|
|
115
130
|
export function persistReceiptRecord(record, outputHtmlPath, requestedDataDir = null) {
|
package/src/core/selector.mjs
CHANGED
|
@@ -37,21 +37,23 @@ export async function promptForRange({ locale, timezone, loadRecentSessions }) {
|
|
|
37
37
|
try {
|
|
38
38
|
console.log(isEnglish ? "\nChoose a receipt range:\n" : "\n请选择小票统计范围:\n");
|
|
39
39
|
console.log(isEnglish ? "1. All activity today (recommended)" : "1. 今天全部活动(推荐)");
|
|
40
|
-
console.log(isEnglish ? "2. Last
|
|
41
|
-
console.log(isEnglish ? "3.
|
|
42
|
-
console.log(isEnglish ? "4.
|
|
40
|
+
console.log(isEnglish ? "2. Last 3 hours" : "2. 最近 3 小时");
|
|
41
|
+
console.log(isEnglish ? "3. Last 7 calendar days" : "3. 最近 7 个自然日");
|
|
42
|
+
console.log(isEnglish ? "4. This week (Monday to now)" : "4. 本周(周一至今)");
|
|
43
|
+
console.log(isEnglish ? "5. Choose a specific session" : "5. 选择一个具体会话");
|
|
43
44
|
|
|
44
45
|
const choice = await askForNumber(
|
|
45
46
|
readline,
|
|
46
|
-
isEnglish ? "\nEnter 1–
|
|
47
|
+
isEnglish ? "\nEnter 1–5 (default 1): " : "\n请输入 1–5(默认 1):",
|
|
47
48
|
1,
|
|
48
|
-
|
|
49
|
+
5,
|
|
49
50
|
1,
|
|
50
51
|
);
|
|
51
52
|
|
|
52
53
|
if (choice === 1) return { mode: "today", sessionId: null };
|
|
53
|
-
if (choice === 2) return { mode: "last-
|
|
54
|
-
if (choice === 3) return { mode: "
|
|
54
|
+
if (choice === 2) return { mode: "last-hours", sessionId: null, hours: 3 };
|
|
55
|
+
if (choice === 3) return { mode: "last-7-days", sessionId: null };
|
|
56
|
+
if (choice === 4) return { mode: "this-week", sessionId: null };
|
|
55
57
|
|
|
56
58
|
const sessions = await loadRecentSessions();
|
|
57
59
|
if (!sessions.length) throw new Error(isEnglish ? "No Codex sessions found" : "没有找到可选择的 Codex 会话");
|
package/src/renderers/html.mjs
CHANGED
|
@@ -3,7 +3,12 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
|
|
4
4
|
import { buildCode128B } from "../core/barcode.mjs";
|
|
5
5
|
import { formatDate, formatDuration, formatNumber, formatTime } from "../lib/time.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
buildCompensation,
|
|
8
|
+
DEFAULT_LOCALE,
|
|
9
|
+
getReceiptCopy,
|
|
10
|
+
getScopeLabel,
|
|
11
|
+
} from "../core/presentation.mjs";
|
|
7
12
|
|
|
8
13
|
const require = createRequire(import.meta.url);
|
|
9
14
|
const DOM_TO_IMAGE_SOURCE = fs.readFileSync(require.resolve("dom-to-image-more"), "utf8");
|
|
@@ -65,7 +70,7 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
65
70
|
const rangeEndDate = record.period.range_end_date || formatDateKey(record.period.end_at.slice(0, 10), "zh-CN").replaceAll("/", "-");
|
|
66
71
|
const isCalendarScope = new Set(["today", "last-7-days", "this-week"]).has(record.source.scope);
|
|
67
72
|
const spansMultipleDates = rangeStartDate !== rangeEndDate;
|
|
68
|
-
const scopeLabel =
|
|
73
|
+
const scopeLabel = getScopeLabel(record.source.scope, locale, record.source.hours);
|
|
69
74
|
const dateRange = spansMultipleDates
|
|
70
75
|
? `${formatDateKey(rangeStartDate, locale)}—${formatDateKey(rangeEndDate, locale)}`
|
|
71
76
|
: formatDateKey(rangeEndDate, locale);
|