@invago/mixin 1.0.7 → 1.0.9
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 +148 -3
- package/README.zh-CN.md +148 -3
- package/package.json +1 -1
- package/src/channel.ts +233 -15
- package/src/config-schema.ts +21 -0
- package/src/config.ts +99 -10
- package/src/inbound-handler.ts +479 -178
- package/src/outbound-plan.ts +197 -0
- package/src/reply-format.ts +90 -23
- package/src/send-service.ts +233 -14
- package/src/status.ts +100 -0
- package/tools/mixin-plugin-onboard/README.md +98 -0
- package/tools/mixin-plugin-onboard/bin/mixin-plugin-onboard.mjs +3 -0
- package/tools/mixin-plugin-onboard/src/commands/doctor.ts +28 -0
- package/tools/mixin-plugin-onboard/src/commands/info.ts +23 -0
- package/tools/mixin-plugin-onboard/src/commands/install.ts +5 -0
- package/tools/mixin-plugin-onboard/src/commands/update.ts +5 -0
- package/tools/mixin-plugin-onboard/src/index.ts +49 -0
- package/tools/mixin-plugin-onboard/src/utils.ts +189 -0
|
@@ -0,0 +1,189 @@
|
|
|
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
|
+
}
|