@tencent-weixin/openclaw-weixin 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/CHANGELOG.zh_CN.md +3 -0
  3. package/LICENSE +21 -0
  4. package/README.md +271 -0
  5. package/README.zh_CN.md +269 -0
  6. package/index.ts +27 -0
  7. package/openclaw.plugin.json +9 -0
  8. package/package.json +55 -0
  9. package/src/api/api.ts +240 -0
  10. package/src/api/config-cache.ts +79 -0
  11. package/src/api/session-guard.ts +58 -0
  12. package/src/api/types.ts +222 -0
  13. package/src/auth/accounts.ts +321 -0
  14. package/src/auth/login-qr.ts +331 -0
  15. package/src/auth/pairing.ts +120 -0
  16. package/src/cdn/aes-ecb.ts +21 -0
  17. package/src/cdn/cdn-upload.ts +77 -0
  18. package/src/cdn/cdn-url.ts +17 -0
  19. package/src/cdn/pic-decrypt.ts +85 -0
  20. package/src/cdn/upload.ts +155 -0
  21. package/src/channel.ts +380 -0
  22. package/src/config/config-schema.ts +22 -0
  23. package/src/log-upload.ts +126 -0
  24. package/src/media/media-download.ts +141 -0
  25. package/src/media/mime.ts +76 -0
  26. package/src/media/silk-transcode.ts +74 -0
  27. package/src/messaging/debug-mode.ts +69 -0
  28. package/src/messaging/error-notice.ts +31 -0
  29. package/src/messaging/inbound.ts +171 -0
  30. package/src/messaging/process-message.ts +381 -0
  31. package/src/messaging/send-media.ts +72 -0
  32. package/src/messaging/send.ts +267 -0
  33. package/src/messaging/slash-commands.ts +110 -0
  34. package/src/monitor/monitor.ts +221 -0
  35. package/src/runtime.ts +70 -0
  36. package/src/storage/state-dir.ts +11 -0
  37. package/src/storage/sync-buf.ts +81 -0
  38. package/src/util/logger.ts +143 -0
  39. package/src/util/random.ts +17 -0
  40. package/src/util/redact.ts +46 -0
  41. package/src/vendor.d.ts +25 -0
