@cored-im/openclaw-plugin 0.1.4 → 0.1.5
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.cjs +196 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -60
- package/dist/index.d.ts +11 -60
- package/dist/index.js +201 -54
- package/dist/index.js.map +1 -1
- package/dist/setup-entry.cjs +311 -36
- package/dist/setup-entry.cjs.map +1 -1
- package/dist/setup-entry.d.cts +9 -1
- package/dist/setup-entry.d.ts +9 -1
- package/dist/setup-entry.js +314 -36
- package/dist/setup-entry.js.map +1 -1
- package/openclaw.plugin.json +2 -3
- package/package.json +3 -3
- package/src/channel.ts +152 -4
- package/src/index.test.ts +52 -26
- package/src/index.ts +56 -44
- package/src/messaging/inbound.ts +43 -11
- package/src/setup-entry.ts +2 -46
- package/src/types.ts +3 -33
- package/src/typings/openclaw-plugin-sdk.d.ts +152 -19
package/src/channel.ts
CHANGED
|
@@ -1,28 +1,113 @@
|
|
|
1
1
|
// Copyright (c) 2026 Cored Limited
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
+
import {
|
|
5
|
+
createChatChannelPlugin,
|
|
6
|
+
createChannelPluginBase,
|
|
7
|
+
} from "openclaw/plugin-sdk/core";
|
|
8
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
4
9
|
import { listAccountIds, resolveAccountConfig } from "./config.js";
|
|
5
10
|
import { sendText } from "./messaging/outbound.js";
|
|
6
11
|
import { parseTarget } from "./targets.js";
|
|
7
12
|
|
|
8
|
-
|
|
9
|
-
|
|
13
|
+
type ResolvedAccount = {
|
|
14
|
+
accountId: string | null;
|
|
15
|
+
appId: string;
|
|
16
|
+
appSecret: string;
|
|
17
|
+
backendUrl: string;
|
|
18
|
+
enableEncryption: boolean;
|
|
19
|
+
requestTimeout: number;
|
|
20
|
+
requireMention: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function resolveAccount(
|
|
24
|
+
cfg: OpenClawConfig,
|
|
25
|
+
accountId?: string | null,
|
|
26
|
+
): ResolvedAccount {
|
|
27
|
+
const section = (cfg.channels as Record<string, any>)?.["cored"];
|
|
28
|
+
const accounts = section?.accounts;
|
|
29
|
+
const defaultAccount = section?.defaultAccount;
|
|
30
|
+
|
|
31
|
+
// If multi-account mode, resolve the specific account
|
|
32
|
+
if (accounts && Object.keys(accounts).length > 0) {
|
|
33
|
+
const targetId = accountId ?? defaultAccount ?? Object.keys(accounts)[0];
|
|
34
|
+
const account = accounts[targetId];
|
|
35
|
+
if (!account) {
|
|
36
|
+
throw new Error(`cored: account "${targetId}" not found`);
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
accountId: targetId,
|
|
40
|
+
appId: account.appId,
|
|
41
|
+
appSecret: account.appSecret,
|
|
42
|
+
backendUrl: account.backendUrl,
|
|
43
|
+
enableEncryption: account.enableEncryption ?? section.enableEncryption ?? true,
|
|
44
|
+
requestTimeout: account.requestTimeout ?? section.requestTimeout ?? 30000,
|
|
45
|
+
requireMention: account.requireMention ?? section.requireMention ?? true,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Single-account mode
|
|
50
|
+
const appId = section?.appId;
|
|
51
|
+
const appSecret = section?.appSecret;
|
|
52
|
+
const backendUrl = section?.backendUrl;
|
|
53
|
+
|
|
54
|
+
if (!appId || !appSecret || !backendUrl) {
|
|
55
|
+
throw new Error("cored: appId, appSecret, and backendUrl are required");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
accountId: null,
|
|
60
|
+
appId,
|
|
61
|
+
appSecret,
|
|
62
|
+
backendUrl,
|
|
63
|
+
enableEncryption: section?.enableEncryption ?? true,
|
|
64
|
+
requestTimeout: section?.requestTimeout ?? 30000,
|
|
65
|
+
requireMention: section?.requireMention ?? true,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const coredPlugin = createChatChannelPlugin<ResolvedAccount>({
|
|
70
|
+
base: createChannelPluginBase({
|
|
71
|
+
id: "cored",
|
|
72
|
+
setup: {
|
|
73
|
+
resolveAccount,
|
|
74
|
+
inspectAccount(cfg, accountId) {
|
|
75
|
+
const section = (cfg.channels as Record<string, any>)?.["cored"];
|
|
76
|
+
const hasConfig = Boolean(
|
|
77
|
+
section?.appId && section?.appSecret && section?.backendUrl,
|
|
78
|
+
);
|
|
79
|
+
return {
|
|
80
|
+
enabled: Boolean(section?.enabled !== false),
|
|
81
|
+
configured: hasConfig,
|
|
82
|
+
tokenStatus: hasConfig ? "available" : "missing",
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
|
|
88
|
+
// Plugin metadata
|
|
10
89
|
meta: {
|
|
11
90
|
id: "cored",
|
|
12
91
|
label: "Cored",
|
|
13
92
|
selectionLabel: "Cored",
|
|
14
93
|
docsPath: "/channels/cored",
|
|
15
|
-
blurb: "
|
|
94
|
+
blurb: "Connect OpenClaw to Cored",
|
|
16
95
|
aliases: ["cored", "co"],
|
|
17
96
|
},
|
|
97
|
+
|
|
98
|
+
// Capabilities
|
|
18
99
|
capabilities: {
|
|
19
100
|
chatTypes: ["direct", "group"] as const,
|
|
20
101
|
},
|
|
102
|
+
|
|
103
|
+
// Config
|
|
21
104
|
config: {
|
|
22
105
|
listAccountIds: (cfg: unknown) => listAccountIds(cfg),
|
|
23
106
|
resolveAccount: (cfg: unknown, accountId?: string) =>
|
|
24
107
|
resolveAccountConfig(cfg, accountId),
|
|
25
108
|
},
|
|
109
|
+
|
|
110
|
+
// Outbound messaging
|
|
26
111
|
outbound: {
|
|
27
112
|
deliveryMode: "direct" as const,
|
|
28
113
|
resolveTarget: ({ to }: { to?: string }) => {
|
|
@@ -58,4 +143,67 @@ export const coredPlugin = {
|
|
|
58
143
|
return sendText(target.id, text, accountId);
|
|
59
144
|
},
|
|
60
145
|
},
|
|
61
|
-
|
|
146
|
+
|
|
147
|
+
// Setup wizard for openclaw onboard
|
|
148
|
+
setupWizard: {
|
|
149
|
+
channel: "cored",
|
|
150
|
+
status: {
|
|
151
|
+
configuredLabel: "Connected",
|
|
152
|
+
unconfiguredLabel: "Not configured",
|
|
153
|
+
resolveConfigured: ({ cfg }: { cfg: OpenClawConfig }) => {
|
|
154
|
+
const section = (cfg.channels as Record<string, any>)?.["cored"];
|
|
155
|
+
return Boolean(section?.appId && section?.appSecret && section?.backendUrl);
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
credentials: [
|
|
159
|
+
{
|
|
160
|
+
inputKey: "appId",
|
|
161
|
+
providerHint: "cored",
|
|
162
|
+
credentialLabel: "App ID",
|
|
163
|
+
preferredEnvVar: "CORED_APP_ID",
|
|
164
|
+
envPrompt: "Use CORED_APP_ID from environment?",
|
|
165
|
+
keepPrompt: "Keep current App ID?",
|
|
166
|
+
inputPrompt: "Enter your Cored App ID:",
|
|
167
|
+
inspect: ({ cfg }: { cfg: OpenClawConfig }) => {
|
|
168
|
+
const section = (cfg.channels as Record<string, any>)?.["cored"];
|
|
169
|
+
return {
|
|
170
|
+
accountConfigured: Boolean(section?.appId),
|
|
171
|
+
hasConfiguredValue: Boolean(section?.appId),
|
|
172
|
+
};
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
inputKey: "appSecret",
|
|
177
|
+
providerHint: "cored",
|
|
178
|
+
credentialLabel: "App Secret",
|
|
179
|
+
preferredEnvVar: "CORED_APP_SECRET",
|
|
180
|
+
envPrompt: "Use CORED_APP_SECRET from environment?",
|
|
181
|
+
keepPrompt: "Keep current App Secret?",
|
|
182
|
+
inputPrompt: "Enter your Cored App Secret:",
|
|
183
|
+
inspect: ({ cfg }: { cfg: OpenClawConfig }) => {
|
|
184
|
+
const section = (cfg.channels as Record<string, any>)?.["cored"];
|
|
185
|
+
return {
|
|
186
|
+
accountConfigured: Boolean(section?.appSecret),
|
|
187
|
+
hasConfiguredValue: Boolean(section?.appSecret),
|
|
188
|
+
};
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
inputKey: "backendUrl",
|
|
193
|
+
providerHint: "cored",
|
|
194
|
+
credentialLabel: "Backend URL",
|
|
195
|
+
preferredEnvVar: "CORED_BACKEND_URL",
|
|
196
|
+
envPrompt: "Use CORED_BACKEND_URL from environment?",
|
|
197
|
+
keepPrompt: "Keep current Backend URL?",
|
|
198
|
+
inputPrompt: "Enter your Cored backend server URL:",
|
|
199
|
+
inspect: ({ cfg }: { cfg: OpenClawConfig }) => {
|
|
200
|
+
const section = (cfg.channels as Record<string, any>)?.["cored"];
|
|
201
|
+
return {
|
|
202
|
+
accountConfigured: Boolean(section?.backendUrl),
|
|
203
|
+
hasConfiguredValue: Boolean(section?.backendUrl),
|
|
204
|
+
};
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
});
|
package/src/index.test.ts
CHANGED
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
4
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
|
|
6
|
+
// Mock the SDK module before any imports
|
|
7
|
+
vi.mock("openclaw/plugin-sdk/core", () => ({
|
|
8
|
+
defineChannelPluginEntry: (config: unknown) => config,
|
|
9
|
+
createChatChannelPlugin: (config: unknown) => config,
|
|
10
|
+
createChannelPluginBase: (config: unknown) => config,
|
|
11
|
+
}));
|
|
7
12
|
|
|
8
13
|
vi.mock("./core/cored-client.js", () => ({
|
|
9
14
|
createClient: vi.fn(),
|
|
@@ -18,20 +23,29 @@ vi.mock("./messaging/outbound.js", () => ({
|
|
|
18
23
|
readMessage: vi.fn().mockResolvedValue(undefined),
|
|
19
24
|
}));
|
|
20
25
|
|
|
26
|
+
// Import after mocks are set up
|
|
21
27
|
import { createClient, clientCount } from "./core/cored-client.js";
|
|
22
28
|
|
|
23
|
-
|
|
29
|
+
// Import the entry module - need to use dynamic import for esm
|
|
30
|
+
const entry = await import("./index.js");
|
|
31
|
+
|
|
32
|
+
interface MockPluginApi {
|
|
33
|
+
registerChannel: ReturnType<typeof vi.fn>;
|
|
34
|
+
registerService: ReturnType<typeof vi.fn>;
|
|
35
|
+
config: { channels?: Record<string, unknown> };
|
|
36
|
+
logger?: {
|
|
37
|
+
info: ReturnType<typeof vi.fn>;
|
|
38
|
+
warn: ReturnType<typeof vi.fn>;
|
|
39
|
+
error: ReturnType<typeof vi.fn>;
|
|
40
|
+
debug: ReturnType<typeof vi.fn>;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function createMockApi(config: unknown = {}): MockPluginApi {
|
|
24
45
|
return {
|
|
25
46
|
registerChannel: vi.fn(),
|
|
26
47
|
registerService: vi.fn(),
|
|
27
|
-
config: config as
|
|
28
|
-
runtime: {
|
|
29
|
-
channel: {
|
|
30
|
-
reply: { dispatchReplyWithBufferedBlockDispatcher: vi.fn() },
|
|
31
|
-
session: { recordInboundSession: vi.fn() },
|
|
32
|
-
routing: { resolveAgentRoute: vi.fn() },
|
|
33
|
-
},
|
|
34
|
-
},
|
|
48
|
+
config: config as MockPluginApi["config"],
|
|
35
49
|
logger: {
|
|
36
50
|
info: vi.fn(),
|
|
37
51
|
warn: vi.fn(),
|
|
@@ -41,25 +55,37 @@ function createMockApi(config: unknown = {}): PluginApi {
|
|
|
41
55
|
};
|
|
42
56
|
}
|
|
43
57
|
|
|
44
|
-
function extractStartFn(api:
|
|
58
|
+
function extractStartFn(api: MockPluginApi): () => Promise<void> {
|
|
45
59
|
const call = (api.registerService as ReturnType<typeof vi.fn>).mock.calls[0];
|
|
46
60
|
return call[0].start;
|
|
47
61
|
}
|
|
48
62
|
|
|
49
|
-
describe("
|
|
63
|
+
describe("entry module", () => {
|
|
50
64
|
beforeEach(() => {
|
|
51
65
|
vi.mocked(clientCount).mockReturnValue(0);
|
|
52
66
|
vi.mocked(createClient).mockResolvedValue({ client: {}, config: {} } as any);
|
|
53
67
|
});
|
|
54
68
|
|
|
55
|
-
it("
|
|
56
|
-
|
|
57
|
-
|
|
69
|
+
it("exports entry with correct id", () => {
|
|
70
|
+
expect(entry.default.id).toBe("cored");
|
|
71
|
+
});
|
|
58
72
|
|
|
59
|
-
|
|
60
|
-
expect(
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
it("exports entry with correct name", () => {
|
|
74
|
+
expect(entry.default.name).toBe("Cored");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("exports entry with plugin", () => {
|
|
78
|
+
expect(entry.default.plugin).toBeDefined();
|
|
79
|
+
expect(entry.default.plugin.base.id).toBe("cored");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("exports registerFull function", () => {
|
|
83
|
+
expect(entry.default.registerFull).toBeInstanceOf(Function);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("registerFull registers service", () => {
|
|
87
|
+
const api = createMockApi();
|
|
88
|
+
entry.default.registerFull(api);
|
|
63
89
|
|
|
64
90
|
expect(api.registerService).toHaveBeenCalledOnce();
|
|
65
91
|
expect(api.registerService).toHaveBeenCalledWith(
|
|
@@ -67,9 +93,9 @@ describe("register", () => {
|
|
|
67
93
|
);
|
|
68
94
|
});
|
|
69
95
|
|
|
70
|
-
it("logs plugin registration", () => {
|
|
96
|
+
it("registerFull logs plugin registration", () => {
|
|
71
97
|
const api = createMockApi();
|
|
72
|
-
|
|
98
|
+
entry.default.registerFull(api);
|
|
73
99
|
expect(api.logger!.info).toHaveBeenCalledWith("[cored] plugin registered");
|
|
74
100
|
});
|
|
75
101
|
});
|
|
@@ -91,7 +117,7 @@ describe("service start — config validation", () => {
|
|
|
91
117
|
},
|
|
92
118
|
},
|
|
93
119
|
});
|
|
94
|
-
|
|
120
|
+
entry.default.registerFull(api);
|
|
95
121
|
await extractStartFn(api)();
|
|
96
122
|
|
|
97
123
|
expect(api.logger!.warn).toHaveBeenCalledWith(
|
|
@@ -113,7 +139,7 @@ describe("service start — config validation", () => {
|
|
|
113
139
|
},
|
|
114
140
|
},
|
|
115
141
|
});
|
|
116
|
-
|
|
142
|
+
entry.default.registerFull(api);
|
|
117
143
|
await extractStartFn(api)();
|
|
118
144
|
|
|
119
145
|
expect(api.logger!.warn).toHaveBeenCalledWith(
|
|
@@ -128,7 +154,7 @@ describe("service start — config validation", () => {
|
|
|
128
154
|
cored: { appId: "a", appSecret: "s", backendUrl: "https://ok.io" },
|
|
129
155
|
},
|
|
130
156
|
});
|
|
131
|
-
|
|
157
|
+
entry.default.registerFull(api);
|
|
132
158
|
await extractStartFn(api)();
|
|
133
159
|
|
|
134
160
|
expect(createClient).toHaveBeenCalledOnce();
|
|
@@ -148,7 +174,7 @@ describe("service start — config validation", () => {
|
|
|
148
174
|
},
|
|
149
175
|
},
|
|
150
176
|
});
|
|
151
|
-
|
|
177
|
+
entry.default.registerFull(api);
|
|
152
178
|
await extractStartFn(api)();
|
|
153
179
|
|
|
154
180
|
// bad account skipped with warning
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Copyright (c) 2026 Cored Limited
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
+
import { defineChannelPluginEntry, type PluginApi } from "openclaw/plugin-sdk/core";
|
|
4
5
|
import { coredPlugin } from "./channel.js";
|
|
5
6
|
import { listEnabledAccountConfigs, validateAccountConfig } from "./config.js";
|
|
6
7
|
import {
|
|
@@ -8,72 +9,83 @@ import {
|
|
|
8
9
|
destroyAllClients,
|
|
9
10
|
clientCount,
|
|
10
11
|
} from "./core/cored-client.js";
|
|
11
|
-
import { processInboundMessage } from "./messaging/inbound.js";
|
|
12
|
+
import { processInboundMessage, type ExtendedPluginApi } from "./messaging/inbound.js";
|
|
12
13
|
import { makeDeliver, setTyping, clearTyping, readMessage } from "./messaging/outbound.js";
|
|
13
|
-
import type {
|
|
14
|
+
import type { CoredAccountConfig, CoredMessageEvent } from "./types.js";
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
// Extended PluginApi with config type for service registration
|
|
17
|
+
interface ServicePluginApi extends ExtendedPluginApi {
|
|
18
|
+
config: { channels?: Record<string, unknown> };
|
|
19
|
+
}
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
export default defineChannelPluginEntry({
|
|
22
|
+
id: "cored",
|
|
23
|
+
name: "Cored",
|
|
24
|
+
description: "Connect OpenClaw with Cored",
|
|
25
|
+
plugin: coredPlugin,
|
|
26
|
+
registerFull(api) {
|
|
27
|
+
const typedApi = api as ServicePluginApi;
|
|
22
28
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
29
|
+
typedApi.registerService({
|
|
30
|
+
id: "cored-sdk",
|
|
31
|
+
start: async () => {
|
|
32
|
+
if (clientCount() > 0) return;
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
`[cored] skipping account=${account.accountId}: ${errors.map((e) => e.message).join("; ")}`,
|
|
34
|
-
);
|
|
35
|
-
continue;
|
|
34
|
+
const accounts = listEnabledAccountConfigs(typedApi.config);
|
|
35
|
+
if (accounts.length === 0) {
|
|
36
|
+
typedApi.logger?.warn?.("[cored] no enabled account config found — service idle");
|
|
37
|
+
return;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
for (const account of accounts) {
|
|
41
|
+
const errors = validateAccountConfig(account);
|
|
42
|
+
if (errors.length > 0) {
|
|
43
|
+
typedApi.logger?.warn?.(
|
|
44
|
+
`[cored] skipping account=${account.accountId}: ${errors.map((e) => e.message).join("; ")}`,
|
|
45
|
+
);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
await startAccount(typedApi, account);
|
|
51
|
+
typedApi.logger?.info?.(
|
|
52
|
+
`[cored] account=${account.accountId} connected (appId=${account.appId})`,
|
|
53
|
+
);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
typedApi.logger?.error?.(
|
|
56
|
+
`[cored] account=${account.accountId} failed to start: ${err instanceof Error ? err.message : String(err)}`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
47
59
|
}
|
|
48
|
-
}
|
|
49
60
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
typedApi.logger?.info?.(`[cored] service started with ${clientCount()} account(s)`);
|
|
62
|
+
},
|
|
63
|
+
stop: async () => {
|
|
64
|
+
await destroyAllClients();
|
|
65
|
+
typedApi.logger?.info?.("[cored] service stopped — all clients disconnected");
|
|
66
|
+
},
|
|
67
|
+
});
|
|
57
68
|
|
|
58
|
-
|
|
59
|
-
}
|
|
69
|
+
typedApi.logger?.info?.("[cored] plugin registered");
|
|
70
|
+
},
|
|
71
|
+
});
|
|
60
72
|
|
|
61
73
|
/**
|
|
62
74
|
* Start a single account — create client, subscribe to inbound events.
|
|
63
75
|
*/
|
|
64
76
|
async function startAccount(
|
|
65
|
-
api:
|
|
77
|
+
api: ServicePluginApi,
|
|
66
78
|
account: CoredAccountConfig,
|
|
67
79
|
): Promise<void> {
|
|
68
|
-
const deliver = makeDeliver(account.accountId, (msg) => api.logger?.warn(msg));
|
|
80
|
+
const deliver = makeDeliver(account.accountId, (msg) => api.logger?.warn?.(msg));
|
|
69
81
|
|
|
70
82
|
await createClient({
|
|
71
83
|
config: account,
|
|
72
|
-
log: (msg: string) => api.logger?.debug(msg),
|
|
84
|
+
log: (msg: string) => api.logger?.debug?.(msg),
|
|
73
85
|
onMessage: (event: CoredMessageEvent, accountConfig: CoredAccountConfig) => {
|
|
74
86
|
// Fire-and-forget: process inbound with typing indicator
|
|
75
87
|
handleInbound(api, accountConfig, event, deliver).catch((err) => {
|
|
76
|
-
api.logger?.error(
|
|
88
|
+
api.logger?.error?.(
|
|
77
89
|
`[cored] unhandled inbound error for account=${accountConfig.accountId}: ${err}`,
|
|
78
90
|
);
|
|
79
91
|
});
|
|
@@ -85,7 +97,7 @@ async function startAccount(
|
|
|
85
97
|
* Handle a single inbound message with typing indicator lifecycle.
|
|
86
98
|
*/
|
|
87
99
|
async function handleInbound(
|
|
88
|
-
api:
|
|
100
|
+
api: ServicePluginApi,
|
|
89
101
|
account: CoredAccountConfig,
|
|
90
102
|
event: CoredMessageEvent,
|
|
91
103
|
deliver: (chatId: string, text: string) => Promise<void>,
|
package/src/messaging/inbound.ts
CHANGED
|
@@ -17,6 +17,30 @@ import type {
|
|
|
17
17
|
PluginApi,
|
|
18
18
|
} from "../types.js";
|
|
19
19
|
|
|
20
|
+
// Extended PluginApi with runtime surfaces used by this module
|
|
21
|
+
export interface ExtendedPluginApi extends PluginApi {
|
|
22
|
+
runtime?: {
|
|
23
|
+
channel?: {
|
|
24
|
+
reply?: {
|
|
25
|
+
dispatchReplyWithBufferedBlockDispatcher?: (opts: unknown) => Promise<void>;
|
|
26
|
+
};
|
|
27
|
+
session?: {
|
|
28
|
+
recordInboundSession?: (opts: unknown) => Promise<void>;
|
|
29
|
+
resolveStorePath?: (store: unknown, opts: unknown) => string;
|
|
30
|
+
};
|
|
31
|
+
routing?: {
|
|
32
|
+
resolveAgentRoute?: (opts: unknown) => unknown;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
logger?: {
|
|
37
|
+
debug?: (msg: string) => void;
|
|
38
|
+
info?: (msg: string) => void;
|
|
39
|
+
warn?: (msg: string) => void;
|
|
40
|
+
error?: (msg: string) => void;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
20
44
|
// ---------------------------------------------------------------------------
|
|
21
45
|
// Parse — extract usable text body from incoming message
|
|
22
46
|
// ---------------------------------------------------------------------------
|
|
@@ -290,17 +314,25 @@ export interface InboundDispatchOptions {
|
|
|
290
314
|
* Returns `true` if the message was dispatched, `false` if filtered.
|
|
291
315
|
*/
|
|
292
316
|
export async function processInboundMessage(
|
|
293
|
-
api:
|
|
317
|
+
api: ExtendedPluginApi,
|
|
294
318
|
account: CoredAccountConfig,
|
|
295
319
|
event: CoredMessageEvent,
|
|
296
320
|
opts: InboundDispatchOptions,
|
|
297
321
|
): Promise<boolean> {
|
|
298
322
|
const logger = api.logger;
|
|
299
323
|
|
|
324
|
+
// Helper for safe logging
|
|
325
|
+
const log = {
|
|
326
|
+
debug: (msg: string) => { logger?.debug?.(msg); },
|
|
327
|
+
info: (msg: string) => { logger?.info?.(msg); },
|
|
328
|
+
warn: (msg: string) => { logger?.warn?.(msg); },
|
|
329
|
+
error: (msg: string) => { logger?.error?.(msg); },
|
|
330
|
+
};
|
|
331
|
+
|
|
300
332
|
// 1. Parse
|
|
301
333
|
const parsed = parseMessageEvent(event);
|
|
302
334
|
if (!parsed) {
|
|
303
|
-
|
|
335
|
+
log.debug(
|
|
304
336
|
`[cored] ignoring unparseable event (messageId=${event?.message?.messageId ?? "unknown"} messageType=${event?.message?.messageType ?? "undefined"})`,
|
|
305
337
|
);
|
|
306
338
|
return false;
|
|
@@ -309,7 +341,7 @@ export async function processInboundMessage(
|
|
|
309
341
|
// 2. Gate
|
|
310
342
|
const gate = checkMessageGate(parsed, account);
|
|
311
343
|
if (!gate.pass) {
|
|
312
|
-
|
|
344
|
+
log.debug(
|
|
313
345
|
`[cored] gated message=${parsed.messageId} reason=${gate.reason} chat=${parsed.chatId}`,
|
|
314
346
|
);
|
|
315
347
|
return false;
|
|
@@ -317,7 +349,7 @@ export async function processInboundMessage(
|
|
|
317
349
|
|
|
318
350
|
// 3. Dedup
|
|
319
351
|
if (isDuplicate(parsed.messageId)) {
|
|
320
|
-
|
|
352
|
+
log.debug(
|
|
321
353
|
`[cored] duplicate message=${parsed.messageId} chat=${parsed.chatId}`,
|
|
322
354
|
);
|
|
323
355
|
return false;
|
|
@@ -326,14 +358,14 @@ export async function processInboundMessage(
|
|
|
326
358
|
// 4. Build context
|
|
327
359
|
const ctx = buildContext(parsed, account);
|
|
328
360
|
|
|
329
|
-
|
|
361
|
+
log.info(
|
|
330
362
|
`[cored] dispatching message=${parsed.messageId} chat=${parsed.chatId} sender=${parsed.senderId} type=${parsed.chatType}`,
|
|
331
363
|
);
|
|
332
364
|
|
|
333
365
|
// 5. Dispatch
|
|
334
366
|
const runtime = api.runtime;
|
|
335
367
|
if (!runtime?.channel?.reply?.dispatchReplyWithBufferedBlockDispatcher) {
|
|
336
|
-
|
|
368
|
+
log.warn("[cored] runtime.channel.reply not available — cannot dispatch");
|
|
337
369
|
return false;
|
|
338
370
|
}
|
|
339
371
|
|
|
@@ -361,7 +393,7 @@ export async function processInboundMessage(
|
|
|
361
393
|
});
|
|
362
394
|
|
|
363
395
|
// Dispatch reply with buffered block dispatcher
|
|
364
|
-
|
|
396
|
+
log.debug(
|
|
365
397
|
`[cored] dispatch starting for message=${parsed.messageId} session=${ctx.SessionKey}`,
|
|
366
398
|
);
|
|
367
399
|
|
|
@@ -370,25 +402,25 @@ export async function processInboundMessage(
|
|
|
370
402
|
cfg: api.config,
|
|
371
403
|
dispatcherOptions: {
|
|
372
404
|
deliver: async (payload: { text?: string }) => {
|
|
373
|
-
|
|
405
|
+
log.info(
|
|
374
406
|
`[cored] deliver callback called for message=${parsed.messageId} hasText=${!!payload.text} textLen=${payload.text?.length ?? 0}`,
|
|
375
407
|
);
|
|
376
408
|
if (payload.text) {
|
|
377
409
|
await opts.deliver(parsed.chatId, payload.text);
|
|
378
|
-
|
|
410
|
+
log.info(
|
|
379
411
|
`[cored] deliver completed for message=${parsed.messageId} chat=${parsed.chatId}`,
|
|
380
412
|
);
|
|
381
413
|
}
|
|
382
414
|
},
|
|
383
415
|
onError: (err: unknown, info?: { kind?: string }) => {
|
|
384
|
-
|
|
416
|
+
log.error(
|
|
385
417
|
`[cored] ${info?.kind ?? "reply"} error for message=${parsed.messageId}: ${err}`,
|
|
386
418
|
);
|
|
387
419
|
},
|
|
388
420
|
},
|
|
389
421
|
});
|
|
390
422
|
|
|
391
|
-
|
|
423
|
+
log.info(
|
|
392
424
|
`[cored] dispatch finished for message=${parsed.messageId}`,
|
|
393
425
|
);
|
|
394
426
|
|
package/src/setup-entry.ts
CHANGED
|
@@ -2,50 +2,6 @@
|
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
4
|
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core";
|
|
5
|
+
import { coredPlugin } from "./channel.js";
|
|
5
6
|
|
|
6
|
-
export default defineSetupPluginEntry(
|
|
7
|
-
async onSetup(context) {
|
|
8
|
-
const appId = await context.prompt({
|
|
9
|
-
type: "text",
|
|
10
|
-
message: "Enter your Cored App ID:",
|
|
11
|
-
validate: (val: string) => val.length > 0 || "This field is required",
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
const appSecret = await context.prompt({
|
|
15
|
-
type: "password",
|
|
16
|
-
message: "Enter your Cored App Secret:",
|
|
17
|
-
validate: (val: string) => val.length > 0 || "This field is required",
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
const backendUrl = await context.prompt({
|
|
21
|
-
type: "text",
|
|
22
|
-
message: "Enter Cored backend server URL:",
|
|
23
|
-
validate: (val: string) => {
|
|
24
|
-
if (val.length === 0) return "This field is required";
|
|
25
|
-
try {
|
|
26
|
-
new URL(val);
|
|
27
|
-
return true;
|
|
28
|
-
} catch {
|
|
29
|
-
return "Please enter a valid URL";
|
|
30
|
-
}
|
|
31
|
-
},
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const enableEncryption = await context.prompt({
|
|
35
|
-
type: "confirm",
|
|
36
|
-
message: "Enable message encryption?",
|
|
37
|
-
default: true,
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
// Write config to ~/.openclaw/openclaw.json
|
|
41
|
-
await context.updateConfig("channels.cored.appId", appId);
|
|
42
|
-
await context.updateConfig("channels.cored.appSecret", appSecret);
|
|
43
|
-
await context.updateConfig("channels.cored.backendUrl", backendUrl);
|
|
44
|
-
await context.updateConfig("channels.cored.enableEncryption", enableEncryption);
|
|
45
|
-
await context.updateConfig("channels.cored.enabled", true);
|
|
46
|
-
|
|
47
|
-
console.log("✅ Cored channel configuration saved successfully!");
|
|
48
|
-
console.log("📄 Config file: ~/.openclaw/openclaw.json");
|
|
49
|
-
console.log("📖 Deploy docs: https://coredim.com/docs/admin/bots/openclaw");
|
|
50
|
-
},
|
|
51
|
-
});
|
|
7
|
+
export default defineSetupPluginEntry(coredPlugin);
|