codeksei 0.1.0 → 0.1.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/LICENSE +661 -661
- package/README.en.md +109 -47
- package/README.md +79 -58
- package/bin/cyberboss.js +1 -1
- package/package.json +86 -86
- package/scripts/open_shared_wechat_thread.sh +77 -77
- package/scripts/open_wechat_thread.sh +108 -108
- package/scripts/shared-common.js +144 -144
- package/scripts/shared-open.js +14 -14
- package/scripts/shared-start.js +5 -5
- package/scripts/shared-status.js +27 -27
- package/scripts/show_shared_status.sh +45 -45
- package/scripts/start_shared_app_server.sh +52 -52
- package/scripts/start_shared_wechat.sh +94 -94
- package/scripts/timeline-screenshot.sh +14 -14
- package/src/adapters/channel/weixin/account-store.js +99 -99
- package/src/adapters/channel/weixin/api-v2.js +50 -50
- package/src/adapters/channel/weixin/api.js +169 -169
- package/src/adapters/channel/weixin/context-token-store.js +84 -84
- package/src/adapters/channel/weixin/index.js +618 -604
- package/src/adapters/channel/weixin/legacy.js +579 -566
- package/src/adapters/channel/weixin/media-mime.js +22 -22
- package/src/adapters/channel/weixin/media-receive.js +370 -370
- package/src/adapters/channel/weixin/media-send.js +102 -102
- package/src/adapters/channel/weixin/message-utils-v2.js +282 -282
- package/src/adapters/channel/weixin/message-utils.js +199 -199
- package/src/adapters/channel/weixin/redact.js +41 -41
- package/src/adapters/channel/weixin/reminder-queue-store.js +101 -101
- package/src/adapters/channel/weixin/sync-buffer-store.js +35 -35
- package/src/adapters/runtime/codex/events.js +215 -215
- package/src/adapters/runtime/codex/index.js +109 -104
- package/src/adapters/runtime/codex/message-utils.js +95 -95
- package/src/adapters/runtime/codex/model-catalog.js +106 -106
- package/src/adapters/runtime/codex/protocol-leak-monitor.js +75 -75
- package/src/adapters/runtime/codex/rpc-client.js +339 -339
- package/src/adapters/runtime/codex/session-store.js +286 -286
- package/src/app/channel-send-file-cli.js +57 -57
- package/src/app/diary-write-cli.js +236 -88
- package/src/app/note-sync-cli.js +2 -2
- package/src/app/reminder-write-cli.js +215 -210
- package/src/app/review-cli.js +7 -5
- package/src/app/system-checkin-poller.js +64 -64
- package/src/app/system-send-cli.js +129 -129
- package/src/app/timeline-event-cli.js +28 -25
- package/src/app/timeline-screenshot-cli.js +103 -100
- package/src/core/app.js +1763 -1763
- package/src/core/branding.js +2 -1
- package/src/core/command-registry.js +381 -369
- package/src/core/config.js +30 -14
- package/src/core/default-targets.js +163 -163
- package/src/core/durable-note-schema.js +9 -8
- package/src/core/instructions-template.js +17 -16
- package/src/core/note-sync.js +8 -7
- package/src/core/path-utils.js +54 -0
- package/src/core/project-radar.js +11 -10
- package/src/core/review.js +48 -50
- package/src/core/stream-delivery.js +1162 -983
- package/src/core/system-message-dispatcher.js +68 -68
- package/src/core/system-message-queue-store.js +128 -128
- package/src/core/thread-state-store.js +96 -96
- package/src/core/timeline-screenshot-queue-store.js +134 -134
- package/src/core/timezone.js +436 -0
- package/src/core/workspace-bootstrap.js +9 -1
- package/src/index.js +148 -146
- package/src/integrations/timeline/index.js +130 -74
- package/src/integrations/timeline/state-sync.js +240 -0
- package/templates/weixin-instructions.md +12 -38
- package/templates/weixin-operations.md +29 -31
|
@@ -1,134 +1,134 @@
|
|
|
1
|
-
const fs = require("fs");
|
|
2
|
-
const path = require("path");
|
|
3
|
-
|
|
4
|
-
class TimelineScreenshotQueueStore {
|
|
5
|
-
constructor({ filePath }) {
|
|
6
|
-
this.filePath = filePath;
|
|
7
|
-
this.state = { jobs: [] };
|
|
8
|
-
this.ensureParentDirectory();
|
|
9
|
-
this.load();
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
ensureParentDirectory() {
|
|
13
|
-
fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
load() {
|
|
17
|
-
try {
|
|
18
|
-
const raw = fs.readFileSync(this.filePath, "utf8");
|
|
19
|
-
const parsed = JSON.parse(raw);
|
|
20
|
-
const jobs = Array.isArray(parsed?.jobs) ? parsed.jobs : [];
|
|
21
|
-
this.state = {
|
|
22
|
-
jobs: jobs
|
|
23
|
-
.map(normalizeTimelineScreenshotJob)
|
|
24
|
-
.filter(Boolean)
|
|
25
|
-
.sort(compareTimelineScreenshotJobs),
|
|
26
|
-
};
|
|
27
|
-
} catch {
|
|
28
|
-
this.state = { jobs: [] };
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
save() {
|
|
33
|
-
fs.writeFileSync(this.filePath, JSON.stringify(this.state, null, 2));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
enqueue(job) {
|
|
37
|
-
this.load();
|
|
38
|
-
const normalized = normalizeTimelineScreenshotJob(job);
|
|
39
|
-
if (!normalized) {
|
|
40
|
-
throw new Error("invalid timeline screenshot job");
|
|
41
|
-
}
|
|
42
|
-
this.state.jobs.push(normalized);
|
|
43
|
-
this.state.jobs.sort(compareTimelineScreenshotJobs);
|
|
44
|
-
this.save();
|
|
45
|
-
return normalized;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
drainForAccount(accountId) {
|
|
49
|
-
this.load();
|
|
50
|
-
const normalizedAccountId = normalizeText(accountId);
|
|
51
|
-
const drained = [];
|
|
52
|
-
const pending = [];
|
|
53
|
-
|
|
54
|
-
for (const job of this.state.jobs) {
|
|
55
|
-
if (job.accountId === normalizedAccountId) {
|
|
56
|
-
drained.push(job);
|
|
57
|
-
} else {
|
|
58
|
-
pending.push(job);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (drained.length) {
|
|
63
|
-
this.state.jobs = pending;
|
|
64
|
-
this.save();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return drained;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
hasPendingForAccount(accountId) {
|
|
71
|
-
this.load();
|
|
72
|
-
const normalizedAccountId = normalizeText(accountId);
|
|
73
|
-
return this.state.jobs.some((job) => job.accountId === normalizedAccountId);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function normalizeTimelineScreenshotJob(job) {
|
|
78
|
-
if (!job || typeof job !== "object") {
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const id = normalizeText(job.id);
|
|
83
|
-
const accountId = normalizeText(job.accountId);
|
|
84
|
-
const senderId = normalizeText(job.senderId);
|
|
85
|
-
const outputFile = normalizeText(job.outputFile);
|
|
86
|
-
const createdAt = normalizeIsoTime(job.createdAt);
|
|
87
|
-
const args = normalizeArgs(job.args);
|
|
88
|
-
|
|
89
|
-
if (!id || !accountId || !senderId) {
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return {
|
|
94
|
-
id,
|
|
95
|
-
accountId,
|
|
96
|
-
senderId,
|
|
97
|
-
outputFile,
|
|
98
|
-
args,
|
|
99
|
-
createdAt: createdAt || new Date().toISOString(),
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function normalizeArgs(args) {
|
|
104
|
-
return Array.isArray(args)
|
|
105
|
-
? args.map((value) => normalizeText(value)).filter(Boolean)
|
|
106
|
-
: [];
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function normalizeIsoTime(value) {
|
|
110
|
-
const normalized = normalizeText(value);
|
|
111
|
-
if (!normalized) {
|
|
112
|
-
return "";
|
|
113
|
-
}
|
|
114
|
-
const parsed = Date.parse(normalized);
|
|
115
|
-
if (!Number.isFinite(parsed)) {
|
|
116
|
-
return "";
|
|
117
|
-
}
|
|
118
|
-
return new Date(parsed).toISOString();
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function compareTimelineScreenshotJobs(left, right) {
|
|
122
|
-
const leftTime = Date.parse(left?.createdAt || "") || 0;
|
|
123
|
-
const rightTime = Date.parse(right?.createdAt || "") || 0;
|
|
124
|
-
if (leftTime !== rightTime) {
|
|
125
|
-
return leftTime - rightTime;
|
|
126
|
-
}
|
|
127
|
-
return String(left?.id || "").localeCompare(String(right?.id || ""));
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function normalizeText(value) {
|
|
131
|
-
return typeof value === "string" ? value.trim() : "";
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
module.exports = { TimelineScreenshotQueueStore };
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
class TimelineScreenshotQueueStore {
|
|
5
|
+
constructor({ filePath }) {
|
|
6
|
+
this.filePath = filePath;
|
|
7
|
+
this.state = { jobs: [] };
|
|
8
|
+
this.ensureParentDirectory();
|
|
9
|
+
this.load();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
ensureParentDirectory() {
|
|
13
|
+
fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
load() {
|
|
17
|
+
try {
|
|
18
|
+
const raw = fs.readFileSync(this.filePath, "utf8");
|
|
19
|
+
const parsed = JSON.parse(raw);
|
|
20
|
+
const jobs = Array.isArray(parsed?.jobs) ? parsed.jobs : [];
|
|
21
|
+
this.state = {
|
|
22
|
+
jobs: jobs
|
|
23
|
+
.map(normalizeTimelineScreenshotJob)
|
|
24
|
+
.filter(Boolean)
|
|
25
|
+
.sort(compareTimelineScreenshotJobs),
|
|
26
|
+
};
|
|
27
|
+
} catch {
|
|
28
|
+
this.state = { jobs: [] };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
save() {
|
|
33
|
+
fs.writeFileSync(this.filePath, JSON.stringify(this.state, null, 2));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
enqueue(job) {
|
|
37
|
+
this.load();
|
|
38
|
+
const normalized = normalizeTimelineScreenshotJob(job);
|
|
39
|
+
if (!normalized) {
|
|
40
|
+
throw new Error("invalid timeline screenshot job");
|
|
41
|
+
}
|
|
42
|
+
this.state.jobs.push(normalized);
|
|
43
|
+
this.state.jobs.sort(compareTimelineScreenshotJobs);
|
|
44
|
+
this.save();
|
|
45
|
+
return normalized;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
drainForAccount(accountId) {
|
|
49
|
+
this.load();
|
|
50
|
+
const normalizedAccountId = normalizeText(accountId);
|
|
51
|
+
const drained = [];
|
|
52
|
+
const pending = [];
|
|
53
|
+
|
|
54
|
+
for (const job of this.state.jobs) {
|
|
55
|
+
if (job.accountId === normalizedAccountId) {
|
|
56
|
+
drained.push(job);
|
|
57
|
+
} else {
|
|
58
|
+
pending.push(job);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (drained.length) {
|
|
63
|
+
this.state.jobs = pending;
|
|
64
|
+
this.save();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return drained;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
hasPendingForAccount(accountId) {
|
|
71
|
+
this.load();
|
|
72
|
+
const normalizedAccountId = normalizeText(accountId);
|
|
73
|
+
return this.state.jobs.some((job) => job.accountId === normalizedAccountId);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizeTimelineScreenshotJob(job) {
|
|
78
|
+
if (!job || typeof job !== "object") {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const id = normalizeText(job.id);
|
|
83
|
+
const accountId = normalizeText(job.accountId);
|
|
84
|
+
const senderId = normalizeText(job.senderId);
|
|
85
|
+
const outputFile = normalizeText(job.outputFile);
|
|
86
|
+
const createdAt = normalizeIsoTime(job.createdAt);
|
|
87
|
+
const args = normalizeArgs(job.args);
|
|
88
|
+
|
|
89
|
+
if (!id || !accountId || !senderId) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
id,
|
|
95
|
+
accountId,
|
|
96
|
+
senderId,
|
|
97
|
+
outputFile,
|
|
98
|
+
args,
|
|
99
|
+
createdAt: createdAt || new Date().toISOString(),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizeArgs(args) {
|
|
104
|
+
return Array.isArray(args)
|
|
105
|
+
? args.map((value) => normalizeText(value)).filter(Boolean)
|
|
106
|
+
: [];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function normalizeIsoTime(value) {
|
|
110
|
+
const normalized = normalizeText(value);
|
|
111
|
+
if (!normalized) {
|
|
112
|
+
return "";
|
|
113
|
+
}
|
|
114
|
+
const parsed = Date.parse(normalized);
|
|
115
|
+
if (!Number.isFinite(parsed)) {
|
|
116
|
+
return "";
|
|
117
|
+
}
|
|
118
|
+
return new Date(parsed).toISOString();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function compareTimelineScreenshotJobs(left, right) {
|
|
122
|
+
const leftTime = Date.parse(left?.createdAt || "") || 0;
|
|
123
|
+
const rightTime = Date.parse(right?.createdAt || "") || 0;
|
|
124
|
+
if (leftTime !== rightTime) {
|
|
125
|
+
return leftTime - rightTime;
|
|
126
|
+
}
|
|
127
|
+
return String(left?.id || "").localeCompare(String(right?.id || ""));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizeText(value) {
|
|
131
|
+
return typeof value === "string" ? value.trim() : "";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = { TimelineScreenshotQueueStore };
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const LEGACY_TIMELINE_TIMEZONE = "Asia/Shanghai";
|
|
5
|
+
const DEFAULT_FALLBACK_TIMEZONE = "UTC";
|
|
6
|
+
|
|
7
|
+
function normalizeText(value) {
|
|
8
|
+
return typeof value === "string" ? value.trim() : "";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizeTimezone(value) {
|
|
12
|
+
const raw = normalizeText(value);
|
|
13
|
+
if (!raw) {
|
|
14
|
+
return "";
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
new Intl.DateTimeFormat("en-US", { timeZone: raw }).format(new Date());
|
|
18
|
+
return raw;
|
|
19
|
+
} catch {
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function resolveSystemTimezone() {
|
|
25
|
+
return normalizeTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveTimezoneConfig({ explicitTimezone = "", timelineStateDir = "" } = {}) {
|
|
29
|
+
const explicit = normalizeTimezone(explicitTimezone);
|
|
30
|
+
const timelineStateTimezone = readTimelineStateTimezone(timelineStateDir);
|
|
31
|
+
if (explicit) {
|
|
32
|
+
return {
|
|
33
|
+
timezone: explicit,
|
|
34
|
+
source: "env",
|
|
35
|
+
explicit: true,
|
|
36
|
+
timelineStateTimezone,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Preserve an explicitly established timeline timezone when it is not just
|
|
41
|
+
// the old hard-coded default. This keeps diary/review/reminder aligned with
|
|
42
|
+
// existing timeline business data instead of silently drifting per machine.
|
|
43
|
+
if (timelineStateTimezone && timelineStateTimezone !== LEGACY_TIMELINE_TIMEZONE) {
|
|
44
|
+
return {
|
|
45
|
+
timezone: timelineStateTimezone,
|
|
46
|
+
source: "timeline_state",
|
|
47
|
+
explicit: false,
|
|
48
|
+
timelineStateTimezone,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const systemTimezone = resolveSystemTimezone();
|
|
53
|
+
if (systemTimezone) {
|
|
54
|
+
return {
|
|
55
|
+
timezone: systemTimezone,
|
|
56
|
+
source: "system",
|
|
57
|
+
explicit: false,
|
|
58
|
+
timelineStateTimezone,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (timelineStateTimezone) {
|
|
63
|
+
return {
|
|
64
|
+
timezone: timelineStateTimezone,
|
|
65
|
+
source: "timeline_state_legacy",
|
|
66
|
+
explicit: false,
|
|
67
|
+
timelineStateTimezone,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
timezone: DEFAULT_FALLBACK_TIMEZONE,
|
|
73
|
+
source: "fallback",
|
|
74
|
+
explicit: false,
|
|
75
|
+
timelineStateTimezone,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function readTimelineStateTimezone(timelineStateDir = "") {
|
|
80
|
+
const snapshot = loadTimelineStateSnapshot(timelineStateDir);
|
|
81
|
+
return snapshot.timezone;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function loadTimelineStateSnapshot(timelineStateDir = "") {
|
|
85
|
+
const paths = resolveTimelineStateFiles(timelineStateDir);
|
|
86
|
+
if (!paths.dir) {
|
|
87
|
+
return {
|
|
88
|
+
paths,
|
|
89
|
+
hasAnyFile: false,
|
|
90
|
+
hasFacts: false,
|
|
91
|
+
timezone: "",
|
|
92
|
+
stateDoc: null,
|
|
93
|
+
taxonomyDoc: null,
|
|
94
|
+
factsDoc: null,
|
|
95
|
+
taxonomy: {},
|
|
96
|
+
facts: {},
|
|
97
|
+
proposals: [],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const stateDoc = readJsonFile(paths.stateFile);
|
|
102
|
+
const taxonomyDoc = readJsonFile(paths.taxonomyFile);
|
|
103
|
+
const factsDoc = readJsonFile(paths.factsFile);
|
|
104
|
+
const facts = readFacts(stateDoc, factsDoc);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
paths,
|
|
108
|
+
hasAnyFile: Boolean(stateDoc || taxonomyDoc || factsDoc),
|
|
109
|
+
hasFacts: Object.keys(facts).length > 0,
|
|
110
|
+
timezone: normalizeTimezone(stateDoc?.timezone)
|
|
111
|
+
|| normalizeTimezone(taxonomyDoc?.timezone)
|
|
112
|
+
|| normalizeTimezone(factsDoc?.timezone),
|
|
113
|
+
stateDoc,
|
|
114
|
+
taxonomyDoc,
|
|
115
|
+
factsDoc,
|
|
116
|
+
taxonomy: readTaxonomy(stateDoc, taxonomyDoc),
|
|
117
|
+
facts,
|
|
118
|
+
proposals: readProposals(stateDoc, factsDoc),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function resolveTimelineStateFiles(timelineStateDir = "") {
|
|
123
|
+
const normalizedDir = normalizeText(timelineStateDir);
|
|
124
|
+
if (!normalizedDir) {
|
|
125
|
+
return {
|
|
126
|
+
dir: "",
|
|
127
|
+
stateFile: "",
|
|
128
|
+
taxonomyFile: "",
|
|
129
|
+
factsFile: "",
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const baseDir = path.resolve(normalizedDir);
|
|
134
|
+
const nestedDir = path.join(baseDir, "timeline");
|
|
135
|
+
// timeline-for-agent's canonical layout is <stateDir>/timeline/*.json. We
|
|
136
|
+
// still detect legacy direct files for migration, but a fresh root should
|
|
137
|
+
// default to the nested layout so bootstrap, state sync, and the runtime all
|
|
138
|
+
// converge on the same paths.
|
|
139
|
+
const candidates = [nestedDir, baseDir];
|
|
140
|
+
const existingDir = candidates.find(hasAnyTimelineStateFile) || nestedDir;
|
|
141
|
+
return buildTimelineStateFiles(existingDir);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function hasAnyTimelineStateFile(dirPath) {
|
|
145
|
+
if (!dirPath) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
const files = buildTimelineStateFiles(dirPath);
|
|
149
|
+
return fs.existsSync(files.stateFile)
|
|
150
|
+
|| fs.existsSync(files.taxonomyFile)
|
|
151
|
+
|| fs.existsSync(files.factsFile);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function buildTimelineStateFiles(dirPath) {
|
|
155
|
+
return {
|
|
156
|
+
dir: dirPath,
|
|
157
|
+
stateFile: path.join(dirPath, "timeline-state.json"),
|
|
158
|
+
taxonomyFile: path.join(dirPath, "timeline-taxonomy.json"),
|
|
159
|
+
factsFile: path.join(dirPath, "timeline-facts.json"),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function readTaxonomy(stateDoc, taxonomyDoc) {
|
|
164
|
+
const fromState = stateDoc?.taxonomy;
|
|
165
|
+
if (fromState && typeof fromState === "object") {
|
|
166
|
+
return fromState;
|
|
167
|
+
}
|
|
168
|
+
const fromTaxonomy = taxonomyDoc?.taxonomy;
|
|
169
|
+
return fromTaxonomy && typeof fromTaxonomy === "object" ? fromTaxonomy : {};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function readFacts(stateDoc, factsDoc) {
|
|
173
|
+
const fromState = stateDoc?.facts;
|
|
174
|
+
if (fromState && typeof fromState === "object") {
|
|
175
|
+
return fromState;
|
|
176
|
+
}
|
|
177
|
+
const fromFacts = factsDoc?.facts;
|
|
178
|
+
return fromFacts && typeof fromFacts === "object" ? fromFacts : {};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function readProposals(stateDoc, factsDoc) {
|
|
182
|
+
if (Array.isArray(stateDoc?.proposals)) {
|
|
183
|
+
return stateDoc.proposals;
|
|
184
|
+
}
|
|
185
|
+
return Array.isArray(factsDoc?.proposals) ? factsDoc.proposals : [];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function readJsonFile(filePath) {
|
|
189
|
+
try {
|
|
190
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
191
|
+
} catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function formatDateInTimezone(value, timezone = LEGACY_TIMELINE_TIMEZONE) {
|
|
197
|
+
return formatInTimezone(value, timezone, "en-CA", {
|
|
198
|
+
year: "numeric",
|
|
199
|
+
month: "2-digit",
|
|
200
|
+
day: "2-digit",
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function formatTimeInTimezone(value, timezone = LEGACY_TIMELINE_TIMEZONE, locale = "zh-CN") {
|
|
205
|
+
return formatInTimezone(value, timezone, locale, {
|
|
206
|
+
hour: "2-digit",
|
|
207
|
+
minute: "2-digit",
|
|
208
|
+
hour12: false,
|
|
209
|
+
hourCycle: "h23",
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function formatDateTimeInTimezone(value, timezone = LEGACY_TIMELINE_TIMEZONE) {
|
|
214
|
+
return formatInTimezone(value, timezone, "sv-SE", {
|
|
215
|
+
year: "numeric",
|
|
216
|
+
month: "2-digit",
|
|
217
|
+
day: "2-digit",
|
|
218
|
+
hour: "2-digit",
|
|
219
|
+
minute: "2-digit",
|
|
220
|
+
hour12: false,
|
|
221
|
+
hourCycle: "h23",
|
|
222
|
+
}).replace(" ", "T");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function getCurrentDateStringInTimezone(timezone = LEGACY_TIMELINE_TIMEZONE, now = new Date()) {
|
|
226
|
+
return formatDateInTimezone(now, timezone);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function coerceLocalDateTimeToIso(value, {
|
|
230
|
+
timeZone = LEGACY_TIMELINE_TIMEZONE,
|
|
231
|
+
defaultDate = "",
|
|
232
|
+
defaultTime = "",
|
|
233
|
+
} = {}) {
|
|
234
|
+
const normalized = normalizeText(value);
|
|
235
|
+
if (!normalized) {
|
|
236
|
+
return "";
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (/^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2})?([zZ]|[+-]\d{2}:\d{2})$/.test(normalized)) {
|
|
240
|
+
return normalized.replace(" ", "T");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const localDateTime = normalized.match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}(?::\d{2})?)$/);
|
|
244
|
+
if (localDateTime) {
|
|
245
|
+
return buildZonedIsoString(localDateTime[1], localDateTime[2], timeZone);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const localTime = normalized.match(/^(\d{2}:\d{2}(?::\d{2})?)$/);
|
|
249
|
+
if (localTime && normalizeText(defaultDate)) {
|
|
250
|
+
return buildZonedIsoString(defaultDate, localTime[1], timeZone);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const localDate = normalized.match(/^(\d{4}-\d{2}-\d{2})$/);
|
|
254
|
+
if (localDate && normalizeText(defaultTime)) {
|
|
255
|
+
return buildZonedIsoString(localDate[1], defaultTime, timeZone);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return "";
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function buildZonedIsoString(dateString, timeString, timeZone = LEGACY_TIMELINE_TIMEZONE) {
|
|
262
|
+
const resolvedTimezone = normalizeTimezone(timeZone) || LEGACY_TIMELINE_TIMEZONE;
|
|
263
|
+
const parts = parseLocalDateTimeParts(dateString, timeString);
|
|
264
|
+
if (!parts) {
|
|
265
|
+
return "";
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Convert a wall-clock time in the target timezone into a real instant. We
|
|
269
|
+
// iterate because offsets can change across DST boundaries.
|
|
270
|
+
const instantMs = resolveInstantFromLocalParts(parts, resolvedTimezone);
|
|
271
|
+
const offsetMinutes = getOffsetMinutesForInstant(instantMs, resolvedTimezone);
|
|
272
|
+
return `${formatPartsDate(parts)}T${formatPartsTime(parts)}${formatOffsetMinutes(offsetMinutes)}`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function resolveInstantFromLocalParts(parts, timezone) {
|
|
276
|
+
let guessMs = partsToUtcMs(parts);
|
|
277
|
+
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
278
|
+
const zonedParts = getLocalDateTimePartsForInstant(guessMs, timezone);
|
|
279
|
+
const deltaMs = partsToUtcMs(parts) - partsToUtcMs(zonedParts);
|
|
280
|
+
if (deltaMs === 0) {
|
|
281
|
+
return guessMs;
|
|
282
|
+
}
|
|
283
|
+
guessMs += deltaMs;
|
|
284
|
+
}
|
|
285
|
+
return guessMs;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function getOffsetMinutesForInstant(instantMs, timezone) {
|
|
289
|
+
const zonedParts = getLocalDateTimePartsForInstant(instantMs, timezone);
|
|
290
|
+
return Math.round((partsToUtcMs(zonedParts) - instantMs) / 60000);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function getLocalDateTimePartsForInstant(instantMs, timezone) {
|
|
294
|
+
const formatter = new Intl.DateTimeFormat("en-CA", {
|
|
295
|
+
timeZone: normalizeTimezone(timezone) || LEGACY_TIMELINE_TIMEZONE,
|
|
296
|
+
year: "numeric",
|
|
297
|
+
month: "2-digit",
|
|
298
|
+
day: "2-digit",
|
|
299
|
+
hour: "2-digit",
|
|
300
|
+
minute: "2-digit",
|
|
301
|
+
second: "2-digit",
|
|
302
|
+
hour12: false,
|
|
303
|
+
hourCycle: "h23",
|
|
304
|
+
});
|
|
305
|
+
const parts = Object.create(null);
|
|
306
|
+
for (const part of formatter.formatToParts(new Date(instantMs))) {
|
|
307
|
+
if (part.type === "literal") {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
parts[part.type] = part.value;
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
year: Number.parseInt(parts.year, 10),
|
|
314
|
+
month: Number.parseInt(parts.month, 10),
|
|
315
|
+
day: Number.parseInt(parts.day, 10),
|
|
316
|
+
hour: Number.parseInt(parts.hour, 10),
|
|
317
|
+
minute: Number.parseInt(parts.minute, 10),
|
|
318
|
+
second: Number.parseInt(parts.second, 10),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function parseLocalDateTimeParts(dateString, timeString) {
|
|
323
|
+
const dateMatch = /^(\d{4})-(\d{2})-(\d{2})$/.exec(normalizeText(dateString));
|
|
324
|
+
const timeMatch = /^(\d{2}):(\d{2})(?::(\d{2}))?$/.exec(normalizeText(timeString));
|
|
325
|
+
if (!dateMatch || !timeMatch) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const parts = {
|
|
330
|
+
year: Number.parseInt(dateMatch[1], 10),
|
|
331
|
+
month: Number.parseInt(dateMatch[2], 10),
|
|
332
|
+
day: Number.parseInt(dateMatch[3], 10),
|
|
333
|
+
hour: Number.parseInt(timeMatch[1], 10),
|
|
334
|
+
minute: Number.parseInt(timeMatch[2], 10),
|
|
335
|
+
second: Number.parseInt(timeMatch[3] || "00", 10),
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
if (!isValidDateTimeParts(parts)) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
return parts;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function isValidDateTimeParts(parts) {
|
|
345
|
+
if (!Number.isInteger(parts.year) || parts.year < 1) {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
if (!Number.isInteger(parts.month) || parts.month < 1 || parts.month > 12) {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
if (!Number.isInteger(parts.day) || parts.day < 1 || parts.day > 31) {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
if (!Number.isInteger(parts.hour) || parts.hour < 0 || parts.hour > 23) {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
if (!Number.isInteger(parts.minute) || parts.minute < 0 || parts.minute > 59) {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
if (!Number.isInteger(parts.second) || parts.second < 0 || parts.second > 59) {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const probe = new Date(Date.UTC(
|
|
365
|
+
parts.year,
|
|
366
|
+
parts.month - 1,
|
|
367
|
+
parts.day,
|
|
368
|
+
parts.hour,
|
|
369
|
+
parts.minute,
|
|
370
|
+
parts.second
|
|
371
|
+
));
|
|
372
|
+
return probe.getUTCFullYear() === parts.year
|
|
373
|
+
&& probe.getUTCMonth() + 1 === parts.month
|
|
374
|
+
&& probe.getUTCDate() === parts.day
|
|
375
|
+
&& probe.getUTCHours() === parts.hour
|
|
376
|
+
&& probe.getUTCMinutes() === parts.minute
|
|
377
|
+
&& probe.getUTCSeconds() === parts.second;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function partsToUtcMs(parts) {
|
|
381
|
+
return Date.UTC(
|
|
382
|
+
parts.year,
|
|
383
|
+
parts.month - 1,
|
|
384
|
+
parts.day,
|
|
385
|
+
parts.hour,
|
|
386
|
+
parts.minute,
|
|
387
|
+
parts.second || 0
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function formatPartsDate(parts) {
|
|
392
|
+
return `${String(parts.year).padStart(4, "0")}-${String(parts.month).padStart(2, "0")}-${String(parts.day).padStart(2, "0")}`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function formatPartsTime(parts) {
|
|
396
|
+
return `${String(parts.hour).padStart(2, "0")}:${String(parts.minute).padStart(2, "0")}:${String(parts.second || 0).padStart(2, "0")}`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function formatOffsetMinutes(offsetMinutes) {
|
|
400
|
+
const normalized = Number.isFinite(offsetMinutes) ? offsetMinutes : 0;
|
|
401
|
+
const sign = normalized >= 0 ? "+" : "-";
|
|
402
|
+
const absoluteMinutes = Math.abs(normalized);
|
|
403
|
+
const hours = String(Math.floor(absoluteMinutes / 60)).padStart(2, "0");
|
|
404
|
+
const minutes = String(absoluteMinutes % 60).padStart(2, "0");
|
|
405
|
+
return `${sign}${hours}:${minutes}`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function formatInTimezone(value, timezone, locale, options) {
|
|
409
|
+
const resolvedTimezone = normalizeTimezone(timezone) || LEGACY_TIMELINE_TIMEZONE;
|
|
410
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
411
|
+
if (Number.isNaN(date.getTime())) {
|
|
412
|
+
return "";
|
|
413
|
+
}
|
|
414
|
+
return new Intl.DateTimeFormat(locale, {
|
|
415
|
+
timeZone: resolvedTimezone,
|
|
416
|
+
...options,
|
|
417
|
+
}).format(date);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
module.exports = {
|
|
421
|
+
DEFAULT_FALLBACK_TIMEZONE,
|
|
422
|
+
LEGACY_TIMELINE_TIMEZONE,
|
|
423
|
+
buildZonedIsoString,
|
|
424
|
+
coerceLocalDateTimeToIso,
|
|
425
|
+
formatDateInTimezone,
|
|
426
|
+
formatDateTimeInTimezone,
|
|
427
|
+
formatOffsetMinutes,
|
|
428
|
+
formatTimeInTimezone,
|
|
429
|
+
getCurrentDateStringInTimezone,
|
|
430
|
+
loadTimelineStateSnapshot,
|
|
431
|
+
normalizeTimezone,
|
|
432
|
+
readTimelineStateTimezone,
|
|
433
|
+
resolveSystemTimezone,
|
|
434
|
+
resolveTimelineStateFiles,
|
|
435
|
+
resolveTimezoneConfig,
|
|
436
|
+
};
|