omp-wechat 1.5.0 → 1.6.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 +38 -3
- package/dist/index.js +405 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,6 +32,9 @@ For boot-time persistence, install a launchd/systemd service via `/wechat instal
|
|
|
32
32
|
- **Failover**: 30s timer takes over automatically if the lock holder crashes
|
|
33
33
|
- **Bidirectional**: receive and reply to WeChat text messages
|
|
34
34
|
- **Image recognition**: inbound images are downloaded from WeChat CDN, AES-decrypted, and passed to the vision model
|
|
35
|
+
- **Inbound file content**: text files (`.txt/.md/.csv/.json/.py/…`) sent by the user are downloaded, decrypted, and their content is handed to the AI; binary files arrive as metadata placeholders
|
|
36
|
+
- **Markdown stripping**: AI replies are stripped of markdown formatting before delivery — WeChat renders plain text only
|
|
37
|
+
- **File delivery**: AI-generated files (documents, images, PDFs, spreadsheets, code…) written to a per-chat outbox directory are automatically uploaded to the WeChat CDN (AES-128-ECB encrypted) and sent back to the user as file/image messages
|
|
35
38
|
- **Per-chat sessions**: each WeChat chat gets an independent AI session (concurrent, isolated)
|
|
36
39
|
- **LRU pool**: caps memory usage by evicting least-recently-used sessions (default: 50)
|
|
37
40
|
- **Typing indicator**: native WeChat "Typing..." shown during AI processing
|
|
@@ -110,11 +113,39 @@ systemPrompt: |
|
|
|
110
113
|
| `model` | OMP default | Default model: role alias (`@smol`, `@slow`) or `provider/id` |
|
|
111
114
|
| `cwd` | `process.cwd()` | Working directory for AI sessions — determines which project context (CLAUDE.md, .omp/) the agent loads |
|
|
112
115
|
| `systemPrompt` | Built-in | System prompt for WeChat chat sessions |
|
|
116
|
+
| `outboxDir` | `~/.omp-wechat/outbox` | Base directory for per-chat outboxes (see File Delivery below) |
|
|
117
|
+
| `maxFileSizeMb` | `100` | Max file size delivered via WeChat, in MB |
|
|
118
|
+
| `sendFiles` | `true` | Set to `false` to disable file delivery entirely |
|
|
113
119
|
|
|
114
120
|
> **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.
|
|
115
121
|
>
|
|
116
122
|
> **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.
|
|
117
123
|
|
|
124
|
+
## File Delivery
|
|
125
|
+
|
|
126
|
+
Ask the AI to generate a file (report, PDF, spreadsheet, image, code, …) — the finished artifact is sent back to you as a WeChat file or image message automatically.
|
|
127
|
+
|
|
128
|
+
### How it works
|
|
129
|
+
|
|
130
|
+
1. Every chat gets a private **outbox directory**: `~/.omp-wechat/outbox/<wxid>/`
|
|
131
|
+
2. The session's system prompt teaches the AI to write final deliverables there (and only there)
|
|
132
|
+
3. When the AI finishes a turn, the plugin diffs the outbox and uploads every new/changed file:
|
|
133
|
+
- `getuploadurl` → CDN upload (AES-128-ECB encrypted) → `sendmessage` as a file item
|
|
134
|
+
- Images (`.png/.jpg/.jpeg/.gif/.webp/.bmp`) are auto-routed to image messages; everything else goes as a file
|
|
135
|
+
4. Delivered files are removed from the outbox; failed or oversized files stay on disk and a text notice is sent instead
|
|
136
|
+
|
|
137
|
+
### Example
|
|
138
|
+
|
|
139
|
+
> **You**: Please generate a weekly report as a PDF
|
|
140
|
+
> **AI**: *(writes `weekly-report.pdf` to the outbox)* Done — the report is attached.
|
|
141
|
+
> **You** (WeChat): receives the text reply **plus** the `weekly-report.pdf` file
|
|
142
|
+
|
|
143
|
+
### Limitations
|
|
144
|
+
|
|
145
|
+
- Files are sent via `sendmessage` using the latest inbound `context_token`, same as text replies — an expired token (long-running task, restart) may fail delivery until you message again
|
|
146
|
+
- Files larger than `maxFileSizeMb` are skipped with a text notice
|
|
147
|
+
- Only files in the per-chat outbox are ever sent — the AI cannot exfiltrate arbitrary paths
|
|
148
|
+
|
|
118
149
|
## Slash Commands
|
|
119
150
|
|
|
120
151
|
| Command | Description |
|
|
@@ -174,9 +205,11 @@ OMP-Wechat/
|
|
|
174
205
|
│ ├── ilink/
|
|
175
206
|
│ │ ├── types.ts # iLink Bot API type definitions
|
|
176
207
|
│ │ ├── client.ts # iLink API client (poll/send/typing)
|
|
208
|
+
│ │ ├── upload.ts # Outbound media: CDN upload + file/image send
|
|
209
|
+
│ │ ├── cdn.ts # CDN media download/upload + AES-128-ECB crypto
|
|
177
210
|
│ │ └── login.ts # QR code login flow
|
|
178
211
|
│ ├── engine/
|
|
179
|
-
│ │ ├── session.ts # AI session creation + reply subscription
|
|
212
|
+
│ │ ├── session.ts # AI session creation + reply/outbox subscription
|
|
180
213
|
│ │ └── pool.ts # Session pool (LRU eviction, concurrency)
|
|
181
214
|
│ ├── access/
|
|
182
215
|
│ │ └── control.ts # Access control (pairing/allowlist/disabled)
|
|
@@ -191,15 +224,17 @@ OMP-Wechat/
|
|
|
191
224
|
|
|
192
225
|
## Limitations
|
|
193
226
|
|
|
194
|
-
- **Reply-only**: iLink requires `context_token` from an inbound message; you cannot initiate conversations
|
|
227
|
+
- **Reply-only**: iLink requires `context_token` from an inbound message; you cannot initiate conversations (applies to text and file replies alike)
|
|
195
228
|
- **1:1 only**: iLink Bot API does not support group chats
|
|
196
229
|
- **Single instance**: iLink allows only one bot connection per account
|
|
197
|
-
- **Media**: inbound images
|
|
230
|
+
- **Media**: inbound images and text files are fully processed (vision model / content extraction); voice (unless the server provides transcription) and video remain as placeholders
|
|
198
231
|
|
|
199
232
|
## Roadmap
|
|
200
233
|
|
|
201
234
|
- [x] **Phase 2a**: Inbound image support (CDN download + AES decrypt + vision model)
|
|
202
235
|
- [ ] **Phase 2b**: Voice transcription / video support
|
|
236
|
+
- [x] **Phase 2c**: Outbound file delivery — AI-generated files sent back via WeChat (CDN upload + file/image messages)
|
|
237
|
+
- [x] **Phase 2d**: Inbound file content extraction + markdown stripping for replies
|
|
203
238
|
- [x] **Phase 3**: Persistent sessions — `SessionManager.continueRecent()` per chat, context survives restarts
|
|
204
239
|
- [x] **Phase 4**: Per-chat model selection — `/model` `/models` chat commands for manual switching
|
|
205
240
|
- [ ] **Phase 5**: Fine-grained permissions (per-user tool restrictions, bash approval via WeChat)
|
package/dist/index.js
CHANGED
|
@@ -1326,6 +1326,13 @@ function saveSyncBuf(buf) {
|
|
|
1326
1326
|
mkdirSync2(STATE_DIR, { recursive: true });
|
|
1327
1327
|
writeFileSync(SYNC_BUF_FILE, buf);
|
|
1328
1328
|
}
|
|
1329
|
+
function formatSize(bytes) {
|
|
1330
|
+
if (bytes < 1024)
|
|
1331
|
+
return `${bytes}B`;
|
|
1332
|
+
if (bytes < 1024 * 1024)
|
|
1333
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
1334
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
1335
|
+
}
|
|
1329
1336
|
function extractInboundText(msg, includeImagePlaceholder = true) {
|
|
1330
1337
|
const items = msg.item_list ?? [];
|
|
1331
1338
|
const parts = [];
|
|
@@ -1343,7 +1350,11 @@ function extractInboundText(msg, includeImagePlaceholder = true) {
|
|
|
1343
1350
|
parts.push(item.voice_item?.text ?? "(voice)");
|
|
1344
1351
|
break;
|
|
1345
1352
|
case 4:
|
|
1346
|
-
|
|
1353
|
+
{
|
|
1354
|
+
const name = item.file_item?.file_name ?? "unknown";
|
|
1355
|
+
const len = parseInt(item.file_item?.len ?? "", 10);
|
|
1356
|
+
parts.push(`(file: ${name}${Number.isFinite(len) && len > 0 ? ", " + formatSize(len) : ""})`);
|
|
1357
|
+
}
|
|
1347
1358
|
break;
|
|
1348
1359
|
case 5:
|
|
1349
1360
|
parts.push("(video)");
|
|
@@ -1572,7 +1583,9 @@ async function pollQrStatus(qrcode, baseUrl) {
|
|
|
1572
1583
|
}
|
|
1573
1584
|
|
|
1574
1585
|
// src/bridge.ts
|
|
1575
|
-
import { randomBytes as
|
|
1586
|
+
import { randomBytes as randomBytes5 } from "crypto";
|
|
1587
|
+
import { rmSync as rmSync2 } from "fs";
|
|
1588
|
+
import { basename as basename3, extname as extname2 } from "path";
|
|
1576
1589
|
|
|
1577
1590
|
// src/config.ts
|
|
1578
1591
|
import { homedir as homedir4 } from "os";
|
|
@@ -1607,6 +1620,15 @@ function loadConfig() {
|
|
|
1607
1620
|
config.cwd = expandTilde(parsed.cwd);
|
|
1608
1621
|
if (parsed.systemPrompt)
|
|
1609
1622
|
config.systemPrompt = parsed.systemPrompt;
|
|
1623
|
+
if (parsed.outboxDir)
|
|
1624
|
+
config.outboxDir = expandTilde(parsed.outboxDir);
|
|
1625
|
+
if (parsed.maxFileSizeMb) {
|
|
1626
|
+
const mb = parseInt(parsed.maxFileSizeMb, 10);
|
|
1627
|
+
if (Number.isFinite(mb) && mb > 0)
|
|
1628
|
+
config.maxFileSizeMb = mb;
|
|
1629
|
+
}
|
|
1630
|
+
if (parsed.sendFiles === "false")
|
|
1631
|
+
config.sendFiles = false;
|
|
1610
1632
|
}
|
|
1611
1633
|
} catch (err) {
|
|
1612
1634
|
logger.warn("Failed to load config.yml, using defaults", err);
|
|
@@ -1742,7 +1764,7 @@ function isDuplicate(key) {
|
|
|
1742
1764
|
}
|
|
1743
1765
|
|
|
1744
1766
|
// src/ilink/cdn.ts
|
|
1745
|
-
import { createDecipheriv } from "crypto";
|
|
1767
|
+
import { createCipheriv, createDecipheriv, randomBytes as randomBytes3 } from "crypto";
|
|
1746
1768
|
var CDN_BASE_URL = "https://novac2c.cdn.weixin.qq.com/c2c";
|
|
1747
1769
|
function parseAesKey(aeskeyHex, aesKeyBase64) {
|
|
1748
1770
|
if (aeskeyHex && /^[0-9a-fA-F]{32}$/.test(aeskeyHex)) {
|
|
@@ -1762,6 +1784,13 @@ function decryptAesEcb(ciphertext, key) {
|
|
|
1762
1784
|
const decipher = createDecipheriv("aes-128-ecb", key, null);
|
|
1763
1785
|
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
1764
1786
|
}
|
|
1787
|
+
function encryptAesEcb(plaintext, key) {
|
|
1788
|
+
const cipher = createCipheriv("aes-128-ecb", key, null);
|
|
1789
|
+
return Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
1790
|
+
}
|
|
1791
|
+
function generateAesKey() {
|
|
1792
|
+
return randomBytes3(16);
|
|
1793
|
+
}
|
|
1765
1794
|
function buildCdnUrl(encryptQueryParam, fullUrl) {
|
|
1766
1795
|
if (fullUrl)
|
|
1767
1796
|
return fullUrl;
|
|
@@ -1796,13 +1825,185 @@ async function downloadAndDecrypt(encryptQueryParam, fullUrl, aeskeyHex, aesKeyB
|
|
|
1796
1825
|
}
|
|
1797
1826
|
}
|
|
1798
1827
|
|
|
1828
|
+
// src/ilink/upload.ts
|
|
1829
|
+
import { createHash, randomBytes as randomBytes4 } from "crypto";
|
|
1830
|
+
import { readFileSync as readFileSync5, statSync as statSync2 } from "fs";
|
|
1831
|
+
import { basename as basename2, extname } from "path";
|
|
1832
|
+
var CDN_BASE_URL2 = "https://novac2c.cdn.weixin.qq.com/c2c";
|
|
1833
|
+
var UPLOAD_MAX_RETRIES = 3;
|
|
1834
|
+
var SEND_MAX_RETRIES = 2;
|
|
1835
|
+
var IMAGE_EXTENSIONS = {
|
|
1836
|
+
".png": true,
|
|
1837
|
+
".jpg": true,
|
|
1838
|
+
".jpeg": true,
|
|
1839
|
+
".gif": true,
|
|
1840
|
+
".webp": true,
|
|
1841
|
+
".bmp": true
|
|
1842
|
+
};
|
|
1843
|
+
function mediaTypeFor(fileName) {
|
|
1844
|
+
return IMAGE_EXTENSIONS[extname(fileName).toLowerCase()] ? 1 /* IMAGE */ : 3 /* FILE */;
|
|
1845
|
+
}
|
|
1846
|
+
async function uploadAndSendFile(creds, to, contextToken, filePath, maxSizeBytes = 100 * 1024 * 1024) {
|
|
1847
|
+
let size;
|
|
1848
|
+
try {
|
|
1849
|
+
size = statSync2(filePath).size;
|
|
1850
|
+
} catch (err) {
|
|
1851
|
+
return { status: "error", error: `cannot stat: ${err instanceof Error ? err.message : String(err)}` };
|
|
1852
|
+
}
|
|
1853
|
+
if (size <= 0) {
|
|
1854
|
+
return { status: "error", error: "empty file" };
|
|
1855
|
+
}
|
|
1856
|
+
if (size > maxSizeBytes) {
|
|
1857
|
+
return { status: "too-large", bytes: size, maxBytes: maxSizeBytes };
|
|
1858
|
+
}
|
|
1859
|
+
const fileName = basename2(filePath);
|
|
1860
|
+
const mediaType = mediaTypeFor(fileName);
|
|
1861
|
+
const data = readFileSync5(filePath);
|
|
1862
|
+
try {
|
|
1863
|
+
const aesKey = generateAesKey();
|
|
1864
|
+
const ciphertext = encryptAesEcb(data, aesKey);
|
|
1865
|
+
const filekey = randomBytes4(16).toString("hex");
|
|
1866
|
+
const rawMd5 = createHash("md5").update(data).digest("hex");
|
|
1867
|
+
const uploadParams = await apiFetch(creds, "ilink/bot/getuploadurl", {
|
|
1868
|
+
filekey,
|
|
1869
|
+
media_type: mediaType,
|
|
1870
|
+
to_user_id: to,
|
|
1871
|
+
rawsize: data.length,
|
|
1872
|
+
rawfilemd5: rawMd5,
|
|
1873
|
+
filesize: ciphertext.length,
|
|
1874
|
+
no_need_thumb: true,
|
|
1875
|
+
aeskey: aesKey.toString("hex"),
|
|
1876
|
+
base_info: { channel_version: "0.1.0" }
|
|
1877
|
+
}, 15000);
|
|
1878
|
+
const uploadFullUrl = uploadParams.upload_full_url?.trim();
|
|
1879
|
+
if (!uploadFullUrl && !uploadParams.upload_param) {
|
|
1880
|
+
return { status: "error", error: "getuploadurl returned no upload URL" };
|
|
1881
|
+
}
|
|
1882
|
+
const uploadUrl = uploadFullUrl || `${CDN_BASE_URL2}/upload?encrypted_query_param=${encodeURIComponent(uploadParams.upload_param)}&filekey=${encodeURIComponent(filekey)}`;
|
|
1883
|
+
let encryptQueryParam;
|
|
1884
|
+
let lastError;
|
|
1885
|
+
for (let attempt = 1;attempt <= UPLOAD_MAX_RETRIES; attempt++) {
|
|
1886
|
+
try {
|
|
1887
|
+
const resp = await fetch(uploadUrl, {
|
|
1888
|
+
method: "POST",
|
|
1889
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
1890
|
+
body: new Uint8Array(ciphertext),
|
|
1891
|
+
signal: AbortSignal.timeout(60000)
|
|
1892
|
+
});
|
|
1893
|
+
if (resp.status >= 400 && resp.status < 500) {
|
|
1894
|
+
return {
|
|
1895
|
+
status: "error",
|
|
1896
|
+
error: `CDN upload client error ${resp.status}: ${resp.headers.get("x-error-message") ?? "rejected"}`
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
if (!resp.ok) {
|
|
1900
|
+
throw new Error(`CDN upload server error: ${resp.status}`);
|
|
1901
|
+
}
|
|
1902
|
+
encryptQueryParam = resp.headers.get("x-encrypted-param") ?? undefined;
|
|
1903
|
+
if (!encryptQueryParam) {
|
|
1904
|
+
throw new Error("CDN upload response missing x-encrypted-param header");
|
|
1905
|
+
}
|
|
1906
|
+
break;
|
|
1907
|
+
} catch (err) {
|
|
1908
|
+
lastError = err;
|
|
1909
|
+
if (attempt < UPLOAD_MAX_RETRIES) {
|
|
1910
|
+
logger.warn(`CDN upload attempt ${attempt}/${UPLOAD_MAX_RETRIES} failed, retrying:`, err);
|
|
1911
|
+
await Bun.sleep(1000 * attempt);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
if (!encryptQueryParam) {
|
|
1916
|
+
return {
|
|
1917
|
+
status: "error",
|
|
1918
|
+
error: `CDN upload failed after ${UPLOAD_MAX_RETRIES} attempts: ${lastError instanceof Error ? lastError.message : String(lastError)}`
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
const media = {
|
|
1922
|
+
encrypt_query_param: encryptQueryParam,
|
|
1923
|
+
aes_key: aesKey.toString("base64"),
|
|
1924
|
+
encrypt_type: 1
|
|
1925
|
+
};
|
|
1926
|
+
const item = mediaType === 1 /* IMAGE */ ? {
|
|
1927
|
+
type: 2,
|
|
1928
|
+
image_item: { media, mid_size: ciphertext.length }
|
|
1929
|
+
} : {
|
|
1930
|
+
type: 4,
|
|
1931
|
+
file_item: {
|
|
1932
|
+
media,
|
|
1933
|
+
file_name: fileName,
|
|
1934
|
+
md5: rawMd5,
|
|
1935
|
+
len: String(data.length)
|
|
1936
|
+
}
|
|
1937
|
+
};
|
|
1938
|
+
let lastSendError;
|
|
1939
|
+
for (let attempt = 0;attempt <= SEND_MAX_RETRIES; attempt++) {
|
|
1940
|
+
try {
|
|
1941
|
+
await apiFetch(creds, "ilink/bot/sendmessage", {
|
|
1942
|
+
msg: {
|
|
1943
|
+
from_user_id: "",
|
|
1944
|
+
to_user_id: to,
|
|
1945
|
+
client_id: `omp-wechat-${Date.now()}-${randomBytes4(4).toString("hex")}`,
|
|
1946
|
+
message_type: 2,
|
|
1947
|
+
message_state: 2,
|
|
1948
|
+
item_list: [item],
|
|
1949
|
+
context_token: contextToken
|
|
1950
|
+
},
|
|
1951
|
+
base_info: { channel_version: "0.1.0" }
|
|
1952
|
+
}, 15000);
|
|
1953
|
+
logger.info(`Sent ${mediaType === 1 /* IMAGE */ ? "image" : "file"} to ${to}: ${fileName} (${data.length} bytes)`);
|
|
1954
|
+
return { status: "sent", mediaType, bytes: data.length };
|
|
1955
|
+
} catch (err) {
|
|
1956
|
+
lastSendError = err;
|
|
1957
|
+
if (attempt < SEND_MAX_RETRIES) {
|
|
1958
|
+
logger.warn(`Send retry ${attempt + 1}/${SEND_MAX_RETRIES}:`, err);
|
|
1959
|
+
await Bun.sleep(1000 * (attempt + 1));
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
return {
|
|
1964
|
+
status: "error",
|
|
1965
|
+
error: `send failed: ${lastSendError instanceof Error ? lastSendError.message : String(lastSendError)}`
|
|
1966
|
+
};
|
|
1967
|
+
} catch (err) {
|
|
1968
|
+
return { status: "error", error: err instanceof Error ? err.message : String(err) };
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
// src/utils/markdown.ts
|
|
1973
|
+
function stripMarkdown(text) {
|
|
1974
|
+
let result = text;
|
|
1975
|
+
result = result.replace(/```[^\n]*\n?([\s\S]*?)```/g, (_match, code) => code.trim());
|
|
1976
|
+
result = result.replace(/`([^`]+)`/g, "$1");
|
|
1977
|
+
result = result.replace(/!\[[^\]]*\]\([^)]*\)/g, "");
|
|
1978
|
+
result = result.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
|
|
1979
|
+
result = result.replace(/^\|[\s:|-]+\|$/gm, "");
|
|
1980
|
+
result = result.replace(/^\|(.+)\|$/gm, (_match, inner) => inner.split("|").map((cell) => cell.trim()).join(" "));
|
|
1981
|
+
result = result.replace(/^#{1,6}\s+/gm, "");
|
|
1982
|
+
result = result.replace(/^[-*_]{3,}\s*$/gm, "");
|
|
1983
|
+
result = result.replace(/\*\*\*(.+?)\*\*\*/g, "$1");
|
|
1984
|
+
result = result.replace(/\*\*(.+?)\*\*/g, "$1");
|
|
1985
|
+
result = result.replace(/\*(.+?)\*/g, "$1");
|
|
1986
|
+
result = result.replace(/___(.+?)___/g, "$1");
|
|
1987
|
+
result = result.replace(/__(.+?)__/g, "$1");
|
|
1988
|
+
result = result.replace(/_(.+?)_/g, "$1");
|
|
1989
|
+
result = result.replace(/~~(.+?)~~/g, "$1");
|
|
1990
|
+
result = result.replace(/^>\s?/gm, "");
|
|
1991
|
+
result = result.replace(/^[\s]*[-*+]\s+/gm, "\u2022 ");
|
|
1992
|
+
result = result.replace(/^[\s]*\d+\.\s+/gm, "");
|
|
1993
|
+
result = result.replace(/<[^>]+>/g, "");
|
|
1994
|
+
result = result.replace(/\n{3,}/g, `
|
|
1995
|
+
|
|
1996
|
+
`);
|
|
1997
|
+
return result.trim();
|
|
1998
|
+
}
|
|
1999
|
+
|
|
1799
2000
|
// src/engine/session.ts
|
|
1800
2001
|
import { createAgentSession, SessionManager } from "@oh-my-pi/pi-coding-agent";
|
|
1801
2002
|
|
|
1802
2003
|
// src/engine/session-store.ts
|
|
1803
2004
|
import { join as join7 } from "path";
|
|
1804
2005
|
import { homedir as homedir6 } from "os";
|
|
1805
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync5, readdirSync as readdirSync2, statSync as
|
|
2006
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync5, readdirSync as readdirSync2, statSync as statSync3, rmSync } from "fs";
|
|
1806
2007
|
var STATE_DIR4 = join7(homedir6(), ".omp-wechat");
|
|
1807
2008
|
var SESSIONS_DIR = join7(STATE_DIR4, "sessions");
|
|
1808
2009
|
var STALE_THRESHOLD_MS = 30 * 24 * 60 * 60 * 1000;
|
|
@@ -1839,7 +2040,7 @@ function cleanupStaleSessions() {
|
|
|
1839
2040
|
let newestMtime = 0;
|
|
1840
2041
|
try {
|
|
1841
2042
|
for (const file of readdirSync2(dir)) {
|
|
1842
|
-
const mtime =
|
|
2043
|
+
const mtime = statSync3(join7(dir, file)).mtimeMs;
|
|
1843
2044
|
if (mtime > newestMtime)
|
|
1844
2045
|
newestMtime = mtime;
|
|
1845
2046
|
}
|
|
@@ -1879,6 +2080,28 @@ function clearAllSessions() {
|
|
|
1879
2080
|
}
|
|
1880
2081
|
|
|
1881
2082
|
// src/engine/session.ts
|
|
2083
|
+
import { mkdirSync as mkdirSync6, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
|
|
2084
|
+
import { homedir as homedir7 } from "os";
|
|
2085
|
+
import { join as join8 } from "path";
|
|
2086
|
+
var DEFAULT_OUTBOX_BASE = join8(homedir7(), ".omp-wechat", "outbox");
|
|
2087
|
+
function sanitizeChatId2(chatId) {
|
|
2088
|
+
return chatId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
2089
|
+
}
|
|
2090
|
+
function outboxDirFor(chatId, config) {
|
|
2091
|
+
return join8(config.outboxDir ?? DEFAULT_OUTBOX_BASE, sanitizeChatId2(chatId));
|
|
2092
|
+
}
|
|
2093
|
+
function outboxInstructions(outboxDir) {
|
|
2094
|
+
return `## File delivery to WeChat
|
|
2095
|
+
|
|
2096
|
+
You can send files (documents, spreadsheets, images, PDFs, code, etc.) to the WeChat user.
|
|
2097
|
+
|
|
2098
|
+
Rules:
|
|
2099
|
+
- Write each final deliverable to the outbox directory:
|
|
2100
|
+
${outboxDir}
|
|
2101
|
+
- Only final deliverables belong there \u2014 never intermediate or scratch files.
|
|
2102
|
+
- You may write multiple files; every new file in the outbox is delivered to the user after your turn.
|
|
2103
|
+
- Mention the file name(s) in your reply so the user knows what to expect.`;
|
|
2104
|
+
}
|
|
1882
2105
|
function extractAssistantText(message) {
|
|
1883
2106
|
const content = message.content;
|
|
1884
2107
|
if (!Array.isArray(content))
|
|
@@ -1899,14 +2122,30 @@ class ChatSession {
|
|
|
1899
2122
|
contextToken;
|
|
1900
2123
|
lastActive;
|
|
1901
2124
|
replyCount = 0;
|
|
1902
|
-
|
|
2125
|
+
outboxDir;
|
|
2126
|
+
sendFilesEnabled;
|
|
2127
|
+
onFiles;
|
|
2128
|
+
outboxSnapshot = new Map;
|
|
2129
|
+
constructor(session, chatId, contextToken, outboxDir, sendFilesEnabled, onFiles) {
|
|
1903
2130
|
this.session = session;
|
|
1904
2131
|
this.chatId = chatId;
|
|
1905
2132
|
this.contextToken = contextToken;
|
|
1906
2133
|
this.lastActive = Date.now();
|
|
2134
|
+
this.outboxDir = outboxDir;
|
|
2135
|
+
this.sendFilesEnabled = sendFilesEnabled;
|
|
2136
|
+
this.onFiles = onFiles;
|
|
1907
2137
|
}
|
|
1908
|
-
static async create(chatId, contextToken, config, onReply) {
|
|
2138
|
+
static async create(chatId, contextToken, config, onReply, onFiles) {
|
|
1909
2139
|
logger.info(`Creating session: ${chatId}`);
|
|
2140
|
+
const outboxDir = outboxDirFor(chatId, config);
|
|
2141
|
+
const sendFilesEnabled = config.sendFiles !== false;
|
|
2142
|
+
if (sendFilesEnabled) {
|
|
2143
|
+
try {
|
|
2144
|
+
mkdirSync6(outboxDir, { recursive: true });
|
|
2145
|
+
} catch (err) {
|
|
2146
|
+
logger.warn(`[${chatId}] Could not create outbox dir ${outboxDir}:`, err);
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
1910
2149
|
ensureSessionsDir();
|
|
1911
2150
|
const sessionDir = sessionDirFor(chatId);
|
|
1912
2151
|
const sessionManager = await SessionManager.continueRecent(config.cwd || process.cwd(), sessionDir);
|
|
@@ -1915,17 +2154,23 @@ class ChatSession {
|
|
|
1915
2154
|
sessionManager,
|
|
1916
2155
|
enableMCP: false,
|
|
1917
2156
|
enableLsp: false,
|
|
1918
|
-
systemPrompt: config.systemPrompt
|
|
2157
|
+
systemPrompt: sendFilesEnabled ? `${config.systemPrompt}
|
|
2158
|
+
|
|
2159
|
+
${outboxInstructions(outboxDir)}` : config.systemPrompt,
|
|
1919
2160
|
modelPattern: config.model
|
|
1920
2161
|
});
|
|
1921
2162
|
session.setAdvisorEnabled(false);
|
|
1922
2163
|
if (modelFallbackMessage) {
|
|
1923
2164
|
logger.warn(`Model fallback: ${modelFallbackMessage}`);
|
|
1924
2165
|
}
|
|
1925
|
-
const wrapper = new ChatSession(session, chatId, contextToken);
|
|
2166
|
+
const wrapper = new ChatSession(session, chatId, contextToken, outboxDir, sendFilesEnabled, onFiles);
|
|
1926
2167
|
const visionRole = session.settings.getModelRole("vision");
|
|
1927
2168
|
logger.info(`[${chatId}] Model: ${session.model?.id ?? "unknown"}, vision role: ${visionRole ?? "(not configured)"}`);
|
|
1928
2169
|
session.subscribe((event) => {
|
|
2170
|
+
if (event.type === "turn_end") {
|
|
2171
|
+
wrapper.flushOutbox();
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
1929
2174
|
if (event.type !== "message_end")
|
|
1930
2175
|
return;
|
|
1931
2176
|
if (event.message.role !== "assistant")
|
|
@@ -1941,8 +2186,39 @@ class ChatSession {
|
|
|
1941
2186
|
}
|
|
1942
2187
|
async prompt(text, images) {
|
|
1943
2188
|
this.lastActive = Date.now();
|
|
2189
|
+
this.outboxSnapshot = this.snapshotOutbox();
|
|
1944
2190
|
await this.session.prompt(text, images?.length ? { images } : undefined);
|
|
1945
2191
|
}
|
|
2192
|
+
flushOutbox() {
|
|
2193
|
+
if (!this.sendFilesEnabled)
|
|
2194
|
+
return;
|
|
2195
|
+
const current = this.snapshotOutbox();
|
|
2196
|
+
const fresh = [];
|
|
2197
|
+
for (const [name, stat] of current) {
|
|
2198
|
+
const prev = this.outboxSnapshot.get(name);
|
|
2199
|
+
if (!prev || prev.size !== stat.size || prev.mtimeMs !== stat.mtimeMs) {
|
|
2200
|
+
fresh.push(join8(this.outboxDir, name));
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
this.outboxSnapshot = current;
|
|
2204
|
+
if (fresh.length > 0) {
|
|
2205
|
+
logger.info(`[${this.chatId}] Outbox: ${fresh.length} new file(s) for delivery`);
|
|
2206
|
+
this.onFiles(this.chatId, fresh);
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
snapshotOutbox() {
|
|
2210
|
+
const map = new Map;
|
|
2211
|
+
try {
|
|
2212
|
+
for (const name of readdirSync3(this.outboxDir)) {
|
|
2213
|
+
try {
|
|
2214
|
+
const st = statSync4(join8(this.outboxDir, name));
|
|
2215
|
+
if (st.isFile())
|
|
2216
|
+
map.set(name, { size: st.size, mtimeMs: st.mtimeMs });
|
|
2217
|
+
} catch {}
|
|
2218
|
+
}
|
|
2219
|
+
} catch {}
|
|
2220
|
+
return map;
|
|
2221
|
+
}
|
|
1946
2222
|
supportsVision() {
|
|
1947
2223
|
return this.session.settings.getModelRole("vision") !== undefined;
|
|
1948
2224
|
}
|
|
@@ -1965,9 +2241,11 @@ class SessionPool {
|
|
|
1965
2241
|
pool = new Map;
|
|
1966
2242
|
maxSessions;
|
|
1967
2243
|
replyHandler;
|
|
1968
|
-
|
|
2244
|
+
fileHandler;
|
|
2245
|
+
constructor(maxSessions, replyHandler, fileHandler) {
|
|
1969
2246
|
this.maxSessions = maxSessions;
|
|
1970
2247
|
this.replyHandler = replyHandler;
|
|
2248
|
+
this.fileHandler = fileHandler;
|
|
1971
2249
|
}
|
|
1972
2250
|
setMaxSessions(n) {
|
|
1973
2251
|
this.maxSessions = n;
|
|
@@ -1981,7 +2259,7 @@ class SessionPool {
|
|
|
1981
2259
|
if (this.pool.size >= this.maxSessions) {
|
|
1982
2260
|
this.evictOldest();
|
|
1983
2261
|
}
|
|
1984
|
-
entry = await ChatSession.create(chatId, contextToken, config, this.replyHandler);
|
|
2262
|
+
entry = await ChatSession.create(chatId, contextToken, config, this.replyHandler, this.fileHandler);
|
|
1985
2263
|
this.pool.set(chatId, entry);
|
|
1986
2264
|
return entry;
|
|
1987
2265
|
}
|
|
@@ -2223,6 +2501,34 @@ var RETRY_MS = 2000;
|
|
|
2223
2501
|
var MAX_SEND_RETRIES = 2;
|
|
2224
2502
|
var CHUNK_LIMIT = 2000;
|
|
2225
2503
|
var LOCK_PORT = 19821;
|
|
2504
|
+
var FILE_TEXT_LIMIT = 1e4;
|
|
2505
|
+
var TEXT_FILE_EXTENSIONS = {
|
|
2506
|
+
".txt": true,
|
|
2507
|
+
".md": true,
|
|
2508
|
+
".csv": true,
|
|
2509
|
+
".json": true,
|
|
2510
|
+
".xml": true,
|
|
2511
|
+
".html": true,
|
|
2512
|
+
".yaml": true,
|
|
2513
|
+
".yml": true,
|
|
2514
|
+
".toml": true,
|
|
2515
|
+
".log": true,
|
|
2516
|
+
".py": true,
|
|
2517
|
+
".js": true,
|
|
2518
|
+
".jsx": true,
|
|
2519
|
+
".ts": true,
|
|
2520
|
+
".tsx": true,
|
|
2521
|
+
".go": true,
|
|
2522
|
+
".rs": true,
|
|
2523
|
+
".java": true,
|
|
2524
|
+
".c": true,
|
|
2525
|
+
".cpp": true,
|
|
2526
|
+
".h": true,
|
|
2527
|
+
".sh": true,
|
|
2528
|
+
".bash": true,
|
|
2529
|
+
".sql": true,
|
|
2530
|
+
".css": true
|
|
2531
|
+
};
|
|
2226
2532
|
|
|
2227
2533
|
class WeChatBridge {
|
|
2228
2534
|
state = null;
|
|
@@ -2257,7 +2563,12 @@ class WeChatBridge {
|
|
|
2257
2563
|
logger.error(`[${chatId}] Reply send failed:`, err);
|
|
2258
2564
|
});
|
|
2259
2565
|
};
|
|
2260
|
-
|
|
2566
|
+
const fileHandler = (chatId, files) => {
|
|
2567
|
+
this.sendFiles(creds, chatId, files).catch((err) => {
|
|
2568
|
+
logger.error(`[${chatId}] File delivery failed:`, err);
|
|
2569
|
+
});
|
|
2570
|
+
};
|
|
2571
|
+
this.pool = new SessionPool(config.maxSessions, replyHandler, fileHandler);
|
|
2261
2572
|
this.state = { running: true, config, creds, lastError: null };
|
|
2262
2573
|
logger.info("OMP-Wechat poll loop starting", {
|
|
2263
2574
|
maxSessions: config.maxSessions,
|
|
@@ -2402,7 +2713,11 @@ class WeChatBridge {
|
|
|
2402
2713
|
if (!text && !hasImages)
|
|
2403
2714
|
return;
|
|
2404
2715
|
const images = await this.downloadImages(creds, msg, senderId);
|
|
2405
|
-
await
|
|
2716
|
+
const fileTexts = await this.downloadFileTexts(creds, msg, senderId);
|
|
2717
|
+
const fullText = [text, ...fileTexts].filter(Boolean).join(`
|
|
2718
|
+
|
|
2719
|
+
`);
|
|
2720
|
+
await session.prompt(fullText, images);
|
|
2406
2721
|
} catch (err) {
|
|
2407
2722
|
logger.error(`[${senderId}] prompt failed:`, err);
|
|
2408
2723
|
await this.sendReply(creds, senderId, "Processing failed, please try again.");
|
|
@@ -2435,15 +2750,71 @@ class WeChatBridge {
|
|
|
2435
2750
|
}
|
|
2436
2751
|
return results;
|
|
2437
2752
|
}
|
|
2753
|
+
async sendFiles(creds, chatId, files) {
|
|
2754
|
+
const contextToken = this.pool?.getContextToken(chatId) ?? "";
|
|
2755
|
+
if (!contextToken) {
|
|
2756
|
+
logger.warn(`[${chatId}] No context_token, cannot send files`);
|
|
2757
|
+
return;
|
|
2758
|
+
}
|
|
2759
|
+
const config = loadConfig();
|
|
2760
|
+
const maxBytes = (config.maxFileSizeMb ?? 100) * 1024 * 1024;
|
|
2761
|
+
for (const filePath of files) {
|
|
2762
|
+
const result = await uploadAndSendFile(creds, chatId, contextToken, filePath, maxBytes);
|
|
2763
|
+
switch (result.status) {
|
|
2764
|
+
case "sent":
|
|
2765
|
+
logger.info(`[${chatId}] Delivered: ${filePath}`);
|
|
2766
|
+
try {
|
|
2767
|
+
rmSync2(filePath, { force: true });
|
|
2768
|
+
} catch (err) {
|
|
2769
|
+
logger.warn(`[${chatId}] Could not remove delivered file ${filePath}:`, err);
|
|
2770
|
+
}
|
|
2771
|
+
break;
|
|
2772
|
+
case "too-large":
|
|
2773
|
+
logger.warn(`[${chatId}] File too large, skipped: ${filePath}`);
|
|
2774
|
+
await this.sendReply(creds, chatId, `[File not sent: ${basename3(filePath)} exceeds ${Math.round(result.maxBytes / 1024 / 1024)}MB limit]`);
|
|
2775
|
+
break;
|
|
2776
|
+
case "error":
|
|
2777
|
+
logger.error(`[${chatId}] File send failed: ${filePath}: ${result.error}`);
|
|
2778
|
+
await this.sendReply(creds, chatId, `[File delivery failed: ${basename3(filePath)} \u2014 ${result.error}]`);
|
|
2779
|
+
break;
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
async downloadFileTexts(_creds, msg, chatId) {
|
|
2784
|
+
const items = (msg.item_list ?? []).filter((item) => item.type === 4);
|
|
2785
|
+
if (items.length === 0)
|
|
2786
|
+
return [];
|
|
2787
|
+
const results = [];
|
|
2788
|
+
for (const item of items) {
|
|
2789
|
+
const file = item.file_item;
|
|
2790
|
+
const name = file.file_name ?? "unknown";
|
|
2791
|
+
if (!TEXT_FILE_EXTENSIONS[extname2(name).toLowerCase()])
|
|
2792
|
+
continue;
|
|
2793
|
+
const buf = await downloadAndDecrypt(file.media?.encrypt_query_param, file.media?.full_url, undefined, file.media?.aes_key, `file[${chatId}]`);
|
|
2794
|
+
if (!buf)
|
|
2795
|
+
continue;
|
|
2796
|
+
const len = parseInt(file.len ?? "", 10);
|
|
2797
|
+
const text = buf.toString("utf-8");
|
|
2798
|
+
const truncated = text.length > FILE_TEXT_LIMIT ? `${text.slice(0, FILE_TEXT_LIMIT)}
|
|
2799
|
+
... [truncated]` : text;
|
|
2800
|
+
results.push(`[File: ${name}${Number.isFinite(len) && len > 0 ? ` (${formatSize(len)})` : ""}]
|
|
2801
|
+
|
|
2802
|
+
\`\`\`
|
|
2803
|
+
${truncated}
|
|
2804
|
+
\`\`\``);
|
|
2805
|
+
logger.info(`[${chatId}] Extracted text from file: ${name} (${buf.length} bytes)`);
|
|
2806
|
+
}
|
|
2807
|
+
return results;
|
|
2808
|
+
}
|
|
2438
2809
|
async sendReply(creds, chatId, text) {
|
|
2439
2810
|
const contextToken = this.pool?.getContextToken(chatId) ?? "";
|
|
2440
2811
|
if (!contextToken) {
|
|
2441
2812
|
logger.warn(`[${chatId}] No context_token, cannot reply`);
|
|
2442
2813
|
return;
|
|
2443
2814
|
}
|
|
2444
|
-
const chunks = chunkText(text, CHUNK_LIMIT);
|
|
2815
|
+
const chunks = chunkText(stripMarkdown(text), CHUNK_LIMIT);
|
|
2445
2816
|
for (const chunk of chunks) {
|
|
2446
|
-
const clientId = `omp-wechat-${Date.now()}-${
|
|
2817
|
+
const clientId = `omp-wechat-${Date.now()}-${randomBytes5(4).toString("hex")}`;
|
|
2447
2818
|
let retries = 0;
|
|
2448
2819
|
while (retries <= MAX_SEND_RETRIES) {
|
|
2449
2820
|
try {
|
|
@@ -2464,9 +2835,9 @@ class WeChatBridge {
|
|
|
2464
2835
|
}
|
|
2465
2836
|
|
|
2466
2837
|
// src/service.ts
|
|
2467
|
-
import { platform, homedir as
|
|
2468
|
-
import { join as
|
|
2469
|
-
import { existsSync as existsSync3, mkdirSync as
|
|
2838
|
+
import { platform, homedir as homedir8 } from "os";
|
|
2839
|
+
import { join as join9, basename as basename4 } from "path";
|
|
2840
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync7, writeFileSync as writeFileSync4, rmSync as rmSync3 } from "fs";
|
|
2470
2841
|
var PLIST_LABEL = "com.omp-wechat";
|
|
2471
2842
|
var SERVICE_NAME = "omp-wechat";
|
|
2472
2843
|
function detectPlatform() {
|
|
@@ -2480,14 +2851,14 @@ function detectPlatform() {
|
|
|
2480
2851
|
return "other";
|
|
2481
2852
|
}
|
|
2482
2853
|
function getLogDir() {
|
|
2483
|
-
return
|
|
2854
|
+
return join9(homedir8(), ".omp", "logs");
|
|
2484
2855
|
}
|
|
2485
2856
|
function resolveHostBinary() {
|
|
2486
2857
|
const exe = process.execPath || "omp";
|
|
2487
|
-
return
|
|
2858
|
+
return basename4(exe);
|
|
2488
2859
|
}
|
|
2489
2860
|
function plistPath() {
|
|
2490
|
-
return
|
|
2861
|
+
return join9(homedir8(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
|
|
2491
2862
|
}
|
|
2492
2863
|
function generatePlist() {
|
|
2493
2864
|
const omp = resolveHostBinary();
|
|
@@ -2523,16 +2894,16 @@ function generatePlist() {
|
|
|
2523
2894
|
<key>PATH</key>
|
|
2524
2895
|
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
2525
2896
|
<key>HOME</key>
|
|
2526
|
-
<string>${
|
|
2897
|
+
<string>${homedir8()}</string>
|
|
2527
2898
|
</dict>
|
|
2528
2899
|
</dict>
|
|
2529
2900
|
</plist>
|
|
2530
2901
|
`;
|
|
2531
2902
|
}
|
|
2532
2903
|
function installLaunchd() {
|
|
2533
|
-
const dir =
|
|
2534
|
-
|
|
2535
|
-
|
|
2904
|
+
const dir = join9(homedir8(), "Library", "LaunchAgents");
|
|
2905
|
+
mkdirSync7(dir, { recursive: true });
|
|
2906
|
+
mkdirSync7(getLogDir(), { recursive: true });
|
|
2536
2907
|
const plist = plistPath();
|
|
2537
2908
|
if (existsSync3(plist)) {
|
|
2538
2909
|
Bun.spawnSync(["launchctl", "unload", plist], { stderr: "ignore" });
|
|
@@ -2550,7 +2921,7 @@ function uninstallLaunchd() {
|
|
|
2550
2921
|
throw new Error("No launchd service found (may not be installed)");
|
|
2551
2922
|
}
|
|
2552
2923
|
Bun.spawnSync(["launchctl", "unload", plist], { stderr: "inherit" });
|
|
2553
|
-
|
|
2924
|
+
rmSync3(plist);
|
|
2554
2925
|
}
|
|
2555
2926
|
function servicePath() {
|
|
2556
2927
|
return `/etc/systemd/system/${SERVICE_NAME}.service`;
|
|
@@ -2572,7 +2943,7 @@ ExecStart=/bin/sh -c 'while true; do echo "{\\"id\\":\\"ka\\",\\"type\\":\\"get_
|
|
|
2572
2943
|
Restart=always
|
|
2573
2944
|
RestartSec=10
|
|
2574
2945
|
|
|
2575
|
-
Environment=HOME=${
|
|
2946
|
+
Environment=HOME=${homedir8()}
|
|
2576
2947
|
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
|
2577
2948
|
|
|
2578
2949
|
StandardOutput=null
|
|
@@ -2581,7 +2952,7 @@ StandardError=append:${logDir}/rpc.log
|
|
|
2581
2952
|
NoNewPrivileges=true
|
|
2582
2953
|
ProtectSystem=strict
|
|
2583
2954
|
ProtectHome=read-only
|
|
2584
|
-
ReadWritePaths=${logDir} ${
|
|
2955
|
+
ReadWritePaths=${logDir} ${join9(homedir8(), ".omp-wechat")} ${join9(homedir8(), ".omp")}
|
|
2585
2956
|
PrivateTmp=true
|
|
2586
2957
|
|
|
2587
2958
|
[Install]
|
|
@@ -2590,7 +2961,7 @@ WantedBy=multi-user.target
|
|
|
2590
2961
|
}
|
|
2591
2962
|
function installSystemd() {
|
|
2592
2963
|
const svc = servicePath();
|
|
2593
|
-
|
|
2964
|
+
mkdirSync7(getLogDir(), { recursive: true });
|
|
2594
2965
|
if (existsSync3(svc)) {
|
|
2595
2966
|
Bun.spawnSync(["sudo", "systemctl", "stop", SERVICE_NAME], { stderr: "inherit" });
|
|
2596
2967
|
}
|
|
@@ -2600,7 +2971,7 @@ function installSystemd() {
|
|
|
2600
2971
|
if (result.exitCode !== 0) {
|
|
2601
2972
|
result = Bun.spawnSync(["sudo", "cp", tmp, svc], { stderr: "inherit" });
|
|
2602
2973
|
}
|
|
2603
|
-
|
|
2974
|
+
rmSync3(tmp);
|
|
2604
2975
|
if (result.exitCode !== 0) {
|
|
2605
2976
|
throw new Error("Failed to write service file (need sudo)");
|
|
2606
2977
|
}
|
|
@@ -2622,7 +2993,7 @@ function uninstallSystemd() {
|
|
|
2622
2993
|
}
|
|
2623
2994
|
var WIN_TASK_NAME = "OMP-Wechat";
|
|
2624
2995
|
function winScriptPath() {
|
|
2625
|
-
return
|
|
2996
|
+
return join9(homedir8(), ".omp-wechat", "omp-wechat-rpc.ps1");
|
|
2626
2997
|
}
|
|
2627
2998
|
function generateWinScript() {
|
|
2628
2999
|
const omp = process.execPath || "omp";
|
|
@@ -2665,8 +3036,8 @@ while ($true) {
|
|
|
2665
3036
|
`;
|
|
2666
3037
|
}
|
|
2667
3038
|
function installWinTask() {
|
|
2668
|
-
|
|
2669
|
-
|
|
3039
|
+
mkdirSync7(join9(homedir8(), ".omp-wechat"), { recursive: true });
|
|
3040
|
+
mkdirSync7(getLogDir(), { recursive: true });
|
|
2670
3041
|
writeFileSync4(winScriptPath(), generateWinScript());
|
|
2671
3042
|
Bun.spawnSync(["schtasks", "/end", "/tn", WIN_TASK_NAME], { stderr: "ignore" });
|
|
2672
3043
|
Bun.spawnSync(["schtasks", "/delete", "/tn", WIN_TASK_NAME, "/f"], { stderr: "ignore" });
|
|
@@ -2689,7 +3060,7 @@ function uninstallWinTask() {
|
|
|
2689
3060
|
}
|
|
2690
3061
|
const script = winScriptPath();
|
|
2691
3062
|
if (existsSync3(script)) {
|
|
2692
|
-
|
|
3063
|
+
rmSync3(script);
|
|
2693
3064
|
}
|
|
2694
3065
|
}
|
|
2695
3066
|
function winTaskExists() {
|