@@ -0,0 +1,81 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { deriveRawAccountId } from "../auth/accounts.js";
5
+
6
+ import { resolveStateDir } from "./state-dir.js";
7
+
8
+ function resolveAccountsDir(): string {
9
+ return path.join(resolveStateDir(), "openclaw-weixin", "accounts");
10
+ }
11
+
12
+ /**
13
+ * Path to the persistent get_updates_buf file for an account.
14
+ * Stored alongside account data: ~/.openclaw/openclaw-weixin/accounts/{accountId}.sync.json
15
+ */
16
+ export function getSyncBufFilePath(accountId: string): string {
17
+ return path.join(resolveAccountsDir(), `${accountId}.sync.json`);
18
+ }
19
+
20
+ /** Legacy single-account syncbuf (pre multi-account): `.openclaw-weixin-sync/default.json`. */
21
+ function getLegacySyncBufDefaultJsonPath(): string {
22
+ return path.join(
23
+ resolveStateDir(),
24
+ "agents",
25
+ "default",
26
+ "sessions",
27
+ ".openclaw-weixin-sync",
28
+ "default.json",
29
+ );
30
+ }
31
+
32
+ export type SyncBufData = {
33
+ get_updates_buf: string;
34
+ };
35
+
36
+ function readSyncBufFile(filePath: string): string | undefined {
37
+ try {
38
+ const raw = fs.readFileSync(filePath, "utf-8");
39
+ const data = JSON.parse(raw) as { get_updates_buf?: string };
40
+ if (typeof data.get_updates_buf === "string") {
41
+ return data.get_updates_buf;
42
+ }
43
+ } catch {
44
+ // file not found or invalid
45
+ }
46
+ return undefined;
47
+ }
48
+
49
+ /**
50
+ * Load persisted get_updates_buf.
51
+ * Falls back in order:
52
+ * 1. Primary path (normalized accountId, new installs)
53
+ * 2. Compat path (raw accountId derived from pattern, old installs)
54
+ * 3. Legacy single-account path (very old installs without multi-account support)
55
+ */
56
+ export function loadGetUpdatesBuf(filePath: string): string | undefined {
57
+ const value = readSyncBufFile(filePath);
58
+ if (value !== undefined) return value;
59
+
60
+ // Compat: if given path uses a normalized accountId (e.g. "b0f5860fdecb-im-bot.sync.json"),
61
+ // also try the old raw-ID filename (e.g. "b0f5860fdecb@im.bot.sync.json").
62
+ const accountId = path.basename(filePath, ".sync.json");
63
+ const rawId = deriveRawAccountId(accountId);
64
+ if (rawId) {
65
+ const compatPath = path.join(resolveAccountsDir(), `${rawId}.sync.json`);
66
+ const compatValue = readSyncBufFile(compatPath);
67
+ if (compatValue !== undefined) return compatValue;
68
+ }
69
+
70
+ // Legacy fallback: old single-account installs stored syncbuf without accountId.
71
+ return readSyncBufFile(getLegacySyncBufDefaultJsonPath());
72
+ }
73
+
74
+ /**
75
+ * Persist get_updates_buf. Creates parent dir if needed.
76
+ */
77
+ export function saveGetUpdatesBuf(filePath: string, getUpdatesBuf: string): void {
78
+ const dir = path.dirname(filePath);
79
+ fs.mkdirSync(dir, { recursive: true });
80
+ fs.writeFileSync(filePath, JSON.stringify({ get_updates_buf: getUpdatesBuf }, null, 0), "utf-8");
81
+ }
@@ -0,0 +1,143 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ /**
6
+ * Plugin logger — writes JSON lines to the main openclaw log file:
7
+ * /tmp/openclaw/openclaw-YYYY-MM-DD.log
8
+ * Same file and format used by all other channels.
9
+ */
10
+
11
+ const MAIN_LOG_DIR = path.join("/tmp", "openclaw");
12
+ const SUBSYSTEM = "gateway/channels/openclaw-weixin";
13
+ const RUNTIME = "node";
14
+ const RUNTIME_VERSION = process.versions.node;
15
+ const HOSTNAME = os.hostname() || "unknown";
16
+ const PARENT_NAMES = ["openclaw"];
17
+
18
+ /** tslog-compatible level IDs (higher = more severe). */
19
+ const LEVEL_IDS: Record<string, number> = {
20
+ TRACE: 1,
21
+ DEBUG: 2,
22
+ INFO: 3,
23
+ WARN: 4,
24
+ ERROR: 5,
25
+ FATAL: 6,
26
+ };
27
+
28
+ const DEFAULT_LOG_LEVEL = "INFO";
29
+
30
+ function resolveMinLevel(): number {
31
+ const env = process.env.OPENCLAW_LOG_LEVEL?.toUpperCase();
32
+ if (env && env in LEVEL_IDS) return LEVEL_IDS[env];
33
+ return LEVEL_IDS[DEFAULT_LOG_LEVEL];
34
+ }
35
+
36
+ let minLevelId = resolveMinLevel();
37
+
38
+ /** Dynamically change the minimum log level at runtime. */
39
+ export function setLogLevel(level: string): void {
40
+ const upper = level.toUpperCase();
41
+ if (!(upper in LEVEL_IDS)) {
42
+ throw new Error(`Invalid log level: ${level}. Valid levels: ${Object.keys(LEVEL_IDS).join(", ")}`);
43
+ }
44
+ minLevelId = LEVEL_IDS[upper];
45
+ }
46
+
47
+ /** Shift a Date into local time so toISOString() renders local clock digits. */
48
+ function toLocalISO(now: Date): string {
49
+ const offsetMs = -now.getTimezoneOffset() * 60_000;
50
+ const sign = offsetMs >= 0 ? "+" : "-";
51
+ const abs = Math.abs(now.getTimezoneOffset());
52
+ const offStr = `${sign}${String(Math.floor(abs / 60)).padStart(2, "0")}:${String(abs % 60).padStart(2, "0")}`;
53
+ return new Date(now.getTime() + offsetMs).toISOString().replace("Z", offStr);
54
+ }
55
+
56
+ function localDateKey(now: Date): string {
57
+ return toLocalISO(now).slice(0, 10);
58
+ }
59
+
60
+ function resolveMainLogPath(): string {
61
+ const dateKey = localDateKey(new Date());
62
+ return path.join(MAIN_LOG_DIR, `openclaw-${dateKey}.log`);
63
+ }
64
+
65
+ let logDirEnsured = false;
66
+
67
+ export type Logger = {
68
+ info(message: string): void;
69
+ debug(message: string): void;
70
+ warn(message: string): void;
71
+ error(message: string): void;
72
+ /** Returns a child logger whose messages are prefixed with `[accountId]`. */
73
+ withAccount(accountId: string): Logger;
74
+ /** Returns the current main log file path. */
75
+ getLogFilePath(): string;
76
+ close(): void;
77
+ };
78
+
79
+ function buildLoggerName(accountId?: string): string {
80
+ return accountId ? `${SUBSYSTEM}/${accountId}` : SUBSYSTEM;
81
+ }
82
+
83
+ function writeLog(level: string, message: string, accountId?: string): void {
84
+ const levelId = LEVEL_IDS[level] ?? LEVEL_IDS.INFO;
85
+ if (levelId < minLevelId) return;
86
+
87
+ const now = new Date();
88
+ const loggerName = buildLoggerName(accountId);
89
+ const prefixedMessage = accountId ? `[${accountId}] ${message}` : message;
90
+ const entry = JSON.stringify({
91
+ "0": loggerName,
92
+ "1": prefixedMessage,
93
+ _meta: {
94
+ runtime: RUNTIME,
95
+ runtimeVersion: RUNTIME_VERSION,
96
+ hostname: HOSTNAME,
97
+ name: loggerName,
98
+ parentNames: PARENT_NAMES,
99
+ date: now.toISOString(),
100
+ logLevelId: LEVEL_IDS[level] ?? LEVEL_IDS.INFO,
101
+ logLevelName: level,
102
+ },
103
+ time: toLocalISO(now),
104
+ });
105
+ try {
106
+ if (!logDirEnsured) {
107
+ fs.mkdirSync(MAIN_LOG_DIR, { recursive: true });
108
+ logDirEnsured = true;
109
+ }
110
+ fs.appendFileSync(resolveMainLogPath(), `${entry}\n`, "utf-8");
111
+ } catch {
112
+ // Best-effort; never block on logging failures.
113
+ }
114
+ }
115
+
116
+ /** Creates a logger instance, optionally bound to a specific account. */
117
+ function createLogger(accountId?: string): Logger {
118
+ return {
119
+ info(message: string): void {
120
+ writeLog("INFO", message, accountId);
121
+ },
122
+ debug(message: string): void {
123
+ writeLog("DEBUG", message, accountId);
124
+ },
125
+ warn(message: string): void {
126
+ writeLog("WARN", message, accountId);
127
+ },
128
+ error(message: string): void {
129
+ writeLog("ERROR", message, accountId);
130
+ },
131
+ withAccount(id: string): Logger {
132
+ return createLogger(id);
133
+ },
134
+ getLogFilePath(): string {
135
+ return resolveMainLogPath();
136
+ },
137
+ close(): void {
138
+ // No-op: appendFileSync has no persistent handle to close.
139
+ },
140
+ };
141
+ }
142
+
143
+ export const logger: Logger = createLogger();
@@ -0,0 +1,17 @@
1
+ import crypto from "node:crypto";
2
+
3
+ /**
4
+ * Generate a prefixed unique ID using timestamp + crypto random bytes.
5
+ * Format: `{prefix}:{timestamp}-{8-char hex}`
6
+ */
7
+ export function generateId(prefix: string): string {
8
+ return `${prefix}:${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
9
+ }
10
+
11
+ /**
12
+ * Generate a temporary file name with random suffix.
13
+ * Format: `{prefix}-{timestamp}-{8-char hex}{ext}`
14
+ */
15
+ export function tempFileName(prefix: string, ext: string): string {
16
+ return `${prefix}-${Date.now()}-${crypto.randomBytes(4).toString("hex")}${ext}`;
17
+ }
@@ -0,0 +1,46 @@
1
+ const DEFAULT_BODY_MAX_LEN = 200;
2
+ const DEFAULT_TOKEN_PREFIX_LEN = 6;
3
+
4
+ /**
5
+ * Truncate a string, appending a length indicator when trimmed.
6
+ * Returns `""` for empty/undefined input.
7
+ */
8
+ export function truncate(s: string | undefined, max: number): string {
9
+ if (!s) return "";
10
+ if (s.length <= max) return s;
11
+ return `${s.slice(0, max)}…(len=${s.length})`;
12
+ }
13
+
14
+ /**
15
+ * Redact a token/secret: show only the first few chars + total length.
16
+ * Returns `"(none)"` when absent.
17
+ */
18
+ export function redactToken(token: string | undefined, prefixLen = DEFAULT_TOKEN_PREFIX_LEN): string {
19
+ if (!token) return "(none)";
20
+ if (token.length <= prefixLen) return `****(len=${token.length})`;
21
+ return `${token.slice(0, prefixLen)}…(len=${token.length})`;
22
+ }
23
+
24
+ /**
25
+ * Truncate a JSON body string to `maxLen` chars for safe logging.
26
+ * Appends original length so the reader knows how much was dropped.
27
+ */
28
+ export function redactBody(body: string | undefined, maxLen = DEFAULT_BODY_MAX_LEN): string {
29
+ if (!body) return "(empty)";
30
+ if (body.length <= maxLen) return body;
31
+ return `${body.slice(0, maxLen)}…(truncated, totalLen=${body.length})`;
32
+ }
33
+
34
+ /**
35
+ * Strip query string (which often contains signatures/tokens) from a URL,
36
+ * keeping only origin + pathname.
37
+ */
38
+ export function redactUrl(rawUrl: string): string {
39
+ try {
40
+ const u = new URL(rawUrl);
41
+ const base = `${u.origin}${u.pathname}`;
42
+ return u.search ? `${base}?<redacted>` : base;
43
+ } catch {
44
+ return truncate(rawUrl, 80);
45
+ }
46
+ }
@@ -0,0 +1,25 @@
1
+ declare module "qrcode-terminal" {
2
+ const qrcodeTerminal: {
3
+ generate(
4
+ text: string,
5
+ options?: { small?: boolean },
6
+ callback?: (qr: string) => void,
7
+ ): void;
8
+ };
9
+ export default qrcodeTerminal;
10
+ }
11
+
12
+ declare module "fluent-ffmpeg" {
13
+ interface FfmpegCommand {
14
+ setFfmpegPath(path: string): FfmpegCommand;
15
+ seekInput(time: number): FfmpegCommand;
16
+ frames(n: number): FfmpegCommand;
17
+ outputOptions(opts: string[]): FfmpegCommand;
18
+ output(path: string): FfmpegCommand;
19
+ on(event: "end", cb: () => void): FfmpegCommand;
20
+ on(event: "error", cb: (err: Error) => void): FfmpegCommand;
21
+ run(): void;
22
+ }
23
+ function ffmpeg(input: string): FfmpegCommand;
24
+ export default ffmpeg;
25
+ }