omp-wechat 1.4.0 → 1.5.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 (3) hide show
  1. package/README.md +10 -7
  2. package/dist/index.js +107 -9
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -37,7 +37,7 @@ For boot-time persistence, install a launchd/systemd service via `/wechat instal
37
37
  - **Typing indicator**: native WeChat "Typing..." shown during AI processing
38
38
  - **Access control**: pairing / allowlist / disabled modes
39
39
  - **Long text chunking**: splits replies >2000 chars at paragraph/line/space boundaries
40
- - **Boot service**: optional launchd/systemd service for auto-start on boot
40
+ - **Boot service**: optional launchd/systemd/Task Scheduler service for auto-start on boot
41
41
 
42
42
  ## Quick Start
43
43
 
@@ -78,10 +78,13 @@ To check status: `/wechat status`. To stop: `/wechat stop`.
78
78
  /wechat install
79
79
  ```
80
80
 
81
- Installs a launchd (macOS) or systemd (Linux) service that runs the host (`omp --mode rpc` or `pi --mode rpc`) at boot. A `get_state` JSON-RPC heartbeat is piped to stdin every 5s to keep the process alive (without an active RPC client, `omp --mode rpc` exits on idle stdin). `KeepAlive`/`Restart=always` handles crashes and reboots.
81
+ Installs a launchd (macOS), systemd (Linux), or Task Scheduler (Windows) service that runs the host (`omp --mode rpc` or `pi --mode rpc`) at boot (macOS/Linux) or user logon (Windows). A `get_state` JSON-RPC heartbeat is piped to stdin every 5s to keep the process alive (without an active RPC client, `omp --mode rpc` exits on idle stdin). launchd `KeepAlive`/systemd `Restart=always`/PowerShell restart-loop handles crashes. On Windows, the task uses `/sc onlogon` (no admin required); the host starts when the user logs in, not at bare-metal boot.
82
82
 
83
83
  Logs: `~/.omp/logs/rpc.log` (stderr only; stdout discarded) and `~/.omp/logs/wechat.log` (poll loop)
84
- Manage: `launchctl start|stop com.omp-wechat` (macOS) or `sudo systemctl start|stop omp-wechat` (Linux)
84
+ Manage:
85
+ - macOS: `launchctl start|stop com.omp-wechat`
86
+ - Linux: `sudo systemctl start|stop omp-wechat`
87
+ - Windows: `schtasks /run|/end /tn OMP-Wechat`
85
88
 
86
89
  To remove: `/wechat uninstall`
87
90
 
@@ -123,7 +126,7 @@ systemPrompt: |
123
126
  | `/wechat revoke <wxid>` | Revoke a user's authorization |
124
127
  | `/wechat list` | List authorized users |
125
128
  | `/wechat stop` | Stop the poll loop |
126
- | `/wechat install` | Install boot-time launchd/systemd service |
129
+ | `/wechat install` | Install boot-time launchd/systemd/Task Scheduler service |
127
130
  | `/wechat uninstall` | Remove boot-time service |
128
131
 
129
132
  ### Chat Commands (via WeChat message)
@@ -152,8 +155,8 @@ The logged-in user (who scanned the QR code) is automatically added to the allow
152
155
  | Host process starts | Poll loop starts at extension load time (acquires singleton lock) |
153
156
  | Other host processes | Standby with 30s failover timer, take over if lock holder dies |
154
157
  | Host process exits | Poll loop stops, lock released, all sessions disposed |
155
- | Host crashes | Failover timer in another process detects dead lock and takes over; or launchd/systemd restarts the host (if `/wechat install` was run) |
156
- | Machine reboots | Service auto-starts the host (if installed), poll loop resumes |
158
+ | Host crashes | Failover timer in another process detects dead lock and takes over; or launchd/systemd/Task Scheduler restarts the host (if `/wechat install` was run) |
159
+ | Machine reboots | macOS/Linux: service auto-starts at boot; Windows: service starts at user logon (if installed), poll loop resumes |
157
160
  | No boot service | Poll loop only runs while a host process is active |
158
161
 
159
162
  Logs: `~/.omp/logs/wechat.log` (poll loop) and `~/.omp/logs/rpc.log` (boot service stderr)
