codex-work-receipt 0.6.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -1
- package/README.en.md +33 -4
- package/README.md +33 -4
- 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 +16 -4
- package/docs/cli.md +16 -4
- 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 +11 -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 +67 -19
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": "本周全部会话",
|
|
@@ -141,6 +142,7 @@ const RECEIPT_COPY = {
|
|
|
141
142
|
multipartRestart: "重新显示小程序码",
|
|
142
143
|
dataQrAlt: "当前小票数据二维码",
|
|
143
144
|
miniProgramAlt: "微信小程序码",
|
|
145
|
+
exportMiniProgramLabel: "微信扫码打开小程序",
|
|
144
146
|
placeholderLabel: "小程序码",
|
|
145
147
|
placeholderValue: "待接入",
|
|
146
148
|
placeholderAria: "小程序码待接入",
|
|
@@ -165,6 +167,7 @@ const RECEIPT_COPY = {
|
|
|
165
167
|
scope: {
|
|
166
168
|
latest: "Latest session",
|
|
167
169
|
session: "Selected session",
|
|
170
|
+
"last-hours": "Last {hours} hours",
|
|
168
171
|
today: "All sessions today",
|
|
169
172
|
"last-7-days": "Last 7 calendar days",
|
|
170
173
|
"this-week": "All sessions this week",
|
|
@@ -213,6 +216,7 @@ const RECEIPT_COPY = {
|
|
|
213
216
|
multipartRestart: "Show mini-program code again",
|
|
214
217
|
dataQrAlt: "Current receipt data QR code",
|
|
215
218
|
miniProgramAlt: "WeChat mini-program code",
|
|
219
|
+
exportMiniProgramLabel: "Scan to open the mini program",
|
|
216
220
|
placeholderLabel: "Mini program",
|
|
217
221
|
placeholderValue: "Pending",
|
|
218
222
|
placeholderAria: "Mini-program code pending",
|
|
@@ -225,6 +229,7 @@ const COMPENSATION_COPY = {
|
|
|
225
229
|
"zh-CN": {
|
|
226
230
|
latest: "本单工资",
|
|
227
231
|
session: "本单工资",
|
|
232
|
+
"last-hours": "本段工资",
|
|
228
233
|
today: "本日工资",
|
|
229
234
|
"last-7-days": "近七日工资",
|
|
230
235
|
"this-week": "本周工资",
|
|
@@ -234,6 +239,7 @@ const COMPENSATION_COPY = {
|
|
|
234
239
|
en: {
|
|
235
240
|
latest: "SHIFT PAY",
|
|
236
241
|
session: "SHIFT PAY",
|
|
242
|
+
"last-hours": "WINDOW PAY",
|
|
237
243
|
today: "TODAY'S PAY",
|
|
238
244
|
"last-7-days": "7-DAY PAY",
|
|
239
245
|
"this-week": "THIS WEEK'S PAY",
|
|
@@ -262,6 +268,11 @@ export function getReceiptCopy(locale = DEFAULT_LOCALE) {
|
|
|
262
268
|
return RECEIPT_COPY[SUPPORTED_LOCALES.has(locale) ? locale : DEFAULT_LOCALE];
|
|
263
269
|
}
|
|
264
270
|
|
|
271
|
+
export function getScopeLabel(scope, locale = DEFAULT_LOCALE, hours = null) {
|
|
272
|
+
const template = getReceiptCopy(locale).scope[scope] || getReceiptCopy(locale).scope.latest;
|
|
273
|
+
return String(template).replaceAll("{hours}", String(hours || 3));
|
|
274
|
+
}
|
|
275
|
+
|
|
265
276
|
export function buildCompensation(scope, amount, locale = DEFAULT_LOCALE) {
|
|
266
277
|
const copy = COMPENSATION_COPY[SUPPORTED_LOCALES.has(locale) ? locale : DEFAULT_LOCALE];
|
|
267
278
|
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);
|
|
@@ -86,6 +91,7 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
86
91
|
busyLabel: copy.exportingImage,
|
|
87
92
|
successLabel: copy.exportSuccess,
|
|
88
93
|
errorLabel: copy.exportError,
|
|
94
|
+
miniProgramLabel: copy.exportMiniProgramLabel,
|
|
89
95
|
fileBase: `codex-work-receipt-${record.source.scope}-${spansMultipleDates ? `${rangeStartDate}-to-${rangeEndDate}` : `${rangeEndDate}-${record.id.slice(4, 12)}`}`,
|
|
90
96
|
}).replaceAll("<", "\\u003c");
|
|
91
97
|
const rows = [
|
|
@@ -146,7 +152,7 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
146
152
|
<div class="qr-grid qr-grid--export-only" hidden>
|
|
147
153
|
<div class="qr-item">
|
|
148
154
|
<div class="qr-frame">${miniProgramVisual}</div>
|
|
149
|
-
<strong>${escapeHtml(copy.openMiniProgram)}</strong>
|
|
155
|
+
<strong data-export-mini-label>${escapeHtml(copy.openMiniProgram)}</strong>
|
|
150
156
|
<span>${escapeHtml(copy.openMiniProgramHint)}</span>
|
|
151
157
|
</div>
|
|
152
158
|
${dataQrItems}
|
|
@@ -155,7 +161,7 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
155
161
|
<div class="qr-grid qr-grid--single">
|
|
156
162
|
<div class="qr-item">
|
|
157
163
|
<div class="qr-frame">${miniProgramVisual}</div>
|
|
158
|
-
<strong>${escapeHtml(copy.openMiniProgram)}</strong>
|
|
164
|
+
<strong data-export-mini-label>${escapeHtml(copy.openMiniProgram)}</strong>
|
|
159
165
|
<span>${escapeHtml(copy.openMiniProgramHint)}</span>
|
|
160
166
|
</div>
|
|
161
167
|
${dataQrItems}
|
|
@@ -687,6 +693,50 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
687
693
|
}));
|
|
688
694
|
}
|
|
689
695
|
|
|
696
|
+
function sanitizeExportNode(node) {
|
|
697
|
+
const multipartLiveNode = node.querySelector(".multipart-live");
|
|
698
|
+
if (multipartLiveNode) multipartLiveNode.remove();
|
|
699
|
+
|
|
700
|
+
const exportGridNode = node.querySelector(".qr-grid--export-only");
|
|
701
|
+
if (exportGridNode) {
|
|
702
|
+
exportGridNode.removeAttribute("hidden");
|
|
703
|
+
exportGridNode.style.display = "grid";
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
node.querySelectorAll("[data-data-qr-index]").forEach((item) => item.remove());
|
|
707
|
+
node.querySelectorAll(".qr-grid").forEach((grid) => {
|
|
708
|
+
grid.style.gridTemplateColumns = "minmax(0, 1fr)";
|
|
709
|
+
grid.style.justifyItems = "center";
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
const miniProgramLabel = node.querySelector("[data-export-mini-label]");
|
|
713
|
+
if (miniProgramLabel) miniProgramLabel.textContent = exportConfig.miniProgramLabel;
|
|
714
|
+
const transferDescription = node.querySelector(".transfer-heading p");
|
|
715
|
+
if (transferDescription) transferDescription.remove();
|
|
716
|
+
const transferNote = node.querySelector(".transfer-note");
|
|
717
|
+
if (transferNote) transferNote.remove();
|
|
718
|
+
|
|
719
|
+
node.querySelectorAll(".paper").forEach((paper) => {
|
|
720
|
+
paper.style.boxShadow = "none";
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function createExportNode(source, paperColor) {
|
|
725
|
+
const clone = source.cloneNode(true);
|
|
726
|
+
sanitizeExportNode(clone);
|
|
727
|
+
const sourceWidth = Math.ceil(source.getBoundingClientRect().width || source.scrollWidth);
|
|
728
|
+
Object.assign(clone.style, {
|
|
729
|
+
position: "absolute",
|
|
730
|
+
left: "-100000px",
|
|
731
|
+
top: "0",
|
|
732
|
+
width: sourceWidth + "px",
|
|
733
|
+
background: paperColor,
|
|
734
|
+
pointerEvents: "none",
|
|
735
|
+
});
|
|
736
|
+
document.body.appendChild(clone);
|
|
737
|
+
return clone;
|
|
738
|
+
}
|
|
739
|
+
|
|
690
740
|
function downloadImage(dataUrl, filename) {
|
|
691
741
|
const link = document.createElement("a");
|
|
692
742
|
link.href = dataUrl;
|
|
@@ -703,15 +753,17 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
703
753
|
exportButton.disabled = true;
|
|
704
754
|
exportButton.textContent = exportConfig.busyLabel;
|
|
705
755
|
setExportStatus("");
|
|
756
|
+
let renderNode = null;
|
|
706
757
|
|
|
707
758
|
try {
|
|
708
759
|
if (document.fonts?.ready) await document.fonts.ready;
|
|
709
|
-
await waitForImages(exportNode);
|
|
710
|
-
const width = Math.ceil(exportNode.scrollWidth);
|
|
711
|
-
const height = Math.ceil(exportNode.scrollHeight);
|
|
712
|
-
const scale = Math.max(2, Math.min(3, 1500 / Math.max(1, width)));
|
|
713
760
|
const paperColor = getComputedStyle(document.documentElement).getPropertyValue("--paper").trim() || "#ffffff";
|
|
714
|
-
|
|
761
|
+
renderNode = createExportNode(exportNode, paperColor);
|
|
762
|
+
await waitForImages(renderNode);
|
|
763
|
+
const width = Math.ceil(renderNode.scrollWidth);
|
|
764
|
+
const height = Math.ceil(renderNode.scrollHeight);
|
|
765
|
+
const scale = Math.max(2, Math.min(3, 1500 / Math.max(1, width)));
|
|
766
|
+
const dataUrl = await domtoimage.toPng(renderNode, {
|
|
715
767
|
width,
|
|
716
768
|
height,
|
|
717
769
|
scale,
|
|
@@ -720,17 +772,12 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
720
772
|
cacheBust: false,
|
|
721
773
|
style: { background: paperColor },
|
|
722
774
|
onclone(clone) {
|
|
775
|
+
sanitizeExportNode(clone);
|
|
776
|
+
clone.style.position = "static";
|
|
777
|
+
clone.style.left = "auto";
|
|
778
|
+
clone.style.top = "auto";
|
|
779
|
+
clone.style.width = width + "px";
|
|
723
780
|
clone.style.background = paperColor;
|
|
724
|
-
const multipartLiveClone = clone.querySelector(".multipart-live");
|
|
725
|
-
if (multipartLiveClone) multipartLiveClone.remove();
|
|
726
|
-
const exportGridClone = clone.querySelector(".qr-grid--export-only");
|
|
727
|
-
if (exportGridClone) {
|
|
728
|
-
exportGridClone.removeAttribute("hidden");
|
|
729
|
-
exportGridClone.style.display = "grid";
|
|
730
|
-
}
|
|
731
|
-
clone.querySelectorAll(".paper").forEach((paper) => {
|
|
732
|
-
paper.style.boxShadow = "none";
|
|
733
|
-
});
|
|
734
781
|
},
|
|
735
782
|
logger: {},
|
|
736
783
|
});
|
|
@@ -743,6 +790,7 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
743
790
|
console.error(error);
|
|
744
791
|
setExportStatus(exportConfig.errorLabel, "error");
|
|
745
792
|
} finally {
|
|
793
|
+
if (renderNode) renderNode.remove();
|
|
746
794
|
exportButton.disabled = false;
|
|
747
795
|
exportButton.textContent = exportConfig.idleLabel;
|
|
748
796
|
}
|