codex-work-receipt 0.7.0 → 0.7.2
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 +11 -1
- package/README.en.md +16 -4
- package/README.md +16 -4
- package/docs/cli.en.md +2 -2
- package/docs/cli.md +2 -2
- package/docs/codex-pet.md +2 -0
- package/docs/codex-skill.md +2 -0
- package/docs/data-schema.en.md +3 -1
- package/docs/data-schema.md +3 -1
- package/docs/images/codex-pet-showcase.png +0 -0
- package/package.json +1 -1
- package/skills/ai-work-receipt/SKILL.md +2 -0
- package/src/cli.mjs +7 -1
- package/src/core/fact-buckets.mjs +5 -0
- package/src/core/presentation.mjs +8 -0
- package/src/core/selector.mjs +3 -1
- package/src/parsers/codex.mjs +35 -4
- package/src/renderers/html.mjs +72 -17
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.7.
|
|
3
|
+
## 0.7.2
|
|
4
|
+
|
|
5
|
+
- 修复同一 Codex 会话存在多份 append-only 日志修订时生成重复 canonical factId、导致小程序云端入库拒绝的问题。
|
|
6
|
+
- cwr2 manifest 输出前新增 factId 唯一性防线,避免无效二维码进入扫码链路。
|
|
7
|
+
- “最近 N 小时”在交互选择、终端输出和 HTML 中明确标记为私人滚动摘要,并提示其不参与 AI 供销社统计。
|
|
8
|
+
|
|
9
|
+
## 0.7.1
|
|
10
|
+
|
|
11
|
+
- 修正 HTML 长图导出:PNG 只保留完整小票和微信小程序码,不再包含数据二维码或分片数据码。
|
|
12
|
+
|
|
13
|
+
## 0.7.0
|
|
4
14
|
|
|
5
15
|
- 新增 Codex 原生桌宠“票仔 · AI 小票工”,提供夜班绿票风格的核心状态与左右移动动画,并支持通过 `--install-pet`、`--uninstall-pet` 和 `--install-companion` 安装管理。
|
|
6
16
|
- AI 打工小票 Skill 新增“票仔,开今天的票”等自然语言入口。
|
package/README.en.md
CHANGED
|
@@ -24,6 +24,16 @@ Codex Work Receipt summarizes turns, tool calls, Tokens, duration, and models, t
|
|
|
24
24
|
|
|
25
25
|
Version `0.6.0` uses the cwr2 protocol to create stable privacy-safe facts for each session and calendar day. Overlapping today, last-seven-days, and this-week receipts can be deduplicated by the receiver, and oversized payloads are automatically split into reorderable multipart QR codes that rotate one at a time in the HTML so the camera never sees multiple data codes at once.
|
|
26
26
|
|
|
27
|
+
## Meet Ticket Buddy
|
|
28
|
+
|
|
29
|
+
<p align="center">
|
|
30
|
+
<a href="docs/codex-pet.en.md"><img src="docs/images/codex-pet-showcase.png" alt="Ticket Buddy in idle, working, needs-input, ready, and failed states" width="920"></a>
|
|
31
|
+
</p>
|
|
32
|
+
|
|
33
|
+
<p align="center">
|
|
34
|
+
<sub>Ticket Buddy changes expression as Codex becomes idle, runs a task, needs input, finishes, or gets blocked. Select the image for setup and state details.</sub>
|
|
35
|
+
</p>
|
|
36
|
+
|
|
27
37
|
## Quickstart
|
|
28
38
|
|
|
29
39
|
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:
|
|
@@ -44,7 +54,9 @@ Summarize the last 3 hours:
|
|
|
44
54
|
npx codex-work-receipt@latest --hours 3 --lang en
|
|
45
55
|
```
|
|
46
56
|
|
|
47
|
-
|
|
57
|
+
“Last N hours” is a rolling summary for private history only. It does not participate in AI Work Cooperative deduplicated accounting. Use today, this week, the last seven days, or a specific session when you want accountable canonical facts.
|
|
58
|
+
|
|
59
|
+
HTML and structured data are written to `./codex-work-receipt-output/` by default. The generated page can save a high-resolution PNG containing only the full receipt and WeChat mini-program code; data QR codes are excluded. Non-interactive options such as `--latest` and `--today` remain available; see the [CLI guide](docs/cli.en.md).
|
|
48
60
|
|
|
49
61
|
## Ask Codex directly
|
|
50
62
|
|
|
@@ -89,9 +101,9 @@ The desktop page generates one or more privacy-safe data QR codes. The companion
|
|
|
89
101
|
## Sponsor
|
|
90
102
|
|
|
91
103
|
<p align="center">
|
|
92
|
-
<a href="https://modelflare.dev/"><img src="docs/images/sponsors/modelflare-logo.png" alt="ModelFlare Logo" width="56"></a><br>
|
|
93
|
-
<a href="https://modelflare.dev/"><strong>ModelFlare</strong></a> · <a href="https://modelflare.dev/">modelflare.dev</a><br>
|
|
94
|
-
<sub>
|
|
104
|
+
<a href="https://modelflare.dev/sign-up?partner=OB9YXNSEEGOL"><img src="docs/images/sponsors/modelflare-logo.png" alt="ModelFlare Logo" width="56"></a><br>
|
|
105
|
+
<a href="https://modelflare.dev/sign-up?partner=OB9YXNSEEGOL"><strong>ModelFlare</strong></a> · <a href="https://modelflare.dev/sign-up?partner=OB9YXNSEEGOL">modelflare.dev</a><br>
|
|
106
|
+
<sub>Affordable and reliable access to leading AI model APIs, helping independent developers validate ideas faster and turn them into real products.</sub>
|
|
95
107
|
</p>
|
|
96
108
|
|
|
97
109
|
## License
|
package/README.md
CHANGED
|
@@ -24,6 +24,16 @@
|
|
|
24
24
|
|
|
25
25
|
`0.6.0` 使用 cwr2 协议,按“会话 × 自然日”生成稳定的脱敏事实。今日、近 7 日和本周小票即使范围重叠,接收端也能识别相同工作;数据过大时会自动生成多张可重组的分片二维码,并在 HTML 中逐张自动轮播,避免镜头同时识别多个数据码。
|
|
26
26
|
|
|
27
|
+
## 认识票仔
|
|
28
|
+
|
|
29
|
+
<p align="center">
|
|
30
|
+
<a href="docs/codex-pet.md"><img src="docs/images/codex-pet-showcase.png" alt="票仔 AI 小票工的待机、打工、等待确认、完成和失败状态" width="920"></a>
|
|
31
|
+
</p>
|
|
32
|
+
|
|
33
|
+
<p align="center">
|
|
34
|
+
<sub>票仔会跟随 Codex 的空闲、执行、等待确认、完成和失败状态切换表情。点击图片查看安装与状态说明。</sub>
|
|
35
|
+
</p>
|
|
36
|
+
|
|
27
37
|
## Quickstart
|
|
28
38
|
|
|
29
39
|
需要 Node.js 20 或更高版本,并且本机已经使用过 Codex。无需克隆仓库,运行后选择“今天 / 最近 3 小时 / 近 7 日 / 本周 / 指定会话”:
|
|
@@ -44,7 +54,9 @@ npx codex-work-receipt@latest --today
|
|
|
44
54
|
npx codex-work-receipt@latest --hours 3
|
|
45
55
|
```
|
|
46
56
|
|
|
47
|
-
|
|
57
|
+
“最近 N 小时”是滚动摘要,只用于查看和保存私人历史,不参与 AI 供销社的去重统计。需要进入供销社时,请生成“今日 / 本周 / 近 7 日 / 指定会话”小票。
|
|
58
|
+
|
|
59
|
+
网页和结构数据默认保存在 `./codex-work-receipt-output/`。生成的网页支持一键保存只包含完整小票和微信小程序码的高清长图,数据二维码不会进入图片。也可以继续使用 `--latest`、`--today` 等非交互参数,详见 [CLI 使用文档](docs/cli.md)。
|
|
48
60
|
|
|
49
61
|
## 直接跟 Codex 说
|
|
50
62
|
|
|
@@ -89,9 +101,9 @@ Codex 会选择统计范围和主题、执行命令并打开小票。详见 [Cod
|
|
|
89
101
|
## 赞助商
|
|
90
102
|
|
|
91
103
|
<p align="center">
|
|
92
|
-
<a href="https://modelflare.dev/"><img src="docs/images/sponsors/modelflare-logo.png" alt="ModelFlare Logo" width="56"></a><br>
|
|
93
|
-
<a href="https://modelflare.dev/"><strong>ModelFlare</strong></a> · <a href="https://modelflare.dev/">modelflare.dev</a><br>
|
|
94
|
-
<sub
|
|
104
|
+
<a href="https://modelflare.dev/sign-up?partner=OB9YXNSEEGOL"><img src="docs/images/sponsors/modelflare-logo.png" alt="ModelFlare Logo" width="56"></a><br>
|
|
105
|
+
<a href="https://modelflare.dev/sign-up?partner=OB9YXNSEEGOL"><strong>ModelFlare</strong></a> · <a href="https://modelflare.dev/sign-up?partner=OB9YXNSEEGOL">modelflare.dev</a><br>
|
|
106
|
+
<sub>为个人开发者提供价格友好、稳定便捷的前沿 AI 模型 API 接入,帮助你更快完成原型验证,把想法变成真正可用的产品。</sub>
|
|
95
107
|
</p>
|
|
96
108
|
|
|
97
109
|
## License
|
package/docs/cli.en.md
CHANGED
|
@@ -39,7 +39,7 @@ Summarize the last 3 hours:
|
|
|
39
39
|
npx codex-work-receipt@latest --hours 3 --lang en
|
|
40
40
|
```
|
|
41
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
|
|
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, stay in private history, and do not participate in AI Work Cooperative accounting. Use today, this week, the last seven days, or a specific session for cwr2 session-day canonical facts.
|
|
43
43
|
|
|
44
44
|
Last 7 calendar days, including today:
|
|
45
45
|
|
|
@@ -87,7 +87,7 @@ Default output:
|
|
|
87
87
|
|
|
88
88
|
Default filenames include the calendar range. Latest-session and selected-session receipts include a short identifier so receipts from different dates or sessions do not overwrite one another.
|
|
89
89
|
|
|
90
|
-
Open the generated HTML and click “Save full PNG” to download a high-resolution image. It
|
|
90
|
+
Open the generated HTML and click “Save full PNG” to download a high-resolution image. It contains only the complete main receipt and WeChat mini-program code in the currently selected theme. Data QR codes, multipart data codes, theme controls, the page background, and the footer note are excluded. The HTML page still presents data codes normally: a single code stays beside the mini-program code, while oversized cwr2 payloads rotate their parts after the mini program has been opened so they can be imported on the phone.
|
|
91
91
|
|
|
92
92
|
Set a timezone and output path:
|
|
93
93
|
|
package/docs/cli.md
CHANGED
|
@@ -39,7 +39,7 @@ npx codex-work-receipt@latest --today
|
|
|
39
39
|
npx codex-work-receipt@latest --hours 3
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
`--hours` 支持 1~168 的整数,并按精确时间戳筛选,可以跨越午夜。滚动小时小票使用兼容的 cwr1 摘要协议,不会写入 cwr2 的“会话 ×
|
|
42
|
+
`--hours` 支持 1~168 的整数,并按精确时间戳筛选,可以跨越午夜。滚动小时小票使用兼容的 cwr1 摘要协议,不会写入 cwr2 的“会话 × 自然日”规范事实,只保存到私人历史,不参与 AI 供销社统计。需要进入供销社时,请生成“今日 / 本周 / 近 7 日 / 指定会话”小票。
|
|
43
43
|
|
|
44
44
|
最近 7 个自然日(含今天):
|
|
45
45
|
|
|
@@ -87,7 +87,7 @@ npx codex-work-receipt@latest --latest --theme diner
|
|
|
87
87
|
|
|
88
88
|
默认文件名会携带统计日期范围;最近会话和指定会话则携带短编号,避免不同日期或会话的小票互相覆盖。
|
|
89
89
|
|
|
90
|
-
打开生成的 HTML 后,点击“保存完整长图”即可下载高清 PNG
|
|
90
|
+
打开生成的 HTML 后,点击“保存完整长图”即可下载高清 PNG。图片只包含完整主小票和微信小程序码,并自动使用当前选择的主题;数据二维码、分片数据码、主题按钮、网页背景和底部说明都不会进入图片。网页本身仍会正常展示数据二维码:单码时与小程序码并排,cwr2 超出单码安全容量时则在打开小程序后自动逐张轮播分片数据码,供用户在小程序中完成导入。
|
|
91
91
|
|
|
92
92
|
指定时区和输出路径:
|
|
93
93
|
|
package/docs/codex-pet.md
CHANGED
package/docs/codex-skill.md
CHANGED
|
@@ -38,6 +38,8 @@ Skill 会安装到:
|
|
|
38
38
|
|
|
39
39
|
Codex 会根据表达选择单次会话、最近 1~168 小时、今天、近七日或本周,以及语言和主题,然后执行命令并打开生成的网页。“最近几个小时”默认统计最近 3 小时。
|
|
40
40
|
|
|
41
|
+
“最近 N 小时”生成的是只用于私人历史的滚动摘要,不进入 AI 供销社;如果用户明确希望参与供销社统计,应改用今日、本周、近七日或指定会话范围。
|
|
42
|
+
|
|
41
43
|
如果当前 Codex 任务仍在运行,而你希望立刻查看小票,建议新开一个本地 Codex 会话再说“开今天的票”;在当前运行会话中发送的消息可能根据 Follow-up behavior 被用于 Steer 或 Queue。
|
|
42
44
|
|
|
43
45
|
也可以通过 `$ai-work-receipt` 显式调用这个 Skill。
|
package/docs/data-schema.en.md
CHANGED
|
@@ -32,7 +32,9 @@ 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`, `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.
|
|
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; it stays in private history and does not participate in AI Work Cooperative accounting. The updated mini program can still import older QR codes.
|
|
36
|
+
|
|
37
|
+
When the same Codex session has multiple append-only log revisions, the generator keeps the more complete revision. Fact IDs in a cwr2 manifest must be unique; an identity collision stops QR generation instead of emitting an invalid payload.
|
|
36
38
|
|
|
37
39
|
`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
40
|
|
package/docs/data-schema.md
CHANGED
|
@@ -32,7 +32,9 @@ cwr2p.<transferId>.<partIndex>.<partCount>.<totalChecksum>.<partChecksum>.<chunk
|
|
|
32
32
|
|
|
33
33
|
小程序应检查前缀和校验值,再解压并解析数据。未来结构升级通过 `v` 字段兼容。
|
|
34
34
|
|
|
35
|
-
精简字段 `o` 显式携带 `latest`、`session`、`last-hours`、`today`、`last-7-days` 或 `this-week`;`d[3]` 和 `d[4]` 携带日期边界。今日、近 7 日和本周继续使用 cwr2 canonical facts。`last-hours` 是滚动时间窗,为避免与“会话 × 自然日”事实发生身份冲突,使用兼容的 cwr1
|
|
35
|
+
精简字段 `o` 显式携带 `latest`、`session`、`last-hours`、`today`、`last-7-days` 或 `this-week`;`d[3]` 和 `d[4]` 携带日期边界。今日、近 7 日和本周继续使用 cwr2 canonical facts。`last-hours` 是滚动时间窗,为避免与“会话 × 自然日”事实发生身份冲突,使用兼容的 cwr1 摘要载荷,只进入私人历史、不参与 AI 供销社统计。旧二维码仍可由新版小程序导入。
|
|
36
|
+
|
|
37
|
+
同一 Codex 会话出现多份 append-only 日志修订时,生成器只采用更完整的修订;cwr2 manifest 中的 factId 必须唯一,检测到身份冲突时不会输出可扫码二维码。
|
|
36
38
|
|
|
37
39
|
`presentation.compensation` 是娱乐化 AI 工分,不代表真实 API 费用。为兼容当前中文小程序,二维码展示文案继续使用中文,并通过精简字段 `l` 和 `r` 携带桌面语言及工种语义 ID;英文 HTML 和本地 JSON 不受影响。
|
|
38
40
|
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -13,6 +13,7 @@ Use the published `codex-work-receipt` CLI as the only statistics and rendering
|
|
|
13
13
|
- Use `--hours <N>` for “最近 N 小时”“过去 N 小时” or “近 N 小时”. Accept integers from 1 to 168.
|
|
14
14
|
- Use `--hours 3` for an unspecified “最近几个小时”“过去几个小时” request.
|
|
15
15
|
- Use `--hours 12` for “最近半天”.
|
|
16
|
+
- Treat `--hours` receipts as rolling summaries for private history only. They do not participate in AI Work Cooperative accounting; if the user asks for an accountable cooperative receipt, use today, this week, the last seven days, or a specific session instead.
|
|
16
17
|
- Use `--today` for “今天”“今日”“今天全部工作” or a daily summary.
|
|
17
18
|
- Use `--range last-7-days` for “最近七天”“近 7 日” or a rolling seven-calendar-day summary.
|
|
18
19
|
- Use `--range this-week` for “本周”“这周” or a Monday-to-now summary.
|
|
@@ -39,6 +40,7 @@ Replace only the mode and optional flags according to the request.
|
|
|
39
40
|
|
|
40
41
|
3. Let the CLI open the generated HTML unless the user requested `--no-open`.
|
|
41
42
|
4. On success, report the HTML path and tell the user to scan the official mini-program code first, then the adjacent data QR code.
|
|
43
|
+
5. When `--hours` was used, explicitly state that the receipt is a private rolling summary and will not enter AI Work Cooperative statistics.
|
|
42
44
|
|
|
43
45
|
## Privacy and failures
|
|
44
46
|
|
package/src/cli.mjs
CHANGED
|
@@ -9,7 +9,7 @@ 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 { getScopeLabel } from "./core/presentation.mjs";
|
|
12
|
+
import { getRollingSummaryNotice, 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";
|
|
@@ -145,6 +145,9 @@ async function main() {
|
|
|
145
145
|
console.log(record.manifest
|
|
146
146
|
? `Data QR: ${qrPayloads.length} code(s) · ${record.manifest.fact_count} canonical fact(s) · schema v${record.schema_version}`
|
|
147
147
|
: `Data QR: ${qrPayloads.length} code(s) · rolling summary · schema v${record.schema_version}`);
|
|
148
|
+
if (record.source.scope === "last-hours") {
|
|
149
|
+
console.log(`Note: ${getRollingSummaryNotice(options.locale, record.source.hours)}`);
|
|
150
|
+
}
|
|
148
151
|
if (!miniProgramCodeDataUrl) console.log("Mini-program code: not configured; using the explicit placeholder");
|
|
149
152
|
} else {
|
|
150
153
|
console.log(`已生成网页:${outputFile}`);
|
|
@@ -155,6 +158,9 @@ async function main() {
|
|
|
155
158
|
console.log(record.manifest
|
|
156
159
|
? `数据二维码:${qrPayloads.length} 个 · ${record.manifest.fact_count} 条规范事实 · schema v${record.schema_version}`
|
|
157
160
|
: `数据二维码:${qrPayloads.length} 个 · 滚动摘要 · schema v${record.schema_version}`);
|
|
161
|
+
if (record.source.scope === "last-hours") {
|
|
162
|
+
console.log(`提示:${getRollingSummaryNotice(options.locale, record.source.hours)}`);
|
|
163
|
+
}
|
|
158
164
|
if (!miniProgramCodeDataUrl) console.log("小程序码:尚未配置,页面使用明确占位符");
|
|
159
165
|
}
|
|
160
166
|
if (options.open) openFile(outputFile);
|
|
@@ -198,6 +198,11 @@ export function buildCanonicalFacts(sessions, range, options = {}) {
|
|
|
198
198
|
left.local_date.localeCompare(right.local_date) || left.fact_id.localeCompare(right.fact_id)
|
|
199
199
|
));
|
|
200
200
|
|
|
201
|
+
const uniqueFactIds = new Set(facts.map((fact) => fact.fact_id));
|
|
202
|
+
if (uniqueFactIds.size !== facts.length) {
|
|
203
|
+
throw new Error("检测到重复的规范事实,请更新生成器后重新生成小票");
|
|
204
|
+
}
|
|
205
|
+
|
|
201
206
|
return {
|
|
202
207
|
facts,
|
|
203
208
|
coverage: coverageForRange(range, sessions.scanMode || options.scanMode || "best_effort", observedAt),
|
|
@@ -142,10 +142,12 @@ const RECEIPT_COPY = {
|
|
|
142
142
|
multipartRestart: "重新显示小程序码",
|
|
143
143
|
dataQrAlt: "当前小票数据二维码",
|
|
144
144
|
miniProgramAlt: "微信小程序码",
|
|
145
|
+
exportMiniProgramLabel: "微信扫码打开小程序",
|
|
145
146
|
placeholderLabel: "小程序码",
|
|
146
147
|
placeholderValue: "待接入",
|
|
147
148
|
placeholderAria: "小程序码待接入",
|
|
148
149
|
transferNote: "数据码只包含时间、轮次、Token和工具调用等统计,不包含Prompt、回复正文、代码、项目路径或文件名。",
|
|
150
|
+
rollingSummaryNotice: "最近 {hours} 小时属于滚动摘要,只保存到私人历史,不参与 AI 供销社统计。需要统计时请生成“今日 / 本周 / 近 7 日 / 指定会话”小票。",
|
|
149
151
|
privacy: "结构数据同时保存在本机,未来可以批量导入小程序历史记录。",
|
|
150
152
|
},
|
|
151
153
|
en: {
|
|
@@ -215,10 +217,12 @@ const RECEIPT_COPY = {
|
|
|
215
217
|
multipartRestart: "Show mini-program code again",
|
|
216
218
|
dataQrAlt: "Current receipt data QR code",
|
|
217
219
|
miniProgramAlt: "WeChat mini-program code",
|
|
220
|
+
exportMiniProgramLabel: "Scan to open the mini program",
|
|
218
221
|
placeholderLabel: "Mini program",
|
|
219
222
|
placeholderValue: "Pending",
|
|
220
223
|
placeholderAria: "Mini-program code pending",
|
|
221
224
|
transferNote: "The data code contains only statistics such as time, turns, Tokens, and tool calls. It does not contain prompts, responses, code, project paths, or file names.",
|
|
225
|
+
rollingSummaryNotice: "The last {hours} hours is a rolling summary for private history only. It does not participate in AI Work Cooperative accounting. Use today, this week, the last seven days, or a specific session for accountable facts.",
|
|
222
226
|
privacy: "Structured data is also stored locally for future batch import into your mini-program history.",
|
|
223
227
|
},
|
|
224
228
|
};
|
|
@@ -271,6 +275,10 @@ export function getScopeLabel(scope, locale = DEFAULT_LOCALE, hours = null) {
|
|
|
271
275
|
return String(template).replaceAll("{hours}", String(hours || 3));
|
|
272
276
|
}
|
|
273
277
|
|
|
278
|
+
export function getRollingSummaryNotice(locale = DEFAULT_LOCALE, hours = null) {
|
|
279
|
+
return String(getReceiptCopy(locale).rollingSummaryNotice).replaceAll("{hours}", String(hours || 3));
|
|
280
|
+
}
|
|
281
|
+
|
|
274
282
|
export function buildCompensation(scope, amount, locale = DEFAULT_LOCALE) {
|
|
275
283
|
const copy = COMPENSATION_COPY[SUPPORTED_LOCALES.has(locale) ? locale : DEFAULT_LOCALE];
|
|
276
284
|
return {
|
package/src/core/selector.mjs
CHANGED
|
@@ -37,7 +37,9 @@ 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
|
|
40
|
+
console.log(isEnglish
|
|
41
|
+
? "2. Last 3 hours (private rolling summary; not counted by the cooperative)"
|
|
42
|
+
: "2. 最近 3 小时(私人滚动摘要,不计入供销社)");
|
|
41
43
|
console.log(isEnglish ? "3. Last 7 calendar days" : "3. 最近 7 个自然日");
|
|
42
44
|
console.log(isEnglish ? "4. This week (Monday to now)" : "4. 本周(周一至今)");
|
|
43
45
|
console.log(isEnglish ? "5. Choose a specific session" : "5. 选择一个具体会话");
|
package/src/parsers/codex.mjs
CHANGED
|
@@ -69,6 +69,38 @@ function calendarCandidates(files, startDate) {
|
|
|
69
69
|
return files.filter((file) => file.modifiedAt >= approximateStart);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
function revisionDominates(candidate, current) {
|
|
73
|
+
const candidateRevision = candidate.sourceRevision || {};
|
|
74
|
+
const currentRevision = current.sourceRevision || {};
|
|
75
|
+
const candidateRows = Number(candidateRevision.row_count || 0);
|
|
76
|
+
const currentRows = Number(currentRevision.row_count || 0);
|
|
77
|
+
const candidateBytes = Number(candidateRevision.byte_length || 0);
|
|
78
|
+
const currentBytes = Number(currentRevision.byte_length || 0);
|
|
79
|
+
return candidateRows >= currentRows && candidateBytes >= currentBytes
|
|
80
|
+
&& (candidateRows > currentRows || candidateBytes > currentBytes);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function preferSession(candidate, current) {
|
|
84
|
+
if (revisionDominates(candidate, current)) return true;
|
|
85
|
+
if (revisionDominates(current, candidate)) return false;
|
|
86
|
+
|
|
87
|
+
const candidateModifiedAt = Number(candidate.modifiedAt || candidate.endAt || 0);
|
|
88
|
+
const currentModifiedAt = Number(current.modifiedAt || current.endAt || 0);
|
|
89
|
+
if (candidateModifiedAt !== currentModifiedAt) return candidateModifiedAt > currentModifiedAt;
|
|
90
|
+
|
|
91
|
+
return String(candidate.filePath || "").localeCompare(String(current.filePath || "")) > 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function deduplicateCodexSessions(sessions) {
|
|
95
|
+
const selected = new Map();
|
|
96
|
+
for (const session of sessions) {
|
|
97
|
+
const key = `${session.identityQuality || "metadata"}:${session.sessionId}`;
|
|
98
|
+
const current = selected.get(key);
|
|
99
|
+
if (!current || preferSession(session, current)) selected.set(key, session);
|
|
100
|
+
}
|
|
101
|
+
return [...selected.values()];
|
|
102
|
+
}
|
|
103
|
+
|
|
72
104
|
export function loadCodexSessions(range) {
|
|
73
105
|
const files = codexSessionFiles();
|
|
74
106
|
let candidates = files;
|
|
@@ -85,8 +117,7 @@ export function loadCodexSessions(range) {
|
|
|
85
117
|
scanMode = fullScan ? "full" : "best_effort";
|
|
86
118
|
}
|
|
87
119
|
|
|
88
|
-
const sessions = candidates
|
|
89
|
-
.map(sessionFromFile)
|
|
120
|
+
const sessions = deduplicateCodexSessions(candidates.map(sessionFromFile))
|
|
90
121
|
.sort((left, right) => right.endAt - left.endAt);
|
|
91
122
|
|
|
92
123
|
if (range.scope === "latest") {
|
|
@@ -106,10 +137,10 @@ export function loadCodexSessions(range) {
|
|
|
106
137
|
}
|
|
107
138
|
|
|
108
139
|
export function listRecentCodexSessions(limit = 10) {
|
|
109
|
-
const sessions = codexSessionFiles()
|
|
140
|
+
const sessions = deduplicateCodexSessions(codexSessionFiles()
|
|
110
141
|
.slice(0, Math.max(40, limit * 3))
|
|
111
142
|
.map(sessionFromFile)
|
|
112
|
-
.filter((session) => session.rows.length)
|
|
143
|
+
.filter((session) => session.rows.length))
|
|
113
144
|
.sort((left, right) => right.endAt - left.endAt)
|
|
114
145
|
.slice(0, limit);
|
|
115
146
|
|
package/src/renderers/html.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
buildCompensation,
|
|
8
8
|
DEFAULT_LOCALE,
|
|
9
9
|
getReceiptCopy,
|
|
10
|
+
getRollingSummaryNotice,
|
|
10
11
|
getScopeLabel,
|
|
11
12
|
} from "../core/presentation.mjs";
|
|
12
13
|
|
|
@@ -71,6 +72,9 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
71
72
|
const isCalendarScope = new Set(["today", "last-7-days", "this-week"]).has(record.source.scope);
|
|
72
73
|
const spansMultipleDates = rangeStartDate !== rangeEndDate;
|
|
73
74
|
const scopeLabel = getScopeLabel(record.source.scope, locale, record.source.hours);
|
|
75
|
+
const rollingSummaryNotice = record.source.scope === "last-hours"
|
|
76
|
+
? getRollingSummaryNotice(locale, record.source.hours)
|
|
77
|
+
: null;
|
|
74
78
|
const dateRange = spansMultipleDates
|
|
75
79
|
? `${formatDateKey(rangeStartDate, locale)}—${formatDateKey(rangeEndDate, locale)}`
|
|
76
80
|
: formatDateKey(rangeEndDate, locale);
|
|
@@ -91,6 +95,7 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
91
95
|
busyLabel: copy.exportingImage,
|
|
92
96
|
successLabel: copy.exportSuccess,
|
|
93
97
|
errorLabel: copy.exportError,
|
|
98
|
+
miniProgramLabel: copy.exportMiniProgramLabel,
|
|
94
99
|
fileBase: `codex-work-receipt-${record.source.scope}-${spansMultipleDates ? `${rangeStartDate}-to-${rangeEndDate}` : `${rangeEndDate}-${record.id.slice(4, 12)}`}`,
|
|
95
100
|
}).replaceAll("<", "\\u003c");
|
|
96
101
|
const rows = [
|
|
@@ -151,7 +156,7 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
151
156
|
<div class="qr-grid qr-grid--export-only" hidden>
|
|
152
157
|
<div class="qr-item">
|
|
153
158
|
<div class="qr-frame">${miniProgramVisual}</div>
|
|
154
|
-
<strong>${escapeHtml(copy.openMiniProgram)}</strong>
|
|
159
|
+
<strong data-export-mini-label>${escapeHtml(copy.openMiniProgram)}</strong>
|
|
155
160
|
<span>${escapeHtml(copy.openMiniProgramHint)}</span>
|
|
156
161
|
</div>
|
|
157
162
|
${dataQrItems}
|
|
@@ -160,7 +165,7 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
160
165
|
<div class="qr-grid qr-grid--single">
|
|
161
166
|
<div class="qr-item">
|
|
162
167
|
<div class="qr-frame">${miniProgramVisual}</div>
|
|
163
|
-
<strong>${escapeHtml(copy.openMiniProgram)}</strong>
|
|
168
|
+
<strong data-export-mini-label>${escapeHtml(copy.openMiniProgram)}</strong>
|
|
164
169
|
<span>${escapeHtml(copy.openMiniProgramHint)}</span>
|
|
165
170
|
</div>
|
|
166
171
|
${dataQrItems}
|
|
@@ -492,6 +497,13 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
492
497
|
line-height: 1.65;
|
|
493
498
|
text-align: center;
|
|
494
499
|
}
|
|
500
|
+
.transfer-note--warning {
|
|
501
|
+
padding: 12px 14px;
|
|
502
|
+
border: 1px solid color-mix(in srgb, var(--ink) 28%, transparent);
|
|
503
|
+
background: color-mix(in srgb, var(--accent) 76%, var(--paper));
|
|
504
|
+
color: var(--ink);
|
|
505
|
+
font-weight: 700;
|
|
506
|
+
}
|
|
495
507
|
.privacy {
|
|
496
508
|
margin: 24px auto 0;
|
|
497
509
|
max-width: 440px;
|
|
@@ -568,6 +580,7 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
568
580
|
<p>${escapeHtml(copy.transferDescription)}</p>
|
|
569
581
|
</header>
|
|
570
582
|
${transferVisual}
|
|
583
|
+
${rollingSummaryNotice ? `<p class="transfer-note transfer-note--warning">${escapeHtml(rollingSummaryNotice)}</p>` : ""}
|
|
571
584
|
<p class="transfer-note">${escapeHtml(copy.transferNote)}</p>
|
|
572
585
|
</section>
|
|
573
586
|
</div>
|
|
@@ -692,6 +705,50 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
692
705
|
}));
|
|
693
706
|
}
|
|
694
707
|
|
|
708
|
+
function sanitizeExportNode(node) {
|
|
709
|
+
const multipartLiveNode = node.querySelector(".multipart-live");
|
|
710
|
+
if (multipartLiveNode) multipartLiveNode.remove();
|
|
711
|
+
|
|
712
|
+
const exportGridNode = node.querySelector(".qr-grid--export-only");
|
|
713
|
+
if (exportGridNode) {
|
|
714
|
+
exportGridNode.removeAttribute("hidden");
|
|
715
|
+
exportGridNode.style.display = "grid";
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
node.querySelectorAll("[data-data-qr-index]").forEach((item) => item.remove());
|
|
719
|
+
node.querySelectorAll(".qr-grid").forEach((grid) => {
|
|
720
|
+
grid.style.gridTemplateColumns = "minmax(0, 1fr)";
|
|
721
|
+
grid.style.justifyItems = "center";
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
const miniProgramLabel = node.querySelector("[data-export-mini-label]");
|
|
725
|
+
if (miniProgramLabel) miniProgramLabel.textContent = exportConfig.miniProgramLabel;
|
|
726
|
+
const transferDescription = node.querySelector(".transfer-heading p");
|
|
727
|
+
if (transferDescription) transferDescription.remove();
|
|
728
|
+
const transferNote = node.querySelector(".transfer-note");
|
|
729
|
+
if (transferNote) transferNote.remove();
|
|
730
|
+
|
|
731
|
+
node.querySelectorAll(".paper").forEach((paper) => {
|
|
732
|
+
paper.style.boxShadow = "none";
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function createExportNode(source, paperColor) {
|
|
737
|
+
const clone = source.cloneNode(true);
|
|
738
|
+
sanitizeExportNode(clone);
|
|
739
|
+
const sourceWidth = Math.ceil(source.getBoundingClientRect().width || source.scrollWidth);
|
|
740
|
+
Object.assign(clone.style, {
|
|
741
|
+
position: "absolute",
|
|
742
|
+
left: "-100000px",
|
|
743
|
+
top: "0",
|
|
744
|
+
width: sourceWidth + "px",
|
|
745
|
+
background: paperColor,
|
|
746
|
+
pointerEvents: "none",
|
|
747
|
+
});
|
|
748
|
+
document.body.appendChild(clone);
|
|
749
|
+
return clone;
|
|
750
|
+
}
|
|
751
|
+
|
|
695
752
|
function downloadImage(dataUrl, filename) {
|
|
696
753
|
const link = document.createElement("a");
|
|
697
754
|
link.href = dataUrl;
|
|
@@ -708,15 +765,17 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
708
765
|
exportButton.disabled = true;
|
|
709
766
|
exportButton.textContent = exportConfig.busyLabel;
|
|
710
767
|
setExportStatus("");
|
|
768
|
+
let renderNode = null;
|
|
711
769
|
|
|
712
770
|
try {
|
|
713
771
|
if (document.fonts?.ready) await document.fonts.ready;
|
|
714
|
-
await waitForImages(exportNode);
|
|
715
|
-
const width = Math.ceil(exportNode.scrollWidth);
|
|
716
|
-
const height = Math.ceil(exportNode.scrollHeight);
|
|
717
|
-
const scale = Math.max(2, Math.min(3, 1500 / Math.max(1, width)));
|
|
718
772
|
const paperColor = getComputedStyle(document.documentElement).getPropertyValue("--paper").trim() || "#ffffff";
|
|
719
|
-
|
|
773
|
+
renderNode = createExportNode(exportNode, paperColor);
|
|
774
|
+
await waitForImages(renderNode);
|
|
775
|
+
const width = Math.ceil(renderNode.scrollWidth);
|
|
776
|
+
const height = Math.ceil(renderNode.scrollHeight);
|
|
777
|
+
const scale = Math.max(2, Math.min(3, 1500 / Math.max(1, width)));
|
|
778
|
+
const dataUrl = await domtoimage.toPng(renderNode, {
|
|
720
779
|
width,
|
|
721
780
|
height,
|
|
722
781
|
scale,
|
|
@@ -725,17 +784,12 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
725
784
|
cacheBust: false,
|
|
726
785
|
style: { background: paperColor },
|
|
727
786
|
onclone(clone) {
|
|
787
|
+
sanitizeExportNode(clone);
|
|
788
|
+
clone.style.position = "static";
|
|
789
|
+
clone.style.left = "auto";
|
|
790
|
+
clone.style.top = "auto";
|
|
791
|
+
clone.style.width = width + "px";
|
|
728
792
|
clone.style.background = paperColor;
|
|
729
|
-
const multipartLiveClone = clone.querySelector(".multipart-live");
|
|
730
|
-
if (multipartLiveClone) multipartLiveClone.remove();
|
|
731
|
-
const exportGridClone = clone.querySelector(".qr-grid--export-only");
|
|
732
|
-
if (exportGridClone) {
|
|
733
|
-
exportGridClone.removeAttribute("hidden");
|
|
734
|
-
exportGridClone.style.display = "grid";
|
|
735
|
-
}
|
|
736
|
-
clone.querySelectorAll(".paper").forEach((paper) => {
|
|
737
|
-
paper.style.boxShadow = "none";
|
|
738
|
-
});
|
|
739
793
|
},
|
|
740
794
|
logger: {},
|
|
741
795
|
});
|
|
@@ -748,6 +802,7 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
748
802
|
console.error(error);
|
|
749
803
|
setExportStatus(exportConfig.errorLabel, "error");
|
|
750
804
|
} finally {
|
|
805
|
+
if (renderNode) renderNode.remove();
|
|
751
806
|
exportButton.disabled = false;
|
|
752
807
|
exportButton.textContent = exportConfig.idleLabel;
|
|
753
808
|
}
|