Skip to content

Commit 07d9f72

Browse files
committed
refactor: unify plugin sdk primitives
1 parent bea90b7 commit 07d9f72

58 files changed

Lines changed: 994 additions & 575 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/plugins/architecture.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -925,6 +925,12 @@ authoring plugins:
925925

926926
- `openclaw/plugin-sdk/plugin-entry` for plugin registration primitives.
927927
- `openclaw/plugin-sdk/core` for the generic shared plugin-facing contract.
928+
- Stable channel primitives such as `openclaw/plugin-sdk/channel-setup`,
929+
`openclaw/plugin-sdk/channel-pairing`,
930+
`openclaw/plugin-sdk/channel-reply-pipeline`,
931+
`openclaw/plugin-sdk/secret-input`, and
932+
`openclaw/plugin-sdk/webhook-ingress` for shared setup/auth/reply/webhook
933+
wiring.
928934
- Domain subpaths such as `openclaw/plugin-sdk/channel-config-helpers`,
929935
`openclaw/plugin-sdk/channel-config-schema`,
930936
`openclaw/plugin-sdk/channel-policy`,
@@ -961,6 +967,9 @@ authoring plugins:
961967
Compatibility note:
962968

963969
- Avoid the root `openclaw/plugin-sdk` barrel for new code.
970+
- Prefer the narrow stable primitives first. The newer setup/pairing/reply/
971+
secret-input/webhook subpaths are the intended contract for new bundled and
972+
external plugin work.
964973
- Bundled extension-specific helper barrels are not stable by default. If a
965974
helper is only needed by a bundled extension, keep it behind the extension's
966975
local `api.js` or `runtime-api.js` seam instead of promoting it into

