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,201 @@
|
|
|
1
|
+
const {
|
|
2
|
+
ensureDurableNoteSections,
|
|
3
|
+
inspectDurableNoteRouting,
|
|
4
|
+
resolveDurableNoteRoute,
|
|
5
|
+
} = require("../core/durable-note-schema");
|
|
6
|
+
const { syncNoteFile } = require("../core/note-sync");
|
|
7
|
+
|
|
8
|
+
async function runNoteAutoCommand(config, args = process.argv.slice(4)) {
|
|
9
|
+
const options = parseNoteAutoArgs(args);
|
|
10
|
+
if (options.help) {
|
|
11
|
+
printNoteAutoHelp();
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const text = await resolveBody(options);
|
|
16
|
+
if (!text) {
|
|
17
|
+
throw new Error("note 内容不能为空,传 --text 或通过 stdin 输入");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const route = resolveDurableNoteRoute(config, options);
|
|
21
|
+
const schemaResult = ensureDurableNoteSections(route.filePath, route.sections);
|
|
22
|
+
const result = syncNoteFile({
|
|
23
|
+
filePath: route.filePath,
|
|
24
|
+
section: route.section,
|
|
25
|
+
text,
|
|
26
|
+
style: route.style,
|
|
27
|
+
slot: route.slot,
|
|
28
|
+
maxItems: route.maxItems,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const action = result.changed ? "updated" : "noop";
|
|
32
|
+
console.log(`note auto ${action}: ${result.filePath} [${route.family}:${route.kind} -> ${route.section}]`);
|
|
33
|
+
if (schemaResult.changed) {
|
|
34
|
+
console.log(`schema ensured: ${schemaResult.filePath} (${schemaResult.createdSections.join(", ")})`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function runNoteMaybeCommand(config, args = process.argv.slice(4)) {
|
|
39
|
+
const options = parseNoteAutoArgs(args);
|
|
40
|
+
if (options.help) {
|
|
41
|
+
printNoteMaybeHelp();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const inspection = inspectDurableNoteRouting(config, options);
|
|
46
|
+
if (options.json) {
|
|
47
|
+
console.log(JSON.stringify(inspection, null, 2));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
console.log(formatInspection(inspection));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseNoteAutoArgs(args) {
|
|
54
|
+
const options = {
|
|
55
|
+
help: false,
|
|
56
|
+
json: false,
|
|
57
|
+
project: "",
|
|
58
|
+
scope: "",
|
|
59
|
+
kind: "",
|
|
60
|
+
text: "",
|
|
61
|
+
useStdin: false,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
65
|
+
const arg = String(args[index] || "").trim();
|
|
66
|
+
if (!arg) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (arg === "--help" || arg === "-h") {
|
|
70
|
+
options.help = true;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (arg === "--json") {
|
|
74
|
+
options.json = true;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (arg === "--stdin") {
|
|
78
|
+
options.useStdin = true;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (!arg.startsWith("--")) {
|
|
82
|
+
throw new Error(`未知参数: ${arg}`);
|
|
83
|
+
}
|
|
84
|
+
const value = String(args[index + 1] || "");
|
|
85
|
+
if (!value || value.startsWith("--")) {
|
|
86
|
+
throw new Error(`参数缺少值: ${arg}`);
|
|
87
|
+
}
|
|
88
|
+
if (arg === "--project") {
|
|
89
|
+
options.project = value;
|
|
90
|
+
} else if (arg === "--scope") {
|
|
91
|
+
options.scope = value;
|
|
92
|
+
} else if (arg === "--kind") {
|
|
93
|
+
options.kind = value;
|
|
94
|
+
} else if (arg === "--text") {
|
|
95
|
+
options.text = value;
|
|
96
|
+
} else {
|
|
97
|
+
throw new Error(`未知参数: ${arg}`);
|
|
98
|
+
}
|
|
99
|
+
index += 1;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return options;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function resolveBody(options) {
|
|
106
|
+
const inline = String(options.text || "").trim();
|
|
107
|
+
if (inline) {
|
|
108
|
+
return inline;
|
|
109
|
+
}
|
|
110
|
+
if (!options.useStdin && process.stdin.isTTY) {
|
|
111
|
+
return "";
|
|
112
|
+
}
|
|
113
|
+
return readStdin();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function readStdin() {
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
let buffer = "";
|
|
119
|
+
process.stdin.setEncoding("utf8");
|
|
120
|
+
process.stdin.on("data", (chunk) => {
|
|
121
|
+
buffer += chunk;
|
|
122
|
+
});
|
|
123
|
+
process.stdin.on("end", () => resolve(buffer.trim()));
|
|
124
|
+
process.stdin.on("error", reject);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function formatInspection(inspection) {
|
|
129
|
+
if (inspection.mode === "overview") {
|
|
130
|
+
const lines = [
|
|
131
|
+
"durable note 路由概览:",
|
|
132
|
+
"",
|
|
133
|
+
`workspace: ${inspection.workspaceRoot}`,
|
|
134
|
+
`project kinds: ${inspection.project.kinds.join(", ") || "none"}`,
|
|
135
|
+
`tracked projects: ${inspection.project.availableProjects.join(", ") || "none"}`,
|
|
136
|
+
];
|
|
137
|
+
for (const [scope, info] of Object.entries(inspection.scopes || {})) {
|
|
138
|
+
lines.push("");
|
|
139
|
+
lines.push(`[${scope}] ${info.filePath}`);
|
|
140
|
+
lines.push(`kinds: ${info.kinds.join(", ") || "none"}`);
|
|
141
|
+
lines.push(`sections: ${info.sections.join(" / ") || "none"}`);
|
|
142
|
+
}
|
|
143
|
+
return lines.join("\n");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (inspection.mode === "family") {
|
|
147
|
+
return [
|
|
148
|
+
`durable note family: ${inspection.family}`,
|
|
149
|
+
`file: ${inspection.filePath}`,
|
|
150
|
+
`kinds: ${inspection.kinds.join(", ") || "none"}`,
|
|
151
|
+
`sections: ${inspection.sections.join(" / ") || "none"}`,
|
|
152
|
+
].join("\n");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return [
|
|
156
|
+
`durable note route: ${inspection.family}:${inspection.route.kind}`,
|
|
157
|
+
`file: ${inspection.filePath}`,
|
|
158
|
+
`section: ${inspection.route.section}`,
|
|
159
|
+
`style: ${inspection.route.style}`,
|
|
160
|
+
`slot: ${inspection.route.slot || "-"}`,
|
|
161
|
+
`maxItems: ${inspection.route.maxItems || 0}`,
|
|
162
|
+
].join("\n");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function printNoteAutoHelp() {
|
|
166
|
+
console.log([
|
|
167
|
+
"用法: npm run note:auto -- (--project <slug> | --scope <name>) --kind <kind> [--text \"内容\" | --stdin]",
|
|
168
|
+
"",
|
|
169
|
+
"说明:",
|
|
170
|
+
" 按 workspace 级 durable note schema 自动决定 file / section / style / slot。",
|
|
171
|
+
" 代码项目常用 --project;生活助理、灵感等 durable note 用 --scope。",
|
|
172
|
+
"",
|
|
173
|
+
"示例:",
|
|
174
|
+
" npm run note:auto -- --project <slug> --kind recent --text \"补了 note:auto / note:maybe 路由层\"",
|
|
175
|
+
" npm run note:auto -- --project <slug> --kind status --text \"当前已接上 durable note schema,下一步观察真实线程里的使用手感。\"",
|
|
176
|
+
" npm run note:auto -- --scope assistant --kind preference --text \"默认先接住,再定向,再推进;少 mirror,少催债式 check-in。\"",
|
|
177
|
+
" npm run note:auto -- --scope inspiration --kind idea --text \"做一个只在切换点发力的 transition mode,让主动提醒更像接线而不是催债。\"",
|
|
178
|
+
].join("\n"));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function printNoteMaybeHelp() {
|
|
182
|
+
console.log([
|
|
183
|
+
"用法: npm run note:maybe -- [--project <slug> | --scope <name>] [--kind <kind>] [--json]",
|
|
184
|
+
"",
|
|
185
|
+
"说明:",
|
|
186
|
+
" 只看 durable note 路由,不落盘。",
|
|
187
|
+
" 不传参数时列出当前 workspace 可用 scope、kinds 和 tracked projects。",
|
|
188
|
+
"",
|
|
189
|
+
"示例:",
|
|
190
|
+
" npm run note:maybe",
|
|
191
|
+
" npm run note:maybe -- --project <slug>",
|
|
192
|
+
" npm run note:maybe -- --scope assistant --kind preference",
|
|
193
|
+
" npm run note:maybe -- --scope inspiration --json",
|
|
194
|
+
].join("\n"));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = {
|
|
198
|
+
parseNoteAutoArgs,
|
|
199
|
+
runNoteAutoCommand,
|
|
200
|
+
runNoteMaybeCommand,
|
|
201
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
const {
|
|
2
|
+
resolveNoteSyncTarget,
|
|
3
|
+
syncNoteFile,
|
|
4
|
+
} = require("../core/note-sync");
|
|
5
|
+
|
|
6
|
+
async function runNoteSyncCommand(config, args = process.argv.slice(4)) {
|
|
7
|
+
const options = parseNoteSyncArgs(args);
|
|
8
|
+
if (options.help) {
|
|
9
|
+
printNoteSyncHelp();
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const text = await resolveBody(options);
|
|
14
|
+
if (!text) {
|
|
15
|
+
throw new Error("note 内容不能为空,传 --text 或通过 stdin 输入");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const target = resolveNoteSyncTarget(config, options);
|
|
19
|
+
const result = syncNoteFile({
|
|
20
|
+
filePath: target.filePath,
|
|
21
|
+
section: options.section,
|
|
22
|
+
text,
|
|
23
|
+
style: options.style,
|
|
24
|
+
slot: options.slot,
|
|
25
|
+
maxItems: options.maxItems,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const action = result.changed ? "updated" : "noop";
|
|
29
|
+
console.log(`note ${action}: ${result.filePath} [${options.section}]`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseNoteSyncArgs(args) {
|
|
33
|
+
const options = {
|
|
34
|
+
help: false,
|
|
35
|
+
project: "",
|
|
36
|
+
path: "",
|
|
37
|
+
section: "",
|
|
38
|
+
text: "",
|
|
39
|
+
style: "bullet",
|
|
40
|
+
slot: "",
|
|
41
|
+
maxItems: "",
|
|
42
|
+
useStdin: false,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
46
|
+
const arg = String(args[index] || "").trim();
|
|
47
|
+
if (!arg) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (arg === "--help" || arg === "-h") {
|
|
51
|
+
options.help = true;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (arg === "--stdin") {
|
|
55
|
+
options.useStdin = true;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!arg.startsWith("--")) {
|
|
59
|
+
throw new Error(`未知参数: ${arg}`);
|
|
60
|
+
}
|
|
61
|
+
const value = String(args[index + 1] || "");
|
|
62
|
+
if (!value || value.startsWith("--")) {
|
|
63
|
+
throw new Error(`参数缺少值: ${arg}`);
|
|
64
|
+
}
|
|
65
|
+
if (arg === "--project") {
|
|
66
|
+
options.project = value;
|
|
67
|
+
} else if (arg === "--path") {
|
|
68
|
+
options.path = value;
|
|
69
|
+
} else if (arg === "--section") {
|
|
70
|
+
options.section = value;
|
|
71
|
+
} else if (arg === "--text") {
|
|
72
|
+
options.text = value;
|
|
73
|
+
} else if (arg === "--style") {
|
|
74
|
+
options.style = value;
|
|
75
|
+
} else if (arg === "--slot") {
|
|
76
|
+
options.slot = value;
|
|
77
|
+
} else if (arg === "--max-items") {
|
|
78
|
+
options.maxItems = value;
|
|
79
|
+
} else {
|
|
80
|
+
throw new Error(`未知参数: ${arg}`);
|
|
81
|
+
}
|
|
82
|
+
index += 1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return options;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function resolveBody(options) {
|
|
89
|
+
const inline = String(options.text || "").trim();
|
|
90
|
+
if (inline) {
|
|
91
|
+
return inline;
|
|
92
|
+
}
|
|
93
|
+
if (!options.useStdin && process.stdin.isTTY) {
|
|
94
|
+
return "";
|
|
95
|
+
}
|
|
96
|
+
return readStdin();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function readStdin() {
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
let buffer = "";
|
|
102
|
+
process.stdin.setEncoding("utf8");
|
|
103
|
+
process.stdin.on("data", (chunk) => {
|
|
104
|
+
buffer += chunk;
|
|
105
|
+
});
|
|
106
|
+
process.stdin.on("end", () => resolve(buffer.trim()));
|
|
107
|
+
process.stdin.on("error", reject);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function printNoteSyncHelp() {
|
|
112
|
+
console.log([
|
|
113
|
+
"用法: npm run note:sync -- (--project <slug> | --path <path>) --section <标题> [--text \"内容\" | --stdin] [--style bullet|paragraph] [--slot <id>] [--max-items N]",
|
|
114
|
+
"",
|
|
115
|
+
"说明:",
|
|
116
|
+
" 轻量把一条 durable 摘要写回指定 note 的指定 section。",
|
|
117
|
+
" 默认 style 是 bullet;传 --slot 时会用受控 block 替换同一槽位的旧内容。",
|
|
118
|
+
" 不传 --slot 时会做轻量追加,并对相同内容去重。",
|
|
119
|
+
"",
|
|
120
|
+
"示例:",
|
|
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 \"当前默认主 workspace 是 Website,shared bridge 正常运行。\"",
|
|
123
|
+
" npm run note:sync -- --path \"项目/Codeksei 生活助理/README.md\" --section \"当前定位\" --text \"默认先接住,再定向,再推进,不做催债式主动提醒。\"",
|
|
124
|
+
].join("\n"));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
parseNoteSyncArgs,
|
|
129
|
+
runNoteSyncCommand,
|
|
130
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
const { collectProjectRadars, listTrackedProjects, loadProjectRadarConfig } = require("../core/project-radar");
|
|
2
|
+
|
|
3
|
+
async function runProjectRadarCommand(config) {
|
|
4
|
+
const options = parseProjectRadarArgs(process.argv.slice(4));
|
|
5
|
+
if (options.help) {
|
|
6
|
+
printProjectRadarHelp(config);
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (options.list) {
|
|
11
|
+
const radarConfig = loadProjectRadarConfig(config);
|
|
12
|
+
const tracked = listTrackedProjects(config);
|
|
13
|
+
if (options.json) {
|
|
14
|
+
console.log(JSON.stringify({
|
|
15
|
+
workspaceRoot: radarConfig.workspaceRoot,
|
|
16
|
+
configFile: radarConfig.configFile,
|
|
17
|
+
projects: tracked,
|
|
18
|
+
}, null, 2));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
printProjectList(radarConfig, tracked);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const result = collectProjectRadars(config, options);
|
|
26
|
+
if (options.json) {
|
|
27
|
+
console.log(JSON.stringify(result, null, 2));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
console.log(renderProjectRadarsText(result));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseProjectRadarArgs(args) {
|
|
34
|
+
const options = {
|
|
35
|
+
help: false,
|
|
36
|
+
list: false,
|
|
37
|
+
json: false,
|
|
38
|
+
project: "",
|
|
39
|
+
commits: 5,
|
|
40
|
+
changes: 20,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
44
|
+
const token = String(args[index] || "").trim();
|
|
45
|
+
if (!token) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (token === "--help" || token === "-h") {
|
|
49
|
+
options.help = true;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (token === "--list") {
|
|
53
|
+
options.list = true;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (token === "--json") {
|
|
57
|
+
options.json = true;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (!token.startsWith("--")) {
|
|
61
|
+
throw new Error(`未知参数: ${token}`);
|
|
62
|
+
}
|
|
63
|
+
const key = token.slice(2);
|
|
64
|
+
const value = String(args[index + 1] || "");
|
|
65
|
+
if (!value || value.startsWith("--")) {
|
|
66
|
+
throw new Error(`参数缺少值: ${token}`);
|
|
67
|
+
}
|
|
68
|
+
if (key === "project") {
|
|
69
|
+
options.project = value.trim();
|
|
70
|
+
} else if (key === "commits") {
|
|
71
|
+
options.commits = value.trim();
|
|
72
|
+
} else if (key === "changes") {
|
|
73
|
+
options.changes = value.trim();
|
|
74
|
+
} else {
|
|
75
|
+
throw new Error(`未知参数: ${token}`);
|
|
76
|
+
}
|
|
77
|
+
index += 1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return options;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function printProjectRadarHelp(config = {}) {
|
|
84
|
+
console.log([
|
|
85
|
+
"用法: npm run project:radar -- [--list] [--project <slug>] [--json] [--commits 5] [--changes 20]",
|
|
86
|
+
"",
|
|
87
|
+
"说明:",
|
|
88
|
+
" 默认从当前 workspace 的 .codex/code-projects.json 读取已跟踪代码项目。",
|
|
89
|
+
` 当前配置文件: ${config.projectRadarConfigFile || "(auto)"}`,
|
|
90
|
+
"",
|
|
91
|
+
"示例:",
|
|
92
|
+
" npm run project:radar -- --list",
|
|
93
|
+
" npm run project:radar -- --project <slug> --json",
|
|
94
|
+
" npm run project:radar -- --project engineering-issues --commits 8 --changes 30",
|
|
95
|
+
].join("\n"));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function printProjectList(radarConfig, trackedProjects) {
|
|
99
|
+
const lines = [
|
|
100
|
+
`workspace: ${radarConfig.workspaceRoot}`,
|
|
101
|
+
`config: ${radarConfig.configFile}`,
|
|
102
|
+
"",
|
|
103
|
+
"tracked projects:",
|
|
104
|
+
];
|
|
105
|
+
for (const project of trackedProjects) {
|
|
106
|
+
lines.push(`- ${project.slug} | ${project.title}`);
|
|
107
|
+
lines.push(` repo: ${project.repoRoot}`);
|
|
108
|
+
lines.push(` note: ${project.notePath}`);
|
|
109
|
+
}
|
|
110
|
+
console.log(lines.join("\n"));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function renderProjectRadarsText(result) {
|
|
114
|
+
const lines = [
|
|
115
|
+
`workspace: ${result.workspaceRoot}`,
|
|
116
|
+
`config: ${result.configFile}`,
|
|
117
|
+
`generatedAt: ${result.generatedAt}`,
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
for (const project of result.projects) {
|
|
121
|
+
lines.push("");
|
|
122
|
+
lines.push(`## ${project.slug} | ${project.title}`);
|
|
123
|
+
lines.push(`repo: ${project.repoRoot}`);
|
|
124
|
+
lines.push(`note: ${project.notePath || "(none)"}`);
|
|
125
|
+
if (project.timelineLabel) {
|
|
126
|
+
lines.push(`timeline: ${project.timelineLabel}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (project.readFirst.length) {
|
|
130
|
+
lines.push("readFirst:");
|
|
131
|
+
for (const file of project.readFirst) {
|
|
132
|
+
lines.push(`- [${file.kind}] ${file.path}${file.exists ? "" : " (missing)"}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!project.git.ok) {
|
|
137
|
+
lines.push(`git: ${project.git.message}`);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
lines.push(`branch: ${project.git.branch || "(unknown)"}`);
|
|
142
|
+
if (project.git.upstream) {
|
|
143
|
+
lines.push(`upstream: ${project.git.upstream} | ahead=${project.git.ahead} behind=${project.git.behind}`);
|
|
144
|
+
}
|
|
145
|
+
lines.push(
|
|
146
|
+
`dirty: ${project.git.dirty ? "yes" : "no"} | staged=${project.git.summary.staged} unstaged=${project.git.summary.unstaged} untracked=${project.git.summary.untracked} conflicted=${project.git.summary.conflicted}`
|
|
147
|
+
);
|
|
148
|
+
if (project.git.statusEntries.length) {
|
|
149
|
+
lines.push("changes:");
|
|
150
|
+
for (const entry of project.git.statusEntries) {
|
|
151
|
+
lines.push(`- ${entry.code} ${entry.path}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (project.git.recentCommits.length) {
|
|
155
|
+
lines.push("recentCommits:");
|
|
156
|
+
for (const commit of project.git.recentCommits) {
|
|
157
|
+
lines.push(`- ${commit.shortHash} ${commit.committedAt} ${commit.subject}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return lines.join("\n");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = { runProjectRadarCommand };
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
2
|
+
|
|
3
|
+
const { resolveSelectedAccount } = require("../adapters/channel/weixin/account-store");
|
|
4
|
+
const { loadPersistedContextTokens } = require("../adapters/channel/weixin/context-token-store");
|
|
5
|
+
const { ReminderQueueStore } = require("../adapters/channel/weixin/reminder-queue-store");
|
|
6
|
+
const { SessionStore } = require("../adapters/runtime/codex/session-store");
|
|
7
|
+
const { resolvePreferredSenderId } = require("../core/default-targets");
|
|
8
|
+
|
|
9
|
+
const DELAY_UNIT_MS = {
|
|
10
|
+
s: 1_000,
|
|
11
|
+
m: 60_000,
|
|
12
|
+
h: 60 * 60_000,
|
|
13
|
+
d: 24 * 60 * 60_000,
|
|
14
|
+
};
|
|
15
|
+
const LOCAL_TIMEZONE_OFFSET = "+08:00";
|
|
16
|
+
|
|
17
|
+
async function runReminderWriteCommand(config) {
|
|
18
|
+
const args = process.argv.slice(4);
|
|
19
|
+
const options = parseArgs(args);
|
|
20
|
+
const body = await resolveBody(options);
|
|
21
|
+
if (!body) {
|
|
22
|
+
throw new Error("提醒内容不能为空,传 --text 或通过 stdin 输入");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const dueAtMs = resolveDueAtMs(options);
|
|
26
|
+
if (!Number.isFinite(dueAtMs) || dueAtMs <= Date.now()) {
|
|
27
|
+
throw new Error("缺少有效时间,使用 --delay 30s|10m|1h30m|2d4h20m 或 --at 2026-04-07T21:30+08:00");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const account = resolveSelectedAccount(config);
|
|
31
|
+
const sessionStore = new SessionStore({ filePath: config.sessionsFile });
|
|
32
|
+
const senderId = resolvePreferredSenderId({
|
|
33
|
+
config,
|
|
34
|
+
accountId: account.accountId,
|
|
35
|
+
explicitUser: options.user,
|
|
36
|
+
sessionStore,
|
|
37
|
+
});
|
|
38
|
+
if (!senderId) {
|
|
39
|
+
throw new Error("无法确定 reminder 的微信用户,传 --user 或先让唯一活跃用户和 bot 聊过一次");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const contextTokens = loadPersistedContextTokens(config, account.accountId);
|
|
43
|
+
const contextToken = String(contextTokens[senderId] || "").trim();
|
|
44
|
+
if (!contextToken) {
|
|
45
|
+
throw new Error(`找不到 ${senderId} 的 context_token,先让这个用户和 bot 聊过一次`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const queue = new ReminderQueueStore({ filePath: config.reminderQueueFile });
|
|
49
|
+
const reminder = queue.enqueue({
|
|
50
|
+
id: crypto.randomUUID(),
|
|
51
|
+
accountId: account.accountId,
|
|
52
|
+
senderId,
|
|
53
|
+
contextToken,
|
|
54
|
+
text: body,
|
|
55
|
+
dueAtMs,
|
|
56
|
+
createdAt: new Date().toISOString(),
|
|
57
|
+
});
|
|
58
|
+
console.log(`reminder queued: ${reminder.id}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseArgs(args) {
|
|
62
|
+
const options = {
|
|
63
|
+
delay: "",
|
|
64
|
+
at: "",
|
|
65
|
+
text: "",
|
|
66
|
+
user: "",
|
|
67
|
+
useStdin: false,
|
|
68
|
+
};
|
|
69
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
70
|
+
const arg = args[index];
|
|
71
|
+
if (arg === "--delay") {
|
|
72
|
+
options.delay = String(args[index + 1] || "");
|
|
73
|
+
index += 1;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (arg === "--at") {
|
|
77
|
+
options.at = String(args[index + 1] || "");
|
|
78
|
+
index += 1;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (arg === "--text") {
|
|
82
|
+
options.text = String(args[index + 1] || "");
|
|
83
|
+
index += 1;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (arg === "--user") {
|
|
87
|
+
options.user = String(args[index + 1] || "");
|
|
88
|
+
index += 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (arg === "--stdin") {
|
|
92
|
+
options.useStdin = true;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
throw new Error(`未知参数: ${arg}`);
|
|
96
|
+
}
|
|
97
|
+
return options;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resolveDueAtMs(options) {
|
|
101
|
+
const delayMs = parseDelay(options.delay);
|
|
102
|
+
const scheduledAtMs = parseAbsoluteTime(options.at);
|
|
103
|
+
if (delayMs && scheduledAtMs) {
|
|
104
|
+
throw new Error("--delay 和 --at 不能同时传");
|
|
105
|
+
}
|
|
106
|
+
if (delayMs) {
|
|
107
|
+
return Date.now() + delayMs;
|
|
108
|
+
}
|
|
109
|
+
if (scheduledAtMs) {
|
|
110
|
+
return scheduledAtMs;
|
|
111
|
+
}
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function parseDelay(rawValue) {
|
|
116
|
+
const normalized = String(rawValue || "").trim().toLowerCase();
|
|
117
|
+
if (!normalized) {
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let totalMs = 0;
|
|
122
|
+
let index = 0;
|
|
123
|
+
while (index < normalized.length) {
|
|
124
|
+
while (index < normalized.length && /\s/.test(normalized[index])) {
|
|
125
|
+
index += 1;
|
|
126
|
+
}
|
|
127
|
+
if (index >= normalized.length) {
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const match = normalized.slice(index).match(/^(\d+)\s*([smhd])/);
|
|
132
|
+
if (!match) {
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const amount = Number.parseInt(match[1], 10);
|
|
137
|
+
const unitMs = DELAY_UNIT_MS[match[2]] || 0;
|
|
138
|
+
if (!Number.isFinite(amount) || amount <= 0 || !unitMs) {
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
totalMs += amount * unitMs;
|
|
143
|
+
index += match[0].length;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return totalMs > 0 ? totalMs : 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function parseAbsoluteTime(rawValue) {
|
|
150
|
+
const normalized = String(rawValue || "").trim();
|
|
151
|
+
if (!normalized) {
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const normalizedIso = normalizeAbsoluteTimeString(normalized);
|
|
156
|
+
const parsed = Date.parse(normalizedIso);
|
|
157
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function normalizeAbsoluteTimeString(value) {
|
|
161
|
+
const normalized = String(value || "").trim();
|
|
162
|
+
if (!normalized) {
|
|
163
|
+
return "";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (/([zZ]|[+-]\d{2}:\d{2})$/.test(normalized)) {
|
|
167
|
+
return normalized.replace(" ", "T");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const dateTimeMatch = normalized.match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}(?::\d{2})?)$/);
|
|
171
|
+
if (dateTimeMatch) {
|
|
172
|
+
return `${dateTimeMatch[1]}T${dateTimeMatch[2]}${LOCAL_TIMEZONE_OFFSET}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const dateOnlyMatch = normalized.match(/^(\d{4}-\d{2}-\d{2})$/);
|
|
176
|
+
if (dateOnlyMatch) {
|
|
177
|
+
return `${dateOnlyMatch[1]}T09:00:00${LOCAL_TIMEZONE_OFFSET}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return normalized;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function resolveBody(options) {
|
|
184
|
+
const inlineText = normalizeBody(options.text);
|
|
185
|
+
if (inlineText) {
|
|
186
|
+
return inlineText;
|
|
187
|
+
}
|
|
188
|
+
if (!options.useStdin && process.stdin.isTTY) {
|
|
189
|
+
return "";
|
|
190
|
+
}
|
|
191
|
+
return normalizeBody(await readStdin());
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function readStdin() {
|
|
195
|
+
return new Promise((resolve, reject) => {
|
|
196
|
+
let buffer = "";
|
|
197
|
+
process.stdin.setEncoding("utf8");
|
|
198
|
+
process.stdin.on("data", (chunk) => {
|
|
199
|
+
buffer += chunk;
|
|
200
|
+
});
|
|
201
|
+
process.stdin.on("end", () => resolve(buffer));
|
|
202
|
+
process.stdin.on("error", reject);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function normalizeBody(value) {
|
|
207
|
+
return String(value || "").replace(/\r\n/g, "\n").trim();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = { runReminderWriteCommand };
|