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,57 +1,57 @@
|
|
|
1
|
-
async function runChannelSendFileCommand(app) {
|
|
2
|
-
const args = process.argv.slice(4);
|
|
3
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
4
|
-
printHelp();
|
|
5
|
-
return;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const options = parseArgs(args);
|
|
9
|
-
if (!options.path) {
|
|
10
|
-
throw new Error("缺少 --path,指定要发回微信的本地文件路径");
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const result = await app.sendLocalFileToCurrentChat({
|
|
14
|
-
senderId: options.user,
|
|
15
|
-
filePath: options.path,
|
|
16
|
-
});
|
|
17
|
-
console.log(`file sent: ${result.filePath}`);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function parseArgs(args) {
|
|
21
|
-
const options = {
|
|
22
|
-
path: "",
|
|
23
|
-
user: "",
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
27
|
-
const arg = String(args[index] || "");
|
|
28
|
-
if (arg === "--path") {
|
|
29
|
-
options.path = String(args[index + 1] || "");
|
|
30
|
-
index += 1;
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
if (arg === "--user") {
|
|
34
|
-
options.user = String(args[index + 1] || "");
|
|
35
|
-
index += 1;
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
throw new Error(`未知参数: ${arg}`);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return options;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function printHelp() {
|
|
45
|
-
console.log([
|
|
46
|
-
"用法: npm run channel:send-file -- --path /绝对路径 [--user <wechatUserId>]",
|
|
47
|
-
"",
|
|
48
|
-
"参数:",
|
|
49
|
-
" --path /绝对路径 要发回当前微信聊天的本地文件",
|
|
50
|
-
" --user <wechatUserId> 可选,覆盖默认接收用户",
|
|
51
|
-
"",
|
|
52
|
-
"示例:",
|
|
53
|
-
" npm run channel:send-file -- --path /Users/name/project/README.md",
|
|
54
|
-
].join("\n"));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
module.exports = { runChannelSendFileCommand };
|
|
1
|
+
async function runChannelSendFileCommand(app) {
|
|
2
|
+
const args = process.argv.slice(4);
|
|
3
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
4
|
+
printHelp();
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const options = parseArgs(args);
|
|
9
|
+
if (!options.path) {
|
|
10
|
+
throw new Error("缺少 --path,指定要发回微信的本地文件路径");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const result = await app.sendLocalFileToCurrentChat({
|
|
14
|
+
senderId: options.user,
|
|
15
|
+
filePath: options.path,
|
|
16
|
+
});
|
|
17
|
+
console.log(`file sent: ${result.filePath}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseArgs(args) {
|
|
21
|
+
const options = {
|
|
22
|
+
path: "",
|
|
23
|
+
user: "",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
27
|
+
const arg = String(args[index] || "");
|
|
28
|
+
if (arg === "--path") {
|
|
29
|
+
options.path = String(args[index + 1] || "");
|
|
30
|
+
index += 1;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (arg === "--user") {
|
|
34
|
+
options.user = String(args[index + 1] || "");
|
|
35
|
+
index += 1;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
throw new Error(`未知参数: ${arg}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return options;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function printHelp() {
|
|
45
|
+
console.log([
|
|
46
|
+
"用法: npm run channel:send-file -- --path /绝对路径 [--user <wechatUserId>]",
|
|
47
|
+
"",
|
|
48
|
+
"参数:",
|
|
49
|
+
" --path /绝对路径 要发回当前微信聊天的本地文件",
|
|
50
|
+
" --user <wechatUserId> 可选,覆盖默认接收用户",
|
|
51
|
+
"",
|
|
52
|
+
"示例:",
|
|
53
|
+
" npm run channel:send-file -- --path /Users/name/project/README.md",
|
|
54
|
+
].join("\n"));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { runChannelSendFileCommand };
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const { PACKAGE_NAME } = require("../core/branding");
|
|
4
|
+
const {
|
|
5
|
+
LEGACY_TIMELINE_TIMEZONE,
|
|
6
|
+
formatDateInTimezone,
|
|
7
|
+
formatDateTimeInTimezone,
|
|
8
|
+
formatTimeInTimezone,
|
|
9
|
+
} = require("../core/timezone");
|
|
4
10
|
|
|
5
11
|
const DEFAULT_SECTION = "supplement";
|
|
6
12
|
const SECTION_HEADINGS = Object.freeze({
|
|
@@ -14,6 +20,8 @@ const TODO_STATE_MARKERS = Object.freeze({
|
|
|
14
20
|
open: " ",
|
|
15
21
|
done: "x",
|
|
16
22
|
});
|
|
23
|
+
const TODO_LINE_RE = /^- \[( |x|X)\] (.*)$/u;
|
|
24
|
+
const TODO_START_MARKER_RE = /\s*<!--\s*codeksei-todo:start=(\d{2}:\d{2})\s*-->\s*$/u;
|
|
17
25
|
|
|
18
26
|
async function runDiaryWriteCommand(config) {
|
|
19
27
|
const args = process.argv.slice(4);
|
|
@@ -22,36 +30,47 @@ async function runDiaryWriteCommand(config) {
|
|
|
22
30
|
if (!body) {
|
|
23
31
|
throw new Error("日记内容不能为空,传 --text 或通过 stdin 输入");
|
|
24
32
|
}
|
|
25
|
-
|
|
33
|
+
|
|
26
34
|
const now = new Date();
|
|
27
|
-
const
|
|
28
|
-
const
|
|
35
|
+
const timezone = config?.timezone || LEGACY_TIMELINE_TIMEZONE;
|
|
36
|
+
const dateString = options.date || formatDate(now, timezone);
|
|
37
|
+
const timeString = options.time || formatTime(now, timezone);
|
|
29
38
|
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
39
|
const filePath = path.join(config.diaryDir, `${dateString}.md`);
|
|
37
|
-
|
|
40
|
+
fs.mkdirSync(config.diaryDir, { recursive: true });
|
|
41
|
+
ensureDiaryFile(filePath, now, timezone);
|
|
42
|
+
const current = fs.readFileSync(filePath, "utf8");
|
|
43
|
+
const timelineResolution = resolveTodoDoneTimelineText({
|
|
44
|
+
existingContent: current,
|
|
38
45
|
section,
|
|
39
46
|
timeString,
|
|
40
47
|
title: options.title,
|
|
41
48
|
body,
|
|
42
|
-
|
|
49
|
+
// Keep the raw CLI flag here too. Otherwise non-todo writes inherit the
|
|
50
|
+
// synthesized internal "open" default and trip the same guard that is
|
|
51
|
+
// meant only for explicit --state misuse.
|
|
52
|
+
todoState: options.state,
|
|
43
53
|
timelineText: options.timelineText,
|
|
44
54
|
});
|
|
45
|
-
if (
|
|
55
|
+
if (timelineResolution.mode === "point_in_time") {
|
|
46
56
|
console.warn(
|
|
47
|
-
`[${PACKAGE_NAME}] diary:write
|
|
48
|
-
+ "synthesized a
|
|
57
|
+
`[${PACKAGE_NAME}] diary:write todo-done call omitted --timeline-text and no captured Todo start time was found; `
|
|
58
|
+
+ "synthesized only a point-in-time diary fact. Prefer opening the live Todo earlier, or pass exact cutover wording via --timeline-text."
|
|
49
59
|
);
|
|
50
60
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
61
|
+
const entryPayloads = buildDiaryWriteEntryPayloads({
|
|
62
|
+
existingContent: current,
|
|
63
|
+
section,
|
|
64
|
+
timeString,
|
|
65
|
+
title: options.title,
|
|
66
|
+
body,
|
|
67
|
+
// Keep the raw CLI flag here. Non-todo writes internally normalize the
|
|
68
|
+
// missing state to "open" for cutover bookkeeping, but treating that
|
|
69
|
+
// synthesized default as an explicit --state would wrongly reject
|
|
70
|
+
// fragment/timeline writes.
|
|
71
|
+
todoState: options.state,
|
|
72
|
+
timelineText: options.timelineText,
|
|
73
|
+
});
|
|
55
74
|
const next = entryPayloads.reduce(
|
|
56
75
|
(draft, payload) => insertDiaryEntry(draft, payload, dateString),
|
|
57
76
|
current
|
|
@@ -61,7 +80,7 @@ async function runDiaryWriteCommand(config) {
|
|
|
61
80
|
}
|
|
62
81
|
|
|
63
82
|
function parseArgs(args) {
|
|
64
|
-
const options = {
|
|
83
|
+
const options = {
|
|
65
84
|
text: "",
|
|
66
85
|
title: "",
|
|
67
86
|
date: "",
|
|
@@ -124,30 +143,30 @@ function readOptionValue(args, index, optionName) {
|
|
|
124
143
|
}
|
|
125
144
|
return String(next);
|
|
126
145
|
}
|
|
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
|
-
|
|
146
|
+
|
|
147
|
+
async function resolveBody(options) {
|
|
148
|
+
const inlineText = normalizeBody(options.text);
|
|
149
|
+
if (inlineText) {
|
|
150
|
+
return inlineText;
|
|
151
|
+
}
|
|
152
|
+
if (!options.useStdin && process.stdin.isTTY) {
|
|
153
|
+
return "";
|
|
154
|
+
}
|
|
155
|
+
return normalizeBody(await readStdin());
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function readStdin() {
|
|
159
|
+
return new Promise((resolve, reject) => {
|
|
160
|
+
let buffer = "";
|
|
161
|
+
process.stdin.setEncoding("utf8");
|
|
162
|
+
process.stdin.on("data", (chunk) => {
|
|
163
|
+
buffer += chunk;
|
|
164
|
+
});
|
|
165
|
+
process.stdin.on("end", () => resolve(buffer));
|
|
166
|
+
process.stdin.on("error", reject);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
151
170
|
function buildDiaryEntry({ timeString, title, body }) {
|
|
152
171
|
const heading = title ? `### ${timeString} ${title.trim()}` : `### ${timeString}`;
|
|
153
172
|
return `${heading}\n\n${body}`;
|
|
@@ -181,11 +200,17 @@ function buildDiaryEntryPayload({ section = DEFAULT_SECTION, timeString, title,
|
|
|
181
200
|
|
|
182
201
|
if (normalizedSection === "todo") {
|
|
183
202
|
const normalizedState = normalizeTodoState(todoState, normalizedSection);
|
|
203
|
+
const todoStartedAt = normalizedState === "open" ? normalizeTodoClock(timeString) : "";
|
|
184
204
|
return {
|
|
185
205
|
section: normalizedSection,
|
|
186
|
-
entry:
|
|
206
|
+
entry: buildTodoLine({
|
|
207
|
+
text: lineText,
|
|
208
|
+
todoState: normalizedState,
|
|
209
|
+
todoStartedAt,
|
|
210
|
+
}),
|
|
187
211
|
text: lineText,
|
|
188
212
|
todoState: normalizedState,
|
|
213
|
+
todoStartedAt,
|
|
189
214
|
};
|
|
190
215
|
}
|
|
191
216
|
|
|
@@ -197,17 +222,25 @@ function buildDiaryEntryPayload({ section = DEFAULT_SECTION, timeString, title,
|
|
|
197
222
|
}
|
|
198
223
|
|
|
199
224
|
function buildDiaryWriteEntryPayloads({
|
|
225
|
+
existingContent = "",
|
|
200
226
|
section = DEFAULT_SECTION,
|
|
201
227
|
timeString,
|
|
202
228
|
title,
|
|
203
229
|
body,
|
|
204
|
-
todoState = "
|
|
230
|
+
todoState = "",
|
|
205
231
|
timelineText = "",
|
|
206
232
|
}) {
|
|
207
233
|
const normalizedSection = normalizeSection(section);
|
|
208
234
|
const normalizedTodoState = normalizeTodoState(todoState, normalizedSection);
|
|
209
|
-
const normalizedTimelineText =
|
|
210
|
-
|
|
235
|
+
const normalizedTimelineText = resolveTodoDoneTimelineText({
|
|
236
|
+
existingContent,
|
|
237
|
+
section,
|
|
238
|
+
timeString,
|
|
239
|
+
title,
|
|
240
|
+
body,
|
|
241
|
+
todoState,
|
|
242
|
+
timelineText,
|
|
243
|
+
}).text;
|
|
211
244
|
|
|
212
245
|
if (normalizedTimelineText && (normalizedSection !== "todo" || normalizedTodoState !== "done")) {
|
|
213
246
|
throw new Error("--timeline-text 只支持和 --section todo --state done 一起使用");
|
|
@@ -242,12 +275,55 @@ function shouldSynthesizeTodoDoneTimelineText({ section = DEFAULT_SECTION, todoS
|
|
|
242
275
|
&& !normalizeLineItem(timelineText);
|
|
243
276
|
}
|
|
244
277
|
|
|
278
|
+
function resolveTodoDoneTimelineText({
|
|
279
|
+
existingContent = "",
|
|
280
|
+
section = DEFAULT_SECTION,
|
|
281
|
+
timeString = "",
|
|
282
|
+
title = "",
|
|
283
|
+
body = "",
|
|
284
|
+
todoState = "open",
|
|
285
|
+
timelineText = "",
|
|
286
|
+
}) {
|
|
287
|
+
const explicitTimelineText = normalizeLineItem(timelineText);
|
|
288
|
+
if (explicitTimelineText) {
|
|
289
|
+
return {
|
|
290
|
+
text: explicitTimelineText,
|
|
291
|
+
mode: "explicit",
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!shouldSynthesizeTodoDoneTimelineText({ section, todoState })) {
|
|
296
|
+
return {
|
|
297
|
+
text: "",
|
|
298
|
+
mode: "none",
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const existingTodoStartTime = findTodoStartTimeInDiaryContent(existingContent, {
|
|
303
|
+
title,
|
|
304
|
+
body,
|
|
305
|
+
});
|
|
306
|
+
const synthesized = synthesizeTodoDoneTimelineText({
|
|
307
|
+
section,
|
|
308
|
+
timeString,
|
|
309
|
+
title,
|
|
310
|
+
body,
|
|
311
|
+
todoState,
|
|
312
|
+
existingTodoStartTime,
|
|
313
|
+
});
|
|
314
|
+
return {
|
|
315
|
+
text: synthesized,
|
|
316
|
+
mode: existingTodoStartTime ? "range_from_todo" : "point_in_time",
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
245
320
|
function synthesizeTodoDoneTimelineText({
|
|
246
321
|
section = DEFAULT_SECTION,
|
|
247
322
|
timeString = "",
|
|
248
323
|
title = "",
|
|
249
324
|
body = "",
|
|
250
325
|
todoState = "open",
|
|
326
|
+
existingTodoStartTime = "",
|
|
251
327
|
}) {
|
|
252
328
|
if (!shouldSynthesizeTodoDoneTimelineText({ section, todoState })) {
|
|
253
329
|
return "";
|
|
@@ -258,19 +334,23 @@ function synthesizeTodoDoneTimelineText({
|
|
|
258
334
|
return "";
|
|
259
335
|
}
|
|
260
336
|
|
|
261
|
-
// Keep stale prompts from dropping the diary hard fact entirely.
|
|
262
|
-
//
|
|
263
|
-
//
|
|
337
|
+
// Keep stale prompts from dropping the diary hard fact entirely. When the
|
|
338
|
+
// same live Todo already captured a reliable start time, reuse it here so
|
|
339
|
+
// todo-first workflow can still produce an accurate diary range at cutover.
|
|
340
|
+
const capturedStartTime = normalizeTodoClock(existingTodoStartTime);
|
|
264
341
|
const normalizedTime = normalizeLineItem(timeString);
|
|
342
|
+
if (capturedStartTime && normalizedTime && capturedStartTime !== normalizedTime) {
|
|
343
|
+
return `${capturedStartTime}-${normalizedTime} ${lineText}`;
|
|
344
|
+
}
|
|
265
345
|
return normalizedTime ? `${normalizedTime} ${lineText}` : lineText;
|
|
266
346
|
}
|
|
267
347
|
|
|
268
|
-
function ensureDiaryFile(filePath, now) {
|
|
348
|
+
function ensureDiaryFile(filePath, now, timezone = LEGACY_TIMELINE_TIMEZONE) {
|
|
269
349
|
if (fs.existsSync(filePath) && fs.statSync(filePath).size > 0) {
|
|
270
350
|
return;
|
|
271
351
|
}
|
|
272
|
-
const createdAt = formatDateTime(now);
|
|
273
|
-
const updated = formatDate(now);
|
|
352
|
+
const createdAt = formatDateTime(now, timezone);
|
|
353
|
+
const updated = formatDate(now, timezone);
|
|
274
354
|
fs.writeFileSync(filePath, buildDiaryFileSkeleton({ createdAt, updated }), "utf8");
|
|
275
355
|
}
|
|
276
356
|
|
|
@@ -349,6 +429,7 @@ function normalizeEntryPayload(entry) {
|
|
|
349
429
|
title: normalizeLineItem(payload.title),
|
|
350
430
|
body: normalizeBody(payload.body),
|
|
351
431
|
todoState: normalizeTodoState(payload.todoState, normalizeSection(payload.section)),
|
|
432
|
+
todoStartedAt: normalizeTodoClock(payload.todoStartedAt),
|
|
352
433
|
};
|
|
353
434
|
}
|
|
354
435
|
|
|
@@ -423,22 +504,49 @@ function isPlaceholderLine(line, section) {
|
|
|
423
504
|
|
|
424
505
|
function upsertTodoLine(lines, payload) {
|
|
425
506
|
const nextLines = Array.isArray(lines) ? [...lines] : [];
|
|
426
|
-
const desiredLine = `- [${TODO_STATE_MARKERS[payload.todoState]}] ${payload.text}`;
|
|
427
507
|
for (let index = 0; index < nextLines.length; index += 1) {
|
|
428
|
-
const
|
|
429
|
-
if (!
|
|
508
|
+
const parsed = parseTodoLine(nextLines[index]);
|
|
509
|
+
if (!parsed) {
|
|
430
510
|
continue;
|
|
431
511
|
}
|
|
432
|
-
if (
|
|
512
|
+
if (parsed.text !== payload.text) {
|
|
433
513
|
continue;
|
|
434
514
|
}
|
|
435
|
-
nextLines[index] =
|
|
515
|
+
nextLines[index] = buildTodoLine({
|
|
516
|
+
text: payload.text,
|
|
517
|
+
todoState: payload.todoState,
|
|
518
|
+
todoStartedAt: resolveTodoStartedAtForUpsert(parsed, payload),
|
|
519
|
+
});
|
|
436
520
|
return nextLines;
|
|
437
521
|
}
|
|
438
|
-
nextLines.push(
|
|
522
|
+
nextLines.push(buildTodoLine({
|
|
523
|
+
text: payload.text,
|
|
524
|
+
todoState: payload.todoState,
|
|
525
|
+
todoStartedAt: payload.todoStartedAt,
|
|
526
|
+
}));
|
|
439
527
|
return nextLines;
|
|
440
528
|
}
|
|
441
529
|
|
|
530
|
+
function resolveTodoStartedAtForUpsert(parsed, payload) {
|
|
531
|
+
const existingState = parsed?.todoState || "open";
|
|
532
|
+
const existingStartedAt = normalizeTodoClock(parsed?.todoStartedAt);
|
|
533
|
+
const payloadStartedAt = normalizeTodoClock(payload?.todoStartedAt);
|
|
534
|
+
const nextState = normalizeTodoState(payload?.todoState, "todo");
|
|
535
|
+
|
|
536
|
+
// Repeated writes against the same live open Todo should keep the original
|
|
537
|
+
// block start. But if the same text gets reopened after it was already done,
|
|
538
|
+
// the new open write represents a fresh block and must reset the captured
|
|
539
|
+
// start time instead of leaking the old finished block's timestamp.
|
|
540
|
+
if (nextState === "open") {
|
|
541
|
+
if (existingState === "open") {
|
|
542
|
+
return existingStartedAt || payloadStartedAt;
|
|
543
|
+
}
|
|
544
|
+
return payloadStartedAt || existingStartedAt;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return existingStartedAt || payloadStartedAt;
|
|
548
|
+
}
|
|
549
|
+
|
|
442
550
|
function upsertBulletLine(lines, payload) {
|
|
443
551
|
const nextLines = Array.isArray(lines) ? [...lines] : [];
|
|
444
552
|
for (const line of nextLines) {
|
|
@@ -531,6 +639,58 @@ function buildSectionLineText({ title, body }) {
|
|
|
531
639
|
return normalizedTitle || normalizedBody;
|
|
532
640
|
}
|
|
533
641
|
|
|
642
|
+
function buildTodoLine({ text, todoState = "open", todoStartedAt = "" }) {
|
|
643
|
+
const startMarker = buildTodoStartMarker(todoStartedAt);
|
|
644
|
+
return `- [${TODO_STATE_MARKERS[todoState]}] ${text}${startMarker}`;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function buildTodoStartMarker(value) {
|
|
648
|
+
const normalized = normalizeTodoClock(value);
|
|
649
|
+
return normalized ? ` <!-- codeksei-todo:start=${normalized} -->` : "";
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function parseTodoLine(line) {
|
|
653
|
+
const match = TODO_LINE_RE.exec(String(line || ""));
|
|
654
|
+
if (!match) {
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
const rawText = String(match[2] || "");
|
|
658
|
+
const startMarker = TODO_START_MARKER_RE.exec(rawText);
|
|
659
|
+
return {
|
|
660
|
+
todoState: String(match[1] || "").toLowerCase() === "x" ? "done" : "open",
|
|
661
|
+
text: normalizeLineItem(stripTodoMetadata(rawText)),
|
|
662
|
+
todoStartedAt: normalizeTodoClock(startMarker?.[1] || ""),
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function stripTodoMetadata(value) {
|
|
667
|
+
return String(value || "").replace(TODO_START_MARKER_RE, "").trim();
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function findTodoStartTimeInDiaryContent(content, { title = "", body = "" } = {}) {
|
|
671
|
+
const normalizedContent = normalizeFileEnding(content);
|
|
672
|
+
if (!normalizedContent.trim()) {
|
|
673
|
+
return "";
|
|
674
|
+
}
|
|
675
|
+
const range = findSectionRange(normalizedContent, SECTION_HEADINGS.todo);
|
|
676
|
+
if (!range) {
|
|
677
|
+
return "";
|
|
678
|
+
}
|
|
679
|
+
const targetText = buildSectionLineText({ title, body });
|
|
680
|
+
if (!targetText) {
|
|
681
|
+
return "";
|
|
682
|
+
}
|
|
683
|
+
const lines = extractSectionLines(normalizedContent.slice(range.contentStart, range.end), "todo");
|
|
684
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
685
|
+
const parsed = parseTodoLine(lines[index]);
|
|
686
|
+
if (!parsed || parsed.text !== targetText) {
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
return parsed.todoStartedAt;
|
|
690
|
+
}
|
|
691
|
+
return "";
|
|
692
|
+
}
|
|
693
|
+
|
|
534
694
|
function normalizeSection(value) {
|
|
535
695
|
const normalized = String(value || "").trim().toLowerCase();
|
|
536
696
|
switch (normalized) {
|
|
@@ -576,35 +736,21 @@ function normalizeLineItem(value) {
|
|
|
576
736
|
return normalizeBody(value).replace(/\s*\n+\s*/g, " ").replace(/\s{2,}/g, " ").trim();
|
|
577
737
|
}
|
|
578
738
|
|
|
579
|
-
function
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
function formatTime(date) {
|
|
589
|
-
return
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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");
|
|
739
|
+
function normalizeTodoClock(value) {
|
|
740
|
+
const normalized = normalizeLineItem(value);
|
|
741
|
+
return /^\d{2}:\d{2}$/u.test(normalized) ? normalized : "";
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function formatDate(date, timezone = LEGACY_TIMELINE_TIMEZONE) {
|
|
745
|
+
return formatDateInTimezone(date, timezone);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function formatTime(date, timezone = LEGACY_TIMELINE_TIMEZONE) {
|
|
749
|
+
return formatTimeInTimezone(date, timezone);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function formatDateTime(date, timezone = LEGACY_TIMELINE_TIMEZONE) {
|
|
753
|
+
return formatDateTimeInTimezone(date, timezone);
|
|
608
754
|
}
|
|
609
755
|
|
|
610
756
|
module.exports = {
|
|
@@ -616,5 +762,7 @@ module.exports = {
|
|
|
616
762
|
normalizeSection,
|
|
617
763
|
normalizeTodoState,
|
|
618
764
|
parseArgs,
|
|
765
|
+
parseTodoLine,
|
|
619
766
|
runDiaryWriteCommand,
|
|
767
|
+
resolveTodoDoneTimelineText,
|
|
620
768
|
};
|
package/src/app/note-sync-cli.js
CHANGED
|
@@ -119,8 +119,8 @@ function printNoteSyncHelp() {
|
|
|
119
119
|
"",
|
|
120
120
|
"示例:",
|
|
121
121
|
" npm run note:sync -- --project <slug> --section \"最近动作\" --text \"把微信 prompt 收口为更温柔的 chief-of-staff 风格\" --max-items 6",
|
|
122
|
-
" npm run note:sync -- --project <slug> --section \"当前状态\" --slot current-status --style paragraph --text \"
|
|
123
|
-
" npm run note:sync -- --path \"
|
|
122
|
+
" npm run note:sync -- --project <slug> --section \"当前状态\" --slot current-status --style paragraph --text \"当前 shared bridge 正常运行,默认入口稳定。\"",
|
|
123
|
+
" npm run note:sync -- --path \"/absolute/path/to/note.md\" --section \"当前定位\" --text \"默认先接住,再定向,再推进。\"",
|
|
124
124
|
].join("\n"));
|
|
125
125
|
}
|
|
126
126
|
|