docs/plugins/building-extensions.md

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,10 @@ subpaths rather than the monolithic root:
9595
```typescript
9696
// Correct: focused subpaths
9797
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
98-
import { resolveOutboundSendDep } from "openclaw/plugin-sdk/channel-runtime";
98+
import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
99+
import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
99100
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
101+
import { createOptionalChannelSetupSurface } from "openclaw/plugin-sdk/channel-setup";
100102
import { resolveChannelGroupRequireMention } from "openclaw/plugin-sdk/channel-policy";
101103

102104
// Wrong: monolithic root (lint will reject this)
@@ -105,17 +107,24 @@ import { ... } from "openclaw/plugin-sdk";
105107

106108
Common subpaths:
107109

108-
| Subpath | Purpose |
109-
| ---------------------------------- | ------------------------------------ |
110-
| `plugin-sdk/core` | Plugin entry definitions, base types |
111-
| `plugin-sdk/channel-runtime` | Channel runtime helpers |
112-
| `plugin-sdk/channel-config-schema` | Config schema builders |
113-
| `plugin-sdk/channel-policy` | Group/DM policy helpers |
114-
| `plugin-sdk/setup` | Setup wizard adapters |
115-
| `plugin-sdk/runtime-store` | Persistent plugin storage |
116-
| `plugin-sdk/allow-from` | Allowlist resolution |
117-
| `plugin-sdk/reply-payload` | Message reply types |
118-
| `plugin-sdk/testing` | Test utilities |
110+
| Subpath | Purpose |
111+
| ----------------------------------- | ------------------------------------ |
112+
| `plugin-sdk/core` | Plugin entry definitions, base types |
113+
| `plugin-sdk/channel-setup` | Optional setup adapters/wizards |
114+
| `plugin-sdk/channel-pairing` | DM pairing primitives |
115+
| `plugin-sdk/channel-reply-pipeline` | Prefix + typing reply wiring |
116+
| `plugin-sdk/channel-config-schema` | Config schema builders |
117+
| `plugin-sdk/channel-policy` | Group/DM policy helpers |
118+
| `plugin-sdk/secret-input` | Secret input parsing/helpers |
119+
| `plugin-sdk/webhook-ingress` | Webhook request/target helpers |
120+
| `plugin-sdk/runtime-store` | Persistent plugin storage |
121+
| `plugin-sdk/allow-from` | Allowlist resolution |
122+
| `plugin-sdk/reply-payload` | Message reply types |
123+
| `plugin-sdk/provider-onboard` | Provider onboarding config patches |
124+
| `plugin-sdk/testing` | Test utilities |
125+
126+
Use the narrowest primitive that matches the job. Reach for `channel-runtime`
127+
or other larger helper barrels only when a dedicated subpath does not exist yet.
119128

120129
## Step 4: Use local barrels for internal imports
121130

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
1-
import {
2-
hasConfiguredSecretInput,
3-
normalizeResolvedSecretInputString,
4-
normalizeSecretInputString,
5-
} from "openclaw/plugin-sdk/secret-input-runtime";
6-
import { buildSecretInputSchema } from "openclaw/plugin-sdk/secret-input-schema";
71
export {
82
buildSecretInputSchema,
93
hasConfiguredSecretInput,
104
normalizeResolvedSecretInputString,
115
normalizeSecretInputString,
12-
};
6+
} from "openclaw/plugin-sdk/secret-input";

extensions/chutes/onboard.ts

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
} from "openclaw/plugin-sdk/provider-models";
77
import {
88
applyAgentDefaultModelPrimary,
9-
applyProviderConfigWithModelCatalog,
9+
applyProviderConfigWithModelCatalogPreset,
1010
type OpenClawConfig,
1111
} from "openclaw/plugin-sdk/provider-onboard";
1212

@@ -17,24 +17,20 @@ export { CHUTES_DEFAULT_MODEL_REF };
1717
* Registers all catalog models and sets provider aliases (chutes-fast, etc.).
1818
*/
1919
export function applyChutesProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
20-
const models = { ...cfg.agents?.defaults?.models };
21-
for (const m of CHUTES_MODEL_CATALOG) {
22-
models[`chutes/${m.id}`] = {
23-
...models[`chutes/${m.id}`],
24-
};
25-
}
26-
27-
models["chutes-fast"] = { alias: "chutes/zai-org/GLM-4.7-FP8" };
28-
models["chutes-vision"] = { alias: "chutes/chutesai/Mistral-Small-3.2-24B-Instruct-2506" };
29-
models["chutes-pro"] = { alias: "chutes/deepseek-ai/DeepSeek-V3.2-TEE" };
30-
31-
const chutesModels = CHUTES_MODEL_CATALOG.map(buildChutesModelDefinition);
32-
return applyProviderConfigWithModelCatalog(cfg, {
33-
agentModels: models,
20+
return applyProviderConfigWithModelCatalogPreset(cfg, {
3421
providerId: "chutes",
3522
api: "openai-completions",
3623
baseUrl: CHUTES_BASE_URL,
37-
catalogModels: chutesModels,
24+
catalogModels: CHUTES_MODEL_CATALOG.map(buildChutesModelDefinition),
25+
aliases: [
26+
...CHUTES_MODEL_CATALOG.map((model) => `chutes/${model.id}`),
27+
{ modelRef: "chutes-fast", alias: "chutes/zai-org/GLM-4.7-FP8" },
28+
{
29+
modelRef: "chutes-vision",
30+
alias: "chutes/chutesai/Mistral-Small-3.2-24B-Instruct-2506",
31+
},
32+
{ modelRef: "chutes-pro", alias: "chutes/deepseek-ai/DeepSeek-V3.2-TEE" },
33+
],
3834
});
3935
}
4036

extensions/discord/src/monitor/message-handler.process.ts

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
import { ChannelType, type RequestClient } from "@buape/carbon";
22
import { resolveAckReaction, resolveHumanDelayConfig } from "openclaw/plugin-sdk/agent-runtime";
33
import { EmbeddedBlockChunker } from "openclaw/plugin-sdk/agent-runtime";
4+
import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
45
import { shouldAckReaction as shouldAckReactionGate } from "openclaw/plugin-sdk/channel-runtime";
56
import { logTypingFailure, logAckFailure } from "openclaw/plugin-sdk/channel-runtime";
6-
import { createReplyPrefixOptions } from "openclaw/plugin-sdk/channel-runtime";
77
import { recordInboundSession } from "openclaw/plugin-sdk/channel-runtime";
88
import {
99
createStatusReactionController,
1010
DEFAULT_TIMING,
1111
type StatusReactionAdapter,
1212
} from "openclaw/plugin-sdk/channel-runtime";
13-
import { createTypingCallbacks } from "openclaw/plugin-sdk/channel-runtime";
1413
import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/config-runtime";
1514
import { resolveDiscordPreviewStreamMode } from "openclaw/plugin-sdk/config-runtime";
1615
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/config-runtime";
@@ -420,11 +419,24 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
420419
? deliverTarget.slice("channel:".length)
421420
: messageChannelId;
422421

423-
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
422+
const { onModelSelected, ...replyPipeline } = createChannelReplyPipeline({
424423
cfg,
425424
agentId: route.agentId,
426425
channel: "discord",
427426
accountId: route.accountId,
427+
typing: {
428+
start: () => sendTyping({ client, channelId: typingChannelId }),
429+
onStartError: (err) => {
430+
logTypingFailure({
431+
log: logVerbose,
432+
channel: "discord",
433+
target: typingChannelId,
434+
error: err,
435+
});
436+
},
437+
// Long tool-heavy runs are expected on Discord; keep heartbeats alive.
438+
maxDurationMs: DISCORD_TYPING_MAX_DURATION_MS,
439+
},
428440
});
429441
const tableMode = resolveMarkdownTableMode({
430442
cfg,
@@ -438,20 +450,6 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
438450
});
439451
const chunkMode = resolveChunkMode(cfg, "discord", accountId);
440452

441-
const typingCallbacks = createTypingCallbacks({
442-
start: () => sendTyping({ client, channelId: typingChannelId }),
443-
onStartError: (err) => {
444-
logTypingFailure({
445-
log: logVerbose,
446-
channel: "discord",
447-
target: typingChannelId,
448-
error: err,
449-
});
450-
},
451-
// Long tool-heavy runs are expected on Discord; keep heartbeats alive.
452-
maxDurationMs: DISCORD_TYPING_MAX_DURATION_MS,
453-
});
454-
455453
// --- Discord draft stream (edit-based preview streaming) ---
456454
const discordStreamMode = resolveDiscordPreviewStreamMode(discordConfig);
457455
const draftMaxChars = Math.min(textLimit, 2000);
@@ -597,9 +595,8 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
597595

598596
const { dispatcher, replyOptions, markDispatchIdle, markRunComplete } =
599597
createReplyDispatcherWithTyping({
600-
...prefixOptions,
598+
...replyPipeline,
601599
humanDelay: resolveHumanDelayConfig(cfg, route.agentId),
602-
typingCallbacks,
603600
deliver: async (payload: ReplyPayload, info) => {
604601
if (isProcessAborted(abortSignal)) {
605602
return;
@@ -715,7 +712,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
715712
if (isProcessAborted(abortSignal)) {
716713
return;
717714
}
718-
await typingCallbacks.onReplyStart();
715+
await replyPipeline.typingCallbacks?.onReplyStart();
719716
await statusReactions.setThinking();
720717
},
721718
});

extensions/feishu/src/bot.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,9 @@ import {
1010
buildAgentMediaPayload,
1111
buildPendingHistoryContextFromMap,
1212
clearHistoryEntriesIfEnabled,
13-
createScopedPairingAccess,
13+
createChannelPairingController,
1414
DEFAULT_GROUP_HISTORY_LIMIT,
1515
type HistoryEntry,
16-
issuePairingChallenge,
1716
normalizeAgentId,
1817
recordPendingHistoryEntryIfEnabled,
1918
resolveAgentOutboundIdentity,
@@ -445,7 +444,7 @@ export async function handleFeishuMessage(params: {
445444

446445
try {
447446
const core = getFeishuRuntime();
448-
const pairing = createScopedPairingAccess({
447+
const pairing = createChannelPairingController({
449448
core,
450449
channel: "feishu",
451450
accountId: account.accountId,
@@ -471,12 +470,10 @@ export async function handleFeishuMessage(params: {
471470

472471
if (isDirect && dmPolicy !== "open" && !dmAllowed) {
473472
if (dmPolicy === "pairing") {
474-
await issuePairingChallenge({
475-
channel: "feishu",
473+
await pairing.issueChallenge({
476474
senderId: ctx.senderOpenId,
477475
senderIdLine: `Your Feishu user id: ${ctx.senderOpenId}`,
478476
meta: { name: ctx.senderName },
479-
upsertPairingRequest: pairing.upsertPairingRequest,
480477
onCreated: () => {
481478
log(`feishu[${account.accountId}]: pairing request sender=${ctx.senderOpenId}`);
482479
},
Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
1-
import {
2-
buildSecretInputSchema,
3-
hasConfiguredSecretInput,
4-
normalizeResolvedSecretInputString,
5-
normalizeSecretInputString,
6-
} from "../runtime-api.js";
7-
81
export {
92
buildSecretInputSchema,
103
hasConfiguredSecretInput,
114
normalizeResolvedSecretInputString,
125
normalizeSecretInputString,
13-
};
6+
} from "openclaw/plugin-sdk/secret-input";

extensions/googlechat/src/monitor-access.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import {
22
GROUP_POLICY_BLOCKED_LABEL,
3-
createScopedPairingAccess,
3+
createChannelPairingController,
44
evaluateGroupRouteAccessForPolicy,
5-
issuePairingChallenge,
65
isDangerousNameMatchingEnabled,
76
resolveAllowlistProviderRuntimeGroupPolicy,
87
resolveDefaultGroupPolicy,
@@ -166,7 +165,7 @@ export async function applyGoogleChatInboundAccessPolicy(params: {
166165
} = params;
167166
const allowNameMatching = isDangerousNameMatchingEnabled(account.config);
168167
const spaceId = space.name ?? "";
169-
const pairing = createScopedPairingAccess({
168+
const pairing = createChannelPairingController({
170169
core,
171170
channel: "googlechat",
172171
accountId: account.accountId,
@@ -311,12 +310,10 @@ export async function applyGoogleChatInboundAccessPolicy(params: {
311310

312311
if (access.decision !== "allow") {
313312
if (access.decision === "pairing") {
314-
await issuePairingChallenge({
315-
channel: "googlechat",
313+
await pairing.issueChallenge({
316314
senderId,
317315
senderIdLine: `Your Google Chat user id: ${senderId}`,
318316
meta: { name: senderName || undefined, email: senderEmail },
319-
upsertPairingRequest: pairing.upsertPairingRequest,
320317
onCreated: () => {
321318
logVerbose(`googlechat pairing request sender=${senderId}`);
322319
},

extensions/googlechat/src/monitor.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import {
55
} from "openclaw/plugin-sdk/reply-payload";
66
import type { OpenClawConfig } from "../runtime-api.js";
77
import {
8+
createChannelReplyPipeline,
89
createWebhookInFlightLimiter,
9-
createReplyPrefixOptions,
1010
registerWebhookTargetWithPluginRoute,
1111
resolveInboundRouteEnvelopeBuilderWithRuntime,
1212
resolveWebhookPath,
@@ -307,7 +307,7 @@ async function processMessageWithPipeline(params: {
307307
}
308308
}
309309

310-
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
310+
const { onModelSelected, ...replyPipeline } = createChannelReplyPipeline({
311311
cfg: config,
312312
agentId: route.agentId,
313313
channel: "googlechat",
@@ -318,7 +318,7 @@ async function processMessageWithPipeline(params: {
318318
ctx: ctxPayload,
319319
cfg: config,
320320
dispatcherOptions: {
321-
...prefixOptions,
321+
...replyPipeline,
322322
deliver: async (payload) => {
323323
await deliverGoogleChatReply({
324324
payload,

extensions/huggingface/onboard.ts

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,27 @@ import {
44
HUGGINGFACE_MODEL_CATALOG,
55
} from "openclaw/plugin-sdk/provider-models";
66
import {
7-
applyAgentDefaultModelPrimary,
8-
applyProviderConfigWithModelCatalog,
7+
applyProviderConfigWithModelCatalogPreset,
98
type OpenClawConfig,
109
} from "openclaw/plugin-sdk/provider-onboard";
1110

1211
export const HUGGINGFACE_DEFAULT_MODEL_REF = "huggingface/deepseek-ai/DeepSeek-R1";
1312

14-
export function applyHuggingfaceProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
15-
const models = { ...cfg.agents?.defaults?.models };
16-
models[HUGGINGFACE_DEFAULT_MODEL_REF] = {
17-
...models[HUGGINGFACE_DEFAULT_MODEL_REF],
18-
alias: models[HUGGINGFACE_DEFAULT_MODEL_REF]?.alias ?? "Hugging Face",
19-
};
20-
21-
return applyProviderConfigWithModelCatalog(cfg, {
22-
agentModels: models,
13+
function applyHuggingfacePreset(cfg: OpenClawConfig, primaryModelRef?: string): OpenClawConfig {
14+
return applyProviderConfigWithModelCatalogPreset(cfg, {
2315
providerId: "huggingface",
2416
api: "openai-completions",
2517
baseUrl: HUGGINGFACE_BASE_URL,
2618
catalogModels: HUGGINGFACE_MODEL_CATALOG.map(buildHuggingfaceModelDefinition),
19+
aliases: [{ modelRef: HUGGINGFACE_DEFAULT_MODEL_REF, alias: "Hugging Face" }],
20+
primaryModelRef,
2721
});
2822
}
2923

24+
export function applyHuggingfaceProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
25+
return applyHuggingfacePreset(cfg);
26+
}
27+
3028
export function applyHuggingfaceConfig(cfg: OpenClawConfig): OpenClawConfig {
31-
return applyAgentDefaultModelPrimary(
32-
applyHuggingfaceProviderConfig(cfg),
33-
HUGGINGFACE_DEFAULT_MODEL_REF,
34-
);
29+
return applyHuggingfacePreset(cfg, HUGGINGFACE_DEFAULT_MODEL_REF);
3530
}

0 commit comments

Comments
 (0)