omp-wechat 1.2.1 → 1.4.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 +16 -12
- package/dist/index.js +35 -35
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,9 +12,9 @@ WeChat user → iLink Bot API → [OMP/Pi process] → SDK → AI provider
|
|
|
12
12
|
└──── reply ← message_end ─┘
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
The extension runs **inside** the OMP/Pi process.
|
|
15
|
+
The extension runs **inside** the OMP/Pi process. The iLink long-poll loop starts at extension load time (not `session_start`) as a background promise. A singleton port lock ensures only one process runs the poll loop at a time — other OMP/Pi processes standby with a 30s failover timer to take over if the lock holder crashes.
|
|
16
16
|
|
|
17
|
-
For boot-time persistence, install a launchd/systemd service via `/wechat install`. The service runs `omp --mode rpc` (or `pi --mode rpc`) with `
|
|
17
|
+
For boot-time persistence, install a launchd/systemd service via `/wechat install`. The service runs `omp --mode rpc` (or `pi --mode rpc`) with a `get_state` JSON-RPC heartbeat piped to stdin every 5s — without an active RPC client, `omp --mode rpc` exits on idle stdin, so the heartbeat keeps the process alive. `KeepAlive`/`Restart=always` handles crashes and reboots.
|
|
18
18
|
|
|
19
19
|
- **No external `bun` required** — OMP/Pi is a standalone binary with an embedded runtime
|
|
20
20
|
- **Singleton** — port lock guarantees one poll loop across all concurrent OMP/Pi processes
|
|
@@ -26,11 +26,12 @@ For boot-time persistence, install a launchd/systemd service via `/wechat instal
|
|
|
26
26
|
|
|
27
27
|
## Features
|
|
28
28
|
|
|
29
|
-
- **OMP/Pi extension**: installs via `omp plugin link .` or `pi plugin link .`, auto-starts poll loop
|
|
29
|
+
- **OMP/Pi extension**: installs via `omp plugin link .` or `pi plugin link .`, auto-starts poll loop at extension load time
|
|
30
30
|
- **Slash commands**: `/wechat login`, `/wechat status`, `/wechat pair`, `/wechat allow`, `/wechat revoke`, `/wechat list`, `/wechat stop`, `/wechat install`, `/wechat uninstall`
|
|
31
31
|
- **Singleton**: port lock guarantees one poll loop across all concurrent OMP/Pi processes — no duplicate replies
|
|
32
32
|
- **Failover**: 30s timer takes over automatically if the lock holder crashes
|
|
33
33
|
- **Bidirectional**: receive and reply to WeChat text messages
|
|
34
|
+
- **Image recognition**: inbound images are downloaded from WeChat CDN, AES-decrypted, and passed to the vision model
|
|
34
35
|
- **Per-chat sessions**: each WeChat chat gets an independent AI session (concurrent, isolated)
|
|
35
36
|
- **LRU pool**: caps memory usage by evicting least-recently-used sessions (default: 50)
|
|
36
37
|
- **Typing indicator**: native WeChat "Typing..." shown during AI processing
|
|
@@ -55,7 +56,7 @@ bun run build
|
|
|
55
56
|
omp plugin link . # or: pi plugin link .
|
|
56
57
|
```
|
|
57
58
|
|
|
58
|
-
This links the extension into OMP/Pi. The poll loop starts
|
|
59
|
+
This links the extension into OMP/Pi. The poll loop starts at extension load time — no `session_start` required.
|
|
59
60
|
|
|
60
61
|
### Login (scan QR code)
|
|
61
62
|
|
|
@@ -67,7 +68,7 @@ A QR code appears in the terminal. Scan it with WeChat and confirm on your phone
|
|
|
67
68
|
|
|
68
69
|
### Run
|
|
69
70
|
|
|
70
|
-
No explicit run command needed — the poll loop
|
|
71
|
+
No explicit run command needed — the poll loop starts at extension load time. Once running, send a message to the bot on WeChat — it will be processed and the reply sent back.
|
|
71
72
|
|
|
72
73
|
To check status: `/wechat status`. To stop: `/wechat stop`.
|
|
73
74
|
|
|
@@ -77,9 +78,9 @@ To check status: `/wechat status`. To stop: `/wechat stop`.
|
|
|
77
78
|
/wechat install
|
|
78
79
|
```
|
|
79
80
|
|
|
80
|
-
Installs a launchd (macOS) or systemd (Linux) service that runs the host (`omp --mode rpc` or `pi --mode rpc`) at boot.
|
|
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
82
|
|
|
82
|
-
Logs: `~/.omp
|
|
83
|
+
Logs: `~/.omp/logs/rpc.log` (stderr only; stdout discarded) and `~/.omp/logs/wechat.log` (poll loop)
|
|
83
84
|
Manage: `launchctl start|stop com.omp-wechat` (macOS) or `sudo systemctl start|stop omp-wechat` (Linux)
|
|
84
85
|
|
|
85
86
|
To remove: `/wechat uninstall`
|
|
@@ -108,6 +109,8 @@ systemPrompt: |
|
|
|
108
109
|
| `systemPrompt` | Built-in | System prompt for WeChat chat sessions |
|
|
109
110
|
|
|
110
111
|
> **Model and tools are managed by OMP/Pi.** `createAgentSession()` automatically calls `discoverAuthStorage()`, reusing your existing `omp login` / `pi login` OAuth, `~/.omp/agent/agent.db` API keys, or `models.yml` config. This project never touches API keys.
|
|
112
|
+
>
|
|
113
|
+
> **Image recognition** requires a vision model role configured in OMP (e.g. `omp model role vision xfyun/xopkimik25`). If no vision role is set, inbound images are skipped — only the text placeholder is sent to the AI.
|
|
111
114
|
|
|
112
115
|
## Slash Commands
|
|
113
116
|
|
|
@@ -146,14 +149,14 @@ The logged-in user (who scanned the QR code) is automatically added to the allow
|
|
|
146
149
|
|
|
147
150
|
| Scenario | Behavior |
|
|
148
151
|
|---|---|
|
|
149
|
-
| Host process starts | Poll loop starts
|
|
152
|
+
| Host process starts | Poll loop starts at extension load time (acquires singleton lock) |
|
|
150
153
|
| Other host processes | Standby with 30s failover timer, take over if lock holder dies |
|
|
151
154
|
| Host process exits | Poll loop stops, lock released, all sessions disposed |
|
|
152
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) |
|
|
153
156
|
| Machine reboots | Service auto-starts the host (if installed), poll loop resumes |
|
|
154
157
|
| No boot service | Poll loop only runs while a host process is active |
|
|
155
158
|
|
|
156
|
-
Logs: `~/.omp
|
|
159
|
+
Logs: `~/.omp/logs/wechat.log` (poll loop) and `~/.omp/logs/rpc.log` (boot service stderr)
|
|
157
160
|
|
|
158
161
|
## Project Structure
|
|
159
162
|
|
|
@@ -161,7 +164,7 @@ Logs: `~/.omp-wechat/logs/daemon.log` (poll loop) and `~/.omp-wechat/logs/rpc.lo
|
|
|
161
164
|
OMP-Wechat/
|
|
162
165
|
├── package.json # omp.extensions / pi.extensions manifest
|
|
163
166
|
├── src/
|
|
164
|
-
│ ├── index.ts # OMP/Pi extension entry (
|
|
167
|
+
│ ├── index.ts # OMP/Pi extension entry (extension load + /wechat commands)
|
|
165
168
|
│ ├── bridge.ts # In-process poll loop + message handling + singleton port lock
|
|
166
169
|
│ ├── service.ts # Boot-time launchd/systemd install
|
|
167
170
|
│ ├── config.ts # Config loading (config.yml + defaults)
|
|
@@ -188,11 +191,12 @@ OMP-Wechat/
|
|
|
188
191
|
- **Reply-only**: iLink requires `context_token` from an inbound message; you cannot initiate conversations
|
|
189
192
|
- **1:1 only**: iLink Bot API does not support group chats
|
|
190
193
|
- **Single instance**: iLink allows only one bot connection per account
|
|
191
|
-
- **
|
|
194
|
+
- **Media**: inbound images are downloaded from WeChat CDN, AES-decrypted, and passed to the vision model (if `modelRoles.vision` is configured); voice/video remain as placeholders
|
|
192
195
|
|
|
193
196
|
## Roadmap
|
|
194
197
|
|
|
195
|
-
- [
|
|
198
|
+
- [x] **Phase 2a**: Inbound image support (CDN download + AES decrypt + vision model)
|
|
199
|
+
- [ ] **Phase 2b**: Voice transcription / video support
|
|
196
200
|
- [x] **Phase 3**: Persistent sessions — `SessionManager.continueRecent()` per chat, context survives restarts
|
|
197
201
|
- [x] **Phase 4**: Per-chat model selection — `/model` `/models` chat commands for manual switching
|
|
198
202
|
- [ ] **Phase 5**: Fine-grained permissions (per-user tool restrictions, bash approval via WeChat)
|
package/dist/index.js
CHANGED
|
@@ -1758,17 +1758,15 @@ function decryptAesEcb(ciphertext, key) {
|
|
|
1758
1758
|
const decipher = createDecipheriv("aes-128-ecb", key, null);
|
|
1759
1759
|
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
1760
1760
|
}
|
|
1761
|
-
function buildCdnUrl(
|
|
1761
|
+
function buildCdnUrl(encryptQueryParam, fullUrl) {
|
|
1762
1762
|
if (fullUrl)
|
|
1763
1763
|
return fullUrl;
|
|
1764
|
-
if (!
|
|
1764
|
+
if (!encryptQueryParam)
|
|
1765
1765
|
return null;
|
|
1766
|
-
|
|
1767
|
-
return fileUrl;
|
|
1768
|
-
return `${CDN_BASE_URL}/download?encrypted_query_param=${encodeURIComponent(fileUrl)}`;
|
|
1766
|
+
return `${CDN_BASE_URL}/download?encrypted_query_param=${encodeURIComponent(encryptQueryParam)}`;
|
|
1769
1767
|
}
|
|
1770
|
-
async function downloadAndDecrypt(
|
|
1771
|
-
const url = buildCdnUrl(
|
|
1768
|
+
async function downloadAndDecrypt(encryptQueryParam, fullUrl, aeskeyHex, aesKeyBase64, label = "media") {
|
|
1769
|
+
const url = buildCdnUrl(encryptQueryParam, fullUrl);
|
|
1772
1770
|
if (!url) {
|
|
1773
1771
|
logger.warn(`[${label}] No CDN URL available`);
|
|
1774
1772
|
return null;
|
|
@@ -2411,7 +2409,8 @@ class WeChatBridge {
|
|
|
2411
2409
|
const results = [];
|
|
2412
2410
|
for (const item of imageItems) {
|
|
2413
2411
|
const img = item.image_item;
|
|
2414
|
-
|
|
2412
|
+
logger.debug(`[${chatId}] image_item raw: ${JSON.stringify(img)}`);
|
|
2413
|
+
const buf = await downloadAndDecrypt(img.media?.encrypt_query_param, img.media?.full_url, img.aeskey, img.media?.aes_key, `image[${chatId}]`);
|
|
2415
2414
|
if (buf) {
|
|
2416
2415
|
const mimeType = buf.length > 4 && buf[0] === 137 && buf[1] === 80 ? "image/png" : "image/jpeg";
|
|
2417
2416
|
results.push({
|
|
@@ -2472,7 +2471,9 @@ function getLogDir() {
|
|
|
2472
2471
|
return join8(homedir7(), ".omp", "logs");
|
|
2473
2472
|
}
|
|
2474
2473
|
function resolveHostBinary() {
|
|
2475
|
-
|
|
2474
|
+
const exe = process.execPath || "omp";
|
|
2475
|
+
const basename2 = exe.split("/").pop() || "omp";
|
|
2476
|
+
return basename2;
|
|
2476
2477
|
}
|
|
2477
2478
|
function plistPath() {
|
|
2478
2479
|
return join8(homedir7(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
|
|
@@ -2489,10 +2490,9 @@ function generatePlist() {
|
|
|
2489
2490
|
|
|
2490
2491
|
<key>ProgramArguments</key>
|
|
2491
2492
|
<array>
|
|
2492
|
-
<string
|
|
2493
|
-
<string
|
|
2494
|
-
<string>rpc</string>
|
|
2495
|
-
<string>--no-title</string>
|
|
2493
|
+
<string>/bin/sh</string>
|
|
2494
|
+
<string>-c</string>
|
|
2495
|
+
<string>while true; do echo '{"id":"ka","type":"get_state"}'; sleep 5; done | exec ${omp} --mode rpc --no-title >/dev/null</string>
|
|
2496
2496
|
</array>
|
|
2497
2497
|
|
|
2498
2498
|
<key>RunAtLoad</key>
|
|
@@ -2504,11 +2504,6 @@ function generatePlist() {
|
|
|
2504
2504
|
<key>ThrottleInterval</key>
|
|
2505
2505
|
<integer>10</integer>
|
|
2506
2506
|
|
|
2507
|
-
<key>StandardInputPath</key>
|
|
2508
|
-
<string>/dev/null</string>
|
|
2509
|
-
|
|
2510
|
-
<key>StandardOutPath</key>
|
|
2511
|
-
<string>${logDir}/rpc.log</string>
|
|
2512
2507
|
<key>StandardErrorPath</key>
|
|
2513
2508
|
<string>${logDir}/rpc.log</string>
|
|
2514
2509
|
|
|
@@ -2562,15 +2557,14 @@ Wants=network-online.target
|
|
|
2562
2557
|
Type=simple
|
|
2563
2558
|
User=${user}
|
|
2564
2559
|
Group=${user}
|
|
2565
|
-
ExecStart
|
|
2566
|
-
StandardInput=null
|
|
2560
|
+
ExecStart=/bin/sh -c 'while true; do echo "{\\"id\\":\\"ka\\",\\"type\\":\\"get_state\\"}"; sleep 5; done | exec ${omp} --mode rpc --no-title >/dev/null'
|
|
2567
2561
|
Restart=always
|
|
2568
2562
|
RestartSec=10
|
|
2569
2563
|
|
|
2570
2564
|
Environment=HOME=${homedir7()}
|
|
2571
2565
|
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
|
2572
2566
|
|
|
2573
|
-
StandardOutput=
|
|
2567
|
+
StandardOutput=null
|
|
2574
2568
|
StandardError=append:${logDir}/rpc.log
|
|
2575
2569
|
|
|
2576
2570
|
NoNewPrivileges=true
|
|
@@ -2660,21 +2654,27 @@ function wechatExtension(pi) {
|
|
|
2660
2654
|
if (pi.pi && typeof pi.setLabel === "function") {
|
|
2661
2655
|
pi.setLabel("OMP-Wechat Bridge");
|
|
2662
2656
|
}
|
|
2657
|
+
bridge = new WeChatBridge;
|
|
2658
|
+
daemonState = bridge.start();
|
|
2659
|
+
if (daemonState.running) {
|
|
2660
|
+
logger.info("WeChat bridge started at extension load");
|
|
2661
|
+
} else {
|
|
2662
|
+
logger.debug("WeChat bridge: another instance holds the lock, starting failover watch");
|
|
2663
|
+
setInterval(() => {
|
|
2664
|
+
if (daemonState?.running)
|
|
2665
|
+
return;
|
|
2666
|
+
daemonState = bridge.start();
|
|
2667
|
+
if (daemonState.running) {
|
|
2668
|
+
logger.info("WeChat bridge: took over from failed instance");
|
|
2669
|
+
}
|
|
2670
|
+
}, 30000);
|
|
2671
|
+
}
|
|
2663
2672
|
pi.on("session_start", async (_event, ctx) => {
|
|
2664
|
-
bridge
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
logger.debug("WeChat bridge: another instance holds the lock, starting failover watch");
|
|
2670
|
-
ctx.setInterval(() => {
|
|
2671
|
-
if (daemonState?.running)
|
|
2672
|
-
return;
|
|
2673
|
-
daemonState = bridge.start();
|
|
2674
|
-
if (daemonState.running) {
|
|
2675
|
-
ctx.ui.notify("WeChat bridge: took over from failed instance", "info");
|
|
2676
|
-
}
|
|
2677
|
-
}, 30000);
|
|
2673
|
+
if (bridge && !daemonState?.running) {
|
|
2674
|
+
daemonState = bridge.start();
|
|
2675
|
+
if (daemonState.running) {
|
|
2676
|
+
ctx.ui.notify("WeChat bridge started", "info");
|
|
2677
|
+
}
|
|
2678
2678
|
}
|
|
2679
2679
|
});
|
|
2680
2680
|
pi.registerCommand("wechat", {
|