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.
Files changed (80) hide show
  1. package/LICENSE +661 -0
  2. package/README.en.md +215 -0
  3. package/README.md +259 -0
  4. package/bin/codeksei.js +10 -0
  5. package/bin/cyberboss.js +11 -0
  6. package/package.json +86 -0
  7. package/scripts/install-background-tasks.ps1 +135 -0
  8. package/scripts/open_shared_wechat_thread.sh +94 -0
  9. package/scripts/open_wechat_thread.sh +117 -0
  10. package/scripts/shared-common.js +791 -0
  11. package/scripts/shared-open.js +46 -0
  12. package/scripts/shared-start.js +41 -0
  13. package/scripts/shared-status.js +74 -0
  14. package/scripts/shared-supervisor.js +141 -0
  15. package/scripts/shared-task-runner.ps1 +87 -0
  16. package/scripts/shared-watchdog.js +290 -0
  17. package/scripts/show_shared_status.sh +53 -0
  18. package/scripts/start_shared_app_server.sh +65 -0
  19. package/scripts/start_shared_wechat.sh +108 -0
  20. package/scripts/timeline-screenshot.sh +15 -0
  21. package/scripts/uninstall-background-tasks.ps1 +23 -0
  22. package/src/adapters/channel/weixin/account-store.js +135 -0
  23. package/src/adapters/channel/weixin/api-v2.js +258 -0
  24. package/src/adapters/channel/weixin/api.js +180 -0
  25. package/src/adapters/channel/weixin/context-token-store.js +84 -0
  26. package/src/adapters/channel/weixin/index.js +605 -0
  27. package/src/adapters/channel/weixin/legacy.js +567 -0
  28. package/src/adapters/channel/weixin/login-common.js +63 -0
  29. package/src/adapters/channel/weixin/login-legacy.js +124 -0
  30. package/src/adapters/channel/weixin/login-v2.js +186 -0
  31. package/src/adapters/channel/weixin/media-mime.js +22 -0
  32. package/src/adapters/channel/weixin/media-receive.js +370 -0
  33. package/src/adapters/channel/weixin/media-send.js +331 -0
  34. package/src/adapters/channel/weixin/message-utils-v2.js +282 -0
  35. package/src/adapters/channel/weixin/message-utils.js +199 -0
  36. package/src/adapters/channel/weixin/protocol.js +77 -0
  37. package/src/adapters/channel/weixin/redact.js +41 -0
  38. package/src/adapters/channel/weixin/reminder-queue-store.js +101 -0
  39. package/src/adapters/channel/weixin/sync-buffer-store.js +35 -0
  40. package/src/adapters/runtime/codex/events.js +252 -0
  41. package/src/adapters/runtime/codex/index.js +502 -0
  42. package/src/adapters/runtime/codex/message-utils.js +141 -0
  43. package/src/adapters/runtime/codex/model-catalog.js +106 -0
  44. package/src/adapters/runtime/codex/protocol-leak-monitor.js +75 -0
  45. package/src/adapters/runtime/codex/rpc-client.js +443 -0
  46. package/src/adapters/runtime/codex/session-store.js +376 -0
  47. package/src/app/channel-send-file-cli.js +57 -0
  48. package/src/app/diary-write-cli.js +620 -0
  49. package/src/app/note-auto-cli.js +201 -0
  50. package/src/app/note-sync-cli.js +130 -0
  51. package/src/app/project-radar-cli.js +165 -0
  52. package/src/app/reminder-write-cli.js +210 -0
  53. package/src/app/review-cli.js +134 -0
  54. package/src/app/system-checkin-poller.js +100 -0
  55. package/src/app/system-send-cli.js +129 -0
  56. package/src/app/timeline-event-cli.js +273 -0
  57. package/src/app/timeline-screenshot-cli.js +109 -0
  58. package/src/core/app.js +1810 -0
  59. package/src/core/branding.js +167 -0
  60. package/src/core/command-registry.js +609 -0
  61. package/src/core/config.js +84 -0
  62. package/src/core/default-targets.js +163 -0
  63. package/src/core/durable-note-schema.js +325 -0
  64. package/src/core/instructions-template.js +31 -0
  65. package/src/core/note-sync.js +433 -0
  66. package/src/core/project-radar.js +402 -0
  67. package/src/core/review-semantic.js +524 -0
  68. package/src/core/review.js +1081 -0
  69. package/src/core/shared-bridge-heartbeat.js +140 -0
  70. package/src/core/stream-delivery.js +990 -0
  71. package/src/core/system-message-dispatcher.js +68 -0
  72. package/src/core/system-message-queue-store.js +128 -0
  73. package/src/core/thread-state-store.js +135 -0
  74. package/src/core/timeline-screenshot-queue-store.js +134 -0
  75. package/src/core/workspace-alias.js +163 -0
  76. package/src/core/workspace-bootstrap.js +338 -0
  77. package/src/index.js +270 -0
  78. package/src/integrations/timeline/index.js +191 -0
  79. package/templates/weixin-instructions.md +53 -0
  80. 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
+ }