Merge branch 'main' into fix/token-usage-input-output-breakdown
This commit is contained in:
commit
aab1ed7b1b
@ -42,10 +42,13 @@ pnpm test:parallels:macos \
|
||||
## Notes
|
||||
|
||||
- Snapshot target: closest to `macOS 26.3.1 fresh`.
|
||||
- Snapshot resolver now prefers matching `*-poweroff*` clones when the base hint also matches. That lets the harness reuse disk-only recovery snapshots without passing a longer hint.
|
||||
- If Windows/Linux snapshot restore logs show `PET_QUESTION_SNAPSHOT_STATE_INCOMPATIBLE_CPU`, drop the suspended state once, create a `*-poweroff*` replacement snapshot, and rerun. The smoke scripts now auto-start restored power-off snapshots.
|
||||
- Harness configures Discord inside the guest; no checked-in token/config.
|
||||
- Use the `openclaw` wrapper for guest `message send/read`; `node openclaw.mjs message ...` does not expose the lazy message subcommands the same way.
|
||||
- Write `channels.discord.guilds` in one JSON object (`--strict-json`), not dotted `config set channels.discord.guilds.<snowflake>...` paths; numeric snowflakes get treated like array indexes.
|
||||
- Avoid `prlctl enter` / expect for long Discord setup scripts; it line-wraps/corrupts long commands. Use `prlctl exec --current-user /bin/sh -lc ...` for the Discord config phase.
|
||||
- Full 3-OS sweeps: the shared build lock is safe in parallel, but snapshot restore is still a Parallels bottleneck. Prefer serialized Windows/Linux restore-heavy reruns if the host is already under load.
|
||||
- Harness cleanup deletes the temporary Discord smoke messages at exit.
|
||||
- Per-phase logs: `/tmp/openclaw-parallels-smoke.*`
|
||||
- Machine summary: pass `--json`
|
||||
|
||||
@ -78,6 +78,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Z.AI/onboarding: add `glm-5-turbo` to the default Z.AI provider catalog so onboarding-generated configs expose the new model alongside the existing GLM defaults. (#46670) Thanks @tomsun28.
|
||||
- Zalo Personal/group gating: stop reapplying `dmPolicy.allowFrom` as a sender gate for already-allowlisted groups when `groupAllowFrom` is unset, so any member of an allowed group can trigger replies while DMs stay restricted. (#46663) Fixes #40146. Thanks @Takhoffman.
|
||||
- Zalo/plugin runtime: export `resolveClientIp` from `openclaw/plugin-sdk/zalo` so installed builds no longer crash on startup when the webhook monitor loads from the packaged extension instead of the monorepo source tree. (#46549) Thanks @No898.
|
||||
- Docker/live tests: mount external CLI auth homes into writable container copies, derive Codex OAuth expiry from JWT `exp`, refresh synced CLI creds instead of trusting stale cached expiry, and make gateway live probes wait on transcript output so `pnpm test:docker:all` stays green in Linux.
|
||||
- Plugins/install precedence: keep bundled plugins ahead of auto-discovered globals by default, but let an explicitly installed plugin record win its own duplicate-id tie so installed channel plugins load from `~/.openclaw/extensions` after `openclaw plugins install`. (#46722) Thanks @Takhoffman.
|
||||
- Control UI/logging: make browser-safe logger imports avoid eager temp-dir resolution so the bundled Control UI no longer crashes to a blank screen when logging reaches `tmp-openclaw-dir`. (#48469) Fixes #48062. Thanks @7inspire.
|
||||
- Plugins/scoped ids: preserve scoped plugin ids during install and config keying, and keep bundled plugins ahead of discovered duplicate ids by default so `@scope/name` plugins no longer collide with unscoped installs. (#47413) Thanks @vincentkoc.
|
||||
|
||||
@ -362,7 +362,7 @@ If you want to rely on env keys (e.g. exported in your `~/.profile`), run local
|
||||
|
||||
## Docker runners (optional “works in Linux” checks)
|
||||
|
||||
These run `pnpm test:live` inside the repo Docker image, mounting your local config dir and workspace (and sourcing `~/.profile` if mounted). They also bind-mount CLI auth homes like `~/.codex`, `~/.claude`, `~/.qwen`, and `~/.minimax` when present so external-CLI OAuth stays available in-container:
|
||||
These run `pnpm test:live` inside the repo Docker image, mounting your local config dir and workspace (and sourcing `~/.profile` if mounted). They also bind-mount CLI auth homes like `~/.codex`, `~/.claude`, `~/.qwen`, and `~/.minimax` when present, then copy them into the container home before the run so external-CLI OAuth can refresh tokens without mutating the host auth store:
|
||||
|
||||
- Direct models: `pnpm test:docker:live-models` (script: `scripts/test-live-models-docker.sh`)
|
||||
- Gateway + dev agent: `pnpm test:docker:live-gateway` (script: `scripts/test-live-gateway-models-docker.sh`)
|
||||
@ -373,6 +373,9 @@ These run `pnpm test:live` inside the repo Docker image, mounting your local con
|
||||
The live-model Docker runners also bind-mount the current checkout read-only and
|
||||
stage it into a temporary workdir inside the container. This keeps the runtime
|
||||
image slim while still running Vitest against your exact local source/config.
|
||||
`test:docker:live-models` still runs `pnpm test:live`, so pass through
|
||||
`OPENCLAW_LIVE_GATEWAY_*` as well when you need to narrow or exclude gateway
|
||||
live coverage from that Docker lane.
|
||||
|
||||
Manual ACP plain-language thread smoke (not CI):
|
||||
|
||||
@ -384,8 +387,9 @@ Useful env vars:
|
||||
- `OPENCLAW_CONFIG_DIR=...` (default: `~/.openclaw`) mounted to `/home/node/.openclaw`
|
||||
- `OPENCLAW_WORKSPACE_DIR=...` (default: `~/.openclaw/workspace`) mounted to `/home/node/.openclaw/workspace`
|
||||
- `OPENCLAW_PROFILE_FILE=...` (default: `~/.profile`) mounted to `/home/node/.profile` and sourced before running tests
|
||||
- External CLI auth dirs under `$HOME` (`.codex`, `.claude`, `.qwen`, `.minimax`) are mounted read-only to the matching `/home/node/...` paths when present
|
||||
- External CLI auth dirs under `$HOME` (`.codex`, `.claude`, `.qwen`, `.minimax`) are mounted read-only under `/host-auth/...`, then copied into `/home/node/...` before tests start
|
||||
- `OPENCLAW_LIVE_GATEWAY_MODELS=...` / `OPENCLAW_LIVE_MODELS=...` to narrow the run
|
||||
- `OPENCLAW_LIVE_GATEWAY_PROVIDERS=...` / `OPENCLAW_LIVE_PROVIDERS=...` to filter providers in-container
|
||||
- `OPENCLAW_LIVE_REQUIRE_PROFILE_KEYS=1` to ensure creds come from the profile store (not env)
|
||||
|
||||
## Docs sanity
|
||||
|
||||
@ -10,6 +10,10 @@ title: "Media Understanding"
|
||||
|
||||
OpenClaw can **summarize inbound media** (image/audio/video) before the reply pipeline runs. It auto‑detects when local tools or provider keys are available, and can be disabled or customized. If understanding is off, models still receive the original files/URLs as usual.
|
||||
|
||||
Vendor-specific media behavior is registered by vendor plugins, while OpenClaw
|
||||
core owns the shared `tools.media` config, fallback order, and reply-pipeline
|
||||
integration.
|
||||
|
||||
## Goals
|
||||
|
||||
- Optional: pre‑digest inbound media into short text for faster routing + better command parsing.
|
||||
@ -184,7 +188,10 @@ If you set `capabilities`, the entry only runs for those media types. For shared
|
||||
lists, OpenClaw can infer defaults:
|
||||
|
||||
- `openai`, `anthropic`, `minimax`: **image**
|
||||
- `moonshot`: **image + video**
|
||||
- `google` (Gemini API): **image + audio + video**
|
||||
- `mistral`: **audio**
|
||||
- `zai`: **image**
|
||||
- `groq`: **audio**
|
||||
- `deepgram`: **audio**
|
||||
|
||||
@ -193,11 +200,11 @@ If you omit `capabilities`, the entry is eligible for the list it appears in.
|
||||
|
||||
## Provider support matrix (OpenClaw integrations)
|
||||
|
||||
| Capability | Provider integration | Notes |
|
||||
| ---------- | ------------------------------------------------ | --------------------------------------------------------- |
|
||||
| Image | OpenAI / Anthropic / Google / others via `pi-ai` | Any image-capable model in the registry works. |
|
||||
| Audio | OpenAI, Groq, Deepgram, Google, Mistral | Provider transcription (Whisper/Deepgram/Gemini/Voxtral). |
|
||||
| Video | Google (Gemini API) | Provider video understanding. |
|
||||
| Capability | Provider integration | Notes |
|
||||
| ---------- | -------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| Image | OpenAI, Anthropic, Google, MiniMax, Moonshot, Z.AI | Vendor plugins register image support against core media understanding. |
|
||||
| Audio | OpenAI, Groq, Deepgram, Google, Mistral | Provider transcription (Whisper/Deepgram/Gemini/Voxtral). |
|
||||
| Video | Google, Moonshot | Provider video understanding via vendor plugins. |
|
||||
|
||||
## Model selection guidance
|
||||
|
||||
|
||||
@ -113,9 +113,11 @@ That means:
|
||||
Examples:
|
||||
|
||||
- the bundled `openai` plugin owns OpenAI model-provider behavior and OpenAI
|
||||
speech behavior
|
||||
speech + media-understanding behavior
|
||||
- the bundled `elevenlabs` plugin owns ElevenLabs speech behavior
|
||||
- the bundled `microsoft` plugin owns Microsoft speech behavior
|
||||
- the bundled `google`, `minimax`, `mistral`, `moonshot`, and `zai` plugins own
|
||||
their media-understanding backends
|
||||
- the `voice-call` plugin is a feature plugin: it owns call transport, tools,
|
||||
CLI, routes, and runtime, but it consumes core TTS/STT capability instead of
|
||||
inventing a second speech stack
|
||||
@ -167,6 +169,24 @@ For example, TTS follows this shape:
|
||||
|
||||
That same pattern should be preferred for future capabilities.
|
||||
|
||||
### Capability example: video understanding
|
||||
|
||||
OpenClaw already treats image/audio/video understanding as one shared
|
||||
capability. The same ownership model applies there:
|
||||
|
||||
1. core defines the media-understanding contract
|
||||
2. vendor plugins register `describeImage`, `transcribeAudio`, and
|
||||
`describeVideo` as applicable
|
||||
3. channels and feature plugins consume the shared core behavior instead of
|
||||
wiring directly to vendor code
|
||||
|
||||
That avoids baking one provider's video assumptions into core. The plugin owns
|
||||
the vendor surface; core owns the capability contract and fallback behavior.
|
||||
|
||||
If OpenClaw adds a new domain later, such as video generation, use the same
|
||||
sequence again: define the core capability first, then let vendor plugins
|
||||
register implementations against it.
|
||||
|
||||
## Compatible bundles
|
||||
|
||||
OpenClaw also recognizes two compatible external bundle layouts:
|
||||
@ -331,7 +351,8 @@ There are two layers of enforcement:
|
||||
2. **contract tests**
|
||||
Bundled plugins are captured in contract registries during test runs so
|
||||
OpenClaw can assert ownership explicitly. Today this is used for model
|
||||
providers, web search providers, and bundled registration ownership.
|
||||
providers, speech providers, web search providers, and bundled registration
|
||||
ownership.
|
||||
|
||||
The practical effect is that OpenClaw knows, up front, which plugin owns which
|
||||
surface. That lets core and channels compose seamlessly because ownership is
|
||||
@ -649,19 +670,32 @@ to think of as short-lived performance caches, not persistence.
|
||||
|
||||
## Runtime helpers
|
||||
|
||||
Plugins can access selected core helpers via `api.runtime`. For telephony TTS:
|
||||
Plugins can access selected core helpers via `api.runtime`. For TTS:
|
||||
|
||||
```ts
|
||||
const clip = await api.runtime.tts.textToSpeech({
|
||||
text: "Hello from OpenClaw",
|
||||
cfg: api.config,
|
||||
});
|
||||
|
||||
const result = await api.runtime.tts.textToSpeechTelephony({
|
||||
text: "Hello from OpenClaw",
|
||||
cfg: api.config,
|
||||
});
|
||||
|
||||
const voices = await api.runtime.tts.listVoices({
|
||||
provider: "elevenlabs",
|
||||
cfg: api.config,
|
||||
});
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `textToSpeech` returns the normal core TTS output payload for file/voice-note surfaces.
|
||||
- Uses core `messages.tts` configuration and provider selection.
|
||||
- Returns PCM audio buffer + sample rate. Plugins must resample/encode for providers.
|
||||
- `listVoices` is optional per provider. Use it for vendor-owned voice pickers or setup flows.
|
||||
- Voice listings can include richer metadata such as locale, gender, and personality tags for provider-aware pickers.
|
||||
- OpenAI and ElevenLabs support telephony today. Microsoft does not.
|
||||
|
||||
Plugins can also register speech providers via `api.registerSpeechProvider(...)`.
|
||||
@ -691,10 +725,48 @@ Notes:
|
||||
text, speech, image, and future media providers as OpenClaw adds those
|
||||
capability contracts.
|
||||
|
||||
For STT/transcription, plugins can call:
|
||||
For image/audio/video understanding, plugins register one typed
|
||||
media-understanding provider instead of a generic key/value bag:
|
||||
|
||||
```ts
|
||||
const { text } = await api.runtime.stt.transcribeAudioFile({
|
||||
api.registerMediaUnderstandingProvider({
|
||||
id: "google",
|
||||
capabilities: ["image", "audio", "video"],
|
||||
describeImage: async (req) => ({ text: "..." }),
|
||||
transcribeAudio: async (req) => ({ text: "..." }),
|
||||
describeVideo: async (req) => ({ text: "..." }),
|
||||
});
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Keep orchestration, fallback, config, and channel wiring in core.
|
||||
- Keep vendor behavior in the provider plugin.
|
||||
- Additive expansion should stay typed: new optional methods, new optional
|
||||
result fields, new optional capabilities.
|
||||
- If OpenClaw adds a new capability such as video generation later, define the
|
||||
core capability contract first, then let vendor plugins register against it.
|
||||
|
||||
For media-understanding runtime helpers, plugins can call:
|
||||
|
||||
```ts
|
||||
const image = await api.runtime.mediaUnderstanding.describeImageFile({
|
||||
filePath: "/tmp/inbound-photo.jpg",
|
||||
cfg: api.config,
|
||||
agentDir: "/tmp/agent",
|
||||
});
|
||||
|
||||
const video = await api.runtime.mediaUnderstanding.describeVideoFile({
|
||||
filePath: "/tmp/inbound-video.mp4",
|
||||
cfg: api.config,
|
||||
});
|
||||
```
|
||||
|
||||
For audio transcription, plugins can use either the media-understanding runtime
|
||||
or the older STT alias:
|
||||
|
||||
```ts
|
||||
const { text } = await api.runtime.mediaUnderstanding.transcribeAudioFile({
|
||||
filePath: "/tmp/inbound-audio.ogg",
|
||||
cfg: api.config,
|
||||
// Optional when MIME cannot be inferred reliably:
|
||||
@ -704,8 +776,11 @@ const { text } = await api.runtime.stt.transcribeAudioFile({
|
||||
|
||||
Notes:
|
||||
|
||||
- `api.runtime.mediaUnderstanding.*` is the preferred shared surface for
|
||||
image/audio/video understanding.
|
||||
- Uses core media-understanding audio configuration (`tools.media.audio`) and provider fallback order.
|
||||
- Returns `{ text: undefined }` when no transcription output is produced (for example skipped/unsupported input).
|
||||
- `api.runtime.stt.transcribeAudioFile(...)` remains as a compatibility alias.
|
||||
|
||||
## Gateway HTTP routes
|
||||
|
||||
@ -1268,6 +1343,7 @@ Plugins export either:
|
||||
- `registerChannel`
|
||||
- `registerProvider`
|
||||
- `registerSpeechProvider`
|
||||
- `registerMediaUnderstandingProvider`
|
||||
- `registerWebSearchProvider`
|
||||
- `registerHttpRoute`
|
||||
- `registerCommand`
|
||||
|
||||
@ -26,6 +26,7 @@ import { fetchClaudeUsage } from "../../src/infra/provider-usage.fetch.js";
|
||||
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
|
||||
import type { ProviderAuthResult } from "../../src/plugins/types.js";
|
||||
import { normalizeSecretInput } from "../../src/utils/normalize-secret-input.js";
|
||||
import { anthropicMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
||||
|
||||
const PROVIDER_ID = "anthropic";
|
||||
const DEFAULT_ANTHROPIC_MODEL = "anthropic/claude-sonnet-4-6";
|
||||
@ -394,6 +395,7 @@ const anthropicPlugin = {
|
||||
profileId: ctx.profileId,
|
||||
}),
|
||||
});
|
||||
api.registerMediaUnderstandingProvider(anthropicMediaUnderstandingProvider);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
8
extensions/anthropic/media-understanding-provider.ts
Normal file
8
extensions/anthropic/media-understanding-provider.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { describeImageWithModel } from "../../src/media-understanding/providers/image.js";
|
||||
import type { MediaUnderstandingProvider } from "../../src/media-understanding/types.js";
|
||||
|
||||
export const anthropicMediaUnderstandingProvider: MediaUnderstandingProvider = {
|
||||
id: "anthropic",
|
||||
capabilities: ["image"],
|
||||
describeImage: describeImageWithModel,
|
||||
};
|
||||
@ -1,13 +1,12 @@
|
||||
import {
|
||||
applyAccountNameToChannelSection,
|
||||
migrateBaseNameToDefaultAccount,
|
||||
patchScopedAccountConfig,
|
||||
prepareScopedSetupConfig,
|
||||
} from "../../../src/channels/plugins/setup-helpers.js";
|
||||
import { setTopLevelChannelDmPolicyWithAllowFrom } from "../../../src/channels/plugins/setup-wizard-helpers.js";
|
||||
import type { ChannelSetupAdapter } from "../../../src/channels/plugins/types.adapters.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import type { DmPolicy } from "../../../src/config/types.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../src/routing/session-key.js";
|
||||
import { normalizeAccountId } from "../../../src/routing/session-key.js";
|
||||
import { applyBlueBubblesConnectionConfig } from "./config-apply.js";
|
||||
|
||||
const channel = "bluebubbles" as const;
|
||||
@ -38,7 +37,7 @@ export function setBlueBubblesAllowFrom(
|
||||
export const blueBubblesSetupAdapter: ChannelSetupAdapter = {
|
||||
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
||||
applyAccountName: ({ cfg, accountId, name }) =>
|
||||
applyAccountNameToChannelSection({
|
||||
prepareScopedSetupConfig({
|
||||
cfg,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
@ -57,19 +56,13 @@ export const blueBubblesSetupAdapter: ChannelSetupAdapter = {
|
||||
return null;
|
||||
},
|
||||
applyAccountConfig: ({ cfg, accountId, input }) => {
|
||||
const namedConfig = applyAccountNameToChannelSection({
|
||||
const next = prepareScopedSetupConfig({
|
||||
cfg,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
name: input.name,
|
||||
migrateBaseName: true,
|
||||
});
|
||||
const next =
|
||||
accountId !== DEFAULT_ACCOUNT_ID
|
||||
? migrateBaseNameToDefaultAccount({
|
||||
cfg: namedConfig,
|
||||
channelKey: channel,
|
||||
})
|
||||
: namedConfig;
|
||||
return applyBlueBubblesConnectionConfig({
|
||||
cfg: next,
|
||||
accountId,
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
formatDocsLink,
|
||||
mergeAllowFromEntries,
|
||||
resolveSetupAccountId,
|
||||
} from "../../../src/channels/plugins/setup-wizard-helpers.js";
|
||||
import type { ChannelSetupDmPolicy } from "../../../src/channels/plugins/setup-wizard-types.js";
|
||||
import type { ChannelSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import type { DmPolicy } from "../../../src/config/types.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js";
|
||||
import { formatDocsLink } from "../../../src/terminal/links.js";
|
||||
import type { WizardPrompter } from "../../../src/wizard/prompts.js";
|
||||
type ChannelSetupDmPolicy,
|
||||
type ChannelSetupWizard,
|
||||
type DmPolicy,
|
||||
type OpenClawConfig,
|
||||
type WizardPrompter,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import {
|
||||
listBlueBubblesAccountIds,
|
||||
resolveBlueBubblesAccount,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
||||
import { ensureModelAllowlistEntry } from "../../src/commands/model-allowlist.js";
|
||||
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
|
||||
import { buildPairedProviderApiKeyCatalog } from "../../src/plugins/provider-catalog.js";
|
||||
import { buildBytePlusCodingProvider, buildBytePlusProvider } from "./provider-catalog.js";
|
||||
|
||||
const PROVIDER_ID = "byteplus";
|
||||
@ -45,18 +46,15 @@ const byteplusPlugin = {
|
||||
],
|
||||
catalog: {
|
||||
order: "paired",
|
||||
run: async (ctx) => {
|
||||
const apiKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey;
|
||||
if (!apiKey) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
providers: {
|
||||
byteplus: { ...buildBytePlusProvider(), apiKey },
|
||||
"byteplus-plan": { ...buildBytePlusCodingProvider(), apiKey },
|
||||
},
|
||||
};
|
||||
},
|
||||
run: (ctx) =>
|
||||
buildPairedProviderApiKeyCatalog({
|
||||
ctx,
|
||||
providerId: PROVIDER_ID,
|
||||
buildProviders: () => ({
|
||||
byteplus: buildBytePlusProvider(),
|
||||
"byteplus-plan": buildBytePlusCodingProvider(),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@ -11,15 +11,16 @@ import {
|
||||
validateApiKeyInput,
|
||||
} from "../../src/commands/auth-choice.api-key.js";
|
||||
import { ensureApiKeyFromOptionEnvOrPrompt } from "../../src/commands/auth-choice.apply-helpers.js";
|
||||
import { buildApiKeyCredential } from "../../src/commands/onboard-auth.credentials.js";
|
||||
import {
|
||||
applyCloudflareAiGatewayConfig,
|
||||
applyAuthProfileConfig,
|
||||
CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF,
|
||||
} from "../../src/commands/onboard-auth.js";
|
||||
import { buildApiKeyCredential } from "../../src/commands/auth-credentials.js";
|
||||
import { applyAuthProfileConfig } from "../../src/commands/onboard-auth.js";
|
||||
import type { SecretInput } from "../../src/config/types.secrets.js";
|
||||
import { coerceSecretRef } from "../../src/config/types.secrets.js";
|
||||
import { normalizeOptionalSecretInput } from "../../src/utils/normalize-secret-input.js";
|
||||
import {
|
||||
applyCloudflareAiGatewayConfig,
|
||||
buildCloudflareAiGatewayConfigPatch,
|
||||
CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF,
|
||||
} from "./onboard.js";
|
||||
|
||||
const PROVIDER_ID = "cloudflare-ai-gateway";
|
||||
const PROVIDER_ENV_VAR = "CLOUDFLARE_AI_GATEWAY_API_KEY";
|
||||
@ -53,30 +54,6 @@ function resolveMetadataFromCredential(
|
||||
};
|
||||
}
|
||||
|
||||
function buildCloudflareConfigPatch(params: { accountId: string; gatewayId: string }) {
|
||||
const baseUrl = resolveCloudflareAiGatewayBaseUrl(params);
|
||||
return {
|
||||
models: {
|
||||
providers: {
|
||||
[PROVIDER_ID]: {
|
||||
baseUrl,
|
||||
api: "anthropic-messages" as const,
|
||||
models: [buildCloudflareAiGatewayModelDefinition()],
|
||||
},
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
[CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF]: {
|
||||
alias: "Cloudflare AI Gateway",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveCloudflareGatewayMetadataInteractive(ctx: {
|
||||
accountId?: string;
|
||||
gatewayId?: string;
|
||||
@ -180,7 +157,7 @@ const cloudflareAiGatewayPlugin = {
|
||||
),
|
||||
},
|
||||
],
|
||||
configPatch: buildCloudflareConfigPatch(metadata),
|
||||
configPatch: buildCloudflareAiGatewayConfigPatch(metadata),
|
||||
defaultModel: CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF,
|
||||
};
|
||||
},
|
||||
|
||||
93
extensions/cloudflare-ai-gateway/onboard.ts
Normal file
93
extensions/cloudflare-ai-gateway/onboard.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import {
|
||||
buildCloudflareAiGatewayModelDefinition,
|
||||
CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF,
|
||||
resolveCloudflareAiGatewayBaseUrl,
|
||||
} from "../../src/agents/cloudflare-ai-gateway.js";
|
||||
import {
|
||||
applyAgentDefaultModelPrimary,
|
||||
applyProviderConfigWithDefaultModel,
|
||||
} from "../../src/commands/onboard-auth.config-shared.js";
|
||||
import type { OpenClawConfig } from "../../src/config/config.js";
|
||||
|
||||
export { CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF };
|
||||
|
||||
export function buildCloudflareAiGatewayConfigPatch(params: {
|
||||
accountId: string;
|
||||
gatewayId: string;
|
||||
}) {
|
||||
const baseUrl = resolveCloudflareAiGatewayBaseUrl(params);
|
||||
return {
|
||||
models: {
|
||||
providers: {
|
||||
"cloudflare-ai-gateway": {
|
||||
baseUrl,
|
||||
api: "anthropic-messages" as const,
|
||||
models: [buildCloudflareAiGatewayModelDefinition()],
|
||||
},
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
[CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF]: {
|
||||
alias: "Cloudflare AI Gateway",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function applyCloudflareAiGatewayProviderConfig(
|
||||
cfg: OpenClawConfig,
|
||||
params?: { accountId?: string; gatewayId?: string },
|
||||
): OpenClawConfig {
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
models[CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF] = {
|
||||
...models[CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF],
|
||||
alias: models[CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF]?.alias ?? "Cloudflare AI Gateway",
|
||||
};
|
||||
|
||||
const existingProvider = cfg.models?.providers?.["cloudflare-ai-gateway"] as
|
||||
| { baseUrl?: unknown }
|
||||
| undefined;
|
||||
const baseUrl =
|
||||
params?.accountId && params?.gatewayId
|
||||
? resolveCloudflareAiGatewayBaseUrl({
|
||||
accountId: params.accountId,
|
||||
gatewayId: params.gatewayId,
|
||||
})
|
||||
: typeof existingProvider?.baseUrl === "string"
|
||||
? existingProvider.baseUrl
|
||||
: undefined;
|
||||
if (!baseUrl) {
|
||||
return {
|
||||
...cfg,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
models,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return applyProviderConfigWithDefaultModel(cfg, {
|
||||
agentModels: models,
|
||||
providerId: "cloudflare-ai-gateway",
|
||||
api: "anthropic-messages",
|
||||
baseUrl,
|
||||
defaultModel: buildCloudflareAiGatewayModelDefinition(),
|
||||
});
|
||||
}
|
||||
|
||||
export function applyCloudflareAiGatewayConfig(
|
||||
cfg: OpenClawConfig,
|
||||
params?: { accountId?: string; gatewayId?: string },
|
||||
): OpenClawConfig {
|
||||
return applyAgentDefaultModelPrimary(
|
||||
applyCloudflareAiGatewayProviderConfig(cfg, params),
|
||||
CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF,
|
||||
);
|
||||
}
|
||||
@ -1,13 +1,13 @@
|
||||
import {
|
||||
hasConfiguredSecretInput,
|
||||
normalizeSecretInputString,
|
||||
} from "../../../src/config/types.secrets.js";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
type OpenClawConfig,
|
||||
type DiscordAccountConfig,
|
||||
} from "openclaw/plugin-sdk/discord";
|
||||
import {
|
||||
hasConfiguredSecretInput,
|
||||
normalizeSecretInputString,
|
||||
} from "../../../src/config/types.secrets.js";
|
||||
} from "../../../src/plugin-sdk-internal/discord.js";
|
||||
import {
|
||||
mergeDiscordAccountConfig,
|
||||
resolveDefaultDiscordAccountId,
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import type {
|
||||
OpenClawConfig,
|
||||
DiscordAccountConfig,
|
||||
DiscordActionConfig,
|
||||
} from "openclaw/plugin-sdk/discord";
|
||||
import {
|
||||
createAccountActionGate,
|
||||
createAccountListHelpers,
|
||||
normalizeAccountId,
|
||||
resolveAccountEntry,
|
||||
} from "../../../src/plugin-sdk-internal/accounts.js";
|
||||
import type {
|
||||
OpenClawConfig,
|
||||
DiscordAccountConfig,
|
||||
DiscordActionConfig,
|
||||
} from "../../../src/plugin-sdk-internal/discord.js";
|
||||
import { resolveDiscordToken } from "./token.js";
|
||||
|
||||
export type ResolvedDiscordAccount = {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import { readBooleanParam } from "openclaw/plugin-sdk/boolean-param";
|
||||
import {
|
||||
readNumberParam,
|
||||
readStringArrayParam,
|
||||
@ -9,7 +10,6 @@ import { handleDiscordAction } from "../../../../src/agents/tools/discord-action
|
||||
import { resolveReactionMessageId } from "../../../../src/channels/plugins/actions/reaction-message-id.js";
|
||||
import type { ChannelMessageActionContext } from "../../../../src/channels/plugins/types.js";
|
||||
import { normalizeInteractiveReply } from "../../../../src/interactive/payload.js";
|
||||
import { readBooleanParam } from "../../../../src/plugin-sdk/boolean-param.js";
|
||||
import { buildDiscordInteractiveComponents } from "../shared-interactive.js";
|
||||
import { resolveDiscordChannelId } from "../targets.js";
|
||||
import { tryHandleDiscordMessageActionGuildAdmin } from "./handle-action.guild-admin.js";
|
||||
|
||||
@ -1,77 +1,8 @@
|
||||
import { createScopedChannelConfigBase } from "openclaw/plugin-sdk/compat";
|
||||
import {
|
||||
createScopedAccountConfigAccessors,
|
||||
formatAllowFromLowercase,
|
||||
} from "openclaw/plugin-sdk/compat";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
DiscordConfigSchema,
|
||||
getChatChannelMeta,
|
||||
type ChannelPlugin,
|
||||
} from "openclaw/plugin-sdk/discord";
|
||||
import { inspectDiscordAccount } from "./account-inspect.js";
|
||||
import {
|
||||
listDiscordAccountIds,
|
||||
resolveDefaultDiscordAccountId,
|
||||
resolveDiscordAccount,
|
||||
type ResolvedDiscordAccount,
|
||||
} from "./accounts.js";
|
||||
import { createDiscordSetupWizardProxy, discordSetupAdapter } from "./setup-core.js";
|
||||
import type { ChannelPlugin } from "openclaw/plugin-sdk/discord";
|
||||
import type { ResolvedDiscordAccount } from "./accounts.js";
|
||||
import { discordSetupAdapter } from "./setup-core.js";
|
||||
import { createDiscordPluginBase } from "./shared.js";
|
||||
|
||||
async function loadDiscordChannelRuntime() {
|
||||
return await import("./channel.runtime.js");
|
||||
}
|
||||
|
||||
const discordConfigAccessors = createScopedAccountConfigAccessors({
|
||||
resolveAccount: ({ cfg, accountId }) => resolveDiscordAccount({ cfg, accountId }),
|
||||
resolveAllowFrom: (account: ResolvedDiscordAccount) => account.config.dm?.allowFrom,
|
||||
formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }),
|
||||
resolveDefaultTo: (account: ResolvedDiscordAccount) => account.config.defaultTo,
|
||||
});
|
||||
|
||||
const discordConfigBase = createScopedChannelConfigBase({
|
||||
sectionKey: "discord",
|
||||
listAccountIds: listDiscordAccountIds,
|
||||
resolveAccount: (cfg, accountId) => resolveDiscordAccount({ cfg, accountId }),
|
||||
inspectAccount: (cfg, accountId) => inspectDiscordAccount({ cfg, accountId }),
|
||||
defaultAccountId: resolveDefaultDiscordAccountId,
|
||||
clearBaseFields: ["token", "name"],
|
||||
});
|
||||
|
||||
const discordSetupWizard = createDiscordSetupWizardProxy(async () => ({
|
||||
discordSetupWizard: (await loadDiscordChannelRuntime()).discordSetupWizard,
|
||||
}));
|
||||
|
||||
export const discordSetupPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
|
||||
id: "discord",
|
||||
meta: {
|
||||
...getChatChannelMeta("discord"),
|
||||
},
|
||||
setupWizard: discordSetupWizard,
|
||||
capabilities: {
|
||||
chatTypes: ["direct", "channel", "thread"],
|
||||
polls: true,
|
||||
reactions: true,
|
||||
threads: true,
|
||||
media: true,
|
||||
nativeCommands: true,
|
||||
},
|
||||
streaming: {
|
||||
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
|
||||
},
|
||||
reload: { configPrefixes: ["channels.discord"] },
|
||||
configSchema: buildChannelConfigSchema(DiscordConfigSchema),
|
||||
config: {
|
||||
...discordConfigBase,
|
||||
isConfigured: (account) => Boolean(account.token?.trim()),
|
||||
describeAccount: (account) => ({
|
||||
accountId: account.accountId,
|
||||
name: account.name,
|
||||
enabled: account.enabled,
|
||||
configured: Boolean(account.token?.trim()),
|
||||
tokenSource: account.tokenSource,
|
||||
}),
|
||||
...discordConfigAccessors,
|
||||
},
|
||||
export const discordSetupPlugin: ChannelPlugin<ResolvedDiscordAccount> = createDiscordPluginBase({
|
||||
setup: discordSetupAdapter,
|
||||
};
|
||||
});
|
||||
|
||||
@ -1,12 +1,9 @@
|
||||
import { Separator, TextDisplay } from "@buape/carbon";
|
||||
import { createScopedChannelConfigBase } from "openclaw/plugin-sdk/compat";
|
||||
import {
|
||||
buildAccountScopedAllowlistConfigEditor,
|
||||
buildAccountScopedDmSecurityPolicy,
|
||||
collectOpenProviderGroupPolicyWarnings,
|
||||
collectOpenGroupPolicyConfiguredRouteWarnings,
|
||||
createScopedAccountConfigAccessors,
|
||||
formatAllowFromLowercase,
|
||||
} from "openclaw/plugin-sdk/compat";
|
||||
import {
|
||||
buildAgentSessionKey,
|
||||
@ -15,11 +12,8 @@ import {
|
||||
} from "openclaw/plugin-sdk/core";
|
||||
import {
|
||||
buildComputedAccountStatusSnapshot,
|
||||
buildChannelConfigSchema,
|
||||
buildTokenChannelStatusSummary,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
DiscordConfigSchema,
|
||||
getChatChannelMeta,
|
||||
listDiscordDirectoryGroupsFromConfig,
|
||||
listDiscordDirectoryPeersFromConfig,
|
||||
PAIRING_APPROVED_MESSAGE,
|
||||
@ -32,12 +26,11 @@ import {
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/discord";
|
||||
import { resolveOutboundSendDep } from "../../../src/infra/outbound/send-deps.js";
|
||||
import { normalizeOutboundThreadId } from "../../../src/infra/outbound/thread-id.js";
|
||||
import { normalizeMessageChannel } from "../../../src/utils/message-channel.js";
|
||||
import { inspectDiscordAccount } from "./account-inspect.js";
|
||||
import {
|
||||
listDiscordAccountIds,
|
||||
resolveDiscordAccount,
|
||||
resolveDefaultDiscordAccountId,
|
||||
type ResolvedDiscordAccount,
|
||||
} from "./accounts.js";
|
||||
import { collectDiscordAuditChannelIds } from "./audit.js";
|
||||
@ -54,7 +47,8 @@ import type { DiscordProbe } from "./probe.js";
|
||||
import { resolveDiscordUserAllowlist } from "./resolve-users.js";
|
||||
import { getDiscordRuntime } from "./runtime.js";
|
||||
import { fetchChannelPermissionsDiscord } from "./send.js";
|
||||
import { createDiscordSetupWizardProxy, discordSetupAdapter } from "./setup-core.js";
|
||||
import { discordSetupAdapter } from "./setup-core.js";
|
||||
import { createDiscordPluginBase, discordConfigAccessors } from "./shared.js";
|
||||
import { collectDiscordStatusIssues } from "./status-issues.js";
|
||||
import { parseDiscordTarget } from "./targets.js";
|
||||
import { DiscordUiContainer } from "./ui.js";
|
||||
@ -63,13 +57,8 @@ type DiscordSendFn = ReturnType<
|
||||
typeof getDiscordRuntime
|
||||
>["channel"]["discord"]["sendMessageDiscord"];
|
||||
|
||||
const meta = getChatChannelMeta("discord");
|
||||
const REQUIRED_DISCORD_PERMISSIONS = ["ViewChannel", "SendMessages"] as const;
|
||||
|
||||
async function loadDiscordChannelRuntime() {
|
||||
return await import("./channel.runtime.js");
|
||||
}
|
||||
|
||||
function formatDiscordIntents(intents?: {
|
||||
messageContent?: string;
|
||||
guildMembers?: string;
|
||||
@ -208,20 +197,6 @@ function parseDiscordExplicitTarget(raw: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeOutboundThreadId(value?: string | number | null): string | undefined {
|
||||
if (value == null) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof value === "number") {
|
||||
if (!Number.isFinite(value)) {
|
||||
return undefined;
|
||||
}
|
||||
return String(Math.trunc(value));
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function buildDiscordBaseSessionKey(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentId: string;
|
||||
@ -304,32 +279,10 @@ function resolveDiscordOutboundSessionRoute(params: {
|
||||
};
|
||||
}
|
||||
|
||||
const discordConfigAccessors = createScopedAccountConfigAccessors({
|
||||
resolveAccount: ({ cfg, accountId }) => resolveDiscordAccount({ cfg, accountId }),
|
||||
resolveAllowFrom: (account: ResolvedDiscordAccount) => account.config.dm?.allowFrom,
|
||||
formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }),
|
||||
resolveDefaultTo: (account: ResolvedDiscordAccount) => account.config.defaultTo,
|
||||
});
|
||||
|
||||
const discordConfigBase = createScopedChannelConfigBase({
|
||||
sectionKey: "discord",
|
||||
listAccountIds: listDiscordAccountIds,
|
||||
resolveAccount: (cfg, accountId) => resolveDiscordAccount({ cfg, accountId }),
|
||||
inspectAccount: (cfg, accountId) => inspectDiscordAccount({ cfg, accountId }),
|
||||
defaultAccountId: resolveDefaultDiscordAccountId,
|
||||
clearBaseFields: ["token", "name"],
|
||||
});
|
||||
|
||||
const discordSetupWizard = createDiscordSetupWizardProxy(async () => ({
|
||||
discordSetupWizard: (await loadDiscordChannelRuntime()).discordSetupWizard,
|
||||
}));
|
||||
|
||||
export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
|
||||
id: "discord",
|
||||
meta: {
|
||||
...meta,
|
||||
},
|
||||
setupWizard: discordSetupWizard,
|
||||
...createDiscordPluginBase({
|
||||
setup: discordSetupAdapter,
|
||||
}),
|
||||
pairing: {
|
||||
idLabel: "discordUserId",
|
||||
normalizeAllowEntry: (entry) => entry.replace(/^(discord|user):/i, ""),
|
||||
@ -340,31 +293,6 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
|
||||
);
|
||||
},
|
||||
},
|
||||
capabilities: {
|
||||
chatTypes: ["direct", "channel", "thread"],
|
||||
polls: true,
|
||||
reactions: true,
|
||||
threads: true,
|
||||
media: true,
|
||||
nativeCommands: true,
|
||||
},
|
||||
streaming: {
|
||||
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
|
||||
},
|
||||
reload: { configPrefixes: ["channels.discord"] },
|
||||
configSchema: buildChannelConfigSchema(DiscordConfigSchema),
|
||||
config: {
|
||||
...discordConfigBase,
|
||||
isConfigured: (account) => Boolean(account.token?.trim()),
|
||||
describeAccount: (account) => ({
|
||||
accountId: account.accountId,
|
||||
name: account.name,
|
||||
enabled: account.enabled,
|
||||
configured: Boolean(account.token?.trim()),
|
||||
tokenSource: account.tokenSource,
|
||||
}),
|
||||
...discordConfigAccessors,
|
||||
},
|
||||
allowlist: {
|
||||
supportsScope: ({ scope }) => scope === "dm",
|
||||
readConfig: ({ cfg, accountId }) =>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { Guild, User } from "@buape/carbon";
|
||||
import { evaluateGroupRouteAccessForPolicy } from "openclaw/plugin-sdk/group-access";
|
||||
import type { AllowlistMatch } from "../../../../src/channels/allowlist-match.js";
|
||||
import {
|
||||
buildChannelKeyCandidates,
|
||||
@ -6,7 +7,6 @@ import {
|
||||
resolveChannelMatchConfig,
|
||||
type ChannelMatchSource,
|
||||
} from "../../../../src/channels/channel-config.js";
|
||||
import { evaluateGroupRouteAccessForPolicy } from "../../../../src/plugin-sdk/group-access.js";
|
||||
import { formatDiscordUserTag } from "./format.js";
|
||||
|
||||
export type DiscordAllowList = {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue";
|
||||
import { createRunStateMachine } from "../../../../src/channels/run-state-machine.js";
|
||||
import { danger } from "../../../../src/globals.js";
|
||||
import { formatDurationSeconds } from "../../../../src/infra/format-time/format-duration.ts";
|
||||
import { KeyedAsyncQueue } from "../../../../src/plugin-sdk/keyed-async-queue.js";
|
||||
import { materializeDiscordInboundJob, type DiscordInboundJob } from "./inbound-job.js";
|
||||
import type { RuntimeEnv } from "./message-handler.preflight.types.js";
|
||||
import { processDiscordMessage } from "./message-handler.process.js";
|
||||
|
||||
@ -1,14 +1,11 @@
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { normalizeAccountId as normalizeSharedAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import { readJsonFileWithFallback, writeJsonFileAtomically } from "openclaw/plugin-sdk/json-store";
|
||||
import { normalizeProviderId } from "../../../../src/agents/model-selection.js";
|
||||
import { resolveStateDir } from "../../../../src/config/paths.js";
|
||||
import { withFileLock } from "../../../../src/infra/file-lock.js";
|
||||
import { resolveRequiredHomeDir } from "../../../../src/infra/home-dir.js";
|
||||
import {
|
||||
readJsonFileWithFallback,
|
||||
writeJsonFileAtomically,
|
||||
} from "../../../../src/plugin-sdk/json-store.js";
|
||||
import { normalizeAccountId as normalizeSharedAccountId } from "../../../../src/routing/account-id.js";
|
||||
|
||||
const MODEL_PICKER_PREFERENCES_LOCK_OPTIONS = {
|
||||
retries: {
|
||||
|
||||
@ -212,6 +212,58 @@ describe("Discord native plugin command dispatch", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("round-trips Discord native aliases through the real plugin registry", async () => {
|
||||
const cfg = createConfig();
|
||||
const commandSpec: NativeCommandSpec = {
|
||||
name: "pairdiscord",
|
||||
description: "Pair",
|
||||
acceptsArgs: true,
|
||||
};
|
||||
const command = createDiscordNativeCommand({
|
||||
command: commandSpec,
|
||||
cfg,
|
||||
discordConfig: cfg.channels?.discord ?? {},
|
||||
accountId: "default",
|
||||
sessionPrefix: "discord:slash",
|
||||
ephemeralDefault: true,
|
||||
threadBindings: createNoopThreadBindingManager("default"),
|
||||
});
|
||||
const interaction = createInteraction();
|
||||
|
||||
expect(
|
||||
registerPluginCommand("demo-plugin", {
|
||||
name: "pair",
|
||||
nativeNames: {
|
||||
telegram: "pair_device",
|
||||
discord: "pairdiscord",
|
||||
},
|
||||
description: "Pair device",
|
||||
acceptsArgs: true,
|
||||
requireAuth: false,
|
||||
handler: async ({ args }) => ({ text: `paired:${args ?? ""}` }),
|
||||
}),
|
||||
).toEqual({ ok: true });
|
||||
|
||||
const dispatchSpy = vi
|
||||
.spyOn(dispatcherModule, "dispatchReplyWithDispatcher")
|
||||
.mockResolvedValue({} as never);
|
||||
|
||||
await (command as { run: (interaction: unknown) => Promise<void> }).run(
|
||||
Object.assign(interaction, {
|
||||
options: {
|
||||
getString: () => "now",
|
||||
getBoolean: () => null,
|
||||
getFocused: () => "",
|
||||
},
|
||||
}) as unknown,
|
||||
);
|
||||
|
||||
expect(dispatchSpy).not.toHaveBeenCalled();
|
||||
expect(interaction.reply).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ content: "paired:now" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("blocks unauthorized Discord senders before requireAuth:false plugin commands execute", async () => {
|
||||
const cfg = {
|
||||
commands: {
|
||||
|
||||
39
extensions/discord/src/plugin-shared.ts
Normal file
39
extensions/discord/src/plugin-shared.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import {
|
||||
createScopedAccountConfigAccessors,
|
||||
createScopedChannelConfigBase,
|
||||
formatAllowFromLowercase,
|
||||
} from "../../../src/plugin-sdk-internal/channel-config.js";
|
||||
import { type OpenClawConfig } from "../../../src/plugin-sdk-internal/discord.js";
|
||||
import { inspectDiscordAccount } from "./account-inspect.js";
|
||||
import {
|
||||
listDiscordAccountIds,
|
||||
resolveDefaultDiscordAccountId,
|
||||
resolveDiscordAccount,
|
||||
type ResolvedDiscordAccount,
|
||||
} from "./accounts.js";
|
||||
import { createDiscordSetupWizardProxy } from "./setup-core.js";
|
||||
|
||||
async function loadDiscordChannelRuntime() {
|
||||
return await import("./channel.runtime.js");
|
||||
}
|
||||
|
||||
export const discordConfigAccessors = createScopedAccountConfigAccessors({
|
||||
resolveAccount: ({ cfg, accountId }: { cfg: OpenClawConfig; accountId?: string | null }) =>
|
||||
resolveDiscordAccount({ cfg, accountId }),
|
||||
resolveAllowFrom: (account: ResolvedDiscordAccount) => account.config.dm?.allowFrom,
|
||||
formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }),
|
||||
resolveDefaultTo: (account: ResolvedDiscordAccount) => account.config.defaultTo,
|
||||
});
|
||||
|
||||
export const discordConfigBase = createScopedChannelConfigBase({
|
||||
sectionKey: "discord",
|
||||
listAccountIds: listDiscordAccountIds,
|
||||
resolveAccount: (cfg, accountId) => resolveDiscordAccount({ cfg, accountId }),
|
||||
inspectAccount: (cfg, accountId) => inspectDiscordAccount({ cfg, accountId }),
|
||||
defaultAccountId: resolveDefaultDiscordAccountId,
|
||||
clearBaseFields: ["token", "name"],
|
||||
});
|
||||
|
||||
export const discordSetupWizard = createDiscordSetupWizardProxy(async () => ({
|
||||
discordSetupWizard: (await loadDiscordChannelRuntime()).discordSetupWizard,
|
||||
}));
|
||||
@ -1,5 +1,7 @@
|
||||
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/compat";
|
||||
import type { PluginRuntime } from "openclaw/plugin-sdk/core";
|
||||
import {
|
||||
createPluginRuntimeStore,
|
||||
type PluginRuntime,
|
||||
} from "../../../src/plugin-sdk-internal/core.js";
|
||||
|
||||
const { setRuntime: setDiscordRuntime, getRuntime: getDiscordRuntime } =
|
||||
createPluginRuntimeStore<PluginRuntime>("Discord runtime not initialized");
|
||||
|
||||
@ -1,10 +1,7 @@
|
||||
import { createPatchedAccountSetupAdapter } from "../../../src/channels/plugins/setup-helpers.js";
|
||||
import type { DiscordGuildEntry } from "../../../src/config/types.discord.js";
|
||||
import {
|
||||
applyAccountNameToChannelSection,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
formatDocsLink,
|
||||
migrateBaseNameToDefaultAccount,
|
||||
normalizeAccountId,
|
||||
noteChannelLookupFailure,
|
||||
noteChannelLookupSummary,
|
||||
parseMentionOrPrefixedId,
|
||||
@ -18,6 +15,7 @@ import {
|
||||
type ChannelSetupDmPolicy,
|
||||
type ChannelSetupWizard,
|
||||
} from "../../../src/plugin-sdk-internal/setup.js";
|
||||
import { formatDocsLink } from "../../../src/terminal/links.js";
|
||||
import { inspectDiscordAccount } from "./account-inspect.js";
|
||||
import { listDiscordAccountIds, resolveDiscordAccount } from "./accounts.js";
|
||||
|
||||
@ -72,15 +70,8 @@ export function parseDiscordAllowFromId(value: string): string | null {
|
||||
});
|
||||
}
|
||||
|
||||
export const discordSetupAdapter: ChannelSetupAdapter = {
|
||||
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
||||
applyAccountName: ({ cfg, accountId, name }) =>
|
||||
applyAccountNameToChannelSection({
|
||||
cfg,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
name,
|
||||
}),
|
||||
export const discordSetupAdapter: ChannelSetupAdapter = createPatchedAccountSetupAdapter({
|
||||
channelKey: channel,
|
||||
validateInput: ({ accountId, input }) => {
|
||||
if (input.useEnv && accountId !== DEFAULT_ACCOUNT_ID) {
|
||||
return "DISCORD_BOT_TOKEN can only be used for the default account.";
|
||||
@ -90,57 +81,46 @@ export const discordSetupAdapter: ChannelSetupAdapter = {
|
||||
}
|
||||
return null;
|
||||
},
|
||||
applyAccountConfig: ({ cfg, accountId, input }) => {
|
||||
const namedConfig = applyAccountNameToChannelSection({
|
||||
cfg,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
name: input.name,
|
||||
});
|
||||
const next =
|
||||
accountId !== DEFAULT_ACCOUNT_ID
|
||||
? migrateBaseNameToDefaultAccount({
|
||||
cfg: namedConfig,
|
||||
channelKey: channel,
|
||||
})
|
||||
: namedConfig;
|
||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||
return {
|
||||
...next,
|
||||
channels: {
|
||||
...next.channels,
|
||||
discord: {
|
||||
...next.channels?.discord,
|
||||
enabled: true,
|
||||
...(input.useEnv ? {} : input.token ? { token: input.token } : {}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
...next,
|
||||
channels: {
|
||||
...next.channels,
|
||||
discord: {
|
||||
...next.channels?.discord,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...next.channels?.discord?.accounts,
|
||||
[accountId]: {
|
||||
...next.channels?.discord?.accounts?.[accountId],
|
||||
enabled: true,
|
||||
...(input.token ? { token: input.token } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
buildPatch: (input) => (input.useEnv ? {} : input.token ? { token: input.token } : {}),
|
||||
});
|
||||
|
||||
type DiscordAllowFromResolverParams = {
|
||||
cfg: OpenClawConfig;
|
||||
accountId: string;
|
||||
credentialValues: { token?: string };
|
||||
entries: string[];
|
||||
};
|
||||
|
||||
export function createDiscordSetupWizardProxy(
|
||||
loadWizard: () => Promise<{ discordSetupWizard: ChannelSetupWizard }>,
|
||||
) {
|
||||
type DiscordGroupAllowlistResolverParams = DiscordAllowFromResolverParams & {
|
||||
prompter: { note: (message: string, title?: string) => Promise<void> };
|
||||
};
|
||||
|
||||
type DiscordGroupAllowlistResolution = Array<{
|
||||
input: string;
|
||||
resolved: boolean;
|
||||
}>;
|
||||
|
||||
type DiscordSetupWizardHandlers = {
|
||||
promptAllowFrom: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
prompter: import("../../../src/plugin-sdk-internal/setup.js").WizardPrompter;
|
||||
accountId?: string;
|
||||
}) => Promise<OpenClawConfig>;
|
||||
resolveAllowFromEntries: (params: DiscordAllowFromResolverParams) => Promise<
|
||||
Array<{
|
||||
input: string;
|
||||
resolved: boolean;
|
||||
id: string | null;
|
||||
}>
|
||||
>;
|
||||
resolveGroupAllowlist: (
|
||||
params: DiscordGroupAllowlistResolverParams,
|
||||
) => Promise<DiscordGroupAllowlistResolution>;
|
||||
};
|
||||
|
||||
export function createDiscordSetupWizardBase(
|
||||
handlers: DiscordSetupWizardHandlers,
|
||||
): ChannelSetupWizard {
|
||||
const discordDmPolicy: ChannelSetupDmPolicy = {
|
||||
label: "Discord",
|
||||
channel,
|
||||
@ -154,13 +134,7 @@ export function createDiscordSetupWizardProxy(
|
||||
channel,
|
||||
dmPolicy: policy,
|
||||
}),
|
||||
promptAllowFrom: async ({ cfg, prompter, accountId }) => {
|
||||
const wizard = (await loadWizard()).discordSetupWizard;
|
||||
if (!wizard.dmPolicy?.promptAllowFrom) {
|
||||
return cfg;
|
||||
}
|
||||
return await wizard.dmPolicy.promptAllowFrom({ cfg, prompter, accountId });
|
||||
},
|
||||
promptAllowFrom: handlers.promptAllowFrom,
|
||||
};
|
||||
|
||||
return {
|
||||
@ -238,44 +212,22 @@ export function createDiscordSetupWizardProxy(
|
||||
accountId,
|
||||
patch: { groupPolicy: policy },
|
||||
}),
|
||||
resolveAllowlist: async ({
|
||||
cfg,
|
||||
accountId,
|
||||
credentialValues,
|
||||
entries,
|
||||
prompter,
|
||||
}: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId: string;
|
||||
credentialValues: { token?: string };
|
||||
entries: string[];
|
||||
prompter: { note: (message: string, title?: string) => Promise<void> };
|
||||
}) => {
|
||||
const wizard = (await loadWizard()).discordSetupWizard;
|
||||
if (!wizard.groupAccess?.resolveAllowlist) {
|
||||
return entries.map((input) => ({ input, resolved: false }));
|
||||
}
|
||||
resolveAllowlist: async (params: DiscordGroupAllowlistResolverParams) => {
|
||||
try {
|
||||
return await wizard.groupAccess.resolveAllowlist({
|
||||
cfg,
|
||||
accountId,
|
||||
credentialValues,
|
||||
entries,
|
||||
prompter,
|
||||
});
|
||||
return await handlers.resolveGroupAllowlist(params);
|
||||
} catch (error) {
|
||||
await noteChannelLookupFailure({
|
||||
prompter,
|
||||
prompter: params.prompter,
|
||||
label: "Discord channels",
|
||||
error,
|
||||
});
|
||||
await noteChannelLookupSummary({
|
||||
prompter,
|
||||
prompter: params.prompter,
|
||||
label: "Discord channels",
|
||||
resolvedSections: [],
|
||||
unresolved: entries,
|
||||
unresolved: params.entries,
|
||||
});
|
||||
return entries.map((input) => ({ input, resolved: false }));
|
||||
return params.entries.map((input) => ({ input, resolved: false }));
|
||||
}
|
||||
},
|
||||
applyAllowlist: ({
|
||||
@ -305,28 +257,7 @@ export function createDiscordSetupWizardProxy(
|
||||
invalidWithoutCredentialNote:
|
||||
"Bot token missing; use numeric user ids (or mention form) only.",
|
||||
parseId: parseDiscordAllowFromId,
|
||||
resolveEntries: async ({
|
||||
cfg,
|
||||
accountId,
|
||||
credentialValues,
|
||||
entries,
|
||||
}: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId: string;
|
||||
credentialValues: { token?: string };
|
||||
entries: string[];
|
||||
}) => {
|
||||
const wizard = (await loadWizard()).discordSetupWizard;
|
||||
if (!wizard.allowFrom) {
|
||||
return entries.map((input) => ({ input, resolved: false, id: null }));
|
||||
}
|
||||
return await wizard.allowFrom.resolveEntries({
|
||||
cfg,
|
||||
accountId,
|
||||
credentialValues,
|
||||
entries,
|
||||
});
|
||||
},
|
||||
resolveEntries: handlers.resolveAllowFromEntries,
|
||||
apply: async ({
|
||||
cfg,
|
||||
accountId,
|
||||
@ -347,3 +278,42 @@ export function createDiscordSetupWizardProxy(
|
||||
disable: (cfg: OpenClawConfig) => setSetupChannelEnabled(cfg, channel, false),
|
||||
} satisfies ChannelSetupWizard;
|
||||
}
|
||||
|
||||
export function createDiscordSetupWizardProxy(
|
||||
loadWizard: () => Promise<{ discordSetupWizard: ChannelSetupWizard }>,
|
||||
) {
|
||||
return createDiscordSetupWizardBase({
|
||||
promptAllowFrom: async ({ cfg, prompter, accountId }) => {
|
||||
const wizard = (await loadWizard()).discordSetupWizard;
|
||||
if (!wizard.dmPolicy?.promptAllowFrom) {
|
||||
return cfg;
|
||||
}
|
||||
return await wizard.dmPolicy.promptAllowFrom({ cfg, prompter, accountId });
|
||||
},
|
||||
resolveAllowFromEntries: async ({ cfg, accountId, credentialValues, entries }) => {
|
||||
const wizard = (await loadWizard()).discordSetupWizard;
|
||||
if (!wizard.allowFrom) {
|
||||
return entries.map((input) => ({ input, resolved: false, id: null }));
|
||||
}
|
||||
return await wizard.allowFrom.resolveEntries({
|
||||
cfg,
|
||||
accountId,
|
||||
credentialValues,
|
||||
entries,
|
||||
});
|
||||
},
|
||||
resolveGroupAllowlist: async ({ cfg, accountId, credentialValues, entries, prompter }) => {
|
||||
const wizard = (await loadWizard()).discordSetupWizard;
|
||||
if (!wizard.groupAccess?.resolveAllowlist) {
|
||||
return entries.map((input) => ({ input, resolved: false }));
|
||||
}
|
||||
return (await wizard.groupAccess.resolveAllowlist({
|
||||
cfg,
|
||||
accountId,
|
||||
credentialValues,
|
||||
entries,
|
||||
prompter,
|
||||
})) as DiscordGroupAllowlistResolution;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,27 +1,14 @@
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
formatDocsLink,
|
||||
noteChannelLookupFailure,
|
||||
noteChannelLookupSummary,
|
||||
type OpenClawConfig,
|
||||
parseMentionOrPrefixedId,
|
||||
patchChannelConfigForAccount,
|
||||
promptLegacyChannelAllowFrom,
|
||||
resolveSetupAccountId,
|
||||
setLegacyChannelDmPolicyWithAllowFrom,
|
||||
setSetupChannelEnabled,
|
||||
type WizardPrompter,
|
||||
} from "../../../src/plugin-sdk-internal/setup.js";
|
||||
import {
|
||||
type ChannelSetupDmPolicy,
|
||||
type ChannelSetupWizard,
|
||||
} from "../../../src/plugin-sdk-internal/setup.js";
|
||||
import { inspectDiscordAccount } from "./account-inspect.js";
|
||||
import {
|
||||
listDiscordAccountIds,
|
||||
resolveDefaultDiscordAccountId,
|
||||
resolveDiscordAccount,
|
||||
} from "./accounts.js";
|
||||
import { type ChannelSetupWizard } from "../../../src/plugin-sdk-internal/setup.js";
|
||||
import { formatDocsLink } from "../../../src/terminal/links.js";
|
||||
import { resolveDefaultDiscordAccountId, resolveDiscordAccount } from "./accounts.js";
|
||||
import { normalizeDiscordSlug } from "./monitor/allow-list.js";
|
||||
import {
|
||||
resolveDiscordChannelAllowlist,
|
||||
@ -29,6 +16,7 @@ import {
|
||||
} from "./resolve-channels.js";
|
||||
import { resolveDiscordUserAllowlist } from "./resolve-users.js";
|
||||
import {
|
||||
createDiscordSetupWizardBase,
|
||||
discordSetupAdapter,
|
||||
DISCORD_TOKEN_HELP_LINES,
|
||||
parseDiscordAllowFromId,
|
||||
@ -94,186 +82,62 @@ async function promptDiscordAllowFrom(params: {
|
||||
});
|
||||
}
|
||||
|
||||
const discordDmPolicy: ChannelSetupDmPolicy = {
|
||||
label: "Discord",
|
||||
channel,
|
||||
policyKey: "channels.discord.dmPolicy",
|
||||
allowFromKey: "channels.discord.allowFrom",
|
||||
getCurrent: (cfg) =>
|
||||
cfg.channels?.discord?.dmPolicy ?? cfg.channels?.discord?.dm?.policy ?? "pairing",
|
||||
setPolicy: (cfg, policy) =>
|
||||
setLegacyChannelDmPolicyWithAllowFrom({
|
||||
cfg,
|
||||
channel,
|
||||
dmPolicy: policy,
|
||||
}),
|
||||
export const discordSetupWizard: ChannelSetupWizard = createDiscordSetupWizardBase({
|
||||
promptAllowFrom: promptDiscordAllowFrom,
|
||||
};
|
||||
|
||||
export const discordSetupWizard: ChannelSetupWizard = {
|
||||
channel,
|
||||
status: {
|
||||
configuredLabel: "configured",
|
||||
unconfiguredLabel: "needs token",
|
||||
configuredHint: "configured",
|
||||
unconfiguredHint: "needs token",
|
||||
configuredScore: 2,
|
||||
unconfiguredScore: 1,
|
||||
resolveConfigured: ({ cfg }) =>
|
||||
listDiscordAccountIds(cfg).some(
|
||||
(accountId) => inspectDiscordAccount({ cfg, accountId }).configured,
|
||||
),
|
||||
},
|
||||
credentials: [
|
||||
{
|
||||
inputKey: "token",
|
||||
providerHint: channel,
|
||||
credentialLabel: "Discord bot token",
|
||||
preferredEnvVar: "DISCORD_BOT_TOKEN",
|
||||
helpTitle: "Discord bot token",
|
||||
helpLines: DISCORD_TOKEN_HELP_LINES,
|
||||
envPrompt: "DISCORD_BOT_TOKEN detected. Use env var?",
|
||||
keepPrompt: "Discord token already configured. Keep it?",
|
||||
inputPrompt: "Enter Discord bot token",
|
||||
allowEnv: ({ accountId }) => accountId === DEFAULT_ACCOUNT_ID,
|
||||
inspect: ({ cfg, accountId }) => {
|
||||
const account = inspectDiscordAccount({ cfg, accountId });
|
||||
return {
|
||||
accountConfigured: account.configured,
|
||||
hasConfiguredValue: account.tokenStatus !== "missing",
|
||||
resolvedValue: account.token?.trim() || undefined,
|
||||
envValue:
|
||||
accountId === DEFAULT_ACCOUNT_ID
|
||||
? process.env.DISCORD_BOT_TOKEN?.trim() || undefined
|
||||
: undefined,
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
groupAccess: {
|
||||
label: "Discord channels",
|
||||
placeholder: "My Server/#general, guildId/channelId, #support",
|
||||
currentPolicy: ({ cfg, accountId }) =>
|
||||
resolveDiscordAccount({ cfg, accountId }).config.groupPolicy ?? "allowlist",
|
||||
currentEntries: ({ cfg, accountId }) =>
|
||||
Object.entries(resolveDiscordAccount({ cfg, accountId }).config.guilds ?? {}).flatMap(
|
||||
([guildKey, value]) => {
|
||||
const channels = value?.channels ?? {};
|
||||
const channelKeys = Object.keys(channels);
|
||||
if (channelKeys.length === 0) {
|
||||
const input = /^\d+$/.test(guildKey) ? `guild:${guildKey}` : guildKey;
|
||||
return [input];
|
||||
}
|
||||
return channelKeys.map((channelKey) => `${guildKey}/${channelKey}`);
|
||||
},
|
||||
),
|
||||
updatePrompt: ({ cfg, accountId }) =>
|
||||
Boolean(resolveDiscordAccount({ cfg, accountId }).config.guilds),
|
||||
setPolicy: ({ cfg, accountId, policy }) =>
|
||||
patchChannelConfigForAccount({
|
||||
cfg,
|
||||
channel,
|
||||
accountId,
|
||||
patch: { groupPolicy: policy },
|
||||
}),
|
||||
resolveAllowlist: async ({ cfg, accountId, credentialValues, entries, prompter }) => {
|
||||
const token =
|
||||
resolveAllowFromEntries: async ({ cfg, accountId, credentialValues, entries }) =>
|
||||
await resolveDiscordAllowFromEntries({
|
||||
token:
|
||||
resolveDiscordAccount({ cfg, accountId }).token ||
|
||||
(typeof credentialValues.token === "string" ? credentialValues.token : "");
|
||||
let resolved: DiscordChannelResolution[] = entries.map((input) => ({
|
||||
input,
|
||||
resolved: false,
|
||||
}));
|
||||
if (!token || entries.length === 0) {
|
||||
return resolved;
|
||||
}
|
||||
try {
|
||||
resolved = await resolveDiscordChannelAllowlist({
|
||||
token,
|
||||
entries,
|
||||
});
|
||||
const resolvedChannels = resolved.filter((entry) => entry.resolved && entry.channelId);
|
||||
const resolvedGuilds = resolved.filter(
|
||||
(entry) => entry.resolved && entry.guildId && !entry.channelId,
|
||||
);
|
||||
const unresolved = resolved.filter((entry) => !entry.resolved).map((entry) => entry.input);
|
||||
await noteChannelLookupSummary({
|
||||
prompter,
|
||||
label: "Discord channels",
|
||||
resolvedSections: [
|
||||
{
|
||||
title: "Resolved channels",
|
||||
values: resolvedChannels
|
||||
.map((entry) => entry.channelId)
|
||||
.filter((value): value is string => Boolean(value)),
|
||||
},
|
||||
{
|
||||
title: "Resolved guilds",
|
||||
values: resolvedGuilds
|
||||
.map((entry) => entry.guildId)
|
||||
.filter((value): value is string => Boolean(value)),
|
||||
},
|
||||
],
|
||||
unresolved,
|
||||
});
|
||||
} catch (error) {
|
||||
await noteChannelLookupFailure({
|
||||
prompter,
|
||||
label: "Discord channels",
|
||||
error,
|
||||
});
|
||||
}
|
||||
(typeof credentialValues.token === "string" ? credentialValues.token : ""),
|
||||
entries,
|
||||
}),
|
||||
resolveGroupAllowlist: async ({ cfg, accountId, credentialValues, entries, prompter }) => {
|
||||
const token =
|
||||
resolveDiscordAccount({ cfg, accountId }).token ||
|
||||
(typeof credentialValues.token === "string" ? credentialValues.token : "");
|
||||
let resolved: DiscordChannelResolution[] = entries.map((input) => ({
|
||||
input,
|
||||
resolved: false,
|
||||
}));
|
||||
if (!token || entries.length === 0) {
|
||||
return resolved;
|
||||
},
|
||||
applyAllowlist: ({ cfg, accountId, resolved }) => {
|
||||
const allowlistEntries: Array<{ guildKey: string; channelKey?: string }> = [];
|
||||
for (const entry of resolved as DiscordChannelResolution[]) {
|
||||
const guildKey =
|
||||
entry.guildId ??
|
||||
(entry.guildName ? normalizeDiscordSlug(entry.guildName) : undefined) ??
|
||||
"*";
|
||||
const channelKey =
|
||||
entry.channelId ??
|
||||
(entry.channelName ? normalizeDiscordSlug(entry.channelName) : undefined);
|
||||
if (!channelKey && guildKey === "*") {
|
||||
continue;
|
||||
}
|
||||
allowlistEntries.push({ guildKey, ...(channelKey ? { channelKey } : {}) });
|
||||
}
|
||||
return setDiscordGuildChannelAllowlist(cfg, accountId, allowlistEntries);
|
||||
},
|
||||
},
|
||||
allowFrom: {
|
||||
credentialInputKey: "token",
|
||||
helpTitle: "Discord allowlist",
|
||||
helpLines: [
|
||||
"Allowlist Discord DMs by username (we resolve to user ids).",
|
||||
"Examples:",
|
||||
"- 123456789012345678",
|
||||
"- @alice",
|
||||
"- alice#1234",
|
||||
"Multiple entries: comma-separated.",
|
||||
`Docs: ${formatDocsLink("/discord", "discord")}`,
|
||||
],
|
||||
message: "Discord allowFrom (usernames or ids)",
|
||||
placeholder: "@alice, 123456789012345678",
|
||||
invalidWithoutCredentialNote: "Bot token missing; use numeric user ids (or mention form) only.",
|
||||
parseId: parseDiscordAllowFromId,
|
||||
resolveEntries: async ({ cfg, accountId, credentialValues, entries }) =>
|
||||
await resolveDiscordAllowFromEntries({
|
||||
token:
|
||||
resolveDiscordAccount({ cfg, accountId }).token ||
|
||||
(typeof credentialValues.token === "string" ? credentialValues.token : ""),
|
||||
}
|
||||
try {
|
||||
resolved = await resolveDiscordChannelAllowlist({
|
||||
token,
|
||||
entries,
|
||||
}),
|
||||
apply: async ({ cfg, accountId, allowFrom }) =>
|
||||
patchChannelConfigForAccount({
|
||||
cfg,
|
||||
channel,
|
||||
accountId,
|
||||
patch: { dmPolicy: "allowlist", allowFrom },
|
||||
}),
|
||||
});
|
||||
const resolvedChannels = resolved.filter((entry) => entry.resolved && entry.channelId);
|
||||
const resolvedGuilds = resolved.filter(
|
||||
(entry) => entry.resolved && entry.guildId && !entry.channelId,
|
||||
);
|
||||
const unresolved = resolved.filter((entry) => !entry.resolved).map((entry) => entry.input);
|
||||
await noteChannelLookupSummary({
|
||||
prompter,
|
||||
label: "Discord channels",
|
||||
resolvedSections: [
|
||||
{
|
||||
title: "Resolved channels",
|
||||
values: resolvedChannels
|
||||
.map((entry) => entry.channelId)
|
||||
.filter((value): value is string => Boolean(value)),
|
||||
},
|
||||
{
|
||||
title: "Resolved guilds",
|
||||
values: resolvedGuilds
|
||||
.map((entry) => entry.guildId)
|
||||
.filter((value): value is string => Boolean(value)),
|
||||
},
|
||||
],
|
||||
unresolved,
|
||||
});
|
||||
} catch (error) {
|
||||
await noteChannelLookupFailure({
|
||||
prompter,
|
||||
label: "Discord channels",
|
||||
error,
|
||||
});
|
||||
}
|
||||
return resolved;
|
||||
},
|
||||
dmPolicy: discordDmPolicy,
|
||||
disable: (cfg) => setSetupChannelEnabled(cfg, channel, false),
|
||||
};
|
||||
});
|
||||
|
||||
94
extensions/discord/src/shared.ts
Normal file
94
extensions/discord/src/shared.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import { createScopedChannelConfigBase } from "openclaw/plugin-sdk/compat";
|
||||
import {
|
||||
createScopedAccountConfigAccessors,
|
||||
formatAllowFromLowercase,
|
||||
} from "openclaw/plugin-sdk/compat";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
DiscordConfigSchema,
|
||||
getChatChannelMeta,
|
||||
type ChannelPlugin,
|
||||
} from "openclaw/plugin-sdk/discord";
|
||||
import { inspectDiscordAccount } from "./account-inspect.js";
|
||||
import {
|
||||
listDiscordAccountIds,
|
||||
resolveDefaultDiscordAccountId,
|
||||
resolveDiscordAccount,
|
||||
type ResolvedDiscordAccount,
|
||||
} from "./accounts.js";
|
||||
import { createDiscordSetupWizardProxy } from "./setup-core.js";
|
||||
|
||||
export const DISCORD_CHANNEL = "discord" as const;
|
||||
|
||||
async function loadDiscordChannelRuntime() {
|
||||
return await import("./channel.runtime.js");
|
||||
}
|
||||
|
||||
export const discordSetupWizard = createDiscordSetupWizardProxy(async () => ({
|
||||
discordSetupWizard: (await loadDiscordChannelRuntime()).discordSetupWizard,
|
||||
}));
|
||||
|
||||
export const discordConfigAccessors = createScopedAccountConfigAccessors({
|
||||
resolveAccount: ({ cfg, accountId }) => resolveDiscordAccount({ cfg, accountId }),
|
||||
resolveAllowFrom: (account: ResolvedDiscordAccount) => account.config.dm?.allowFrom,
|
||||
formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }),
|
||||
resolveDefaultTo: (account: ResolvedDiscordAccount) => account.config.defaultTo,
|
||||
});
|
||||
|
||||
export const discordConfigBase = createScopedChannelConfigBase<ResolvedDiscordAccount>({
|
||||
sectionKey: DISCORD_CHANNEL,
|
||||
listAccountIds: listDiscordAccountIds,
|
||||
resolveAccount: (cfg, accountId) => resolveDiscordAccount({ cfg, accountId }),
|
||||
inspectAccount: (cfg, accountId) => inspectDiscordAccount({ cfg, accountId }),
|
||||
defaultAccountId: resolveDefaultDiscordAccountId,
|
||||
clearBaseFields: ["token", "name"],
|
||||
});
|
||||
|
||||
export function createDiscordPluginBase(params: {
|
||||
setup: NonNullable<ChannelPlugin<ResolvedDiscordAccount>["setup"]>;
|
||||
}): Pick<
|
||||
ChannelPlugin<ResolvedDiscordAccount>,
|
||||
| "id"
|
||||
| "meta"
|
||||
| "setupWizard"
|
||||
| "capabilities"
|
||||
| "streaming"
|
||||
| "reload"
|
||||
| "configSchema"
|
||||
| "config"
|
||||
| "setup"
|
||||
> {
|
||||
return {
|
||||
id: DISCORD_CHANNEL,
|
||||
meta: {
|
||||
...getChatChannelMeta(DISCORD_CHANNEL),
|
||||
},
|
||||
setupWizard: discordSetupWizard,
|
||||
capabilities: {
|
||||
chatTypes: ["direct", "channel", "thread"],
|
||||
polls: true,
|
||||
reactions: true,
|
||||
threads: true,
|
||||
media: true,
|
||||
nativeCommands: true,
|
||||
},
|
||||
streaming: {
|
||||
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
|
||||
},
|
||||
reload: { configPrefixes: ["channels.discord"] },
|
||||
configSchema: buildChannelConfigSchema(DiscordConfigSchema),
|
||||
config: {
|
||||
...discordConfigBase,
|
||||
isConfigured: (account) => Boolean(account.token?.trim()),
|
||||
describeAccount: (account) => ({
|
||||
accountId: account.accountId,
|
||||
name: account.name,
|
||||
enabled: account.enabled,
|
||||
configured: Boolean(account.token?.trim()),
|
||||
tokenSource: account.tokenSource,
|
||||
}),
|
||||
...discordConfigAccessors,
|
||||
},
|
||||
setup: params.setup,
|
||||
};
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
||||
import type { OpenClawPluginApi } from "../../../src/plugin-sdk-internal/core.js";
|
||||
import { resolveDiscordAccount } from "./accounts.js";
|
||||
import {
|
||||
autoBindSpawnedDiscordSubagent,
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import type { ChannelSetupAdapter } from "../../../src/channels/plugins/types.adapters.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
type ChannelSetupAdapter,
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import type { FeishuConfig } from "./types.js";
|
||||
|
||||
export function setFeishuNamedAccountEnabled(
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
import {
|
||||
buildSingleChannelSecretPromptState,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
formatDocsLink,
|
||||
hasConfiguredSecretInput,
|
||||
mergeAllowFromEntries,
|
||||
promptSingleChannelSecretInput,
|
||||
setTopLevelChannelAllowFrom,
|
||||
setTopLevelChannelDmPolicyWithAllowFrom,
|
||||
setTopLevelChannelGroupPolicy,
|
||||
splitSetupEntries,
|
||||
} from "../../../src/channels/plugins/setup-wizard-helpers.js";
|
||||
import type { ChannelSetupDmPolicy } from "../../../src/channels/plugins/setup-wizard-types.js";
|
||||
import type { ChannelSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import type { DmPolicy } from "../../../src/config/types.js";
|
||||
import type { SecretInput } from "../../../src/config/types.secrets.js";
|
||||
import { hasConfiguredSecretInput } from "../../../src/config/types.secrets.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js";
|
||||
import { formatDocsLink } from "../../../src/terminal/links.js";
|
||||
type ChannelSetupDmPolicy,
|
||||
type ChannelSetupWizard,
|
||||
type DmPolicy,
|
||||
type OpenClawConfig,
|
||||
type SecretInput,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import { listFeishuAccountIds, resolveFeishuCredentials } from "./accounts.js";
|
||||
import { probeFeishu } from "./probe.js";
|
||||
import { feishuSetupAdapter } from "./setup-core.js";
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/core";
|
||||
import { fetchGeminiUsage } from "../../src/infra/provider-usage.fetch.js";
|
||||
import { buildOauthProviderAuthResult } from "../../src/plugin-sdk/provider-auth-result.js";
|
||||
import type {
|
||||
OpenClawPluginApi,
|
||||
ProviderAuthContext,
|
||||
|
||||
@ -11,6 +11,7 @@ import { emptyPluginConfigSchema } from "../../src/plugins/config-schema.js";
|
||||
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
|
||||
import type { OpenClawPluginApi } from "../../src/plugins/types.js";
|
||||
import { registerGoogleGeminiCliProvider } from "./gemini-cli-provider.js";
|
||||
import { googleMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
||||
import { isModernGoogleModel, resolveGoogle31ForwardCompatModel } from "./provider-models.js";
|
||||
|
||||
const googlePlugin = {
|
||||
@ -51,6 +52,7 @@ const googlePlugin = {
|
||||
isModernModelRef: ({ modelId }) => isModernGoogleModel(modelId),
|
||||
});
|
||||
registerGoogleGeminiCliProvider(api);
|
||||
api.registerMediaUnderstandingProvider(googleMediaUnderstandingProvider);
|
||||
api.registerWebSearchProvider(
|
||||
createPluginBackedWebSearchProvider({
|
||||
id: "gemini",
|
||||
|
||||
150
extensions/google/media-understanding-provider.ts
Normal file
150
extensions/google/media-understanding-provider.ts
Normal file
@ -0,0 +1,150 @@
|
||||
import { normalizeGoogleModelId } from "../../src/agents/model-id-normalization.js";
|
||||
import { parseGeminiAuth } from "../../src/infra/gemini-auth.js";
|
||||
import { describeImageWithModel } from "../../src/media-understanding/providers/image.js";
|
||||
import {
|
||||
assertOkOrThrowHttpError,
|
||||
normalizeBaseUrl,
|
||||
postJsonRequest,
|
||||
} from "../../src/media-understanding/providers/shared.js";
|
||||
import type {
|
||||
AudioTranscriptionRequest,
|
||||
AudioTranscriptionResult,
|
||||
MediaUnderstandingProvider,
|
||||
VideoDescriptionRequest,
|
||||
VideoDescriptionResult,
|
||||
} from "../../src/media-understanding/types.js";
|
||||
|
||||
export const DEFAULT_GOOGLE_AUDIO_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
||||
export const DEFAULT_GOOGLE_VIDEO_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
||||
const DEFAULT_GOOGLE_AUDIO_MODEL = "gemini-3-flash-preview";
|
||||
const DEFAULT_GOOGLE_VIDEO_MODEL = "gemini-3-flash-preview";
|
||||
const DEFAULT_GOOGLE_AUDIO_PROMPT = "Transcribe the audio.";
|
||||
const DEFAULT_GOOGLE_VIDEO_PROMPT = "Describe the video.";
|
||||
|
||||
async function generateGeminiInlineDataText(params: {
|
||||
buffer: Buffer;
|
||||
mime?: string;
|
||||
apiKey: string;
|
||||
baseUrl?: string;
|
||||
headers?: Record<string, string>;
|
||||
model?: string;
|
||||
prompt?: string;
|
||||
timeoutMs: number;
|
||||
fetchFn?: typeof fetch;
|
||||
defaultBaseUrl: string;
|
||||
defaultModel: string;
|
||||
defaultPrompt: string;
|
||||
defaultMime: string;
|
||||
httpErrorLabel: string;
|
||||
missingTextError: string;
|
||||
}): Promise<{ text: string; model: string }> {
|
||||
const fetchFn = params.fetchFn ?? fetch;
|
||||
const baseUrl = normalizeBaseUrl(params.baseUrl, params.defaultBaseUrl);
|
||||
const allowPrivate = Boolean(params.baseUrl?.trim());
|
||||
const model = (() => {
|
||||
const trimmed = params.model?.trim();
|
||||
if (!trimmed) {
|
||||
return params.defaultModel;
|
||||
}
|
||||
return normalizeGoogleModelId(trimmed);
|
||||
})();
|
||||
const url = `${baseUrl}/models/${model}:generateContent`;
|
||||
|
||||
const authHeaders = parseGeminiAuth(params.apiKey);
|
||||
const headers = new Headers(params.headers);
|
||||
for (const [key, value] of Object.entries(authHeaders.headers)) {
|
||||
if (!headers.has(key)) {
|
||||
headers.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
const prompt = (() => {
|
||||
const trimmed = params.prompt?.trim();
|
||||
return trimmed || params.defaultPrompt;
|
||||
})();
|
||||
|
||||
const body = {
|
||||
contents: [
|
||||
{
|
||||
role: "user",
|
||||
parts: [
|
||||
{ text: prompt },
|
||||
{
|
||||
inline_data: {
|
||||
mime_type: params.mime ?? params.defaultMime,
|
||||
data: params.buffer.toString("base64"),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { response: res, release } = await postJsonRequest({
|
||||
url,
|
||||
headers,
|
||||
body,
|
||||
timeoutMs: params.timeoutMs,
|
||||
fetchFn,
|
||||
allowPrivateNetwork: allowPrivate,
|
||||
});
|
||||
|
||||
try {
|
||||
await assertOkOrThrowHttpError(res, params.httpErrorLabel);
|
||||
|
||||
const payload = (await res.json()) as {
|
||||
candidates?: Array<{
|
||||
content?: { parts?: Array<{ text?: string }> };
|
||||
}>;
|
||||
};
|
||||
const parts = payload.candidates?.[0]?.content?.parts ?? [];
|
||||
const text = parts
|
||||
.map((part) => part?.text?.trim())
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
if (!text) {
|
||||
throw new Error(params.missingTextError);
|
||||
}
|
||||
return { text, model };
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
}
|
||||
|
||||
export async function transcribeGeminiAudio(
|
||||
params: AudioTranscriptionRequest,
|
||||
): Promise<AudioTranscriptionResult> {
|
||||
const { text, model } = await generateGeminiInlineDataText({
|
||||
...params,
|
||||
defaultBaseUrl: DEFAULT_GOOGLE_AUDIO_BASE_URL,
|
||||
defaultModel: DEFAULT_GOOGLE_AUDIO_MODEL,
|
||||
defaultPrompt: DEFAULT_GOOGLE_AUDIO_PROMPT,
|
||||
defaultMime: "audio/wav",
|
||||
httpErrorLabel: "Audio transcription failed",
|
||||
missingTextError: "Audio transcription response missing text",
|
||||
});
|
||||
return { text, model };
|
||||
}
|
||||
|
||||
export async function describeGeminiVideo(
|
||||
params: VideoDescriptionRequest,
|
||||
): Promise<VideoDescriptionResult> {
|
||||
const { text, model } = await generateGeminiInlineDataText({
|
||||
...params,
|
||||
defaultBaseUrl: DEFAULT_GOOGLE_VIDEO_BASE_URL,
|
||||
defaultModel: DEFAULT_GOOGLE_VIDEO_MODEL,
|
||||
defaultPrompt: DEFAULT_GOOGLE_VIDEO_PROMPT,
|
||||
defaultMime: "video/mp4",
|
||||
httpErrorLabel: "Video description failed",
|
||||
missingTextError: "Video description response missing text",
|
||||
});
|
||||
return { text, model };
|
||||
}
|
||||
|
||||
export const googleMediaUnderstandingProvider: MediaUnderstandingProvider = {
|
||||
id: "google",
|
||||
capabilities: ["image", "audio", "video"],
|
||||
describeImage: describeImageWithModel,
|
||||
transcribeAudio: transcribeGeminiAudio,
|
||||
describeVideo: describeGeminiVideo,
|
||||
};
|
||||
@ -1,22 +1,10 @@
|
||||
import {
|
||||
applyAccountNameToChannelSection,
|
||||
applySetupAccountConfigPatch,
|
||||
migrateBaseNameToDefaultAccount,
|
||||
} from "../../../src/channels/plugins/setup-helpers.js";
|
||||
import type { ChannelSetupAdapter } from "../../../src/channels/plugins/types.adapters.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../src/routing/session-key.js";
|
||||
import { createPatchedAccountSetupAdapter } from "../../../src/channels/plugins/setup-helpers.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js";
|
||||
|
||||
const channel = "googlechat" as const;
|
||||
|
||||
export const googlechatSetupAdapter: ChannelSetupAdapter = {
|
||||
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
||||
applyAccountName: ({ cfg, accountId, name }) =>
|
||||
applyAccountNameToChannelSection({
|
||||
cfg,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
name,
|
||||
}),
|
||||
export const googlechatSetupAdapter = createPatchedAccountSetupAdapter({
|
||||
channelKey: channel,
|
||||
validateInput: ({ accountId, input }) => {
|
||||
if (input.useEnv && accountId !== DEFAULT_ACCOUNT_ID) {
|
||||
return "GOOGLE_CHAT_SERVICE_ACCOUNT env vars can only be used for the default account.";
|
||||
@ -26,20 +14,7 @@ export const googlechatSetupAdapter: ChannelSetupAdapter = {
|
||||
}
|
||||
return null;
|
||||
},
|
||||
applyAccountConfig: ({ cfg, accountId, input }) => {
|
||||
const namedConfig = applyAccountNameToChannelSection({
|
||||
cfg,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
name: input.name,
|
||||
});
|
||||
const next =
|
||||
accountId !== DEFAULT_ACCOUNT_ID
|
||||
? migrateBaseNameToDefaultAccount({
|
||||
cfg: namedConfig,
|
||||
channelKey: channel,
|
||||
})
|
||||
: namedConfig;
|
||||
buildPatch: (input) => {
|
||||
const patch = input.useEnv
|
||||
? {}
|
||||
: input.tokenFile
|
||||
@ -51,17 +26,12 @@ export const googlechatSetupAdapter: ChannelSetupAdapter = {
|
||||
const audience = input.audience?.trim();
|
||||
const webhookPath = input.webhookPath?.trim();
|
||||
const webhookUrl = input.webhookUrl?.trim();
|
||||
return applySetupAccountConfigPatch({
|
||||
cfg: next,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
patch: {
|
||||
...patch,
|
||||
...(audienceType ? { audienceType } : {}),
|
||||
...(audience ? { audience } : {}),
|
||||
...(webhookPath ? { webhookPath } : {}),
|
||||
...(webhookUrl ? { webhookUrl } : {}),
|
||||
},
|
||||
});
|
||||
return {
|
||||
...patch,
|
||||
...(audienceType ? { audienceType } : {}),
|
||||
...(audience ? { audience } : {}),
|
||||
...(webhookPath ? { webhookPath } : {}),
|
||||
...(webhookUrl ? { webhookUrl } : {}),
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@ -1,19 +1,17 @@
|
||||
import {
|
||||
applySetupAccountConfigPatch,
|
||||
migrateBaseNameToDefaultAccount,
|
||||
} from "../../../src/channels/plugins/setup-helpers.js";
|
||||
import {
|
||||
addWildcardAllowFrom,
|
||||
applySetupAccountConfigPatch,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
formatDocsLink,
|
||||
mergeAllowFromEntries,
|
||||
migrateBaseNameToDefaultAccount,
|
||||
setTopLevelChannelDmPolicyWithAllowFrom,
|
||||
splitSetupEntries,
|
||||
} from "../../../src/channels/plugins/setup-wizard-helpers.js";
|
||||
import type { ChannelSetupDmPolicy } from "../../../src/channels/plugins/setup-wizard-types.js";
|
||||
import type { ChannelSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import type { DmPolicy } from "../../../src/config/types.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js";
|
||||
import { formatDocsLink } from "../../../src/terminal/links.js";
|
||||
type ChannelSetupDmPolicy,
|
||||
type ChannelSetupWizard,
|
||||
type DmPolicy,
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import {
|
||||
listGoogleChatAccountIds,
|
||||
resolveDefaultGoogleChatAccountId,
|
||||
|
||||
@ -1,9 +1,6 @@
|
||||
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
||||
import {
|
||||
applyHuggingfaceConfig,
|
||||
HUGGINGFACE_DEFAULT_MODEL_REF,
|
||||
} from "../../src/commands/onboard-auth.js";
|
||||
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
|
||||
import { applyHuggingfaceConfig, HUGGINGFACE_DEFAULT_MODEL_REF } from "./onboard.js";
|
||||
import { buildHuggingfaceProvider } from "./provider-catalog.js";
|
||||
|
||||
const PROVIDER_ID = "huggingface";
|
||||
|
||||
35
extensions/huggingface/onboard.ts
Normal file
35
extensions/huggingface/onboard.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import {
|
||||
buildHuggingfaceModelDefinition,
|
||||
HUGGINGFACE_BASE_URL,
|
||||
HUGGINGFACE_MODEL_CATALOG,
|
||||
} from "../../src/agents/huggingface-models.js";
|
||||
import {
|
||||
applyAgentDefaultModelPrimary,
|
||||
applyProviderConfigWithModelCatalog,
|
||||
} from "../../src/commands/onboard-auth.config-shared.js";
|
||||
import type { OpenClawConfig } from "../../src/config/config.js";
|
||||
|
||||
export const HUGGINGFACE_DEFAULT_MODEL_REF = "huggingface/deepseek-ai/DeepSeek-R1";
|
||||
|
||||
export function applyHuggingfaceProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
models[HUGGINGFACE_DEFAULT_MODEL_REF] = {
|
||||
...models[HUGGINGFACE_DEFAULT_MODEL_REF],
|
||||
alias: models[HUGGINGFACE_DEFAULT_MODEL_REF]?.alias ?? "Hugging Face",
|
||||
};
|
||||
|
||||
return applyProviderConfigWithModelCatalog(cfg, {
|
||||
agentModels: models,
|
||||
providerId: "huggingface",
|
||||
api: "openai-completions",
|
||||
baseUrl: HUGGINGFACE_BASE_URL,
|
||||
catalogModels: HUGGINGFACE_MODEL_CATALOG.map(buildHuggingfaceModelDefinition),
|
||||
});
|
||||
}
|
||||
|
||||
export function applyHuggingfaceConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
return applyAgentDefaultModelPrimary(
|
||||
applyHuggingfaceProviderConfig(cfg),
|
||||
HUGGINGFACE_DEFAULT_MODEL_REF,
|
||||
);
|
||||
}
|
||||
@ -1,10 +1,10 @@
|
||||
import type { IMessageAccountConfig } from "openclaw/plugin-sdk/imessage";
|
||||
import {
|
||||
type OpenClawConfig,
|
||||
createAccountListHelpers,
|
||||
normalizeAccountId,
|
||||
resolveAccountEntry,
|
||||
} from "../../../src/plugin-sdk-internal/accounts.js";
|
||||
import type { IMessageAccountConfig } from "../../../src/plugin-sdk-internal/imessage.js";
|
||||
|
||||
export type ResolvedIMessageAccount = {
|
||||
accountId: string;
|
||||
|
||||
@ -1,101 +1,11 @@
|
||||
import {
|
||||
buildAccountScopedDmSecurityPolicy,
|
||||
collectAllowlistProviderRestrictSendersWarnings,
|
||||
} from "openclaw/plugin-sdk/compat";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
deleteAccountFromConfigSection,
|
||||
formatTrimmedAllowFromEntries,
|
||||
getChatChannelMeta,
|
||||
IMessageConfigSchema,
|
||||
resolveIMessageConfigAllowFrom,
|
||||
resolveIMessageConfigDefaultTo,
|
||||
setAccountEnabledInConfigSection,
|
||||
type ChannelPlugin,
|
||||
} from "openclaw/plugin-sdk/imessage";
|
||||
import {
|
||||
listIMessageAccountIds,
|
||||
resolveDefaultIMessageAccountId,
|
||||
resolveIMessageAccount,
|
||||
type ResolvedIMessageAccount,
|
||||
} from "./accounts.js";
|
||||
import { createIMessageSetupWizardProxy, imessageSetupAdapter } from "./setup-core.js";
|
||||
import { type ChannelPlugin } from "openclaw/plugin-sdk/imessage";
|
||||
import { type ResolvedIMessageAccount } from "./accounts.js";
|
||||
import { imessageSetupAdapter } from "./setup-core.js";
|
||||
import { createIMessagePluginBase, imessageSetupWizard } from "./shared.js";
|
||||
|
||||
async function loadIMessageChannelRuntime() {
|
||||
return await import("./channel.runtime.js");
|
||||
}
|
||||
|
||||
const imessageSetupWizard = createIMessageSetupWizardProxy(async () => ({
|
||||
imessageSetupWizard: (await loadIMessageChannelRuntime()).imessageSetupWizard,
|
||||
}));
|
||||
|
||||
export const imessageSetupPlugin: ChannelPlugin<ResolvedIMessageAccount> = {
|
||||
id: "imessage",
|
||||
meta: {
|
||||
...getChatChannelMeta("imessage"),
|
||||
aliases: ["imsg"],
|
||||
showConfigured: false,
|
||||
export const imessageSetupPlugin: ChannelPlugin<ResolvedIMessageAccount> = createIMessagePluginBase(
|
||||
{
|
||||
setupWizard: imessageSetupWizard,
|
||||
setup: imessageSetupAdapter,
|
||||
},
|
||||
setupWizard: imessageSetupWizard,
|
||||
capabilities: {
|
||||
chatTypes: ["direct", "group"],
|
||||
media: true,
|
||||
},
|
||||
reload: { configPrefixes: ["channels.imessage"] },
|
||||
configSchema: buildChannelConfigSchema(IMessageConfigSchema),
|
||||
config: {
|
||||
listAccountIds: (cfg) => listIMessageAccountIds(cfg),
|
||||
resolveAccount: (cfg, accountId) => resolveIMessageAccount({ cfg, accountId }),
|
||||
defaultAccountId: (cfg) => resolveDefaultIMessageAccountId(cfg),
|
||||
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
||||
setAccountEnabledInConfigSection({
|
||||
cfg,
|
||||
sectionKey: "imessage",
|
||||
accountId,
|
||||
enabled,
|
||||
allowTopLevel: true,
|
||||
}),
|
||||
deleteAccount: ({ cfg, accountId }) =>
|
||||
deleteAccountFromConfigSection({
|
||||
cfg,
|
||||
sectionKey: "imessage",
|
||||
accountId,
|
||||
clearBaseFields: ["cliPath", "dbPath", "service", "region", "name"],
|
||||
}),
|
||||
isConfigured: (account) => account.configured,
|
||||
describeAccount: (account) => ({
|
||||
accountId: account.accountId,
|
||||
name: account.name,
|
||||
enabled: account.enabled,
|
||||
configured: account.configured,
|
||||
}),
|
||||
resolveAllowFrom: ({ cfg, accountId }) => resolveIMessageConfigAllowFrom({ cfg, accountId }),
|
||||
formatAllowFrom: ({ allowFrom }) => formatTrimmedAllowFromEntries(allowFrom),
|
||||
resolveDefaultTo: ({ cfg, accountId }) => resolveIMessageConfigDefaultTo({ cfg, accountId }),
|
||||
},
|
||||
security: {
|
||||
resolveDmPolicy: ({ cfg, accountId, account }) =>
|
||||
buildAccountScopedDmSecurityPolicy({
|
||||
cfg,
|
||||
channelKey: "imessage",
|
||||
accountId,
|
||||
fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
|
||||
policy: account.config.dmPolicy,
|
||||
allowFrom: account.config.allowFrom ?? [],
|
||||
policyPathSuffix: "dmPolicy",
|
||||
}),
|
||||
collectWarnings: ({ account, cfg }) =>
|
||||
collectAllowlistProviderRestrictSendersWarnings({
|
||||
cfg,
|
||||
providerConfigPresent: cfg.channels?.imessage !== undefined,
|
||||
configuredGroupPolicy: account.config.groupPolicy,
|
||||
surface: "iMessage groups",
|
||||
openScope: "any member",
|
||||
groupPolicyPath: "channels.imessage.groupPolicy",
|
||||
groupAllowFromPath: "channels.imessage.groupAllowFrom",
|
||||
mentionGated: false,
|
||||
}),
|
||||
},
|
||||
setup: imessageSetupAdapter,
|
||||
};
|
||||
);
|
||||
|
||||
@ -1,50 +1,25 @@
|
||||
import {
|
||||
buildAccountScopedAllowlistConfigEditor,
|
||||
buildAccountScopedDmSecurityPolicy,
|
||||
collectAllowlistProviderRestrictSendersWarnings,
|
||||
} from "openclaw/plugin-sdk/compat";
|
||||
import { buildAccountScopedAllowlistConfigEditor } from "openclaw/plugin-sdk/compat";
|
||||
import { buildAgentSessionKey, type RoutePeer } from "openclaw/plugin-sdk/core";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
collectStatusIssuesFromLastError,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
deleteAccountFromConfigSection,
|
||||
formatTrimmedAllowFromEntries,
|
||||
getChatChannelMeta,
|
||||
IMessageConfigSchema,
|
||||
looksLikeIMessageTargetId,
|
||||
normalizeIMessageMessagingTarget,
|
||||
PAIRING_APPROVED_MESSAGE,
|
||||
resolveChannelMediaMaxBytes,
|
||||
resolveIMessageConfigAllowFrom,
|
||||
resolveIMessageConfigDefaultTo,
|
||||
resolveIMessageGroupRequireMention,
|
||||
resolveIMessageGroupToolPolicy,
|
||||
setAccountEnabledInConfigSection,
|
||||
type ChannelPlugin,
|
||||
} from "openclaw/plugin-sdk/imessage";
|
||||
import { resolveOutboundSendDep } from "../../../src/infra/outbound/send-deps.js";
|
||||
import { buildPassiveProbedChannelStatusSummary } from "../../shared/channel-status-summary.js";
|
||||
import {
|
||||
listIMessageAccountIds,
|
||||
resolveDefaultIMessageAccountId,
|
||||
resolveIMessageAccount,
|
||||
type ResolvedIMessageAccount,
|
||||
} from "./accounts.js";
|
||||
import { resolveIMessageAccount, type ResolvedIMessageAccount } from "./accounts.js";
|
||||
import { getIMessageRuntime } from "./runtime.js";
|
||||
import { createIMessageSetupWizardProxy, imessageSetupAdapter } from "./setup-core.js";
|
||||
import { imessageSetupAdapter } from "./setup-core.js";
|
||||
import { createIMessagePluginBase, imessageSetupWizard } from "./shared.js";
|
||||
import { normalizeIMessageHandle, parseIMessageTarget } from "./targets.js";
|
||||
|
||||
const meta = getChatChannelMeta("imessage");
|
||||
|
||||
async function loadIMessageChannelRuntime() {
|
||||
return await import("./channel.runtime.js");
|
||||
}
|
||||
|
||||
const imessageSetupWizard = createIMessageSetupWizardProxy(async () => ({
|
||||
imessageSetupWizard: (await loadIMessageChannelRuntime()).imessageSetupWizard,
|
||||
}));
|
||||
|
||||
type IMessageSendFn = ReturnType<
|
||||
typeof getIMessageRuntime
|
||||
>["channel"]["imessage"]["sendMessageIMessage"];
|
||||
@ -157,55 +132,16 @@ function resolveIMessageOutboundSessionRoute(params: {
|
||||
}
|
||||
|
||||
export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount> = {
|
||||
id: "imessage",
|
||||
meta: {
|
||||
...meta,
|
||||
aliases: ["imsg"],
|
||||
showConfigured: false,
|
||||
},
|
||||
setupWizard: imessageSetupWizard,
|
||||
...createIMessagePluginBase({
|
||||
setupWizard: imessageSetupWizard,
|
||||
setup: imessageSetupAdapter,
|
||||
}),
|
||||
pairing: {
|
||||
idLabel: "imessageSenderId",
|
||||
notifyApproval: async ({ id }) => {
|
||||
await getIMessageRuntime().channel.imessage.sendMessageIMessage(id, PAIRING_APPROVED_MESSAGE);
|
||||
},
|
||||
},
|
||||
capabilities: {
|
||||
chatTypes: ["direct", "group"],
|
||||
media: true,
|
||||
},
|
||||
reload: { configPrefixes: ["channels.imessage"] },
|
||||
configSchema: buildChannelConfigSchema(IMessageConfigSchema),
|
||||
config: {
|
||||
listAccountIds: (cfg) => listIMessageAccountIds(cfg),
|
||||
resolveAccount: (cfg, accountId) => resolveIMessageAccount({ cfg, accountId }),
|
||||
defaultAccountId: (cfg) => resolveDefaultIMessageAccountId(cfg),
|
||||
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
||||
setAccountEnabledInConfigSection({
|
||||
cfg,
|
||||
sectionKey: "imessage",
|
||||
accountId,
|
||||
enabled,
|
||||
allowTopLevel: true,
|
||||
}),
|
||||
deleteAccount: ({ cfg, accountId }) =>
|
||||
deleteAccountFromConfigSection({
|
||||
cfg,
|
||||
sectionKey: "imessage",
|
||||
accountId,
|
||||
clearBaseFields: ["cliPath", "dbPath", "service", "region", "name"],
|
||||
}),
|
||||
isConfigured: (account) => account.configured,
|
||||
describeAccount: (account) => ({
|
||||
accountId: account.accountId,
|
||||
name: account.name,
|
||||
enabled: account.enabled,
|
||||
configured: account.configured,
|
||||
}),
|
||||
resolveAllowFrom: ({ cfg, accountId }) => resolveIMessageConfigAllowFrom({ cfg, accountId }),
|
||||
formatAllowFrom: ({ allowFrom }) => formatTrimmedAllowFromEntries(allowFrom),
|
||||
resolveDefaultTo: ({ cfg, accountId }) => resolveIMessageConfigDefaultTo({ cfg, accountId }),
|
||||
},
|
||||
allowlist: {
|
||||
supportsScope: ({ scope }) => scope === "dm" || scope === "group" || scope === "all",
|
||||
readConfig: ({ cfg, accountId }) => {
|
||||
@ -226,31 +162,6 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount> = {
|
||||
}),
|
||||
}),
|
||||
},
|
||||
security: {
|
||||
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
||||
return buildAccountScopedDmSecurityPolicy({
|
||||
cfg,
|
||||
channelKey: "imessage",
|
||||
accountId,
|
||||
fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
|
||||
policy: account.config.dmPolicy,
|
||||
allowFrom: account.config.allowFrom ?? [],
|
||||
policyPathSuffix: "dmPolicy",
|
||||
});
|
||||
},
|
||||
collectWarnings: ({ account, cfg }) => {
|
||||
return collectAllowlistProviderRestrictSendersWarnings({
|
||||
cfg,
|
||||
providerConfigPresent: cfg.channels?.imessage !== undefined,
|
||||
configuredGroupPolicy: account.config.groupPolicy,
|
||||
surface: "iMessage groups",
|
||||
openScope: "any member",
|
||||
groupPolicyPath: "channels.imessage.groupPolicy",
|
||||
groupAllowFromPath: "channels.imessage.groupAllowFrom",
|
||||
mentionGated: false,
|
||||
});
|
||||
},
|
||||
},
|
||||
groups: {
|
||||
resolveRequireMention: resolveIMessageGroupRequireMention,
|
||||
resolveToolPolicy: resolveIMessageGroupToolPolicy,
|
||||
@ -263,7 +174,6 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount> = {
|
||||
hint: "<handle|chat_id:ID>",
|
||||
},
|
||||
},
|
||||
setup: imessageSetupAdapter,
|
||||
outbound: {
|
||||
deliveryMode: "direct",
|
||||
chunker: (text, limit) => getIMessageRuntime().channel.text.chunkText(text, limit),
|
||||
|
||||
11
extensions/imessage/src/plugin-shared.ts
Normal file
11
extensions/imessage/src/plugin-shared.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { type ChannelPlugin } from "../../../src/plugin-sdk-internal/imessage.js";
|
||||
import { type ResolvedIMessageAccount } from "./accounts.js";
|
||||
import { createIMessageSetupWizardProxy } from "./setup-core.js";
|
||||
|
||||
async function loadIMessageChannelRuntime() {
|
||||
return await import("./channel.runtime.js");
|
||||
}
|
||||
|
||||
export const imessageSetupWizard = createIMessageSetupWizardProxy(async () => ({
|
||||
imessageSetupWizard: (await loadIMessageChannelRuntime()).imessageSetupWizard,
|
||||
})) satisfies NonNullable<ChannelPlugin<ResolvedIMessageAccount>["setupWizard"]>;
|
||||
@ -1,5 +1,7 @@
|
||||
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/compat";
|
||||
import type { PluginRuntime } from "openclaw/plugin-sdk/core";
|
||||
import {
|
||||
createPluginRuntimeStore,
|
||||
type PluginRuntime,
|
||||
} from "../../../src/plugin-sdk-internal/core.js";
|
||||
|
||||
const { setRuntime: setIMessageRuntime, getRuntime: getIMessageRuntime } =
|
||||
createPluginRuntimeStore<PluginRuntime>("iMessage runtime not initialized");
|
||||
|
||||
@ -1,9 +1,5 @@
|
||||
import { createPatchedAccountSetupAdapter } from "../../../src/channels/plugins/setup-helpers.js";
|
||||
import {
|
||||
applyAccountNameToChannelSection,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
formatDocsLink,
|
||||
migrateBaseNameToDefaultAccount,
|
||||
normalizeAccountId,
|
||||
parseSetupEntriesAllowingWildcard,
|
||||
promptParsedAllowFromForScopedChannel,
|
||||
setChannelDmPolicyWithAllowFrom,
|
||||
@ -16,6 +12,7 @@ import type {
|
||||
ChannelSetupDmPolicy,
|
||||
ChannelSetupWizard,
|
||||
} from "../../../src/plugin-sdk-internal/setup.js";
|
||||
import { formatDocsLink } from "../../../src/terminal/links.js";
|
||||
import {
|
||||
listIMessageAccountIds,
|
||||
resolveDefaultIMessageAccountId,
|
||||
@ -98,66 +95,23 @@ async function promptIMessageAllowFrom(params: {
|
||||
});
|
||||
}
|
||||
|
||||
export const imessageSetupAdapter: ChannelSetupAdapter = {
|
||||
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
||||
applyAccountName: ({ cfg, accountId, name }) =>
|
||||
applyAccountNameToChannelSection({
|
||||
cfg,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
name,
|
||||
}),
|
||||
applyAccountConfig: ({ cfg, accountId, input }) => {
|
||||
const namedConfig = applyAccountNameToChannelSection({
|
||||
cfg,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
name: input.name,
|
||||
});
|
||||
const next =
|
||||
accountId !== DEFAULT_ACCOUNT_ID
|
||||
? migrateBaseNameToDefaultAccount({
|
||||
cfg: namedConfig,
|
||||
channelKey: channel,
|
||||
})
|
||||
: namedConfig;
|
||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||
return {
|
||||
...next,
|
||||
channels: {
|
||||
...next.channels,
|
||||
imessage: {
|
||||
...next.channels?.imessage,
|
||||
enabled: true,
|
||||
...buildIMessageSetupPatch(input),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
...next,
|
||||
channels: {
|
||||
...next.channels,
|
||||
imessage: {
|
||||
...next.channels?.imessage,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...next.channels?.imessage?.accounts,
|
||||
[accountId]: {
|
||||
...next.channels?.imessage?.accounts?.[accountId],
|
||||
enabled: true,
|
||||
...buildIMessageSetupPatch(input),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
export const imessageSetupAdapter: ChannelSetupAdapter = createPatchedAccountSetupAdapter({
|
||||
channelKey: channel,
|
||||
buildPatch: (input) => buildIMessageSetupPatch(input),
|
||||
});
|
||||
|
||||
type IMessageSetupWizardHandlers = {
|
||||
resolveStatusLines: NonNullable<ChannelSetupWizard["status"]>["resolveStatusLines"];
|
||||
resolveSelectionHint: NonNullable<ChannelSetupWizard["status"]>["resolveSelectionHint"];
|
||||
resolveQuickstartScore: NonNullable<ChannelSetupWizard["status"]>["resolveQuickstartScore"];
|
||||
shouldPromptCliPath: NonNullable<
|
||||
NonNullable<ChannelSetupWizard["textInputs"]>[number]["shouldPrompt"]
|
||||
>;
|
||||
};
|
||||
|
||||
export function createIMessageSetupWizardProxy(
|
||||
loadWizard: () => Promise<{ imessageSetupWizard: ChannelSetupWizard }>,
|
||||
) {
|
||||
export function createIMessageSetupWizardBase(
|
||||
handlers: IMessageSetupWizardHandlers,
|
||||
): ChannelSetupWizard {
|
||||
const imessageDmPolicy: ChannelSetupDmPolicy = {
|
||||
label: "iMessage",
|
||||
channel,
|
||||
@ -193,12 +147,9 @@ export function createIMessageSetupWizardProxy(
|
||||
account.config.region,
|
||||
);
|
||||
}),
|
||||
resolveStatusLines: async (params) =>
|
||||
(await loadWizard()).imessageSetupWizard.status.resolveStatusLines?.(params) ?? [],
|
||||
resolveSelectionHint: async (params) =>
|
||||
await (await loadWizard()).imessageSetupWizard.status.resolveSelectionHint?.(params),
|
||||
resolveQuickstartScore: async (params) =>
|
||||
await (await loadWizard()).imessageSetupWizard.status.resolveQuickstartScore?.(params),
|
||||
resolveStatusLines: handlers.resolveStatusLines,
|
||||
resolveSelectionHint: handlers.resolveSelectionHint,
|
||||
resolveQuickstartScore: handlers.resolveQuickstartScore,
|
||||
},
|
||||
credentials: [],
|
||||
textInputs: [
|
||||
@ -209,12 +160,7 @@ export function createIMessageSetupWizardProxy(
|
||||
resolveIMessageAccount({ cfg, accountId }).config.cliPath ?? "imsg",
|
||||
currentValue: ({ cfg, accountId }) =>
|
||||
resolveIMessageAccount({ cfg, accountId }).config.cliPath ?? "imsg",
|
||||
shouldPrompt: async (params) => {
|
||||
const input = (await loadWizard()).imessageSetupWizard.textInputs?.find(
|
||||
(entry) => entry.inputKey === "cliPath",
|
||||
);
|
||||
return (await input?.shouldPrompt?.(params)) ?? false;
|
||||
},
|
||||
shouldPrompt: handlers.shouldPromptCliPath,
|
||||
confirmCurrentValue: false,
|
||||
applyCurrentValue: true,
|
||||
helpTitle: "iMessage",
|
||||
@ -235,3 +181,22 @@ export function createIMessageSetupWizardProxy(
|
||||
disable: (cfg: OpenClawConfig) => setSetupChannelEnabled(cfg, channel, false),
|
||||
} satisfies ChannelSetupWizard;
|
||||
}
|
||||
|
||||
export function createIMessageSetupWizardProxy(
|
||||
loadWizard: () => Promise<{ imessageSetupWizard: ChannelSetupWizard }>,
|
||||
) {
|
||||
return createIMessageSetupWizardBase({
|
||||
resolveStatusLines: async (params) =>
|
||||
(await loadWizard()).imessageSetupWizard.status.resolveStatusLines?.(params) ?? [],
|
||||
resolveSelectionHint: async (params) =>
|
||||
await (await loadWizard()).imessageSetupWizard.status.resolveSelectionHint?.(params),
|
||||
resolveQuickstartScore: async (params) =>
|
||||
await (await loadWizard()).imessageSetupWizard.status.resolveQuickstartScore?.(params),
|
||||
shouldPromptCliPath: async (params) => {
|
||||
const input = (await loadWizard()).imessageSetupWizard.textInputs?.find(
|
||||
(entry) => entry.inputKey === "cliPath",
|
||||
);
|
||||
return (await input?.shouldPrompt?.(params)) ?? false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,137 +1,23 @@
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
detectBinary,
|
||||
formatDocsLink,
|
||||
type OpenClawConfig,
|
||||
parseSetupEntriesAllowingWildcard,
|
||||
promptParsedAllowFromForScopedChannel,
|
||||
setChannelDmPolicyWithAllowFrom,
|
||||
setSetupChannelEnabled,
|
||||
type WizardPrompter,
|
||||
} from "../../../src/plugin-sdk-internal/setup.js";
|
||||
import type {
|
||||
ChannelSetupDmPolicy,
|
||||
ChannelSetupWizard,
|
||||
} from "../../../src/plugin-sdk-internal/setup.js";
|
||||
import {
|
||||
listIMessageAccountIds,
|
||||
resolveDefaultIMessageAccountId,
|
||||
resolveIMessageAccount,
|
||||
} from "./accounts.js";
|
||||
import { imessageSetupAdapter, parseIMessageAllowFromEntries } from "./setup-core.js";
|
||||
import { detectBinary } from "../../../src/plugin-sdk-internal/setup.js";
|
||||
import { createIMessageSetupWizardBase, imessageSetupAdapter } from "./setup-core.js";
|
||||
|
||||
const channel = "imessage" as const;
|
||||
|
||||
async function promptIMessageAllowFrom(params: {
|
||||
cfg: OpenClawConfig;
|
||||
prompter: WizardPrompter;
|
||||
accountId?: string;
|
||||
}): Promise<OpenClawConfig> {
|
||||
return promptParsedAllowFromForScopedChannel({
|
||||
cfg: params.cfg,
|
||||
channel,
|
||||
accountId: params.accountId,
|
||||
defaultAccountId: resolveDefaultIMessageAccountId(params.cfg),
|
||||
prompter: params.prompter,
|
||||
noteTitle: "iMessage allowlist",
|
||||
noteLines: [
|
||||
"Allowlist iMessage DMs by handle or chat target.",
|
||||
"Examples:",
|
||||
"- +15555550123",
|
||||
"- user@example.com",
|
||||
"- chat_id:123",
|
||||
"- chat_guid:... or chat_identifier:...",
|
||||
"Multiple entries: comma-separated.",
|
||||
`Docs: ${formatDocsLink("/imessage", "imessage")}`,
|
||||
],
|
||||
message: "iMessage allowFrom (handle or chat_id)",
|
||||
placeholder: "+15555550123, user@example.com, chat_id:123",
|
||||
parseEntries: parseIMessageAllowFromEntries,
|
||||
getExistingAllowFrom: ({ cfg, accountId }) =>
|
||||
resolveIMessageAccount({ cfg, accountId }).config.allowFrom ?? [],
|
||||
});
|
||||
}
|
||||
|
||||
const imessageDmPolicy: ChannelSetupDmPolicy = {
|
||||
label: "iMessage",
|
||||
channel,
|
||||
policyKey: "channels.imessage.dmPolicy",
|
||||
allowFromKey: "channels.imessage.allowFrom",
|
||||
getCurrent: (cfg) => cfg.channels?.imessage?.dmPolicy ?? "pairing",
|
||||
setPolicy: (cfg, policy) =>
|
||||
setChannelDmPolicyWithAllowFrom({
|
||||
cfg,
|
||||
channel,
|
||||
dmPolicy: policy,
|
||||
}),
|
||||
promptAllowFrom: promptIMessageAllowFrom,
|
||||
};
|
||||
|
||||
export const imessageSetupWizard: ChannelSetupWizard = {
|
||||
channel,
|
||||
status: {
|
||||
configuredLabel: "configured",
|
||||
unconfiguredLabel: "needs setup",
|
||||
configuredHint: "imsg found",
|
||||
unconfiguredHint: "imsg missing",
|
||||
configuredScore: 1,
|
||||
unconfiguredScore: 0,
|
||||
resolveConfigured: ({ cfg }) =>
|
||||
listIMessageAccountIds(cfg).some((accountId) => {
|
||||
const account = resolveIMessageAccount({ cfg, accountId });
|
||||
return Boolean(
|
||||
account.config.cliPath ||
|
||||
account.config.dbPath ||
|
||||
account.config.allowFrom ||
|
||||
account.config.service ||
|
||||
account.config.region,
|
||||
);
|
||||
}),
|
||||
resolveStatusLines: async ({ cfg, configured }) => {
|
||||
const cliPath = cfg.channels?.imessage?.cliPath ?? "imsg";
|
||||
const cliDetected = await detectBinary(cliPath);
|
||||
return [
|
||||
`iMessage: ${configured ? "configured" : "needs setup"}`,
|
||||
`imsg: ${cliDetected ? "found" : "missing"} (${cliPath})`,
|
||||
];
|
||||
},
|
||||
resolveSelectionHint: async ({ cfg }) => {
|
||||
const cliPath = cfg.channels?.imessage?.cliPath ?? "imsg";
|
||||
return (await detectBinary(cliPath)) ? "imsg found" : "imsg missing";
|
||||
},
|
||||
resolveQuickstartScore: async ({ cfg }) => {
|
||||
const cliPath = cfg.channels?.imessage?.cliPath ?? "imsg";
|
||||
return (await detectBinary(cliPath)) ? 1 : 0;
|
||||
},
|
||||
export const imessageSetupWizard = createIMessageSetupWizardBase({
|
||||
resolveStatusLines: async ({ cfg, configured }) => {
|
||||
const cliPath = cfg.channels?.imessage?.cliPath ?? "imsg";
|
||||
const cliDetected = await detectBinary(cliPath);
|
||||
return [
|
||||
`iMessage: ${configured ? "configured" : "needs setup"}`,
|
||||
`imsg: ${cliDetected ? "found" : "missing"} (${cliPath})`,
|
||||
];
|
||||
},
|
||||
credentials: [],
|
||||
textInputs: [
|
||||
{
|
||||
inputKey: "cliPath",
|
||||
message: "imsg CLI path",
|
||||
initialValue: ({ cfg, accountId }) =>
|
||||
resolveIMessageAccount({ cfg, accountId }).config.cliPath ?? "imsg",
|
||||
currentValue: ({ cfg, accountId }) =>
|
||||
resolveIMessageAccount({ cfg, accountId }).config.cliPath ?? "imsg",
|
||||
shouldPrompt: async ({ currentValue }) => !(await detectBinary(currentValue ?? "imsg")),
|
||||
confirmCurrentValue: false,
|
||||
applyCurrentValue: true,
|
||||
helpTitle: "iMessage",
|
||||
helpLines: ["imsg CLI path required to enable iMessage."],
|
||||
},
|
||||
],
|
||||
completionNote: {
|
||||
title: "iMessage next steps",
|
||||
lines: [
|
||||
"This is still a work in progress.",
|
||||
"Ensure OpenClaw has Full Disk Access to Messages DB.",
|
||||
"Grant Automation permission for Messages when prompted.",
|
||||
"List chats with: imsg chats --limit 20",
|
||||
`Docs: ${formatDocsLink("/imessage", "imessage")}`,
|
||||
],
|
||||
resolveSelectionHint: async ({ cfg }) => {
|
||||
const cliPath = cfg.channels?.imessage?.cliPath ?? "imsg";
|
||||
return (await detectBinary(cliPath)) ? "imsg found" : "imsg missing";
|
||||
},
|
||||
dmPolicy: imessageDmPolicy,
|
||||
disable: (cfg) => setSetupChannelEnabled(cfg, channel, false),
|
||||
};
|
||||
|
||||
export { imessageSetupAdapter, parseIMessageAllowFromEntries };
|
||||
resolveQuickstartScore: async ({ cfg }) => {
|
||||
const cliPath = cfg.channels?.imessage?.cliPath ?? "imsg";
|
||||
return (await detectBinary(cliPath)) ? 1 : 0;
|
||||
},
|
||||
shouldPromptCliPath: async ({ currentValue }) => !(await detectBinary(currentValue ?? "imsg")),
|
||||
});
|
||||
export { imessageSetupAdapter, parseIMessageAllowFromEntries } from "./setup-core.js";
|
||||
|
||||
119
extensions/imessage/src/shared.ts
Normal file
119
extensions/imessage/src/shared.ts
Normal file
@ -0,0 +1,119 @@
|
||||
import {
|
||||
buildAccountScopedDmSecurityPolicy,
|
||||
collectAllowlistProviderRestrictSendersWarnings,
|
||||
} from "openclaw/plugin-sdk/compat";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
deleteAccountFromConfigSection,
|
||||
formatTrimmedAllowFromEntries,
|
||||
getChatChannelMeta,
|
||||
IMessageConfigSchema,
|
||||
resolveIMessageConfigAllowFrom,
|
||||
resolveIMessageConfigDefaultTo,
|
||||
setAccountEnabledInConfigSection,
|
||||
type ChannelPlugin,
|
||||
} from "openclaw/plugin-sdk/imessage";
|
||||
import {
|
||||
listIMessageAccountIds,
|
||||
resolveDefaultIMessageAccountId,
|
||||
resolveIMessageAccount,
|
||||
type ResolvedIMessageAccount,
|
||||
} from "./accounts.js";
|
||||
import { createIMessageSetupWizardProxy } from "./setup-core.js";
|
||||
|
||||
export const IMESSAGE_CHANNEL = "imessage" as const;
|
||||
|
||||
async function loadIMessageChannelRuntime() {
|
||||
return await import("./channel.runtime.js");
|
||||
}
|
||||
|
||||
export const imessageSetupWizard = createIMessageSetupWizardProxy(async () => ({
|
||||
imessageSetupWizard: (await loadIMessageChannelRuntime()).imessageSetupWizard,
|
||||
}));
|
||||
|
||||
export function createIMessagePluginBase(params: {
|
||||
setupWizard?: NonNullable<ChannelPlugin<ResolvedIMessageAccount>["setupWizard"]>;
|
||||
setup: NonNullable<ChannelPlugin<ResolvedIMessageAccount>["setup"]>;
|
||||
}): Pick<
|
||||
ChannelPlugin<ResolvedIMessageAccount>,
|
||||
| "id"
|
||||
| "meta"
|
||||
| "setupWizard"
|
||||
| "capabilities"
|
||||
| "reload"
|
||||
| "configSchema"
|
||||
| "config"
|
||||
| "security"
|
||||
| "setup"
|
||||
> {
|
||||
return {
|
||||
id: IMESSAGE_CHANNEL,
|
||||
meta: {
|
||||
...getChatChannelMeta(IMESSAGE_CHANNEL),
|
||||
aliases: ["imsg"],
|
||||
showConfigured: false,
|
||||
},
|
||||
setupWizard: params.setupWizard,
|
||||
capabilities: {
|
||||
chatTypes: ["direct", "group"],
|
||||
media: true,
|
||||
},
|
||||
reload: { configPrefixes: ["channels.imessage"] },
|
||||
configSchema: buildChannelConfigSchema(IMessageConfigSchema),
|
||||
config: {
|
||||
listAccountIds: (cfg) => listIMessageAccountIds(cfg),
|
||||
resolveAccount: (cfg, accountId) => resolveIMessageAccount({ cfg, accountId }),
|
||||
defaultAccountId: (cfg) => resolveDefaultIMessageAccountId(cfg),
|
||||
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
||||
setAccountEnabledInConfigSection({
|
||||
cfg,
|
||||
sectionKey: IMESSAGE_CHANNEL,
|
||||
accountId,
|
||||
enabled,
|
||||
allowTopLevel: true,
|
||||
}),
|
||||
deleteAccount: ({ cfg, accountId }) =>
|
||||
deleteAccountFromConfigSection({
|
||||
cfg,
|
||||
sectionKey: IMESSAGE_CHANNEL,
|
||||
accountId,
|
||||
clearBaseFields: ["cliPath", "dbPath", "service", "region", "name"],
|
||||
}),
|
||||
isConfigured: (account) => account.configured,
|
||||
describeAccount: (account) => ({
|
||||
accountId: account.accountId,
|
||||
name: account.name,
|
||||
enabled: account.enabled,
|
||||
configured: account.configured,
|
||||
}),
|
||||
resolveAllowFrom: ({ cfg, accountId }) => resolveIMessageConfigAllowFrom({ cfg, accountId }),
|
||||
formatAllowFrom: ({ allowFrom }) => formatTrimmedAllowFromEntries(allowFrom),
|
||||
resolveDefaultTo: ({ cfg, accountId }) => resolveIMessageConfigDefaultTo({ cfg, accountId }),
|
||||
},
|
||||
security: {
|
||||
resolveDmPolicy: ({ cfg, accountId, account }) =>
|
||||
buildAccountScopedDmSecurityPolicy({
|
||||
cfg,
|
||||
channelKey: IMESSAGE_CHANNEL,
|
||||
accountId,
|
||||
fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
|
||||
policy: account.config.dmPolicy,
|
||||
allowFrom: account.config.allowFrom ?? [],
|
||||
policyPathSuffix: "dmPolicy",
|
||||
}),
|
||||
collectWarnings: ({ account, cfg }) =>
|
||||
collectAllowlistProviderRestrictSendersWarnings({
|
||||
cfg,
|
||||
providerConfigPresent: cfg.channels?.imessage !== undefined,
|
||||
configuredGroupPolicy: account.config.groupPolicy,
|
||||
surface: "iMessage groups",
|
||||
openScope: "any member",
|
||||
groupPolicyPath: "channels.imessage.groupPolicy",
|
||||
groupAllowFromPath: "channels.imessage.groupAllowFrom",
|
||||
mentionGated: false,
|
||||
}),
|
||||
},
|
||||
setup: params.setup,
|
||||
};
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { isAllowedParsedChatSender } from "../../../src/plugin-sdk/allow-from.js";
|
||||
import { isAllowedParsedChatSender } from "../../../src/plugin-sdk-internal/imessage.js";
|
||||
|
||||
export type ServicePrefix<TService extends string> = { prefix: string; service: TService };
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import {
|
||||
applyAccountNameToChannelSection,
|
||||
patchScopedAccountConfig,
|
||||
prepareScopedSetupConfig,
|
||||
} from "../../../src/channels/plugins/setup-helpers.js";
|
||||
import {
|
||||
setTopLevelChannelAllowFrom,
|
||||
@ -100,7 +100,7 @@ export function setIrcGroupAccess(
|
||||
export const ircSetupAdapter: ChannelSetupAdapter = {
|
||||
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
||||
applyAccountName: ({ cfg, accountId, name }) =>
|
||||
applyAccountNameToChannelSection({
|
||||
prepareScopedSetupConfig({
|
||||
cfg,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
@ -118,7 +118,7 @@ export const ircSetupAdapter: ChannelSetupAdapter = {
|
||||
},
|
||||
applyAccountConfig: ({ cfg, accountId, input }) => {
|
||||
const setupInput = input as IrcSetupInput;
|
||||
const namedConfig = applyAccountNameToChannelSection({
|
||||
const namedConfig = prepareScopedSetupConfig({
|
||||
cfg,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
|
||||
@ -3,11 +3,9 @@ import {
|
||||
createKilocodeWrapper,
|
||||
isProxyReasoningUnsupported,
|
||||
} from "../../src/agents/pi-embedded-runner/proxy-stream-wrappers.js";
|
||||
import {
|
||||
applyKilocodeConfig,
|
||||
KILOCODE_DEFAULT_MODEL_REF,
|
||||
} from "../../src/commands/onboard-auth.js";
|
||||
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
|
||||
import { applyKilocodeConfig, KILOCODE_DEFAULT_MODEL_REF } from "./onboard.js";
|
||||
import { buildSingleProviderApiKeyCatalog } from "../../src/plugins/provider-catalog.js";
|
||||
import { buildKilocodeProviderWithDiscovery } from "./provider-catalog.js";
|
||||
|
||||
const PROVIDER_ID = "kilocode";
|
||||
@ -47,18 +45,12 @@ const kilocodePlugin = {
|
||||
],
|
||||
catalog: {
|
||||
order: "simple",
|
||||
run: async (ctx) => {
|
||||
const apiKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey;
|
||||
if (!apiKey) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
provider: {
|
||||
...(await buildKilocodeProviderWithDiscovery()),
|
||||
apiKey,
|
||||
},
|
||||
};
|
||||
},
|
||||
run: (ctx) =>
|
||||
buildSingleProviderApiKeyCatalog({
|
||||
ctx,
|
||||
providerId: PROVIDER_ID,
|
||||
buildProvider: buildKilocodeProviderWithDiscovery,
|
||||
}),
|
||||
},
|
||||
capabilities: {
|
||||
geminiThoughtSignatureSanitization: true,
|
||||
|
||||
35
extensions/kilocode/onboard.ts
Normal file
35
extensions/kilocode/onboard.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import {
|
||||
applyAgentDefaultModelPrimary,
|
||||
applyProviderConfigWithModelCatalog,
|
||||
} from "../../src/commands/onboard-auth.config-shared.js";
|
||||
import type { OpenClawConfig } from "../../src/config/config.js";
|
||||
import {
|
||||
KILOCODE_BASE_URL,
|
||||
KILOCODE_DEFAULT_MODEL_REF,
|
||||
} from "../../src/providers/kilocode-shared.js";
|
||||
import { buildKilocodeProvider } from "./provider-catalog.js";
|
||||
|
||||
export { KILOCODE_BASE_URL, KILOCODE_DEFAULT_MODEL_REF };
|
||||
|
||||
export function applyKilocodeProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
models[KILOCODE_DEFAULT_MODEL_REF] = {
|
||||
...models[KILOCODE_DEFAULT_MODEL_REF],
|
||||
alias: models[KILOCODE_DEFAULT_MODEL_REF]?.alias ?? "Kilo Gateway",
|
||||
};
|
||||
|
||||
return applyProviderConfigWithModelCatalog(cfg, {
|
||||
agentModels: models,
|
||||
providerId: "kilocode",
|
||||
api: "openai-completions",
|
||||
baseUrl: KILOCODE_BASE_URL,
|
||||
catalogModels: buildKilocodeProvider().models ?? [],
|
||||
});
|
||||
}
|
||||
|
||||
export function applyKilocodeConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
return applyAgentDefaultModelPrimary(
|
||||
applyKilocodeProviderConfig(cfg),
|
||||
KILOCODE_DEFAULT_MODEL_REF,
|
||||
);
|
||||
}
|
||||
@ -1,47 +1,69 @@
|
||||
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
||||
import { applyKimiCodeConfig, KIMI_CODING_MODEL_REF } from "../../src/commands/onboard-auth.js";
|
||||
import {
|
||||
emptyPluginConfigSchema,
|
||||
type OpenClawPluginApi,
|
||||
type ProviderRuntimeModel,
|
||||
} from "openclaw/plugin-sdk/core";
|
||||
import { findNormalizedProviderValue, normalizeProviderId } from "../../src/agents/provider-id.js";
|
||||
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
|
||||
import { isRecord } from "../../src/utils.js";
|
||||
import { buildKimiCodingProvider } from "./provider-catalog.js";
|
||||
import { applyKimiCodeConfig, KIMI_DEFAULT_MODEL_REF } from "./onboard.js";
|
||||
import {
|
||||
buildKimiProvider,
|
||||
KIMI_DEFAULT_MODEL_ID,
|
||||
KIMI_LEGACY_MODEL_ID,
|
||||
KIMI_UPSTREAM_MODEL_ID,
|
||||
} from "./provider-catalog.js";
|
||||
|
||||
const PROVIDER_ID = "kimi-coding";
|
||||
const PROVIDER_ID = "kimi";
|
||||
const KIMI_TRANSPORT_MODEL_IDS = new Set([KIMI_DEFAULT_MODEL_ID, KIMI_LEGACY_MODEL_ID]);
|
||||
|
||||
function normalizeKimiTransportModel(model: ProviderRuntimeModel): ProviderRuntimeModel {
|
||||
if (!KIMI_TRANSPORT_MODEL_IDS.has(model.id)) {
|
||||
return model;
|
||||
}
|
||||
return {
|
||||
...model,
|
||||
id: KIMI_UPSTREAM_MODEL_ID,
|
||||
name: "Kimi Code",
|
||||
};
|
||||
}
|
||||
|
||||
const kimiCodingPlugin = {
|
||||
id: PROVIDER_ID,
|
||||
name: "Kimi Coding Provider",
|
||||
description: "Bundled Kimi Coding provider plugin",
|
||||
name: "Kimi Code Provider",
|
||||
description: "Bundled Kimi Code provider plugin",
|
||||
configSchema: emptyPluginConfigSchema(),
|
||||
register(api: OpenClawPluginApi) {
|
||||
api.registerProvider({
|
||||
id: PROVIDER_ID,
|
||||
label: "Kimi Coding",
|
||||
aliases: ["kimi-code"],
|
||||
label: "Kimi Code",
|
||||
aliases: ["kimi-code", "kimi-coding"],
|
||||
docsPath: "/providers/moonshot",
|
||||
envVars: ["KIMI_API_KEY", "KIMICODE_API_KEY"],
|
||||
auth: [
|
||||
createProviderApiKeyAuthMethod({
|
||||
providerId: PROVIDER_ID,
|
||||
methodId: "api-key",
|
||||
label: "Kimi Code API key (subscription)",
|
||||
hint: "Kimi K2.5 + Kimi Coding",
|
||||
label: "Kimi Code API key",
|
||||
hint: "Dedicated coding endpoint",
|
||||
optionKey: "kimiCodeApiKey",
|
||||
flagName: "--kimi-code-api-key",
|
||||
envVar: "KIMI_API_KEY",
|
||||
promptMessage: "Enter Kimi Coding API key",
|
||||
defaultModel: KIMI_CODING_MODEL_REF,
|
||||
expectedProviders: ["kimi-code", "kimi-coding"],
|
||||
promptMessage: "Enter Kimi Code API key",
|
||||
defaultModel: KIMI_DEFAULT_MODEL_REF,
|
||||
expectedProviders: ["kimi", "kimi-code", "kimi-coding"],
|
||||
applyConfig: (cfg) => applyKimiCodeConfig(cfg),
|
||||
noteMessage: [
|
||||
"Kimi Coding uses a dedicated endpoint and API key.",
|
||||
"Kimi Code uses a dedicated coding endpoint and API key.",
|
||||
"Get your API key at: https://www.kimi.com/code/en",
|
||||
].join("\n"),
|
||||
noteTitle: "Kimi Coding",
|
||||
noteTitle: "Kimi Code",
|
||||
wizard: {
|
||||
choiceId: "kimi-code-api-key",
|
||||
choiceLabel: "Kimi Code API key (subscription)",
|
||||
groupId: "moonshot",
|
||||
groupLabel: "Moonshot AI (Kimi K2.5)",
|
||||
groupHint: "Kimi K2.5 + Kimi Coding",
|
||||
choiceLabel: "Kimi Code API key",
|
||||
groupId: "kimi-code",
|
||||
groupLabel: "Kimi Code",
|
||||
groupHint: "Dedicated coding endpoint",
|
||||
},
|
||||
}),
|
||||
],
|
||||
@ -52,8 +74,11 @@ const kimiCodingPlugin = {
|
||||
if (!apiKey) {
|
||||
return null;
|
||||
}
|
||||
const explicitProvider = ctx.config.models?.providers?.[PROVIDER_ID];
|
||||
const builtInProvider = buildKimiCodingProvider();
|
||||
const explicitProvider = findNormalizedProviderValue(
|
||||
ctx.config.models?.providers,
|
||||
PROVIDER_ID,
|
||||
);
|
||||
const builtInProvider = buildKimiProvider();
|
||||
const explicitBaseUrl =
|
||||
typeof explicitProvider?.baseUrl === "string" ? explicitProvider.baseUrl.trim() : "";
|
||||
const explicitHeaders = isRecord(explicitProvider?.headers)
|
||||
@ -79,6 +104,12 @@ const kimiCodingPlugin = {
|
||||
capabilities: {
|
||||
preserveAnthropicThinkingSignatures: false,
|
||||
},
|
||||
normalizeResolvedModel: (ctx) => {
|
||||
if (normalizeProviderId(ctx.provider) !== PROVIDER_ID) {
|
||||
return undefined;
|
||||
}
|
||||
return normalizeKimiTransportModel(ctx.model);
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
44
extensions/kimi-coding/onboard.ts
Normal file
44
extensions/kimi-coding/onboard.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import {
|
||||
applyAgentDefaultModelPrimary,
|
||||
applyProviderConfigWithModelCatalog,
|
||||
} from "../../src/commands/onboard-auth.config-shared.js";
|
||||
import type { OpenClawConfig } from "../../src/config/config.js";
|
||||
import {
|
||||
buildKimiCodingProvider,
|
||||
KIMI_BASE_URL,
|
||||
KIMI_DEFAULT_MODEL_ID,
|
||||
KIMI_LEGACY_MODEL_ID,
|
||||
} from "./provider-catalog.js";
|
||||
|
||||
export const KIMI_DEFAULT_MODEL_REF = `kimi/${KIMI_DEFAULT_MODEL_ID}`;
|
||||
export const KIMI_LEGACY_MODEL_REF = `kimi/${KIMI_LEGACY_MODEL_ID}`;
|
||||
export const KIMI_CODING_MODEL_REF = KIMI_DEFAULT_MODEL_REF;
|
||||
|
||||
export function applyKimiCodeProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
models[KIMI_DEFAULT_MODEL_REF] = {
|
||||
...models[KIMI_DEFAULT_MODEL_REF],
|
||||
alias: models[KIMI_DEFAULT_MODEL_REF]?.alias ?? "Kimi Code",
|
||||
};
|
||||
models[KIMI_LEGACY_MODEL_REF] = {
|
||||
...models[KIMI_LEGACY_MODEL_REF],
|
||||
alias: models[KIMI_LEGACY_MODEL_REF]?.alias ?? "Kimi Code",
|
||||
};
|
||||
|
||||
const catalog = buildKimiCodingProvider().models ?? [];
|
||||
if (catalog.length === 0) {
|
||||
return cfg;
|
||||
}
|
||||
|
||||
return applyProviderConfigWithModelCatalog(cfg, {
|
||||
agentModels: models,
|
||||
providerId: "kimi",
|
||||
api: "anthropic-messages",
|
||||
baseUrl: KIMI_BASE_URL,
|
||||
catalogModels: catalog,
|
||||
});
|
||||
}
|
||||
|
||||
export function applyKimiCodeConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
return applyAgentDefaultModelPrimary(applyKimiCodeProviderConfig(cfg), KIMI_DEFAULT_MODEL_REF);
|
||||
}
|
||||
@ -1,22 +1,23 @@
|
||||
{
|
||||
"id": "kimi-coding",
|
||||
"providers": ["kimi-coding"],
|
||||
"id": "kimi",
|
||||
"providers": ["kimi", "kimi-coding"],
|
||||
"providerAuthEnvVars": {
|
||||
"kimi": ["KIMI_API_KEY", "KIMICODE_API_KEY"],
|
||||
"kimi-coding": ["KIMI_API_KEY", "KIMICODE_API_KEY"]
|
||||
},
|
||||
"providerAuthChoices": [
|
||||
{
|
||||
"provider": "kimi-coding",
|
||||
"provider": "kimi",
|
||||
"method": "api-key",
|
||||
"choiceId": "kimi-code-api-key",
|
||||
"choiceLabel": "Kimi Code API key (subscription)",
|
||||
"groupId": "moonshot",
|
||||
"groupLabel": "Moonshot AI (Kimi K2.5)",
|
||||
"groupHint": "Kimi K2.5 + Kimi Coding",
|
||||
"choiceLabel": "Kimi Code API key",
|
||||
"groupId": "kimi-code",
|
||||
"groupLabel": "Kimi Code",
|
||||
"groupHint": "Dedicated coding endpoint",
|
||||
"optionKey": "kimiCodeApiKey",
|
||||
"cliFlag": "--kimi-code-api-key",
|
||||
"cliOption": "--kimi-code-api-key <key>",
|
||||
"cliDescription": "Kimi Coding API key"
|
||||
"cliDescription": "Kimi Code API key"
|
||||
}
|
||||
],
|
||||
"configSchema": {
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "@openclaw/kimi-coding-provider",
|
||||
"name": "@openclaw/kimi-provider",
|
||||
"version": "2026.3.14",
|
||||
"private": true,
|
||||
"description": "OpenClaw Kimi Coding provider plugin",
|
||||
"description": "OpenClaw Kimi provider plugin",
|
||||
"type": "module",
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import type { ModelProviderConfig } from "../../src/config/types.models.js";
|
||||
|
||||
export const KIMI_CODING_BASE_URL = "https://api.kimi.com/coding/";
|
||||
export const KIMI_BASE_URL = "https://api.kimi.com/coding/";
|
||||
const KIMI_CODING_USER_AGENT = "claude-code/0.1.0";
|
||||
export const KIMI_CODING_DEFAULT_MODEL_ID = "k2p5";
|
||||
export const KIMI_DEFAULT_MODEL_ID = "kimi-code";
|
||||
export const KIMI_UPSTREAM_MODEL_ID = "kimi-for-coding";
|
||||
export const KIMI_LEGACY_MODEL_ID = "k2p5";
|
||||
const KIMI_CODING_DEFAULT_CONTEXT_WINDOW = 262144;
|
||||
const KIMI_CODING_DEFAULT_MAX_TOKENS = 32768;
|
||||
const KIMI_CODING_DEFAULT_COST = {
|
||||
@ -14,15 +16,24 @@ const KIMI_CODING_DEFAULT_COST = {
|
||||
|
||||
export function buildKimiCodingProvider(): ModelProviderConfig {
|
||||
return {
|
||||
baseUrl: KIMI_CODING_BASE_URL,
|
||||
baseUrl: KIMI_BASE_URL,
|
||||
api: "anthropic-messages",
|
||||
headers: {
|
||||
"User-Agent": KIMI_CODING_USER_AGENT,
|
||||
},
|
||||
models: [
|
||||
{
|
||||
id: KIMI_CODING_DEFAULT_MODEL_ID,
|
||||
name: "Kimi for Coding",
|
||||
id: KIMI_DEFAULT_MODEL_ID,
|
||||
name: "Kimi Code",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: KIMI_CODING_DEFAULT_COST,
|
||||
contextWindow: KIMI_CODING_DEFAULT_CONTEXT_WINDOW,
|
||||
maxTokens: KIMI_CODING_DEFAULT_MAX_TOKENS,
|
||||
},
|
||||
{
|
||||
id: KIMI_LEGACY_MODEL_ID,
|
||||
name: "Kimi Code (legacy model id)",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: KIMI_CODING_DEFAULT_COST,
|
||||
@ -32,3 +43,8 @@ export function buildKimiCodingProvider(): ModelProviderConfig {
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export const KIMI_CODING_BASE_URL = KIMI_BASE_URL;
|
||||
export const KIMI_CODING_DEFAULT_MODEL_ID = KIMI_DEFAULT_MODEL_ID;
|
||||
export const KIMI_CODING_LEGACY_MODEL_ID = KIMI_LEGACY_MODEL_ID;
|
||||
export const buildKimiProvider = buildKimiCodingProvider;
|
||||
|
||||
@ -45,6 +45,7 @@ function fakeApi(overrides: Partial<OpenClawPluginApi> = {}): OpenClawPluginApi
|
||||
registerService() {},
|
||||
registerProvider() {},
|
||||
registerSpeechProvider() {},
|
||||
registerMediaUnderstandingProvider() {},
|
||||
registerWebSearchProvider() {},
|
||||
registerInteractiveHandler() {},
|
||||
registerHook() {},
|
||||
|
||||
@ -1,10 +1,7 @@
|
||||
import {
|
||||
applyAccountNameToChannelSection,
|
||||
migrateBaseNameToDefaultAccount,
|
||||
} from "../../../src/channels/plugins/setup-helpers.js";
|
||||
import { prepareScopedSetupConfig } from "../../../src/channels/plugins/setup-helpers.js";
|
||||
import type { ChannelSetupAdapter } from "../../../src/channels/plugins/types.adapters.js";
|
||||
import { normalizeSecretInputString } from "../../../src/config/types.secrets.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../src/routing/session-key.js";
|
||||
import { normalizeAccountId } from "../../../src/routing/session-key.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
|
||||
const channel = "matrix" as const;
|
||||
@ -44,12 +41,12 @@ export function buildMatrixConfigUpdate(
|
||||
export const matrixSetupAdapter: ChannelSetupAdapter = {
|
||||
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
||||
applyAccountName: ({ cfg, accountId, name }) =>
|
||||
applyAccountNameToChannelSection({
|
||||
prepareScopedSetupConfig({
|
||||
cfg: cfg as CoreConfig,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
name,
|
||||
}),
|
||||
}) as CoreConfig,
|
||||
validateInput: ({ input }) => {
|
||||
if (input.useEnv) {
|
||||
return null;
|
||||
@ -74,19 +71,13 @@ export const matrixSetupAdapter: ChannelSetupAdapter = {
|
||||
return null;
|
||||
},
|
||||
applyAccountConfig: ({ cfg, accountId, input }) => {
|
||||
const namedConfig = applyAccountNameToChannelSection({
|
||||
const next = prepareScopedSetupConfig({
|
||||
cfg: cfg as CoreConfig,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
name: input.name,
|
||||
});
|
||||
const next =
|
||||
accountId !== DEFAULT_ACCOUNT_ID
|
||||
? migrateBaseNameToDefaultAccount({
|
||||
cfg: namedConfig,
|
||||
channelKey: channel,
|
||||
})
|
||||
: namedConfig;
|
||||
migrateBaseName: true,
|
||||
}) as CoreConfig;
|
||||
if (input.useEnv) {
|
||||
return {
|
||||
...next,
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
import {
|
||||
addWildcardAllowFrom,
|
||||
buildSingleChannelSecretPromptState,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
formatDocsLink,
|
||||
formatResolvedUnresolvedNote,
|
||||
hasConfiguredSecretInput,
|
||||
mergeAllowFromEntries,
|
||||
promptSingleChannelSecretInput,
|
||||
setTopLevelChannelGroupPolicy,
|
||||
} from "../../../src/channels/plugins/setup-wizard-helpers.js";
|
||||
import type { ChannelSetupDmPolicy } from "../../../src/channels/plugins/setup-wizard-types.js";
|
||||
import type { ChannelSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import type { DmPolicy } from "../../../src/config/types.js";
|
||||
import type { SecretInput } from "../../../src/config/types.secrets.js";
|
||||
import { hasConfiguredSecretInput } from "../../../src/config/types.secrets.js";
|
||||
import { formatResolvedUnresolvedNote } from "../../../src/plugin-sdk/resolution-notes.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js";
|
||||
import { formatDocsLink } from "../../../src/terminal/links.js";
|
||||
import type { WizardPrompter } from "../../../src/wizard/prompts.js";
|
||||
type ChannelSetupDmPolicy,
|
||||
type ChannelSetupWizard,
|
||||
type DmPolicy,
|
||||
type OpenClawConfig,
|
||||
type SecretInput,
|
||||
type WizardPrompter,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import { listMatrixDirectoryGroupsLive } from "./directory-live.js";
|
||||
import { resolveMatrixAccount } from "./matrix/accounts.js";
|
||||
import { ensureMatrixSdkInstalled, isMatrixSdkAvailable } from "./matrix/deps.js";
|
||||
|
||||
@ -1,12 +1,9 @@
|
||||
import {
|
||||
applyAccountNameToChannelSection,
|
||||
applySetupAccountConfigPatch,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
hasConfiguredSecretInput,
|
||||
migrateBaseNameToDefaultAccount,
|
||||
normalizeAccountId,
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/mattermost";
|
||||
import { createPatchedAccountSetupAdapter } from "../../../src/channels/plugins/setup-helpers.js";
|
||||
import type { ChannelSetupAdapter } from "../../../src/channels/plugins/types.adapters.js";
|
||||
import { resolveMattermostAccount, type ResolvedMattermostAccount } from "./mattermost/accounts.js";
|
||||
import { normalizeMattermostBaseUrl } from "./mattermost/client.js";
|
||||
@ -27,15 +24,8 @@ export function resolveMattermostAccountWithSecrets(cfg: OpenClawConfig, account
|
||||
});
|
||||
}
|
||||
|
||||
export const mattermostSetupAdapter: ChannelSetupAdapter = {
|
||||
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
||||
applyAccountName: ({ cfg, accountId, name }) =>
|
||||
applyAccountNameToChannelSection({
|
||||
cfg,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
name,
|
||||
}),
|
||||
export const mattermostSetupAdapter: ChannelSetupAdapter = createPatchedAccountSetupAdapter({
|
||||
channelKey: channel,
|
||||
validateInput: ({ accountId, input }) => {
|
||||
const token = input.botToken ?? input.token;
|
||||
const baseUrl = normalizeMattermostBaseUrl(input.httpUrl);
|
||||
@ -50,32 +40,14 @@ export const mattermostSetupAdapter: ChannelSetupAdapter = {
|
||||
}
|
||||
return null;
|
||||
},
|
||||
applyAccountConfig: ({ cfg, accountId, input }) => {
|
||||
buildPatch: (input) => {
|
||||
const token = input.botToken ?? input.token;
|
||||
const baseUrl = normalizeMattermostBaseUrl(input.httpUrl);
|
||||
const namedConfig = applyAccountNameToChannelSection({
|
||||
cfg,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
name: input.name,
|
||||
});
|
||||
const next =
|
||||
accountId !== DEFAULT_ACCOUNT_ID
|
||||
? migrateBaseNameToDefaultAccount({
|
||||
cfg: namedConfig,
|
||||
channelKey: channel,
|
||||
})
|
||||
: namedConfig;
|
||||
return applySetupAccountConfigPatch({
|
||||
cfg: next,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
patch: input.useEnv
|
||||
? {}
|
||||
: {
|
||||
...(token ? { botToken: token } : {}),
|
||||
...(baseUrl ? { baseUrl } : {}),
|
||||
},
|
||||
});
|
||||
return input.useEnv
|
||||
? {}
|
||||
: {
|
||||
...(token ? { botToken: token } : {}),
|
||||
...(baseUrl ? { baseUrl } : {}),
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@ -8,13 +8,14 @@ import {
|
||||
} from "openclaw/plugin-sdk/minimax-portal-auth";
|
||||
import { ensureAuthProfileStore, listProfilesForProvider } from "../../src/agents/auth-profiles.js";
|
||||
import { MINIMAX_OAUTH_MARKER } from "../../src/agents/model-auth-markers.js";
|
||||
import {
|
||||
applyMinimaxApiConfig,
|
||||
applyMinimaxApiConfigCn,
|
||||
} from "../../src/commands/onboard-auth.config-minimax.js";
|
||||
import { fetchMinimaxUsage } from "../../src/infra/provider-usage.fetch.js";
|
||||
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
|
||||
import {
|
||||
minimaxMediaUnderstandingProvider,
|
||||
minimaxPortalMediaUnderstandingProvider,
|
||||
} from "./media-understanding-provider.js";
|
||||
import { loginMiniMaxPortalOAuth, type MiniMaxRegion } from "./oauth.js";
|
||||
import { applyMinimaxApiConfig, applyMinimaxApiConfigCn } from "./onboard.js";
|
||||
import { buildMinimaxPortalProvider, buildMinimaxProvider } from "./provider-catalog.js";
|
||||
|
||||
const API_PROVIDER_ID = "minimax";
|
||||
@ -273,6 +274,8 @@ const minimaxPlugin = {
|
||||
],
|
||||
isModernModelRef: ({ modelId }) => isModernMiniMaxModel(modelId),
|
||||
});
|
||||
api.registerMediaUnderstandingProvider(minimaxMediaUnderstandingProvider);
|
||||
api.registerMediaUnderstandingProvider(minimaxPortalMediaUnderstandingProvider);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
14
extensions/minimax/media-understanding-provider.ts
Normal file
14
extensions/minimax/media-understanding-provider.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { describeImageWithModel } from "../../src/media-understanding/providers/image.js";
|
||||
import type { MediaUnderstandingProvider } from "../../src/media-understanding/types.js";
|
||||
|
||||
export const minimaxMediaUnderstandingProvider: MediaUnderstandingProvider = {
|
||||
id: "minimax",
|
||||
capabilities: ["image"],
|
||||
describeImage: describeImageWithModel,
|
||||
};
|
||||
|
||||
export const minimaxPortalMediaUnderstandingProvider: MediaUnderstandingProvider = {
|
||||
id: "minimax-portal",
|
||||
capabilities: ["image"],
|
||||
describeImage: describeImageWithModel,
|
||||
};
|
||||
64
extensions/minimax/model-definitions.ts
Normal file
64
extensions/minimax/model-definitions.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import type { ModelDefinitionConfig } from "../../src/config/types.models.js";
|
||||
|
||||
export const DEFAULT_MINIMAX_BASE_URL = "https://api.minimax.io/v1";
|
||||
export const MINIMAX_API_BASE_URL = "https://api.minimax.io/anthropic";
|
||||
export const MINIMAX_CN_API_BASE_URL = "https://api.minimaxi.com/anthropic";
|
||||
export const MINIMAX_HOSTED_MODEL_ID = "MiniMax-M2.5";
|
||||
export const MINIMAX_HOSTED_MODEL_REF = `minimax/${MINIMAX_HOSTED_MODEL_ID}`;
|
||||
export const DEFAULT_MINIMAX_CONTEXT_WINDOW = 200000;
|
||||
export const DEFAULT_MINIMAX_MAX_TOKENS = 8192;
|
||||
|
||||
export const MINIMAX_API_COST = {
|
||||
input: 0.3,
|
||||
output: 1.2,
|
||||
cacheRead: 0.03,
|
||||
cacheWrite: 0.12,
|
||||
};
|
||||
export const MINIMAX_HOSTED_COST = {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
};
|
||||
export const MINIMAX_LM_STUDIO_COST = {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
};
|
||||
|
||||
const MINIMAX_MODEL_CATALOG = {
|
||||
"MiniMax-M2.5": { name: "MiniMax M2.5", reasoning: true },
|
||||
"MiniMax-M2.5-highspeed": { name: "MiniMax M2.5 Highspeed", reasoning: true },
|
||||
} as const;
|
||||
|
||||
type MinimaxCatalogId = keyof typeof MINIMAX_MODEL_CATALOG;
|
||||
|
||||
export function buildMinimaxModelDefinition(params: {
|
||||
id: string;
|
||||
name?: string;
|
||||
reasoning?: boolean;
|
||||
cost: ModelDefinitionConfig["cost"];
|
||||
contextWindow: number;
|
||||
maxTokens: number;
|
||||
}): ModelDefinitionConfig {
|
||||
const catalog = MINIMAX_MODEL_CATALOG[params.id as MinimaxCatalogId];
|
||||
return {
|
||||
id: params.id,
|
||||
name: params.name ?? catalog?.name ?? `MiniMax ${params.id}`,
|
||||
reasoning: params.reasoning ?? catalog?.reasoning ?? false,
|
||||
input: ["text"],
|
||||
cost: params.cost,
|
||||
contextWindow: params.contextWindow,
|
||||
maxTokens: params.maxTokens,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildMinimaxApiModelDefinition(modelId: string): ModelDefinitionConfig {
|
||||
return buildMinimaxModelDefinition({
|
||||
id: modelId,
|
||||
cost: MINIMAX_API_COST,
|
||||
contextWindow: DEFAULT_MINIMAX_CONTEXT_WINDOW,
|
||||
maxTokens: DEFAULT_MINIMAX_MAX_TOKENS,
|
||||
});
|
||||
}
|
||||
104
extensions/minimax/onboard.ts
Normal file
104
extensions/minimax/onboard.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import {
|
||||
applyAgentDefaultModelPrimary,
|
||||
applyOnboardAuthAgentModelsAndProviders,
|
||||
} from "../../src/commands/onboard-auth.config-shared.js";
|
||||
import type { OpenClawConfig } from "../../src/config/config.js";
|
||||
import type { ModelProviderConfig } from "../../src/config/types.models.js";
|
||||
import {
|
||||
buildMinimaxApiModelDefinition,
|
||||
MINIMAX_API_BASE_URL,
|
||||
MINIMAX_CN_API_BASE_URL,
|
||||
} from "./model-definitions.js";
|
||||
|
||||
type MinimaxApiProviderConfigParams = {
|
||||
providerId: string;
|
||||
modelId: string;
|
||||
baseUrl: string;
|
||||
};
|
||||
|
||||
function applyMinimaxApiProviderConfigWithBaseUrl(
|
||||
cfg: OpenClawConfig,
|
||||
params: MinimaxApiProviderConfigParams,
|
||||
): OpenClawConfig {
|
||||
const providers = { ...cfg.models?.providers } as Record<string, ModelProviderConfig>;
|
||||
const existingProvider = providers[params.providerId];
|
||||
const existingModels = existingProvider?.models ?? [];
|
||||
const apiModel = buildMinimaxApiModelDefinition(params.modelId);
|
||||
const hasApiModel = existingModels.some((model) => model.id === params.modelId);
|
||||
const mergedModels = hasApiModel ? existingModels : [...existingModels, apiModel];
|
||||
const { apiKey: existingApiKey, ...existingProviderRest } = existingProvider ?? {
|
||||
baseUrl: params.baseUrl,
|
||||
models: [],
|
||||
};
|
||||
const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined;
|
||||
const normalizedApiKey = resolvedApiKey?.trim() === "minimax" ? "" : resolvedApiKey;
|
||||
providers[params.providerId] = {
|
||||
...existingProviderRest,
|
||||
baseUrl: params.baseUrl,
|
||||
api: "anthropic-messages",
|
||||
authHeader: true,
|
||||
...(normalizedApiKey?.trim() ? { apiKey: normalizedApiKey } : {}),
|
||||
models: mergedModels.length > 0 ? mergedModels : [apiModel],
|
||||
};
|
||||
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
const modelRef = `${params.providerId}/${params.modelId}`;
|
||||
models[modelRef] = {
|
||||
...models[modelRef],
|
||||
alias: "Minimax",
|
||||
};
|
||||
|
||||
return applyOnboardAuthAgentModelsAndProviders(cfg, { agentModels: models, providers });
|
||||
}
|
||||
|
||||
function applyMinimaxApiConfigWithBaseUrl(
|
||||
cfg: OpenClawConfig,
|
||||
params: MinimaxApiProviderConfigParams,
|
||||
): OpenClawConfig {
|
||||
const next = applyMinimaxApiProviderConfigWithBaseUrl(cfg, params);
|
||||
return applyAgentDefaultModelPrimary(next, `${params.providerId}/${params.modelId}`);
|
||||
}
|
||||
|
||||
export function applyMinimaxApiProviderConfig(
|
||||
cfg: OpenClawConfig,
|
||||
modelId: string = "MiniMax-M2.5",
|
||||
): OpenClawConfig {
|
||||
return applyMinimaxApiProviderConfigWithBaseUrl(cfg, {
|
||||
providerId: "minimax",
|
||||
modelId,
|
||||
baseUrl: MINIMAX_API_BASE_URL,
|
||||
});
|
||||
}
|
||||
|
||||
export function applyMinimaxApiConfig(
|
||||
cfg: OpenClawConfig,
|
||||
modelId: string = "MiniMax-M2.5",
|
||||
): OpenClawConfig {
|
||||
return applyMinimaxApiConfigWithBaseUrl(cfg, {
|
||||
providerId: "minimax",
|
||||
modelId,
|
||||
baseUrl: MINIMAX_API_BASE_URL,
|
||||
});
|
||||
}
|
||||
|
||||
export function applyMinimaxApiProviderConfigCn(
|
||||
cfg: OpenClawConfig,
|
||||
modelId: string = "MiniMax-M2.5",
|
||||
): OpenClawConfig {
|
||||
return applyMinimaxApiProviderConfigWithBaseUrl(cfg, {
|
||||
providerId: "minimax",
|
||||
modelId,
|
||||
baseUrl: MINIMAX_CN_API_BASE_URL,
|
||||
});
|
||||
}
|
||||
|
||||
export function applyMinimaxApiConfigCn(
|
||||
cfg: OpenClawConfig,
|
||||
modelId: string = "MiniMax-M2.5",
|
||||
): OpenClawConfig {
|
||||
return applyMinimaxApiConfigWithBaseUrl(cfg, {
|
||||
providerId: "minimax",
|
||||
modelId,
|
||||
baseUrl: MINIMAX_CN_API_BASE_URL,
|
||||
});
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
||||
import { applyMistralConfig, MISTRAL_DEFAULT_MODEL_REF } from "../../src/commands/onboard-auth.js";
|
||||
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
|
||||
import { mistralMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
||||
import { applyMistralConfig, MISTRAL_DEFAULT_MODEL_REF } from "./onboard.js";
|
||||
|
||||
const PROVIDER_ID = "mistral";
|
||||
|
||||
@ -50,6 +51,7 @@ const mistralPlugin = {
|
||||
],
|
||||
},
|
||||
});
|
||||
api.registerMediaUnderstandingProvider(mistralMediaUnderstandingProvider);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
17
extensions/mistral/media-understanding-provider.ts
Normal file
17
extensions/mistral/media-understanding-provider.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { transcribeOpenAiCompatibleAudio } from "../../src/media-understanding/providers/openai-compatible-audio.js";
|
||||
import type { MediaUnderstandingProvider } from "../../src/media-understanding/types.js";
|
||||
|
||||
const DEFAULT_MISTRAL_AUDIO_BASE_URL = "https://api.mistral.ai/v1";
|
||||
const DEFAULT_MISTRAL_AUDIO_MODEL = "voxtral-mini-latest";
|
||||
|
||||
export const mistralMediaUnderstandingProvider: MediaUnderstandingProvider = {
|
||||
id: "mistral",
|
||||
capabilities: ["audio"],
|
||||
transcribeAudio: async (req) =>
|
||||
await transcribeOpenAiCompatibleAudio({
|
||||
...req,
|
||||
baseUrl: req.baseUrl ?? DEFAULT_MISTRAL_AUDIO_BASE_URL,
|
||||
defaultBaseUrl: DEFAULT_MISTRAL_AUDIO_BASE_URL,
|
||||
defaultModel: DEFAULT_MISTRAL_AUDIO_MODEL,
|
||||
}),
|
||||
};
|
||||
25
extensions/mistral/model-definitions.ts
Normal file
25
extensions/mistral/model-definitions.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import type { ModelDefinitionConfig } from "../../src/config/types.models.js";
|
||||
|
||||
export const MISTRAL_BASE_URL = "https://api.mistral.ai/v1";
|
||||
export const MISTRAL_DEFAULT_MODEL_ID = "mistral-large-latest";
|
||||
export const MISTRAL_DEFAULT_MODEL_REF = `mistral/${MISTRAL_DEFAULT_MODEL_ID}`;
|
||||
export const MISTRAL_DEFAULT_CONTEXT_WINDOW = 262144;
|
||||
export const MISTRAL_DEFAULT_MAX_TOKENS = 262144;
|
||||
export const MISTRAL_DEFAULT_COST = {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
};
|
||||
|
||||
export function buildMistralModelDefinition(): ModelDefinitionConfig {
|
||||
return {
|
||||
id: MISTRAL_DEFAULT_MODEL_ID,
|
||||
name: "Mistral Large",
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
cost: MISTRAL_DEFAULT_COST,
|
||||
contextWindow: MISTRAL_DEFAULT_CONTEXT_WINDOW,
|
||||
maxTokens: MISTRAL_DEFAULT_MAX_TOKENS,
|
||||
};
|
||||
}
|
||||
34
extensions/mistral/onboard.ts
Normal file
34
extensions/mistral/onboard.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import {
|
||||
applyAgentDefaultModelPrimary,
|
||||
applyProviderConfigWithDefaultModel,
|
||||
} from "../../src/commands/onboard-auth.config-shared.js";
|
||||
import type { OpenClawConfig } from "../../src/config/config.js";
|
||||
import {
|
||||
buildMistralModelDefinition,
|
||||
MISTRAL_BASE_URL,
|
||||
MISTRAL_DEFAULT_MODEL_ID,
|
||||
MISTRAL_DEFAULT_MODEL_REF,
|
||||
} from "./model-definitions.js";
|
||||
|
||||
export { MISTRAL_DEFAULT_MODEL_REF };
|
||||
|
||||
export function applyMistralProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
models[MISTRAL_DEFAULT_MODEL_REF] = {
|
||||
...models[MISTRAL_DEFAULT_MODEL_REF],
|
||||
alias: models[MISTRAL_DEFAULT_MODEL_REF]?.alias ?? "Mistral",
|
||||
};
|
||||
|
||||
return applyProviderConfigWithDefaultModel(cfg, {
|
||||
agentModels: models,
|
||||
providerId: "mistral",
|
||||
api: "openai-completions",
|
||||
baseUrl: MISTRAL_BASE_URL,
|
||||
defaultModel: buildMistralModelDefinition(),
|
||||
defaultModelId: MISTRAL_DEFAULT_MODEL_ID,
|
||||
});
|
||||
}
|
||||
|
||||
export function applyMistralConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
return applyAgentDefaultModelPrimary(applyMistralProviderConfig(cfg), MISTRAL_DEFAULT_MODEL_REF);
|
||||
}
|
||||
@ -1,10 +1,11 @@
|
||||
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
||||
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
|
||||
import { buildSingleProviderApiKeyCatalog } from "../../src/plugins/provider-catalog.js";
|
||||
import {
|
||||
applyModelStudioConfig,
|
||||
applyModelStudioConfigCn,
|
||||
MODELSTUDIO_DEFAULT_MODEL_REF,
|
||||
} from "../../src/commands/onboard-auth.js";
|
||||
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
|
||||
} from "./onboard.js";
|
||||
import { buildModelStudioProvider } from "./provider-catalog.js";
|
||||
|
||||
const PROVIDER_ID = "modelstudio";
|
||||
@ -78,22 +79,13 @@ const modelStudioPlugin = {
|
||||
],
|
||||
catalog: {
|
||||
order: "simple",
|
||||
run: async (ctx) => {
|
||||
const apiKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey;
|
||||
if (!apiKey) {
|
||||
return null;
|
||||
}
|
||||
const explicitProvider = ctx.config.models?.providers?.[PROVIDER_ID];
|
||||
const explicitBaseUrl =
|
||||
typeof explicitProvider?.baseUrl === "string" ? explicitProvider.baseUrl.trim() : "";
|
||||
return {
|
||||
provider: {
|
||||
...buildModelStudioProvider(),
|
||||
...(explicitBaseUrl ? { baseUrl: explicitBaseUrl } : {}),
|
||||
apiKey,
|
||||
},
|
||||
};
|
||||
},
|
||||
run: (ctx) =>
|
||||
buildSingleProviderApiKeyCatalog({
|
||||
ctx,
|
||||
providerId: PROVIDER_ID,
|
||||
buildProvider: buildModelStudioProvider,
|
||||
allowExplicitBaseUrl: true,
|
||||
}),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
102
extensions/modelstudio/model-definitions.ts
Normal file
102
extensions/modelstudio/model-definitions.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import type { ModelDefinitionConfig } from "../../src/config/types.models.js";
|
||||
|
||||
export const MODELSTUDIO_CN_BASE_URL = "https://coding.dashscope.aliyuncs.com/v1";
|
||||
export const MODELSTUDIO_GLOBAL_BASE_URL = "https://coding-intl.dashscope.aliyuncs.com/v1";
|
||||
export const MODELSTUDIO_DEFAULT_MODEL_ID = "qwen3.5-plus";
|
||||
export const MODELSTUDIO_DEFAULT_MODEL_REF = `modelstudio/${MODELSTUDIO_DEFAULT_MODEL_ID}`;
|
||||
export const MODELSTUDIO_DEFAULT_COST = {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
};
|
||||
|
||||
const MODELSTUDIO_MODEL_CATALOG = {
|
||||
"qwen3.5-plus": {
|
||||
name: "qwen3.5-plus",
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
contextWindow: 1000000,
|
||||
maxTokens: 65536,
|
||||
},
|
||||
"qwen3-max-2026-01-23": {
|
||||
name: "qwen3-max-2026-01-23",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
contextWindow: 262144,
|
||||
maxTokens: 65536,
|
||||
},
|
||||
"qwen3-coder-next": {
|
||||
name: "qwen3-coder-next",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
contextWindow: 262144,
|
||||
maxTokens: 65536,
|
||||
},
|
||||
"qwen3-coder-plus": {
|
||||
name: "qwen3-coder-plus",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
contextWindow: 1000000,
|
||||
maxTokens: 65536,
|
||||
},
|
||||
"MiniMax-M2.5": {
|
||||
name: "MiniMax-M2.5",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
contextWindow: 1000000,
|
||||
maxTokens: 65536,
|
||||
},
|
||||
"glm-5": {
|
||||
name: "glm-5",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
contextWindow: 202752,
|
||||
maxTokens: 16384,
|
||||
},
|
||||
"glm-4.7": {
|
||||
name: "glm-4.7",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
contextWindow: 202752,
|
||||
maxTokens: 16384,
|
||||
},
|
||||
"kimi-k2.5": {
|
||||
name: "kimi-k2.5",
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
contextWindow: 262144,
|
||||
maxTokens: 32768,
|
||||
},
|
||||
} as const;
|
||||
|
||||
type ModelStudioCatalogId = keyof typeof MODELSTUDIO_MODEL_CATALOG;
|
||||
|
||||
export function buildModelStudioModelDefinition(params: {
|
||||
id: string;
|
||||
name?: string;
|
||||
reasoning?: boolean;
|
||||
input?: string[];
|
||||
cost?: ModelDefinitionConfig["cost"];
|
||||
contextWindow?: number;
|
||||
maxTokens?: number;
|
||||
}): ModelDefinitionConfig {
|
||||
const catalog = MODELSTUDIO_MODEL_CATALOG[params.id as ModelStudioCatalogId];
|
||||
return {
|
||||
id: params.id,
|
||||
name: params.name ?? catalog?.name ?? params.id,
|
||||
reasoning: params.reasoning ?? catalog?.reasoning ?? false,
|
||||
input:
|
||||
(params.input as ("text" | "image")[]) ??
|
||||
([...(catalog?.input ?? ["text"])] as ("text" | "image")[]),
|
||||
cost: params.cost ?? MODELSTUDIO_DEFAULT_COST,
|
||||
contextWindow: params.contextWindow ?? catalog?.contextWindow ?? 262144,
|
||||
maxTokens: params.maxTokens ?? catalog?.maxTokens ?? 65536,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildModelStudioDefaultModelDefinition(): ModelDefinitionConfig {
|
||||
return buildModelStudioModelDefinition({
|
||||
id: MODELSTUDIO_DEFAULT_MODEL_ID,
|
||||
});
|
||||
}
|
||||
61
extensions/modelstudio/onboard.ts
Normal file
61
extensions/modelstudio/onboard.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import {
|
||||
applyAgentDefaultModelPrimary,
|
||||
applyProviderConfigWithModelCatalog,
|
||||
} from "../../src/commands/onboard-auth.config-shared.js";
|
||||
import type { OpenClawConfig } from "../../src/config/config.js";
|
||||
import {
|
||||
MODELSTUDIO_CN_BASE_URL,
|
||||
MODELSTUDIO_DEFAULT_MODEL_REF,
|
||||
MODELSTUDIO_GLOBAL_BASE_URL,
|
||||
} from "./model-definitions.js";
|
||||
import { buildModelStudioProvider } from "./provider-catalog.js";
|
||||
|
||||
export { MODELSTUDIO_CN_BASE_URL, MODELSTUDIO_DEFAULT_MODEL_REF, MODELSTUDIO_GLOBAL_BASE_URL };
|
||||
|
||||
function applyModelStudioProviderConfigWithBaseUrl(
|
||||
cfg: OpenClawConfig,
|
||||
baseUrl: string,
|
||||
): OpenClawConfig {
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
const provider = buildModelStudioProvider();
|
||||
for (const model of provider.models ?? []) {
|
||||
const modelRef = `modelstudio/${model.id}`;
|
||||
if (!models[modelRef]) {
|
||||
models[modelRef] = {};
|
||||
}
|
||||
}
|
||||
models[MODELSTUDIO_DEFAULT_MODEL_REF] = {
|
||||
...models[MODELSTUDIO_DEFAULT_MODEL_REF],
|
||||
alias: models[MODELSTUDIO_DEFAULT_MODEL_REF]?.alias ?? "Qwen",
|
||||
};
|
||||
|
||||
return applyProviderConfigWithModelCatalog(cfg, {
|
||||
agentModels: models,
|
||||
providerId: "modelstudio",
|
||||
api: provider.api ?? "openai-completions",
|
||||
baseUrl,
|
||||
catalogModels: provider.models ?? [],
|
||||
});
|
||||
}
|
||||
|
||||
export function applyModelStudioProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
return applyModelStudioProviderConfigWithBaseUrl(cfg, MODELSTUDIO_GLOBAL_BASE_URL);
|
||||
}
|
||||
|
||||
export function applyModelStudioProviderConfigCn(cfg: OpenClawConfig): OpenClawConfig {
|
||||
return applyModelStudioProviderConfigWithBaseUrl(cfg, MODELSTUDIO_CN_BASE_URL);
|
||||
}
|
||||
|
||||
export function applyModelStudioConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
return applyAgentDefaultModelPrimary(
|
||||
applyModelStudioProviderConfig(cfg),
|
||||
MODELSTUDIO_DEFAULT_MODEL_REF,
|
||||
);
|
||||
}
|
||||
|
||||
export function applyModelStudioConfigCn(cfg: OpenClawConfig): OpenClawConfig {
|
||||
return applyAgentDefaultModelPrimary(
|
||||
applyModelStudioProviderConfigCn(cfg),
|
||||
MODELSTUDIO_DEFAULT_MODEL_REF,
|
||||
);
|
||||
}
|
||||
@ -7,14 +7,16 @@ import {
|
||||
getScopedCredentialValue,
|
||||
setScopedCredentialValue,
|
||||
} from "../../src/agents/tools/web-search-plugin-factory.js";
|
||||
import { emptyPluginConfigSchema } from "../../src/plugins/config-schema.js";
|
||||
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
|
||||
import { buildSingleProviderApiKeyCatalog } from "../../src/plugins/provider-catalog.js";
|
||||
import type { OpenClawPluginApi } from "../../src/plugins/types.js";
|
||||
import { moonshotMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
||||
import {
|
||||
applyMoonshotConfig,
|
||||
applyMoonshotConfigCn,
|
||||
} from "../../src/commands/onboard-auth.config-core.js";
|
||||
import { MOONSHOT_DEFAULT_MODEL_REF } from "../../src/commands/onboard-auth.models.js";
|
||||
import { emptyPluginConfigSchema } from "../../src/plugins/config-schema.js";
|
||||
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
|
||||
import type { OpenClawPluginApi } from "../../src/plugins/types.js";
|
||||
MOONSHOT_DEFAULT_MODEL_REF,
|
||||
} from "./onboard.js";
|
||||
import { buildMoonshotProvider } from "./provider-catalog.js";
|
||||
|
||||
const PROVIDER_ID = "moonshot";
|
||||
@ -34,8 +36,8 @@ const moonshotPlugin = {
|
||||
createProviderApiKeyAuthMethod({
|
||||
providerId: PROVIDER_ID,
|
||||
methodId: "api-key",
|
||||
label: "Kimi API key (.ai)",
|
||||
hint: "Kimi K2.5 + Kimi Coding",
|
||||
label: "Moonshot API key (.ai)",
|
||||
hint: "Kimi K2.5",
|
||||
optionKey: "moonshotApiKey",
|
||||
flagName: "--moonshot-api-key",
|
||||
envVar: "MOONSHOT_API_KEY",
|
||||
@ -45,17 +47,17 @@ const moonshotPlugin = {
|
||||
applyConfig: (cfg) => applyMoonshotConfig(cfg),
|
||||
wizard: {
|
||||
choiceId: "moonshot-api-key",
|
||||
choiceLabel: "Kimi API key (.ai)",
|
||||
choiceLabel: "Moonshot API key (.ai)",
|
||||
groupId: "moonshot",
|
||||
groupLabel: "Moonshot AI (Kimi K2.5)",
|
||||
groupHint: "Kimi K2.5 + Kimi Coding",
|
||||
groupHint: "Kimi K2.5",
|
||||
},
|
||||
}),
|
||||
createProviderApiKeyAuthMethod({
|
||||
providerId: PROVIDER_ID,
|
||||
methodId: "api-key-cn",
|
||||
label: "Kimi API key (.cn)",
|
||||
hint: "Kimi K2.5 + Kimi Coding",
|
||||
label: "Moonshot API key (.cn)",
|
||||
hint: "Kimi K2.5",
|
||||
optionKey: "moonshotApiKey",
|
||||
flagName: "--moonshot-api-key",
|
||||
envVar: "MOONSHOT_API_KEY",
|
||||
@ -65,31 +67,22 @@ const moonshotPlugin = {
|
||||
applyConfig: (cfg) => applyMoonshotConfigCn(cfg),
|
||||
wizard: {
|
||||
choiceId: "moonshot-api-key-cn",
|
||||
choiceLabel: "Kimi API key (.cn)",
|
||||
choiceLabel: "Moonshot API key (.cn)",
|
||||
groupId: "moonshot",
|
||||
groupLabel: "Moonshot AI (Kimi K2.5)",
|
||||
groupHint: "Kimi K2.5 + Kimi Coding",
|
||||
groupHint: "Kimi K2.5",
|
||||
},
|
||||
}),
|
||||
],
|
||||
catalog: {
|
||||
order: "simple",
|
||||
run: async (ctx) => {
|
||||
const apiKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey;
|
||||
if (!apiKey) {
|
||||
return null;
|
||||
}
|
||||
const explicitProvider = ctx.config.models?.providers?.[PROVIDER_ID];
|
||||
const explicitBaseUrl =
|
||||
typeof explicitProvider?.baseUrl === "string" ? explicitProvider.baseUrl.trim() : "";
|
||||
return {
|
||||
provider: {
|
||||
...buildMoonshotProvider(),
|
||||
...(explicitBaseUrl ? { baseUrl: explicitBaseUrl } : {}),
|
||||
apiKey,
|
||||
},
|
||||
};
|
||||
},
|
||||
run: (ctx) =>
|
||||
buildSingleProviderApiKeyCatalog({
|
||||
ctx,
|
||||
providerId: PROVIDER_ID,
|
||||
buildProvider: buildMoonshotProvider,
|
||||
allowExplicitBaseUrl: true,
|
||||
}),
|
||||
},
|
||||
wrapStreamFn: (ctx) => {
|
||||
const thinkingType = resolveMoonshotThinkingType({
|
||||
@ -99,6 +92,7 @@ const moonshotPlugin = {
|
||||
return createMoonshotThinkingWrapper(ctx.streamFn, thinkingType);
|
||||
},
|
||||
});
|
||||
api.registerMediaUnderstandingProvider(moonshotMediaUnderstandingProvider);
|
||||
api.registerWebSearchProvider(
|
||||
createPluginBackedWebSearchProvider({
|
||||
id: "kimi",
|
||||
|
||||
@ -1,5 +1,14 @@
|
||||
import type { VideoDescriptionRequest, VideoDescriptionResult } from "../../types.js";
|
||||
import { assertOkOrThrowHttpError, normalizeBaseUrl, postJsonRequest } from "../shared.js";
|
||||
import { describeImageWithModel } from "../../src/media-understanding/providers/image.js";
|
||||
import {
|
||||
assertOkOrThrowHttpError,
|
||||
normalizeBaseUrl,
|
||||
postJsonRequest,
|
||||
} from "../../src/media-understanding/providers/shared.js";
|
||||
import type {
|
||||
MediaUnderstandingProvider,
|
||||
VideoDescriptionRequest,
|
||||
VideoDescriptionResult,
|
||||
} from "../../src/media-understanding/types.js";
|
||||
|
||||
export const DEFAULT_MOONSHOT_VIDEO_BASE_URL = "https://api.moonshot.ai/v1";
|
||||
const DEFAULT_MOONSHOT_VIDEO_MODEL = "kimi-k2.5";
|
||||
@ -104,3 +113,10 @@ export async function describeMoonshotVideo(
|
||||
await release();
|
||||
}
|
||||
}
|
||||
|
||||
export const moonshotMediaUnderstandingProvider: MediaUnderstandingProvider = {
|
||||
id: "moonshot",
|
||||
capabilities: ["image", "video"],
|
||||
describeImage: describeImageWithModel,
|
||||
describeVideo: describeMoonshotVideo,
|
||||
};
|
||||
60
extensions/moonshot/onboard.ts
Normal file
60
extensions/moonshot/onboard.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import {
|
||||
applyAgentDefaultModelPrimary,
|
||||
applyProviderConfigWithDefaultModel,
|
||||
} from "../../src/commands/onboard-auth.config-shared.js";
|
||||
import type { OpenClawConfig } from "../../src/config/config.js";
|
||||
import {
|
||||
buildMoonshotProvider,
|
||||
MOONSHOT_BASE_URL,
|
||||
MOONSHOT_DEFAULT_MODEL_ID,
|
||||
} from "./provider-catalog.js";
|
||||
|
||||
export const MOONSHOT_CN_BASE_URL = "https://api.moonshot.cn/v1";
|
||||
export const MOONSHOT_DEFAULT_MODEL_REF = `moonshot/${MOONSHOT_DEFAULT_MODEL_ID}`;
|
||||
|
||||
export function applyMoonshotProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
return applyMoonshotProviderConfigWithBaseUrl(cfg, MOONSHOT_BASE_URL);
|
||||
}
|
||||
|
||||
export function applyMoonshotProviderConfigCn(cfg: OpenClawConfig): OpenClawConfig {
|
||||
return applyMoonshotProviderConfigWithBaseUrl(cfg, MOONSHOT_CN_BASE_URL);
|
||||
}
|
||||
|
||||
function applyMoonshotProviderConfigWithBaseUrl(
|
||||
cfg: OpenClawConfig,
|
||||
baseUrl: string,
|
||||
): OpenClawConfig {
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
models[MOONSHOT_DEFAULT_MODEL_REF] = {
|
||||
...models[MOONSHOT_DEFAULT_MODEL_REF],
|
||||
alias: models[MOONSHOT_DEFAULT_MODEL_REF]?.alias ?? "Kimi",
|
||||
};
|
||||
|
||||
const defaultModel = buildMoonshotProvider().models[0];
|
||||
if (!defaultModel) {
|
||||
return cfg;
|
||||
}
|
||||
|
||||
return applyProviderConfigWithDefaultModel(cfg, {
|
||||
agentModels: models,
|
||||
providerId: "moonshot",
|
||||
api: "openai-completions",
|
||||
baseUrl,
|
||||
defaultModel,
|
||||
defaultModelId: MOONSHOT_DEFAULT_MODEL_ID,
|
||||
});
|
||||
}
|
||||
|
||||
export function applyMoonshotConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
return applyAgentDefaultModelPrimary(
|
||||
applyMoonshotProviderConfig(cfg),
|
||||
MOONSHOT_DEFAULT_MODEL_REF,
|
||||
);
|
||||
}
|
||||
|
||||
export function applyMoonshotConfigCn(cfg: OpenClawConfig): OpenClawConfig {
|
||||
return applyAgentDefaultModelPrimary(
|
||||
applyMoonshotProviderConfigCn(cfg),
|
||||
MOONSHOT_DEFAULT_MODEL_REF,
|
||||
);
|
||||
}
|
||||
@ -9,10 +9,10 @@
|
||||
"provider": "moonshot",
|
||||
"method": "api-key",
|
||||
"choiceId": "moonshot-api-key",
|
||||
"choiceLabel": "Kimi API key (.ai)",
|
||||
"choiceLabel": "Moonshot API key (.ai)",
|
||||
"groupId": "moonshot",
|
||||
"groupLabel": "Moonshot AI (Kimi K2.5)",
|
||||
"groupHint": "Kimi K2.5 + Kimi Coding",
|
||||
"groupHint": "Kimi K2.5",
|
||||
"optionKey": "moonshotApiKey",
|
||||
"cliFlag": "--moonshot-api-key",
|
||||
"cliOption": "--moonshot-api-key <key>",
|
||||
@ -22,10 +22,10 @@
|
||||
"provider": "moonshot",
|
||||
"method": "api-key-cn",
|
||||
"choiceId": "moonshot-api-key-cn",
|
||||
"choiceLabel": "Kimi API key (.cn)",
|
||||
"choiceLabel": "Moonshot API key (.cn)",
|
||||
"groupId": "moonshot",
|
||||
"groupLabel": "Moonshot AI (Kimi K2.5)",
|
||||
"groupHint": "Kimi K2.5 + Kimi Coding",
|
||||
"groupHint": "Kimi K2.5",
|
||||
"optionKey": "moonshotApiKey",
|
||||
"cliFlag": "--moonshot-api-key",
|
||||
"cliOption": "--moonshot-api-key <key>",
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import type { ChannelSetupAdapter } from "../../../src/channels/plugins/types.adapters.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js";
|
||||
import { DEFAULT_ACCOUNT_ID, type ChannelSetupAdapter } from "openclaw/plugin-sdk/setup";
|
||||
|
||||
export const msteamsSetupAdapter: ChannelSetupAdapter = {
|
||||
resolveAccountId: () => DEFAULT_ACCOUNT_ID,
|
||||
|
||||
@ -1,17 +1,18 @@
|
||||
import type { MSTeamsTeamConfig } from "openclaw/plugin-sdk/msteams";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
formatDocsLink,
|
||||
mergeAllowFromEntries,
|
||||
setTopLevelChannelAllowFrom,
|
||||
setTopLevelChannelDmPolicyWithAllowFrom,
|
||||
setTopLevelChannelGroupPolicy,
|
||||
splitSetupEntries,
|
||||
} from "../../../src/channels/plugins/setup-wizard-helpers.js";
|
||||
import type { ChannelSetupDmPolicy } from "../../../src/channels/plugins/setup-wizard-types.js";
|
||||
import type { ChannelSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import type { DmPolicy, MSTeamsTeamConfig } from "../../../src/config/types.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js";
|
||||
import { formatDocsLink } from "../../../src/terminal/links.js";
|
||||
import type { WizardPrompter } from "../../../src/wizard/prompts.js";
|
||||
type ChannelSetupDmPolicy,
|
||||
type ChannelSetupWizard,
|
||||
type DmPolicy,
|
||||
type OpenClawConfig,
|
||||
type WizardPrompter,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import {
|
||||
parseMSTeamsTeamEntry,
|
||||
resolveMSTeamsChannelAllowlist,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import {
|
||||
applyAccountNameToChannelSection,
|
||||
patchScopedAccountConfig,
|
||||
prepareScopedSetupConfig,
|
||||
} from "../../../src/channels/plugins/setup-helpers.js";
|
||||
import {
|
||||
mergeAllowFromEntries,
|
||||
@ -115,7 +115,7 @@ export function clearNextcloudTalkAccountFields(
|
||||
} as CoreConfig;
|
||||
}
|
||||
|
||||
async function promptNextcloudTalkAllowFrom(params: {
|
||||
export async function promptNextcloudTalkAllowFrom(params: {
|
||||
cfg: CoreConfig;
|
||||
prompter: WizardPrompter;
|
||||
accountId: string;
|
||||
@ -127,7 +127,7 @@ async function promptNextcloudTalkAllowFrom(params: {
|
||||
"1) Check the Nextcloud admin panel for user IDs",
|
||||
"2) Or look at the webhook payload logs when someone messages",
|
||||
"3) User IDs are typically lowercase usernames in Nextcloud",
|
||||
`Docs: ${formatDocsLink("/channels/nextcloud-talk", "nextcloud-talk")}`,
|
||||
`Docs: ${formatDocsLink("/channels/nextcloud-talk", "channels/nextcloud-talk")}`,
|
||||
].join("\n"),
|
||||
"Nextcloud Talk user id",
|
||||
);
|
||||
@ -158,7 +158,7 @@ async function promptNextcloudTalkAllowFrom(params: {
|
||||
});
|
||||
}
|
||||
|
||||
async function promptNextcloudTalkAllowFromForAccount(params: {
|
||||
export async function promptNextcloudTalkAllowFromForAccount(params: {
|
||||
cfg: OpenClawConfig;
|
||||
prompter: WizardPrompter;
|
||||
accountId?: string;
|
||||
@ -174,7 +174,7 @@ async function promptNextcloudTalkAllowFromForAccount(params: {
|
||||
});
|
||||
}
|
||||
|
||||
const nextcloudTalkDmPolicy: ChannelSetupDmPolicy = {
|
||||
export const nextcloudTalkDmPolicy: ChannelSetupDmPolicy = {
|
||||
label: "Nextcloud Talk",
|
||||
channel,
|
||||
policyKey: "channels.nextcloud-talk.dmPolicy",
|
||||
@ -187,7 +187,7 @@ const nextcloudTalkDmPolicy: ChannelSetupDmPolicy = {
|
||||
export const nextcloudTalkSetupAdapter: ChannelSetupAdapter = {
|
||||
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
||||
applyAccountName: ({ cfg, accountId, name }) =>
|
||||
applyAccountNameToChannelSection({
|
||||
prepareScopedSetupConfig({
|
||||
cfg,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
@ -208,7 +208,7 @@ export const nextcloudTalkSetupAdapter: ChannelSetupAdapter = {
|
||||
},
|
||||
applyAccountConfig: ({ cfg, accountId, input }) => {
|
||||
const setupInput = input as NextcloudSetupInput;
|
||||
const namedConfig = applyAccountNameToChannelSection({
|
||||
const namedConfig = prepareScopedSetupConfig({
|
||||
cfg,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
|
||||
@ -1,111 +1,22 @@
|
||||
import {
|
||||
mergeAllowFromEntries,
|
||||
resolveSetupAccountId,
|
||||
setSetupChannelEnabled,
|
||||
setTopLevelChannelDmPolicyWithAllowFrom,
|
||||
} from "../../../src/channels/plugins/setup-wizard-helpers.js";
|
||||
import type { ChannelSetupDmPolicy } from "../../../src/channels/plugins/setup-wizard-types.js";
|
||||
import { setSetupChannelEnabled } from "../../../src/channels/plugins/setup-wizard-helpers.js";
|
||||
import { type ChannelSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
|
||||
import type { ChannelSetupInput } from "../../../src/channels/plugins/types.core.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import { hasConfiguredSecretInput } from "../../../src/config/types.secrets.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js";
|
||||
import { formatDocsLink } from "../../../src/terminal/links.js";
|
||||
import type { WizardPrompter } from "../../../src/wizard/prompts.js";
|
||||
import {
|
||||
listNextcloudTalkAccountIds,
|
||||
resolveDefaultNextcloudTalkAccountId,
|
||||
resolveNextcloudTalkAccount,
|
||||
} from "./accounts.js";
|
||||
import { listNextcloudTalkAccountIds, resolveNextcloudTalkAccount } from "./accounts.js";
|
||||
import {
|
||||
clearNextcloudTalkAccountFields,
|
||||
nextcloudTalkDmPolicy,
|
||||
nextcloudTalkSetupAdapter,
|
||||
normalizeNextcloudTalkBaseUrl,
|
||||
setNextcloudTalkAccountConfig,
|
||||
validateNextcloudTalkBaseUrl,
|
||||
} from "./setup-core.js";
|
||||
import type { CoreConfig, DmPolicy } from "./types.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
|
||||
const channel = "nextcloud-talk" as const;
|
||||
const CONFIGURE_API_FLAG = "__nextcloudTalkConfigureApiCredentials";
|
||||
|
||||
function setNextcloudTalkDmPolicy(cfg: CoreConfig, dmPolicy: DmPolicy): CoreConfig {
|
||||
return setTopLevelChannelDmPolicyWithAllowFrom({
|
||||
cfg,
|
||||
channel,
|
||||
dmPolicy,
|
||||
}) as CoreConfig;
|
||||
}
|
||||
|
||||
async function promptNextcloudTalkAllowFrom(params: {
|
||||
cfg: CoreConfig;
|
||||
prompter: WizardPrompter;
|
||||
accountId: string;
|
||||
}): Promise<CoreConfig> {
|
||||
const resolved = resolveNextcloudTalkAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
const existingAllowFrom = resolved.config.allowFrom ?? [];
|
||||
await params.prompter.note(
|
||||
[
|
||||
"1) Check the Nextcloud admin panel for user IDs",
|
||||
"2) Or look at the webhook payload logs when someone messages",
|
||||
"3) User IDs are typically lowercase usernames in Nextcloud",
|
||||
`Docs: ${formatDocsLink("/channels/nextcloud-talk", "channels/nextcloud-talk")}`,
|
||||
].join("\n"),
|
||||
"Nextcloud Talk user id",
|
||||
);
|
||||
|
||||
let resolvedIds: string[] = [];
|
||||
while (resolvedIds.length === 0) {
|
||||
const entry = await params.prompter.text({
|
||||
message: "Nextcloud Talk allowFrom (user id)",
|
||||
placeholder: "username",
|
||||
initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
|
||||
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||
});
|
||||
resolvedIds = String(entry)
|
||||
.split(/[\n,;]+/g)
|
||||
.map((value) => value.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
if (resolvedIds.length === 0) {
|
||||
await params.prompter.note("Please enter at least one valid user ID.", "Nextcloud Talk");
|
||||
}
|
||||
}
|
||||
|
||||
return setNextcloudTalkAccountConfig(params.cfg, params.accountId, {
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: mergeAllowFromEntries(
|
||||
existingAllowFrom.map((value) => String(value).trim().toLowerCase()),
|
||||
resolvedIds,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
async function promptNextcloudTalkAllowFromForAccount(params: {
|
||||
cfg: OpenClawConfig;
|
||||
prompter: WizardPrompter;
|
||||
accountId?: string;
|
||||
}): Promise<OpenClawConfig> {
|
||||
const accountId = resolveSetupAccountId({
|
||||
accountId: params.accountId,
|
||||
defaultAccountId: resolveDefaultNextcloudTalkAccountId(params.cfg as CoreConfig),
|
||||
});
|
||||
return await promptNextcloudTalkAllowFrom({
|
||||
cfg: params.cfg as CoreConfig,
|
||||
prompter: params.prompter,
|
||||
accountId,
|
||||
});
|
||||
}
|
||||
|
||||
const nextcloudTalkDmPolicy: ChannelSetupDmPolicy = {
|
||||
label: "Nextcloud Talk",
|
||||
channel,
|
||||
policyKey: "channels.nextcloud-talk.dmPolicy",
|
||||
allowFromKey: "channels.nextcloud-talk.allowFrom",
|
||||
getCurrent: (cfg) => cfg.channels?.["nextcloud-talk"]?.dmPolicy ?? "pairing",
|
||||
setPolicy: (cfg, policy) => setNextcloudTalkDmPolicy(cfg as CoreConfig, policy as DmPolicy),
|
||||
promptAllowFrom: promptNextcloudTalkAllowFromForAccount,
|
||||
};
|
||||
|
||||
export const nextcloudTalkSetupWizard: ChannelSetupWizard = {
|
||||
channel,
|
||||
stepOrder: "text-first",
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
||||
import { buildSingleProviderApiKeyCatalog } from "../../src/plugins/provider-catalog.js";
|
||||
import { buildNvidiaProvider } from "./provider-catalog.js";
|
||||
|
||||
const PROVIDER_ID = "nvidia";
|
||||
@ -17,18 +18,12 @@ const nvidiaPlugin = {
|
||||
auth: [],
|
||||
catalog: {
|
||||
order: "simple",
|
||||
run: async (ctx) => {
|
||||
const apiKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey;
|
||||
if (!apiKey) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
provider: {
|
||||
...buildNvidiaProvider(),
|
||||
apiKey,
|
||||
},
|
||||
};
|
||||
},
|
||||
run: (ctx) =>
|
||||
buildSingleProviderApiKeyCatalog({
|
||||
ctx,
|
||||
providerId: PROVIDER_ID,
|
||||
buildProvider: buildNvidiaProvider,
|
||||
}),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
||||
import { buildOpenAISpeechProvider } from "../../src/tts/providers/openai.js";
|
||||
import { openaiMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
||||
import { buildOpenAICodexProviderPlugin } from "./openai-codex-provider.js";
|
||||
import { buildOpenAIProvider } from "./openai-provider.js";
|
||||
|
||||
@ -12,6 +13,7 @@ const openAIPlugin = {
|
||||
api.registerProvider(buildOpenAIProvider());
|
||||
api.registerProvider(buildOpenAICodexProviderPlugin());
|
||||
api.registerSpeechProvider(buildOpenAISpeechProvider());
|
||||
api.registerMediaUnderstandingProvider(openaiMediaUnderstandingProvider);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
23
extensions/openai/media-understanding-provider.ts
Normal file
23
extensions/openai/media-understanding-provider.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { describeImageWithModel } from "../../src/media-understanding/providers/image.js";
|
||||
import { transcribeOpenAiCompatibleAudio } from "../../src/media-understanding/providers/openai-compatible-audio.js";
|
||||
import type { MediaUnderstandingProvider } from "../../src/media-understanding/types.js";
|
||||
|
||||
export const DEFAULT_OPENAI_AUDIO_BASE_URL = "https://api.openai.com/v1";
|
||||
const DEFAULT_OPENAI_AUDIO_MODEL = "gpt-4o-mini-transcribe";
|
||||
|
||||
export async function transcribeOpenAiAudio(
|
||||
params: import("../../src/media-understanding/types.js").AudioTranscriptionRequest,
|
||||
) {
|
||||
return await transcribeOpenAiCompatibleAudio({
|
||||
...params,
|
||||
defaultBaseUrl: DEFAULT_OPENAI_AUDIO_BASE_URL,
|
||||
defaultModel: DEFAULT_OPENAI_AUDIO_MODEL,
|
||||
});
|
||||
}
|
||||
|
||||
export const openaiMediaUnderstandingProvider: MediaUnderstandingProvider = {
|
||||
id: "openai",
|
||||
capabilities: ["image", "audio"],
|
||||
describeImage: describeImageWithModel,
|
||||
transcribeAudio: transcribeOpenAiAudio,
|
||||
};
|
||||
@ -4,6 +4,7 @@ import type {
|
||||
ProviderResolveDynamicModelContext,
|
||||
ProviderRuntimeModel,
|
||||
} from "openclaw/plugin-sdk/core";
|
||||
import { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/core";
|
||||
import { CODEX_CLI_PROFILE_ID } from "../../src/agents/auth-profiles.js";
|
||||
import { listProfilesForProvider } from "../../src/agents/auth-profiles/profiles.js";
|
||||
import { ensureAuthProfileStore } from "../../src/agents/auth-profiles/store.js";
|
||||
@ -13,7 +14,6 @@ import { normalizeModelCompat } from "../../src/agents/model-compat.js";
|
||||
import { normalizeProviderId } from "../../src/agents/provider-id.js";
|
||||
import { loginOpenAICodexOAuth } from "../../src/commands/openai-codex-oauth.js";
|
||||
import { fetchCodexUsage } from "../../src/infra/provider-usage.fetch.js";
|
||||
import { buildOauthProviderAuthResult } from "../../src/plugin-sdk/provider-auth-result.js";
|
||||
import type { ProviderPlugin } from "../../src/plugins/types.js";
|
||||
import { buildOpenAICodexProvider } from "./openai-codex-catalog.js";
|
||||
import {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { normalizeModelCompat } from "../../src/agents/model-compat.js";
|
||||
import { findCatalogTemplate } from "../../src/plugins/provider-catalog.js";
|
||||
import type {
|
||||
ProviderResolveDynamicModelContext,
|
||||
ProviderRuntimeModel,
|
||||
@ -48,18 +49,4 @@ export function cloneFirstTemplateModel(params: {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function findCatalogTemplate(params: {
|
||||
entries: ReadonlyArray<{ provider: string; id: string }>;
|
||||
providerId: string;
|
||||
templateIds: readonly string[];
|
||||
}) {
|
||||
return params.templateIds
|
||||
.map((templateId) =>
|
||||
params.entries.find(
|
||||
(entry) =>
|
||||
entry.provider.toLowerCase() === params.providerId.toLowerCase() &&
|
||||
entry.id.toLowerCase() === templateId.toLowerCase(),
|
||||
),
|
||||
)
|
||||
.find((entry) => entry !== undefined);
|
||||
}
|
||||
export { findCatalogTemplate };
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
||||
import { applyOpencodeGoConfig } from "../../src/commands/onboard-auth.config-opencode-go.js";
|
||||
import { OPENCODE_GO_DEFAULT_MODEL_REF } from "../../src/commands/opencode-go-model-default.js";
|
||||
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
|
||||
import { applyOpencodeGoConfig } from "./onboard.js";
|
||||
|
||||
const PROVIDER_ID = "opencode-go";
|
||||
|
||||
|
||||
39
extensions/opencode-go/onboard.ts
Normal file
39
extensions/opencode-go/onboard.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { applyAgentDefaultModelPrimary } from "../../src/commands/onboard-auth.config-shared.js";
|
||||
import { OPENCODE_GO_DEFAULT_MODEL_REF } from "../../src/commands/opencode-go-model-default.js";
|
||||
import type { OpenClawConfig } from "../../src/config/config.js";
|
||||
|
||||
export { OPENCODE_GO_DEFAULT_MODEL_REF };
|
||||
|
||||
const OPENCODE_GO_ALIAS_DEFAULTS: Record<string, string> = {
|
||||
"opencode-go/kimi-k2.5": "Kimi",
|
||||
"opencode-go/glm-5": "GLM",
|
||||
"opencode-go/minimax-m2.5": "MiniMax",
|
||||
};
|
||||
|
||||
export function applyOpencodeGoProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
for (const [modelRef, alias] of Object.entries(OPENCODE_GO_ALIAS_DEFAULTS)) {
|
||||
models[modelRef] = {
|
||||
...models[modelRef],
|
||||
alias: models[modelRef]?.alias ?? alias,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
models,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function applyOpencodeGoConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
return applyAgentDefaultModelPrimary(
|
||||
applyOpencodeGoProviderConfig(cfg),
|
||||
OPENCODE_GO_DEFAULT_MODEL_REF,
|
||||
);
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
||||
import { applyOpencodeZenConfig } from "../../src/commands/onboard-auth.config-opencode.js";
|
||||
import { OPENCODE_ZEN_DEFAULT_MODEL } from "../../src/commands/opencode-zen-model-default.js";
|
||||
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
|
||||
import { applyOpencodeZenConfig } from "./onboard.js";
|
||||
|
||||
const PROVIDER_ID = "opencode";
|
||||
const MINIMAX_PREFIX = "minimax-m2.5";
|
||||
|
||||
31
extensions/opencode/onboard.ts
Normal file
31
extensions/opencode/onboard.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { OPENCODE_ZEN_DEFAULT_MODEL_REF } from "../../src/agents/opencode-zen-models.js";
|
||||
import { applyAgentDefaultModelPrimary } from "../../src/commands/onboard-auth.config-shared.js";
|
||||
import type { OpenClawConfig } from "../../src/config/config.js";
|
||||
|
||||
export { OPENCODE_ZEN_DEFAULT_MODEL_REF };
|
||||
|
||||
export function applyOpencodeZenProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
models[OPENCODE_ZEN_DEFAULT_MODEL_REF] = {
|
||||
...models[OPENCODE_ZEN_DEFAULT_MODEL_REF],
|
||||
alias: models[OPENCODE_ZEN_DEFAULT_MODEL_REF]?.alias ?? "Opus",
|
||||
};
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
models,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function applyOpencodeZenConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
return applyAgentDefaultModelPrimary(
|
||||
applyOpencodeZenProviderConfig(cfg),
|
||||
OPENCODE_ZEN_DEFAULT_MODEL_REF,
|
||||
);
|
||||
}
|
||||
@ -15,11 +15,8 @@ import {
|
||||
createOpenRouterWrapper,
|
||||
isProxyReasoningUnsupported,
|
||||
} from "../../src/agents/pi-embedded-runner/proxy-stream-wrappers.js";
|
||||
import {
|
||||
applyOpenrouterConfig,
|
||||
OPENROUTER_DEFAULT_MODEL_REF,
|
||||
} from "../../src/commands/onboard-auth.js";
|
||||
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
|
||||
import { applyOpenrouterConfig, OPENROUTER_DEFAULT_MODEL_REF } from "./onboard.js";
|
||||
import { buildOpenrouterProvider } from "./provider-catalog.js";
|
||||
|
||||
const PROVIDER_ID = "openrouter";
|
||||
|
||||
30
extensions/openrouter/onboard.ts
Normal file
30
extensions/openrouter/onboard.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { applyAgentDefaultModelPrimary } from "../../src/commands/onboard-auth.config-shared.js";
|
||||
import type { OpenClawConfig } from "../../src/config/config.js";
|
||||
|
||||
export const OPENROUTER_DEFAULT_MODEL_REF = "openrouter/auto";
|
||||
|
||||
export function applyOpenrouterProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
models[OPENROUTER_DEFAULT_MODEL_REF] = {
|
||||
...models[OPENROUTER_DEFAULT_MODEL_REF],
|
||||
alias: models[OPENROUTER_DEFAULT_MODEL_REF]?.alias ?? "OpenRouter",
|
||||
};
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
models,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function applyOpenrouterConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
return applyAgentDefaultModelPrimary(
|
||||
applyOpenrouterProviderConfig(cfg),
|
||||
OPENROUTER_DEFAULT_MODEL_REF,
|
||||
);
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
||||
import { applyQianfanConfig, QIANFAN_DEFAULT_MODEL_REF } from "../../src/commands/onboard-auth.js";
|
||||
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
|
||||
import { applyQianfanConfig, QIANFAN_DEFAULT_MODEL_REF } from "./onboard.js";
|
||||
import { buildSingleProviderApiKeyCatalog } from "../../src/plugins/provider-catalog.js";
|
||||
import { buildQianfanProvider } from "./provider-catalog.js";
|
||||
|
||||
const PROVIDER_ID = "qianfan";
|
||||
@ -40,18 +41,12 @@ const qianfanPlugin = {
|
||||
],
|
||||
catalog: {
|
||||
order: "simple",
|
||||
run: async (ctx) => {
|
||||
const apiKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey;
|
||||
if (!apiKey) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
provider: {
|
||||
...buildQianfanProvider(),
|
||||
apiKey,
|
||||
},
|
||||
};
|
||||
},
|
||||
run: (ctx) =>
|
||||
buildSingleProviderApiKeyCatalog({
|
||||
ctx,
|
||||
providerId: PROVIDER_ID,
|
||||
buildProvider: buildQianfanProvider,
|
||||
}),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
48
extensions/qianfan/onboard.ts
Normal file
48
extensions/qianfan/onboard.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import {
|
||||
applyAgentDefaultModelPrimary,
|
||||
applyProviderConfigWithDefaultModels,
|
||||
} from "../../src/commands/onboard-auth.config-shared.js";
|
||||
import type { OpenClawConfig } from "../../src/config/config.js";
|
||||
import type { ModelApi } from "../../src/config/types.models.js";
|
||||
import {
|
||||
buildQianfanProvider,
|
||||
QIANFAN_BASE_URL,
|
||||
QIANFAN_DEFAULT_MODEL_ID,
|
||||
} from "./provider-catalog.js";
|
||||
|
||||
export const QIANFAN_DEFAULT_MODEL_REF = `qianfan/${QIANFAN_DEFAULT_MODEL_ID}`;
|
||||
|
||||
export function applyQianfanProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
models[QIANFAN_DEFAULT_MODEL_REF] = {
|
||||
...models[QIANFAN_DEFAULT_MODEL_REF],
|
||||
alias: models[QIANFAN_DEFAULT_MODEL_REF]?.alias ?? "QIANFAN",
|
||||
};
|
||||
const defaultProvider = buildQianfanProvider();
|
||||
const existingProvider = cfg.models?.providers?.qianfan as
|
||||
| {
|
||||
baseUrl?: unknown;
|
||||
api?: unknown;
|
||||
}
|
||||
| undefined;
|
||||
const existingBaseUrl =
|
||||
typeof existingProvider?.baseUrl === "string" ? existingProvider.baseUrl.trim() : "";
|
||||
const resolvedBaseUrl = existingBaseUrl || QIANFAN_BASE_URL;
|
||||
const resolvedApi =
|
||||
typeof existingProvider?.api === "string"
|
||||
? (existingProvider.api as ModelApi)
|
||||
: "openai-completions";
|
||||
|
||||
return applyProviderConfigWithDefaultModels(cfg, {
|
||||
agentModels: models,
|
||||
providerId: "qianfan",
|
||||
api: resolvedApi,
|
||||
baseUrl: resolvedBaseUrl,
|
||||
defaultModels: defaultProvider.models ?? [],
|
||||
defaultModelId: QIANFAN_DEFAULT_MODEL_ID,
|
||||
});
|
||||
}
|
||||
|
||||
export function applyQianfanConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
return applyAgentDefaultModelPrimary(applyQianfanProviderConfig(cfg), QIANFAN_DEFAULT_MODEL_REF);
|
||||
}
|
||||
@ -1,10 +1,10 @@
|
||||
import type { SignalAccountConfig } from "openclaw/plugin-sdk/signal";
|
||||
import {
|
||||
type OpenClawConfig,
|
||||
createAccountListHelpers,
|
||||
normalizeAccountId,
|
||||
resolveAccountEntry,
|
||||
} from "../../../src/plugin-sdk-internal/accounts.js";
|
||||
import type { SignalAccountConfig } from "../../../src/plugin-sdk-internal/signal.js";
|
||||
|
||||
export type ResolvedSignalAccount = {
|
||||
accountId: string;
|
||||
|
||||
@ -1,114 +1,9 @@
|
||||
import {
|
||||
createScopedAccountConfigAccessors,
|
||||
buildAccountScopedDmSecurityPolicy,
|
||||
collectAllowlistProviderRestrictSendersWarnings,
|
||||
} from "openclaw/plugin-sdk/compat";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
deleteAccountFromConfigSection,
|
||||
getChatChannelMeta,
|
||||
normalizeE164,
|
||||
setAccountEnabledInConfigSection,
|
||||
SignalConfigSchema,
|
||||
type ChannelPlugin,
|
||||
} from "openclaw/plugin-sdk/signal";
|
||||
import {
|
||||
listSignalAccountIds,
|
||||
resolveDefaultSignalAccountId,
|
||||
resolveSignalAccount,
|
||||
type ResolvedSignalAccount,
|
||||
} from "./accounts.js";
|
||||
import { createSignalSetupWizardProxy, signalSetupAdapter } from "./setup-core.js";
|
||||
import { type ChannelPlugin } from "openclaw/plugin-sdk/signal";
|
||||
import { type ResolvedSignalAccount } from "./accounts.js";
|
||||
import { signalSetupAdapter } from "./setup-core.js";
|
||||
import { createSignalPluginBase, signalSetupWizard } from "./shared.js";
|
||||
|
||||
async function loadSignalChannelRuntime() {
|
||||
return await import("./channel.runtime.js");
|
||||
}
|
||||
|
||||
const signalSetupWizard = createSignalSetupWizardProxy(async () => ({
|
||||
signalSetupWizard: (await loadSignalChannelRuntime()).signalSetupWizard,
|
||||
}));
|
||||
|
||||
const signalConfigAccessors = createScopedAccountConfigAccessors({
|
||||
resolveAccount: ({ cfg, accountId }) => resolveSignalAccount({ cfg, accountId }),
|
||||
resolveAllowFrom: (account: ResolvedSignalAccount) => account.config.allowFrom,
|
||||
formatAllowFrom: (allowFrom) =>
|
||||
allowFrom
|
||||
.map((entry) => String(entry).trim())
|
||||
.filter(Boolean)
|
||||
.map((entry) => (entry === "*" ? "*" : normalizeE164(entry.replace(/^signal:/i, ""))))
|
||||
.filter(Boolean),
|
||||
resolveDefaultTo: (account: ResolvedSignalAccount) => account.config.defaultTo,
|
||||
});
|
||||
|
||||
export const signalSetupPlugin: ChannelPlugin<ResolvedSignalAccount> = {
|
||||
id: "signal",
|
||||
meta: {
|
||||
...getChatChannelMeta("signal"),
|
||||
},
|
||||
export const signalSetupPlugin: ChannelPlugin<ResolvedSignalAccount> = createSignalPluginBase({
|
||||
setupWizard: signalSetupWizard,
|
||||
capabilities: {
|
||||
chatTypes: ["direct", "group"],
|
||||
media: true,
|
||||
reactions: true,
|
||||
},
|
||||
streaming: {
|
||||
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
|
||||
},
|
||||
reload: { configPrefixes: ["channels.signal"] },
|
||||
configSchema: buildChannelConfigSchema(SignalConfigSchema),
|
||||
config: {
|
||||
listAccountIds: (cfg) => listSignalAccountIds(cfg),
|
||||
resolveAccount: (cfg, accountId) => resolveSignalAccount({ cfg, accountId }),
|
||||
defaultAccountId: (cfg) => resolveDefaultSignalAccountId(cfg),
|
||||
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
||||
setAccountEnabledInConfigSection({
|
||||
cfg,
|
||||
sectionKey: "signal",
|
||||
accountId,
|
||||
enabled,
|
||||
allowTopLevel: true,
|
||||
}),
|
||||
deleteAccount: ({ cfg, accountId }) =>
|
||||
deleteAccountFromConfigSection({
|
||||
cfg,
|
||||
sectionKey: "signal",
|
||||
accountId,
|
||||
clearBaseFields: ["account", "httpUrl", "httpHost", "httpPort", "cliPath", "name"],
|
||||
}),
|
||||
isConfigured: (account) => account.configured,
|
||||
describeAccount: (account) => ({
|
||||
accountId: account.accountId,
|
||||
name: account.name,
|
||||
enabled: account.enabled,
|
||||
configured: account.configured,
|
||||
baseUrl: account.baseUrl,
|
||||
}),
|
||||
...signalConfigAccessors,
|
||||
},
|
||||
security: {
|
||||
resolveDmPolicy: ({ cfg, accountId, account }) =>
|
||||
buildAccountScopedDmSecurityPolicy({
|
||||
cfg,
|
||||
channelKey: "signal",
|
||||
accountId,
|
||||
fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
|
||||
policy: account.config.dmPolicy,
|
||||
allowFrom: account.config.allowFrom ?? [],
|
||||
policyPathSuffix: "dmPolicy",
|
||||
normalizeEntry: (raw) => normalizeE164(raw.replace(/^signal:/i, "").trim()),
|
||||
}),
|
||||
collectWarnings: ({ account, cfg }) =>
|
||||
collectAllowlistProviderRestrictSendersWarnings({
|
||||
cfg,
|
||||
providerConfigPresent: cfg.channels?.signal !== undefined,
|
||||
configuredGroupPolicy: account.config.groupPolicy,
|
||||
surface: "Signal groups",
|
||||
openScope: "any member",
|
||||
groupPolicyPath: "channels.signal.groupPolicy",
|
||||
groupAllowFromPath: "channels.signal.groupAllowFrom",
|
||||
mentionGated: false,
|
||||
}),
|
||||
},
|
||||
setup: signalSetupAdapter,
|
||||
};
|
||||
});
|
||||
|
||||
@ -1,26 +1,15 @@
|
||||
import {
|
||||
buildAccountScopedAllowlistConfigEditor,
|
||||
buildAccountScopedDmSecurityPolicy,
|
||||
createScopedAccountConfigAccessors,
|
||||
collectAllowlistProviderRestrictSendersWarnings,
|
||||
} from "openclaw/plugin-sdk/compat";
|
||||
import { buildAccountScopedAllowlistConfigEditor } from "openclaw/plugin-sdk/compat";
|
||||
import { buildAgentSessionKey, type RoutePeer } from "openclaw/plugin-sdk/core";
|
||||
import {
|
||||
buildBaseAccountStatusSnapshot,
|
||||
buildBaseChannelStatusSummary,
|
||||
buildChannelConfigSchema,
|
||||
collectStatusIssuesFromLastError,
|
||||
createDefaultChannelRuntimeState,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
deleteAccountFromConfigSection,
|
||||
getChatChannelMeta,
|
||||
looksLikeSignalTargetId,
|
||||
normalizeE164,
|
||||
normalizeSignalMessagingTarget,
|
||||
PAIRING_APPROVED_MESSAGE,
|
||||
resolveChannelMediaMaxBytes,
|
||||
setAccountEnabledInConfigSection,
|
||||
SignalConfigSchema,
|
||||
type ChannelMessageActionAdapter,
|
||||
type ChannelPlugin,
|
||||
} from "openclaw/plugin-sdk/signal";
|
||||
@ -42,15 +31,8 @@ import {
|
||||
} from "./identity.js";
|
||||
import type { SignalProbe } from "./probe.js";
|
||||
import { getSignalRuntime } from "./runtime.js";
|
||||
import { createSignalSetupWizardProxy, signalSetupAdapter } from "./setup-core.js";
|
||||
|
||||
async function loadSignalChannelRuntime() {
|
||||
return await import("./channel.runtime.js");
|
||||
}
|
||||
|
||||
const signalSetupWizard = createSignalSetupWizardProxy(async () => ({
|
||||
signalSetupWizard: (await loadSignalChannelRuntime()).signalSetupWizard,
|
||||
}));
|
||||
import { signalSetupAdapter } from "./setup-core.js";
|
||||
import { createSignalPluginBase, signalConfigAccessors, signalSetupWizard } from "./shared.js";
|
||||
|
||||
const signalMessageActions: ChannelMessageActionAdapter = {
|
||||
listActions: (ctx) => getSignalRuntime().channel.signal.messageActions?.listActions?.(ctx) ?? [],
|
||||
@ -65,18 +47,6 @@ const signalMessageActions: ChannelMessageActionAdapter = {
|
||||
},
|
||||
};
|
||||
|
||||
const signalConfigAccessors = createScopedAccountConfigAccessors({
|
||||
resolveAccount: ({ cfg, accountId }) => resolveSignalAccount({ cfg, accountId }),
|
||||
resolveAllowFrom: (account: ResolvedSignalAccount) => account.config.allowFrom,
|
||||
formatAllowFrom: (allowFrom) =>
|
||||
allowFrom
|
||||
.map((entry) => String(entry).trim())
|
||||
.filter(Boolean)
|
||||
.map((entry) => (entry === "*" ? "*" : normalizeE164(entry.replace(/^signal:/i, ""))))
|
||||
.filter(Boolean),
|
||||
resolveDefaultTo: (account: ResolvedSignalAccount) => account.config.defaultTo,
|
||||
});
|
||||
|
||||
type SignalSendFn = ReturnType<typeof getSignalRuntime>["channel"]["signal"]["sendMessageSignal"];
|
||||
|
||||
function resolveSignalSendContext(params: {
|
||||
@ -312,11 +282,10 @@ async function sendFormattedSignalMedia(ctx: {
|
||||
}
|
||||
|
||||
export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
|
||||
id: "signal",
|
||||
meta: {
|
||||
...getChatChannelMeta("signal"),
|
||||
},
|
||||
setupWizard: signalSetupWizard,
|
||||
...createSignalPluginBase({
|
||||
setupWizard: signalSetupWizard,
|
||||
setup: signalSetupAdapter,
|
||||
}),
|
||||
pairing: {
|
||||
idLabel: "signalNumber",
|
||||
normalizeAllowEntry: (entry) => entry.replace(/^signal:/i, ""),
|
||||
@ -324,46 +293,7 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
|
||||
await getSignalRuntime().channel.signal.sendMessageSignal(id, PAIRING_APPROVED_MESSAGE);
|
||||
},
|
||||
},
|
||||
capabilities: {
|
||||
chatTypes: ["direct", "group"],
|
||||
media: true,
|
||||
reactions: true,
|
||||
},
|
||||
actions: signalMessageActions,
|
||||
streaming: {
|
||||
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
|
||||
},
|
||||
reload: { configPrefixes: ["channels.signal"] },
|
||||
configSchema: buildChannelConfigSchema(SignalConfigSchema),
|
||||
config: {
|
||||
listAccountIds: (cfg) => listSignalAccountIds(cfg),
|
||||
resolveAccount: (cfg, accountId) => resolveSignalAccount({ cfg, accountId }),
|
||||
defaultAccountId: (cfg) => resolveDefaultSignalAccountId(cfg),
|
||||
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
||||
setAccountEnabledInConfigSection({
|
||||
cfg,
|
||||
sectionKey: "signal",
|
||||
accountId,
|
||||
enabled,
|
||||
allowTopLevel: true,
|
||||
}),
|
||||
deleteAccount: ({ cfg, accountId }) =>
|
||||
deleteAccountFromConfigSection({
|
||||
cfg,
|
||||
sectionKey: "signal",
|
||||
accountId,
|
||||
clearBaseFields: ["account", "httpUrl", "httpHost", "httpPort", "cliPath", "name"],
|
||||
}),
|
||||
isConfigured: (account) => account.configured,
|
||||
describeAccount: (account) => ({
|
||||
accountId: account.accountId,
|
||||
name: account.name,
|
||||
enabled: account.enabled,
|
||||
configured: account.configured,
|
||||
baseUrl: account.baseUrl,
|
||||
}),
|
||||
...signalConfigAccessors,
|
||||
},
|
||||
allowlist: {
|
||||
supportsScope: ({ scope }) => scope === "dm" || scope === "group" || scope === "all",
|
||||
readConfig: ({ cfg, accountId }) => {
|
||||
@ -385,32 +315,6 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
|
||||
}),
|
||||
}),
|
||||
},
|
||||
security: {
|
||||
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
||||
return buildAccountScopedDmSecurityPolicy({
|
||||
cfg,
|
||||
channelKey: "signal",
|
||||
accountId,
|
||||
fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
|
||||
policy: account.config.dmPolicy,
|
||||
allowFrom: account.config.allowFrom ?? [],
|
||||
policyPathSuffix: "dmPolicy",
|
||||
normalizeEntry: (raw) => normalizeE164(raw.replace(/^signal:/i, "").trim()),
|
||||
});
|
||||
},
|
||||
collectWarnings: ({ account, cfg }) => {
|
||||
return collectAllowlistProviderRestrictSendersWarnings({
|
||||
cfg,
|
||||
providerConfigPresent: cfg.channels?.signal !== undefined,
|
||||
configuredGroupPolicy: account.config.groupPolicy,
|
||||
surface: "Signal groups",
|
||||
openScope: "any member",
|
||||
groupPolicyPath: "channels.signal.groupPolicy",
|
||||
groupAllowFromPath: "channels.signal.groupAllowFrom",
|
||||
mentionGated: false,
|
||||
});
|
||||
},
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: normalizeSignalMessagingTarget,
|
||||
parseExplicitTarget: ({ raw }) => parseSignalExplicitTarget(raw),
|
||||
@ -421,7 +325,6 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
|
||||
hint: "<E.164|uuid:ID|group:ID|signal:group:ID|signal:+E.164>",
|
||||
},
|
||||
},
|
||||
setup: signalSetupAdapter,
|
||||
outbound: {
|
||||
deliveryMode: "direct",
|
||||
chunker: (text, limit) => getSignalRuntime().channel.text.chunkText(text, limit),
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { evaluateSenderGroupAccessForPolicy } from "../../../src/plugin-sdk/group-access.js";
|
||||
import { evaluateSenderGroupAccessForPolicy } from "../../../src/plugin-sdk-internal/signal.js";
|
||||
import { normalizeE164 } from "../../../src/utils.js";
|
||||
|
||||
export type SignalSender =
|
||||
|
||||
25
extensions/signal/src/plugin-shared.ts
Normal file
25
extensions/signal/src/plugin-shared.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { createScopedAccountConfigAccessors } from "../../../src/plugin-sdk-internal/channel-config.js";
|
||||
import { normalizeE164, type OpenClawConfig } from "../../../src/plugin-sdk-internal/signal.js";
|
||||
import { resolveSignalAccount, type ResolvedSignalAccount } from "./accounts.js";
|
||||
import { createSignalSetupWizardProxy } from "./setup-core.js";
|
||||
|
||||
async function loadSignalChannelRuntime() {
|
||||
return await import("./channel.runtime.js");
|
||||
}
|
||||
|
||||
export const signalSetupWizard = createSignalSetupWizardProxy(async () => ({
|
||||
signalSetupWizard: (await loadSignalChannelRuntime()).signalSetupWizard,
|
||||
}));
|
||||
|
||||
export const signalConfigAccessors = createScopedAccountConfigAccessors({
|
||||
resolveAccount: ({ cfg, accountId }: { cfg: OpenClawConfig; accountId?: string | null }) =>
|
||||
resolveSignalAccount({ cfg, accountId }),
|
||||
resolveAllowFrom: (account: ResolvedSignalAccount) => account.config.allowFrom,
|
||||
formatAllowFrom: (allowFrom) =>
|
||||
allowFrom
|
||||
.map((entry) => String(entry).trim())
|
||||
.filter(Boolean)
|
||||
.map((entry) => (entry === "*" ? "*" : normalizeE164(entry.replace(/^signal:/i, ""))))
|
||||
.filter(Boolean),
|
||||
resolveDefaultTo: (account: ResolvedSignalAccount) => account.config.defaultTo,
|
||||
});
|
||||
@ -1,5 +1,7 @@
|
||||
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/compat";
|
||||
import type { PluginRuntime } from "openclaw/plugin-sdk/core";
|
||||
import {
|
||||
createPluginRuntimeStore,
|
||||
type PluginRuntime,
|
||||
} from "../../../src/plugin-sdk-internal/core.js";
|
||||
|
||||
const { setRuntime: setSignalRuntime, getRuntime: getSignalRuntime } =
|
||||
createPluginRuntimeStore<PluginRuntime>("Signal runtime not initialized");
|
||||
|
||||
@ -1,10 +1,5 @@
|
||||
import { createPatchedAccountSetupAdapter } from "../../../src/channels/plugins/setup-helpers.js";
|
||||
import {
|
||||
applyAccountNameToChannelSection,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
formatCliCommand,
|
||||
formatDocsLink,
|
||||
migrateBaseNameToDefaultAccount,
|
||||
normalizeAccountId,
|
||||
normalizeE164,
|
||||
parseSetupEntriesAllowingWildcard,
|
||||
promptParsedAllowFromForScopedChannel,
|
||||
@ -18,6 +13,7 @@ import type {
|
||||
ChannelSetupDmPolicy,
|
||||
ChannelSetupWizard,
|
||||
} from "../../../src/plugin-sdk-internal/setup.js";
|
||||
import { formatDocsLink } from "../../../src/terminal/links.js";
|
||||
import {
|
||||
listSignalAccountIds,
|
||||
resolveDefaultSignalAccountId,
|
||||
@ -28,7 +24,7 @@ const channel = "signal" as const;
|
||||
const MIN_E164_DIGITS = 5;
|
||||
const MAX_E164_DIGITS = 15;
|
||||
const DIGITS_ONLY = /^\d+$/;
|
||||
const INVALID_SIGNAL_ACCOUNT_ERROR =
|
||||
export const INVALID_SIGNAL_ACCOUNT_ERROR =
|
||||
"Invalid E.164 phone number (must start with + and country code, e.g. +15555550123)";
|
||||
|
||||
export function normalizeSignalAccountInput(value: string | null | undefined): string | null {
|
||||
@ -87,7 +83,7 @@ function buildSignalSetupPatch(input: {
|
||||
};
|
||||
}
|
||||
|
||||
async function promptSignalAllowFrom(params: {
|
||||
export async function promptSignalAllowFrom(params: {
|
||||
cfg: OpenClawConfig;
|
||||
prompter: WizardPrompter;
|
||||
accountId?: string;
|
||||
@ -115,15 +111,8 @@ async function promptSignalAllowFrom(params: {
|
||||
});
|
||||
}
|
||||
|
||||
export const signalSetupAdapter: ChannelSetupAdapter = {
|
||||
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
||||
applyAccountName: ({ cfg, accountId, name }) =>
|
||||
applyAccountNameToChannelSection({
|
||||
cfg,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
name,
|
||||
}),
|
||||
export const signalSetupAdapter: ChannelSetupAdapter = createPatchedAccountSetupAdapter({
|
||||
channelKey: channel,
|
||||
validateInput: ({ input }) => {
|
||||
if (
|
||||
!input.signalNumber &&
|
||||
@ -136,74 +125,40 @@ export const signalSetupAdapter: ChannelSetupAdapter = {
|
||||
}
|
||||
return null;
|
||||
},
|
||||
applyAccountConfig: ({ cfg, accountId, input }) => {
|
||||
const namedConfig = applyAccountNameToChannelSection({
|
||||
cfg,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
name: input.name,
|
||||
});
|
||||
const next =
|
||||
accountId !== DEFAULT_ACCOUNT_ID
|
||||
? migrateBaseNameToDefaultAccount({
|
||||
cfg: namedConfig,
|
||||
channelKey: channel,
|
||||
})
|
||||
: namedConfig;
|
||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||
return {
|
||||
...next,
|
||||
channels: {
|
||||
...next.channels,
|
||||
signal: {
|
||||
...next.channels?.signal,
|
||||
enabled: true,
|
||||
...buildSignalSetupPatch(input),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
...next,
|
||||
channels: {
|
||||
...next.channels,
|
||||
signal: {
|
||||
...next.channels?.signal,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...next.channels?.signal?.accounts,
|
||||
[accountId]: {
|
||||
...next.channels?.signal?.accounts?.[accountId],
|
||||
enabled: true,
|
||||
...buildSignalSetupPatch(input),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
buildPatch: (input) => buildSignalSetupPatch(input),
|
||||
});
|
||||
|
||||
type SignalSetupWizardHandlers = {
|
||||
resolveStatusLines: NonNullable<ChannelSetupWizard["status"]>["resolveStatusLines"];
|
||||
resolveSelectionHint: NonNullable<ChannelSetupWizard["status"]>["resolveSelectionHint"];
|
||||
resolveQuickstartScore: NonNullable<ChannelSetupWizard["status"]>["resolveQuickstartScore"];
|
||||
prepare?: ChannelSetupWizard["prepare"];
|
||||
shouldPromptCliPath: NonNullable<
|
||||
NonNullable<ChannelSetupWizard["textInputs"]>[number]["shouldPrompt"]
|
||||
>;
|
||||
};
|
||||
|
||||
export function createSignalSetupWizardProxy(
|
||||
loadWizard: () => Promise<{ signalSetupWizard: ChannelSetupWizard }>,
|
||||
) {
|
||||
export function createSignalSetupWizardBase(
|
||||
handlers: SignalSetupWizardHandlers,
|
||||
): ChannelSetupWizard {
|
||||
const setupChannel = "signal" as const;
|
||||
const signalDmPolicy: ChannelSetupDmPolicy = {
|
||||
label: "Signal",
|
||||
channel,
|
||||
channel: setupChannel,
|
||||
policyKey: "channels.signal.dmPolicy",
|
||||
allowFromKey: "channels.signal.allowFrom",
|
||||
getCurrent: (cfg: OpenClawConfig) => cfg.channels?.signal?.dmPolicy ?? "pairing",
|
||||
setPolicy: (cfg: OpenClawConfig, policy) =>
|
||||
setChannelDmPolicyWithAllowFrom({
|
||||
cfg,
|
||||
channel,
|
||||
channel: setupChannel,
|
||||
dmPolicy: policy,
|
||||
}),
|
||||
promptAllowFrom: promptSignalAllowFrom,
|
||||
};
|
||||
|
||||
return {
|
||||
channel,
|
||||
channel: setupChannel,
|
||||
status: {
|
||||
configuredLabel: "configured",
|
||||
unconfiguredLabel: "needs setup",
|
||||
@ -215,14 +170,11 @@ export function createSignalSetupWizardProxy(
|
||||
listSignalAccountIds(cfg).some(
|
||||
(accountId) => resolveSignalAccount({ cfg, accountId }).configured,
|
||||
),
|
||||
resolveStatusLines: async (params) =>
|
||||
(await loadWizard()).signalSetupWizard.status.resolveStatusLines?.(params) ?? [],
|
||||
resolveSelectionHint: async (params) =>
|
||||
await (await loadWizard()).signalSetupWizard.status.resolveSelectionHint?.(params),
|
||||
resolveQuickstartScore: async (params) =>
|
||||
await (await loadWizard()).signalSetupWizard.status.resolveQuickstartScore?.(params),
|
||||
resolveStatusLines: handlers.resolveStatusLines,
|
||||
resolveSelectionHint: handlers.resolveSelectionHint,
|
||||
resolveQuickstartScore: handlers.resolveQuickstartScore,
|
||||
},
|
||||
prepare: async (params) => await (await loadWizard()).signalSetupWizard.prepare?.(params),
|
||||
prepare: handlers.prepare,
|
||||
credentials: [],
|
||||
textInputs: [
|
||||
{
|
||||
@ -236,12 +188,7 @@ export function createSignalSetupWizardProxy(
|
||||
(typeof credentialValues.cliPath === "string" ? credentialValues.cliPath : undefined) ??
|
||||
resolveSignalAccount({ cfg, accountId }).config.cliPath ??
|
||||
"signal-cli",
|
||||
shouldPrompt: async (params) => {
|
||||
const input = (await loadWizard()).signalSetupWizard.textInputs?.find(
|
||||
(entry) => entry.inputKey === "cliPath",
|
||||
);
|
||||
return (await input?.shouldPrompt?.(params)) ?? false;
|
||||
},
|
||||
shouldPrompt: handlers.shouldPromptCliPath,
|
||||
confirmCurrentValue: false,
|
||||
applyCurrentValue: true,
|
||||
helpTitle: "Signal",
|
||||
@ -266,11 +213,31 @@ export function createSignalSetupWizardProxy(
|
||||
lines: [
|
||||
'Link device with: signal-cli link -n "OpenClaw"',
|
||||
"Scan QR in Signal -> Linked Devices",
|
||||
`Then run: ${formatCliCommand("openclaw gateway call channels.status --params '{\"probe\":true}'")}`,
|
||||
`Docs: ${formatDocsLink("/signal", "signal")}`,
|
||||
`Then run: openclaw gateway call channels.status --params '{"probe":true}'`,
|
||||
"Docs: https://docs.openclaw.ai/signal",
|
||||
],
|
||||
},
|
||||
dmPolicy: signalDmPolicy,
|
||||
disable: (cfg: OpenClawConfig) => setSetupChannelEnabled(cfg, channel, false),
|
||||
disable: (cfg: OpenClawConfig) => setSetupChannelEnabled(cfg, setupChannel, false),
|
||||
} satisfies ChannelSetupWizard;
|
||||
}
|
||||
|
||||
export function createSignalSetupWizardProxy(
|
||||
loadWizard: () => Promise<{ signalSetupWizard: ChannelSetupWizard }>,
|
||||
) {
|
||||
return createSignalSetupWizardBase({
|
||||
resolveStatusLines: async (params) =>
|
||||
(await loadWizard()).signalSetupWizard.status.resolveStatusLines?.(params) ?? [],
|
||||
resolveSelectionHint: async (params) =>
|
||||
await (await loadWizard()).signalSetupWizard.status.resolveSelectionHint?.(params),
|
||||
resolveQuickstartScore: async (params) =>
|
||||
await (await loadWizard()).signalSetupWizard.status.resolveQuickstartScore?.(params),
|
||||
prepare: async (params) => await (await loadWizard()).signalSetupWizard.prepare?.(params),
|
||||
shouldPromptCliPath: async (params) => {
|
||||
const input = (await loadWizard()).signalSetupWizard.textInputs?.find(
|
||||
(entry) => entry.inputKey === "cliPath",
|
||||
);
|
||||
return (await input?.shouldPrompt?.(params)) ?? false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,107 +1,34 @@
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
detectBinary,
|
||||
formatCliCommand,
|
||||
formatDocsLink,
|
||||
installSignalCli,
|
||||
type OpenClawConfig,
|
||||
parseSetupEntriesAllowingWildcard,
|
||||
promptParsedAllowFromForScopedChannel,
|
||||
setChannelDmPolicyWithAllowFrom,
|
||||
setSetupChannelEnabled,
|
||||
type WizardPrompter,
|
||||
} from "../../../src/plugin-sdk-internal/setup.js";
|
||||
import type {
|
||||
ChannelSetupDmPolicy,
|
||||
ChannelSetupWizard,
|
||||
} from "../../../src/plugin-sdk-internal/setup.js";
|
||||
import type { ChannelSetupWizard } from "../../../src/plugin-sdk-internal/setup.js";
|
||||
import { resolveSignalAccount } from "./accounts.js";
|
||||
import {
|
||||
listSignalAccountIds,
|
||||
resolveDefaultSignalAccountId,
|
||||
resolveSignalAccount,
|
||||
} from "./accounts.js";
|
||||
import {
|
||||
createSignalSetupWizardBase,
|
||||
INVALID_SIGNAL_ACCOUNT_ERROR,
|
||||
normalizeSignalAccountInput,
|
||||
parseSignalAllowFromEntries,
|
||||
promptSignalAllowFrom,
|
||||
signalSetupAdapter,
|
||||
} from "./setup-core.js";
|
||||
|
||||
const channel = "signal" as const;
|
||||
const INVALID_SIGNAL_ACCOUNT_ERROR =
|
||||
"Invalid E.164 phone number (must start with + and country code, e.g. +15555550123)";
|
||||
|
||||
async function promptSignalAllowFrom(params: {
|
||||
cfg: OpenClawConfig;
|
||||
prompter: WizardPrompter;
|
||||
accountId?: string;
|
||||
}): Promise<OpenClawConfig> {
|
||||
return promptParsedAllowFromForScopedChannel({
|
||||
cfg: params.cfg,
|
||||
channel,
|
||||
accountId: params.accountId,
|
||||
defaultAccountId: resolveDefaultSignalAccountId(params.cfg),
|
||||
prompter: params.prompter,
|
||||
noteTitle: "Signal allowlist",
|
||||
noteLines: [
|
||||
"Allowlist Signal DMs by sender id.",
|
||||
"Examples:",
|
||||
"- +15555550123",
|
||||
"- uuid:123e4567-e89b-12d3-a456-426614174000",
|
||||
"Multiple entries: comma-separated.",
|
||||
`Docs: ${formatDocsLink("/signal", "signal")}`,
|
||||
],
|
||||
message: "Signal allowFrom (E.164 or uuid)",
|
||||
placeholder: "+15555550123, uuid:123e4567-e89b-12d3-a456-426614174000",
|
||||
parseEntries: parseSignalAllowFromEntries,
|
||||
getExistingAllowFrom: ({ cfg, accountId }) =>
|
||||
resolveSignalAccount({ cfg, accountId }).config.allowFrom ?? [],
|
||||
});
|
||||
}
|
||||
|
||||
const signalDmPolicy: ChannelSetupDmPolicy = {
|
||||
label: "Signal",
|
||||
channel,
|
||||
policyKey: "channels.signal.dmPolicy",
|
||||
allowFromKey: "channels.signal.allowFrom",
|
||||
getCurrent: (cfg) => cfg.channels?.signal?.dmPolicy ?? "pairing",
|
||||
setPolicy: (cfg, policy) =>
|
||||
setChannelDmPolicyWithAllowFrom({
|
||||
cfg,
|
||||
channel,
|
||||
dmPolicy: policy,
|
||||
}),
|
||||
promptAllowFrom: promptSignalAllowFrom,
|
||||
};
|
||||
|
||||
export const signalSetupWizard: ChannelSetupWizard = {
|
||||
channel,
|
||||
status: {
|
||||
configuredLabel: "configured",
|
||||
unconfiguredLabel: "needs setup",
|
||||
configuredHint: "signal-cli found",
|
||||
unconfiguredHint: "signal-cli missing",
|
||||
configuredScore: 1,
|
||||
unconfiguredScore: 0,
|
||||
resolveConfigured: ({ cfg }) =>
|
||||
listSignalAccountIds(cfg).some(
|
||||
(accountId) => resolveSignalAccount({ cfg, accountId }).configured,
|
||||
),
|
||||
resolveStatusLines: async ({ cfg, configured }) => {
|
||||
const signalCliPath = cfg.channels?.signal?.cliPath ?? "signal-cli";
|
||||
const signalCliDetected = await detectBinary(signalCliPath);
|
||||
return [
|
||||
`Signal: ${configured ? "configured" : "needs setup"}`,
|
||||
`signal-cli: ${signalCliDetected ? "found" : "missing"} (${signalCliPath})`,
|
||||
];
|
||||
},
|
||||
resolveSelectionHint: async ({ cfg }) => {
|
||||
const signalCliPath = cfg.channels?.signal?.cliPath ?? "signal-cli";
|
||||
return (await detectBinary(signalCliPath)) ? "signal-cli found" : "signal-cli missing";
|
||||
},
|
||||
resolveQuickstartScore: async ({ cfg }) => {
|
||||
const signalCliPath = cfg.channels?.signal?.cliPath ?? "signal-cli";
|
||||
return (await detectBinary(signalCliPath)) ? 1 : 0;
|
||||
},
|
||||
export const signalSetupWizard: ChannelSetupWizard = createSignalSetupWizardBase({
|
||||
resolveStatusLines: async ({ cfg, configured }) => {
|
||||
const signalCliPath = cfg.channels?.signal?.cliPath ?? "signal-cli";
|
||||
const signalCliDetected = await detectBinary(signalCliPath);
|
||||
return [
|
||||
`Signal: ${configured ? "configured" : "needs setup"}`,
|
||||
`signal-cli: ${signalCliDetected ? "found" : "missing"} (${signalCliPath})`,
|
||||
];
|
||||
},
|
||||
resolveSelectionHint: async ({ cfg }) => {
|
||||
const signalCliPath = cfg.channels?.signal?.cliPath ?? "signal-cli";
|
||||
return (await detectBinary(signalCliPath)) ? "signal-cli found" : "signal-cli missing";
|
||||
},
|
||||
resolveQuickstartScore: async ({ cfg }) => {
|
||||
const signalCliPath = cfg.channels?.signal?.cliPath ?? "signal-cli";
|
||||
return (await detectBinary(signalCliPath)) ? 1 : 0;
|
||||
},
|
||||
prepare: async ({ cfg, accountId, credentialValues, runtime, prompter, options }) => {
|
||||
if (!options?.allowSignalInstall) {
|
||||
@ -138,50 +65,13 @@ export const signalSetupWizard: ChannelSetupWizard = {
|
||||
await prompter.note(`signal-cli install failed: ${String(error)}`, "Signal");
|
||||
}
|
||||
},
|
||||
credentials: [],
|
||||
textInputs: [
|
||||
{
|
||||
inputKey: "cliPath",
|
||||
message: "signal-cli path",
|
||||
currentValue: ({ cfg, accountId, credentialValues }) =>
|
||||
(typeof credentialValues.cliPath === "string" ? credentialValues.cliPath : undefined) ??
|
||||
resolveSignalAccount({ cfg, accountId }).config.cliPath ??
|
||||
"signal-cli",
|
||||
initialValue: ({ cfg, accountId, credentialValues }) =>
|
||||
(typeof credentialValues.cliPath === "string" ? credentialValues.cliPath : undefined) ??
|
||||
resolveSignalAccount({ cfg, accountId }).config.cliPath ??
|
||||
"signal-cli",
|
||||
shouldPrompt: async ({ currentValue }) => !(await detectBinary(currentValue ?? "signal-cli")),
|
||||
confirmCurrentValue: false,
|
||||
applyCurrentValue: true,
|
||||
helpTitle: "Signal",
|
||||
helpLines: [
|
||||
"signal-cli not found. Install it, then rerun this step or set channels.signal.cliPath.",
|
||||
],
|
||||
},
|
||||
{
|
||||
inputKey: "signalNumber",
|
||||
message: "Signal bot number (E.164)",
|
||||
currentValue: ({ cfg, accountId }) =>
|
||||
normalizeSignalAccountInput(resolveSignalAccount({ cfg, accountId }).config.account) ??
|
||||
undefined,
|
||||
keepPrompt: (value) => `Signal account set (${value}). Keep it?`,
|
||||
validate: ({ value }) =>
|
||||
normalizeSignalAccountInput(value) ? undefined : INVALID_SIGNAL_ACCOUNT_ERROR,
|
||||
normalizeValue: ({ value }) => normalizeSignalAccountInput(value) ?? value,
|
||||
},
|
||||
],
|
||||
completionNote: {
|
||||
title: "Signal next steps",
|
||||
lines: [
|
||||
'Link device with: signal-cli link -n "OpenClaw"',
|
||||
"Scan QR in Signal -> Linked Devices",
|
||||
`Then run: ${formatCliCommand("openclaw gateway call channels.status --params '{\"probe\":true}'")}`,
|
||||
`Docs: ${formatDocsLink("/signal", "signal")}`,
|
||||
],
|
||||
},
|
||||
dmPolicy: signalDmPolicy,
|
||||
disable: (cfg) => setSetupChannelEnabled(cfg, channel, false),
|
||||
};
|
||||
shouldPromptCliPath: async ({ currentValue }) =>
|
||||
!(await detectBinary(currentValue ?? "signal-cli")),
|
||||
});
|
||||
|
||||
export { normalizeSignalAccountInput, parseSignalAllowFromEntries, signalSetupAdapter };
|
||||
export {
|
||||
INVALID_SIGNAL_ACCOUNT_ERROR,
|
||||
normalizeSignalAccountInput,
|
||||
promptSignalAllowFrom,
|
||||
signalSetupAdapter,
|
||||
};
|
||||
|
||||
133
extensions/signal/src/shared.ts
Normal file
133
extensions/signal/src/shared.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import {
|
||||
buildAccountScopedDmSecurityPolicy,
|
||||
collectAllowlistProviderRestrictSendersWarnings,
|
||||
createScopedAccountConfigAccessors,
|
||||
} from "openclaw/plugin-sdk/compat";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
deleteAccountFromConfigSection,
|
||||
getChatChannelMeta,
|
||||
normalizeE164,
|
||||
setAccountEnabledInConfigSection,
|
||||
SignalConfigSchema,
|
||||
type ChannelPlugin,
|
||||
} from "openclaw/plugin-sdk/signal";
|
||||
import {
|
||||
listSignalAccountIds,
|
||||
resolveDefaultSignalAccountId,
|
||||
resolveSignalAccount,
|
||||
type ResolvedSignalAccount,
|
||||
} from "./accounts.js";
|
||||
import { createSignalSetupWizardProxy } from "./setup-core.js";
|
||||
|
||||
export const SIGNAL_CHANNEL = "signal" as const;
|
||||
|
||||
async function loadSignalChannelRuntime() {
|
||||
return await import("./channel.runtime.js");
|
||||
}
|
||||
|
||||
export const signalSetupWizard = createSignalSetupWizardProxy(async () => ({
|
||||
signalSetupWizard: (await loadSignalChannelRuntime()).signalSetupWizard,
|
||||
}));
|
||||
|
||||
export const signalConfigAccessors = createScopedAccountConfigAccessors({
|
||||
resolveAccount: ({ cfg, accountId }) => resolveSignalAccount({ cfg, accountId }),
|
||||
resolveAllowFrom: (account: ResolvedSignalAccount) => account.config.allowFrom,
|
||||
formatAllowFrom: (allowFrom) =>
|
||||
allowFrom
|
||||
.map((entry) => String(entry).trim())
|
||||
.filter(Boolean)
|
||||
.map((entry) => (entry === "*" ? "*" : normalizeE164(entry.replace(/^signal:/i, ""))))
|
||||
.filter(Boolean),
|
||||
resolveDefaultTo: (account: ResolvedSignalAccount) => account.config.defaultTo,
|
||||
});
|
||||
|
||||
export function createSignalPluginBase(params: {
|
||||
setupWizard?: NonNullable<ChannelPlugin<ResolvedSignalAccount>["setupWizard"]>;
|
||||
setup: NonNullable<ChannelPlugin<ResolvedSignalAccount>["setup"]>;
|
||||
}): Pick<
|
||||
ChannelPlugin<ResolvedSignalAccount>,
|
||||
| "id"
|
||||
| "meta"
|
||||
| "setupWizard"
|
||||
| "capabilities"
|
||||
| "streaming"
|
||||
| "reload"
|
||||
| "configSchema"
|
||||
| "config"
|
||||
| "security"
|
||||
| "setup"
|
||||
> {
|
||||
return {
|
||||
id: SIGNAL_CHANNEL,
|
||||
meta: {
|
||||
...getChatChannelMeta(SIGNAL_CHANNEL),
|
||||
},
|
||||
setupWizard: params.setupWizard,
|
||||
capabilities: {
|
||||
chatTypes: ["direct", "group"],
|
||||
media: true,
|
||||
reactions: true,
|
||||
},
|
||||
streaming: {
|
||||
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
|
||||
},
|
||||
reload: { configPrefixes: ["channels.signal"] },
|
||||
configSchema: buildChannelConfigSchema(SignalConfigSchema),
|
||||
config: {
|
||||
listAccountIds: (cfg) => listSignalAccountIds(cfg),
|
||||
resolveAccount: (cfg, accountId) => resolveSignalAccount({ cfg, accountId }),
|
||||
defaultAccountId: (cfg) => resolveDefaultSignalAccountId(cfg),
|
||||
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
||||
setAccountEnabledInConfigSection({
|
||||
cfg,
|
||||
sectionKey: SIGNAL_CHANNEL,
|
||||
accountId,
|
||||
enabled,
|
||||
allowTopLevel: true,
|
||||
}),
|
||||
deleteAccount: ({ cfg, accountId }) =>
|
||||
deleteAccountFromConfigSection({
|
||||
cfg,
|
||||
sectionKey: SIGNAL_CHANNEL,
|
||||
accountId,
|
||||
clearBaseFields: ["account", "httpUrl", "httpHost", "httpPort", "cliPath", "name"],
|
||||
}),
|
||||
isConfigured: (account) => account.configured,
|
||||
describeAccount: (account) => ({
|
||||
accountId: account.accountId,
|
||||
name: account.name,
|
||||
enabled: account.enabled,
|
||||
configured: account.configured,
|
||||
baseUrl: account.baseUrl,
|
||||
}),
|
||||
...signalConfigAccessors,
|
||||
},
|
||||
security: {
|
||||
resolveDmPolicy: ({ cfg, accountId, account }) =>
|
||||
buildAccountScopedDmSecurityPolicy({
|
||||
cfg,
|
||||
channelKey: SIGNAL_CHANNEL,
|
||||
accountId,
|
||||
fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
|
||||
policy: account.config.dmPolicy,
|
||||
allowFrom: account.config.allowFrom ?? [],
|
||||
policyPathSuffix: "dmPolicy",
|
||||
normalizeEntry: (raw) => normalizeE164(raw.replace(/^signal:/i, "").trim()),
|
||||
}),
|
||||
collectWarnings: ({ account, cfg }) =>
|
||||
collectAllowlistProviderRestrictSendersWarnings({
|
||||
cfg,
|
||||
providerConfigPresent: cfg.channels?.signal !== undefined,
|
||||
configuredGroupPolicy: account.config.groupPolicy,
|
||||
surface: "Signal groups",
|
||||
openScope: "any member",
|
||||
groupPolicyPath: "channels.signal.groupPolicy",
|
||||
groupAllowFromPath: "channels.signal.groupAllowFrom",
|
||||
mentionGated: false,
|
||||
}),
|
||||
},
|
||||
setup: params.setup,
|
||||
};
|
||||
}
|
||||
@ -1,13 +1,13 @@
|
||||
import {
|
||||
hasConfiguredSecretInput,
|
||||
normalizeSecretInputString,
|
||||
} from "../../../src/config/types.secrets.js";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
type OpenClawConfig,
|
||||
type SlackAccountConfig,
|
||||
} from "openclaw/plugin-sdk/slack";
|
||||
import {
|
||||
hasConfiguredSecretInput,
|
||||
normalizeSecretInputString,
|
||||
} from "../../../src/config/types.secrets.js";
|
||||
} from "../../../src/plugin-sdk-internal/slack.js";
|
||||
import type { SlackAccountSurfaceFields } from "./account-surface-fields.js";
|
||||
import {
|
||||
mergeSlackAccountConfig,
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import type { SlackAccountConfig } from "openclaw/plugin-sdk/slack";
|
||||
import {
|
||||
type OpenClawConfig,
|
||||
createAccountListHelpers,
|
||||
@ -7,6 +6,7 @@ import {
|
||||
normalizeChatType,
|
||||
resolveAccountEntry,
|
||||
} from "../../../src/plugin-sdk-internal/accounts.js";
|
||||
import type { SlackAccountConfig } from "../../../src/plugin-sdk-internal/slack.js";
|
||||
import type { SlackAccountSurfaceFields } from "./account-surface-fields.js";
|
||||
import { resolveSlackAppToken, resolveSlackBotToken, resolveSlackUserToken } from "./token.js";
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user