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/renderers/html.mjs
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
|
|
4
|
+
import { buildCode128B } from "../core/barcode.mjs";
|
|
4
5
|
import { formatDate, formatDuration, formatNumber, formatTime } from "../lib/time.mjs";
|
|
5
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
buildCompensation,
|
|
8
|
+
DEFAULT_LOCALE,
|
|
9
|
+
getReceiptCopy,
|
|
10
|
+
getScopeLabel,
|
|
11
|
+
} from "../core/presentation.mjs";
|
|
6
12
|
|
|
7
13
|
const require = createRequire(import.meta.url);
|
|
8
14
|
const DOM_TO_IMAGE_SOURCE = fs.readFileSync(require.resolve("dom-to-image-more"), "utf8");
|
|
@@ -24,6 +30,16 @@ function receiptRow(label, value, emphasize = false) {
|
|
|
24
30
|
return `<div class="receipt-row${emphasize ? " receipt-row--emphasize" : ""}"><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong></div>`;
|
|
25
31
|
}
|
|
26
32
|
|
|
33
|
+
function receiptBarcode(value) {
|
|
34
|
+
const barcode = buildCode128B(value);
|
|
35
|
+
const segments = barcode.segments.map((segment) => {
|
|
36
|
+
const kind = segment.isBar ? "bar" : "space";
|
|
37
|
+
return `<span class="barcode-segment barcode-segment--${kind}" style="flex-grow:${segment.width}"></span>`;
|
|
38
|
+
}).join("");
|
|
39
|
+
|
|
40
|
+
return `<div class="barcode" data-barcode-value="${escapeHtml(barcode.value)}" aria-hidden="true">${segments}</div>`;
|
|
41
|
+
}
|
|
42
|
+
|
|
27
43
|
function formatCount(value, units, locale) {
|
|
28
44
|
const amount = Math.max(0, Math.round(value || 0));
|
|
29
45
|
const unit = units[amount === 1 ? 0 : 1];
|
|
@@ -36,6 +52,13 @@ function formatDateKey(value, locale) {
|
|
|
36
52
|
return locale === "en" ? `${match[2]}/${match[3]}/${match[1]}` : `${match[1]}/${match[2]}/${match[3]}`;
|
|
37
53
|
}
|
|
38
54
|
|
|
55
|
+
function formatCopy(template, values) {
|
|
56
|
+
return Object.entries(values).reduce(
|
|
57
|
+
(result, [key, value]) => result.replaceAll(`{${key}}`, String(value)),
|
|
58
|
+
String(template),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
39
62
|
export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null, miniProgramCodeDataUrl = null }) {
|
|
40
63
|
const locale = record.locale || DEFAULT_LOCALE;
|
|
41
64
|
const copy = getReceiptCopy(locale);
|
|
@@ -47,7 +70,7 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
47
70
|
const rangeEndDate = record.period.range_end_date || formatDateKey(record.period.end_at.slice(0, 10), "zh-CN").replaceAll("/", "-");
|
|
48
71
|
const isCalendarScope = new Set(["today", "last-7-days", "this-week"]).has(record.source.scope);
|
|
49
72
|
const spansMultipleDates = rangeStartDate !== rangeEndDate;
|
|
50
|
-
const scopeLabel =
|
|
73
|
+
const scopeLabel = getScopeLabel(record.source.scope, locale, record.source.hours);
|
|
51
74
|
const dateRange = spansMultipleDates
|
|
52
75
|
? `${formatDateKey(rangeStartDate, locale)}—${formatDateKey(rangeEndDate, locale)}`
|
|
53
76
|
: formatDateKey(rangeEndDate, locale);
|
|
@@ -56,7 +79,8 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
56
79
|
: `${formatTime(startAt, timezone, locale)}—${formatTime(endAt, timezone, locale)}`;
|
|
57
80
|
const businessPeriodLabel = isCalendarScope ? copy.meta.period : copy.meta.hours;
|
|
58
81
|
const modelLabel = record.stats.models.length ? record.stats.models.join(" / ") : copy.modelMissing;
|
|
59
|
-
const
|
|
82
|
+
const shortReceiptId = record.id.replace(/^cwr2?_/, "").slice(0, 8).toUpperCase() || "UNKNOWN";
|
|
83
|
+
const receiptNumber = `${shortReceiptId}-${String(record.stats.completed_turns).padStart(3, "0")}`;
|
|
60
84
|
const compensation = record.presentation.compensation || buildCompensation(record.source.scope, 0, locale);
|
|
61
85
|
const responseSeconds = new Intl.NumberFormat(locale === "en" ? "en-US" : "zh-CN", {
|
|
62
86
|
minimumFractionDigits: 1,
|
|
@@ -91,11 +115,56 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
91
115
|
? [dataQrDataUrl]
|
|
92
116
|
: [];
|
|
93
117
|
const dataQrItems = qrUrls.map((url, index) => `
|
|
94
|
-
<div class="qr-item">
|
|
118
|
+
<div class="qr-item" data-data-qr-index="${index}">
|
|
95
119
|
<div class="qr-frame"><img src="${url}" alt="${escapeHtml(copy.dataQrAlt)} ${index + 1}/${qrUrls.length}"></div>
|
|
96
120
|
<strong>${escapeHtml(copy.importData)}${qrUrls.length > 1 ? ` ${index + 1}/${qrUrls.length}` : ""}</strong>
|
|
97
121
|
<span>${escapeHtml(copy.importDataHint)}</span>
|
|
98
122
|
</div>`).join("");
|
|
123
|
+
const isMultipart = qrUrls.length > 1;
|
|
124
|
+
const multipartSetupSeconds = 10;
|
|
125
|
+
const multipartConfig = JSON.stringify({
|
|
126
|
+
enabled: isMultipart,
|
|
127
|
+
setupSeconds: multipartSetupSeconds,
|
|
128
|
+
frameMs: 4000,
|
|
129
|
+
blankMs: 240,
|
|
130
|
+
setupHint: copy.multipartOpenHint,
|
|
131
|
+
partLabel: copy.multipartPartLabel,
|
|
132
|
+
}).replaceAll("<", "\\u003c");
|
|
133
|
+
const transferVisual = isMultipart
|
|
134
|
+
? `
|
|
135
|
+
<div class="multipart-live" id="multipart-live">
|
|
136
|
+
<div class="multipart-panel multipart-setup" id="multipart-setup">
|
|
137
|
+
<div class="qr-item multipart-setup__item">
|
|
138
|
+
<div class="qr-frame qr-frame--large">${miniProgramVisual}</div>
|
|
139
|
+
<strong>${escapeHtml(copy.multipartOpenTitle)}</strong>
|
|
140
|
+
<span id="multipart-setup-hint">${escapeHtml(formatCopy(copy.multipartOpenHint, { seconds: multipartSetupSeconds }))}</span>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
<div class="multipart-panel multipart-stage" id="multipart-stage" hidden>
|
|
144
|
+
<div class="qr-frame qr-frame--large"><img id="multipart-active-qr" alt="${escapeHtml(copy.dataQrAlt)}"></div>
|
|
145
|
+
<strong class="multipart-stage__title">${escapeHtml(copy.multipartTransferTitle)}</strong>
|
|
146
|
+
<span class="multipart-stage__part" id="multipart-part-label">${escapeHtml(formatCopy(copy.multipartPartLabel, { current: 1, total: qrUrls.length }))}</span>
|
|
147
|
+
<span class="multipart-stage__hint">${escapeHtml(copy.multipartTransferHint)}</span>
|
|
148
|
+
<button class="multipart-secondary" id="multipart-show-mini" type="button">${escapeHtml(copy.multipartRestart)}</button>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="qr-grid qr-grid--export-only" hidden>
|
|
152
|
+
<div class="qr-item">
|
|
153
|
+
<div class="qr-frame">${miniProgramVisual}</div>
|
|
154
|
+
<strong>${escapeHtml(copy.openMiniProgram)}</strong>
|
|
155
|
+
<span>${escapeHtml(copy.openMiniProgramHint)}</span>
|
|
156
|
+
</div>
|
|
157
|
+
${dataQrItems}
|
|
158
|
+
</div>`
|
|
159
|
+
: `
|
|
160
|
+
<div class="qr-grid qr-grid--single">
|
|
161
|
+
<div class="qr-item">
|
|
162
|
+
<div class="qr-frame">${miniProgramVisual}</div>
|
|
163
|
+
<strong>${escapeHtml(copy.openMiniProgram)}</strong>
|
|
164
|
+
<span>${escapeHtml(copy.openMiniProgramHint)}</span>
|
|
165
|
+
</div>
|
|
166
|
+
${dataQrItems}
|
|
167
|
+
</div>`;
|
|
99
168
|
|
|
100
169
|
return `<!doctype html>
|
|
101
170
|
<html lang="${escapeHtml(copy.htmlLang)}" data-theme="${escapeHtml(record.presentation.default_theme)}">
|
|
@@ -313,11 +382,28 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
313
382
|
color: var(--muted);
|
|
314
383
|
}
|
|
315
384
|
.barcode {
|
|
385
|
+
display: flex;
|
|
386
|
+
align-items: stretch;
|
|
387
|
+
box-sizing: border-box;
|
|
388
|
+
width: 100%;
|
|
316
389
|
height: 48px;
|
|
317
390
|
margin: 18px auto 8px;
|
|
318
|
-
max-width:
|
|
319
|
-
|
|
320
|
-
|
|
391
|
+
max-width: 320px;
|
|
392
|
+
padding: 0 20px;
|
|
393
|
+
overflow: hidden;
|
|
394
|
+
}
|
|
395
|
+
.barcode-segment {
|
|
396
|
+
display: block;
|
|
397
|
+
flex-basis: 0;
|
|
398
|
+
flex-shrink: 0;
|
|
399
|
+
min-width: 0;
|
|
400
|
+
height: 100%;
|
|
401
|
+
}
|
|
402
|
+
.barcode-segment--bar {
|
|
403
|
+
background: var(--ink);
|
|
404
|
+
}
|
|
405
|
+
.barcode-segment--space {
|
|
406
|
+
background: transparent;
|
|
321
407
|
}
|
|
322
408
|
.footer {
|
|
323
409
|
text-align: center;
|
|
@@ -352,6 +438,39 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
352
438
|
.qr-frame img { display: block; width: 100%; height: 100%; object-fit: contain; }
|
|
353
439
|
.qr-item strong { display: block; font-size: 12px; }
|
|
354
440
|
.qr-item span { display: block; margin-top: 4px; color: var(--muted); font-size: 10px; line-height: 1.45; }
|
|
441
|
+
.qr-grid--export-only { display: none; }
|
|
442
|
+
.multipart-live { margin-top: 20px; }
|
|
443
|
+
.multipart-panel {
|
|
444
|
+
display: grid;
|
|
445
|
+
place-items: center;
|
|
446
|
+
min-height: 310px;
|
|
447
|
+
padding: 18px;
|
|
448
|
+
border: 1px solid var(--line);
|
|
449
|
+
background: color-mix(in srgb, var(--paper) 94%, var(--ink));
|
|
450
|
+
text-align: center;
|
|
451
|
+
}
|
|
452
|
+
.multipart-panel[hidden] { display: none; }
|
|
453
|
+
.multipart-setup__item { width: 100%; }
|
|
454
|
+
.qr-frame--large { width: min(100%, 250px); }
|
|
455
|
+
.multipart-stage__title,
|
|
456
|
+
.multipart-stage__part,
|
|
457
|
+
.multipart-stage__hint { display: block; }
|
|
458
|
+
.multipart-stage__title { margin-top: 4px; font-size: 14px; }
|
|
459
|
+
.multipart-stage__part { margin-top: 8px; font-size: 12px; font-weight: 800; letter-spacing: .06em; }
|
|
460
|
+
.multipart-stage__hint { max-width: 360px; margin-top: 6px; color: var(--muted); font-size: 10px; line-height: 1.5; }
|
|
461
|
+
#multipart-active-qr[aria-busy="true"] { visibility: hidden; }
|
|
462
|
+
.multipart-secondary {
|
|
463
|
+
margin-top: 14px;
|
|
464
|
+
padding: 7px 12px;
|
|
465
|
+
border: 1px solid var(--line);
|
|
466
|
+
border-radius: 999px;
|
|
467
|
+
color: var(--ink);
|
|
468
|
+
background: transparent;
|
|
469
|
+
font: inherit;
|
|
470
|
+
font-size: 10px;
|
|
471
|
+
cursor: pointer;
|
|
472
|
+
}
|
|
473
|
+
.multipart-secondary:hover { background: color-mix(in srgb, var(--ink) 7%, transparent); }
|
|
355
474
|
.mini-placeholder {
|
|
356
475
|
display: grid;
|
|
357
476
|
place-content: center;
|
|
@@ -436,7 +555,7 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
436
555
|
<strong>${escapeHtml(formatNumber(compensation.amount, locale))}<small>${escapeHtml(compensation.unit)}</small></strong>
|
|
437
556
|
</div>
|
|
438
557
|
</section>
|
|
439
|
-
|
|
558
|
+
${receiptBarcode(receiptNumber)}
|
|
440
559
|
<footer class="footer">
|
|
441
560
|
<div>MODEL · ${escapeHtml(modelLabel)}</div>
|
|
442
561
|
<div>${escapeHtml(copy.footerThanks)}</div>
|
|
@@ -448,14 +567,7 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
448
567
|
<h2>${escapeHtml(copy.transferTitle)}</h2>
|
|
449
568
|
<p>${escapeHtml(copy.transferDescription)}</p>
|
|
450
569
|
</header>
|
|
451
|
-
|
|
452
|
-
<div class="qr-item">
|
|
453
|
-
<div class="qr-frame">${miniProgramVisual}</div>
|
|
454
|
-
<strong>${escapeHtml(copy.openMiniProgram)}</strong>
|
|
455
|
-
<span>${escapeHtml(copy.openMiniProgramHint)}</span>
|
|
456
|
-
</div>
|
|
457
|
-
${dataQrItems}
|
|
458
|
-
</div>
|
|
570
|
+
${transferVisual}
|
|
459
571
|
<p class="transfer-note">${escapeHtml(copy.transferNote)}</p>
|
|
460
572
|
</section>
|
|
461
573
|
</div>
|
|
@@ -465,6 +577,7 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
465
577
|
<script>${inlineScript(DOM_TO_IMAGE_SOURCE)}</script>
|
|
466
578
|
<script>
|
|
467
579
|
const exportConfig = ${exportConfig};
|
|
580
|
+
const multipartConfig = ${multipartConfig};
|
|
468
581
|
const themes = new Set(["classic", "diner", "payroll"]);
|
|
469
582
|
const buttons = [...document.querySelectorAll("[data-theme-value]")];
|
|
470
583
|
function applyTheme(theme) {
|
|
@@ -478,6 +591,88 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
478
591
|
try { savedTheme = localStorage.getItem("codex-work-receipt-theme"); } catch {}
|
|
479
592
|
applyTheme(savedTheme || document.documentElement.dataset.theme);
|
|
480
593
|
|
|
594
|
+
const multipartLive = document.getElementById("multipart-live");
|
|
595
|
+
if (multipartConfig.enabled && multipartLive) {
|
|
596
|
+
const setup = document.getElementById("multipart-setup");
|
|
597
|
+
const setupHint = document.getElementById("multipart-setup-hint");
|
|
598
|
+
const stage = document.getElementById("multipart-stage");
|
|
599
|
+
const activeQr = document.getElementById("multipart-active-qr");
|
|
600
|
+
const partLabel = document.getElementById("multipart-part-label");
|
|
601
|
+
const showMiniButton = document.getElementById("multipart-show-mini");
|
|
602
|
+
const urls = [...document.querySelectorAll(".qr-grid--export-only [data-data-qr-index] img")]
|
|
603
|
+
.map((image) => image.src)
|
|
604
|
+
.filter(Boolean);
|
|
605
|
+
let setupTimer = null;
|
|
606
|
+
let rotationTimer = null;
|
|
607
|
+
let switchTimer = null;
|
|
608
|
+
let activeIndex = 0;
|
|
609
|
+
|
|
610
|
+
function stopMultipartTimers() {
|
|
611
|
+
if (setupTimer !== null) clearInterval(setupTimer);
|
|
612
|
+
if (rotationTimer !== null) clearInterval(rotationTimer);
|
|
613
|
+
if (switchTimer !== null) clearTimeout(switchTimer);
|
|
614
|
+
setupTimer = null;
|
|
615
|
+
rotationTimer = null;
|
|
616
|
+
switchTimer = null;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function setupHintText(seconds) {
|
|
620
|
+
return String(multipartConfig.setupHint).replaceAll("{seconds}", String(seconds));
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function partLabelText(index) {
|
|
624
|
+
return String(multipartConfig.partLabel)
|
|
625
|
+
.replaceAll("{current}", String(index + 1))
|
|
626
|
+
.replaceAll("{total}", String(urls.length));
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function showPart(index, immediate = false) {
|
|
630
|
+
if (!urls.length) return;
|
|
631
|
+
activeIndex = (index + urls.length) % urls.length;
|
|
632
|
+
const applyPart = () => {
|
|
633
|
+
activeQr.src = urls[activeIndex];
|
|
634
|
+
activeQr.alt = partLabelText(activeIndex);
|
|
635
|
+
activeQr.setAttribute("aria-busy", "false");
|
|
636
|
+
partLabel.textContent = partLabelText(activeIndex);
|
|
637
|
+
switchTimer = null;
|
|
638
|
+
};
|
|
639
|
+
if (immediate) {
|
|
640
|
+
applyPart();
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
activeQr.setAttribute("aria-busy", "true");
|
|
644
|
+
switchTimer = setTimeout(applyPart, multipartConfig.blankMs);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function startMultipartTransfer() {
|
|
648
|
+
stopMultipartTimers();
|
|
649
|
+
setup.hidden = true;
|
|
650
|
+
stage.hidden = false;
|
|
651
|
+
showPart(0, true);
|
|
652
|
+
rotationTimer = setInterval(() => showPart(activeIndex + 1), multipartConfig.frameMs);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function showMultipartSetup() {
|
|
656
|
+
stopMultipartTimers();
|
|
657
|
+
stage.hidden = true;
|
|
658
|
+
setup.hidden = false;
|
|
659
|
+
let remaining = multipartConfig.setupSeconds;
|
|
660
|
+
setupHint.textContent = setupHintText(remaining);
|
|
661
|
+
setupTimer = setInterval(() => {
|
|
662
|
+
remaining -= 1;
|
|
663
|
+
if (remaining <= 0) {
|
|
664
|
+
startMultipartTransfer();
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
setupHint.textContent = setupHintText(remaining);
|
|
668
|
+
}, 1000);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
showMiniButton.addEventListener("click", showMultipartSetup);
|
|
672
|
+
window.addEventListener("beforeunload", stopMultipartTimers);
|
|
673
|
+
showMultipartSetup();
|
|
674
|
+
}
|
|
675
|
+
|
|
481
676
|
const exportButton = document.getElementById("save-receipt-image");
|
|
482
677
|
const exportStatus = document.getElementById("export-status");
|
|
483
678
|
const exportNode = document.getElementById("receipt-export");
|
|
@@ -488,7 +683,7 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
488
683
|
}
|
|
489
684
|
|
|
490
685
|
function waitForImages(node) {
|
|
491
|
-
return Promise.all([...node.querySelectorAll("img")].map((image) => {
|
|
686
|
+
return Promise.all([...node.querySelectorAll("img")].filter((image) => image.getAttribute("src")).map((image) => {
|
|
492
687
|
if (image.complete && image.naturalWidth > 0) return Promise.resolve();
|
|
493
688
|
return new Promise((resolve, reject) => {
|
|
494
689
|
image.addEventListener("load", resolve, { once: true });
|
|
@@ -531,6 +726,13 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
531
726
|
style: { background: paperColor },
|
|
532
727
|
onclone(clone) {
|
|
533
728
|
clone.style.background = paperColor;
|
|
729
|
+
const multipartLiveClone = clone.querySelector(".multipart-live");
|
|
730
|
+
if (multipartLiveClone) multipartLiveClone.remove();
|
|
731
|
+
const exportGridClone = clone.querySelector(".qr-grid--export-only");
|
|
732
|
+
if (exportGridClone) {
|
|
733
|
+
exportGridClone.removeAttribute("hidden");
|
|
734
|
+
exportGridClone.style.display = "grid";
|
|
735
|
+
}
|
|
534
736
|
clone.querySelectorAll(".paper").forEach((paper) => {
|
|
535
737
|
paper.style.boxShadow = "none";
|
|
536
738
|
});
|