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,134 @@
|
|
|
1
|
+
const { buildReview, writeReview } = require("../core/review");
|
|
2
|
+
|
|
3
|
+
async function runReviewCommand(config, kind, args = process.argv.slice(4)) {
|
|
4
|
+
const options = parseReviewArgs(args, kind);
|
|
5
|
+
if (options.help) {
|
|
6
|
+
printReviewHelp(kind);
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
if (options.stdout) {
|
|
10
|
+
const review = await buildReview(config, kind, options);
|
|
11
|
+
process.stdout.write(review.draft.periodTitle + "\n");
|
|
12
|
+
process.stdout.write(review.draft.windowFacts.map((line) => `- ${line}`).join("\n") + "\n");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const result = await writeReview(config, kind, options);
|
|
16
|
+
const action = result.changed ? "updated" : "noop";
|
|
17
|
+
const semantic = result.semanticUsed ? " semantic=hybrid" : "";
|
|
18
|
+
console.log(`review ${action}: ${result.filePath} [${kind}:${result.periodLabel}] diaries=${result.diaryCount}${semantic}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseReviewArgs(args, kind) {
|
|
22
|
+
const options = {
|
|
23
|
+
help: false,
|
|
24
|
+
stdout: false,
|
|
25
|
+
deterministic: false,
|
|
26
|
+
date: "",
|
|
27
|
+
week: "",
|
|
28
|
+
month: "",
|
|
29
|
+
model: "",
|
|
30
|
+
};
|
|
31
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
32
|
+
const arg = String(args[index] || "").trim();
|
|
33
|
+
if (!arg) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (arg === "--help" || arg === "-h") {
|
|
37
|
+
options.help = true;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (arg === "--stdout") {
|
|
41
|
+
options.stdout = true;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (arg === "--deterministic") {
|
|
45
|
+
options.deterministic = true;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (!arg.startsWith("--")) {
|
|
49
|
+
throw new Error(`未知参数: ${arg}`);
|
|
50
|
+
}
|
|
51
|
+
const value = String(args[index + 1] || "");
|
|
52
|
+
if (!value || value.startsWith("--")) {
|
|
53
|
+
throw new Error(`参数缺少值: ${arg}`);
|
|
54
|
+
}
|
|
55
|
+
if (arg === "--date") {
|
|
56
|
+
options.date = value;
|
|
57
|
+
} else if (arg === "--week") {
|
|
58
|
+
options.week = value;
|
|
59
|
+
} else if (arg === "--month") {
|
|
60
|
+
options.month = value;
|
|
61
|
+
} else if (arg === "--model") {
|
|
62
|
+
options.model = value;
|
|
63
|
+
} else {
|
|
64
|
+
throw new Error(`未知参数: ${arg}`);
|
|
65
|
+
}
|
|
66
|
+
index += 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (kind === "weekly" && options.month) {
|
|
70
|
+
throw new Error("review:weekly 不支持 --month");
|
|
71
|
+
}
|
|
72
|
+
if (kind === "monthly" && options.week) {
|
|
73
|
+
throw new Error("review:monthly 不支持 --week");
|
|
74
|
+
}
|
|
75
|
+
if (kind === "nightly" && (options.week || options.month)) {
|
|
76
|
+
throw new Error("review:nightly 只支持 --date");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return options;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function printReviewHelp(kind) {
|
|
83
|
+
const nightly = [
|
|
84
|
+
"用法: npm run review:nightly -- [--date YYYY-MM-DD] [--stdout] [--deterministic] [--model <id>]",
|
|
85
|
+
"",
|
|
86
|
+
"说明:",
|
|
87
|
+
" 从当前 diary 真相源生成一份 Codeksei 睡前收口。",
|
|
88
|
+
" 默认按 Asia/Shanghai 的当前日期推断今天,并给周/月复盘提供更轻的日级原料。",
|
|
89
|
+
" 默认走 hybrid:脚本保骨架,Codex 负责结构化语义提炼;失败时自动回退。",
|
|
90
|
+
"",
|
|
91
|
+
"示例:",
|
|
92
|
+
" npm run review:nightly",
|
|
93
|
+
" npm run review:nightly -- --date 2026-04-10",
|
|
94
|
+
].join("\n");
|
|
95
|
+
|
|
96
|
+
const weekly = [
|
|
97
|
+
"用法: npm run review:weekly -- [--week YYYY-Www] [--date YYYY-MM-DD] [--stdout] [--deterministic] [--model <id>]",
|
|
98
|
+
"",
|
|
99
|
+
"说明:",
|
|
100
|
+
" 从当前 diary 真相源生成一份 Codeksei 生活助理周复盘。",
|
|
101
|
+
" 默认按 Asia/Shanghai 的当前日期推断本周(周一到周日)。",
|
|
102
|
+
" 默认走 hybrid:脚本保骨架,Codex 负责结构化语义提炼;失败时自动回退。",
|
|
103
|
+
"",
|
|
104
|
+
"示例:",
|
|
105
|
+
" npm run review:weekly",
|
|
106
|
+
" npm run review:weekly -- --week 2026-W15",
|
|
107
|
+
" npm run review:weekly -- --date 2026-04-11",
|
|
108
|
+
].join("\n");
|
|
109
|
+
|
|
110
|
+
const monthly = [
|
|
111
|
+
"用法: npm run review:monthly -- [--month YYYY-MM] [--date YYYY-MM-DD] [--stdout] [--deterministic] [--model <id>]",
|
|
112
|
+
"",
|
|
113
|
+
"说明:",
|
|
114
|
+
" 从当前 diary 真相源生成一份 Codeksei 生活助理月复盘。",
|
|
115
|
+
" 默认按 Asia/Shanghai 的当前日期推断本月。",
|
|
116
|
+
" 默认走 hybrid:脚本保骨架,Codex 负责结构化语义提炼;失败时自动回退。",
|
|
117
|
+
"",
|
|
118
|
+
"示例:",
|
|
119
|
+
" npm run review:monthly",
|
|
120
|
+
" npm run review:monthly -- --month 2026-04",
|
|
121
|
+
" npm run review:monthly -- --date 2026-04-11",
|
|
122
|
+
].join("\n");
|
|
123
|
+
|
|
124
|
+
if (kind === "nightly") {
|
|
125
|
+
console.log(nightly);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
console.log(kind === "weekly" ? weekly : monthly);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = {
|
|
132
|
+
parseReviewArgs,
|
|
133
|
+
runReviewCommand,
|
|
134
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
2
|
+
|
|
3
|
+
const { resolveSelectedAccount } = require("../adapters/channel/weixin/account-store");
|
|
4
|
+
const { SessionStore } = require("../adapters/runtime/codex/session-store");
|
|
5
|
+
const { PACKAGE_NAME, readPrefixedEnv } = require("../core/branding");
|
|
6
|
+
const { resolvePreferredSenderId, resolvePreferredWorkspaceRoot } = require("../core/default-targets");
|
|
7
|
+
const { SystemMessageQueueStore } = require("../core/system-message-queue-store");
|
|
8
|
+
|
|
9
|
+
const DEFAULT_MIN_INTERVAL_MS = 3 * 60_000;
|
|
10
|
+
const DEFAULT_MAX_INTERVAL_MS = 60 * 60_000;
|
|
11
|
+
const INTERNAL_CHECKIN_TRIGGER_TEMPLATE = "Decide whether to reach out to %USER% now. You may stay silent, send one short WeChat message, update diary/timeline, or take another useful action. If no user-visible message should be sent, output exactly SILENT. If you do send a message, output only the message text.";
|
|
12
|
+
|
|
13
|
+
async function runSystemCheckinPoller(config) {
|
|
14
|
+
const account = resolveSelectedAccount(config);
|
|
15
|
+
const queue = new SystemMessageQueueStore({ filePath: config.systemMessageQueueFile });
|
|
16
|
+
const sessionStore = new SessionStore({ filePath: config.sessionsFile });
|
|
17
|
+
const target = resolvePollerTarget({ config, account, sessionStore });
|
|
18
|
+
const minIntervalMs = readIntervalMs(readPrefixedEnv(process.env, "CHECKIN_MIN_INTERVAL_MS"), DEFAULT_MIN_INTERVAL_MS);
|
|
19
|
+
const maxIntervalMs = Math.max(
|
|
20
|
+
minIntervalMs,
|
|
21
|
+
readIntervalMs(readPrefixedEnv(process.env, "CHECKIN_MAX_INTERVAL_MS"), DEFAULT_MAX_INTERVAL_MS)
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
console.log(`[${PACKAGE_NAME}] checkin poller ready user=${target.senderId} workspace=${target.workspaceRoot}`);
|
|
25
|
+
console.log(`[${PACKAGE_NAME}] checkin interval range ${Math.round(minIntervalMs / 60000)}m-${Math.round(maxIntervalMs / 60000)}m`);
|
|
26
|
+
|
|
27
|
+
while (true) {
|
|
28
|
+
const delayMs = pickRandomDelayMs(minIntervalMs, maxIntervalMs);
|
|
29
|
+
const wakeAt = new Date(Date.now() + delayMs).toISOString();
|
|
30
|
+
console.log(`[${PACKAGE_NAME}] next checkin in ${Math.round(delayMs / 60000)}m at ${wakeAt}`);
|
|
31
|
+
await sleep(delayMs);
|
|
32
|
+
|
|
33
|
+
if (queue.hasPendingForAccount(account.accountId)) {
|
|
34
|
+
console.log(`[${PACKAGE_NAME}] checkin skipped: pending system message still in queue`);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const queued = queue.enqueue({
|
|
39
|
+
id: crypto.randomUUID(),
|
|
40
|
+
accountId: account.accountId,
|
|
41
|
+
senderId: target.senderId,
|
|
42
|
+
workspaceRoot: target.workspaceRoot,
|
|
43
|
+
text: buildCheckinTrigger(config),
|
|
44
|
+
createdAt: new Date().toISOString(),
|
|
45
|
+
});
|
|
46
|
+
console.log(`[${PACKAGE_NAME}] checkin queued id=${queued.id}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolvePollerTarget({ config, account, sessionStore }) {
|
|
51
|
+
const senderId = resolvePreferredSenderId({
|
|
52
|
+
config,
|
|
53
|
+
accountId: account.accountId,
|
|
54
|
+
explicitUser: readPrefixedEnv(process.env, "CHECKIN_USER_ID") || "",
|
|
55
|
+
sessionStore,
|
|
56
|
+
});
|
|
57
|
+
const workspaceRoot = resolvePreferredWorkspaceRoot({
|
|
58
|
+
config,
|
|
59
|
+
accountId: account.accountId,
|
|
60
|
+
senderId,
|
|
61
|
+
explicitWorkspace: readPrefixedEnv(process.env, "CHECKIN_WORKSPACE") || "",
|
|
62
|
+
sessionStore,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (!senderId) {
|
|
66
|
+
throw new Error("无法确定 checkin poller 的微信用户,先配置 CODEKSEI_CHECKIN_USER_ID(或旧的 CYBERBOSS_CHECKIN_USER_ID)或让唯一活跃用户先和 bot 聊过一次");
|
|
67
|
+
}
|
|
68
|
+
if (!workspaceRoot) {
|
|
69
|
+
throw new Error("无法确定 checkin poller 的 workspace,先设置 CODEKSEI_WORKSPACE_ROOT(或旧的 CYBERBOSS_WORKSPACE_ROOT)");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { senderId, workspaceRoot };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readIntervalMs(rawValue, fallback) {
|
|
76
|
+
const parsed = Number.parseInt(String(rawValue || ""), 10);
|
|
77
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function pickRandomDelayMs(minIntervalMs, maxIntervalMs) {
|
|
81
|
+
if (maxIntervalMs <= minIntervalMs) {
|
|
82
|
+
return minIntervalMs;
|
|
83
|
+
}
|
|
84
|
+
return minIntervalMs + Math.floor(Math.random() * (maxIntervalMs - minIntervalMs + 1));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizeText(value) {
|
|
88
|
+
return typeof value === "string" ? value.trim() : "";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function sleep(ms) {
|
|
92
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function buildCheckinTrigger(config) {
|
|
96
|
+
const userName = normalizeText(config?.userName) || "用户";
|
|
97
|
+
return INTERNAL_CHECKIN_TRIGGER_TEMPLATE.replace("%USER%", userName);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = { runSystemCheckinPoller };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
|
|
5
|
+
const { resolveSelectedAccount } = require("../adapters/channel/weixin/account-store");
|
|
6
|
+
const { loadPersistedContextTokens } = require("../adapters/channel/weixin/context-token-store");
|
|
7
|
+
const { SessionStore } = require("../adapters/runtime/codex/session-store");
|
|
8
|
+
const { resolvePreferredSenderId, resolvePreferredWorkspaceRoot } = require("../core/default-targets");
|
|
9
|
+
const { SystemMessageQueueStore } = require("../core/system-message-queue-store");
|
|
10
|
+
|
|
11
|
+
async function runSystemSendCommand(config) {
|
|
12
|
+
const options = parseSystemSendArgs(process.argv.slice(4));
|
|
13
|
+
if (options.help) {
|
|
14
|
+
printSystemSendHelp();
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const account = resolveSelectedAccount(config);
|
|
19
|
+
const sessionStore = new SessionStore({ filePath: config.sessionsFile });
|
|
20
|
+
const senderId = resolvePreferredSenderId({
|
|
21
|
+
config,
|
|
22
|
+
accountId: account.accountId,
|
|
23
|
+
explicitUser: options.user,
|
|
24
|
+
sessionStore,
|
|
25
|
+
});
|
|
26
|
+
const text = options.text;
|
|
27
|
+
const workspaceRoot = resolvePreferredWorkspaceRoot({
|
|
28
|
+
config,
|
|
29
|
+
accountId: account.accountId,
|
|
30
|
+
senderId,
|
|
31
|
+
explicitWorkspace: options.workspace,
|
|
32
|
+
sessionStore,
|
|
33
|
+
});
|
|
34
|
+
if (!senderId || !text || !workspaceRoot) {
|
|
35
|
+
printSystemSendHelp();
|
|
36
|
+
throw new Error("system send 缺少必要参数");
|
|
37
|
+
}
|
|
38
|
+
if (!path.isAbsolute(workspaceRoot)) {
|
|
39
|
+
throw new Error(`workspace 必须是绝对路径: ${workspaceRoot}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let workspaceStats = null;
|
|
43
|
+
try {
|
|
44
|
+
workspaceStats = fs.statSync(workspaceRoot);
|
|
45
|
+
} catch {
|
|
46
|
+
throw new Error(`workspace 不存在: ${workspaceRoot}`);
|
|
47
|
+
}
|
|
48
|
+
if (!workspaceStats.isDirectory()) {
|
|
49
|
+
throw new Error(`workspace 不是目录: ${workspaceRoot}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const contextTokens = loadPersistedContextTokens(config, account.accountId);
|
|
53
|
+
if (!contextTokens[senderId]) {
|
|
54
|
+
throw new Error(`找不到用户 ${senderId} 的 context token,先让这个用户和 bot 聊过一次`);
|
|
55
|
+
}
|
|
56
|
+
const queue = new SystemMessageQueueStore({ filePath: config.systemMessageQueueFile });
|
|
57
|
+
const queued = queue.enqueue({
|
|
58
|
+
id: crypto.randomUUID(),
|
|
59
|
+
accountId: account.accountId,
|
|
60
|
+
senderId,
|
|
61
|
+
workspaceRoot,
|
|
62
|
+
text,
|
|
63
|
+
createdAt: new Date().toISOString(),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
console.log(`system message queued: ${queued.id}`);
|
|
67
|
+
console.log(`user: ${queued.senderId}`);
|
|
68
|
+
console.log(`workspace: ${queued.workspaceRoot}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parseSystemSendArgs(args) {
|
|
72
|
+
const options = {
|
|
73
|
+
help: false,
|
|
74
|
+
user: "",
|
|
75
|
+
text: "",
|
|
76
|
+
workspace: "",
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
80
|
+
const token = String(args[index] || "").trim();
|
|
81
|
+
if (!token) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (token === "--help" || token === "-h") {
|
|
86
|
+
options.help = true;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!token.startsWith("--")) {
|
|
91
|
+
throw new Error(`未知参数: ${token}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const key = token.slice(2);
|
|
95
|
+
const value = String(args[index + 1] || "");
|
|
96
|
+
if (!value || value.startsWith("--")) {
|
|
97
|
+
throw new Error(`参数缺少值: ${token}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (key === "user") {
|
|
101
|
+
options.user = value.trim();
|
|
102
|
+
} else if (key === "text") {
|
|
103
|
+
options.text = value.trim();
|
|
104
|
+
} else if (key === "workspace") {
|
|
105
|
+
options.workspace = value.trim();
|
|
106
|
+
} else {
|
|
107
|
+
throw new Error(`未知参数: ${token}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
index += 1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return options;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function printSystemSendHelp() {
|
|
117
|
+
console.log(`
|
|
118
|
+
用法: npm run system:send -- --text "<message>" [--user <wechat_user_id>] [--workspace /绝对路径]
|
|
119
|
+
|
|
120
|
+
示例:
|
|
121
|
+
npm run system:send -- --text "提醒她今天早点睡" --workspace "$(pwd)"
|
|
122
|
+
`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function normalizeWorkspacePath(value) {
|
|
126
|
+
return typeof value === "string" ? value.trim() : "";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = { runSystemSendCommand };
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
2
|
+
|
|
3
|
+
const LOCAL_TIMEZONE_OFFSET = "+08:00";
|
|
4
|
+
|
|
5
|
+
async function runTimelineEventCommand(timelineIntegration, args = process.argv.slice(4)) {
|
|
6
|
+
const options = parseTimelineEventArgs(args);
|
|
7
|
+
if (options.help) {
|
|
8
|
+
printTimelineEventHelp();
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const note = await resolveNote(options);
|
|
13
|
+
const writeArgs = buildTimelineEventWriteArgs(options, note);
|
|
14
|
+
await timelineIntegration.runSubcommand("write", writeArgs);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseTimelineEventArgs(args) {
|
|
18
|
+
const options = {
|
|
19
|
+
help: false,
|
|
20
|
+
date: "",
|
|
21
|
+
start: "",
|
|
22
|
+
end: "",
|
|
23
|
+
title: "",
|
|
24
|
+
note: "",
|
|
25
|
+
categoryId: "",
|
|
26
|
+
subcategoryId: "",
|
|
27
|
+
eventNodeId: "",
|
|
28
|
+
mode: "merge",
|
|
29
|
+
eventId: "",
|
|
30
|
+
finalize: false,
|
|
31
|
+
useStdin: false,
|
|
32
|
+
tags: [],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
36
|
+
const token = String(args[index] || "").trim();
|
|
37
|
+
if (!token) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (token === "--help" || token === "-h") {
|
|
41
|
+
options.help = true;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (token === "--stdin") {
|
|
45
|
+
options.useStdin = true;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (token === "--finalize") {
|
|
49
|
+
options.finalize = true;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const value = String(args[index + 1] || "").trim();
|
|
54
|
+
if (!value || value.startsWith("--")) {
|
|
55
|
+
throw new Error(`参数缺少值: ${token}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
switch (token) {
|
|
59
|
+
case "--date":
|
|
60
|
+
options.date = value;
|
|
61
|
+
break;
|
|
62
|
+
case "--start":
|
|
63
|
+
options.start = value;
|
|
64
|
+
break;
|
|
65
|
+
case "--end":
|
|
66
|
+
options.end = value;
|
|
67
|
+
break;
|
|
68
|
+
case "--title":
|
|
69
|
+
options.title = value;
|
|
70
|
+
break;
|
|
71
|
+
case "--note":
|
|
72
|
+
options.note = value;
|
|
73
|
+
break;
|
|
74
|
+
case "--category":
|
|
75
|
+
options.categoryId = value;
|
|
76
|
+
break;
|
|
77
|
+
case "--subcategory":
|
|
78
|
+
options.subcategoryId = value;
|
|
79
|
+
break;
|
|
80
|
+
case "--event-node":
|
|
81
|
+
options.eventNodeId = value;
|
|
82
|
+
break;
|
|
83
|
+
case "--mode":
|
|
84
|
+
options.mode = value;
|
|
85
|
+
break;
|
|
86
|
+
case "--id":
|
|
87
|
+
options.eventId = value;
|
|
88
|
+
break;
|
|
89
|
+
case "--tag":
|
|
90
|
+
options.tags.push(value);
|
|
91
|
+
break;
|
|
92
|
+
default:
|
|
93
|
+
throw new Error(`未知参数: ${token}`);
|
|
94
|
+
}
|
|
95
|
+
index += 1;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return options;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function resolveNote(options) {
|
|
102
|
+
const inline = normalizeText(options.note);
|
|
103
|
+
if (inline) {
|
|
104
|
+
return inline;
|
|
105
|
+
}
|
|
106
|
+
if (!options.useStdin && process.stdin.isTTY) {
|
|
107
|
+
return "";
|
|
108
|
+
}
|
|
109
|
+
return normalizeText(await readStdin());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function buildTimelineEventWriteArgs(options, note = "") {
|
|
113
|
+
const date = normalizeDate(options.date);
|
|
114
|
+
if (!date) {
|
|
115
|
+
throw new Error("缺少有效日期,使用 --date YYYY-MM-DD");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const title = normalizeText(options.title);
|
|
119
|
+
if (!title) {
|
|
120
|
+
throw new Error("缺少标题,使用 --title \"事件标题\"");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const mode = normalizeText(options.mode) || "merge";
|
|
124
|
+
if (!["merge", "replace"].includes(mode)) {
|
|
125
|
+
throw new Error("不支持的写入模式,只能用 --mode merge|replace");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!normalizeText(options.eventNodeId) && !normalizeText(options.subcategoryId)) {
|
|
129
|
+
throw new Error("缺少分类信息,至少传 --event-node 或 --subcategory");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const startAt = normalizeTimelineEventTimestamp(date, options.start, "--start");
|
|
133
|
+
const endAt = normalizeTimelineEventTimestamp(date, options.end, "--end");
|
|
134
|
+
validateEventRange({ date, startAt, endAt });
|
|
135
|
+
|
|
136
|
+
const event = {
|
|
137
|
+
id: normalizeText(options.eventId) || `evt_${crypto.randomUUID()}`,
|
|
138
|
+
startAt,
|
|
139
|
+
endAt,
|
|
140
|
+
title,
|
|
141
|
+
};
|
|
142
|
+
if (note) {
|
|
143
|
+
event.note = note;
|
|
144
|
+
}
|
|
145
|
+
if (normalizeText(options.categoryId)) {
|
|
146
|
+
event.categoryId = normalizeText(options.categoryId);
|
|
147
|
+
}
|
|
148
|
+
if (normalizeText(options.subcategoryId)) {
|
|
149
|
+
event.subcategoryId = normalizeText(options.subcategoryId);
|
|
150
|
+
}
|
|
151
|
+
if (normalizeText(options.eventNodeId)) {
|
|
152
|
+
event.eventNodeId = normalizeText(options.eventNodeId);
|
|
153
|
+
}
|
|
154
|
+
if (Array.isArray(options.tags) && options.tags.length) {
|
|
155
|
+
event.tags = options.tags.map((tag) => normalizeText(tag)).filter(Boolean);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const payload = {
|
|
159
|
+
date,
|
|
160
|
+
events: [event],
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const args = [
|
|
164
|
+
"--date",
|
|
165
|
+
date,
|
|
166
|
+
"--mode",
|
|
167
|
+
mode,
|
|
168
|
+
"--json",
|
|
169
|
+
JSON.stringify(payload),
|
|
170
|
+
];
|
|
171
|
+
if (options.finalize) {
|
|
172
|
+
args.push("--finalize");
|
|
173
|
+
}
|
|
174
|
+
return args;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function normalizeDate(value) {
|
|
178
|
+
const normalized = normalizeText(value);
|
|
179
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(normalized) ? normalized : "";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function normalizeTimelineEventTimestamp(date, value, flagName) {
|
|
183
|
+
const normalized = normalizeText(value);
|
|
184
|
+
if (!normalized) {
|
|
185
|
+
throw new Error(`缺少时间,使用 ${flagName} HH:mm 或完整时间戳`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (/^\d{2}:\d{2}$/.test(normalized)) {
|
|
189
|
+
return `${date}T${normalized}:00${LOCAL_TIMEZONE_OFFSET}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (/^\d{2}:\d{2}:\d{2}$/.test(normalized)) {
|
|
193
|
+
return `${date}T${normalized}${LOCAL_TIMEZONE_OFFSET}`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(:\d{2})?$/.test(normalized)) {
|
|
197
|
+
return `${normalized.replace(" ", "T")}${LOCAL_TIMEZONE_OFFSET}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (/^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2})?([zZ]|[+-]\d{2}:\d{2})$/.test(normalized)) {
|
|
201
|
+
return normalized.replace(" ", "T");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
throw new Error(`不支持的时间格式: ${flagName}=${normalized}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function validateEventRange({ date, startAt, endAt }) {
|
|
208
|
+
if (!startAt.startsWith(`${date}T`) || !endAt.startsWith(`${date}T`)) {
|
|
209
|
+
throw new Error("timeline:event 要求 start/end 都落在 --date 对应这一天内");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const startMs = Date.parse(startAt);
|
|
213
|
+
const endMs = Date.parse(endAt);
|
|
214
|
+
if (!Number.isFinite(startMs) || !Number.isFinite(endMs)) {
|
|
215
|
+
throw new Error("无法解析 start/end 时间");
|
|
216
|
+
}
|
|
217
|
+
if (startMs >= endMs) {
|
|
218
|
+
throw new Error("--end 必须晚于 --start,且 timeline:event 不支持跨天事件");
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function readStdin() {
|
|
223
|
+
return new Promise((resolve, reject) => {
|
|
224
|
+
let buffer = "";
|
|
225
|
+
process.stdin.setEncoding("utf8");
|
|
226
|
+
process.stdin.on("data", (chunk) => {
|
|
227
|
+
buffer += chunk;
|
|
228
|
+
});
|
|
229
|
+
process.stdin.on("end", () => resolve(buffer));
|
|
230
|
+
process.stdin.on("error", reject);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function normalizeText(value) {
|
|
235
|
+
return String(value || "").replace(/\r\n/g, "\n").trim();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function printTimelineEventHelp() {
|
|
239
|
+
console.log(`
|
|
240
|
+
用法: npm run timeline:event -- --date YYYY-MM-DD --start HH:mm --end HH:mm --title "标题" (--event-node <id> | --subcategory <id>) [其他参数]
|
|
241
|
+
|
|
242
|
+
用途:
|
|
243
|
+
- 写单条时间轴事件,不必手写 raw JSON
|
|
244
|
+
- 适合把一个明确的时间块快速追加进当天 timeline
|
|
245
|
+
- 如果要一次写多条事件,或直接替换整批 events,继续用 timeline:write
|
|
246
|
+
|
|
247
|
+
常用参数:
|
|
248
|
+
--date YYYY-MM-DD
|
|
249
|
+
--start HH:mm | 完整时间戳
|
|
250
|
+
--end HH:mm | 完整时间戳
|
|
251
|
+
--title "事件标题"
|
|
252
|
+
--note "详细备注" 可选;也可用 --stdin 从标准输入读 note
|
|
253
|
+
--event-node <id> 与 taxonomy 里的 eventNode 对应
|
|
254
|
+
--subcategory <id> 不传 event-node 时至少提供它
|
|
255
|
+
--category <id> subcategory 无法自动反推时建议一起传
|
|
256
|
+
--tag <text> 可重复传多次
|
|
257
|
+
--mode merge|replace 默认 merge
|
|
258
|
+
--finalize 按 timeline-for-agent 的 finalize 语义写入
|
|
259
|
+
|
|
260
|
+
示例:
|
|
261
|
+
npm run timeline:event -- --date 2026-04-10 --start 09:30 --end 10:15 --title "看 Codeksei 提交历史" --subcategory work.dev --category work --note "为了补日记和时间线先核对最近改动。"
|
|
262
|
+
@'
|
|
263
|
+
补充背景和为什么要记录这段。
|
|
264
|
+
'@ | npm run timeline:event -- --date 2026-04-10 --start 10:20 --end 10:45 --title "整理营养师笔记结构" --subcategory study.reading --stdin
|
|
265
|
+
`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
module.exports = {
|
|
269
|
+
runTimelineEventCommand,
|
|
270
|
+
parseTimelineEventArgs,
|
|
271
|
+
buildTimelineEventWriteArgs,
|
|
272
|
+
normalizeTimelineEventTimestamp,
|
|
273
|
+
};
|