omp-wechat 1.1.0 → 1.2.1
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 +6 -2
- package/dist/index.js +308 -68
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -86,13 +86,14 @@ To remove: `/wechat uninstall`
|
|
|
86
86
|
|
|
87
87
|
## Configuration
|
|
88
88
|
|
|
89
|
-
Configuration is loaded from `~/.omp-wechat/config.yml`, falling back to built-in defaults.
|
|
89
|
+
Configuration is loaded from `~/.omp-wechat/config.yml`, falling back to built-in defaults.
|
|
90
90
|
|
|
91
91
|
```yaml
|
|
92
92
|
# ~/.omp-wechat/config.yml
|
|
93
93
|
maxSessions: 50
|
|
94
94
|
dmPolicy: pairing
|
|
95
95
|
model: "@smol" # default model (role alias or provider/id)
|
|
96
|
+
cwd: ~/projects/my-app # working directory for AI sessions
|
|
96
97
|
systemPrompt: |
|
|
97
98
|
You are an AI assistant chatting via WeChat.
|
|
98
99
|
Keep replies concise and in plain text.
|
|
@@ -103,8 +104,10 @@ systemPrompt: |
|
|
|
103
104
|
| `maxSessions` | `50` | Session pool cap (LRU eviction) |
|
|
104
105
|
| `dmPolicy` | `pairing` | Access policy: `pairing` / `allowlist` / `disabled` |
|
|
105
106
|
| `model` | OMP default | Default model: role alias (`@smol`, `@slow`) or `provider/id` |
|
|
107
|
+
| `cwd` | `process.cwd()` | Working directory for AI sessions — determines which project context (CLAUDE.md, .omp/) the agent loads |
|
|
106
108
|
| `systemPrompt` | Built-in | System prompt for WeChat chat sessions |
|
|
107
|
-
|
|
109
|
+
|
|
110
|
+
> **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.
|
|
108
111
|
|
|
109
112
|
## Slash Commands
|
|
110
113
|
|
|
@@ -127,6 +130,7 @@ systemPrompt: |
|
|
|
127
130
|
| `/model` | Show current AI model |
|
|
128
131
|
| `/models` | List all available models |
|
|
129
132
|
| `/model provider/id` | Switch model for this chat (e.g. `/model anthropic/claude-haiku-4-5`) |
|
|
133
|
+
| `/new` | Reset session — clear context and start fresh |
|
|
130
134
|
|
|
131
135
|
## Access Control
|
|
132
136
|
|
package/dist/index.js
CHANGED
|
@@ -1054,15 +1054,93 @@ import {
|
|
|
1054
1054
|
readFileSync,
|
|
1055
1055
|
writeFileSync,
|
|
1056
1056
|
mkdirSync as mkdirSync2,
|
|
1057
|
-
renameSync
|
|
1057
|
+
renameSync as renameSync2
|
|
1058
1058
|
} from "fs";
|
|
1059
1059
|
import { homedir as homedir2 } from "os";
|
|
1060
|
-
import { join as
|
|
1060
|
+
import { join as join3 } from "path";
|
|
1061
1061
|
|
|
1062
1062
|
// src/utils/logger.ts
|
|
1063
1063
|
import { homedir } from "os";
|
|
1064
|
-
import { join } from "path";
|
|
1065
|
-
import { mkdirSync
|
|
1064
|
+
import { join as join2 } from "path";
|
|
1065
|
+
import { mkdirSync } from "fs";
|
|
1066
|
+
|
|
1067
|
+
// src/utils/rotating-log.ts
|
|
1068
|
+
import { appendFileSync, statSync, renameSync, unlinkSync, readdirSync } from "fs";
|
|
1069
|
+
import { dirname, join, basename } from "path";
|
|
1070
|
+
var DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
|
|
1071
|
+
var DEFAULT_MAX_FILES = 3;
|
|
1072
|
+
var DEFAULT_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
1073
|
+
|
|
1074
|
+
class RotatingLog {
|
|
1075
|
+
filePath;
|
|
1076
|
+
maxBytes;
|
|
1077
|
+
maxFiles;
|
|
1078
|
+
maxAgeMs;
|
|
1079
|
+
baseName;
|
|
1080
|
+
dir;
|
|
1081
|
+
activeFileName;
|
|
1082
|
+
constructor(opts) {
|
|
1083
|
+
this.filePath = opts.filePath;
|
|
1084
|
+
this.maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
1085
|
+
this.maxFiles = opts.maxFiles ?? DEFAULT_MAX_FILES;
|
|
1086
|
+
this.maxAgeMs = opts.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
1087
|
+
this.dir = dirname(this.filePath);
|
|
1088
|
+
this.activeFileName = basename(this.filePath);
|
|
1089
|
+
this.baseName = basename(this.filePath, ".log");
|
|
1090
|
+
}
|
|
1091
|
+
write(line) {
|
|
1092
|
+
try {
|
|
1093
|
+
this.maybeRotate();
|
|
1094
|
+
appendFileSync(this.filePath, line, "utf-8");
|
|
1095
|
+
} catch {}
|
|
1096
|
+
}
|
|
1097
|
+
cleanStale() {
|
|
1098
|
+
const cutoff = Date.now() - this.maxAgeMs;
|
|
1099
|
+
try {
|
|
1100
|
+
for (const name of readdirSync(this.dir)) {
|
|
1101
|
+
if (name === this.activeFileName)
|
|
1102
|
+
continue;
|
|
1103
|
+
if (!name.startsWith(this.baseName) || !name.endsWith(".log"))
|
|
1104
|
+
continue;
|
|
1105
|
+
const fullPath = join(this.dir, name);
|
|
1106
|
+
try {
|
|
1107
|
+
const stat = statSync(fullPath);
|
|
1108
|
+
if (stat.mtimeMs < cutoff)
|
|
1109
|
+
unlinkSync(fullPath);
|
|
1110
|
+
} catch {}
|
|
1111
|
+
}
|
|
1112
|
+
} catch {}
|
|
1113
|
+
}
|
|
1114
|
+
maybeRotate() {
|
|
1115
|
+
let size;
|
|
1116
|
+
try {
|
|
1117
|
+
size = statSync(this.filePath).size;
|
|
1118
|
+
} catch {
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
if (size < this.maxBytes)
|
|
1122
|
+
return;
|
|
1123
|
+
const oldest = this.rotatedPath(this.maxFiles);
|
|
1124
|
+
try {
|
|
1125
|
+
unlinkSync(oldest);
|
|
1126
|
+
} catch {}
|
|
1127
|
+
for (let i = this.maxFiles - 1;i >= 1; i--) {
|
|
1128
|
+
const src = this.rotatedPath(i);
|
|
1129
|
+
const dst = this.rotatedPath(i + 1);
|
|
1130
|
+
try {
|
|
1131
|
+
renameSync(src, dst);
|
|
1132
|
+
} catch {}
|
|
1133
|
+
}
|
|
1134
|
+
try {
|
|
1135
|
+
renameSync(this.filePath, this.rotatedPath(1));
|
|
1136
|
+
} catch {}
|
|
1137
|
+
}
|
|
1138
|
+
rotatedPath(index) {
|
|
1139
|
+
return join(this.dir, `${this.baseName}.${index}.log`);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// src/utils/logger.ts
|
|
1066
1144
|
var LEVEL_ORDER = {
|
|
1067
1145
|
debug: 0,
|
|
1068
1146
|
info: 1,
|
|
@@ -1070,11 +1148,18 @@ var LEVEL_ORDER = {
|
|
|
1070
1148
|
error: 3
|
|
1071
1149
|
};
|
|
1072
1150
|
var minLevel = process.env.OMP_WECHAT_LOG ?? "info";
|
|
1073
|
-
var LOG_DIR =
|
|
1074
|
-
var LOG_FILE =
|
|
1151
|
+
var LOG_DIR = join2(homedir(), ".omp", "logs");
|
|
1152
|
+
var LOG_FILE = join2(LOG_DIR, "wechat.log");
|
|
1075
1153
|
try {
|
|
1076
1154
|
mkdirSync(LOG_DIR, { recursive: true });
|
|
1077
1155
|
} catch {}
|
|
1156
|
+
var rotatingLog = new RotatingLog({
|
|
1157
|
+
filePath: LOG_FILE,
|
|
1158
|
+
maxBytes: 5 * 1024 * 1024,
|
|
1159
|
+
maxFiles: 3,
|
|
1160
|
+
maxAgeMs: 30 * 24 * 60 * 60 * 1000
|
|
1161
|
+
});
|
|
1162
|
+
rotatingLog.cleanStale();
|
|
1078
1163
|
function ts() {
|
|
1079
1164
|
return new Date().toISOString();
|
|
1080
1165
|
}
|
|
@@ -1085,10 +1170,7 @@ function log(level, msg, meta) {
|
|
|
1085
1170
|
const line = meta !== undefined ? `${prefix} ${msg} ${JSON.stringify(meta)}
|
|
1086
1171
|
` : `${prefix} ${msg}
|
|
1087
1172
|
`;
|
|
1088
|
-
|
|
1089
|
-
try {
|
|
1090
|
-
appendFileSync(LOG_FILE, line);
|
|
1091
|
-
} catch {}
|
|
1173
|
+
rotatingLog.write(line);
|
|
1092
1174
|
}
|
|
1093
1175
|
var logger = {
|
|
1094
1176
|
debug: (msg, meta) => log("debug", msg, meta),
|
|
@@ -1098,9 +1180,9 @@ var logger = {
|
|
|
1098
1180
|
};
|
|
1099
1181
|
|
|
1100
1182
|
// src/ilink/client.ts
|
|
1101
|
-
var STATE_DIR =
|
|
1102
|
-
var CREDENTIALS_FILE =
|
|
1103
|
-
var SYNC_BUF_FILE =
|
|
1183
|
+
var STATE_DIR = join3(homedir2(), ".omp-wechat");
|
|
1184
|
+
var CREDENTIALS_FILE = join3(STATE_DIR, "credentials.json");
|
|
1185
|
+
var SYNC_BUF_FILE = join3(STATE_DIR, "sync_buf.txt");
|
|
1104
1186
|
function loadCredentials() {
|
|
1105
1187
|
try {
|
|
1106
1188
|
return JSON.parse(readFileSync(CREDENTIALS_FILE, "utf8"));
|
|
@@ -1113,7 +1195,7 @@ function saveCredentials(creds) {
|
|
|
1113
1195
|
const tmp = CREDENTIALS_FILE + ".tmp";
|
|
1114
1196
|
writeFileSync(tmp, JSON.stringify(creds, null, 2) + `
|
|
1115
1197
|
`, { mode: 384 });
|
|
1116
|
-
|
|
1198
|
+
renameSync2(tmp, CREDENTIALS_FILE);
|
|
1117
1199
|
}
|
|
1118
1200
|
function getCredentials() {
|
|
1119
1201
|
const creds = loadCredentials();
|
|
@@ -1240,9 +1322,10 @@ function saveSyncBuf(buf) {
|
|
|
1240
1322
|
mkdirSync2(STATE_DIR, { recursive: true });
|
|
1241
1323
|
writeFileSync(SYNC_BUF_FILE, buf);
|
|
1242
1324
|
}
|
|
1243
|
-
function extractInboundText(msg) {
|
|
1325
|
+
function extractInboundText(msg, includeImagePlaceholder = true) {
|
|
1244
1326
|
const items = msg.item_list ?? [];
|
|
1245
1327
|
const parts = [];
|
|
1328
|
+
let imgCount = 0;
|
|
1246
1329
|
for (const item of items) {
|
|
1247
1330
|
switch (item.type) {
|
|
1248
1331
|
case 1:
|
|
@@ -1250,7 +1333,7 @@ function extractInboundText(msg) {
|
|
|
1250
1333
|
parts.push(item.text_item.text);
|
|
1251
1334
|
break;
|
|
1252
1335
|
case 2:
|
|
1253
|
-
|
|
1336
|
+
imgCount++;
|
|
1254
1337
|
break;
|
|
1255
1338
|
case 3:
|
|
1256
1339
|
parts.push(item.voice_item?.text ?? "(voice)");
|
|
@@ -1263,9 +1346,17 @@ function extractInboundText(msg) {
|
|
|
1263
1346
|
break;
|
|
1264
1347
|
}
|
|
1265
1348
|
}
|
|
1349
|
+
if (includeImagePlaceholder && imgCount > 0 && parts.length === 0) {
|
|
1350
|
+
parts.push(`(user sent ${imgCount} image${imgCount > 1 ? "s" : ""})`);
|
|
1351
|
+
} else if (includeImagePlaceholder && imgCount > 0) {
|
|
1352
|
+
parts.push(`(+${imgCount} image${imgCount > 1 ? "s" : ""})`);
|
|
1353
|
+
}
|
|
1266
1354
|
return parts.join(`
|
|
1267
1355
|
`) || "";
|
|
1268
1356
|
}
|
|
1357
|
+
function extractInboundImages(msg) {
|
|
1358
|
+
return (msg.item_list ?? []).filter((item) => item.type === 2);
|
|
1359
|
+
}
|
|
1269
1360
|
|
|
1270
1361
|
// src/access/control.ts
|
|
1271
1362
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
@@ -1273,12 +1364,12 @@ import {
|
|
|
1273
1364
|
readFileSync as readFileSync2,
|
|
1274
1365
|
writeFileSync as writeFileSync2,
|
|
1275
1366
|
mkdirSync as mkdirSync3,
|
|
1276
|
-
renameSync as
|
|
1367
|
+
renameSync as renameSync3
|
|
1277
1368
|
} from "fs";
|
|
1278
1369
|
import { homedir as homedir3 } from "os";
|
|
1279
|
-
import { join as
|
|
1280
|
-
var STATE_DIR2 =
|
|
1281
|
-
var ACCESS_FILE =
|
|
1370
|
+
import { join as join4 } from "path";
|
|
1371
|
+
var STATE_DIR2 = join4(homedir3(), ".omp-wechat");
|
|
1372
|
+
var ACCESS_FILE = join4(STATE_DIR2, "access.json");
|
|
1282
1373
|
function defaultAccess() {
|
|
1283
1374
|
return { dmPolicy: "pairing", allowFrom: [], pending: {} };
|
|
1284
1375
|
}
|
|
@@ -1295,7 +1386,7 @@ function readAccessFile() {
|
|
|
1295
1386
|
if (err.code === "ENOENT")
|
|
1296
1387
|
return defaultAccess();
|
|
1297
1388
|
try {
|
|
1298
|
-
|
|
1389
|
+
renameSync3(ACCESS_FILE, `${ACCESS_FILE}.corrupt-${Date.now()}`);
|
|
1299
1390
|
} catch {}
|
|
1300
1391
|
logger.warn("access.json is corrupt, moved aside, using defaults");
|
|
1301
1392
|
return defaultAccess();
|
|
@@ -1306,7 +1397,7 @@ function saveAccess(a) {
|
|
|
1306
1397
|
const tmp = ACCESS_FILE + ".tmp";
|
|
1307
1398
|
writeFileSync2(tmp, JSON.stringify(a, null, 2) + `
|
|
1308
1399
|
`, { mode: 384 });
|
|
1309
|
-
|
|
1400
|
+
renameSync3(tmp, ACCESS_FILE);
|
|
1310
1401
|
}
|
|
1311
1402
|
function loadAccess() {
|
|
1312
1403
|
return readAccessFile();
|
|
@@ -1481,7 +1572,7 @@ import { randomBytes as randomBytes3 } from "crypto";
|
|
|
1481
1572
|
|
|
1482
1573
|
// src/config.ts
|
|
1483
1574
|
import { homedir as homedir4 } from "os";
|
|
1484
|
-
import { join as
|
|
1575
|
+
import { join as join5 } from "path";
|
|
1485
1576
|
import { existsSync, readFileSync as readFileSync3 } from "fs";
|
|
1486
1577
|
var DEFAULT_SYSTEM_PROMPT = `You are an AI assistant chatting with users via WeChat.
|
|
1487
1578
|
|
|
@@ -1490,8 +1581,8 @@ Constraints:
|
|
|
1490
1581
|
- Keep replies concise; WeChat has a ~2000 character limit per message
|
|
1491
1582
|
- If you need to write code or long documents, summarize the key points; the user will review on their computer
|
|
1492
1583
|
- Users may send short or incomplete messages from their phone; proactively understand their intent`;
|
|
1493
|
-
var CONFIG_DIR =
|
|
1494
|
-
var CONFIG_FILE =
|
|
1584
|
+
var CONFIG_DIR = join5(homedir4(), ".omp-wechat");
|
|
1585
|
+
var CONFIG_FILE = join5(CONFIG_DIR, "config.yml");
|
|
1495
1586
|
function loadConfig() {
|
|
1496
1587
|
const config = {
|
|
1497
1588
|
maxSessions: 50,
|
|
@@ -1506,16 +1597,25 @@ function loadConfig() {
|
|
|
1506
1597
|
config.maxSessions = parseInt(parsed.maxSessions, 10);
|
|
1507
1598
|
if (parsed.dmPolicy)
|
|
1508
1599
|
config.dmPolicy = parsed.dmPolicy;
|
|
1509
|
-
if (parsed.systemPrompt)
|
|
1510
|
-
config.systemPrompt = parsed.systemPrompt;
|
|
1511
1600
|
if (parsed.model)
|
|
1512
1601
|
config.model = parsed.model;
|
|
1602
|
+
if (parsed.cwd)
|
|
1603
|
+
config.cwd = expandTilde(parsed.cwd);
|
|
1604
|
+
if (parsed.systemPrompt)
|
|
1605
|
+
config.systemPrompt = parsed.systemPrompt;
|
|
1513
1606
|
}
|
|
1514
1607
|
} catch (err) {
|
|
1515
1608
|
logger.warn("Failed to load config.yml, using defaults", err);
|
|
1516
1609
|
}
|
|
1517
1610
|
return config;
|
|
1518
1611
|
}
|
|
1612
|
+
function expandTilde(p) {
|
|
1613
|
+
if (p.startsWith("~/"))
|
|
1614
|
+
return join5(homedir4(), p.slice(2));
|
|
1615
|
+
if (p === "~")
|
|
1616
|
+
return homedir4();
|
|
1617
|
+
return p;
|
|
1618
|
+
}
|
|
1519
1619
|
function parseSimpleYaml(yaml) {
|
|
1520
1620
|
const result = {};
|
|
1521
1621
|
let inMultiline = false;
|
|
@@ -1580,11 +1680,11 @@ function chunkText(text, limit = 2000) {
|
|
|
1580
1680
|
}
|
|
1581
1681
|
|
|
1582
1682
|
// src/utils/dedup.ts
|
|
1583
|
-
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4, renameSync as
|
|
1683
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4, renameSync as renameSync4 } from "fs";
|
|
1584
1684
|
import { homedir as homedir5 } from "os";
|
|
1585
|
-
import { join as
|
|
1586
|
-
var STATE_DIR3 =
|
|
1587
|
-
var DEDUP_FILE =
|
|
1685
|
+
import { join as join6 } from "path";
|
|
1686
|
+
var STATE_DIR3 = join6(homedir5(), ".omp-wechat");
|
|
1687
|
+
var DEDUP_FILE = join6(STATE_DIR3, "seen_msgs.json");
|
|
1588
1688
|
var MAX_ENTRIES = 500;
|
|
1589
1689
|
var DEDUP_TTL_MS = 5 * 60 * 1000;
|
|
1590
1690
|
var entries = null;
|
|
@@ -1617,7 +1717,7 @@ function persist(s) {
|
|
|
1617
1717
|
const tmp = DEDUP_FILE + ".tmp";
|
|
1618
1718
|
writeFileSync3(tmp, JSON.stringify(arr) + `
|
|
1619
1719
|
`, { mode: 384 });
|
|
1620
|
-
|
|
1720
|
+
renameSync4(tmp, DEDUP_FILE);
|
|
1621
1721
|
} catch (err) {
|
|
1622
1722
|
logger.debug(`dedup persist failed: ${err}`);
|
|
1623
1723
|
}
|
|
@@ -1637,38 +1737,107 @@ function isDuplicate(key) {
|
|
|
1637
1737
|
return false;
|
|
1638
1738
|
}
|
|
1639
1739
|
|
|
1740
|
+
// src/ilink/cdn.ts
|
|
1741
|
+
import { createDecipheriv } from "crypto";
|
|
1742
|
+
var CDN_BASE_URL = "https://novac2c.cdn.weixin.qq.com/c2c";
|
|
1743
|
+
function parseAesKey(aeskeyHex, aesKeyBase64) {
|
|
1744
|
+
if (aeskeyHex && /^[0-9a-fA-F]{32}$/.test(aeskeyHex)) {
|
|
1745
|
+
return Buffer.from(aeskeyHex, "hex");
|
|
1746
|
+
}
|
|
1747
|
+
if (!aesKeyBase64)
|
|
1748
|
+
return null;
|
|
1749
|
+
const decoded = Buffer.from(aesKeyBase64, "base64");
|
|
1750
|
+
if (decoded.length === 16)
|
|
1751
|
+
return decoded;
|
|
1752
|
+
if (decoded.length === 32 && /^[0-9a-fA-F]{32}$/.test(decoded.toString("ascii"))) {
|
|
1753
|
+
return Buffer.from(decoded.toString("ascii"), "hex");
|
|
1754
|
+
}
|
|
1755
|
+
return null;
|
|
1756
|
+
}
|
|
1757
|
+
function decryptAesEcb(ciphertext, key) {
|
|
1758
|
+
const decipher = createDecipheriv("aes-128-ecb", key, null);
|
|
1759
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
1760
|
+
}
|
|
1761
|
+
function buildCdnUrl(fileUrl, fullUrl) {
|
|
1762
|
+
if (fullUrl)
|
|
1763
|
+
return fullUrl;
|
|
1764
|
+
if (!fileUrl)
|
|
1765
|
+
return null;
|
|
1766
|
+
if (fileUrl.startsWith("http"))
|
|
1767
|
+
return fileUrl;
|
|
1768
|
+
return `${CDN_BASE_URL}/download?encrypted_query_param=${encodeURIComponent(fileUrl)}`;
|
|
1769
|
+
}
|
|
1770
|
+
async function downloadAndDecrypt(fileUrl, fullUrl, aeskeyHex, aesKeyBase64, label = "media") {
|
|
1771
|
+
const url = buildCdnUrl(fileUrl, fullUrl);
|
|
1772
|
+
if (!url) {
|
|
1773
|
+
logger.warn(`[${label}] No CDN URL available`);
|
|
1774
|
+
return null;
|
|
1775
|
+
}
|
|
1776
|
+
const key = parseAesKey(aeskeyHex, aesKeyBase64);
|
|
1777
|
+
if (!key) {
|
|
1778
|
+
logger.warn(`[${label}] Could not parse AES key`);
|
|
1779
|
+
return null;
|
|
1780
|
+
}
|
|
1781
|
+
try {
|
|
1782
|
+
const resp = await fetch(url);
|
|
1783
|
+
if (!resp.ok) {
|
|
1784
|
+
logger.warn(`[${label}] CDN download failed: ${resp.status} ${resp.statusText}`);
|
|
1785
|
+
return null;
|
|
1786
|
+
}
|
|
1787
|
+
const encrypted = Buffer.from(await resp.arrayBuffer());
|
|
1788
|
+
const plaintext = decryptAesEcb(encrypted, key);
|
|
1789
|
+
logger.info(`[${label}] Downloaded + decrypted: ${encrypted.length} \u2192 ${plaintext.length} bytes`);
|
|
1790
|
+
return plaintext;
|
|
1791
|
+
} catch (err) {
|
|
1792
|
+
logger.error(`[${label}] CDN download/decrypt error:`, err);
|
|
1793
|
+
return null;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1640
1797
|
// src/engine/session.ts
|
|
1641
1798
|
import { createAgentSession, SessionManager } from "@oh-my-pi/pi-coding-agent";
|
|
1642
1799
|
|
|
1643
1800
|
// src/engine/session-store.ts
|
|
1644
|
-
import { join as
|
|
1801
|
+
import { join as join7 } from "path";
|
|
1645
1802
|
import { homedir as homedir6 } from "os";
|
|
1646
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync5, readdirSync, statSync, rmSync } from "fs";
|
|
1647
|
-
var STATE_DIR4 =
|
|
1648
|
-
var SESSIONS_DIR =
|
|
1803
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync5, readdirSync as readdirSync2, statSync as statSync2, rmSync } from "fs";
|
|
1804
|
+
var STATE_DIR4 = join7(homedir6(), ".omp-wechat");
|
|
1805
|
+
var SESSIONS_DIR = join7(STATE_DIR4, "sessions");
|
|
1649
1806
|
var STALE_THRESHOLD_MS = 30 * 24 * 60 * 60 * 1000;
|
|
1650
1807
|
function sanitizeChatId(chatId) {
|
|
1651
1808
|
return chatId.replace(/[^a-zA-Z0-9_@.-]/g, "_").slice(0, 200);
|
|
1652
1809
|
}
|
|
1653
1810
|
function sessionDirFor(chatId) {
|
|
1654
|
-
return
|
|
1811
|
+
return join7(SESSIONS_DIR, sanitizeChatId(chatId));
|
|
1655
1812
|
}
|
|
1656
1813
|
function ensureSessionsDir() {
|
|
1657
1814
|
mkdirSync5(SESSIONS_DIR, { recursive: true, mode: 448 });
|
|
1658
1815
|
}
|
|
1816
|
+
function removeSessionDir(chatId) {
|
|
1817
|
+
const dir = sessionDirFor(chatId);
|
|
1818
|
+
if (!existsSync2(dir))
|
|
1819
|
+
return false;
|
|
1820
|
+
try {
|
|
1821
|
+
rmSync(dir, { recursive: true, force: true });
|
|
1822
|
+
return true;
|
|
1823
|
+
} catch (err) {
|
|
1824
|
+
logger.warn(`Failed to remove session dir ${dir}: ${err}`);
|
|
1825
|
+
return false;
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1659
1828
|
function cleanupStaleSessions() {
|
|
1660
1829
|
if (!existsSync2(SESSIONS_DIR))
|
|
1661
1830
|
return 0;
|
|
1662
1831
|
const now = Date.now();
|
|
1663
1832
|
let removed = 0;
|
|
1664
|
-
for (const entry of
|
|
1833
|
+
for (const entry of readdirSync2(SESSIONS_DIR, { withFileTypes: true })) {
|
|
1665
1834
|
if (!entry.isDirectory())
|
|
1666
1835
|
continue;
|
|
1667
|
-
const dir =
|
|
1836
|
+
const dir = join7(SESSIONS_DIR, entry.name);
|
|
1668
1837
|
let newestMtime = 0;
|
|
1669
1838
|
try {
|
|
1670
|
-
for (const file of
|
|
1671
|
-
const mtime =
|
|
1839
|
+
for (const file of readdirSync2(dir)) {
|
|
1840
|
+
const mtime = statSync2(join7(dir, file)).mtimeMs;
|
|
1672
1841
|
if (mtime > newestMtime)
|
|
1673
1842
|
newestMtime = mtime;
|
|
1674
1843
|
}
|
|
@@ -1694,11 +1863,11 @@ function clearAllSessions() {
|
|
|
1694
1863
|
if (!existsSync2(SESSIONS_DIR))
|
|
1695
1864
|
return 0;
|
|
1696
1865
|
let removed = 0;
|
|
1697
|
-
for (const entry of
|
|
1866
|
+
for (const entry of readdirSync2(SESSIONS_DIR, { withFileTypes: true })) {
|
|
1698
1867
|
if (!entry.isDirectory())
|
|
1699
1868
|
continue;
|
|
1700
1869
|
try {
|
|
1701
|
-
rmSync(
|
|
1870
|
+
rmSync(join7(SESSIONS_DIR, entry.name), { recursive: true, force: true });
|
|
1702
1871
|
removed++;
|
|
1703
1872
|
} catch (err) {
|
|
1704
1873
|
logger.warn(`Failed to remove session dir ${entry.name}: ${err}`);
|
|
@@ -1738,7 +1907,7 @@ class ChatSession {
|
|
|
1738
1907
|
logger.info(`Creating session: ${chatId}`);
|
|
1739
1908
|
ensureSessionsDir();
|
|
1740
1909
|
const sessionDir = sessionDirFor(chatId);
|
|
1741
|
-
const sessionManager = await SessionManager.continueRecent(process.cwd(), sessionDir);
|
|
1910
|
+
const sessionManager = await SessionManager.continueRecent(config.cwd || process.cwd(), sessionDir);
|
|
1742
1911
|
logger.info(`Session dir: ${sessionDir} (resumed=${sessionManager.getSessionFile() !== null})`);
|
|
1743
1912
|
const { session, modelFallbackMessage } = await createAgentSession({
|
|
1744
1913
|
sessionManager,
|
|
@@ -1751,6 +1920,8 @@ class ChatSession {
|
|
|
1751
1920
|
logger.warn(`Model fallback: ${modelFallbackMessage}`);
|
|
1752
1921
|
}
|
|
1753
1922
|
const wrapper = new ChatSession(session, chatId, contextToken);
|
|
1923
|
+
const visionRole = session.settings.getModelRole("vision");
|
|
1924
|
+
logger.info(`[${chatId}] Model: ${session.model?.id ?? "unknown"}, vision role: ${visionRole ?? "(not configured)"}`);
|
|
1754
1925
|
session.subscribe((event) => {
|
|
1755
1926
|
if (event.type !== "message_end")
|
|
1756
1927
|
return;
|
|
@@ -1765,9 +1936,12 @@ class ChatSession {
|
|
|
1765
1936
|
});
|
|
1766
1937
|
return wrapper;
|
|
1767
1938
|
}
|
|
1768
|
-
async prompt(text) {
|
|
1939
|
+
async prompt(text, images) {
|
|
1769
1940
|
this.lastActive = Date.now();
|
|
1770
|
-
await this.session.prompt(text);
|
|
1941
|
+
await this.session.prompt(text, images?.length ? { images } : undefined);
|
|
1942
|
+
}
|
|
1943
|
+
supportsVision() {
|
|
1944
|
+
return this.session.settings.getModelRole("vision") !== undefined;
|
|
1771
1945
|
}
|
|
1772
1946
|
setContextToken(token) {
|
|
1773
1947
|
this.contextToken = token;
|
|
@@ -1808,16 +1982,27 @@ class SessionPool {
|
|
|
1808
1982
|
this.pool.set(chatId, entry);
|
|
1809
1983
|
return entry;
|
|
1810
1984
|
}
|
|
1811
|
-
async prompt(chatId, contextToken, text, config) {
|
|
1985
|
+
async prompt(chatId, contextToken, text, config, images) {
|
|
1812
1986
|
const entry = await this.ensure(chatId, contextToken, config);
|
|
1813
|
-
await entry.prompt(text);
|
|
1987
|
+
await entry.prompt(text, images);
|
|
1814
1988
|
}
|
|
1815
1989
|
get(chatId) {
|
|
1816
1990
|
return this.pool.get(chatId);
|
|
1817
1991
|
}
|
|
1992
|
+
async resetSession(chatId) {
|
|
1993
|
+
const entry = this.pool.get(chatId);
|
|
1994
|
+
if (entry) {
|
|
1995
|
+
await entry.dispose();
|
|
1996
|
+
this.pool.delete(chatId);
|
|
1997
|
+
}
|
|
1998
|
+
removeSessionDir(chatId);
|
|
1999
|
+
}
|
|
1818
2000
|
getContextToken(chatId) {
|
|
1819
2001
|
return this.pool.get(chatId)?.getContextToken() ?? "";
|
|
1820
2002
|
}
|
|
2003
|
+
supportsVision(chatId) {
|
|
2004
|
+
return this.pool.get(chatId)?.supportsVision() ?? false;
|
|
2005
|
+
}
|
|
1821
2006
|
evictOldest() {
|
|
1822
2007
|
let oldestId = null;
|
|
1823
2008
|
let oldestTime = Infinity;
|
|
@@ -2010,6 +2195,24 @@ class ModelCommand {
|
|
|
2010
2195
|
}
|
|
2011
2196
|
}
|
|
2012
2197
|
|
|
2198
|
+
// src/command/new-session-command.ts
|
|
2199
|
+
class NewSessionInvocation {
|
|
2200
|
+
async execute(ctx) {
|
|
2201
|
+
await ctx.pool.resetSession(ctx.chatId);
|
|
2202
|
+
return "Session reset. Your next message starts a fresh conversation.";
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
class NewSessionCommand {
|
|
2207
|
+
name = "new";
|
|
2208
|
+
parse(text) {
|
|
2209
|
+
if (text === "/new" || text === "/reset") {
|
|
2210
|
+
return new NewSessionInvocation;
|
|
2211
|
+
}
|
|
2212
|
+
return null;
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2013
2216
|
// src/bridge.ts
|
|
2014
2217
|
var MAX_FAILURES = 3;
|
|
2015
2218
|
var BACKOFF_MS = 30000;
|
|
@@ -2027,6 +2230,7 @@ class WeChatBridge {
|
|
|
2027
2230
|
commands = new CommandRegistry;
|
|
2028
2231
|
constructor() {
|
|
2029
2232
|
this.commands.register(new ModelCommand);
|
|
2233
|
+
this.commands.register(new NewSessionCommand);
|
|
2030
2234
|
}
|
|
2031
2235
|
start() {
|
|
2032
2236
|
const config = loadConfig();
|
|
@@ -2153,39 +2357,75 @@ class WeChatBridge {
|
|
|
2153
2357
|
if (result.action === "pair") {
|
|
2154
2358
|
if (contextToken) {
|
|
2155
2359
|
const lead = result.isResend ? "Still waiting for pairing" : "Pairing required";
|
|
2156
|
-
const
|
|
2157
|
-
await sendMessage(creds, senderId,
|
|
2360
|
+
const text = `${lead} \u2014 approve in OMP with: /wechat pair ${result.code}`;
|
|
2361
|
+
await sendMessage(creds, senderId, text, contextToken).catch((err) => {
|
|
2158
2362
|
logger.warn("Pairing reply send failed:", err);
|
|
2159
2363
|
});
|
|
2160
2364
|
}
|
|
2161
2365
|
return;
|
|
2162
2366
|
}
|
|
2163
|
-
|
|
2164
|
-
if (!text)
|
|
2367
|
+
if (!(msg.item_list ?? []).length)
|
|
2165
2368
|
return;
|
|
2166
|
-
const
|
|
2369
|
+
const rawText = (msg.item_list ?? []).filter((item) => item.type === 1).map((item) => item.text_item?.text ?? "").filter(Boolean).join(`
|
|
2370
|
+
`);
|
|
2371
|
+
const hasImages = (msg.item_list ?? []).some((item) => item.type === 2);
|
|
2372
|
+
const dedupKey = makeDedupKey(senderId, msg.create_time_ms, rawText || "(image)");
|
|
2167
2373
|
if (dedupKey && isDuplicate(dedupKey)) {
|
|
2168
|
-
logger.info(`[${senderId}] Skipping duplicate message: ${
|
|
2374
|
+
logger.info(`[${senderId}] Skipping duplicate message: ${(rawText || "(image)").slice(0, 80)}`);
|
|
2169
2375
|
return;
|
|
2170
2376
|
}
|
|
2171
2377
|
const config = loadConfig();
|
|
2172
|
-
logger.info(`[${senderId}] Inbound (ts=${msg.create_time_ms ?? "n/a"}): ${
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2378
|
+
logger.info(`[${senderId}] Inbound (ts=${msg.create_time_ms ?? "n/a"}): ${(rawText || "(image)").slice(0, 80)}`);
|
|
2379
|
+
if (rawText) {
|
|
2380
|
+
const invocation = this.commands.tryParse(rawText);
|
|
2381
|
+
if (invocation) {
|
|
2382
|
+
const reply = await invocation.execute({ pool: this.pool, config, chatId: senderId }).catch((err) => `Command failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2383
|
+
await sendMessage(creds, senderId, reply, contextToken).catch((err) => {
|
|
2384
|
+
logger.warn(`[${senderId}] Command reply send failed:`, err);
|
|
2385
|
+
});
|
|
2386
|
+
return;
|
|
2387
|
+
}
|
|
2180
2388
|
}
|
|
2181
2389
|
await sendTyping(creds, senderId, 1).catch(() => {});
|
|
2182
2390
|
try {
|
|
2183
|
-
await this.pool.
|
|
2391
|
+
const session = await this.pool.ensure(senderId, contextToken, config);
|
|
2392
|
+
const hasVision = session.supportsVision();
|
|
2393
|
+
const text = extractInboundText(msg, !hasVision);
|
|
2394
|
+
if (!text && !hasImages)
|
|
2395
|
+
return;
|
|
2396
|
+
const images = await this.downloadImages(creds, msg, senderId);
|
|
2397
|
+
await session.prompt(text, images);
|
|
2184
2398
|
} catch (err) {
|
|
2185
2399
|
logger.error(`[${senderId}] prompt failed:`, err);
|
|
2186
2400
|
await this.sendReply(creds, senderId, "Processing failed, please try again.");
|
|
2187
2401
|
}
|
|
2188
2402
|
}
|
|
2403
|
+
async downloadImages(_creds, msg, chatId) {
|
|
2404
|
+
const imageItems = extractInboundImages(msg);
|
|
2405
|
+
if (imageItems.length === 0)
|
|
2406
|
+
return [];
|
|
2407
|
+
if (!this.pool?.supportsVision(chatId)) {
|
|
2408
|
+
logger.info(`[${chatId}] Skipping image download \u2014 model does not support vision`);
|
|
2409
|
+
return [];
|
|
2410
|
+
}
|
|
2411
|
+
const results = [];
|
|
2412
|
+
for (const item of imageItems) {
|
|
2413
|
+
const img = item.image_item;
|
|
2414
|
+
const buf = await downloadAndDecrypt(img.file_url, img.full_url, img.aeskey, img.aes_key ?? img.media?.aes_key, `image[${chatId}]`);
|
|
2415
|
+
if (buf) {
|
|
2416
|
+
const mimeType = buf.length > 4 && buf[0] === 137 && buf[1] === 80 ? "image/png" : "image/jpeg";
|
|
2417
|
+
results.push({
|
|
2418
|
+
type: "image",
|
|
2419
|
+
data: buf.toString("base64"),
|
|
2420
|
+
mimeType
|
|
2421
|
+
});
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
if (results.length > 0) {
|
|
2425
|
+
logger.info(`[${chatId}] Downloaded ${results.length}/${imageItems.length} images for AI`);
|
|
2426
|
+
}
|
|
2427
|
+
return results;
|
|
2428
|
+
}
|
|
2189
2429
|
async sendReply(creds, chatId, text) {
|
|
2190
2430
|
const contextToken = this.pool?.getContextToken(chatId) ?? "";
|
|
2191
2431
|
if (!contextToken) {
|
|
@@ -2216,7 +2456,7 @@ class WeChatBridge {
|
|
|
2216
2456
|
|
|
2217
2457
|
// src/service.ts
|
|
2218
2458
|
import { platform, homedir as homedir7 } from "os";
|
|
2219
|
-
import { join as
|
|
2459
|
+
import { join as join8 } from "path";
|
|
2220
2460
|
import { existsSync as existsSync3, mkdirSync as mkdirSync6, writeFileSync as writeFileSync4, rmSync as rmSync2 } from "fs";
|
|
2221
2461
|
var PLIST_LABEL = "com.omp-wechat";
|
|
2222
2462
|
var SERVICE_NAME = "omp-wechat";
|
|
@@ -2229,13 +2469,13 @@ function detectPlatform() {
|
|
|
2229
2469
|
return "other";
|
|
2230
2470
|
}
|
|
2231
2471
|
function getLogDir() {
|
|
2232
|
-
return
|
|
2472
|
+
return join8(homedir7(), ".omp", "logs");
|
|
2233
2473
|
}
|
|
2234
2474
|
function resolveHostBinary() {
|
|
2235
2475
|
return process.execPath || "omp";
|
|
2236
2476
|
}
|
|
2237
2477
|
function plistPath() {
|
|
2238
|
-
return
|
|
2478
|
+
return join8(homedir7(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
|
|
2239
2479
|
}
|
|
2240
2480
|
function generatePlist() {
|
|
2241
2481
|
const omp = resolveHostBinary();
|
|
@@ -2284,7 +2524,7 @@ function generatePlist() {
|
|
|
2284
2524
|
`;
|
|
2285
2525
|
}
|
|
2286
2526
|
function installLaunchd() {
|
|
2287
|
-
const dir =
|
|
2527
|
+
const dir = join8(homedir7(), "Library", "LaunchAgents");
|
|
2288
2528
|
mkdirSync6(dir, { recursive: true });
|
|
2289
2529
|
mkdirSync6(getLogDir(), { recursive: true });
|
|
2290
2530
|
const plist = plistPath();
|
|
@@ -2336,7 +2576,7 @@ StandardError=append:${logDir}/rpc.log
|
|
|
2336
2576
|
NoNewPrivileges=true
|
|
2337
2577
|
ProtectSystem=strict
|
|
2338
2578
|
ProtectHome=read-only
|
|
2339
|
-
ReadWritePaths=${logDir} ${
|
|
2579
|
+
ReadWritePaths=${logDir} ${join8(homedir7(), ".omp-wechat")} ${join8(homedir7(), ".omp")}
|
|
2340
2580
|
PrivateTmp=true
|
|
2341
2581
|
|
|
2342
2582
|
[Install]
|