codeksei 0.1.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/LICENSE +661 -0
- package/README.en.md +215 -0
- package/README.md +259 -0
- package/bin/codeksei.js +10 -0
- package/bin/cyberboss.js +11 -0
- package/package.json +86 -0
- package/scripts/install-background-tasks.ps1 +135 -0
- package/scripts/open_shared_wechat_thread.sh +94 -0
- package/scripts/open_wechat_thread.sh +117 -0
- package/scripts/shared-common.js +791 -0
- package/scripts/shared-open.js +46 -0
- package/scripts/shared-start.js +41 -0
- package/scripts/shared-status.js +74 -0
- package/scripts/shared-supervisor.js +141 -0
- package/scripts/shared-task-runner.ps1 +87 -0
- package/scripts/shared-watchdog.js +290 -0
- package/scripts/show_shared_status.sh +53 -0
- package/scripts/start_shared_app_server.sh +65 -0
- package/scripts/start_shared_wechat.sh +108 -0
- package/scripts/timeline-screenshot.sh +15 -0
- package/scripts/uninstall-background-tasks.ps1 +23 -0
- package/src/adapters/channel/weixin/account-store.js +135 -0
- package/src/adapters/channel/weixin/api-v2.js +258 -0
- package/src/adapters/channel/weixin/api.js +180 -0
- package/src/adapters/channel/weixin/context-token-store.js +84 -0
- package/src/adapters/channel/weixin/index.js +605 -0
- package/src/adapters/channel/weixin/legacy.js +567 -0
- package/src/adapters/channel/weixin/login-common.js +63 -0
- package/src/adapters/channel/weixin/login-legacy.js +124 -0
- package/src/adapters/channel/weixin/login-v2.js +186 -0
- package/src/adapters/channel/weixin/media-mime.js +22 -0
- package/src/adapters/channel/weixin/media-receive.js +370 -0
- package/src/adapters/channel/weixin/media-send.js +331 -0
- package/src/adapters/channel/weixin/message-utils-v2.js +282 -0
- package/src/adapters/channel/weixin/message-utils.js +199 -0
- package/src/adapters/channel/weixin/protocol.js +77 -0
- package/src/adapters/channel/weixin/redact.js +41 -0
- package/src/adapters/channel/weixin/reminder-queue-store.js +101 -0
- package/src/adapters/channel/weixin/sync-buffer-store.js +35 -0
- package/src/adapters/runtime/codex/events.js +252 -0
- package/src/adapters/runtime/codex/index.js +502 -0
- package/src/adapters/runtime/codex/message-utils.js +141 -0
- package/src/adapters/runtime/codex/model-catalog.js +106 -0
- package/src/adapters/runtime/codex/protocol-leak-monitor.js +75 -0
- package/src/adapters/runtime/codex/rpc-client.js +443 -0
- package/src/adapters/runtime/codex/session-store.js +376 -0
- package/src/app/channel-send-file-cli.js +57 -0
- package/src/app/diary-write-cli.js +620 -0
- package/src/app/note-auto-cli.js +201 -0
- package/src/app/note-sync-cli.js +130 -0
- package/src/app/project-radar-cli.js +165 -0
- package/src/app/reminder-write-cli.js +210 -0
- package/src/app/review-cli.js +134 -0
- package/src/app/system-checkin-poller.js +100 -0
- package/src/app/system-send-cli.js +129 -0
- package/src/app/timeline-event-cli.js +273 -0
- package/src/app/timeline-screenshot-cli.js +109 -0
- package/src/core/app.js +1810 -0
- package/src/core/branding.js +167 -0
- package/src/core/command-registry.js +609 -0
- package/src/core/config.js +84 -0
- package/src/core/default-targets.js +163 -0
- package/src/core/durable-note-schema.js +325 -0
- package/src/core/instructions-template.js +31 -0
- package/src/core/note-sync.js +433 -0
- package/src/core/project-radar.js +402 -0
- package/src/core/review-semantic.js +524 -0
- package/src/core/review.js +1081 -0
- package/src/core/shared-bridge-heartbeat.js +140 -0
- package/src/core/stream-delivery.js +990 -0
- package/src/core/system-message-dispatcher.js +68 -0
- package/src/core/system-message-queue-store.js +128 -0
- package/src/core/thread-state-store.js +135 -0
- package/src/core/timeline-screenshot-queue-store.js +134 -0
- package/src/core/workspace-alias.js +163 -0
- package/src/core/workspace-bootstrap.js +338 -0
- package/src/index.js +270 -0
- package/src/integrations/timeline/index.js +191 -0
- package/templates/weixin-instructions.md +53 -0
- package/templates/weixin-operations.md +69 -0
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { PACKAGE_NAME } = require("../core/branding");
|
|
4
|
+
|
|
5
|
+
const DEFAULT_SECTION = "supplement";
|
|
6
|
+
const SECTION_HEADINGS = Object.freeze({
|
|
7
|
+
todo: "Todo",
|
|
8
|
+
timeline: "时间线事实",
|
|
9
|
+
fragment: "今日碎片",
|
|
10
|
+
supplement: "补充记录",
|
|
11
|
+
summary: "总结",
|
|
12
|
+
});
|
|
13
|
+
const TODO_STATE_MARKERS = Object.freeze({
|
|
14
|
+
open: " ",
|
|
15
|
+
done: "x",
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
async function runDiaryWriteCommand(config) {
|
|
19
|
+
const args = process.argv.slice(4);
|
|
20
|
+
const options = parseArgs(args);
|
|
21
|
+
const body = await resolveBody(options);
|
|
22
|
+
if (!body) {
|
|
23
|
+
throw new Error("日记内容不能为空,传 --text 或通过 stdin 输入");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const now = new Date();
|
|
27
|
+
const dateString = options.date || formatDate(now);
|
|
28
|
+
const timeString = options.time || formatTime(now);
|
|
29
|
+
const section = normalizeSection(options.section);
|
|
30
|
+
const todoState = normalizeTodoState(options.state, section);
|
|
31
|
+
const usesLegacyTodoDoneFallback = shouldSynthesizeTodoDoneTimelineText({
|
|
32
|
+
section,
|
|
33
|
+
todoState,
|
|
34
|
+
timelineText: options.timelineText,
|
|
35
|
+
});
|
|
36
|
+
const filePath = path.join(config.diaryDir, `${dateString}.md`);
|
|
37
|
+
const entryPayloads = buildDiaryWriteEntryPayloads({
|
|
38
|
+
section,
|
|
39
|
+
timeString,
|
|
40
|
+
title: options.title,
|
|
41
|
+
body,
|
|
42
|
+
todoState,
|
|
43
|
+
timelineText: options.timelineText,
|
|
44
|
+
});
|
|
45
|
+
if (usesLegacyTodoDoneFallback) {
|
|
46
|
+
console.warn(
|
|
47
|
+
`[${PACKAGE_NAME}] diary:write legacy todo-done call omitted --timeline-text; `
|
|
48
|
+
+ "synthesized a minimal point-in-time diary fact to keep the cutover atomic."
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fs.mkdirSync(config.diaryDir, { recursive: true });
|
|
53
|
+
ensureDiaryFile(filePath, now);
|
|
54
|
+
const current = fs.readFileSync(filePath, "utf8");
|
|
55
|
+
const next = entryPayloads.reduce(
|
|
56
|
+
(draft, payload) => insertDiaryEntry(draft, payload, dateString),
|
|
57
|
+
current
|
|
58
|
+
);
|
|
59
|
+
fs.writeFileSync(filePath, next, "utf8");
|
|
60
|
+
console.log(`diary written: ${filePath}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseArgs(args) {
|
|
64
|
+
const options = {
|
|
65
|
+
text: "",
|
|
66
|
+
title: "",
|
|
67
|
+
date: "",
|
|
68
|
+
time: "",
|
|
69
|
+
section: DEFAULT_SECTION,
|
|
70
|
+
state: "",
|
|
71
|
+
timelineText: "",
|
|
72
|
+
useStdin: false,
|
|
73
|
+
};
|
|
74
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
75
|
+
const arg = args[index];
|
|
76
|
+
if (arg === "--text") {
|
|
77
|
+
options.text = readOptionValue(args, index, "--text");
|
|
78
|
+
index += 1;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (arg === "--title") {
|
|
82
|
+
options.title = readOptionValue(args, index, "--title");
|
|
83
|
+
index += 1;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (arg === "--date") {
|
|
87
|
+
options.date = readOptionValue(args, index, "--date");
|
|
88
|
+
index += 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (arg === "--time") {
|
|
92
|
+
options.time = readOptionValue(args, index, "--time");
|
|
93
|
+
index += 1;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (arg === "--section") {
|
|
97
|
+
options.section = readOptionValue(args, index, "--section");
|
|
98
|
+
index += 1;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (arg === "--state") {
|
|
102
|
+
options.state = readOptionValue(args, index, "--state");
|
|
103
|
+
index += 1;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (arg === "--timeline-text") {
|
|
107
|
+
options.timelineText = readOptionValue(args, index, "--timeline-text");
|
|
108
|
+
index += 1;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (arg === "--stdin") {
|
|
112
|
+
options.useStdin = true;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
throw new Error(`未知参数: ${arg}`);
|
|
116
|
+
}
|
|
117
|
+
return options;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function readOptionValue(args, index, optionName) {
|
|
121
|
+
const next = args[index + 1];
|
|
122
|
+
if (typeof next !== "string" || next.startsWith("--")) {
|
|
123
|
+
throw new Error(`${optionName} 需要一个值`);
|
|
124
|
+
}
|
|
125
|
+
return String(next);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function resolveBody(options) {
|
|
129
|
+
const inlineText = normalizeBody(options.text);
|
|
130
|
+
if (inlineText) {
|
|
131
|
+
return inlineText;
|
|
132
|
+
}
|
|
133
|
+
if (!options.useStdin && process.stdin.isTTY) {
|
|
134
|
+
return "";
|
|
135
|
+
}
|
|
136
|
+
return normalizeBody(await readStdin());
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function readStdin() {
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
let buffer = "";
|
|
142
|
+
process.stdin.setEncoding("utf8");
|
|
143
|
+
process.stdin.on("data", (chunk) => {
|
|
144
|
+
buffer += chunk;
|
|
145
|
+
});
|
|
146
|
+
process.stdin.on("end", () => resolve(buffer));
|
|
147
|
+
process.stdin.on("error", reject);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function buildDiaryEntry({ timeString, title, body }) {
|
|
152
|
+
const heading = title ? `### ${timeString} ${title.trim()}` : `### ${timeString}`;
|
|
153
|
+
return `${heading}\n\n${body}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function buildDiaryEntryPayload({ section = DEFAULT_SECTION, timeString, title, body, todoState = "open" }) {
|
|
157
|
+
const normalizedSection = normalizeSection(section);
|
|
158
|
+
const normalizedTitle = normalizeLineItem(title);
|
|
159
|
+
const normalizedBody = normalizeBody(body);
|
|
160
|
+
if (!normalizedBody) {
|
|
161
|
+
throw new Error("日记内容不能为空,传 --text 或通过 stdin 输入");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (normalizedSection === "supplement") {
|
|
165
|
+
return {
|
|
166
|
+
section: normalizedSection,
|
|
167
|
+
entry: buildDiaryEntry({
|
|
168
|
+
timeString,
|
|
169
|
+
title: normalizedTitle,
|
|
170
|
+
body: normalizedBody,
|
|
171
|
+
}),
|
|
172
|
+
title: normalizedTitle,
|
|
173
|
+
body: normalizedBody,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const lineText = buildSectionLineText({ title: normalizedTitle, body: normalizedBody });
|
|
178
|
+
if (!lineText) {
|
|
179
|
+
throw new Error("当前 section 需要非空单行内容");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (normalizedSection === "todo") {
|
|
183
|
+
const normalizedState = normalizeTodoState(todoState, normalizedSection);
|
|
184
|
+
return {
|
|
185
|
+
section: normalizedSection,
|
|
186
|
+
entry: `- [${TODO_STATE_MARKERS[normalizedState]}] ${lineText}`,
|
|
187
|
+
text: lineText,
|
|
188
|
+
todoState: normalizedState,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
section: normalizedSection,
|
|
194
|
+
entry: `- ${lineText}`,
|
|
195
|
+
text: lineText,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function buildDiaryWriteEntryPayloads({
|
|
200
|
+
section = DEFAULT_SECTION,
|
|
201
|
+
timeString,
|
|
202
|
+
title,
|
|
203
|
+
body,
|
|
204
|
+
todoState = "open",
|
|
205
|
+
timelineText = "",
|
|
206
|
+
}) {
|
|
207
|
+
const normalizedSection = normalizeSection(section);
|
|
208
|
+
const normalizedTodoState = normalizeTodoState(todoState, normalizedSection);
|
|
209
|
+
const normalizedTimelineText = normalizeLineItem(timelineText)
|
|
210
|
+
|| synthesizeTodoDoneTimelineText({ section, timeString, title, body, todoState });
|
|
211
|
+
|
|
212
|
+
if (normalizedTimelineText && (normalizedSection !== "todo" || normalizedTodoState !== "done")) {
|
|
213
|
+
throw new Error("--timeline-text 只支持和 --section todo --state done 一起使用");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const entries = [
|
|
217
|
+
buildDiaryEntryPayload({
|
|
218
|
+
section: normalizedSection,
|
|
219
|
+
timeString,
|
|
220
|
+
title,
|
|
221
|
+
body,
|
|
222
|
+
todoState: normalizedTodoState,
|
|
223
|
+
}),
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
if (normalizedTimelineText) {
|
|
227
|
+
entries.push(buildDiaryEntryPayload({
|
|
228
|
+
section: "timeline",
|
|
229
|
+
timeString,
|
|
230
|
+
body: normalizedTimelineText,
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return entries;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function shouldSynthesizeTodoDoneTimelineText({ section = DEFAULT_SECTION, todoState = "open", timelineText = "" }) {
|
|
238
|
+
const normalizedSection = normalizeSection(section);
|
|
239
|
+
const normalizedTodoState = normalizeTodoState(todoState, normalizedSection);
|
|
240
|
+
return normalizedSection === "todo"
|
|
241
|
+
&& normalizedTodoState === "done"
|
|
242
|
+
&& !normalizeLineItem(timelineText);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function synthesizeTodoDoneTimelineText({
|
|
246
|
+
section = DEFAULT_SECTION,
|
|
247
|
+
timeString = "",
|
|
248
|
+
title = "",
|
|
249
|
+
body = "",
|
|
250
|
+
todoState = "open",
|
|
251
|
+
}) {
|
|
252
|
+
if (!shouldSynthesizeTodoDoneTimelineText({ section, todoState })) {
|
|
253
|
+
return "";
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const lineText = buildSectionLineText({ title, body });
|
|
257
|
+
if (!lineText) {
|
|
258
|
+
return "";
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Keep stale prompts from dropping the diary hard fact entirely. This
|
|
262
|
+
// compatibility bridge records only a minimal point-in-time fact; fresh
|
|
263
|
+
// callers should still pass --timeline-text with the real cutover context.
|
|
264
|
+
const normalizedTime = normalizeLineItem(timeString);
|
|
265
|
+
return normalizedTime ? `${normalizedTime} ${lineText}` : lineText;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function ensureDiaryFile(filePath, now) {
|
|
269
|
+
if (fs.existsSync(filePath) && fs.statSync(filePath).size > 0) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const createdAt = formatDateTime(now);
|
|
273
|
+
const updated = formatDate(now);
|
|
274
|
+
fs.writeFileSync(filePath, buildDiaryFileSkeleton({ createdAt, updated }), "utf8");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function buildDiaryFileSkeleton({ createdAt, updated }) {
|
|
278
|
+
return [
|
|
279
|
+
"---",
|
|
280
|
+
`created: ${createdAt}`,
|
|
281
|
+
`updated: ${updated}`,
|
|
282
|
+
"---",
|
|
283
|
+
"## Todo",
|
|
284
|
+
"- [ ] ",
|
|
285
|
+
"",
|
|
286
|
+
"## 时间线事实",
|
|
287
|
+
"- ",
|
|
288
|
+
"",
|
|
289
|
+
"## 今日碎片",
|
|
290
|
+
"- ",
|
|
291
|
+
"",
|
|
292
|
+
"## 补充记录",
|
|
293
|
+
"",
|
|
294
|
+
"## 总结",
|
|
295
|
+
"",
|
|
296
|
+
].join("\n");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function insertDiaryEntry(content, entry, updatedDate) {
|
|
300
|
+
const normalizedContent = normalizeFileEnding(content);
|
|
301
|
+
const withUpdatedFrontmatter = updateFrontmatterValue(normalizedContent, "updated", updatedDate);
|
|
302
|
+
const payload = normalizeEntryPayload(entry);
|
|
303
|
+
if (payload.section === "supplement") {
|
|
304
|
+
return insertSupplementEntry(withUpdatedFrontmatter, payload);
|
|
305
|
+
}
|
|
306
|
+
return insertBulletSectionEntry(withUpdatedFrontmatter, payload);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function findInsertionPoint(content) {
|
|
310
|
+
const supplementSection = locateSectionStart(content, "补充记录");
|
|
311
|
+
if (supplementSection >= 0) {
|
|
312
|
+
const nextHeading = locateNextLevelTwoHeading(content, supplementSection + 1);
|
|
313
|
+
return nextHeading >= 0 ? nextHeading : content.length;
|
|
314
|
+
}
|
|
315
|
+
const summarySection = locateSectionStart(content, "总结");
|
|
316
|
+
if (summarySection >= 0) {
|
|
317
|
+
return summarySection;
|
|
318
|
+
}
|
|
319
|
+
return content.length;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function locateSectionStart(content, headingText) {
|
|
323
|
+
const pattern = new RegExp(`^##\\s+${escapeRegExp(headingText)}\\s*$`, "m");
|
|
324
|
+
const match = pattern.exec(content);
|
|
325
|
+
return match ? match.index : -1;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function locateNextLevelTwoHeading(content, fromIndex) {
|
|
329
|
+
const pattern = /^##\s+/gm;
|
|
330
|
+
pattern.lastIndex = fromIndex;
|
|
331
|
+
const match = pattern.exec(content);
|
|
332
|
+
return match ? match.index : -1;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function normalizeEntryPayload(entry) {
|
|
336
|
+
if (typeof entry === "string") {
|
|
337
|
+
return {
|
|
338
|
+
section: DEFAULT_SECTION,
|
|
339
|
+
entry: entry.trim(),
|
|
340
|
+
title: "",
|
|
341
|
+
body: "",
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
const payload = entry && typeof entry === "object" ? entry : {};
|
|
345
|
+
return {
|
|
346
|
+
section: normalizeSection(payload.section),
|
|
347
|
+
entry: String(payload.entry || "").trim(),
|
|
348
|
+
text: normalizeLineItem(payload.text),
|
|
349
|
+
title: normalizeLineItem(payload.title),
|
|
350
|
+
body: normalizeBody(payload.body),
|
|
351
|
+
todoState: normalizeTodoState(payload.todoState, normalizeSection(payload.section)),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function insertSupplementEntry(content, payload) {
|
|
356
|
+
const range = findSectionRange(content, SECTION_HEADINGS.supplement);
|
|
357
|
+
if (!range || !payload.entry) {
|
|
358
|
+
return content;
|
|
359
|
+
}
|
|
360
|
+
const existingBody = content.slice(range.contentStart, range.end);
|
|
361
|
+
if (hasSupplementDuplicate(existingBody, payload)) {
|
|
362
|
+
return content;
|
|
363
|
+
}
|
|
364
|
+
const normalizedBody = normalizeFileEnding(existingBody).trim();
|
|
365
|
+
const nextBody = normalizedBody ? `${normalizedBody}\n\n${payload.entry}` : payload.entry;
|
|
366
|
+
return replaceSectionBody(content, range, nextBody);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function insertBulletSectionEntry(content, payload) {
|
|
370
|
+
const headingText = SECTION_HEADINGS[payload.section];
|
|
371
|
+
const range = findSectionRange(content, headingText);
|
|
372
|
+
if (!range || !payload.entry) {
|
|
373
|
+
return content;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const lines = extractSectionLines(content.slice(range.contentStart, range.end), payload.section);
|
|
377
|
+
const nextLines = payload.section === "todo"
|
|
378
|
+
? upsertTodoLine(lines, payload)
|
|
379
|
+
: upsertBulletLine(lines, payload);
|
|
380
|
+
return replaceSectionBody(content, range, nextLines.join("\n"));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function findSectionRange(content, headingText) {
|
|
384
|
+
const sectionStart = locateSectionStart(content, headingText);
|
|
385
|
+
if (sectionStart < 0) {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
const lineEnd = content.indexOf("\n", sectionStart);
|
|
389
|
+
const contentStart = lineEnd >= 0 ? lineEnd + 1 : content.length;
|
|
390
|
+
const end = locateNextLevelTwoHeading(content, contentStart);
|
|
391
|
+
return {
|
|
392
|
+
sectionStart,
|
|
393
|
+
contentStart,
|
|
394
|
+
end: end >= 0 ? end : content.length,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function extractSectionLines(sectionBody, section) {
|
|
399
|
+
const normalizedBody = normalizeFileEnding(sectionBody);
|
|
400
|
+
const lines = normalizedBody.split("\n").map((line) => line.replace(/\s+$/u, ""));
|
|
401
|
+
while (lines.length && !lines[0].trim()) {
|
|
402
|
+
lines.shift();
|
|
403
|
+
}
|
|
404
|
+
while (lines.length && !lines[lines.length - 1].trim()) {
|
|
405
|
+
lines.pop();
|
|
406
|
+
}
|
|
407
|
+
return lines.filter((line) => !isPlaceholderLine(line, section));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function isPlaceholderLine(line, section) {
|
|
411
|
+
const trimmed = String(line || "").trim();
|
|
412
|
+
if (!trimmed) {
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
if (section === "todo") {
|
|
416
|
+
return trimmed === "-"
|
|
417
|
+
|| trimmed === "- [ ]"
|
|
418
|
+
|| trimmed === "- [x]"
|
|
419
|
+
|| trimmed === "- [X]";
|
|
420
|
+
}
|
|
421
|
+
return trimmed === "-" || trimmed === "*";
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function upsertTodoLine(lines, payload) {
|
|
425
|
+
const nextLines = Array.isArray(lines) ? [...lines] : [];
|
|
426
|
+
const desiredLine = `- [${TODO_STATE_MARKERS[payload.todoState]}] ${payload.text}`;
|
|
427
|
+
for (let index = 0; index < nextLines.length; index += 1) {
|
|
428
|
+
const match = /^- \[( |x|X)\] (.*)$/u.exec(String(nextLines[index] || ""));
|
|
429
|
+
if (!match) {
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (normalizeLineItem(match[2]) !== payload.text) {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
nextLines[index] = desiredLine;
|
|
436
|
+
return nextLines;
|
|
437
|
+
}
|
|
438
|
+
nextLines.push(desiredLine);
|
|
439
|
+
return nextLines;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function upsertBulletLine(lines, payload) {
|
|
443
|
+
const nextLines = Array.isArray(lines) ? [...lines] : [];
|
|
444
|
+
for (const line of nextLines) {
|
|
445
|
+
const match = /^- (.*)$/u.exec(String(line || ""));
|
|
446
|
+
if (!match) {
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (normalizeLineItem(match[1]) === payload.text) {
|
|
450
|
+
return nextLines;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
nextLines.push(payload.entry);
|
|
454
|
+
return nextLines;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function replaceSectionBody(content, range, newBody) {
|
|
458
|
+
const before = content.slice(0, range.contentStart).replace(/\s*$/u, "");
|
|
459
|
+
const after = content.slice(range.end).replace(/^\s*/u, "");
|
|
460
|
+
const parts = [before];
|
|
461
|
+
if (newBody.trim()) {
|
|
462
|
+
parts.push(newBody.trimEnd());
|
|
463
|
+
}
|
|
464
|
+
if (after) {
|
|
465
|
+
parts.push(after);
|
|
466
|
+
}
|
|
467
|
+
return `${parts.filter(Boolean).join("\n\n").trimEnd()}\n`;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function hasSupplementDuplicate(sectionBody, payload) {
|
|
471
|
+
const targetBody = normalizeBody(payload.body);
|
|
472
|
+
if (!targetBody) {
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
const targetTitle = normalizeLineItem(payload.title);
|
|
476
|
+
return parseSupplementBlocks(sectionBody).some((block) =>
|
|
477
|
+
block.title === targetTitle && block.body === targetBody
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function parseSupplementBlocks(sectionBody) {
|
|
482
|
+
const normalizedBody = normalizeFileEnding(sectionBody).trim();
|
|
483
|
+
if (!normalizedBody) {
|
|
484
|
+
return [];
|
|
485
|
+
}
|
|
486
|
+
return normalizedBody
|
|
487
|
+
.split(/\n(?=###\s)/u)
|
|
488
|
+
.map((block) => {
|
|
489
|
+
const lines = block.split("\n");
|
|
490
|
+
const heading = String(lines.shift() || "").trim();
|
|
491
|
+
const match = /^###\s+\d{2}:\d{2}(?:\s+(.*))?$/u.exec(heading);
|
|
492
|
+
return {
|
|
493
|
+
title: normalizeLineItem(match?.[1] || ""),
|
|
494
|
+
body: normalizeBody(lines.join("\n")),
|
|
495
|
+
};
|
|
496
|
+
})
|
|
497
|
+
.filter((block) => block.body);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function updateFrontmatterValue(content, key, value) {
|
|
501
|
+
if (!content.startsWith("---\n")) {
|
|
502
|
+
return content;
|
|
503
|
+
}
|
|
504
|
+
const frontmatterEnd = content.indexOf("\n---\n", 4);
|
|
505
|
+
if (frontmatterEnd < 0) {
|
|
506
|
+
return content;
|
|
507
|
+
}
|
|
508
|
+
const frontmatter = content.slice(4, frontmatterEnd);
|
|
509
|
+
const body = content.slice(frontmatterEnd + 5);
|
|
510
|
+
const keyPattern = new RegExp(`^${escapeRegExp(key)}:\\s*.*$`, "m");
|
|
511
|
+
const nextFrontmatter = keyPattern.test(frontmatter)
|
|
512
|
+
? frontmatter.replace(keyPattern, `${key}: ${value}`)
|
|
513
|
+
: `${frontmatter}\n${key}: ${value}`;
|
|
514
|
+
return `---\n${nextFrontmatter}\n---\n${body.replace(/^\n*/u, "")}`;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function normalizeFileEnding(content) {
|
|
518
|
+
return String(content || "").replace(/\r\n/g, "\n");
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function escapeRegExp(value) {
|
|
522
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function buildSectionLineText({ title, body }) {
|
|
526
|
+
const normalizedTitle = normalizeLineItem(title);
|
|
527
|
+
const normalizedBody = normalizeLineItem(body);
|
|
528
|
+
if (normalizedTitle && normalizedBody) {
|
|
529
|
+
return `${normalizedTitle}: ${normalizedBody}`;
|
|
530
|
+
}
|
|
531
|
+
return normalizedTitle || normalizedBody;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function normalizeSection(value) {
|
|
535
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
536
|
+
switch (normalized) {
|
|
537
|
+
case "":
|
|
538
|
+
case "supplement":
|
|
539
|
+
return "supplement";
|
|
540
|
+
case "todo":
|
|
541
|
+
return "todo";
|
|
542
|
+
case "timeline":
|
|
543
|
+
return "timeline";
|
|
544
|
+
case "fragment":
|
|
545
|
+
return "fragment";
|
|
546
|
+
case "summary":
|
|
547
|
+
return "summary";
|
|
548
|
+
default:
|
|
549
|
+
throw new Error(`不支持的日记 section: ${value}`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function normalizeTodoState(value, section = DEFAULT_SECTION) {
|
|
554
|
+
const normalizedSection = normalizeSection(section);
|
|
555
|
+
const normalizedValue = String(value || "").trim().toLowerCase();
|
|
556
|
+
if (normalizedSection !== "todo") {
|
|
557
|
+
if (normalizedValue) {
|
|
558
|
+
throw new Error("--state 只支持和 --section todo 一起使用");
|
|
559
|
+
}
|
|
560
|
+
return "open";
|
|
561
|
+
}
|
|
562
|
+
if (!normalizedValue) {
|
|
563
|
+
return "open";
|
|
564
|
+
}
|
|
565
|
+
if (normalizedValue === "open" || normalizedValue === "done") {
|
|
566
|
+
return normalizedValue;
|
|
567
|
+
}
|
|
568
|
+
throw new Error(`不支持的 Todo state: ${value}`);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function normalizeBody(value) {
|
|
572
|
+
return String(value || "").replace(/\r\n/g, "\n").trim();
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function normalizeLineItem(value) {
|
|
576
|
+
return normalizeBody(value).replace(/\s*\n+\s*/g, " ").replace(/\s{2,}/g, " ").trim();
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function formatDate(date) {
|
|
580
|
+
return new Intl.DateTimeFormat("en-CA", {
|
|
581
|
+
timeZone: "Asia/Shanghai",
|
|
582
|
+
year: "numeric",
|
|
583
|
+
month: "2-digit",
|
|
584
|
+
day: "2-digit",
|
|
585
|
+
}).format(date);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function formatTime(date) {
|
|
589
|
+
return new Intl.DateTimeFormat("zh-CN", {
|
|
590
|
+
timeZone: "Asia/Shanghai",
|
|
591
|
+
hour: "2-digit",
|
|
592
|
+
minute: "2-digit",
|
|
593
|
+
hour12: false,
|
|
594
|
+
}).format(date);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function formatDateTime(date) {
|
|
598
|
+
const formatter = new Intl.DateTimeFormat("sv-SE", {
|
|
599
|
+
timeZone: "Asia/Shanghai",
|
|
600
|
+
year: "numeric",
|
|
601
|
+
month: "2-digit",
|
|
602
|
+
day: "2-digit",
|
|
603
|
+
hour: "2-digit",
|
|
604
|
+
minute: "2-digit",
|
|
605
|
+
hour12: false,
|
|
606
|
+
});
|
|
607
|
+
return formatter.format(date).replace(" ", "T");
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
module.exports = {
|
|
611
|
+
buildDiaryEntry,
|
|
612
|
+
buildDiaryEntryPayload,
|
|
613
|
+
buildDiaryWriteEntryPayloads,
|
|
614
|
+
buildDiaryFileSkeleton,
|
|
615
|
+
insertDiaryEntry,
|
|
616
|
+
normalizeSection,
|
|
617
|
+
normalizeTodoState,
|
|
618
|
+
parseArgs,
|
|
619
|
+
runDiaryWriteCommand,
|
|
620
|
+
};
|