@nextclaw/openclaw-compat 0.3.27 → 0.3.29

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/dist/index.d.ts CHANGED
@@ -329,6 +329,15 @@ type PluginRuntime = {
329
329
  loadConfig: () => Record<string, unknown>;
330
330
  writeConfigFile: (next: Record<string, unknown>) => Promise<void>;
331
331
  };
332
+ logging: {
333
+ shouldLogVerbose: () => boolean;
334
+ };
335
+ media: {
336
+ detectMime: (params: {
337
+ buffer: Buffer;
338
+ }) => Promise<string | undefined>;
339
+ loadWebMedia: (url: string, options?: Record<string, unknown>) => Promise<Record<string, unknown>>;
340
+ };
332
341
  tools: {
333
342
  createMemorySearchTool: (params: {
334
343
  config?: Config;
@@ -340,9 +349,70 @@ type PluginRuntime = {
340
349
  }) => OpenClawPluginTool | null;
341
350
  };
342
351
  channel: {
352
+ media: {
353
+ fetchRemoteMedia: (params: {
354
+ url: string;
355
+ maxBytes?: number;
356
+ }) => Promise<Record<string, unknown>>;
357
+ saveMediaBuffer: (buffer: Buffer, contentType?: string, direction?: string, maxBytes?: number, fileName?: string) => Promise<{
358
+ path: string;
359
+ contentType?: string;
360
+ }>;
361
+ };
362
+ text: {
363
+ chunkMarkdownText: (text: string, limit: number) => string[];
364
+ resolveMarkdownTableMode: (params: Record<string, unknown>) => unknown;
365
+ convertMarkdownTables: (text: string, mode?: unknown) => string;
366
+ resolveTextChunkLimit: (cfg: Config, channel: string, accountId?: string, options?: Record<string, unknown>) => number;
367
+ resolveChunkMode: (cfg: Config, channel: string, accountId?: string) => string;
368
+ chunkTextWithMode: (text: string, limit: number, mode?: unknown) => string[];
369
+ hasControlCommand: (text: string, cfg?: Config) => boolean;
370
+ };
343
371
  reply: {
372
+ resolveEnvelopeFormatOptions: (cfg: Config) => Record<string, unknown>;
373
+ formatAgentEnvelope: (params: Record<string, unknown>) => string;
374
+ finalizeInboundContext: (params: Record<string, unknown>) => Record<string, unknown>;
375
+ withReplyDispatcher: (params: Record<string, unknown>) => Promise<Record<string, unknown>>;
376
+ dispatchReplyFromConfig: (params: Record<string, unknown>) => Promise<Record<string, unknown>>;
377
+ createReplyDispatcherWithTyping: (params: Record<string, unknown>) => {
378
+ dispatcher: {
379
+ sendToolResult: (payload: Record<string, unknown>) => boolean;
380
+ sendBlockReply: (payload: Record<string, unknown>) => boolean;
381
+ sendFinalReply: (payload: Record<string, unknown>) => boolean;
382
+ waitForIdle: () => Promise<void>;
383
+ getQueuedCounts: () => Record<string, number>;
384
+ markComplete: () => void;
385
+ };
386
+ replyOptions: Record<string, unknown>;
387
+ markDispatchIdle: () => void;
388
+ };
389
+ resolveHumanDelayConfig: (cfg: Config, agentId: string) => unknown;
344
390
  dispatchReplyWithBufferedBlockDispatcher: (params: PluginReplyDispatchParams) => Promise<void>;
345
391
  };
392
+ routing: {
393
+ resolveAgentRoute: (params: Record<string, unknown>) => Record<string, unknown>;
394
+ };
395
+ pairing: {
396
+ readAllowFromStore: (params: {
397
+ channel: string;
398
+ accountId?: string;
399
+ }) => Promise<string[]>;
400
+ upsertPairingRequest: (params: Record<string, unknown>) => Promise<{
401
+ code: string;
402
+ created: boolean;
403
+ }>;
404
+ };
405
+ commands: {
406
+ shouldComputeCommandAuthorized: (text: string, cfg?: Config) => boolean;
407
+ resolveCommandAuthorizedFromAuthorizers: (params: Record<string, unknown>) => boolean;
408
+ };
409
+ debounce: {
410
+ resolveInboundDebounceMs: (params: Record<string, unknown>) => number;
411
+ createInboundDebouncer: <T>(params: Record<string, unknown>) => {
412
+ enqueue: (item: T) => Promise<void>;
413
+ flushKey: (key: string) => Promise<void>;
414
+ };
415
+ };
346
416
  };
347
417
  };
