Merge remote-tracking branch 'upstream/main' into feat/add_qwen_official_api
This commit is contained in:
commit
ffd885baef
@ -499,7 +499,7 @@ Treat the snippet above as **secure DM mode**:
|
||||
|
||||
If you run multiple accounts on the same channel, use `per-account-channel-peer` instead. If the same person contacts you on multiple channels, use `session.identityLinks` to collapse those DM sessions into one canonical identity. See [Session Management](/concepts/session) and [Configuration](/gateway/configuration).
|
||||
|
||||
## Allowlists (DM + groups) — terminology
|
||||
## Allowlists (DM + groups) - terminology
|
||||
|
||||
OpenClaw has two separate “who can trigger me?” layers:
|
||||
|
||||
|
||||
@ -67,7 +67,7 @@ Those live under `$OPENCLAW_STATE_DIR`.
|
||||
|
||||
## Migration steps (recommended)
|
||||
|
||||
### Step 0 — Make a backup (old machine)
|
||||
### Step 0 - Make a backup (old machine)
|
||||
|
||||
On the **old** machine, stop the gateway first so files aren’t changing mid-copy:
|
||||
|
||||
@ -87,7 +87,7 @@ tar -czf openclaw-workspace.tgz .openclaw/workspace
|
||||
|
||||
If you have multiple profiles/state dirs (e.g. `~/.openclaw-main`, `~/.openclaw-work`), archive each.
|
||||
|
||||
### Step 1 — Install OpenClaw on the new machine
|
||||
### Step 1 - Install OpenClaw on the new machine
|
||||
|
||||
On the **new** machine, install the CLI (and Node if needed):
|
||||
|
||||
@ -95,7 +95,7 @@ On the **new** machine, install the CLI (and Node if needed):
|
||||
|
||||
At this stage, it’s OK if onboarding creates a fresh `~/.openclaw/` — you will overwrite it in the next step.
|
||||
|
||||
### Step 2 — Copy the state dir + workspace to the new machine
|
||||
### Step 2 - Copy the state dir + workspace to the new machine
|
||||
|
||||
Copy **both**:
|
||||
|
||||
@ -113,7 +113,7 @@ After copying, ensure:
|
||||
- Hidden directories were included (e.g. `.openclaw/`)
|
||||
- File ownership is correct for the user running the gateway
|
||||
|
||||
### Step 3 — Run Doctor (migrations + service repair)
|
||||
### Step 3 - Run Doctor (migrations + service repair)
|
||||
|
||||
On the **new** machine:
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ read_when:
|
||||
title: "Audio and Voice Notes"
|
||||
---
|
||||
|
||||
# Audio / Voice Notes — 2026-01-17
|
||||
# Audio / Voice Notes (2026-01-17)
|
||||
|
||||
## What works
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ read_when:
|
||||
title: "Image and Media Support"
|
||||
---
|
||||
|
||||
# Image & Media Support — 2025-12-05
|
||||
# Image & Media Support (2025-12-05)
|
||||
|
||||
The WhatsApp channel runs via **Baileys Web**. This document captures the current media handling rules for send, gateway, and agent replies.
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ read_when:
|
||||
title: "Media Understanding"
|
||||
---
|
||||
|
||||
# Media Understanding (Inbound) — 2026-01-17
|
||||
# Media Understanding - Inbound (2026-01-17)
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@ -76,5 +76,5 @@ If you only want to reset sessions, delete `agents/<agentId>/sessions/` and `age
|
||||
|
||||
## References
|
||||
|
||||
- [https://docs.openclaw.ai/testing](https://docs.openclaw.ai/testing)
|
||||
- [https://docs.openclaw.ai/start/getting-started](https://docs.openclaw.ai/start/getting-started)
|
||||
- [Testing](/help/testing)
|
||||
- [Getting Started](/start/getting-started)
|
||||
|
||||
@ -35,7 +35,7 @@ MiniMax highlights these improvements in M2.5:
|
||||
|
||||
## Choose a setup
|
||||
|
||||
### MiniMax OAuth (Coding Plan) — recommended
|
||||
### MiniMax OAuth (Coding Plan) - recommended
|
||||
|
||||
**Best for:** quick setup with MiniMax Coding Plan via OAuth, no API key required.
|
||||
|
||||
|
||||
@ -124,7 +124,7 @@ openclaw models list | grep venice
|
||||
|
||||
## Available Models (41 Total)
|
||||
|
||||
### Private Models (26) — Fully Private, No Logging
|
||||
### Private Models (26) - Fully Private, No Logging
|
||||
|
||||
| Model ID | Name | Context | Features |
|
||||
| -------------------------------------- | ----------------------------------- | ------- | -------------------------- |
|
||||
@ -155,7 +155,7 @@ openclaw models list | grep venice
|
||||
| `minimax-m21` | MiniMax M2.1 | 198k | Reasoning |
|
||||
| `minimax-m25` | MiniMax M2.5 | 198k | Reasoning |
|
||||
|
||||
### Anonymized Models (15) — Via Venice Proxy
|
||||
### Anonymized Models (15) - Via Venice Proxy
|
||||
|
||||
| Model ID | Name | Context | Features |
|
||||
| ------------------------------- | ------------------------------ | ------- | ------------------------- |
|
||||
|
||||
@ -6,7 +6,7 @@ read_when:
|
||||
- Enabling or auditing default skills
|
||||
---
|
||||
|
||||
# AGENTS.md — OpenClaw Personal Assistant (default)
|
||||
# AGENTS.md - OpenClaw Personal Assistant (default)
|
||||
|
||||
## First run (recommended)
|
||||
|
||||
|
||||
@ -1144,22 +1144,16 @@ authoring plugins:
|
||||
- `openclaw/plugin-sdk/whatsapp` for WhatsApp channel plugin types and shared channel-facing helpers. Built-in WhatsApp implementation internals stay private to the bundled extension.
|
||||
- `openclaw/plugin-sdk/line` for LINE channel plugins.
|
||||
- `openclaw/plugin-sdk/msteams` for the bundled Microsoft Teams plugin surface.
|
||||
- Bundled extension-specific subpaths are also available:
|
||||
- Additional bundled extension-specific subpaths remain available where OpenClaw
|
||||
intentionally exposes extension-facing helpers:
|
||||
`openclaw/plugin-sdk/acpx`, `openclaw/plugin-sdk/bluebubbles`,
|
||||
`openclaw/plugin-sdk/copilot-proxy`, `openclaw/plugin-sdk/device-pair`,
|
||||
`openclaw/plugin-sdk/diagnostics-otel`, `openclaw/plugin-sdk/diffs`,
|
||||
`openclaw/plugin-sdk/feishu`, `openclaw/plugin-sdk/googlechat`,
|
||||
`openclaw/plugin-sdk/irc`, `openclaw/plugin-sdk/llm-task`,
|
||||
`openclaw/plugin-sdk/lobster`, `openclaw/plugin-sdk/matrix`,
|
||||
`openclaw/plugin-sdk/irc`, `openclaw/plugin-sdk/matrix`,
|
||||
`openclaw/plugin-sdk/mattermost`, `openclaw/plugin-sdk/memory-core`,
|
||||
`openclaw/plugin-sdk/memory-lancedb`,
|
||||
`openclaw/plugin-sdk/minimax-portal-auth`,
|
||||
`openclaw/plugin-sdk/nextcloud-talk`, `openclaw/plugin-sdk/nostr`,
|
||||
`openclaw/plugin-sdk/open-prose`, `openclaw/plugin-sdk/phone-control`,
|
||||
`openclaw/plugin-sdk/qwen-portal-auth`, `openclaw/plugin-sdk/synology-chat`,
|
||||
`openclaw/plugin-sdk/talk-voice`, `openclaw/plugin-sdk/test-utils`,
|
||||
`openclaw/plugin-sdk/thread-ownership`, `openclaw/plugin-sdk/tlon`,
|
||||
`openclaw/plugin-sdk/twitch`, `openclaw/plugin-sdk/voice-call`,
|
||||
`openclaw/plugin-sdk/synology-chat`, `openclaw/plugin-sdk/test-utils`,
|
||||
`openclaw/plugin-sdk/tlon`, `openclaw/plugin-sdk/twitch`,
|
||||
`openclaw/plugin-sdk/zalo`, and `openclaw/plugin-sdk/zalouser`.
|
||||
|
||||
## Channel target resolution
|
||||
|
||||
@ -5,14 +5,14 @@ import type {
|
||||
WindowsSpawnProgram,
|
||||
WindowsSpawnProgramCandidate,
|
||||
WindowsSpawnResolution,
|
||||
} from "../runtime-api.js";
|
||||
} from "../../runtime-api.js";
|
||||
import {
|
||||
applyWindowsSpawnProgramPolicy,
|
||||
listKnownProviderAuthEnvVarNames,
|
||||
materializeWindowsSpawnProgram,
|
||||
omitEnvKeysCaseInsensitive,
|
||||
resolveWindowsSpawnProgramCandidate,
|
||||
} from "../runtime-api.js";
|
||||
} from "../../runtime-api.js";
|
||||
|
||||
export type SpawnExit = {
|
||||
code: number | null;
|
||||
|
||||
@ -4,15 +4,20 @@ import {
|
||||
toDirectoryEntries,
|
||||
type DirectoryConfigParams,
|
||||
} from "openclaw/plugin-sdk/directory-runtime";
|
||||
import type { InspectedDiscordAccount } from "../../../src/channels/read-only-account-inspect.discord.runtime.js";
|
||||
import { inspectReadOnlyChannelAccount } from "../../../src/channels/read-only-account-inspect.js";
|
||||
import { inspectDiscordAccount } from "../api.js";
|
||||
import type { InspectedDiscordAccount } from "../api.js";
|
||||
|
||||
export async function listDiscordDirectoryPeersFromConfig(params: DirectoryConfigParams) {
|
||||
const account = (await inspectReadOnlyChannelAccount({
|
||||
channelId: "discord",
|
||||
function inspectDiscordDirectoryAccount(
|
||||
params: DirectoryConfigParams,
|
||||
): InspectedDiscordAccount | null {
|
||||
return inspectDiscordAccount({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
})) as InspectedDiscordAccount | null;
|
||||
});
|
||||
}
|
||||
|
||||
export async function listDiscordDirectoryPeersFromConfig(params: DirectoryConfigParams) {
|
||||
const account = inspectDiscordDirectoryAccount(params);
|
||||
if (!account || !("config" in account)) {
|
||||
return [];
|
||||
}
|
||||
@ -34,11 +39,7 @@ export async function listDiscordDirectoryPeersFromConfig(params: DirectoryConfi
|
||||
}
|
||||
|
||||
export async function listDiscordDirectoryGroupsFromConfig(params: DirectoryConfigParams) {
|
||||
const account = (await inspectReadOnlyChannelAccount({
|
||||
channelId: "discord",
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
})) as InspectedDiscordAccount | null;
|
||||
const account = inspectDiscordDirectoryAccount(params);
|
||||
if (!account || !("config" in account)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
type VideoDescriptionRequest,
|
||||
type VideoDescriptionResult,
|
||||
} from "openclaw/plugin-sdk/media-understanding";
|
||||
import { normalizeGoogleModelId, parseGeminiAuth } from "../runtime-api.js";
|
||||
import { normalizeGoogleModelId, parseGeminiAuth } from "./runtime-api.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";
|
||||
|
||||
@ -1 +1 @@
|
||||
export * from "openclaw/plugin-sdk/lobster";
|
||||
export * from "../../src/plugin-sdk/lobster.js";
|
||||
|
||||
@ -3,7 +3,7 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { PassThrough } from "node:stream";
|
||||
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk/lobster";
|
||||
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "../runtime-api.js";
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
createWindowsCmdShimFixture,
|
||||
|
||||
@ -1 +1 @@
|
||||
export * from "openclaw/plugin-sdk/qwen-portal-auth";
|
||||
export * from "../../src/plugin-sdk/qwen-portal-auth.js";
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { normalizeSlackMessagingTarget } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import {
|
||||
applyDirectoryQueryAndLimit,
|
||||
collectNormalizedDirectoryIds,
|
||||
@ -5,16 +6,18 @@ import {
|
||||
toDirectoryEntries,
|
||||
type DirectoryConfigParams,
|
||||
} from "openclaw/plugin-sdk/directory-runtime";
|
||||
import { normalizeSlackMessagingTarget } from "../../../src/channels/plugins/normalize/slack.js";
|
||||
import { inspectReadOnlyChannelAccount } from "../../../src/channels/read-only-account-inspect.js";
|
||||
import type { InspectedSlackAccount } from "../../../src/channels/read-only-account-inspect.slack.runtime.js";
|
||||
import { inspectSlackAccount } from "../api.js";
|
||||
import type { InspectedSlackAccount } from "../api.js";
|
||||
|
||||
export async function listSlackDirectoryPeersFromConfig(params: DirectoryConfigParams) {
|
||||
const account = (await inspectReadOnlyChannelAccount({
|
||||
channelId: "slack",
|
||||
function inspectSlackDirectoryAccount(params: DirectoryConfigParams): InspectedSlackAccount | null {
|
||||
return inspectSlackAccount({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
})) as InspectedSlackAccount | null;
|
||||
});
|
||||
}
|
||||
|
||||
export async function listSlackDirectoryPeersFromConfig(params: DirectoryConfigParams) {
|
||||
const account = inspectSlackDirectoryAccount(params);
|
||||
if (!account || !("config" in account)) {
|
||||
return [];
|
||||
}
|
||||
@ -40,11 +43,7 @@ export async function listSlackDirectoryPeersFromConfig(params: DirectoryConfigP
|
||||
}
|
||||
|
||||
export async function listSlackDirectoryGroupsFromConfig(params: DirectoryConfigParams) {
|
||||
const account = (await inspectReadOnlyChannelAccount({
|
||||
channelId: "slack",
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
})) as InspectedSlackAccount | null;
|
||||
const account = inspectSlackDirectoryAccount(params);
|
||||
if (!account || !("config" in account)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import type { ChannelMessageActionContext } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { normalizeInteractiveReply } from "openclaw/plugin-sdk/interactive-runtime";
|
||||
import { readNumberParam, readStringParam } from "../../../src/agents/tools/common.js";
|
||||
import { readNumberParam, readStringParam } from "openclaw/plugin-sdk/slack-core";
|
||||
import { parseSlackBlocksInput } from "./blocks-input.js";
|
||||
import { buildSlackInteractiveBlocks } from "./blocks-render.js";
|
||||
|
||||
|
||||
@ -6,15 +6,20 @@ import {
|
||||
toDirectoryEntries,
|
||||
type DirectoryConfigParams,
|
||||
} from "openclaw/plugin-sdk/directory-runtime";
|
||||
import { inspectReadOnlyChannelAccount } from "../../../src/channels/read-only-account-inspect.js";
|
||||
import type { InspectedTelegramAccount } from "../../../src/channels/read-only-account-inspect.telegram.runtime.js";
|
||||
import { inspectTelegramAccount } from "../api.js";
|
||||
import type { InspectedTelegramAccount } from "../api.js";
|
||||
|
||||
export async function listTelegramDirectoryPeersFromConfig(params: DirectoryConfigParams) {
|
||||
const account = (await inspectReadOnlyChannelAccount({
|
||||
channelId: "telegram",
|
||||
async function inspectTelegramDirectoryAccount(
|
||||
params: DirectoryConfigParams,
|
||||
): Promise<InspectedTelegramAccount | null> {
|
||||
return inspectTelegramAccount({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
})) as InspectedTelegramAccount | null;
|
||||
});
|
||||
}
|
||||
|
||||
export async function listTelegramDirectoryPeersFromConfig(params: DirectoryConfigParams) {
|
||||
const account = await inspectTelegramDirectoryAccount(params);
|
||||
if (!account || !("config" in account)) {
|
||||
return [];
|
||||
}
|
||||
@ -36,11 +41,7 @@ export async function listTelegramDirectoryPeersFromConfig(params: DirectoryConf
|
||||
}
|
||||
|
||||
export async function listTelegramDirectoryGroupsFromConfig(params: DirectoryConfigParams) {
|
||||
const account = (await inspectReadOnlyChannelAccount({
|
||||
channelId: "telegram",
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
})) as InspectedTelegramAccount | null;
|
||||
const account = await inspectTelegramDirectoryAccount(params);
|
||||
if (!account || !("config" in account)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -3,8 +3,8 @@ import {
|
||||
listDirectoryUserEntriesFromAllowFrom,
|
||||
type DirectoryConfigParams,
|
||||
} from "openclaw/plugin-sdk/directory-runtime";
|
||||
import { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "../../../src/whatsapp/normalize.js";
|
||||
import { resolveWhatsAppAccount } from "./accounts.js";
|
||||
import { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "./normalize.js";
|
||||
|
||||
export async function listWhatsAppDirectoryPeersFromConfig(params: DirectoryConfigParams) {
|
||||
const account = resolveWhatsAppAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
export {
|
||||
isWhatsAppGroupJid,
|
||||
looksLikeWhatsAppTargetId,
|
||||
normalizeWhatsAppAllowFromEntries,
|
||||
normalizeWhatsAppMessagingTarget,
|
||||
normalizeWhatsAppTarget,
|
||||
} from "openclaw/plugin-sdk/channel-runtime";
|
||||
|
||||
@ -242,10 +242,6 @@
|
||||
"types": "./dist/plugin-sdk/irc.d.ts",
|
||||
"default": "./dist/plugin-sdk/irc.js"
|
||||
},
|
||||
"./plugin-sdk/lobster": {
|
||||
"types": "./dist/plugin-sdk/lobster.d.ts",
|
||||
"default": "./dist/plugin-sdk/lobster.js"
|
||||
},
|
||||
"./plugin-sdk/lazy-runtime": {
|
||||
"types": "./dist/plugin-sdk/lazy-runtime.d.ts",
|
||||
"default": "./dist/plugin-sdk/lazy-runtime.js"
|
||||
@ -274,10 +270,6 @@
|
||||
"types": "./dist/plugin-sdk/nostr.d.ts",
|
||||
"default": "./dist/plugin-sdk/nostr.js"
|
||||
},
|
||||
"./plugin-sdk/qwen-portal-auth": {
|
||||
"types": "./dist/plugin-sdk/qwen-portal-auth.d.ts",
|
||||
"default": "./dist/plugin-sdk/qwen-portal-auth.js"
|
||||
},
|
||||
"./plugin-sdk/synology-chat": {
|
||||
"types": "./dist/plugin-sdk/synology-chat.d.ts",
|
||||
"default": "./dist/plugin-sdk/synology-chat.js"
|
||||
|
||||
18
pnpm-lock.yaml
generated
18
pnpm-lock.yaml
generated
@ -625,12 +625,6 @@ importers:
|
||||
|
||||
ui:
|
||||
dependencies:
|
||||
'@lit-labs/signals':
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0
|
||||
'@lit/context':
|
||||
specifier: ^1.1.6
|
||||
version: 1.1.6
|
||||
'@noble/ed25519':
|
||||
specifier: 3.0.1
|
||||
version: 3.0.1
|
||||
@ -643,15 +637,6 @@ importers:
|
||||
marked:
|
||||
specifier: ^17.0.4
|
||||
version: 17.0.4
|
||||
signal-polyfill:
|
||||
specifier: ^0.2.2
|
||||
version: 0.2.2
|
||||
signal-utils:
|
||||
specifier: ^0.21.1
|
||||
version: 0.21.1(signal-polyfill@0.2.2)
|
||||
vite:
|
||||
specifier: 8.0.0
|
||||
version: 8.0.0(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
devDependencies:
|
||||
'@vitest/browser-playwright':
|
||||
specifier: 4.1.0
|
||||
@ -662,6 +647,9 @@ importers:
|
||||
playwright:
|
||||
specifier: ^1.58.2
|
||||
version: 1.58.2
|
||||
vite:
|
||||
specifier: 8.0.0
|
||||
version: 8.0.0(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vitest:
|
||||
specifier: 4.1.0
|
||||
version: 4.1.0(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(@vitest/browser-playwright@4.1.0)(jsdom@29.0.0(@noble/hashes@2.0.1))(vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
|
||||
@ -50,7 +50,6 @@
|
||||
"feishu",
|
||||
"googlechat",
|
||||
"irc",
|
||||
"lobster",
|
||||
"lazy-runtime",
|
||||
"matrix",
|
||||
"mattermost",
|
||||
@ -58,7 +57,6 @@
|
||||
"minimax-portal-auth",
|
||||
"nextcloud-talk",
|
||||
"nostr",
|
||||
"qwen-portal-auth",
|
||||
"synology-chat",
|
||||
"testing",
|
||||
"test-utils",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
// Narrow plugin-sdk surface for the bundled acpx plugin.
|
||||
// Keep this list additive and scoped to symbols used under extensions/acpx.
|
||||
// Public ACPX runtime backend helpers.
|
||||
// Keep this surface narrow and limited to the ACP runtime/backend contract.
|
||||
|
||||
export type { AcpRuntimeErrorCode } from "../acp/runtime/errors.js";
|
||||
export { AcpRuntimeError } from "../acp/runtime/errors.js";
|
||||
|
||||
@ -252,7 +252,10 @@ function collectCoreSourceFiles(): string[] {
|
||||
fullPath.includes(".test.") ||
|
||||
fullPath.includes(".spec.") ||
|
||||
fullPath.includes(".fixture.") ||
|
||||
fullPath.includes(".snap")
|
||||
fullPath.includes(".snap") ||
|
||||
// src/plugin-sdk is the curated bridge layer; validate its contracts with dedicated
|
||||
// plugin-sdk guardrails instead of the generic "core should not touch extensions" rule.
|
||||
fullPath.includes(`${resolve(ROOT_DIR, "plugin-sdk")}/`)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -43,6 +43,7 @@ export * from "../channels/plugins/whatsapp-heartbeat.js";
|
||||
export * from "../infra/outbound/send-deps.js";
|
||||
export * from "../polls.js";
|
||||
export * from "../utils/message-channel.js";
|
||||
export * from "../whatsapp/normalize.js";
|
||||
export { createActionGate, jsonResult, readStringParam } from "../agents/tools/common.js";
|
||||
export * from "./channel-lifecycle.js";
|
||||
export * from "./directory-runtime.js";
|
||||
|
||||
145
src/plugin-sdk/package-contract-guardrails.test.ts
Normal file
145
src/plugin-sdk/package-contract-guardrails.test.ts
Normal file
@ -0,0 +1,145 @@
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import { dirname, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { pluginSdkEntrypoints } from "./entrypoints.js";
|
||||
|
||||
const ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const REPO_ROOT = resolve(ROOT_DIR, "..");
|
||||
const REFERENCE_SCAN_ROOTS = ["src", "extensions", "scripts", "test", "docs"] as const;
|
||||
const PLUGIN_SDK_SUBPATH_PATTERN = /openclaw\/plugin-sdk\/([a-z0-9][a-z0-9-]*)\b/g;
|
||||
|
||||
function collectPluginSdkPackageExports(): string[] {
|
||||
const packageJson = JSON.parse(readFileSync(resolve(REPO_ROOT, "package.json"), "utf8")) as {
|
||||
exports?: Record<string, unknown>;
|
||||
};
|
||||
const exports = packageJson.exports ?? {};
|
||||
const subpaths: string[] = [];
|
||||
for (const key of Object.keys(exports)) {
|
||||
if (key === "./plugin-sdk") {
|
||||
subpaths.push("index");
|
||||
continue;
|
||||
}
|
||||
if (!key.startsWith("./plugin-sdk/")) {
|
||||
continue;
|
||||
}
|
||||
subpaths.push(key.slice("./plugin-sdk/".length));
|
||||
}
|
||||
return subpaths.sort();
|
||||
}
|
||||
|
||||
function collectPluginSdkSourceNames(): string[] {
|
||||
const pluginSdkDir = resolve(REPO_ROOT, "src", "plugin-sdk");
|
||||
return readdirSync(pluginSdkDir, { withFileTypes: true })
|
||||
.filter(
|
||||
(entry) => entry.isFile() && entry.name.endsWith(".ts") && !entry.name.endsWith(".test.ts"),
|
||||
)
|
||||
.map((entry) => entry.name.slice(0, -".ts".length))
|
||||
.sort();
|
||||
}
|
||||
|
||||
function collectTextFiles(rootRelativeDir: string): string[] {
|
||||
const rootDir = resolve(REPO_ROOT, rootRelativeDir);
|
||||
const files: string[] = [];
|
||||
const stack = [rootDir];
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop();
|
||||
if (!current) {
|
||||
continue;
|
||||
}
|
||||
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
||||
const fullPath = resolve(current, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "coverage") {
|
||||
continue;
|
||||
}
|
||||
stack.push(fullPath);
|
||||
continue;
|
||||
}
|
||||
if (!entry.isFile()) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
/\.(?:[cm]?ts|[cm]?js|tsx|jsx|md|mdx|json)$/u.test(entry.name) &&
|
||||
!entry.name.endsWith(".snap")
|
||||
) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function collectPluginSdkSubpathReferences() {
|
||||
const references: Array<{ file: string; subpath: string }> = [];
|
||||
for (const rootRelativeDir of REFERENCE_SCAN_ROOTS) {
|
||||
for (const fullPath of collectTextFiles(rootRelativeDir)) {
|
||||
const source = readFileSync(fullPath, "utf8");
|
||||
for (const match of source.matchAll(PLUGIN_SDK_SUBPATH_PATTERN)) {
|
||||
const subpath = match[1];
|
||||
if (!subpath) {
|
||||
continue;
|
||||
}
|
||||
references.push({
|
||||
file: relative(REPO_ROOT, fullPath).replaceAll("\\", "/"),
|
||||
subpath,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return references;
|
||||
}
|
||||
|
||||
describe("plugin-sdk package contract guardrails", () => {
|
||||
it("keeps package.json exports aligned with built plugin-sdk entrypoints", () => {
|
||||
expect(collectPluginSdkPackageExports()).toEqual([...pluginSdkEntrypoints].sort());
|
||||
});
|
||||
|
||||
it("keeps repo openclaw/plugin-sdk/<name> references on exported built subpaths", () => {
|
||||
const entrypoints = new Set(pluginSdkEntrypoints);
|
||||
const exports = new Set(collectPluginSdkPackageExports());
|
||||
const failures: string[] = [];
|
||||
|
||||
for (const reference of collectPluginSdkSubpathReferences()) {
|
||||
const missingFrom: string[] = [];
|
||||
if (!entrypoints.has(reference.subpath)) {
|
||||
missingFrom.push("scripts/lib/plugin-sdk-entrypoints.json");
|
||||
}
|
||||
if (!exports.has(reference.subpath)) {
|
||||
missingFrom.push("package.json exports");
|
||||
}
|
||||
if (missingFrom.length === 0) {
|
||||
continue;
|
||||
}
|
||||
failures.push(
|
||||
`${reference.file} references openclaw/plugin-sdk/${reference.subpath}, but ${reference.subpath} is missing from ${missingFrom.join(" and ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
expect(failures).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not leave referenced src/plugin-sdk source names stranded outside the public contract", () => {
|
||||
const exported = new Set(pluginSdkEntrypoints);
|
||||
const references = collectPluginSdkSubpathReferences();
|
||||
const failures: string[] = [];
|
||||
|
||||
for (const sourceName of collectPluginSdkSourceNames()) {
|
||||
if (exported.has(sourceName) || sourceName === "compat" || sourceName === "index") {
|
||||
continue;
|
||||
}
|
||||
const matchingRefs = references.filter((reference) => reference.subpath === sourceName);
|
||||
if (matchingRefs.length === 0) {
|
||||
continue;
|
||||
}
|
||||
failures.push(
|
||||
`src/plugin-sdk/${sourceName}.ts is referenced as openclaw/plugin-sdk/${sourceName} in ${matchingRefs
|
||||
.map((reference) => reference.file)
|
||||
.sort()
|
||||
.join(", ")}, but ${sourceName} is not exported as a public plugin-sdk subpath`,
|
||||
);
|
||||
}
|
||||
|
||||
expect(failures).toEqual([]);
|
||||
});
|
||||
});
|
||||
@ -27,15 +27,25 @@ const RUNTIME_API_EXPORT_GUARDS: Record<string, readonly string[]> = {
|
||||
'export * from "./src/send.js";',
|
||||
],
|
||||
"extensions/imessage/runtime-api.ts": [
|
||||
'export * from "./src/monitor.js";',
|
||||
'export * from "./src/probe.js";',
|
||||
'export * from "./src/send.js";',
|
||||
'export type { IMessageAccountConfig } from "../../src/config/types.imessage.js";',
|
||||
'export type { ChannelPlugin } from "../../src/channels/plugins/types.plugin.js";',
|
||||
'export { DEFAULT_ACCOUNT_ID, PAIRING_APPROVED_MESSAGE, buildChannelConfigSchema, getChatChannelMeta } from "../../src/plugin-sdk/channel-plugin-common.js";',
|
||||
'export { formatTrimmedAllowFromEntries, resolveIMessageConfigAllowFrom, resolveIMessageConfigDefaultTo } from "../../src/plugin-sdk/channel-config-helpers.js";',
|
||||
'export { collectStatusIssuesFromLastError } from "../../src/plugin-sdk/status-helpers.js";',
|
||||
'export { resolveChannelMediaMaxBytes } from "../../src/channels/plugins/media-limits.js";',
|
||||
'export { looksLikeIMessageTargetId, normalizeIMessageMessagingTarget } from "../../src/channels/plugins/normalize/imessage.js";',
|
||||
'export { IMessageConfigSchema } from "../../src/config/zod-schema.providers-core.js";',
|
||||
'export { resolveIMessageGroupRequireMention, resolveIMessageGroupToolPolicy } from "./src/group-policy.js";',
|
||||
'export { monitorIMessageProvider } from "./src/monitor.js";',
|
||||
'export type { MonitorIMessageOpts } from "./src/monitor.js";',
|
||||
'export { probeIMessage } from "./src/probe.js";',
|
||||
'export { sendMessageIMessage } from "./src/send.js";',
|
||||
],
|
||||
"extensions/googlechat/runtime-api.ts": ['export * from "openclaw/plugin-sdk/googlechat";'],
|
||||
"extensions/nextcloud-talk/runtime-api.ts": [
|
||||
'export * from "openclaw/plugin-sdk/nextcloud-talk";',
|
||||
],
|
||||
"extensions/signal/runtime-api.ts": ['export * from "./src/index.js";'],
|
||||
"extensions/signal/runtime-api.ts": ['export * from "./src/runtime-api.js";'],
|
||||
"extensions/slack/runtime-api.ts": [
|
||||
'export * from "./src/action-runtime.js";',
|
||||
'export * from "./src/directory-live.js";',
|
||||
@ -44,14 +54,21 @@ const RUNTIME_API_EXPORT_GUARDS: Record<string, readonly string[]> = {
|
||||
'export * from "./src/resolve-users.js";',
|
||||
],
|
||||
"extensions/telegram/runtime-api.ts": [
|
||||
'export * from "./src/audit.js";',
|
||||
'export * from "./src/action-runtime.js";',
|
||||
'export * from "./src/channel-actions.js";',
|
||||
'export * from "./src/monitor.js";',
|
||||
'export * from "./src/probe.js";',
|
||||
'export * from "./src/send.js";',
|
||||
'export * from "./src/thread-bindings.js";',
|
||||
'export * from "./src/token.js";',
|
||||
'export type { ChannelPlugin, OpenClawConfig, TelegramActionConfig } from "../../src/plugin-sdk/telegram-core.js";',
|
||||
'export type { ChannelMessageActionAdapter } from "../../src/channels/plugins/types.js";',
|
||||
'export type { TelegramAccountConfig, TelegramNetworkConfig } from "../../src/config/types.js";',
|
||||
'export type { OpenClawPluginApi, OpenClawPluginService, OpenClawPluginServiceContext, PluginLogger } from "../../src/plugins/types.js";',
|
||||
'export type { AcpRuntime, AcpRuntimeCapabilities, AcpRuntimeDoctorReport, AcpRuntimeEnsureInput, AcpRuntimeEvent, AcpRuntimeHandle, AcpRuntimeStatus, AcpRuntimeTurnInput, AcpSessionUpdateTag } from "../../src/acp/runtime/types.js";',
|
||||
'export type { AcpRuntimeErrorCode } from "../../src/acp/runtime/errors.js";',
|
||||
'export { AcpRuntimeError } from "../../src/acp/runtime/errors.js";',
|
||||
'export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../src/routing/session-key.js";',
|
||||
'export { buildChannelConfigSchema, getChatChannelMeta, jsonResult, readNumberParam, readReactionParams, readStringArrayParam, readStringOrNumberParam, readStringParam, resolvePollMaxSelections, TelegramConfigSchema } from "../../src/plugin-sdk/telegram-core.js";',
|
||||
'export { parseTelegramTopicConversation } from "../../src/acp/conversation-id.js";',
|
||||
'export { clearAccountEntryFields } from "../../src/channels/plugins/config-helpers.js";',
|
||||
'export { buildTokenChannelStatusSummary } from "../../src/plugin-sdk/status-helpers.js";',
|
||||
'export { projectCredentialSnapshotFields, resolveConfiguredFromCredentialStatuses } from "../../src/channels/account-snapshot-fields.js";',
|
||||
'export { resolveTelegramPollVisibility } from "../../src/poll-params.js";',
|
||||
'export { PAIRING_APPROVED_MESSAGE } from "../../src/channels/plugins/pairing-message.js";',
|
||||
],
|
||||
"extensions/whatsapp/runtime-api.ts": [
|
||||
'export * from "./src/active-listener.js";',
|
||||
|
||||
@ -48,9 +48,11 @@ const trimmedLegacyExtensionSubpaths = [
|
||||
"diagnostics-otel",
|
||||
"diffs",
|
||||
"llm-task",
|
||||
"lobster",
|
||||
"memory-lancedb",
|
||||
"open-prose",
|
||||
"phone-control",
|
||||
"qwen-portal-auth",
|
||||
"talk-voice",
|
||||
"thread-ownership",
|
||||
"voice-call",
|
||||
@ -313,7 +315,7 @@ describe("plugin-sdk subpath exports", () => {
|
||||
expect(typeof tlonSdk.tlonSetupAdapter).toBe("object");
|
||||
});
|
||||
|
||||
it("exports acpx helpers", async () => {
|
||||
it("exports ACPX runtime backend helpers", async () => {
|
||||
expect(typeof acpxSdk.listKnownProviderAuthEnvVarNames).toBe("function");
|
||||
expect(typeof acpxSdk.omitEnvKeysCaseInsensitive).toBe("function");
|
||||
});
|
||||
|
||||
@ -18,8 +18,8 @@ vi.mock("@mariozechner/pi-ai/oauth", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/qwen-portal-auth", async () => {
|
||||
const actual = await vi.importActual<object>("openclaw/plugin-sdk/qwen-portal-auth");
|
||||
vi.mock("../../plugin-sdk/qwen-portal-auth.js", async () => {
|
||||
const actual = await vi.importActual<object>("../../plugin-sdk/qwen-portal-auth.js");
|
||||
return {
|
||||
...actual,
|
||||
refreshQwenPortalCredentials: refreshQwenPortalCredentialsMock,
|
||||
|
||||
@ -9,20 +9,16 @@
|
||||
"test": "vitest run --config vitest.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lit-labs/signals": "^0.2.0",
|
||||
"@lit/context": "^1.1.6",
|
||||
"@noble/ed25519": "3.0.1",
|
||||
"dompurify": "^3.3.3",
|
||||
"lit": "^3.3.2",
|
||||
"marked": "^17.0.4",
|
||||
"signal-polyfill": "^0.2.2",
|
||||
"signal-utils": "^0.21.1",
|
||||
"vite": "8.0.0"
|
||||
"marked": "^17.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/browser-playwright": "4.1.0",
|
||||
"jsdom": "^29.0.0",
|
||||
"playwright": "^1.58.2",
|
||||
"vite": "8.0.0",
|
||||
"vitest": "4.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -249,6 +249,7 @@
|
||||
|
||||
.topnav-shell__content {
|
||||
display: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.topbar-nav-toggle {
|
||||
@ -650,75 +651,3 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Bottom Tabs (mobile navigation bar)
|
||||
=========================================== */
|
||||
|
||||
.bottom-tabs {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.bottom-tabs {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 60;
|
||||
background: var(--bg);
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 4px 0 calc(4px + env(safe-area-inset-bottom, 0px));
|
||||
justify-content: space-around;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.bottom-tab {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex: 1;
|
||||
padding: 6px 4px;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
color var(--duration-fast) ease,
|
||||
opacity var(--duration-fast) ease;
|
||||
}
|
||||
|
||||
.bottom-tab__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.bottom-tab__icon svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
stroke: currentColor;
|
||||
fill: none;
|
||||
stroke-width: 1.5px;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.bottom-tab__label {
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.bottom-tab--active {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.bottom-tab:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export { exportChatMarkdown } from "./chat/export.ts";
|
||||
@ -1,45 +0,0 @@
|
||||
export const MOONSHOT_KIMI_K2_DEFAULT_ID = "kimi-k2.5";
|
||||
export const MOONSHOT_KIMI_K2_CONTEXT_WINDOW = 256000;
|
||||
export const MOONSHOT_KIMI_K2_MAX_TOKENS = 8192;
|
||||
export const MOONSHOT_KIMI_K2_INPUT = ["text"] as const;
|
||||
export const MOONSHOT_KIMI_K2_COST = {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
} as const;
|
||||
|
||||
export const MOONSHOT_KIMI_K2_MODELS = [
|
||||
{
|
||||
id: "kimi-k2.5",
|
||||
name: "Kimi K2.5",
|
||||
alias: "Kimi K2.5",
|
||||
reasoning: false,
|
||||
},
|
||||
{
|
||||
id: "kimi-k2-0905-preview",
|
||||
name: "Kimi K2 0905 Preview",
|
||||
alias: "Kimi K2",
|
||||
reasoning: false,
|
||||
},
|
||||
{
|
||||
id: "kimi-k2-turbo-preview",
|
||||
name: "Kimi K2 Turbo",
|
||||
alias: "Kimi K2 Turbo",
|
||||
reasoning: false,
|
||||
},
|
||||
{
|
||||
id: "kimi-k2-thinking",
|
||||
name: "Kimi K2 Thinking",
|
||||
alias: "Kimi K2 Thinking",
|
||||
reasoning: true,
|
||||
},
|
||||
{
|
||||
id: "kimi-k2-thinking-turbo",
|
||||
name: "Kimi K2 Thinking Turbo",
|
||||
alias: "Kimi K2 Thinking Turbo",
|
||||
reasoning: true,
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type MoonshotKimiK2Model = (typeof MOONSHOT_KIMI_K2_MODELS)[number];
|
||||
@ -1,39 +0,0 @@
|
||||
/**
|
||||
* Map raw tool names to human-friendly labels for the chat UI.
|
||||
* Unknown tools are title-cased with underscores replaced by spaces.
|
||||
*/
|
||||
|
||||
export const TOOL_LABELS: Record<string, string> = {
|
||||
exec: "Run Command",
|
||||
bash: "Run Command",
|
||||
read: "Read File",
|
||||
write: "Write File",
|
||||
edit: "Edit File",
|
||||
apply_patch: "Apply Patch",
|
||||
web_search: "Web Search",
|
||||
web_fetch: "Fetch Page",
|
||||
browser: "Browser",
|
||||
message: "Send Message",
|
||||
image: "Generate Image",
|
||||
canvas: "Canvas",
|
||||
cron: "Cron",
|
||||
gateway: "Gateway",
|
||||
nodes: "Nodes",
|
||||
memory_search: "Search Memory",
|
||||
memory_get: "Get Memory",
|
||||
session_status: "Session Status",
|
||||
sessions_list: "List Sessions",
|
||||
sessions_history: "Session History",
|
||||
sessions_send: "Send to Session",
|
||||
sessions_spawn: "Spawn Session",
|
||||
agents_list: "List Agents",
|
||||
};
|
||||
|
||||
export function friendlyToolName(raw: string): string {
|
||||
const mapped = TOOL_LABELS[raw];
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
// Title-case fallback: "some_tool_name" → "Some Tool Name"
|
||||
return raw.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
import { html } from "lit";
|
||||
import { icons } from "../icons.ts";
|
||||
import type { Tab } from "../navigation.ts";
|
||||
|
||||
export type BottomTabsProps = {
|
||||
activeTab: Tab;
|
||||
onTabChange: (tab: Tab) => void;
|
||||
};
|
||||
|
||||
const BOTTOM_TABS: Array<{ id: Tab; label: string; icon: keyof typeof icons }> = [
|
||||
{ id: "overview", label: "Dashboard", icon: "barChart" },
|
||||
{ id: "chat", label: "Chat", icon: "messageSquare" },
|
||||
{ id: "sessions", label: "Sessions", icon: "fileText" },
|
||||
{ id: "config", label: "Settings", icon: "settings" },
|
||||
];
|
||||
|
||||
export function renderBottomTabs(props: BottomTabsProps) {
|
||||
return html`
|
||||
<nav class="bottom-tabs">
|
||||
${BOTTOM_TABS.map(
|
||||
(tab) => html`
|
||||
<button
|
||||
class="bottom-tab ${props.activeTab === tab.id ? "bottom-tab--active" : ""}"
|
||||
@click=${() => props.onTabChange(tab.id)}
|
||||
>
|
||||
<span class="bottom-tab__icon">${icons[tab.icon]}</span>
|
||||
<span class="bottom-tab__label">${tab.label}</span>
|
||||
</button>
|
||||
`,
|
||||
)}
|
||||
</nav>
|
||||
`;
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
appendTagFilter,
|
||||
getTagFilters,
|
||||
hasTagFilter,
|
||||
removeTagFilter,
|
||||
replaceTagFilters,
|
||||
toggleTagFilter,
|
||||
} from "./config-search.ts";
|
||||
|
||||
describe("config search tag helper", () => {
|
||||
it("adds a tag when query is empty", () => {
|
||||
expect(appendTagFilter("", "security")).toBe("tag:security");
|
||||
});
|
||||
|
||||
it("appends a tag to existing text query", () => {
|
||||
expect(appendTagFilter("token", "security")).toBe("token tag:security");
|
||||
});
|
||||
|
||||
it("deduplicates existing tag filters case-insensitively", () => {
|
||||
expect(appendTagFilter("token tag:Security", "security")).toBe("token tag:Security");
|
||||
});
|
||||
|
||||
it("detects exact tag terms", () => {
|
||||
expect(hasTagFilter("tag:security token", "security")).toBe(true);
|
||||
expect(hasTagFilter("tag:security-hard token", "security")).toBe(false);
|
||||
});
|
||||
|
||||
it("removes only the selected active tag", () => {
|
||||
expect(removeTagFilter("token tag:security tag:auth", "security")).toBe("token tag:auth");
|
||||
});
|
||||
|
||||
it("toggle removes active tag and keeps text", () => {
|
||||
expect(toggleTagFilter("token tag:security", "security")).toBe("token");
|
||||
});
|
||||
|
||||
it("toggle adds missing tag", () => {
|
||||
expect(toggleTagFilter("token", "channels")).toBe("token tag:channels");
|
||||
});
|
||||
|
||||
it("extracts unique normalized tags from query", () => {
|
||||
expect(getTagFilters("token tag:Security tag:auth tag:security")).toEqual(["security", "auth"]);
|
||||
});
|
||||
|
||||
it("replaces only tag filters and preserves free text", () => {
|
||||
expect(replaceTagFilters("token tag:security mode", ["auth", "channels"])).toBe(
|
||||
"token mode tag:auth tag:channels",
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -1,92 +0,0 @@
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function normalizeTag(tag: string): string {
|
||||
return tag.trim().toLowerCase();
|
||||
}
|
||||
|
||||
export function getTagFilters(query: string): string[] {
|
||||
const seen = new Set<string>();
|
||||
const tags: string[] = [];
|
||||
const pattern = /(^|\s)tag:([^\s]+)/gi;
|
||||
const raw = query.trim();
|
||||
let match: RegExpExecArray | null = pattern.exec(raw);
|
||||
while (match) {
|
||||
const normalized = normalizeTag(match[2] ?? "");
|
||||
if (normalized && !seen.has(normalized)) {
|
||||
seen.add(normalized);
|
||||
tags.push(normalized);
|
||||
}
|
||||
match = pattern.exec(raw);
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
export function hasTagFilter(query: string, tag: string): boolean {
|
||||
const normalizedTag = normalizeTag(tag);
|
||||
if (!normalizedTag) {
|
||||
return false;
|
||||
}
|
||||
const pattern = new RegExp(`(^|\\s)tag:${escapeRegExp(normalizedTag)}(?=\\s|$)`, "i");
|
||||
return pattern.test(query.trim());
|
||||
}
|
||||
|
||||
export function appendTagFilter(query: string, tag: string): string {
|
||||
const normalizedTag = normalizeTag(tag);
|
||||
const trimmed = query.trim();
|
||||
if (!normalizedTag) {
|
||||
return trimmed;
|
||||
}
|
||||
if (!trimmed) {
|
||||
return `tag:${normalizedTag}`;
|
||||
}
|
||||
if (hasTagFilter(trimmed, normalizedTag)) {
|
||||
return trimmed;
|
||||
}
|
||||
return `${trimmed} tag:${normalizedTag}`;
|
||||
}
|
||||
|
||||
export function removeTagFilter(query: string, tag: string): string {
|
||||
const normalizedTag = normalizeTag(tag);
|
||||
const trimmed = query.trim();
|
||||
if (!normalizedTag || !trimmed) {
|
||||
return trimmed;
|
||||
}
|
||||
const pattern = new RegExp(`(^|\\s)tag:${escapeRegExp(normalizedTag)}(?=\\s|$)`, "ig");
|
||||
return trimmed.replace(pattern, " ").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
export function replaceTagFilters(query: string, tags: readonly string[]): string {
|
||||
const uniqueTags: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const tag of tags) {
|
||||
const normalized = normalizeTag(tag);
|
||||
if (!normalized || seen.has(normalized)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(normalized);
|
||||
uniqueTags.push(normalized);
|
||||
}
|
||||
|
||||
const trimmed = query.trim();
|
||||
const withoutTags = trimmed
|
||||
.replace(/(^|\s)tag:([^\s]+)/gi, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
const tagTokens = uniqueTags.map((tag) => `tag:${tag}`).join(" ");
|
||||
if (withoutTags && tagTokens) {
|
||||
return `${withoutTags} ${tagTokens}`;
|
||||
}
|
||||
if (withoutTags) {
|
||||
return withoutTags;
|
||||
}
|
||||
return tagTokens;
|
||||
}
|
||||
|
||||
export function toggleTagFilter(query: string, tag: string): string {
|
||||
if (hasTagFilter(query, tag)) {
|
||||
return removeTagFilter(query, tag);
|
||||
}
|
||||
return appendTagFilter(query, tag);
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
import { html } from "lit";
|
||||
import { t } from "../../i18n/index.ts";
|
||||
import { icons } from "../icons.ts";
|
||||
|
||||
export type OverviewQuickActionsProps = {
|
||||
onNavigate: (tab: string) => void;
|
||||
onRefresh: () => void;
|
||||
};
|
||||
|
||||
export function renderOverviewQuickActions(props: OverviewQuickActionsProps) {
|
||||
return html`
|
||||
<section class="ov-quick-actions">
|
||||
<button class="btn ov-quick-action-btn" @click=${() => props.onNavigate("chat")}>
|
||||
<span class="nav-item__icon">${icons.messageSquare}</span>
|
||||
${t("overview.quickActions.newSession")}
|
||||
</button>
|
||||
<button class="btn ov-quick-action-btn" @click=${() => props.onNavigate("cron")}>
|
||||
<span class="nav-item__icon">${icons.zap}</span>
|
||||
${t("overview.quickActions.automation")}
|
||||
</button>
|
||||
<button class="btn ov-quick-action-btn" @click=${() => props.onRefresh()}>
|
||||
<span class="nav-item__icon">${icons.loader}</span>
|
||||
${t("overview.quickActions.refreshAll")}
|
||||
</button>
|
||||
<button class="btn ov-quick-action-btn" @click=${() => props.onNavigate("sessions")}>
|
||||
<span class="nav-item__icon">${icons.monitor}</span>
|
||||
${t("overview.quickActions.terminal")}
|
||||
</button>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user