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,46 @@
|
|
|
1
|
+
const { spawn } = require("child_process");
|
|
2
|
+
const { readPrefixedEnv } = require("../src/core/branding");
|
|
3
|
+
const { resolveCodexWorkspaceRoot } = require("../src/core/workspace-alias");
|
|
4
|
+
const {
|
|
5
|
+
listenUrl,
|
|
6
|
+
buildSpawnInvocation,
|
|
7
|
+
ensureSharedAppServer,
|
|
8
|
+
resolveBoundThread,
|
|
9
|
+
} = require("./shared-common");
|
|
10
|
+
|
|
11
|
+
async function main() {
|
|
12
|
+
const workspaceRoot = readPrefixedEnv(process.env, "WORKSPACE_ROOT") || process.cwd();
|
|
13
|
+
await ensureSharedAppServer();
|
|
14
|
+
const { threadId, workspaceRoot: resolvedWorkspaceRoot } = resolveBoundThread(workspaceRoot);
|
|
15
|
+
// Keep the session bound to the canonical workspace root, but launch the
|
|
16
|
+
// local Codex client from the ASCII alias so desktop attach does not
|
|
17
|
+
// reintroduce the non-ASCII workspace header bug on Windows.
|
|
18
|
+
const runtimeWorkspaceRoot = resolveCodexWorkspaceRoot(resolvedWorkspaceRoot);
|
|
19
|
+
const spawnSpec = buildSpawnInvocation(readPrefixedEnv(process.env, "CODEX_COMMAND") || "codex", [
|
|
20
|
+
"resume",
|
|
21
|
+
threadId,
|
|
22
|
+
"--remote",
|
|
23
|
+
listenUrl,
|
|
24
|
+
"-C",
|
|
25
|
+
runtimeWorkspaceRoot,
|
|
26
|
+
...process.argv.slice(2),
|
|
27
|
+
]);
|
|
28
|
+
const child = spawn(spawnSpec.command, spawnSpec.args, {
|
|
29
|
+
stdio: "inherit",
|
|
30
|
+
shell: false,
|
|
31
|
+
windowsHide: true,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
child.on("exit", (code, signal) => {
|
|
35
|
+
if (signal) {
|
|
36
|
+
process.kill(process.pid, signal);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
process.exit(code ?? 0);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
main().catch((error) => {
|
|
44
|
+
console.error(error.message || String(error));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const {
|
|
2
|
+
listenUrl,
|
|
3
|
+
ensureManagedAppServer,
|
|
4
|
+
ensureManagedBridge,
|
|
5
|
+
ensureManagedSupervisor,
|
|
6
|
+
} = require("./shared-common");
|
|
7
|
+
const { readPrefixedEnv } = require("../src/core/branding");
|
|
8
|
+
|
|
9
|
+
function parseIntervalMinutes() {
|
|
10
|
+
for (const rawArg of process.argv.slice(2)) {
|
|
11
|
+
if (!rawArg.startsWith("--interval-minutes=")) {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
const value = Number.parseInt(rawArg.slice("--interval-minutes=".length), 10);
|
|
15
|
+
if (Number.isInteger(value) && value >= 1) {
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const value = Number.parseInt(
|
|
20
|
+
String(readPrefixedEnv(process.env, "SHARED_WATCHDOG_INTERVAL_MINUTES") || "5"),
|
|
21
|
+
10
|
|
22
|
+
);
|
|
23
|
+
return Number.isInteger(value) && value >= 1 ? value : 5;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function main() {
|
|
27
|
+
const appServer = await ensureManagedAppServer({ restartUnhealthy: true });
|
|
28
|
+
const appServerPidLabel = appServer.pid ? ` pid=${appServer.pid}` : "";
|
|
29
|
+
console.log(`shared app-server ${appServer.status}${appServerPidLabel} listen=${listenUrl}`);
|
|
30
|
+
|
|
31
|
+
const bridge = await ensureManagedBridge({ restartUnhealthy: true });
|
|
32
|
+
console.log(`shared codeksei ${bridge.status} pid=${bridge.pid}`);
|
|
33
|
+
|
|
34
|
+
const supervisor = await ensureManagedSupervisor({ intervalMinutes: parseIntervalMinutes() });
|
|
35
|
+
console.log(`shared supervisor ${supervisor.status} pid=${supervisor.pid}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
main().catch((error) => {
|
|
39
|
+
console.error(error.message || String(error));
|
|
40
|
+
process.exit(1);
|
|
41
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const http = require("http");
|
|
2
|
+
const {
|
|
3
|
+
listenUrl,
|
|
4
|
+
appServerPidFile,
|
|
5
|
+
bridgePidFile,
|
|
6
|
+
supervisorPidFile,
|
|
7
|
+
watchdogStateFile,
|
|
8
|
+
readPidFile,
|
|
9
|
+
isPidAlive,
|
|
10
|
+
readSharedBridgeHealth,
|
|
11
|
+
resolveReadyAppServerPid,
|
|
12
|
+
readJsonFile,
|
|
13
|
+
} = require("./shared-common");
|
|
14
|
+
|
|
15
|
+
async function main() {
|
|
16
|
+
const ready = await checkReadyz();
|
|
17
|
+
const readyAppServerPid = ready ? await resolveReadyAppServerPid() : 0;
|
|
18
|
+
const bridgeHealth = readSharedBridgeHealth();
|
|
19
|
+
const watchdogState = readJsonFile(watchdogStateFile) || {};
|
|
20
|
+
console.log(`listen=${listenUrl}`);
|
|
21
|
+
printPidState("shared_supervisor_pid", supervisorPidFile);
|
|
22
|
+
printPidState("shared_app_server_pid", appServerPidFile, readyAppServerPid);
|
|
23
|
+
printPidState("shared_codeksei_pid", bridgePidFile);
|
|
24
|
+
printPidState("shared_cyberboss_pid", bridgePidFile);
|
|
25
|
+
console.log(`shared_bridge_heartbeat=${bridgeHealth.classification.status}`);
|
|
26
|
+
console.log(`shared_bridge_heartbeat_at=${bridgeHealth.classification.updatedAt || "missing"}`);
|
|
27
|
+
console.log(`shared_watchdog_last_run=${normalizeText(watchdogState.lastRunAt) || "missing"}`);
|
|
28
|
+
console.log(`shared_watchdog_last_result=${normalizeText(watchdogState.result) || "missing"}`);
|
|
29
|
+
console.log(`readyz=${ready ? "ok" : "down"}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function printPidState(label, filePath, fallbackPid = 0) {
|
|
33
|
+
const pid = readPidFile(filePath) || fallbackPid;
|
|
34
|
+
if (!pid) {
|
|
35
|
+
console.log(`${label}=missing`);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (!isPidAlive(pid)) {
|
|
39
|
+
console.log(`${label}=stale`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
console.log(`${label}=${pid}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function checkReadyz() {
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
const req = http.get(
|
|
48
|
+
{
|
|
49
|
+
hostname: "127.0.0.1",
|
|
50
|
+
port: new URL(listenUrl).port,
|
|
51
|
+
path: "/readyz",
|
|
52
|
+
timeout: 600,
|
|
53
|
+
},
|
|
54
|
+
(res) => {
|
|
55
|
+
res.resume();
|
|
56
|
+
resolve(res.statusCode >= 200 && res.statusCode < 300);
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
req.on("error", () => resolve(false));
|
|
60
|
+
req.on("timeout", () => {
|
|
61
|
+
req.destroy();
|
|
62
|
+
resolve(false);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeText(value) {
|
|
68
|
+
return typeof value === "string" ? value.trim() : "";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
main().catch((error) => {
|
|
72
|
+
console.error(error.message || String(error));
|
|
73
|
+
process.exit(1);
|
|
74
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
const {
|
|
2
|
+
ensureLogDir,
|
|
3
|
+
supervisorPidFile,
|
|
4
|
+
readPidFile,
|
|
5
|
+
writePidFile,
|
|
6
|
+
removePidFileIfMatches,
|
|
7
|
+
isPidAlive,
|
|
8
|
+
readProcessCommandLine,
|
|
9
|
+
} = require("./shared-common");
|
|
10
|
+
const { runWatchdogOnce } = require("./shared-watchdog");
|
|
11
|
+
const { readPrefixedEnv } = require("../src/core/branding");
|
|
12
|
+
|
|
13
|
+
const DEFAULT_INTERVAL_MINUTES = 5;
|
|
14
|
+
|
|
15
|
+
let shuttingDown = false;
|
|
16
|
+
let lastLoggedSignature = "";
|
|
17
|
+
|
|
18
|
+
function parseIntervalMinutes(argv) {
|
|
19
|
+
for (const rawArg of argv) {
|
|
20
|
+
if (!rawArg.startsWith("--interval-minutes=")) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const value = Number.parseInt(rawArg.slice("--interval-minutes=".length), 10);
|
|
24
|
+
if (Number.isInteger(value) && value >= 1) {
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const fromEnv = Number.parseInt(
|
|
30
|
+
String(readPrefixedEnv(process.env, "SHARED_WATCHDOG_INTERVAL_MINUTES") || ""),
|
|
31
|
+
10
|
|
32
|
+
);
|
|
33
|
+
if (Number.isInteger(fromEnv) && fromEnv >= 1) {
|
|
34
|
+
return fromEnv;
|
|
35
|
+
}
|
|
36
|
+
return DEFAULT_INTERVAL_MINUTES;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function formatErrorMessage(error) {
|
|
40
|
+
if (error instanceof Error) {
|
|
41
|
+
return error.message || error.stack || String(error);
|
|
42
|
+
}
|
|
43
|
+
return String(error || "unknown error");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function sleep(ms) {
|
|
47
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function buildStateSignature(state) {
|
|
51
|
+
const actions = Array.isArray(state?.actions) ? state.actions.join("|") : "";
|
|
52
|
+
const error = typeof state?.error === "string" ? state.error.trim() : "";
|
|
53
|
+
const readyz = state?.after?.appServer?.ready ? "ok" : "down";
|
|
54
|
+
const heartbeat = state?.after?.bridge?.heartbeatStatus || "missing";
|
|
55
|
+
return [state?.result || "unknown", readyz, heartbeat, actions, error].join("|");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function logLine(message) {
|
|
59
|
+
const timestamp = new Date().toISOString();
|
|
60
|
+
console.log(`[${timestamp}] ${message}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function ensureSingleInstance() {
|
|
64
|
+
const existingPid = readPidFile(supervisorPidFile);
|
|
65
|
+
if (!existingPid || existingPid === process.pid || !isPidAlive(existingPid)) {
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const commandLine = readProcessCommandLine(existingPid);
|
|
70
|
+
if (String(commandLine).toLowerCase().includes("shared-supervisor.js")) {
|
|
71
|
+
return existingPid;
|
|
72
|
+
}
|
|
73
|
+
throw new Error(`refusing to reuse shared supervisor pid=${existingPid}: unexpected command line`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function installSignalHandlers() {
|
|
77
|
+
const shutdown = (signal) => {
|
|
78
|
+
if (shuttingDown) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
shuttingDown = true;
|
|
82
|
+
logLine(`signal=${signal} shutting_down=true`);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP", "SIGBREAK"]) {
|
|
86
|
+
process.on(signal, () => shutdown(signal));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
process.on("exit", () => {
|
|
90
|
+
removePidFileIfMatches(supervisorPidFile, process.pid);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function main() {
|
|
95
|
+
ensureLogDir();
|
|
96
|
+
const existingPid = ensureSingleInstance();
|
|
97
|
+
if (existingPid) {
|
|
98
|
+
logLine(`already_running pid=${existingPid}`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const intervalMinutes = parseIntervalMinutes(process.argv.slice(2));
|
|
103
|
+
const intervalMs = intervalMinutes * 60_000;
|
|
104
|
+
writePidFile(supervisorPidFile, process.pid);
|
|
105
|
+
installSignalHandlers();
|
|
106
|
+
|
|
107
|
+
// Keep one long-lived background process per desktop session so healthy
|
|
108
|
+
// periods do not require Task Scheduler to spawn a fresh watchdog script
|
|
109
|
+
// every few minutes. Recovery still uses the same watchdog logic, but the
|
|
110
|
+
// idle path is now a sleeping supervisor rather than repeated task launches.
|
|
111
|
+
logLine(`started pid=${process.pid} interval_minutes=${intervalMinutes}`);
|
|
112
|
+
|
|
113
|
+
while (!shuttingDown) {
|
|
114
|
+
try {
|
|
115
|
+
const state = await runWatchdogOnce({ shouldPrintSummary: false });
|
|
116
|
+
const signature = buildStateSignature(state);
|
|
117
|
+
if (signature !== lastLoggedSignature || state.result !== "healthy") {
|
|
118
|
+
const actions = Array.isArray(state.actions) && state.actions.length
|
|
119
|
+
? ` actions=${state.actions.join(" | ")}`
|
|
120
|
+
: "";
|
|
121
|
+
const error = state.error ? ` error=${state.error}` : "";
|
|
122
|
+
logLine(`result=${state.result} readyz=${state.after?.appServer?.ready ? "ok" : "down"} bridge=${state.after?.bridge?.heartbeatStatus || "missing"}${actions}${error}`);
|
|
123
|
+
lastLoggedSignature = signature;
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
logLine(`loop_error=${formatErrorMessage(error)}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!shuttingDown) {
|
|
130
|
+
await sleep(intervalMs);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
removePidFileIfMatches(supervisorPidFile, process.pid);
|
|
135
|
+
logLine(`stopped pid=${process.pid}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
main().catch((error) => {
|
|
139
|
+
logLine(`fatal=${formatErrorMessage(error)}`);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[ValidateSet("Start", "Watchdog", "Supervisor")]
|
|
3
|
+
[string]$Mode = "Watchdog",
|
|
4
|
+
[int]$IntervalMinutes = 5
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
$ErrorActionPreference = "Stop"
|
|
8
|
+
|
|
9
|
+
$repoRoot = Split-Path -Parent $PSScriptRoot
|
|
10
|
+
Set-Location -LiteralPath $repoRoot
|
|
11
|
+
|
|
12
|
+
$stateDir = if ($env:CODEKSEI_STATE_DIR) {
|
|
13
|
+
$env:CODEKSEI_STATE_DIR
|
|
14
|
+
} elseif ($env:CYBERBOSS_STATE_DIR) {
|
|
15
|
+
$env:CYBERBOSS_STATE_DIR
|
|
16
|
+
} else {
|
|
17
|
+
$newStateDir = Join-Path $env:USERPROFILE ".codeksei"
|
|
18
|
+
$legacyStateDir = Join-Path $env:USERPROFILE ".cyberboss"
|
|
19
|
+
if ((Test-Path -LiteralPath $newStateDir) -or -not (Test-Path -LiteralPath $legacyStateDir)) {
|
|
20
|
+
$newStateDir
|
|
21
|
+
} else {
|
|
22
|
+
$legacyStateDir
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
$logDir = Join-Path $stateDir "logs"
|
|
26
|
+
$null = New-Item -ItemType Directory -Force -Path $logDir
|
|
27
|
+
|
|
28
|
+
$logFile = if ($Mode -eq "Start") {
|
|
29
|
+
Join-Path $logDir "shared-task-start.log"
|
|
30
|
+
} elseif ($Mode -eq "Supervisor") {
|
|
31
|
+
Join-Path $logDir "shared-task-supervisor.log"
|
|
32
|
+
} else {
|
|
33
|
+
Join-Path $logDir "shared-task-watchdog.log"
|
|
34
|
+
}
|
|
35
|
+
$supervisorOutputLogFile = if ($Mode -eq "Supervisor") {
|
|
36
|
+
Join-Path $logDir "shared-supervisor.log"
|
|
37
|
+
} else {
|
|
38
|
+
$null
|
|
39
|
+
}
|
|
40
|
+
$errorLogFile = if ($Mode -eq "Supervisor") {
|
|
41
|
+
Join-Path $logDir "shared-supervisor.stderr.log"
|
|
42
|
+
} else {
|
|
43
|
+
$null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
$node = (Get-Command node -ErrorAction Stop).Source
|
|
47
|
+
$scriptPath = if ($Mode -eq "Start") {
|
|
48
|
+
Join-Path $repoRoot "scripts\shared-start.js"
|
|
49
|
+
} elseif ($Mode -eq "Supervisor") {
|
|
50
|
+
Join-Path $repoRoot "scripts\shared-supervisor.js"
|
|
51
|
+
} else {
|
|
52
|
+
Join-Path $repoRoot "scripts\shared-watchdog.js"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
$nodeArgs = @($scriptPath)
|
|
56
|
+
if ($Mode -eq "Supervisor" -or $Mode -eq "Start") {
|
|
57
|
+
$nodeArgs += "--interval-minutes=$IntervalMinutes"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
$startedAt = Get-Date -Format "yyyy-MM-ddTHH:mm:ssK"
|
|
61
|
+
"[$startedAt] mode=$Mode starting" | Out-File -FilePath $logFile -Encoding utf8 -Append
|
|
62
|
+
|
|
63
|
+
if ($Mode -eq "Supervisor") {
|
|
64
|
+
# Start the long-lived supervisor as a detached hidden process. Running it
|
|
65
|
+
# inline under the task shell keeps it attached to the outer PowerShell/cmd
|
|
66
|
+
# host, so closing or recycling that host can propagate SIGHUP and also
|
|
67
|
+
# surface transient console windows. We only want the scheduler to launch
|
|
68
|
+
# the supervisor, not to be the supervisor's lifetime owner.
|
|
69
|
+
$process = Start-Process `
|
|
70
|
+
-FilePath $node `
|
|
71
|
+
-ArgumentList $nodeArgs `
|
|
72
|
+
-WorkingDirectory $repoRoot `
|
|
73
|
+
-WindowStyle Hidden `
|
|
74
|
+
-RedirectStandardOutput $supervisorOutputLogFile `
|
|
75
|
+
-RedirectStandardError $errorLogFile `
|
|
76
|
+
-PassThru
|
|
77
|
+
"started_pid=$($process.Id)" | Out-File -FilePath $logFile -Encoding utf8 -Append
|
|
78
|
+
$exitCode = 0
|
|
79
|
+
} else {
|
|
80
|
+
& $node @nodeArgs 2>&1 | Out-File -FilePath $logFile -Encoding utf8 -Append
|
|
81
|
+
$exitCode = $LASTEXITCODE
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
$endedAt = Get-Date -Format "yyyy-MM-ddTHH:mm:ssK"
|
|
85
|
+
"[$endedAt] mode=$Mode exit=$exitCode" | Out-File -FilePath $logFile -Encoding utf8 -Append
|
|
86
|
+
|
|
87
|
+
exit $exitCode
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const dotenv = require("dotenv");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const {
|
|
5
|
+
ensureCompatHomeEnv,
|
|
6
|
+
ensureStateDirectory,
|
|
7
|
+
listEnvFileCandidates,
|
|
8
|
+
} = require("../src/core/branding");
|
|
9
|
+
|
|
10
|
+
const ALERT_COOLDOWN_MS = 10 * 60_000;
|
|
11
|
+
|
|
12
|
+
function ensureDefaultStateDirectory() {
|
|
13
|
+
ensureStateDirectory();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function loadEnv() {
|
|
17
|
+
ensureDefaultStateDirectory();
|
|
18
|
+
const candidates = listEnvFileCandidates();
|
|
19
|
+
for (const envPath of candidates) {
|
|
20
|
+
if (!fs.existsSync(envPath)) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
dotenv.config({ path: envPath });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
dotenv.config();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function ensureRuntimeEnv() {
|
|
30
|
+
ensureCompatHomeEnv({ fallbackRoot: path.resolve(__dirname, "..") });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
loadEnv();
|
|
34
|
+
ensureRuntimeEnv();
|
|
35
|
+
|
|
36
|
+
const { createWeixinChannelAdapter } = require("../src/adapters/channel/weixin");
|
|
37
|
+
const { resolveSelectedAccount } = require("../src/adapters/channel/weixin/account-store");
|
|
38
|
+
const { loadPersistedContextTokens } = require("../src/adapters/channel/weixin/context-token-store");
|
|
39
|
+
const { SessionStore } = require("../src/adapters/runtime/codex/session-store");
|
|
40
|
+
const { readConfig } = require("../src/core/config");
|
|
41
|
+
const { resolvePreferredSenderId, resolvePreferredWorkspaceRoot } = require("../src/core/default-targets");
|
|
42
|
+
const {
|
|
43
|
+
appServerLogFile,
|
|
44
|
+
appServerPidFile,
|
|
45
|
+
bridgeLogFile,
|
|
46
|
+
bridgePidFile,
|
|
47
|
+
ensureLogDir,
|
|
48
|
+
ensureManagedAppServer,
|
|
49
|
+
ensureManagedBridge,
|
|
50
|
+
readJsonFile,
|
|
51
|
+
readPidFile,
|
|
52
|
+
readSharedBridgeHealth,
|
|
53
|
+
resolveReadyAppServerPid,
|
|
54
|
+
watchdogStateFile,
|
|
55
|
+
writeJsonFile,
|
|
56
|
+
} = require("./shared-common");
|
|
57
|
+
|
|
58
|
+
async function runWatchdogOnce({ shouldPrintSummary = true } = {}) {
|
|
59
|
+
const config = readConfig();
|
|
60
|
+
ensureLogDir();
|
|
61
|
+
const previousState = readJsonFile(watchdogStateFile) || {};
|
|
62
|
+
const before = await collectHealth();
|
|
63
|
+
const actions = [];
|
|
64
|
+
let result = "healthy";
|
|
65
|
+
let errorMessage = "";
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const appServer = await ensureManagedAppServer({ restartUnhealthy: true });
|
|
69
|
+
if (appServer.status !== "already_running") {
|
|
70
|
+
actions.push(`shared app-server ${appServer.status} pid=${appServer.pid}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const bridge = await ensureManagedBridge({ restartUnhealthy: true });
|
|
74
|
+
if (bridge.status !== "already_running") {
|
|
75
|
+
actions.push(`shared codeksei ${bridge.status} pid=${bridge.pid}`);
|
|
76
|
+
}
|
|
77
|
+
} catch (error) {
|
|
78
|
+
result = "failed";
|
|
79
|
+
errorMessage = formatErrorMessage(error);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const after = await collectHealth();
|
|
83
|
+
if (result !== "failed") {
|
|
84
|
+
result = actions.length ? "recovered" : "healthy";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const nextState = {
|
|
88
|
+
lastRunAt: new Date().toISOString(),
|
|
89
|
+
result,
|
|
90
|
+
actions,
|
|
91
|
+
error: errorMessage,
|
|
92
|
+
before,
|
|
93
|
+
after,
|
|
94
|
+
lastAlertAt: normalizeText(previousState.lastAlertAt),
|
|
95
|
+
lastAlertSignature: normalizeText(previousState.lastAlertSignature),
|
|
96
|
+
lastNotification: previousState.lastNotification || null,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const alert = buildAlert({ result, actions, errorMessage, after, config });
|
|
100
|
+
if (alert && shouldSendAlert(previousState, alert.signature)) {
|
|
101
|
+
const notification = await sendVisibleAlert(config, alert.text);
|
|
102
|
+
nextState.lastAlertAt = new Date().toISOString();
|
|
103
|
+
nextState.lastAlertSignature = alert.signature;
|
|
104
|
+
nextState.lastNotification = {
|
|
105
|
+
kind: alert.kind,
|
|
106
|
+
sent: notification.sent,
|
|
107
|
+
reason: normalizeText(notification.reason),
|
|
108
|
+
senderId: normalizeText(notification.senderId),
|
|
109
|
+
workspaceRoot: normalizeText(notification.workspaceRoot),
|
|
110
|
+
sentAt: new Date().toISOString(),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
writeJsonFile(watchdogStateFile, nextState);
|
|
115
|
+
if (shouldPrintSummary) {
|
|
116
|
+
printSummary(nextState);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return nextState;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function main() {
|
|
123
|
+
const state = await runWatchdogOnce({ shouldPrintSummary: true });
|
|
124
|
+
if (state.result === "failed") {
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function collectHealth() {
|
|
130
|
+
const appServerReadyPid = await resolveReadyAppServerPid();
|
|
131
|
+
const bridge = readSharedBridgeHealth();
|
|
132
|
+
return {
|
|
133
|
+
appServer: {
|
|
134
|
+
ready: Boolean(appServerReadyPid),
|
|
135
|
+
readyPid: appServerReadyPid,
|
|
136
|
+
pidFromFile: readPidFile(appServerPidFile),
|
|
137
|
+
},
|
|
138
|
+
bridge: {
|
|
139
|
+
pid: bridge.pid,
|
|
140
|
+
alive: bridge.alive,
|
|
141
|
+
healthy: bridge.healthy,
|
|
142
|
+
heartbeatStatus: bridge.classification.status,
|
|
143
|
+
heartbeatUpdatedAt: normalizeText(bridge.classification.updatedAt),
|
|
144
|
+
lastError: normalizeText(bridge.heartbeat?.lastError),
|
|
145
|
+
consecutiveFailures: Number(bridge.heartbeat?.consecutiveFailures || 0),
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function buildAlert({ result, actions, errorMessage, after, config }) {
|
|
151
|
+
if (result === "recovered") {
|
|
152
|
+
return {
|
|
153
|
+
kind: "recovered",
|
|
154
|
+
signature: `recovered:${actions.join("|")}`,
|
|
155
|
+
text: [
|
|
156
|
+
"后台守护刚自动恢复了共享链路。",
|
|
157
|
+
`动作: ${actions.join(";")}`,
|
|
158
|
+
`workspace: ${normalizeText(config.workspaceRoot) || "(unknown)"}`,
|
|
159
|
+
`readyz: ${after.appServer.ready ? "ok" : "down"}`,
|
|
160
|
+
"现在可以继续直接发微信消息。",
|
|
161
|
+
].join("\n"),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (result === "failed") {
|
|
166
|
+
return {
|
|
167
|
+
kind: "failed",
|
|
168
|
+
signature: [
|
|
169
|
+
"failed",
|
|
170
|
+
errorMessage,
|
|
171
|
+
after.appServer.ready ? "ready" : "down",
|
|
172
|
+
after.bridge.heartbeatStatus,
|
|
173
|
+
String(after.bridge.consecutiveFailures),
|
|
174
|
+
].join("|"),
|
|
175
|
+
text: [
|
|
176
|
+
"后台守护刚发现共享链路异常,但这轮自动恢复没有完全成功。",
|
|
177
|
+
`readyz: ${after.appServer.ready ? "ok" : "down"}`,
|
|
178
|
+
`bridge: ${after.bridge.heartbeatStatus}`,
|
|
179
|
+
after.bridge.lastError ? `bridge error: ${after.bridge.lastError}` : "",
|
|
180
|
+
errorMessage ? `watchdog error: ${errorMessage}` : "",
|
|
181
|
+
`workspace: ${normalizeText(config.workspaceRoot) || "(unknown)"}`,
|
|
182
|
+
`日志: ${appServerLogFile} | ${bridgeLogFile}`,
|
|
183
|
+
].filter(Boolean).join("\n"),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function shouldSendAlert(previousState, signature) {
|
|
191
|
+
const normalizedSignature = normalizeText(signature);
|
|
192
|
+
if (!normalizedSignature) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
const previousSignature = normalizeText(previousState.lastAlertSignature);
|
|
196
|
+
const previousAlertAtMs = Date.parse(normalizeText(previousState.lastAlertAt));
|
|
197
|
+
if (normalizedSignature !== previousSignature) {
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
if (!Number.isFinite(previousAlertAtMs)) {
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
return Date.now() - previousAlertAtMs >= ALERT_COOLDOWN_MS;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function sendVisibleAlert(config, text) {
|
|
207
|
+
try {
|
|
208
|
+
const account = resolveSelectedAccount(config);
|
|
209
|
+
const sessionStore = new SessionStore({ filePath: config.sessionsFile });
|
|
210
|
+
const senderId = resolvePreferredSenderId({
|
|
211
|
+
config,
|
|
212
|
+
accountId: account.accountId,
|
|
213
|
+
sessionStore,
|
|
214
|
+
});
|
|
215
|
+
const workspaceRoot = resolvePreferredWorkspaceRoot({
|
|
216
|
+
config,
|
|
217
|
+
accountId: account.accountId,
|
|
218
|
+
senderId,
|
|
219
|
+
sessionStore,
|
|
220
|
+
});
|
|
221
|
+
const contextToken = loadPersistedContextTokens(config, account.accountId)?.[senderId] || "";
|
|
222
|
+
if (!senderId || !contextToken) {
|
|
223
|
+
return {
|
|
224
|
+
sent: false,
|
|
225
|
+
reason: "missing_sender_or_context_token",
|
|
226
|
+
senderId,
|
|
227
|
+
workspaceRoot,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const channelAdapter = createWeixinChannelAdapter(config);
|
|
232
|
+
await channelAdapter.sendText({
|
|
233
|
+
userId: senderId,
|
|
234
|
+
contextToken,
|
|
235
|
+
preserveBlock: true,
|
|
236
|
+
text,
|
|
237
|
+
});
|
|
238
|
+
return {
|
|
239
|
+
sent: true,
|
|
240
|
+
reason: "",
|
|
241
|
+
senderId,
|
|
242
|
+
workspaceRoot,
|
|
243
|
+
};
|
|
244
|
+
} catch (error) {
|
|
245
|
+
return {
|
|
246
|
+
sent: false,
|
|
247
|
+
reason: formatErrorMessage(error),
|
|
248
|
+
senderId: "",
|
|
249
|
+
workspaceRoot: "",
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function printSummary(state) {
|
|
255
|
+
console.log(`result=${state.result}`);
|
|
256
|
+
console.log(`readyz=${state.after?.appServer?.ready ? "ok" : "down"}`);
|
|
257
|
+
console.log(`shared_app_server_pid=${state.after?.appServer?.readyPid || "missing"}`);
|
|
258
|
+
console.log(`shared_codeksei_pid=${state.after?.bridge?.pid || "missing"}`);
|
|
259
|
+
console.log(`shared_cyberboss_pid=${state.after?.bridge?.pid || "missing"}`);
|
|
260
|
+
console.log(`shared_bridge_heartbeat=${state.after?.bridge?.heartbeatStatus || "missing"}`);
|
|
261
|
+
console.log(`shared_bridge_heartbeat_at=${state.after?.bridge?.heartbeatUpdatedAt || "missing"}`);
|
|
262
|
+
if (Array.isArray(state.actions) && state.actions.length) {
|
|
263
|
+
console.log(`actions=${state.actions.join(" | ")}`);
|
|
264
|
+
}
|
|
265
|
+
if (normalizeText(state.error)) {
|
|
266
|
+
console.log(`error=${state.error}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function formatErrorMessage(error) {
|
|
271
|
+
if (error instanceof Error) {
|
|
272
|
+
return error.message || error.stack || String(error);
|
|
273
|
+
}
|
|
274
|
+
return String(error || "unknown error");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function normalizeText(value) {
|
|
278
|
+
return typeof value === "string" ? value.trim() : "";
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
module.exports = {
|
|
282
|
+
runWatchdogOnce,
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
if (require.main === module) {
|
|
286
|
+
main().catch((error) => {
|
|
287
|
+
console.error(formatErrorMessage(error));
|
|
288
|
+
process.exit(1);
|
|
289
|
+
});
|
|
290
|
+
}
|