codex-work-receipt 0.6.0 → 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 +12 -0
- package/README.en.md +23 -4
- package/README.md +23 -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 +5 -1
- package/docs/data-schema.md +5 -1
- package/docs/images/codex-pet-showcase.png +0 -0
- package/docs/mobile-import.en.md +2 -2
- package/docs/mobile-import.md +2 -2
- 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/barcode.mjs +52 -0
- 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 +21 -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 +219 -17
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
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const CODE128_PATTERNS = [
|
|
2
|
+
"212222", "222122", "222221", "121223", "121322", "131222", "122213", "122312", "132212", "221213", "221312", "231212",
|
|
3
|
+
"112232", "122132", "122231", "113222", "123122", "123221", "223211", "221132", "221231", "213212", "223112", "312131",
|
|
4
|
+
"311222", "321122", "321221", "312212", "322112", "322211", "212123", "212321", "232121", "111323", "131123", "131321",
|
|
5
|
+
"112313", "132113", "132311", "211313", "231113", "231311", "112133", "112331", "132131", "113123", "113321", "133121",
|
|
6
|
+
"313121", "211331", "231131", "213113", "213311", "213131", "311123", "311321", "331121", "312113", "312311", "332111",
|
|
7
|
+
"314111", "221411", "431111", "111224", "111422", "121124", "121421", "141122", "141221", "112214", "112412", "122114",
|
|
8
|
+
"122411", "142112", "142211", "241211", "221114", "413111", "241112", "134111", "111242", "121142", "121241", "114212",
|
|
9
|
+
"124112", "124211", "411212", "421112", "421211", "212141", "214121", "412121", "111143", "111341", "131141", "114113",
|
|
10
|
+
"114311", "411113", "411311", "113141", "114131", "311141", "411131", "211412", "211214", "211232", "2331112",
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const START_CODE_B = 104;
|
|
14
|
+
const STOP_CODE = 106;
|
|
15
|
+
|
|
16
|
+
function normalizeCode128Value(value) {
|
|
17
|
+
return [...String(value)]
|
|
18
|
+
.map((character) => {
|
|
19
|
+
const code = character.charCodeAt(0);
|
|
20
|
+
return code >= 32 && code <= 126 ? character : "?";
|
|
21
|
+
})
|
|
22
|
+
.join("")
|
|
23
|
+
.slice(0, 48) || "CWR";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildCode128B(value) {
|
|
27
|
+
const normalized = normalizeCode128Value(value);
|
|
28
|
+
const dataCodes = [...normalized].map((character) => character.charCodeAt(0) - 32);
|
|
29
|
+
const checksum = dataCodes.reduce(
|
|
30
|
+
(total, code, index) => total + code * (index + 1),
|
|
31
|
+
START_CODE_B,
|
|
32
|
+
) % 103;
|
|
33
|
+
const codes = [START_CODE_B, ...dataCodes, checksum, STOP_CODE];
|
|
34
|
+
const segments = [];
|
|
35
|
+
|
|
36
|
+
for (const code of codes) {
|
|
37
|
+
const pattern = CODE128_PATTERNS[code];
|
|
38
|
+
for (const [index, width] of [...pattern].entries()) {
|
|
39
|
+
segments.push({
|
|
40
|
+
isBar: index % 2 === 0,
|
|
41
|
+
width: Number(width),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
value: normalized,
|
|
48
|
+
checksum,
|
|
49
|
+
segments,
|
|
50
|
+
totalModules: segments.reduce((total, segment) => total + segment.width, 0),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -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": "本周全部会话",
|
|
@@ -133,6 +134,12 @@ const RECEIPT_COPY = {
|
|
|
133
134
|
openMiniProgramHint: "微信扫码进入 AI 打工图鉴",
|
|
134
135
|
importData: "2 · 扫描导入数据",
|
|
135
136
|
importDataHint: "在小程序里点击“从电脑导入”后扫描",
|
|
137
|
+
multipartOpenTitle: "先打开小程序",
|
|
138
|
+
multipartOpenHint: "数据码将在 {seconds} 秒后自动开始轮播,无需点击电脑",
|
|
139
|
+
multipartTransferTitle: "保持手机对准屏幕",
|
|
140
|
+
multipartTransferHint: "数据码会自动逐张轮播;漏扫或重复都没关系",
|
|
141
|
+
multipartPartLabel: "数据分片 {current}/{total}",
|
|
142
|
+
multipartRestart: "重新显示小程序码",
|
|
136
143
|
dataQrAlt: "当前小票数据二维码",
|
|
137
144
|
miniProgramAlt: "微信小程序码",
|
|
138
145
|
placeholderLabel: "小程序码",
|
|
@@ -159,6 +166,7 @@ const RECEIPT_COPY = {
|
|
|
159
166
|
scope: {
|
|
160
167
|
latest: "Latest session",
|
|
161
168
|
session: "Selected session",
|
|
169
|
+
"last-hours": "Last {hours} hours",
|
|
162
170
|
today: "All sessions today",
|
|
163
171
|
"last-7-days": "Last 7 calendar days",
|
|
164
172
|
"this-week": "All sessions this week",
|
|
@@ -199,6 +207,12 @@ const RECEIPT_COPY = {
|
|
|
199
207
|
openMiniProgramHint: "Scan with WeChat to open AI Work Archive",
|
|
200
208
|
importData: "2 · Import receipt data",
|
|
201
209
|
importDataHint: "Tap “从电脑导入小票” in the mini program, then scan this code",
|
|
210
|
+
multipartOpenTitle: "Open the mini program first",
|
|
211
|
+
multipartOpenHint: "The data codes will start rotating automatically in {seconds}s. No desktop click is required.",
|
|
212
|
+
multipartTransferTitle: "Keep your phone pointed at the screen",
|
|
213
|
+
multipartTransferHint: "Data codes rotate automatically. Missed or duplicate scans are safe.",
|
|
214
|
+
multipartPartLabel: "Data part {current}/{total}",
|
|
215
|
+
multipartRestart: "Show mini-program code again",
|
|
202
216
|
dataQrAlt: "Current receipt data QR code",
|
|
203
217
|
miniProgramAlt: "WeChat mini-program code",
|
|
204
218
|
placeholderLabel: "Mini program",
|
|
@@ -213,6 +227,7 @@ const COMPENSATION_COPY = {
|
|
|
213
227
|
"zh-CN": {
|
|
214
228
|
latest: "本单工资",
|
|
215
229
|
session: "本单工资",
|
|
230
|
+
"last-hours": "本段工资",
|
|
216
231
|
today: "本日工资",
|
|
217
232
|
"last-7-days": "近七日工资",
|
|
218
233
|
"this-week": "本周工资",
|
|
@@ -222,6 +237,7 @@ const COMPENSATION_COPY = {
|
|
|
222
237
|
en: {
|
|
223
238
|
latest: "SHIFT PAY",
|
|
224
239
|
session: "SHIFT PAY",
|
|
240
|
+
"last-hours": "WINDOW PAY",
|
|
225
241
|
today: "TODAY'S PAY",
|
|
226
242
|
"last-7-days": "7-DAY PAY",
|
|
227
243
|
"this-week": "THIS WEEK'S PAY",
|
|
@@ -250,6 +266,11 @@ export function getReceiptCopy(locale = DEFAULT_LOCALE) {
|
|
|
250
266
|
return RECEIPT_COPY[SUPPORTED_LOCALES.has(locale) ? locale : DEFAULT_LOCALE];
|
|
251
267
|
}
|
|
252
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
|
+
|
|
253
274
|
export function buildCompensation(scope, amount, locale = DEFAULT_LOCALE) {
|
|
254
275
|
const copy = COMPENSATION_COPY[SUPPORTED_LOCALES.has(locale) ? locale : DEFAULT_LOCALE];
|
|
255
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 会话");
|