@@ -166,7 +169,7 @@ OMP-Wechat/
166
169
  ├── src/
167
170
  │ ├── index.ts # OMP/Pi extension entry (extension load + /wechat commands)
168
171
  │ ├── bridge.ts # In-process poll loop + message handling + singleton port lock
169
- │ ├── service.ts # Boot-time launchd/systemd install
172
+ │ ├── service.ts # Boot-time launchd/systemd/Task Scheduler install
170
173
  │ ├── config.ts # Config loading (config.yml + defaults)
171
174
  │ ├── ilink/
172
175
  │ │ ├── types.ts # iLink Bot API type definitions
package/dist/index.js CHANGED
@@ -1161,7 +1161,12 @@ var rotatingLog = new RotatingLog({
1161
1161
  });
1162
1162
  rotatingLog.cleanStale();
1163
1163
  function ts() {
1164
- return new Date().toISOString();
1164
+ const d = new Date;
1165
+ const off = -d.getTimezoneOffset();
1166
+ const sign = off >= 0 ? "+" : "-";
1167
+ const pad = (n) => String(Math.abs(n)).padStart(2, "0");
1168
+ const tz = `${sign}${pad(Math.trunc(off / 60))}:${pad(off % 60)}`;
1169
+ return new Date(d.getTime() - d.getTimezoneOffset() * 60000).toISOString().replace("Z", tz);
1165
1170
  }