348
418
  type OpenClawPluginApi = {
package/dist/index.js CHANGED
@@ -31,7 +31,7 @@ function buildOauthProviderAuthResult(params) {
31
31
  };
32
32
  }
33
33
  async function sleep(ms) {
34
- await new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
34
+ await new Promise((resolve2) => setTimeout(resolve2, Math.max(0, ms)));
35
35
  }
36
36
  function normalizePluginHttpPath(rawPath) {
37
37
  const trimmed = rawPath.trim();
@@ -883,7 +883,7 @@ async function ensureOpenClawExtensions(manifest) {
883
883
  return list;
884
884
  }
885
885
  async function runCommand(command, args, cwd) {
886
- return new Promise((resolve) => {
886
+ return new Promise((resolve2) => {
887
887
  const child = spawn(command, args, {
888
888
  cwd,
889
889
  env: {
@@ -902,10 +902,10 @@ async function runCommand(command, args, cwd) {
902
902
  stderr += String(chunk);
903
903
  });
904
904
  child.on("close", (code) => {
905
- resolve({ code: code ?? 1, stdout, stderr });
905
+ resolve2({ code: code ?? 1, stdout, stderr });
906
906
  });
907
907
  child.on("error", (error) => {
908
- resolve({ code: 1, stdout, stderr: `${stderr}
908
+ resolve2({ code: 1, stdout, stderr: `${stderr}
909
909
  ${String(error)}` });
910
910
  });
911
911
  });
@@ -1767,8 +1767,520 @@ function registerPluginNcpAgentRuntime(params) {
1767
1767
  }
1768
1768
 
1769
1769
  // src/plugins/runtime.ts
1770
+ import { randomUUID as randomUUID2 } from "crypto";
1770
1771
  import { getPackageVersion } from "@nextclaw/core";
1771
1772
  import { MemoryGetTool, MemorySearchTool } from "@nextclaw/core";
1773
+
1774
+ // src/plugins/runtime-shared.ts
1775
+ function asRecord(value) {
1776
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
1777
+ }
1778
+ function asString(value) {
1779
+ if (typeof value !== "string") {
1780
+ return void 0;
1781
+ }
1782
+ const trimmed = value.trim();
1783
+ return trimmed || void 0;
1784
+ }
1785
+ function asNumber(value) {
1786
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1787
+ return void 0;
1788
+ }
1789
+ return Math.trunc(value);
1790
+ }
1791
+ function readStringArray(value) {
1792
+ if (!Array.isArray(value)) {
1793
+ return [];
1794
+ }
1795
+ return value.map((entry) => asString(entry)).filter((entry) => Boolean(entry));
1796
+ }
1797
+ function resolveChannelConfig(cfg, channel) {
1798
+ const channels = asRecord(asRecord(cfg).channels);
1799
+ return asRecord(channels[channel]);
1800
+ }
1801
+ function resolveDefaultAgentId(cfg) {
1802
+ const agents = asRecord(cfg).agents;
1803
+ const list = Array.isArray(asRecord(agents).list) ? asRecord(agents).list : [];
1804
+ for (const entry of list) {
1805
+ const agent = asRecord(entry);
1806
+ if (agent.default === true) {
1807
+ return asString(agent.id) ?? "main";
1808
+ }
1809
+ }
1810
+ for (const entry of list) {
1811
+ const agentId = asString(asRecord(entry).id);
1812
+ if (agentId) {
1813
+ return agentId;
1814
+ }
1815
+ }
1816
+ return "main";
1817
+ }
1818
+ function resolveAccountId(value) {
1819
+ return asString(value) ?? "default";
1820
+ }
1821
+ function resolvePeer(params) {
1822
+ const peer = asRecord(params.peer);
1823
+ return {
1824
+ kind: asString(peer.kind) ?? "direct",
1825
+ id: asString(peer.id) ?? "default"
1826
+ };
1827
+ }
1828
+ function resolveSessionKey(params) {
1829
+ return [
1830
+ "agent",
1831
+ params.agentId,
1832
+ params.channel || "unknown",
1833
+ params.accountId || "default",
1834
+ params.peerKind || "direct",
1835
+ params.peerId || "default"
1836
+ ].join(":");
1837
+ }
1838
+ function splitTextIntoChunks(text, limit) {
1839
+ const maxLength = Number.isFinite(limit) && limit > 0 ? Math.trunc(limit) : 4e3;
1840
+ const normalized = text ?? "";
1841
+ if (normalized.length <= maxLength) {
1842
+ return [normalized];
1843
+ }
1844
+ const chunks = [];
1845
+ let remaining = normalized;
1846
+ while (remaining.length > maxLength) {
1847
+ let splitAt = remaining.lastIndexOf("\n", maxLength);
1848
+ if (splitAt <= 0 || splitAt < Math.floor(maxLength * 0.5)) {
1849
+ splitAt = maxLength;
1850
+ }
1851
+ chunks.push(remaining.slice(0, splitAt));
1852
+ remaining = remaining.slice(splitAt).replace(/^\n+/, "");
1853
+ }
1854
+ if (remaining.length > 0) {
1855
+ chunks.push(remaining);
1856
+ }
1857
+ return chunks;
1858
+ }
1859
+ function resolveTextChunkLimit(cfg, channel, _accountId, options) {
1860
+ const channelConfig = resolveChannelConfig(cfg, channel);
1861
+ const explicit = asNumber(channelConfig.textChunkLimit);
1862
+ if (explicit !== void 0 && explicit > 0) {
1863
+ return explicit;
1864
+ }
1865
+ const fallback = asNumber(options?.fallbackLimit);
1866
+ return fallback && fallback > 0 ? fallback : 4e3;
1867
+ }
1868
+ function hasControlCommand(text) {
1869
+ const body = text?.trim() ?? "";
1870
+ if (!body) {
1871
+ return false;
1872
+ }
1873
+ if (/^(?:\/|!)[a-z0-9_-]+(?:\s|$)/i.test(body)) {
1874
+ return true;
1875
+ }
1876
+ return /(?:^|\s)(?:\/|!)[a-z0-9_-]+(?:\s|$)/i.test(body);
1877
+ }
1878
+ function resolveCommandAuthorizedFromAuthorizers(params) {
1879
+ const authorizers = Array.isArray(params.authorizers) ? params.authorizers : [];
1880
+ const configured = authorizers.filter((entry) => asRecord(entry).configured === true);
1881
+ if (configured.length === 0) {
1882
+ return true;
1883
+ }
1884
+ return configured.some((entry) => asRecord(entry).allowed === true);
1885
+ }
1886
+ function resolveAgentRoute(params) {
1887
+ const cfg = params.cfg;
1888
+ const channel = asString(params.channel) ?? "unknown";
1889
+ const accountId = resolveAccountId(params.accountId);
1890
+ const peer = resolvePeer(params);
1891
+ const agentId = resolveDefaultAgentId(cfg);
1892
+ const sessionKey = resolveSessionKey({
1893
+ channel,
1894
+ accountId,
1895
+ agentId,
1896
+ peerKind: peer.kind,
1897
+ peerId: peer.id
1898
+ });
1899
+ return {
1900
+ agentId,
1901
+ accountId,
1902
+ channel,
1903
+ sessionKey,
1904
+ mainSessionKey: sessionKey,
1905
+ lastRoutePolicy: "main",
1906
+ matchedBy: "default"
1907
+ };
1908
+ }
1909
+
1910
+ // src/plugins/runtime-debounce.ts
1911
+ function resolveInboundDebounceMs(params) {
1912
+ const cfg = asRecord(params.cfg);
1913
+ const inbound = asRecord(asRecord(cfg.messages).inbound);
1914
+ const override = asNumber(params.overrideMs);
1915
+ const byChannel = asRecord(inbound.byChannel);
1916
+ const channelOverride = asNumber(byChannel[asString(params.channel) ?? ""]);
1917
+ const base = asNumber(inbound.debounceMs);
1918
+ return Math.max(0, override ?? channelOverride ?? base ?? 0);
1919
+ }
1920
+ function createInboundDebouncer(params) {
1921
+ const buffers = /* @__PURE__ */ new Map();
1922
+ const defaultDebounceMs = Math.max(0, Math.trunc(params.debounceMs ?? 0));
1923
+ const flushBuffer = async (key, buffer) => {
1924
+ buffers.delete(key);
1925
+ if (buffer.timeout) {
1926
+ clearTimeout(buffer.timeout);
1927
+ buffer.timeout = null;
1928
+ }
1929
+ if (buffer.items.length === 0 || !params.onFlush) {
1930
+ return;
1931
+ }
1932
+ try {
1933
+ await params.onFlush(buffer.items);
1934
+ } catch (error) {
1935
+ params.onError?.(error, buffer.items);
1936
+ }
1937
+ };
1938
+ const flushKey = async (key) => {
1939
+ const buffer = buffers.get(key);
1940
+ if (!buffer) {
1941
+ return;
1942
+ }
1943
+ await flushBuffer(key, buffer);
1944
+ };
1945
+ const scheduleFlush = (key, buffer) => {
1946
+ if (buffer.timeout) {
1947
+ clearTimeout(buffer.timeout);
1948
+ }
1949
+ buffer.timeout = setTimeout(() => {
1950
+ void flushBuffer(key, buffer);
1951
+ }, buffer.debounceMs);
1952
+ buffer.timeout.unref?.();
1953
+ };
1954
+ const enqueue = async (item) => {
1955
+ const key = params.buildKey?.(item);
1956
+ const resolvedDebounceMs = params.resolveDebounceMs?.(item);
1957
+ const debounceMs = typeof resolvedDebounceMs === "number" && Number.isFinite(resolvedDebounceMs) ? Math.max(0, Math.trunc(resolvedDebounceMs)) : defaultDebounceMs;
1958
+ const canDebounce = debounceMs > 0 && (params.shouldDebounce?.(item) ?? true);
1959
+ if (!key || !canDebounce) {
1960
+ if (key && buffers.has(key)) {
1961
+ await flushKey(key);
1962
+ }
1963
+ if (!params.onFlush) {
1964
+ return;
1965
+ }
1966
+ try {
1967
+ await params.onFlush([item]);
1968
+ } catch (error) {
1969
+ params.onError?.(error, [item]);
1970
+ }
1971
+ return;
1972
+ }
1973
+ const existing = buffers.get(key);
1974
+ if (existing) {
1975
+ existing.items.push(item);
1976
+ existing.debounceMs = debounceMs;
1977
+ scheduleFlush(key, existing);
1978
+ return;
1979
+ }
1980
+ const buffer = {
1981
+ items: [item],
1982
+ timeout: null,
1983
+ debounceMs
1984
+ };
1985
+ buffers.set(key, buffer);
1986
+ scheduleFlush(key, buffer);
1987
+ };
1988
+ return { enqueue, flushKey };
1989
+ }
1990
+
1991
+ // src/plugins/runtime-reply.ts
1992
+ function normalizeTextField(value) {
1993
+ if (typeof value !== "string") {
1994
+ return void 0;
1995
+ }
1996
+ return value.replace(/\r\n/g, "\n").trim() || void 0;
1997
+ }
1998
+ function countMediaEntries(ctx) {
1999
+ const paths = Array.isArray(ctx.MediaPaths) ? ctx.MediaPaths.length : 0;
2000
+ const urls = Array.isArray(ctx.MediaUrls) ? ctx.MediaUrls.length : 0;
2001
+ const single = ctx.MediaPath || ctx.MediaUrl ? 1 : 0;
2002
+ return Math.max(paths, urls, single);
2003
+ }
2004
+ function resolveEnvelopeFormatOptions(_cfg) {
2005
+ return {
2006
+ includeTimestamp: true,
2007
+ includeElapsed: true
2008
+ };
2009
+ }
2010
+ function formatAgentEnvelope(params) {
2011
+ const channel = typeof params.channel === "string" && params.channel.trim() ? params.channel.trim() : "Channel";
2012
+ const from = typeof params.from === "string" && params.from.trim() ? params.from.trim() : void 0;
2013
+ const body = typeof params.body === "string" ? params.body : "";
2014
+ const timestamp = params.timestamp instanceof Date ? params.timestamp : void 0;
2015
+ const parts = [channel];
2016
+ if (from) {
2017
+ parts.push(from);
2018
+ }
2019
+ if (timestamp) {
2020
+ parts.push(timestamp.toISOString());
2021
+ }
2022
+ return `[${parts.join(" ")}] ${body}`.trim();
2023
+ }
2024
+ function finalizeInboundContext(params) {
2025
+ const ctx = { ...params };
2026
+ const body = normalizeTextField(ctx.Body) ?? "";
2027
+ const rawBody = normalizeTextField(ctx.RawBody);
2028
+ const commandBody = normalizeTextField(ctx.CommandBody);
2029
+ ctx.Body = body;
2030
+ ctx.RawBody = rawBody;
2031
+ ctx.CommandBody = commandBody;
2032
+ ctx.BodyForAgent = normalizeTextField(ctx.BodyForAgent) ?? commandBody ?? rawBody ?? body;
2033
+ ctx.BodyForCommands = normalizeTextField(ctx.BodyForCommands) ?? commandBody ?? rawBody ?? body;
2034
+ ctx.CommandAuthorized = ctx.CommandAuthorized === true;
2035
+ const mediaCount = countMediaEntries(ctx);
2036
+ if (mediaCount > 0) {
2037
+ const mediaTypes = readStringArray(ctx.MediaTypes);
2038
+ const fallbackType = typeof ctx.MediaType === "string" && ctx.MediaType.trim() ? ctx.MediaType.trim() : "application/octet-stream";
2039
+ const padded = mediaTypes.slice();
2040
+ while (padded.length < mediaCount) {
2041
+ padded.push(fallbackType);
2042
+ }
2043
+ ctx.MediaTypes = padded;
2044
+ ctx.MediaType = padded[0] ?? fallbackType;
2045
+ }
2046
+ return ctx;
2047
+ }
2048
+ function createReplyDispatcher(options) {
2049
+ let chain = Promise.resolve();
2050
+ let pending = 1;
2051
+ let completeCalled = false;
2052
+ const counts = {
2053
+ tool: 0,
2054
+ block: 0,
2055
+ final: 0
2056
+ };
2057
+ const enqueue = (kind, payload) => {
2058
+ counts[kind] += 1;
2059
+ pending += 1;
2060
+ chain = chain.then(async () => {
2061
+ await options.deliver(payload, { kind });
2062
+ }).catch((error) => {
2063
+ options.onError?.(error, { kind });
2064
+ }).finally(() => {
2065
+ pending -= 1;
2066
+ if (pending === 1 && completeCalled) {
2067
+ pending -= 1;
2068
+ }
2069
+ if (pending === 0) {
2070
+ options.onIdle?.();
2071
+ }
2072
+ });
2073
+ return true;
2074
+ };
2075
+ const markComplete = () => {
2076
+ if (completeCalled) {
2077
+ return;
2078
+ }
2079
+ completeCalled = true;
2080
+ void Promise.resolve().then(() => {
2081
+ if (pending === 1) {
2082
+ pending -= 1;
2083
+ if (pending === 0) {
2084
+ options.onIdle?.();
2085
+ }
2086
+ }
2087
+ });
2088
+ };
2089
+ return {
2090
+ sendToolResult: (payload) => enqueue("tool", payload),
2091
+ sendBlockReply: (payload) => enqueue("block", payload),
2092
+ sendFinalReply: (payload) => enqueue("final", payload),
2093
+ waitForIdle: () => chain,
2094
+ getQueuedCounts: () => ({ ...counts }),
2095
+ markComplete
2096
+ };
2097
+ }
2098
+ function createReplyDispatcherWithTyping(options) {
2099
+ const dispatcher = createReplyDispatcher({
2100
+ deliver: options.deliver,
2101
+ onError: options.onError,
2102
+ onIdle: options.onIdle
2103
+ });
2104
+ return {
2105
+ dispatcher,
2106
+ replyOptions: {
2107
+ onReplyStart: options.onReplyStart
2108
+ },
2109
+ markDispatchIdle: () => {
2110
+ options.onIdle?.();
2111
+ }
2112
+ };
2113
+ }
2114
+ async function withReplyDispatcher(params) {
2115
+ try {
2116
+ return await params.run();
2117
+ } finally {
2118
+ params.dispatcher.markComplete();
2119
+ try {
2120
+ await params.dispatcher.waitForIdle();
2121
+ } finally {
2122
+ await params.onSettled?.();
2123
+ }
2124
+ }
2125
+ }
2126
+ async function dispatchReplyFromConfig(params) {
2127
+ const finalized = finalizeInboundContext(params.ctx);
2128
+ let queuedFinal = false;
2129
+ await params.bridgeDispatch({
2130
+ ctx: finalized,
2131
+ cfg: params.cfg,
2132
+ dispatcherOptions: {
2133
+ deliver: async (payload, info) => {
2134
+ if (info.kind === "tool") {
2135
+ params.dispatcher.sendToolResult(payload);
2136
+ return;
2137
+ }
2138
+ if (info.kind === "block") {
2139
+ params.dispatcher.sendBlockReply(payload);
2140
+ return;
2141
+ }
2142
+ queuedFinal = params.dispatcher.sendFinalReply(payload);
2143
+ },
2144
+ onError: () => void 0,
2145
+ ...params.replyOptions ?? {}
2146
+ }
2147
+ });
2148
+ return {
2149
+ queuedFinal,
2150
+ counts: params.dispatcher.getQueuedCounts()
2151
+ };
2152
+ }
2153
+ function resolveHumanDelayConfig(_cfg, _agentId) {
2154
+ return void 0;
2155
+ }
2156
+
2157
+ // src/plugins/runtime-media.ts
2158
+ import { existsSync } from "fs";
2159
+ import { mkdir, readFile, writeFile } from "fs/promises";
2160
+ import { randomUUID } from "crypto";
2161
+ import { tmpdir } from "os";
2162
+ import { basename, extname, isAbsolute, resolve } from "path";
2163
+ function detectMimeFromBuffer(buffer) {
2164
+ if (buffer.length >= 8 && buffer.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) {
2165
+ return "image/png";
2166
+ }
2167
+ if (buffer.length >= 3 && buffer[0] === 255 && buffer[1] === 216 && buffer[2] === 255) {
2168
+ return "image/jpeg";
2169
+ }
2170
+ if (buffer.length >= 6 && buffer.subarray(0, 6).toString("ascii") === "GIF87a") {
2171
+ return "image/gif";
2172
+ }
2173
+ if (buffer.length >= 6 && buffer.subarray(0, 6).toString("ascii") === "GIF89a") {
2174
+ return "image/gif";
2175
+ }
2176
+ if (buffer.length >= 12 && buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === "WEBP") {
2177
+ return "image/webp";
2178
+ }
2179
+ if (buffer.length >= 4 && buffer.subarray(0, 4).toString("ascii") === "%PDF") {
2180
+ return "application/pdf";
2181
+ }
2182
+ if (buffer.length >= 12 && buffer.subarray(4, 8).toString("ascii") === "ftyp") {
2183
+ return "video/mp4";
2184
+ }
2185
+ return void 0;
2186
+ }
2187
+ function mimeToExtension(contentType, fileName) {
2188
+ const byName = extname(fileName ?? "").trim();
2189
+ if (byName) {
2190
+ return byName;
2191
+ }
2192
+ switch (contentType) {
2193
+ case "image/png":
2194
+ return ".png";
2195
+ case "image/jpeg":
2196
+ return ".jpg";
2197
+ case "image/gif":
2198
+ return ".gif";
2199
+ case "image/webp":
2200
+ return ".webp";
2201
+ case "application/pdf":
2202
+ return ".pdf";
2203
+ case "video/mp4":
2204
+ return ".mp4";
2205
+ default:
2206
+ return ".bin";
2207
+ }
2208
+ }
2209
+ async function saveMediaBuffer(buffer, contentType, direction, maxBytes, fileName) {
2210
+ if (typeof maxBytes === "number" && Number.isFinite(maxBytes) && buffer.length > maxBytes) {
2211
+ throw new Error(`media exceeds maxBytes (${buffer.length} > ${maxBytes})`);
2212
+ }
2213
+ const detectedContentType = contentType ?? detectMimeFromBuffer(buffer) ?? "application/octet-stream";
2214
+ const targetDir = resolve(tmpdir(), "nextclaw-media", direction ?? "shared");
2215
+ await mkdir(targetDir, { recursive: true });
2216
+ const targetPath = resolve(
2217
+ targetDir,
2218
+ `${randomUUID()}${mimeToExtension(detectedContentType, fileName)}`
2219
+ );
2220
+ await writeFile(targetPath, buffer);
2221
+ return {
2222
+ path: targetPath,
2223
+ contentType: detectedContentType
2224
+ };
2225
+ }
2226
+ async function fetchRemoteMedia(params) {
2227
+ const response = await fetch(params.url);
2228
+ if (!response.ok) {
2229
+ throw new Error(`failed to fetch media: ${response.status} ${response.statusText}`);
2230
+ }
2231
+ const arrayBuffer = await response.arrayBuffer();
2232
+ const buffer = Buffer.from(arrayBuffer);
2233
+ if (typeof params.maxBytes === "number" && Number.isFinite(params.maxBytes) && buffer.length > params.maxBytes) {
2234
+ throw new Error(`media exceeds maxBytes (${buffer.length} > ${params.maxBytes})`);
2235
+ }
2236
+ return {
2237
+ buffer,
2238
+ contentType: asString(response.headers.get("content-type")) ?? detectMimeFromBuffer(buffer),
2239
+ fileName: basename(new URL(params.url).pathname) || "file"
2240
+ };
2241
+ }
2242
+ function isRemoteUrl(value) {
2243
+ return /^https?:\/\//i.test(value);
2244
+ }
2245
+ function assertAllowedLocalPath(filePath, localRoots) {
2246
+ if (localRoots.length === 0) {
2247
+ return;
2248
+ }
2249
+ const resolvedPath = resolve(filePath);
2250
+ const allowed = localRoots.some((root) => {
2251
+ const resolvedRoot = resolve(root);
2252
+ return resolvedPath === resolvedRoot || resolvedPath.startsWith(`${resolvedRoot}/`);
2253
+ });
2254
+ if (!allowed) {
2255
+ throw new Error(`local media path is outside allowed roots: ${resolvedPath}`);
2256
+ }
2257
+ }
2258
+ async function loadWebMedia(url, options) {
2259
+ if (isRemoteUrl(url)) {
2260
+ return fetchRemoteMedia({
2261
+ url,
2262
+ maxBytes: asNumber(options?.maxBytes)
2263
+ });
2264
+ }
2265
+ const localRoots = readStringArray(options?.localRoots);
2266
+ const candidate = isAbsolute(url) ? url : resolve(url);
2267
+ if (!existsSync(candidate)) {
2268
+ throw new Error(`local media file not found: ${candidate}`);
2269
+ }
2270
+ assertAllowedLocalPath(candidate, localRoots);
2271
+ const buffer = await readFile(candidate);
2272
+ const maxBytes = asNumber(options?.maxBytes);
2273
+ if (typeof maxBytes === "number" && buffer.length > maxBytes) {
2274
+ throw new Error(`media exceeds maxBytes (${buffer.length} > ${maxBytes})`);
2275
+ }
2276
+ return {
2277
+ buffer,
2278
+ fileName: basename(candidate),
2279
+ contentType: detectMimeFromBuffer(buffer)
2280
+ };
2281
+ }
2282
+
2283
+ // src/plugins/runtime.ts
1772
2284
  var bridge = {};
1773
2285
  function setPluginRuntimeBridge(next) {
1774
2286
  bridge = next ?? {};
@@ -1806,13 +2318,83 @@ function createPluginRuntime(params) {
1806
2318
  loadConfig: () => loadConfigWithFallback(params.config),
1807
2319
  writeConfigFile: async (next) => writeConfigWithFallback(next)
1808
2320
  },
2321
+ logging: {
2322
+ shouldLogVerbose: () => false
2323
+ },
2324
+ media: {
2325
+ detectMime: async ({ buffer }) => detectMimeFromBuffer(buffer),
2326
+ loadWebMedia: async (url, options) => loadWebMedia(url, options)
2327
+ },
1809
2328
  tools: {
1810
2329
  createMemorySearchTool: () => toPluginTool(new MemorySearchTool(params.workspace)),
1811
2330
  createMemoryGetTool: () => toPluginTool(new MemoryGetTool(params.workspace))
1812
2331
  },
1813
2332
  channel: {
2333
+ media: {
2334
+ fetchRemoteMedia: async (input) => fetchRemoteMedia({
2335
+ url: asString(input.url) ?? "",
2336
+ maxBytes: asNumber(input.maxBytes)
2337
+ }),
2338
+ saveMediaBuffer: async (buffer, contentType, direction, maxBytes, fileName) => saveMediaBuffer(buffer, contentType, direction, maxBytes, fileName)
2339
+ },
2340
+ text: {
2341
+ chunkMarkdownText: (text, limit) => splitTextIntoChunks(text, limit),
2342
+ resolveMarkdownTableMode: () => "preserve",
2343
+ convertMarkdownTables: (text) => text,
2344
+ resolveTextChunkLimit,
2345
+ resolveChunkMode: () => "length",
2346
+ chunkTextWithMode: (text, limit) => splitTextIntoChunks(text, limit),
2347
+ hasControlCommand: (text) => hasControlCommand(text)
2348
+ },
1814
2349
  reply: {
2350
+ resolveEnvelopeFormatOptions,
2351
+ formatAgentEnvelope,
2352
+ finalizeInboundContext,
2353
+ withReplyDispatcher: async (input) => withReplyDispatcher({
2354
+ dispatcher: input.dispatcher,
2355
+ run: input.run,
2356
+ onSettled: typeof input.onSettled === "function" ? input.onSettled : void 0
2357
+ }),
2358
+ dispatchReplyFromConfig: async (input) => dispatchReplyFromConfig({
2359
+ ctx: asRecord(input.ctx),
2360
+ cfg: input.cfg,
2361
+ dispatcher: input.dispatcher,
2362
+ bridgeDispatch: dispatchReplyWithFallback,
2363
+ replyOptions: asRecord(input.replyOptions)
2364
+ }),
2365
+ createReplyDispatcherWithTyping: (input) => createReplyDispatcherWithTyping({
2366
+ deliver: input.deliver,
2367
+ onError: typeof input.onError === "function" ? input.onError : void 0,
2368
+ onIdle: typeof input.onIdle === "function" ? input.onIdle : void 0,
2369
+ onReplyStart: typeof input.onReplyStart === "function" ? input.onReplyStart : void 0
2370
+ }),
2371
+ resolveHumanDelayConfig,
1815
2372
  dispatchReplyWithBufferedBlockDispatcher: async (dispatchParams) => dispatchReplyWithFallback(dispatchParams)
2373
+ },
2374
+ routing: {
2375
+ resolveAgentRoute
2376
+ },
2377
+ pairing: {
2378
+ readAllowFromStore: async () => [],
2379
+ upsertPairingRequest: async () => ({
2380
+ code: randomUUID2().replace(/-/g, "").slice(0, 8).toUpperCase(),
2381
+ created: true
2382
+ })
2383
+ },
2384
+ commands: {
2385
+ shouldComputeCommandAuthorized: (text) => hasControlCommand(text),
2386
+ resolveCommandAuthorizedFromAuthorizers
2387
+ },
2388
+ debounce: {
2389
+ resolveInboundDebounceMs,
2390
+ createInboundDebouncer: (input) => createInboundDebouncer({
2391
+ debounceMs: asNumber(input.debounceMs),
2392
+ buildKey: typeof input.buildKey === "function" ? input.buildKey : void 0,
2393
+ shouldDebounce: typeof input.shouldDebounce === "function" ? input.shouldDebounce : void 0,
2394
+ resolveDebounceMs: typeof input.resolveDebounceMs === "function" ? input.resolveDebounceMs : void 0,
2395
+ onFlush: typeof input.onFlush === "function" ? input.onFlush : void 0,
2396
+ onError: typeof input.onError === "function" ? input.onError : void 0
2397
+ })
1816
2398
  }
1817
2399
  }
1818
2400
  };
@@ -2686,7 +3268,7 @@ function buildPluginStatusReport(params) {
2686
3268
 
2687
3269
  // src/plugins/uninstall.ts
2688
3270
  import fs8 from "fs/promises";
2689
- import { existsSync, statSync } from "fs";
3271
+ import { existsSync as existsSync2, statSync } from "fs";
2690
3272
  import path9 from "path";
2691
3273
  import { getWorkspacePathFromConfig as getWorkspacePathFromConfig3 } from "@nextclaw/core";
2692
3274
  function isLinkedPathInstall(record) {
@@ -2827,7 +3409,7 @@ function matchesPluginLoadPath(rawPath, pluginId) {
2827
3409
  return false;
2828
3410
  }
2829
3411
  const resolvedPath = path9.resolve(normalizedPath);
2830
- if (!existsSync(resolvedPath)) {
3412
+ if (!existsSync2(resolvedPath)) {
2831
3413
  return false;
2832
3414
  }
2833
3415
  const candidateRoot = (() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/openclaw-compat",
3
- "version": "0.3.27",
3
+ "version": "0.3.29",
4
4
  "private": false,
5
5
  "description": "OpenClaw plugin compatibility layer for NextClaw.",
6
6
  "type": "module",
@@ -19,21 +19,21 @@
19
19
  "jiti": "^1.21.7",
20
20
  "jszip": "^3.10.1",
21
21
  "tar": "^7.4.3",
22
- "@nextclaw/channel-plugin-dingtalk": "0.2.15",
23
- "@nextclaw/channel-plugin-discord": "0.2.15",
24
- "@nextclaw/channel-plugin-mochat": "0.2.15",
25
- "@nextclaw/channel-plugin-email": "0.2.15",
26
- "@nextclaw/channel-plugin-slack": "0.2.15",
27
- "@nextclaw/channel-plugin-qq": "0.2.15",
28
- "@nextclaw/channel-plugin-telegram": "0.2.15",
29
- "@nextclaw/channel-plugin-whatsapp": "0.2.15",
30
- "@nextclaw/channel-plugin-weixin": "0.1.9",
31
- "@nextclaw/core": "0.11.1",
32
- "@nextclaw/channel-plugin-wecom": "0.2.15",
33
- "@nextclaw/channel-runtime": "0.4.1",
34
- "@nextclaw/ncp": "0.3.2",
22
+ "@nextclaw/channel-plugin-email": "0.2.16",
23
+ "@nextclaw/channel-plugin-dingtalk": "0.2.16",
24
+ "@nextclaw/channel-plugin-slack": "0.2.16",
25
+ "@nextclaw/channel-plugin-mochat": "0.2.16",
26
+ "@nextclaw/channel-plugin-feishu": "0.2.19",
27
+ "@nextclaw/channel-plugin-discord": "0.2.16",
28
+ "@nextclaw/channel-plugin-qq": "0.2.16",
29
+ "@nextclaw/channel-plugin-wecom": "0.2.16",
35
30
  "@nextclaw/ncp-toolkit": "0.4.2",
36
- "@nextclaw/channel-plugin-feishu": "0.2.19"
31
+ "@nextclaw/channel-runtime": "0.4.2",
32
+ "@nextclaw/ncp": "0.3.2",
33
+ "@nextclaw/channel-plugin-telegram": "0.2.16",
34
+ "@nextclaw/channel-plugin-weixin": "0.1.9",
35
+ "@nextclaw/channel-plugin-whatsapp": "0.2.16",
36
+ "@nextclaw/core": "0.11.1"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/node": "^20.17.6",