@invago/mixin 1.0.9 → 1.0.10

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/src/status.ts CHANGED
@@ -2,6 +2,7 @@ import { buildBaseAccountStatusSnapshot, buildBaseChannelStatusSummary } from "o
2
2
  import type { OpenClawConfig } from "openclaw/plugin-sdk";
3
3
  import { getAccountConfig, resolveDefaultAccountId } from "./config.js";
4
4
  import { getOutboxPathsSnapshot, type OutboxStatus } from "./send-service.js";
5
+ import type { getMixpayStatusSnapshot } from "./mixpay-worker.js";
5
6
 
6
7
  type RuntimeLifecycleSnapshot = {
7
8
  running?: boolean | null;
@@ -23,6 +24,9 @@ type MixinChannelStatusSnapshot = {
23
24
  outboxFile?: string | null;
24
25
  outboxPending?: number | null;
25
26
  mediaMaxMb?: number | null;
27
+ mixpayPendingOrders?: number | null;
28
+ mixpayStoreDir?: string | null;
29
+ mixpayStoreFile?: string | null;
26
30
  };
27
31
 
28
32
  type MixinStatusAccount = {
@@ -44,12 +48,16 @@ export function resolveMixinStatusSnapshot(
44
48
  cfg: OpenClawConfig,
45
49
  accountId?: string,
46
50
  outboxStatus?: OutboxStatus | null,
51
+ mixpayStatus?: Awaited<ReturnType<typeof getMixpayStatusSnapshot>> | null,
47
52
  ): {
48
53
  defaultAccountId: string;
49
54
  outboxDir: string;
50
55
  outboxFile: string;
51
56
  outboxPending: number;
52
57
  mediaMaxMb: number | null;
58
+ mixpayPendingOrders: number;
59
+ mixpayStoreDir: string | null;
60
+ mixpayStoreFile: string | null;
53
61
  } {
54
62
  const defaultAccountId = resolveDefaultAccountId(cfg);
55
63
  const resolvedAccountId = accountId ?? defaultAccountId;
@@ -61,6 +69,9 @@ export function resolveMixinStatusSnapshot(
61
69
  outboxFile,
62
70
  outboxPending: outboxStatus?.totalPending ?? 0,
63
71
  mediaMaxMb: accountConfig.mediaMaxMb ?? null,
72
+ mixpayPendingOrders: mixpayStatus?.pendingOrders ?? 0,
73
+ mixpayStoreDir: mixpayStatus?.storeDir ?? null,
74
+ mixpayStoreFile: mixpayStatus?.storeFile ?? null,
64
75
  };
65
76
  }
66
77
 
@@ -75,6 +86,9 @@ export function buildMixinChannelSummary(params: {
75
86
  outboxFile: snapshot.outboxFile ?? null,
76
87
  outboxPending: snapshot.outboxPending ?? 0,
77
88
  mediaMaxMb: snapshot.mediaMaxMb ?? null,
89
+ mixpayPendingOrders: snapshot.mixpayPendingOrders ?? 0,
90
+ mixpayStoreDir: snapshot.mixpayStoreDir ?? null,
91
+ mixpayStoreFile: snapshot.mixpayStoreFile ?? null,
78
92
  };
79
93
  }
80
94
 
package/src/decrypt.ts DELETED
@@ -1,126 +0,0 @@
1
- import crypto from 'crypto';
2
-
3
- /**
4
- * 将 Mixin 的 Ed25519 seed 转换为 Curve25519 私钥,并与对端公钥协商出共享密钥
5
- * @param seedHex 64 字符的 Hex 字符串,对应 session_private_key
6
- * @param peerPublicKey 32 字节的对端 Curve25519 公钥
7
- */
8
- export function x25519KeyAgreement(seedHex: string, peerPublicKey: Buffer): Buffer {
9
- // 1. 将 64 字符的 Hex 转换为 32 字节的 seed
10
- const seedBytes = Buffer.from(seedHex, 'hex');
11
- if (seedBytes.length !== 32) {
12
- throw new Error('Invalid Ed25519 seed length, expected 32 bytes.');
13
- }
14
-
15
- // 2. SHA-512 散列
16
- const hash = crypto.createHash('sha512').update(seedBytes).digest();
17
-
18
- // 3. 提取前 32 字节并进行 Curve25519 位截断 (Clamping)
19
- const privateKeyX25519 = Buffer.from(hash.slice(0, 32));
20
- privateKeyX25519[0] &= 248;
21
- privateKeyX25519[31] &= 127;
22
- privateKeyX25519[31] |= 64;
23
-
24
- const ecdh = crypto.createECDH('x25519');
25
- ecdh.setPrivateKey(privateKeyX25519);
26
-
27
- return ecdh.computeSecret(peerPublicKey);
28
- }
29
-
30
- /**
31
- * 解密 Mixin ENCRYPTED_TEXT 消息 (对应 Go SDK DecryptMessageData)
32
- * @param data Base64 编码的加密数据
33
- * @param sessionId 机器人的 session_id
34
- * @param privateKey 机器人的 ed25519 私钥(hex 格式,实为 seed)
35
- * @returns 解密后的明文,失败返回 null
36
- */
37
- export function decryptMessageData(
38
- data: string,
39
- sessionId: string,
40
- privateKey: string
41
- ): string | null {
42
- try {
43
- // 1. Base64 解码,处理可能的 URL-safe Base64
44
- let base64 = data.replace(/-/g, '+').replace(/_/g, '/');
45
- while (base64.length % 4) {
46
- base64 += '=';
47
- }
48
- const encryptedBytes = Buffer.from(base64, 'base64');
49
-
50
- // 验证最小长度: version(1) + sessionCount(2) + senderPubKey(32) + nonce(12)
51
- if (encryptedBytes.length < 1 + 2 + 32 + 12) {
52
- console.error('[mixin decrypt] data too short:', encryptedBytes.length);
53
- return null;
54
- }
55
-
56
- // 解析消息结构
57
- const version = encryptedBytes[0];
58
- if (version !== 1) {
59
- console.error('[mixin decrypt] unsupported version:', version);
60
- return null;
61
- }
62
-
63
- const sessionCount = encryptedBytes.readUInt16LE(1);
64
- let offset = 3;
65
-
66
- // 2. 提取发送者公钥 (已经是 Curve25519)
67
- const senderPublicKey = encryptedBytes.slice(offset, offset + 32);
68
- offset += 32;
69
-
70
- // 查找匹配的 session
71
- const sessionIdBuffer = Buffer.from(sessionId.replace(/-/g, ''), 'hex');
72
-
73
- let sessionData: Buffer | null = null;
74
- for (let i = 0; i < sessionCount; i++) {
75
- const sessionIdInMsg = encryptedBytes.slice(offset, offset + 16);
76
-
77
- if (sessionIdInMsg.equals(sessionIdBuffer)) {
78
- sessionData = encryptedBytes.slice(offset + 16, offset + 64);
79
- break; // 暂不中断读取,只取我们自己的 session 块
80
- }
81
- offset += 64;
82
- }
83
-
84
- if (!sessionData) {
85
- console.error('[mixin decrypt] session not found');
86
- return null;
87
- }
88
-
89
- // 3. 计算 Shared Secret
90
- const sharedSecret = x25519KeyAgreement(privateKey, senderPublicKey);
91
-
92
- // 4. 解密 Message Key (AES-256-CBC)
93
- // sessionData 的前 16 字节为 IV,后 32 字节为加密后的 key
94
- const sessionIv = sessionData.slice(0, 16);
95
- const encryptedKey = sessionData.slice(16, 48);
96
-
97
- const decipherKey = crypto.createDecipheriv('aes-256-cbc', sharedSecret, sessionIv);
98
- // Mixin SDK 这里加了 padding 处理。如果后续失败,尝试 decipherKey.setAutoPadding(false);
99
- const rawMessageKey = Buffer.concat([decipherKey.update(encryptedKey), decipherKey.final()]);
100
-
101
- // 取前 16 字节!
102
- const messageKey = rawMessageKey.slice(0, 16);
103
-
104
- // 5. 获取 Nonce 和 密文
105
- const prefixSize = 3 + 32 + sessionCount * 64;
106
- const nonce = encryptedBytes.slice(prefixSize, prefixSize + 12); // 注意这里是 12 字节!!!
107
- const encryptedText = encryptedBytes.slice(prefixSize + 12);
108
-
109
- // 6. 解密消息体 (AES-128-GCM)
110
- // 对于 GCM,还需要分离出 authentication tag (后 16 字节)
111
- const tag = encryptedText.slice(-16);
112
- const ciphertext = encryptedText.slice(0, -16);
113
-
114
- const decipherGcm = crypto.createDecipheriv('aes-128-gcm', messageKey, nonce);
115
- decipherGcm.setAuthTag(tag);
116
-
117
- let decryptedText = decipherGcm.update(ciphertext);
118
- decryptedText = Buffer.concat([decryptedText, decipherGcm.final()]);
119
-
120
- return decryptedText.toString('utf8');
121
-
122
- } catch (error) {
123
- console.error('[mixin decrypt] error:', error);
124
- return null;
125
- }
126
- }
@@ -1,98 +0,0 @@
1
- # Mixin Plugin Onboarding CLI
2
-
3
- This CLI is bundled inside [`@invago/mixin`](https://www.npmjs.com/package/@invago/mixin). It helps inspect local OpenClaw installation state, verify key paths, and automate plugin install or update commands.
4
-
5
- ## Commands
6
-
7
- ### `info`
8
-
9
- Prints the current local OpenClaw and Mixin plugin context:
10
-
11
- - OpenClaw home, state, and extensions directories
12
- - detected `openclaw.json` path
13
- - detected Mixin plugin directories
14
- - whether the plugin looks enabled in config
15
- - current outbox path
16
- - whether `ffprobe` is available
17
-
18
- Run:
19
-
20
- ```bash
21
- npx -y @invago/mixin info
22
- ```
23
-
24
- ### `doctor`
25
-
26
- Runs a basic local diagnosis and returns a non-zero exit code when required checks fail.
27
-
28
- Current checks:
29
-
30
- - config file found
31
- - `channels.mixin` present
32
- - plugin enabled in config
33
- - plugin installed in extensions
34
- - outbox directory writable
35
- - `ffprobe` available
36
-
37
- It also reports leftover `.openclaw-install-stage-*` directories if any are detected.
38
-
39
- Run:
40
-
41
- ```bash
42
- npx -y @invago/mixin doctor
43
- ```
44
-
45
- ### `install`
46
-
47
- Runs:
48
-
49
- ```bash
50
- openclaw plugins install @invago/mixin
51
- ```
52
-
53
- You can also pass a custom npm spec:
54
-
55
- ```bash
56
- npx -y @invago/mixin install @invago/mixin@latest
57
- ```
58
-
59
- ### `update`
60
-
61
- Runs:
62
-
63
- ```bash
64
- openclaw plugins install @invago/mixin@latest
65
- ```
66
-
67
- Run:
68
-
69
- ```bash
70
- npx -y @invago/mixin update
71
- ```
72
-
73
- ## Local Development
74
-
75
- From this repository:
76
-
77
- ```bash
78
- node --import jiti/register.js tools/mixin-plugin-onboard/src/index.ts info
79
- node --import jiti/register.js tools/mixin-plugin-onboard/src/index.ts doctor
80
- ```
81
-
82
- Or from the tool directory:
83
-
84
- ```bash
85
- cd tools/mixin-plugin-onboard
86
- npm run info
87
- npm run doctor
88
- ```
89
-
90
- ## Publish
91
-
92
- This CLI is published together with the main `@invago/mixin` package from the repository root.
93
-
94
- ## Notes
95
-
96
- - This CLI is intentionally read-mostly right now.
97
- - `install` and `update` delegate to the local `openclaw` command.
98
- - `doctor` currently treats missing `ffprobe` as a failed check because native outbound audio-as-voice depends on it.
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
- import "jiti/register.js";
3
- await import("../src/index.ts");
@@ -1,28 +0,0 @@
1
- import { buildContext, checkWritableDir, findMixinPluginDirs, isPluginEnabled, readMixinConfig, runFfprobeCheck } from "../utils.ts";
2
-
3
- export async function runDoctor(): Promise<number> {
4
- const ctx = await buildContext();
5
- const pluginDirs = await findMixinPluginDirs(ctx.extensionsDir);
6
- const mixinConfig = readMixinConfig(ctx.config);
7
- const checks = [
8
- { label: "config_found", ok: Boolean(ctx.configPath) },
9
- { label: "mixin_config_present", ok: Boolean(mixinConfig) },
10
- { label: "plugin_enabled", ok: isPluginEnabled(ctx.config) },
11
- { label: "plugin_installed", ok: pluginDirs.length > 0 },
12
- { label: "outbox_writable", ok: await checkWritableDir(ctx.outboxDir) },
13
- { label: "ffprobe_available", ok: runFfprobeCheck() },
14
- ];
15
-
16
- const stageDirs = pluginDirs.filter((dir) => dir.includes(".openclaw-install-stage-"));
17
- console.log(JSON.stringify({
18
- ok: checks.every((item) => item.ok),
19
- checks,
20
- stageDirs,
21
- pluginDirs,
22
- configPath: ctx.configPath,
23
- outboxDir: ctx.outboxDir,
24
- outboxFile: ctx.outboxFile,
25
- }, null, 2));
26
-
27
- return checks.every((item) => item.ok) ? 0 : 1;
28
- }
@@ -1,23 +0,0 @@
1
- import { buildContext, findMixinPluginDirs, isPluginEnabled, readMixinConfig, runFfprobeCheck } from "../utils.ts";
2
-
3
- export async function runInfo(): Promise<number> {
4
- const ctx = await buildContext();
5
- const pluginDirs = await findMixinPluginDirs(ctx.extensionsDir);
6
- const mixinConfig = readMixinConfig(ctx.config);
7
-
8
- console.log(JSON.stringify({
9
- homeDir: ctx.homeDir,
10
- stateDir: ctx.stateDir,
11
- extensionsDir: ctx.extensionsDir,
12
- configPath: ctx.configPath,
13
- pluginDirs,
14
- pluginEnabled: isPluginEnabled(ctx.config),
15
- mixinConfigured: Boolean(mixinConfig),
16
- defaultAccount: typeof mixinConfig?.defaultAccount === "string" ? mixinConfig.defaultAccount : "default",
17
- outboxDir: ctx.outboxDir,
18
- outboxFile: ctx.outboxFile,
19
- ffprobeAvailable: runFfprobeCheck(),
20
- }, null, 2));
21
-
22
- return 0;
23
- }
@@ -1,5 +0,0 @@
1
- import { runOpenClawInstall } from "../utils.ts";
2
-
3
- export async function runInstall(spec?: string): Promise<number> {
4
- return runOpenClawInstall(spec?.trim() || "@invago/mixin");
5
- }
@@ -1,5 +0,0 @@
1
- import { runOpenClawInstall } from "../utils.ts";
2
-
3
- export async function runUpdate(spec?: string): Promise<number> {
4
- return runOpenClawInstall(spec?.trim() || "@invago/mixin@latest");
5
- }
@@ -1,49 +0,0 @@
1
- import { runDoctor } from "./commands/doctor.ts";
2
- import { runInfo } from "./commands/info.ts";
3
- import { runInstall } from "./commands/install.ts";
4
- import { runUpdate } from "./commands/update.ts";
5
-
6
- function printUsage(): void {
7
- console.log(`mixin-plugin-onboard <command>
8
-
9
- Commands:
10
- info
11
- doctor
12
- install [npm-spec]
13
- update [npm-spec]
14
- `);
15
- }
16
-
17
- async function main(): Promise<void> {
18
- const [, , command, arg] = process.argv;
19
- if (!command || command === "help" || command === "--help" || command === "-h") {
20
- printUsage();
21
- process.exitCode = 0;
22
- return;
23
- }
24
-
25
- if (command === "info") {
26
- process.exitCode = await runInfo();
27
- return;
28
- }
29
-
30
- if (command === "doctor") {
31
- process.exitCode = await runDoctor();
32
- return;
33
- }
34
-
35
- if (command === "install") {
36
- process.exitCode = await runInstall(arg);
37
- return;
38
- }
39
-
40
- if (command === "update") {
41
- process.exitCode = await runUpdate(arg);
42
- return;
43
- }
44
-
45
- printUsage();
46
- process.exitCode = 1;
47
- }
48
-
49
- await main();
@@ -1,189 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { spawnSync } from "node:child_process";
5
-
6
- export type OpenClawContext = {
7
- homeDir: string;
8
- stateDir: string;
9
- extensionsDir: string;
10
- configPath: string | null;
11
- config: Record<string, unknown> | null;
12
- outboxDir: string;
13
- outboxFile: string;
14
- };
15
-
16
- export function resolveHomeDir(env: NodeJS.ProcessEnv = process.env): string {
17
- const configured = env.OPENCLAW_HOME?.trim();
18
- if (configured) {
19
- return configured;
20
- }
21
- return path.join(os.homedir(), ".openclaw");
22
- }
23
-
24
- export function resolveStateDir(env: NodeJS.ProcessEnv = process.env): string {
25
- const configured = env.OPENCLAW_STATE_DIR?.trim() || env.CLAWDBOT_STATE_DIR?.trim();
26
- if (configured) {
27
- return configured;
28
- }
29
- return path.join(resolveHomeDir(env), "state");
30
- }
31
-
32
- export function resolveExtensionsDir(env: NodeJS.ProcessEnv = process.env): string {
33
- return path.join(resolveHomeDir(env), "extensions");
34
- }
35
-
36
- export function resolveOutboxPaths(env: NodeJS.ProcessEnv = process.env): {
37
- outboxDir: string;
38
- outboxFile: string;
39
- } {
40
- const outboxDir = path.join(resolveStateDir(env), "mixin");
41
- return {
42
- outboxDir,
43
- outboxFile: path.join(outboxDir, "mixin-outbox.json"),
44
- };
45
- }
46
-
47
- export async function readConfig(env: NodeJS.ProcessEnv = process.env): Promise<{
48
- path: string | null;
49
- config: Record<string, unknown> | null;
50
- }> {
51
- const explicit = env.OPENCLAW_CONFIG?.trim();
52
- const candidates = [
53
- explicit,
54
- path.join(resolveHomeDir(env), "openclaw.json"),
55
- path.join(process.cwd(), "openclaw.json"),
56
- ].filter((value): value is string => Boolean(value));
57
-
58
- for (const candidate of candidates) {
59
- try {
60
- const raw = await fs.readFile(candidate, "utf8");
61
- return {
62
- path: candidate,
63
- config: parseLooseConfig(raw),
64
- };
65
- } catch {
66
- continue;
67
- }
68
- }
69
-
70
- return {
71
- path: null,
72
- config: null,
73
- };
74
- }
75
-
76
- function parseLooseConfig(raw: string): Record<string, unknown> {
77
- try {
78
- return JSON.parse(raw) as Record<string, unknown>;
79
- } catch {
80
- const relaxed = raw
81
- .replace(/^\uFEFF/, "")
82
- .replace(/\/\*[\s\S]*?\*\//g, "")
83
- .replace(/^\s*\/\/.*$/gm, "")
84
- .replace(/,\s*([}\]])/g, "$1");
85
- return JSON.parse(relaxed) as Record<string, unknown>;
86
- }
87
- }
88
-
89
- export async function buildContext(env: NodeJS.ProcessEnv = process.env): Promise<OpenClawContext> {
90
- const config = await readConfig(env);
91
- const outbox = resolveOutboxPaths(env);
92
- return {
93
- homeDir: resolveHomeDir(env),
94
- stateDir: resolveStateDir(env),
95
- extensionsDir: resolveExtensionsDir(env),
96
- configPath: config.path,
97
- config: config.config,
98
- outboxDir: outbox.outboxDir,
99
- outboxFile: outbox.outboxFile,
100
- };
101
- }
102
-
103
- export async function findMixinPluginDirs(extensionsDir: string): Promise<string[]> {
104
- try {
105
- const entries = await fs.readdir(extensionsDir, { withFileTypes: true });
106
- const matched: string[] = [];
107
- for (const entry of entries) {
108
- if (!entry.isDirectory()) {
109
- continue;
110
- }
111
- const dirPath = path.join(extensionsDir, entry.name);
112
- const openclawPluginPath = path.join(dirPath, "openclaw.plugin.json");
113
- const packageJsonPath = path.join(dirPath, "package.json");
114
- try {
115
- const pluginRaw = await fs.readFile(openclawPluginPath, "utf8");
116
- if (pluginRaw.includes("\"mixin\"")) {
117
- matched.push(dirPath);
118
- continue;
119
- }
120
- } catch {
121
- }
122
- try {
123
- const packageRaw = await fs.readFile(packageJsonPath, "utf8");
124
- if (packageRaw.includes("\"@invago/mixin\"") || packageRaw.includes("\"id\":\"mixin\"")) {
125
- matched.push(dirPath);
126
- }
127
- } catch {
128
- }
129
- }
130
- return matched;
131
- } catch {
132
- return [];
133
- }
134
- }
135
-
136
- export async function checkWritableDir(dirPath: string): Promise<boolean> {
137
- try {
138
- await fs.mkdir(dirPath, { recursive: true });
139
- const testPath = path.join(dirPath, `.write-test-${Date.now()}`);
140
- await fs.writeFile(testPath, "ok", "utf8");
141
- await fs.rm(testPath, { force: true });
142
- return true;
143
- } catch {
144
- return false;
145
- }
146
- }
147
-
148
- export function runOpenClawInstall(spec: string): number {
149
- const result = spawnSync("openclaw", ["plugins", "install", spec], {
150
- stdio: "inherit",
151
- shell: process.platform === "win32",
152
- });
153
- return result.status ?? 1;
154
- }
155
-
156
- export function runFfprobeCheck(): boolean {
157
- const result = spawnSync(process.platform === "win32" ? "ffprobe.exe" : "ffprobe", ["-version"], {
158
- stdio: "ignore",
159
- shell: false,
160
- });
161
- return result.status === 0;
162
- }
163
-
164
- export function readMixinConfig(config: Record<string, unknown> | null): Record<string, unknown> | null {
165
- const channels = config?.channels;
166
- if (!channels || typeof channels !== "object") {
167
- return null;
168
- }
169
- const mixin = (channels as Record<string, unknown>).mixin;
170
- return mixin && typeof mixin === "object" ? (mixin as Record<string, unknown>) : null;
171
- }
172
-
173
- export function isPluginEnabled(config: Record<string, unknown> | null): boolean {
174
- const plugins = config?.plugins;
175
- if (!plugins || typeof plugins !== "object") {
176
- return false;
177
- }
178
- const allow = Array.isArray((plugins as Record<string, unknown>).allow)
179
- ? ((plugins as Record<string, unknown>).allow as unknown[]).map(String)
180
- : [];
181
- const entries = (plugins as Record<string, unknown>).entries;
182
- const mixinEntry = entries && typeof entries === "object"
183
- ? (entries as Record<string, unknown>).mixin
184
- : null;
185
- const enabled = mixinEntry && typeof mixinEntry === "object"
186
- ? (mixinEntry as Record<string, unknown>).enabled !== false
187
- : false;
188
- return allow.includes("mixin") && enabled;
189
- }