codex-work-receipt 0.7.3 → 0.8.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 +18 -2
- package/README.en.md +27 -7
- package/README.md +27 -7
- package/docs/cli.en.md +55 -5
- package/docs/cli.md +55 -5
- package/docs/codex-pet.en.md +1 -1
- package/docs/codex-pet.md +1 -1
- package/docs/codex-skill.en.md +2 -0
- package/docs/codex-skill.md +2 -0
- package/docs/data-schema.en.md +29 -4
- package/docs/data-schema.md +29 -4
- package/docs/fixtures/cwr-file-v1.json +33 -0
- package/docs/miniprogram-file-import.en.md +67 -0
- package/docs/miniprogram-file-import.md +67 -0
- package/docs/mobile-import.en.md +19 -11
- package/docs/mobile-import.md +18 -10
- package/docs/privacy.en.md +20 -5
- package/docs/privacy.md +20 -5
- package/package.json +2 -2
- package/skills/ai-work-receipt/SKILL.md +2 -2
- package/src/auto-hook.mjs +46 -0
- package/src/auto-runner.mjs +172 -0
- package/src/cli.mjs +158 -60
- package/src/core/args.mjs +34 -0
- package/src/core/auto-mode.mjs +364 -0
- package/src/core/file-payload.mjs +74 -0
- package/src/core/generator.mjs +98 -0
- package/src/core/mode-selector.mjs +31 -0
- package/src/core/open-source.mjs +13 -0
- package/src/core/presentation.mjs +64 -28
- package/src/core/qr-payload.mjs +7 -178
- package/src/core/receipt-record.mjs +18 -10
- package/src/core/transfer-record.mjs +137 -0
- package/src/lib/files.mjs +52 -0
- package/src/parsers/codex.mjs +6 -6
- package/src/renderers/html.mjs +352 -201
|
@@ -85,6 +85,15 @@ const RECEIPT_COPY = {
|
|
|
85
85
|
exportSuccess: "图片已保存",
|
|
86
86
|
exportError: "图片生成失败,请重试",
|
|
87
87
|
receiptAria: "Codex AI 打工小票",
|
|
88
|
+
sidebar: {
|
|
89
|
+
aria: "相关信息",
|
|
90
|
+
supportTitle: "支持项目",
|
|
91
|
+
supportDescription: "喜欢这个工具?",
|
|
92
|
+
changelogTitle: "更新日志",
|
|
93
|
+
changelogLink: "查看更新日志 →",
|
|
94
|
+
sponsorLabel: "赞助伙伴",
|
|
95
|
+
sponsorAlt: "ModelFlare 标志",
|
|
96
|
+
},
|
|
88
97
|
themes: {
|
|
89
98
|
classic: "经典白票",
|
|
90
99
|
diner: "复古粉票",
|
|
@@ -127,28 +136,37 @@ const RECEIPT_COPY = {
|
|
|
127
136
|
seconds: ["秒", "秒"],
|
|
128
137
|
},
|
|
129
138
|
footerThanks: "谢谢惠顾,欢迎明天继续改需求",
|
|
130
|
-
transferAria: "
|
|
131
|
-
transferTitle: "
|
|
132
|
-
transferDescription: "
|
|
133
|
-
openMiniProgram: "
|
|
139
|
+
transferAria: "微信小程序导入联",
|
|
140
|
+
transferTitle: "导入到微信小程序",
|
|
141
|
+
transferDescription: "下载脱敏数据文件,通过微信聊天文件导入",
|
|
142
|
+
openMiniProgram: "打开小程序",
|
|
134
143
|
openMiniProgramHint: "微信扫码进入 AI 打工图鉴",
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
144
|
+
fileImportTitle: "从聊天文件导入",
|
|
145
|
+
fileImportSteps: [
|
|
146
|
+
"下载微信导入文件",
|
|
147
|
+
"把文件发送到“文件传输助手”或自己的聊天",
|
|
148
|
+
"在小程序中点击“从聊天文件导入”并选择该文件",
|
|
149
|
+
],
|
|
150
|
+
downloadFile: "下载微信导入文件",
|
|
151
|
+
downloadFileHint: "下载后发送到文件传输助手",
|
|
152
|
+
downloadSuccess: "文件已下载,请发送到微信文件传输助手",
|
|
153
|
+
downloadError: "文件下载失败,请重试",
|
|
154
|
+
fileImportPrivacy: "文件只包含脱敏统计。发送到聊天前,请确认接收方可信。",
|
|
155
|
+
scanAlternativeTitle: "也可以扫码导入",
|
|
156
|
+
scanAlternativeHint: "这份数据可以由一个二维码完整承载",
|
|
157
|
+
showDataQr: "显示数据二维码",
|
|
158
|
+
showMiniProgramCode: "返回小程序码",
|
|
159
|
+
dataQrTitle: "扫描导入数据",
|
|
160
|
+
dataQrHint: "在小程序中选择扫码导入后扫描",
|
|
143
161
|
dataQrAlt: "当前小票数据二维码",
|
|
144
162
|
miniProgramAlt: "微信小程序码",
|
|
145
163
|
exportMiniProgramLabel: "微信扫码打开小程序",
|
|
146
164
|
placeholderLabel: "小程序码",
|
|
147
165
|
placeholderValue: "待接入",
|
|
148
166
|
placeholderAria: "小程序码待接入",
|
|
149
|
-
transferNote: "
|
|
167
|
+
transferNote: "导入文件和可选数据码只包含时间、轮次、Token 和工具调用等统计,不包含 Prompt、回复正文、代码、项目路径或文件名。",
|
|
150
168
|
rollingSummaryNotice: "最近 {hours} 小时属于滚动摘要,只保存到私人历史,不参与 AI 供销社统计。需要统计时请生成“今日 / 本周 / 近 7 日 / 指定会话”小票。",
|
|
151
|
-
privacy: "
|
|
169
|
+
privacy: "结构数据和微信导入文件同时保存在本机;只有你主动发送文件或扫码时,脱敏统计才会离开电脑。",
|
|
152
170
|
},
|
|
153
171
|
en: {
|
|
154
172
|
htmlLang: "en",
|
|
@@ -160,6 +178,15 @@ const RECEIPT_COPY = {
|
|
|
160
178
|
exportSuccess: "Image saved",
|
|
161
179
|
exportError: "Could not generate the image. Please try again.",
|
|
162
180
|
receiptAria: "Codex AI Work Receipt",
|
|
181
|
+
sidebar: {
|
|
182
|
+
aria: "Related information",
|
|
183
|
+
supportTitle: "Support the project",
|
|
184
|
+
supportDescription: "Enjoying this tool?",
|
|
185
|
+
changelogTitle: "Changelog",
|
|
186
|
+
changelogLink: "View the changelog →",
|
|
187
|
+
sponsorLabel: "Sponsored by",
|
|
188
|
+
sponsorAlt: "ModelFlare logo",
|
|
189
|
+
},
|
|
163
190
|
themes: {
|
|
164
191
|
classic: "Classic White",
|
|
165
192
|
diner: "Vintage Pink",
|
|
@@ -202,28 +229,37 @@ const RECEIPT_COPY = {
|
|
|
202
229
|
seconds: ["sec", "sec"],
|
|
203
230
|
},
|
|
204
231
|
footerThanks: "Thanks for your business. More revisions welcome tomorrow.",
|
|
205
|
-
transferAria: "
|
|
206
|
-
transferTitle: "
|
|
207
|
-
transferDescription: "
|
|
208
|
-
openMiniProgram: "
|
|
232
|
+
transferAria: "WeChat mini-program import stub",
|
|
233
|
+
transferTitle: "IMPORT INTO WECHAT",
|
|
234
|
+
transferDescription: "Download the privacy-safe data file and import it from a WeChat chat.",
|
|
235
|
+
openMiniProgram: "Open mini program",
|
|
209
236
|
openMiniProgramHint: "Scan with WeChat to open AI Work Archive",
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
237
|
+
fileImportTitle: "Import from a chat file",
|
|
238
|
+
fileImportSteps: [
|
|
239
|
+
"Download the WeChat import file",
|
|
240
|
+
"Send it to File Transfer or one of your own chats",
|
|
241
|
+
"Tap “Import from chat file” in the mini program and select it",
|
|
242
|
+
],
|
|
243
|
+
downloadFile: "Download WeChat import file",
|
|
244
|
+
downloadFileHint: "Then send it to WeChat File Transfer",
|
|
245
|
+
downloadSuccess: "File downloaded. Send it to WeChat File Transfer.",
|
|
246
|
+
downloadError: "Could not download the file. Please try again.",
|
|
247
|
+
fileImportPrivacy: "The file contains privacy-safe metrics only. Send it only to a chat you trust.",
|
|
248
|
+
scanAlternativeTitle: "Or import by scanning",
|
|
249
|
+
scanAlternativeHint: "This receipt fits completely in one data QR code.",
|
|
250
|
+
showDataQr: "Show data QR code",
|
|
251
|
+
showMiniProgramCode: "Back to mini-program code",
|
|
252
|
+
dataQrTitle: "Scan to import data",
|
|
253
|
+
dataQrHint: "Choose scan import in the mini program, then scan this code",
|
|
218
254
|
dataQrAlt: "Current receipt data QR code",
|
|
219
255
|
miniProgramAlt: "WeChat mini-program code",
|
|
220
256
|
exportMiniProgramLabel: "Scan to open the mini program",
|
|
221
257
|
placeholderLabel: "Mini program",
|
|
222
258
|
placeholderValue: "Pending",
|
|
223
259
|
placeholderAria: "Mini-program code pending",
|
|
224
|
-
transferNote: "The data code
|
|
260
|
+
transferNote: "The import file and optional data code contain only statistics such as time, turns, Tokens, and tool calls. They do not contain prompts, responses, code, project paths, or file names.",
|
|
225
261
|
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.",
|
|
226
|
-
privacy: "Structured data
|
|
262
|
+
privacy: "Structured data and the WeChat import file stay on this computer until you explicitly send the file or scan its data code.",
|
|
227
263
|
},
|
|
228
264
|
};
|
|
229
265
|
|
package/src/core/qr-payload.mjs
CHANGED
|
@@ -2,7 +2,9 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import zlib from "node:zlib";
|
|
3
3
|
import QRCode from "qrcode";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { compactReceipt } from "./transfer-record.mjs";
|
|
6
|
+
|
|
7
|
+
export { compactReceipt } from "./transfer-record.mjs";
|
|
6
8
|
|
|
7
9
|
const MAX_QR_VERSION = 25;
|
|
8
10
|
const MAX_MULTIPART_PARTS = 12;
|
|
@@ -11,142 +13,6 @@ function checksum(value, length = 8) {
|
|
|
11
13
|
return crypto.createHash("sha256").update(value).digest("hex").slice(0, length);
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
function compactPresentation(record) {
|
|
15
|
-
const profileId = record.presentation.work_profile;
|
|
16
|
-
const scope = record.source?.scope || (record.presentation.compensation?.label === "本日工资" ? "today" : "latest");
|
|
17
|
-
const mobileProfile = profileId
|
|
18
|
-
? getWorkProfileCopy(profileId, "zh-CN")
|
|
19
|
-
: { title: record.presentation.work_title, review: record.presentation.review };
|
|
20
|
-
const mobileCompensation = profileId
|
|
21
|
-
? buildCompensation(scope, record.presentation.compensation?.amount, "zh-CN")
|
|
22
|
-
: record.presentation.compensation;
|
|
23
|
-
|
|
24
|
-
return {
|
|
25
|
-
profileId,
|
|
26
|
-
scope,
|
|
27
|
-
mobileProfile,
|
|
28
|
-
mobileCompensation,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function compactBase(record) {
|
|
33
|
-
const presentation = compactPresentation(record);
|
|
34
|
-
return {
|
|
35
|
-
v: record.schema_version,
|
|
36
|
-
i: record.id,
|
|
37
|
-
g: record.generated_at,
|
|
38
|
-
o: presentation.scope,
|
|
39
|
-
d: [
|
|
40
|
-
record.period.start_at,
|
|
41
|
-
record.period.end_at,
|
|
42
|
-
record.period.timezone,
|
|
43
|
-
record.period.range_start_date || null,
|
|
44
|
-
record.period.range_end_date || null,
|
|
45
|
-
],
|
|
46
|
-
s: [
|
|
47
|
-
record.stats.session_count,
|
|
48
|
-
record.stats.completed_turns,
|
|
49
|
-
record.stats.user_messages,
|
|
50
|
-
record.stats.tool_calls,
|
|
51
|
-
record.stats.interruptions,
|
|
52
|
-
record.stats.work_duration_ms,
|
|
53
|
-
record.stats.average_first_token_ms,
|
|
54
|
-
],
|
|
55
|
-
t: [
|
|
56
|
-
record.stats.tokens.input_tokens,
|
|
57
|
-
record.stats.tokens.cached_input_tokens,
|
|
58
|
-
record.stats.tokens.output_tokens,
|
|
59
|
-
record.stats.tokens.reasoning_output_tokens,
|
|
60
|
-
record.stats.tokens.total_tokens,
|
|
61
|
-
],
|
|
62
|
-
m: record.stats.models,
|
|
63
|
-
l: record.locale || "zh-CN",
|
|
64
|
-
r: presentation.profileId || null,
|
|
65
|
-
p: [
|
|
66
|
-
record.presentation.default_theme,
|
|
67
|
-
presentation.mobileProfile.title,
|
|
68
|
-
presentation.mobileProfile.review,
|
|
69
|
-
presentation.mobileCompensation
|
|
70
|
-
? [
|
|
71
|
-
presentation.mobileCompensation.label,
|
|
72
|
-
presentation.mobileCompensation.amount,
|
|
73
|
-
presentation.mobileCompensation.unit,
|
|
74
|
-
presentation.mobileCompensation.note,
|
|
75
|
-
presentation.mobileCompensation.formula_version,
|
|
76
|
-
]
|
|
77
|
-
: null,
|
|
78
|
-
],
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function compactFact(fact) {
|
|
83
|
-
const stats = fact.stats;
|
|
84
|
-
return [
|
|
85
|
-
fact.fact_id,
|
|
86
|
-
fact.session_id,
|
|
87
|
-
fact.identity_quality,
|
|
88
|
-
fact.source_type,
|
|
89
|
-
fact.local_date,
|
|
90
|
-
fact.bucket_start_at,
|
|
91
|
-
fact.bucket_end_at,
|
|
92
|
-
fact.source_watermark_at,
|
|
93
|
-
fact.observed_at,
|
|
94
|
-
[
|
|
95
|
-
fact.source_revision.kind,
|
|
96
|
-
fact.source_revision.row_count,
|
|
97
|
-
fact.source_revision.byte_length,
|
|
98
|
-
fact.source_revision.tail_hash,
|
|
99
|
-
],
|
|
100
|
-
fact.content_hash,
|
|
101
|
-
[
|
|
102
|
-
stats.completed_turns,
|
|
103
|
-
stats.user_messages,
|
|
104
|
-
stats.tool_calls,
|
|
105
|
-
stats.interruptions,
|
|
106
|
-
stats.work_duration_ms,
|
|
107
|
-
stats.first_token_total_ms,
|
|
108
|
-
stats.first_token_sample_count,
|
|
109
|
-
stats.input_tokens,
|
|
110
|
-
stats.cached_input_tokens,
|
|
111
|
-
stats.output_tokens,
|
|
112
|
-
stats.reasoning_output_tokens,
|
|
113
|
-
stats.total_tokens,
|
|
114
|
-
stats.token_reset_count,
|
|
115
|
-
stats.models,
|
|
116
|
-
],
|
|
117
|
-
];
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function compactReceipt(record) {
|
|
121
|
-
const compact = compactBase(record);
|
|
122
|
-
if (record.schema_version !== 2) return compact;
|
|
123
|
-
|
|
124
|
-
const coverage = record.manifest.coverage;
|
|
125
|
-
return {
|
|
126
|
-
...compact,
|
|
127
|
-
k: record.source.logical_key,
|
|
128
|
-
h: record.source.snapshot_hash,
|
|
129
|
-
a: [
|
|
130
|
-
record.manifest.version,
|
|
131
|
-
record.manifest.fact_schema_version,
|
|
132
|
-
record.manifest.metric_schema_version,
|
|
133
|
-
record.manifest.accounting_timezone,
|
|
134
|
-
record.manifest.fact_count,
|
|
135
|
-
record.manifest.fact_ids,
|
|
136
|
-
[
|
|
137
|
-
coverage.kind,
|
|
138
|
-
coverage.scan_mode,
|
|
139
|
-
coverage.start_date || null,
|
|
140
|
-
coverage.end_date || null,
|
|
141
|
-
coverage.complete_through_date || null,
|
|
142
|
-
coverage.observed_through_at,
|
|
143
|
-
],
|
|
144
|
-
record.manifest.manifest_hash,
|
|
145
|
-
],
|
|
146
|
-
f: record.facts.map(compactFact),
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
16
|
function encodeCompact(prefix, compact) {
|
|
151
17
|
const compressed = zlib.deflateRawSync(Buffer.from(JSON.stringify(compact), "utf8"));
|
|
152
18
|
return `${prefix}.${checksum(compressed)}.${compressed.toString("base64url")}`;
|
|
@@ -168,48 +34,11 @@ function fitsVersion(payload, maxVersion) {
|
|
|
168
34
|
}
|
|
169
35
|
}
|
|
170
36
|
|
|
171
|
-
function
|
|
172
|
-
return `cwr2p.${transferId}.${partIndex}.${partCount}.${totalChecksum}.${checksum(chunk)}.${chunk}`;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function maxChunkSize(singlePayload, maxVersion) {
|
|
176
|
-
const transferId = checksum(singlePayload, 12);
|
|
177
|
-
const totalChecksum = checksum(singlePayload);
|
|
178
|
-
let low = 32;
|
|
179
|
-
let high = singlePayload.length;
|
|
180
|
-
|
|
181
|
-
while (low < high) {
|
|
182
|
-
const middle = Math.ceil((low + high) / 2);
|
|
183
|
-
const candidate = multipartPayload(transferId, 12, 12, totalChecksum, singlePayload.slice(0, middle));
|
|
184
|
-
if (fitsVersion(candidate, maxVersion)) low = middle;
|
|
185
|
-
else high = middle - 1;
|
|
186
|
-
}
|
|
187
|
-
return low;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
export function encodeReceiptPayloads(record, options = {}) {
|
|
37
|
+
export function encodeSingleReceiptQr(record, options = {}) {
|
|
191
38
|
const maxVersion = options.maxVersion || MAX_QR_VERSION;
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if (!singlePayload.startsWith("cwr2.")) {
|
|
196
|
-
throw new Error("旧版小票数据超过单个二维码容量");
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const chunkSize = maxChunkSize(singlePayload, maxVersion);
|
|
200
|
-
const chunks = [];
|
|
201
|
-
for (let offset = 0; offset < singlePayload.length; offset += chunkSize) {
|
|
202
|
-
chunks.push(singlePayload.slice(offset, offset + chunkSize));
|
|
203
|
-
}
|
|
204
|
-
if (chunks.length > maxParts) {
|
|
205
|
-
throw new Error(`小票数据需要 ${chunks.length} 个二维码,超过 ${maxParts} 个上限,请缩小统计范围`);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const transferId = checksum(singlePayload, 12);
|
|
209
|
-
const totalChecksum = checksum(singlePayload);
|
|
210
|
-
return chunks.map((chunk, index) => (
|
|
211
|
-
multipartPayload(transferId, index + 1, chunks.length, totalChecksum, chunk)
|
|
212
|
-
));
|
|
39
|
+
const payload = encodeReceiptPayload(record);
|
|
40
|
+
if (!fitsVersion(payload, maxVersion)) return null;
|
|
41
|
+
return { payload, version: qrVersion(payload) };
|
|
213
42
|
}
|
|
214
43
|
|
|
215
44
|
function decodeSinglePayload(payload) {
|
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
DEFAULT_LOCALE,
|
|
15
15
|
getWorkProfileCopy,
|
|
16
16
|
} from "./presentation.mjs";
|
|
17
|
+
import { createReceiptFile } from "./file-payload.mjs";
|
|
18
|
+
import { writeFileAtomicSync, writeWithAtomicFileSync } from "../lib/files.mjs";
|
|
17
19
|
|
|
18
20
|
const SCHEMA_VERSION = 2;
|
|
19
21
|
const SOURCE_VERSION = "cwr2";
|
|
@@ -127,7 +129,13 @@ export function buildReceiptRecord(metrics, defaultTheme = "classic", locale = D
|
|
|
127
129
|
return record;
|
|
128
130
|
}
|
|
129
131
|
|
|
130
|
-
export function
|
|
132
|
+
export function transferFilePathForOutput(outputHtmlPath) {
|
|
133
|
+
return /\.html?$/i.test(outputHtmlPath)
|
|
134
|
+
? outputHtmlPath.replace(/\.html?$/i, ".cwr.json")
|
|
135
|
+
: `${outputHtmlPath}.cwr.json`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function persistReceiptRecord(record, outputHtmlPath, requestedDataDir = null, requestedTransferFile = null) {
|
|
131
139
|
const dataDir = path.resolve(
|
|
132
140
|
requestedDataDir || process.env.CODEX_WORK_RECEIPT_HOME || path.join(os.homedir(), ".codex-work-receipt"),
|
|
133
141
|
);
|
|
@@ -135,8 +143,8 @@ export function persistReceiptRecord(record, outputHtmlPath, requestedDataDir =
|
|
|
135
143
|
fs.mkdirSync(receiptsDir, { recursive: true });
|
|
136
144
|
|
|
137
145
|
const receiptPath = path.join(receiptsDir, `${record.id}.json`);
|
|
138
|
-
|
|
139
|
-
|
|
146
|
+
writeFileAtomicSync(receiptPath, `${JSON.stringify(record, null, 2)}\n`);
|
|
147
|
+
writeFileAtomicSync(path.join(dataDir, "latest.json"), `${JSON.stringify(record, null, 2)}\n`);
|
|
140
148
|
|
|
141
149
|
const allRecords = fs.readdirSync(receiptsDir)
|
|
142
150
|
.filter((name) => name.endsWith(".json"))
|
|
@@ -156,16 +164,16 @@ export function persistReceiptRecord(record, outputHtmlPath, requestedDataDir =
|
|
|
156
164
|
const history = [...deduplicated.values()]
|
|
157
165
|
.sort((left, right) => String(left.period?.end_at).localeCompare(String(right.period?.end_at)));
|
|
158
166
|
const historyPath = path.join(dataDir, "history.jsonl");
|
|
159
|
-
|
|
160
|
-
try {
|
|
167
|
+
writeWithAtomicFileSync(historyPath, (historyDescriptor) => {
|
|
161
168
|
for (const item of history) fs.writeSync(historyDescriptor, `${JSON.stringify(item)}\n`, null, "utf8");
|
|
162
|
-
}
|
|
163
|
-
fs.closeSync(historyDescriptor);
|
|
164
|
-
}
|
|
169
|
+
});
|
|
165
170
|
|
|
166
171
|
const companionPath = /\.html?$/i.test(outputHtmlPath)
|
|
167
172
|
? outputHtmlPath.replace(/\.html?$/i, ".json")
|
|
168
173
|
: `${outputHtmlPath}.json`;
|
|
169
|
-
|
|
170
|
-
|
|
174
|
+
writeFileAtomicSync(companionPath, `${JSON.stringify(record, null, 2)}\n`);
|
|
175
|
+
const transferFile = requestedTransferFile || createReceiptFile(record);
|
|
176
|
+
const transferPath = transferFilePathForOutput(outputHtmlPath);
|
|
177
|
+
writeFileAtomicSync(transferPath, transferFile.content);
|
|
178
|
+
return { dataDir, receiptPath, companionPath, transferPath };
|
|
171
179
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { buildCompensation, getWorkProfileCopy } from "./presentation.mjs";
|
|
2
|
+
|
|
3
|
+
function compactPresentation(record) {
|
|
4
|
+
const profileId = record.presentation.work_profile;
|
|
5
|
+
const scope = record.source?.scope || (record.presentation.compensation?.label === "本日工资" ? "today" : "latest");
|
|
6
|
+
const mobileProfile = profileId
|
|
7
|
+
? getWorkProfileCopy(profileId, "zh-CN")
|
|
8
|
+
: { title: record.presentation.work_title, review: record.presentation.review };
|
|
9
|
+
const mobileCompensation = profileId
|
|
10
|
+
? buildCompensation(scope, record.presentation.compensation?.amount, "zh-CN")
|
|
11
|
+
: record.presentation.compensation;
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
profileId,
|
|
15
|
+
scope,
|
|
16
|
+
mobileProfile,
|
|
17
|
+
mobileCompensation,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function compactBase(record) {
|
|
22
|
+
const presentation = compactPresentation(record);
|
|
23
|
+
return {
|
|
24
|
+
v: record.schema_version,
|
|
25
|
+
i: record.id,
|
|
26
|
+
g: record.generated_at,
|
|
27
|
+
o: presentation.scope,
|
|
28
|
+
d: [
|
|
29
|
+
record.period.start_at,
|
|
30
|
+
record.period.end_at,
|
|
31
|
+
record.period.timezone,
|
|
32
|
+
record.period.range_start_date || null,
|
|
33
|
+
record.period.range_end_date || null,
|
|
34
|
+
],
|
|
35
|
+
s: [
|
|
36
|
+
record.stats.session_count,
|
|
37
|
+
record.stats.completed_turns,
|
|
38
|
+
record.stats.user_messages,
|
|
39
|
+
record.stats.tool_calls,
|
|
40
|
+
record.stats.interruptions,
|
|
41
|
+
record.stats.work_duration_ms,
|
|
42
|
+
record.stats.average_first_token_ms,
|
|
43
|
+
],
|
|
44
|
+
t: [
|
|
45
|
+
record.stats.tokens.input_tokens,
|
|
46
|
+
record.stats.tokens.cached_input_tokens,
|
|
47
|
+
record.stats.tokens.output_tokens,
|
|
48
|
+
record.stats.tokens.reasoning_output_tokens,
|
|
49
|
+
record.stats.tokens.total_tokens,
|
|
50
|
+
],
|
|
51
|
+
m: record.stats.models,
|
|
52
|
+
l: record.locale || "zh-CN",
|
|
53
|
+
r: presentation.profileId || null,
|
|
54
|
+
p: [
|
|
55
|
+
record.presentation.default_theme,
|
|
56
|
+
presentation.mobileProfile.title,
|
|
57
|
+
presentation.mobileProfile.review,
|
|
58
|
+
presentation.mobileCompensation
|
|
59
|
+
? [
|
|
60
|
+
presentation.mobileCompensation.label,
|
|
61
|
+
presentation.mobileCompensation.amount,
|
|
62
|
+
presentation.mobileCompensation.unit,
|
|
63
|
+
presentation.mobileCompensation.note,
|
|
64
|
+
presentation.mobileCompensation.formula_version,
|
|
65
|
+
]
|
|
66
|
+
: null,
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function compactFact(fact) {
|
|
72
|
+
const stats = fact.stats;
|
|
73
|
+
return [
|
|
74
|
+
fact.fact_id,
|
|
75
|
+
fact.session_id,
|
|
76
|
+
fact.identity_quality,
|
|
77
|
+
fact.source_type,
|
|
78
|
+
fact.local_date,
|
|
79
|
+
fact.bucket_start_at,
|
|
80
|
+
fact.bucket_end_at,
|
|
81
|
+
fact.source_watermark_at,
|
|
82
|
+
fact.observed_at,
|
|
83
|
+
[
|
|
84
|
+
fact.source_revision.kind,
|
|
85
|
+
fact.source_revision.row_count,
|
|
86
|
+
fact.source_revision.byte_length,
|
|
87
|
+
fact.source_revision.tail_hash,
|
|
88
|
+
],
|
|
89
|
+
fact.content_hash,
|
|
90
|
+
[
|
|
91
|
+
stats.completed_turns,
|
|
92
|
+
stats.user_messages,
|
|
93
|
+
stats.tool_calls,
|
|
94
|
+
stats.interruptions,
|
|
95
|
+
stats.work_duration_ms,
|
|
96
|
+
stats.first_token_total_ms,
|
|
97
|
+
stats.first_token_sample_count,
|
|
98
|
+
stats.input_tokens,
|
|
99
|
+
stats.cached_input_tokens,
|
|
100
|
+
stats.output_tokens,
|
|
101
|
+
stats.reasoning_output_tokens,
|
|
102
|
+
stats.total_tokens,
|
|
103
|
+
stats.token_reset_count,
|
|
104
|
+
stats.models,
|
|
105
|
+
],
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function compactReceipt(record) {
|
|
110
|
+
const compact = compactBase(record);
|
|
111
|
+
if (record.schema_version !== 2) return compact;
|
|
112
|
+
|
|
113
|
+
const coverage = record.manifest.coverage;
|
|
114
|
+
return {
|
|
115
|
+
...compact,
|
|
116
|
+
k: record.source.logical_key,
|
|
117
|
+
h: record.source.snapshot_hash,
|
|
118
|
+
a: [
|
|
119
|
+
record.manifest.version,
|
|
120
|
+
record.manifest.fact_schema_version,
|
|
121
|
+
record.manifest.metric_schema_version,
|
|
122
|
+
record.manifest.accounting_timezone,
|
|
123
|
+
record.manifest.fact_count,
|
|
124
|
+
record.manifest.fact_ids,
|
|
125
|
+
[
|
|
126
|
+
coverage.kind,
|
|
127
|
+
coverage.scan_mode,
|
|
128
|
+
coverage.start_date || null,
|
|
129
|
+
coverage.end_date || null,
|
|
130
|
+
coverage.complete_through_date || null,
|
|
131
|
+
coverage.observed_through_at,
|
|
132
|
+
],
|
|
133
|
+
record.manifest.manifest_hash,
|
|
134
|
+
],
|
|
135
|
+
f: record.facts.map(compactFact),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
function stagingPathFor(filePath) {
|
|
5
|
+
return path.join(
|
|
6
|
+
path.dirname(filePath),
|
|
7
|
+
`.${path.basename(filePath)}.write-${process.pid}-${Date.now()}`,
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function replaceFile(stagingPath, filePath) {
|
|
12
|
+
try {
|
|
13
|
+
fs.renameSync(stagingPath, filePath);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
if (process.platform !== "win32" || !fs.existsSync(filePath)) throw error;
|
|
16
|
+
fs.rmSync(filePath, { force: true });
|
|
17
|
+
fs.renameSync(stagingPath, filePath);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function writeFileAtomicSync(filePath, content, encoding = "utf8") {
|
|
22
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
23
|
+
const stagingPath = stagingPathFor(filePath);
|
|
24
|
+
try {
|
|
25
|
+
fs.writeFileSync(stagingPath, content, encoding);
|
|
26
|
+
replaceFile(stagingPath, filePath);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
fs.rmSync(stagingPath, { force: true });
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function writeJsonAtomicSync(filePath, value) {
|
|
34
|
+
writeFileAtomicSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function writeWithAtomicFileSync(filePath, write) {
|
|
38
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
39
|
+
const stagingPath = stagingPathFor(filePath);
|
|
40
|
+
let descriptor = null;
|
|
41
|
+
try {
|
|
42
|
+
descriptor = fs.openSync(stagingPath, "w");
|
|
43
|
+
write(descriptor);
|
|
44
|
+
fs.closeSync(descriptor);
|
|
45
|
+
descriptor = null;
|
|
46
|
+
replaceFile(stagingPath, filePath);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (descriptor !== null) fs.closeSync(descriptor);
|
|
49
|
+
fs.rmSync(stagingPath, { force: true });
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/parsers/codex.mjs
CHANGED
|
@@ -220,8 +220,8 @@ function sessionFromFile(file) {
|
|
|
220
220
|
};
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
function codexSessionFiles() {
|
|
224
|
-
const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
223
|
+
function codexSessionFiles(requestedCodexHome = null) {
|
|
224
|
+
const codexHome = requestedCodexHome || process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
225
225
|
const sessionsDirectory = path.join(codexHome, "sessions");
|
|
226
226
|
const files = walkJsonlFiles(sessionsDirectory)
|
|
227
227
|
.map((filePath) => {
|
|
@@ -275,8 +275,8 @@ export function deduplicateCodexSessions(sessions) {
|
|
|
275
275
|
return [...selected.values()];
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
-
export function loadCodexSessions(range) {
|
|
279
|
-
const files = codexSessionFiles();
|
|
278
|
+
export function loadCodexSessions(range, { codexHome = null } = {}) {
|
|
279
|
+
const files = codexSessionFiles(codexHome);
|
|
280
280
|
let candidates = files;
|
|
281
281
|
let scanMode = "none";
|
|
282
282
|
|
|
@@ -310,8 +310,8 @@ export function loadCodexSessions(range) {
|
|
|
310
310
|
return sessions;
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
-
export function listRecentCodexSessions(limit = 10) {
|
|
314
|
-
const sessions = deduplicateCodexSessions(codexSessionFiles()
|
|
313
|
+
export function listRecentCodexSessions(limit = 10, { codexHome = null } = {}) {
|
|
314
|
+
const sessions = deduplicateCodexSessions(codexSessionFiles(codexHome)
|
|
315
315
|
.slice(0, Math.max(40, limit * 3))
|
|
316
316
|
.map(sessionFromFile)
|
|
317
317
|
.filter((session) => session.rows.length))
|