omp-wechat 1.4.1 → 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.
- package/README.md +10 -7
- package/dist/index.js +106 -9
- 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)
|
|
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:
|
|
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 |
|
|
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
|
-
|
|
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
|
-
|
|
1204
|
-
process.exit(1);
|
|
1208
|
+
throw new Error("Not logged in \u2014 run /wechat login");
|
|
1205
1209
|
}
|
|
1206
1210
|
return creds;
|
|
1207
1211
|
}
|
|
@@ -2233,7 +2237,12 @@ class WeChatBridge {
|
|
|
2233
2237
|
}
|
|
2234
2238
|
start() {
|
|
2235
2239
|
const config = loadConfig();
|
|
2236
|
-
|
|
2240
|
+
let creds;
|
|
2241
|
+
try {
|
|
2242
|
+
creds = getCredentials();
|
|
2243
|
+
} catch (err) {
|
|
2244
|
+
return { running: false, config, creds: null, lastError: String(err) };
|
|
2245
|
+
}
|
|
2237
2246
|
if (this.pollActive) {
|
|
2238
2247
|
return this.state;
|
|
2239
2248
|
}
|
|
@@ -2456,7 +2465,7 @@ class WeChatBridge {
|
|
|
2456
2465
|
|
|
2457
2466
|
// src/service.ts
|
|
2458
2467
|
import { platform, homedir as homedir7 } from "os";
|
|
2459
|
-
import { join as join8 } from "path";
|
|
2468
|
+
import { join as join8, basename as basename2 } from "path";
|
|
2460
2469
|
import { existsSync as existsSync3, mkdirSync as mkdirSync6, writeFileSync as writeFileSync4, rmSync as rmSync2 } from "fs";
|
|
2461
2470
|
var PLIST_LABEL = "com.omp-wechat";
|
|
2462
2471
|
var SERVICE_NAME = "omp-wechat";
|
|
@@ -2466,6 +2475,8 @@ function detectPlatform() {
|
|
|
2466
2475
|
return "darwin";
|
|
2467
2476
|
if (p === "linux")
|
|
2468
2477
|
return "linux";
|
|
2478
|
+
if (p === "win32")
|
|
2479
|
+
return "win32";
|
|
2469
2480
|
return "other";
|
|
2470
2481
|
}
|
|
2471
2482
|
function getLogDir() {
|
|
@@ -2473,8 +2484,7 @@ function getLogDir() {
|
|
|
2473
2484
|
}
|
|
2474
2485
|
function resolveHostBinary() {
|
|
2475
2486
|
const exe = process.execPath || "omp";
|
|
2476
|
-
|
|
2477
|
-
return basename2;
|
|
2487
|
+
return basename2(exe);
|
|
2478
2488
|
}
|
|
2479
2489
|
function plistPath() {
|
|
2480
2490
|
return join8(homedir7(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
|
|
@@ -2610,6 +2620,85 @@ function uninstallSystemd() {
|
|
|
2610
2620
|
Bun.spawnSync(["sudo", "rm", svc], { stderr: "inherit" });
|
|
2611
2621
|
Bun.spawnSync(["sudo", "systemctl", "daemon-reload"], { stderr: "inherit" });
|
|
2612
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
|
+
}
|
|
2613
2702
|
function installService() {
|
|
2614
2703
|
const p = detectPlatform();
|
|
2615
2704
|
switch (p) {
|
|
@@ -2619,6 +2708,9 @@ function installService() {
|
|
|
2619
2708
|
case "linux":
|
|
2620
2709
|
installSystemd();
|
|
2621
2710
|
return { platform: p, path: servicePath() };
|
|
2711
|
+
case "win32":
|
|
2712
|
+
installWinTask();
|
|
2713
|
+
return { platform: p, path: winScriptPath() };
|
|
2622
2714
|
default:
|
|
2623
2715
|
throw new Error(`Platform ${p} does not support auto-installing a boot service`);
|
|
2624
2716
|
}
|
|
@@ -2632,6 +2724,9 @@ function uninstallService() {
|
|
|
2632
2724
|
case "linux":
|
|
2633
2725
|
uninstallSystemd();
|
|
2634
2726
|
return { platform: p, path: servicePath() };
|
|
2727
|
+
case "win32":
|
|
2728
|
+
uninstallWinTask();
|
|
2729
|
+
return { platform: p, path: winScriptPath() };
|
|
2635
2730
|
default:
|
|
2636
2731
|
throw new Error(`Platform ${p} does not support auto-uninstalling a boot service`);
|
|
2637
2732
|
}
|
|
@@ -2643,6 +2738,8 @@ function isServiceInstalled() {
|
|
|
2643
2738
|
return existsSync3(plistPath());
|
|
2644
2739
|
case "linux":
|
|
2645
2740
|
return existsSync3(servicePath());
|
|
2741
|
+
case "win32":
|
|
2742
|
+
return winTaskExists();
|
|
2646
2743
|
default:
|
|
2647
2744
|
return false;
|
|
2648
2745
|
}
|
|
@@ -2660,7 +2757,7 @@ function wechatExtension(pi) {
|
|
|
2660
2757
|
if (daemonState.running) {
|
|
2661
2758
|
logger.info("WeChat bridge started at extension load");
|
|
2662
2759
|
} else {
|
|
2663
|
-
logger.debug("WeChat bridge
|
|
2760
|
+
logger.debug("WeChat bridge not running, starting 30s retry", { lastError: daemonState.lastError });
|
|
2664
2761
|
setInterval(() => {
|
|
2665
2762
|
if (daemonState?.running)
|
|
2666
2763
|
return;
|
|
@@ -2765,7 +2862,7 @@ function wechatExtension(pi) {
|
|
|
2765
2862
|
const r = installService();
|
|
2766
2863
|
ctx.ui.notify(`Boot service installed (${r.platform}): ${r.path}`, "info");
|
|
2767
2864
|
logger.info(`Service installed on ${r.platform} at ${r.path}
|
|
2768
|
-
` + `OMP will run via launchd
|
|
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"}`);
|
|
2769
2866
|
} catch (err) {
|
|
2770
2867
|
ctx.ui.notify(`Install failed: ${err}`, "error");
|
|
2771
2868
|
}
|