1166
1171
  function log(level, msg, meta) {
1167
1172
  if (LEVEL_ORDER[level] < LEVEL_ORDER[minLevel])
@@ -1200,8 +1205,7 @@ function saveCredentials(creds) {
1200
1205
  function getCredentials() {
1201
1206
  const creds = loadCredentials();
1202
1207
  if (!creds?.token || !creds?.baseUrl) {
1203
- logger.error("Not logged in \u2014 run: omp-wechat login");
1204
- process.exit(1);
1208
+ throw new Error("Not logged in \u2014 run /wechat login");
1205
1209
  }
1206
1210
  return creds;
1207
1211
  }
@@ -1914,6 +1918,7 @@ class ChatSession {
1914
1918
  systemPrompt: config.systemPrompt,
1915
1919
  modelPattern: config.model
1916
1920
  });
1921
+ session.setAdvisorEnabled(false);
1917
1922
  if (modelFallbackMessage) {
1918
1923
  logger.warn(`Model fallback: ${modelFallbackMessage}`);
1919
1924
  }
@@ -2232,7 +2237,12 @@ class WeChatBridge {
2232
2237
  }
2233
2238
  start() {
2234
2239
  const config = loadConfig();
2235
- const creds = getCredentials();
2240
+ let creds;
2241
+ try {
2242
+ creds = getCredentials();
2243
+ } catch (err) {
2244
+ return { running: false, config, creds: null, lastError: String(err) };
2245
+ }
2236
2246
  if (this.pollActive) {
2237
2247
  return this.state;
2238
2248
  }
@@ -2455,7 +2465,7 @@ class WeChatBridge {
2455
2465
 
2456
2466
  // src/service.ts
2457
2467
  import { platform, homedir as homedir7 } from "os";
2458
- import { join as join8 } from "path";
2468
+ import { join as join8, basename as basename2 } from "path";
2459
2469
  import { existsSync as existsSync3, mkdirSync as mkdirSync6, writeFileSync as writeFileSync4, rmSync as rmSync2 } from "fs";
2460
2470
  var PLIST_LABEL = "com.omp-wechat";
2461
2471
  var SERVICE_NAME = "omp-wechat";
@@ -2465,6 +2475,8 @@ function detectPlatform() {
2465
2475
  return "darwin";
2466
2476
  if (p === "linux")
2467
2477
  return "linux";
2478
+ if (p === "win32")
2479
+ return "win32";
2468
2480
  return "other";
2469
2481
  }
2470
2482
  function getLogDir() {
@@ -2472,8 +2484,7 @@ function getLogDir() {
2472
2484
  }
2473
2485
  function resolveHostBinary() {
2474
2486
  const exe = process.execPath || "omp";
2475
- const basename2 = exe.split("/").pop() || "omp";
2476
- return basename2;
2487
+ return basename2(exe);
2477
2488
  }
2478
2489
  function plistPath() {
2479
2490
  return join8(homedir7(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
@@ -2609,6 +2620,85 @@ function uninstallSystemd() {
2609
2620
  Bun.spawnSync(["sudo", "rm", svc], { stderr: "inherit" });
2610
2621
  Bun.spawnSync(["sudo", "systemctl", "daemon-reload"], { stderr: "inherit" });
2611
2622
  }
2623
+ var WIN_TASK_NAME = "OMP-Wechat";
2624
+ function winScriptPath() {
2625
+ return join8(homedir7(), ".omp-wechat", "omp-wechat-rpc.ps1");
2626
+ }
2627
+ function generateWinScript() {
2628
+ const omp = process.execPath || "omp";
2629
+ const ompEscaped = omp.replace(/'/g, "''");
2630
+ return `# OMP-Wechat RPC heartbeat wrapper \u2014 auto-generated by /wechat install
2631
+ # Pipes a get_state JSON-RPC heartbeat to omp stdin every 5s to keep
2632
+ # the --mode rpc process alive (omp exits on idle stdin without an RPC client).
2633
+ # Outer while-loop restarts omp if it crashes, matching launchd KeepAlive
2634
+ # and systemd Restart=always.
2635
+ $ErrorActionPreference = 'Stop'
2636
+ $ompPath = '${ompEscaped}'
2637
+ $logPath = Join-Path $env:USERPROFILE '.omp\\logs\\rpc.log'
2638
+ $logDir = Split-Path $logPath
2639
+ if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Force -Path $logDir | Out-Null }
2640
+ while ($true) {
2641
+ $p = $null
2642
+ $errTask = $null
2643
+ try {
2644
+ $psi = New-Object System.Diagnostics.ProcessStartInfo
2645
+ $psi.FileName = $ompPath
2646
+ $psi.Arguments = '--mode rpc --no-title'
2647
+ $psi.UseShellExecute = $false
2648
+ $psi.RedirectStandardInput = $true
2649
+ $psi.RedirectStandardError = $true
2650
+ $p = [System.Diagnostics.Process]::Start($psi)
2651
+ $errTask = $p.StandardError.ReadToEndAsync()
2652
+ while (-not $p.HasExited) {
2653
+ $p.StandardInput.WriteLine('{"id":"ka","type":"get_state"}')
2654
+ Start-Sleep -Seconds 5
2655
+ }
2656
+ } catch {
2657
+ # omp failed to start or exited \u2014 loop will restart
2658
+ } finally {
2659
+ if ($p -and -not $p.HasExited) { $p.Kill() }
2660
+ if ($p) { $p.WaitForExit() }
2661
+ if ($errTask) { $err = $errTask.Result; if ($err) { Add-Content -Path $logPath -Value $err } }
2662
+ }
2663
+ Start-Sleep -Seconds 10
2664
+ }
2665
+ `;
2666
+ }
2667
+ function installWinTask() {
2668
+ mkdirSync6(join8(homedir7(), ".omp-wechat"), { recursive: true });
2669
+ mkdirSync6(getLogDir(), { recursive: true });
2670
+ writeFileSync4(winScriptPath(), generateWinScript());
2671
+ Bun.spawnSync(["schtasks", "/end", "/tn", WIN_TASK_NAME], { stderr: "ignore" });
2672
+ Bun.spawnSync(["schtasks", "/delete", "/tn", WIN_TASK_NAME, "/f"], { stderr: "ignore" });
2673
+ const scriptPath = winScriptPath();
2674
+ const taskCmd = `powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "${scriptPath}"`;
2675
+ const result = Bun.spawnSync(["schtasks", "/create", "/tn", WIN_TASK_NAME, "/tr", taskCmd, "/sc", "onlogon", "/rl", "limited", "/f"], { stderr: "inherit" });
2676
+ if (result.exitCode !== 0) {
2677
+ throw new Error("schtasks /create failed");
2678
+ }
2679
+ const runResult = Bun.spawnSync(["schtasks", "/run", "/tn", WIN_TASK_NAME], { stderr: "inherit" });
2680
+ if (runResult.exitCode !== 0) {
2681
+ logger.warn(`schtasks /run failed (exit ${runResult.exitCode}) \u2014 task will start at next logon`);
2682
+ }
2683
+ }
2684
+ function uninstallWinTask() {
2685
+ Bun.spawnSync(["schtasks", "/end", "/tn", WIN_TASK_NAME], { stderr: "ignore" });
2686
+ const result = Bun.spawnSync(["schtasks", "/delete", "/tn", WIN_TASK_NAME, "/f"], { stderr: "inherit" });
2687
+ if (result.exitCode !== 0) {
2688
+ throw new Error("Failed to delete scheduled task (may not be installed)");
2689
+ }
2690
+ const script = winScriptPath();
2691
+ if (existsSync3(script)) {
2692
+ rmSync2(script);
2693
+ }
2694
+ }
2695
+ function winTaskExists() {
2696
+ const r = Bun.spawnSync(["schtasks", "/query", "/tn", WIN_TASK_NAME, "/fo", "list"], {
2697
+ stdout: "ignore",
2698
+ stderr: "ignore"
2699
+ });
2700
+ return r.exitCode === 0;
2701
+ }
2612
2702
  function installService() {
2613
2703
  const p = detectPlatform();
2614
2704
  switch (p) {
@@ -2618,6 +2708,9 @@ function installService() {
2618
2708
  case "linux":
2619
2709
  installSystemd();
2620
2710
  return { platform: p, path: servicePath() };
2711
+ case "win32":
2712
+ installWinTask();
2713
+ return { platform: p, path: winScriptPath() };
2621
2714
  default:
2622
2715
  throw new Error(`Platform ${p} does not support auto-installing a boot service`);
2623
2716
  }
@@ -2631,6 +2724,9 @@ function uninstallService() {
2631
2724
  case "linux":
2632
2725
  uninstallSystemd();
2633
2726
  return { platform: p, path: servicePath() };
2727
+ case "win32":
2728
+ uninstallWinTask();
2729
+ return { platform: p, path: winScriptPath() };
2634
2730
  default:
2635
2731
  throw new Error(`Platform ${p} does not support auto-uninstalling a boot service`);
2636
2732
  }
@@ -2642,6 +2738,8 @@ function isServiceInstalled() {
2642
2738
  return existsSync3(plistPath());
2643
2739
  case "linux":
2644
2740
  return existsSync3(servicePath());
2741
+ case "win32":
2742
+ return winTaskExists();
2645
2743
  default:
2646
2744
  return false;
2647
2745
  }
@@ -2659,7 +2757,7 @@ function wechatExtension(pi) {
2659
2757
  if (daemonState.running) {
2660
2758
  logger.info("WeChat bridge started at extension load");
2661
2759
  } else {
2662
- logger.debug("WeChat bridge: another instance holds the lock, starting failover watch");
2760
+ logger.debug("WeChat bridge not running, starting 30s retry", { lastError: daemonState.lastError });
2663
2761
  setInterval(() => {
2664
2762
  if (daemonState?.running)
2665
2763
  return;
@@ -2764,7 +2862,7 @@ function wechatExtension(pi) {
2764
2862
  const r = installService();
2765
2863
  ctx.ui.notify(`Boot service installed (${r.platform}): ${r.path}`, "info");
2766
2864
  logger.info(`Service installed on ${r.platform} at ${r.path}
2767
- ` + `OMP will run via launchd/systemd at boot. ` + `Manage: ${r.platform === "darwin" ? "launchctl start|stop com.omp-wechat" : "sudo systemctl start|stop omp-wechat"}`);
2865
+ ` + `OMP will run via ${r.platform === "darwin" ? "launchd" : r.platform === "win32" ? "Task Scheduler" : "systemd"} at boot. ` + `Manage: ${r.platform === "darwin" ? "launchctl start|stop com.omp-wechat" : r.platform === "win32" ? "schtasks /run|/end /tn OMP-Wechat" : "sudo systemctl start|stop omp-wechat"}`);
2768
2866
  } catch (err) {
2769
2867
  ctx.ui.notify(`Install failed: ${err}`, "error");
2770
2868
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omp-wechat",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "OMP/Pi extension: bridge WeChat messages to OMP's AI engine via the iLink Bot API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",