Merge branch 'openclaw:main' into codex/cortex-openclaw-integration
This commit is contained in:
commit
3ecc16c324
17
CHANGELOG.md
17
CHANGELOG.md
@ -40,12 +40,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/binding: add `onConversationBindingResolved(...)` so plugins can react immediately after bind approvals or denies without blocking channel interaction acknowledgements. (#48678) Thanks @huntharo.
|
||||
- CLI/config: expand `config set` with SecretRef and provider builder modes, JSON/batch assignment support, and `--dry-run` validation with structured JSON output. (#49296) Thanks @joshavant.
|
||||
|
||||
### Breaking
|
||||
|
||||
- Browser/Chrome MCP: remove the legacy Chrome extension relay path, bundled extension assets, `driver: "extension"`, and `browser.relayBindHost`. Run `openclaw doctor --fix` to migrate host-local browser config to `existing-session` / `user`; Docker, headless, sandbox, and remote browser flows still use raw CDP. (#47893) Thanks @vincentkoc.
|
||||
- Plugins/runtime: remove the public `openclaw/extension-api` surface with no compatibility shim. Bundled plugins must use injected runtime for host-side operations (for example `api.runtime.agent.runEmbeddedPiAgent`) and any remaining direct imports must come from narrow `openclaw/plugin-sdk/*` subpaths instead of the monolithic SDK root.
|
||||
- Tools/image generation: standardize the stock image create/edit path on the core `image_generate` tool. The old `nano-banana-pro` docs/examples are gone; if you previously copied that sample-skill config, switch to `agents.defaults.imageGenerationModel` for built-in image generation or install a separate third-party skill explicitly.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Google auth/Node 25: patch `gaxios` to use native fetch without injecting `globalThis.window`, while translating proxy and mTLS transport settings so Google Vertex and Google Chat auth keep working on Node 25. (#47914) Thanks @pdd-cli.
|
||||
@ -128,9 +122,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Telegram/network: preserve sticky IPv4 fallback state across polling restarts so hosts with unstable IPv6 to `api.telegram.org` stop re-triggering repeated Telegram timeouts after each restart. (#48282) Thanks @yassinebkr.
|
||||
- Plugins/subagents: forward per-run provider and model overrides through gateway plugin subagent dispatch so plugin-launched agent delegations honor explicit model selection again. (#48277) Thanks @jalehman.
|
||||
- Agents/compaction: write minimal boundary summaries for empty preparations while keeping split-turn prefixes on the normal path, so no-summarizable-message sessions stop retriggering the safeguard loop. (#42215) thanks @lml2468.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Agents/bootstrap warnings: move bootstrap truncation warnings out of the system prompt and into the per-turn prompt body so prompt-cache reuse stays stable when truncation warnings appear or disappear. (#48753) Thanks @scoootscooob and @obviyus.
|
||||
- Telegram/DM topic session keys: route named-account DM topics through the same per-account base session key across inbound messages, native commands, and session-state lookups so `/status` and thread recovery stop creating phantom `agent:main:main:thread:...` sessions. (#48204) Thanks @vincentkoc.
|
||||
- macOS/node service startup: use `openclaw node start/stop --json` from the Mac app instead of the removed `openclaw service node ...` command shape, so current CLI installs expose the full node exec surface again. (#46843) Fixes #43171. Thanks @Br1an67.
|
||||
@ -141,6 +132,12 @@ Docs: https://docs.openclaw.ai
|
||||
- Telegram/network: unify API and media fetches under the same sticky IPv4 and pinned-IP fallback chain, and re-validate pinned override addresses against SSRF policy. (#49148) Thanks @obviyus.
|
||||
- Agents/prompt composition: append bootstrap truncation warnings to the current-turn prompt and add regression coverage for stable system-prompt cache invariants. (#49237) Thanks @scoootscooob.
|
||||
|
||||
### Breaking
|
||||
|
||||
- Browser/Chrome MCP: remove the legacy Chrome extension relay path, bundled extension assets, `driver: "extension"`, and `browser.relayBindHost`. Run `openclaw doctor --fix` to migrate host-local browser config to `existing-session` / `user`; Docker, headless, sandbox, and remote browser flows still use raw CDP. (#47893) Thanks @vincentkoc.
|
||||
- Plugins/runtime: remove the public `openclaw/extension-api` surface with no compatibility shim. Bundled plugins must use injected runtime for host-side operations (for example `api.runtime.agent.runEmbeddedPiAgent`) and any remaining direct imports must come from narrow `openclaw/plugin-sdk/*` subpaths instead of the monolithic SDK root.
|
||||
- Tools/image generation: standardize the stock image create/edit path on the core `image_generate` tool. The old `nano-banana-pro` docs/examples are gone; if you previously copied that sample-skill config, switch to `agents.defaults.imageGenerationModel` for built-in image generation or install a separate third-party skill explicitly.
|
||||
|
||||
## 2026.3.13
|
||||
|
||||
### Changes
|
||||
@ -218,6 +215,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Auth/login lockout recovery: clear stale `auth_permanent` and `billing` disabled state for all profiles matching the target provider when `openclaw models auth login` is invoked, so users locked out by expired or revoked OAuth tokens can recover by re-authenticating instead of waiting for the cooldown timer to expire. (#43057)
|
||||
- Auto-reply/context-engine compaction: persist the exact embedded-run metadata compaction count for main and followup runner session accounting, so metadata-only auto-compactions no longer undercount multi-compaction runs. (#42629) thanks @uf-hy.
|
||||
- Auth/Codex CLI reuse: sync reused Codex CLI credentials into the supported `openai-codex:default` OAuth profile instead of reviving the deprecated `openai-codex:codex-cli` slot, so doctor cleanup no longer loops. (#45353) thanks @Gugu-sugar.
|
||||
- Deps/audit: bump the pinned `fast-xml-parser` override to the first patched release so `pnpm audit --prod --audit-level=high` no longer fails on the AWS Bedrock XML builder path. Thanks @vincentkoc.
|
||||
- Hooks/after_compaction: forward `sessionFile` for direct/manual compaction events and add `sessionFile` plus `sessionKey` to wired auto-compaction hook context so plugins receive the session metadata already declared in the hook types. (#40781) Thanks @jarimustonen.
|
||||
|
||||
## 2026.3.12
|
||||
@ -647,6 +645,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Control UI/markdown fallback regression coverage: add explicit regression assertions for parser-error fallback behavior so malformed markdown no longer risks reintroducing hard-crash rendering paths in future markdown/parser upgrades. (#36445) Thanks @BinHPdev.
|
||||
- Web UI/config form: treat `additionalProperties: true` object schemas as editable map entries instead of unsupported fields so Accounts-style maps stay editable in form mode. (#35380, supersedes #32072) Thanks @stakeswky and @liuxiaopai-ai.
|
||||
- Feishu/streaming card delivery synthesis: unify snapshot and delta streaming merge semantics, apply overlap-aware final merge, suppress duplicate final text delivery (including text+media final packets), prefer topic-thread `message.reply` routing when a reply target exists, and tune card print cadence to avoid duplicate incremental rendering. (from #33245, #32896, #33840) Thanks @rexl2018, @kcinzgg, and @aerelune.
|
||||
- macOS/tray menu: keep injected sessions and device rows below the controls section so toggles and action buttons stay visible even when many sessions are active. (#38079) Thanks @bernesto.
|
||||
- Feishu/group mention detection: carry startup-probed bot display names through monitor dispatch so `requireMention` checks compare against current bot identity instead of stale config names, fixing missed `@bot` handling in groups while preserving multi-bot false-positive guards. (#36317, #34271) Thanks @liuxiaopai-ai.
|
||||
- Security/dependency audit: patch transitive Hono vulnerabilities by pinning `hono` to `4.12.5` and `@hono/node-server` to `1.19.10` in production resolution paths. Thanks @shakkernerd.
|
||||
- Security/dependency audit: bump `tar` to `7.5.10` (from `7.5.9`) to address the high-severity hardlink path traversal advisory (`GHSA-qffp-2rhf-9h96`). Thanks @shakkernerd.
|
||||
|
||||
@ -1099,38 +1099,33 @@ extension MenuSessionsInjector {
|
||||
// MARK: - Width + placement
|
||||
|
||||
private func findInsertIndex(in menu: NSMenu) -> Int? {
|
||||
// Insert right before the separator above "Send Heartbeats".
|
||||
if let idx = menu.items.firstIndex(where: { $0.title == "Send Heartbeats" }) {
|
||||
if let sepIdx = menu.items[..<idx].lastIndex(where: { $0.isSeparatorItem }) {
|
||||
return sepIdx
|
||||
}
|
||||
return idx
|
||||
}
|
||||
|
||||
if let sepIdx = menu.items.firstIndex(where: { $0.isSeparatorItem }) {
|
||||
return sepIdx
|
||||
}
|
||||
|
||||
if menu.items.count >= 1 { return 1 }
|
||||
return menu.items.count
|
||||
self.findDynamicSectionInsertIndex(in: menu)
|
||||
}
|
||||
|
||||
private func findNodesInsertIndex(in menu: NSMenu) -> Int? {
|
||||
if let idx = menu.items.firstIndex(where: { $0.title == "Send Heartbeats" }) {
|
||||
if let sepIdx = menu.items[..<idx].lastIndex(where: { $0.isSeparatorItem }) {
|
||||
return sepIdx
|
||||
}
|
||||
return idx
|
||||
self.findDynamicSectionInsertIndex(in: menu)
|
||||
}
|
||||
|
||||
private func findDynamicSectionInsertIndex(in menu: NSMenu) -> Int? {
|
||||
// Keep controls and action buttons visible by inserting dynamic rows at the
|
||||
// built-in footer boundary, not by matching localized menu item titles.
|
||||
if let footerSeparatorIndex = menu.items.lastIndex(where: { item in
|
||||
item.isSeparatorItem && !self.isInjectedItem(item)
|
||||
}) {
|
||||
return footerSeparatorIndex
|
||||
}
|
||||
|
||||
if let sepIdx = menu.items.firstIndex(where: { $0.isSeparatorItem }) {
|
||||
return sepIdx
|
||||
if let firstBaseItemIndex = menu.items.firstIndex(where: { !self.isInjectedItem($0) }) {
|
||||
return min(firstBaseItemIndex + 1, menu.items.count)
|
||||
}
|
||||
|
||||
if menu.items.count >= 1 { return 1 }
|
||||
return menu.items.count
|
||||
}
|
||||
|
||||
private func isInjectedItem(_ item: NSMenuItem) -> Bool {
|
||||
item.tag == self.tag || item.tag == self.nodesTag
|
||||
}
|
||||
|
||||
private func initialWidth(for menu: NSMenu) -> CGFloat {
|
||||
if let openWidth = self.menuOpenWidth {
|
||||
return max(300, openWidth)
|
||||
@ -1236,5 +1231,13 @@ extension MenuSessionsInjector {
|
||||
func injectForTesting(into menu: NSMenu) {
|
||||
self.inject(into: menu)
|
||||
}
|
||||
|
||||
func testingFindInsertIndex(in menu: NSMenu) -> Int? {
|
||||
self.findInsertIndex(in: menu)
|
||||
}
|
||||
|
||||
func testingFindNodesInsertIndex(in menu: NSMenu) -> Int? {
|
||||
self.findNodesInsertIndex(in: menu)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -5,7 +5,26 @@ import Testing
|
||||
@Suite(.serialized)
|
||||
@MainActor
|
||||
struct MenuSessionsInjectorTests {
|
||||
@Test func `injects disconnected message`() {
|
||||
@Test func anchorsDynamicRowsBelowControlsAndActions() throws {
|
||||
let injector = MenuSessionsInjector()
|
||||
|
||||
let menu = NSMenu()
|
||||
menu.addItem(NSMenuItem(title: "Header", action: nil, keyEquivalent: ""))
|
||||
menu.addItem(.separator())
|
||||
menu.addItem(NSMenuItem(title: "Send Heartbeats", action: nil, keyEquivalent: ""))
|
||||
menu.addItem(NSMenuItem(title: "Browser Control", action: nil, keyEquivalent: ""))
|
||||
menu.addItem(.separator())
|
||||
menu.addItem(NSMenuItem(title: "Open Dashboard", action: nil, keyEquivalent: ""))
|
||||
menu.addItem(NSMenuItem(title: "Open Chat", action: nil, keyEquivalent: ""))
|
||||
menu.addItem(.separator())
|
||||
menu.addItem(NSMenuItem(title: "Settings…", action: nil, keyEquivalent: ""))
|
||||
|
||||
let footerSeparatorIndex = try #require(menu.items.lastIndex(where: { $0.isSeparatorItem }))
|
||||
#expect(injector.testingFindInsertIndex(in: menu) == footerSeparatorIndex)
|
||||
#expect(injector.testingFindNodesInsertIndex(in: menu) == footerSeparatorIndex)
|
||||
}
|
||||
|
||||
@Test func injectsDisconnectedMessage() {
|
||||
let injector = MenuSessionsInjector()
|
||||
injector.setTestingControlChannelConnected(false)
|
||||
injector.setTestingSnapshot(nil, errorText: nil)
|
||||
@ -19,7 +38,7 @@ struct MenuSessionsInjectorTests {
|
||||
#expect(menu.items.contains { $0.tag == 9_415_557 })
|
||||
}
|
||||
|
||||
@Test func `injects session rows`() {
|
||||
@Test func injectsSessionRows() throws {
|
||||
let injector = MenuSessionsInjector()
|
||||
injector.setTestingControlChannelConnected(true)
|
||||
|
||||
@ -88,10 +107,22 @@ struct MenuSessionsInjectorTests {
|
||||
menu.addItem(NSMenuItem(title: "Header", action: nil, keyEquivalent: ""))
|
||||
menu.addItem(.separator())
|
||||
menu.addItem(NSMenuItem(title: "Send Heartbeats", action: nil, keyEquivalent: ""))
|
||||
menu.addItem(NSMenuItem(title: "Browser Control", action: nil, keyEquivalent: ""))
|
||||
menu.addItem(.separator())
|
||||
menu.addItem(NSMenuItem(title: "Open Dashboard", action: nil, keyEquivalent: ""))
|
||||
menu.addItem(.separator())
|
||||
menu.addItem(NSMenuItem(title: "Settings…", action: nil, keyEquivalent: ""))
|
||||
|
||||
injector.injectForTesting(into: menu)
|
||||
#expect(menu.items.contains { $0.tag == 9_415_557 })
|
||||
#expect(menu.items.contains { $0.tag == 9_415_557 && $0.isSeparatorItem })
|
||||
let sendHeartbeatsIndex = try #require(menu.items.firstIndex(where: { $0.title == "Send Heartbeats" }))
|
||||
let openDashboardIndex = try #require(menu.items.firstIndex(where: { $0.title == "Open Dashboard" }))
|
||||
let firstInjectedIndex = try #require(menu.items.firstIndex(where: { $0.tag == 9_415_557 }))
|
||||
let settingsIndex = try #require(menu.items.firstIndex(where: { $0.title == "Settings…" }))
|
||||
#expect(sendHeartbeatsIndex < firstInjectedIndex)
|
||||
#expect(openDashboardIndex < firstInjectedIndex)
|
||||
#expect(firstInjectedIndex < settingsIndex)
|
||||
}
|
||||
|
||||
@Test func `cost usage submenu does not use injector delegate`() {
|
||||
|
||||
@ -23200,6 +23200,56 @@
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.mattermost.accounts.*.dmChannelRetry",
|
||||
"kind": "channel",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "channels.mattermost.accounts.*.dmChannelRetry.initialDelayMs",
|
||||
"kind": "channel",
|
||||
"type": "integer",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.mattermost.accounts.*.dmChannelRetry.maxDelayMs",
|
||||
"kind": "channel",
|
||||
"type": "integer",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.mattermost.accounts.*.dmChannelRetry.maxRetries",
|
||||
"kind": "channel",
|
||||
"type": "integer",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.mattermost.accounts.*.dmChannelRetry.timeoutMs",
|
||||
"kind": "channel",
|
||||
"type": "integer",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.mattermost.accounts.*.dmPolicy",
|
||||
"kind": "channel",
|
||||
@ -23709,6 +23759,56 @@
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.mattermost.dmChannelRetry",
|
||||
"kind": "channel",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "channels.mattermost.dmChannelRetry.initialDelayMs",
|
||||
"kind": "channel",
|
||||
"type": "integer",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.mattermost.dmChannelRetry.maxDelayMs",
|
||||
"kind": "channel",
|
||||
"type": "integer",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.mattermost.dmChannelRetry.maxRetries",
|
||||
"kind": "channel",
|
||||
"type": "integer",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.mattermost.dmChannelRetry.timeoutMs",
|
||||
"kind": "channel",
|
||||
"type": "integer",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.mattermost.dmPolicy",
|
||||
"kind": "channel",
|
||||
@ -37601,12 +37701,13 @@
|
||||
"path": "channels.zalouser.accounts.*.groupPolicy",
|
||||
"kind": "channel",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"required": true,
|
||||
"enumValues": [
|
||||
"open",
|
||||
"disabled",
|
||||
"allowlist"
|
||||
],
|
||||
"defaultValue": "allowlist",
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
@ -37903,12 +38004,13 @@
|
||||
"path": "channels.zalouser.groupPolicy",
|
||||
"kind": "channel",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"required": true,
|
||||
"enumValues": [
|
||||
"open",
|
||||
"disabled",
|
||||
"allowlist"
|
||||
],
|
||||
"defaultValue": "allowlist",
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
@ -39699,7 +39801,7 @@
|
||||
"network"
|
||||
],
|
||||
"label": "Control UI Allowed Origins",
|
||||
"help": "Allowed browser origins for Control UI/WebChat websocket connections (full origins only, e.g. https://control.example.com). Required for non-loopback Control UI deployments unless dangerous Host-header fallback is explicitly enabled.",
|
||||
"help": "Allowed browser origins for Control UI/WebChat websocket connections (full origins only, e.g. https://control.example.com). Required for non-loopback Control UI deployments unless dangerous Host-header fallback is explicitly enabled. Setting [\"*\"] means allow any browser origin and should be avoided outside tightly controlled local testing.",
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@ -41038,7 +41140,7 @@
|
||||
"access"
|
||||
],
|
||||
"label": "Hooks Allowed Agent IDs",
|
||||
"help": "Allowlist of agent IDs that hook mappings are allowed to target when selecting execution agents. Use this to constrain automation events to dedicated service agents.",
|
||||
"help": "Allowlist of agent IDs that hook mappings are allowed to target when selecting execution agents. Use this to constrain automation events to dedicated service agents and reduce blast radius if a hook token is exposed.",
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@ -42156,7 +42258,7 @@
|
||||
"security"
|
||||
],
|
||||
"label": "Hooks Auth Token",
|
||||
"help": "Shared bearer token checked by hooks ingress for request authentication before mappings run. Use environment substitution and rotate regularly when webhook endpoints are internet-accessible.",
|
||||
"help": "Shared bearer token checked by hooks ingress for request authentication before mappings run. Treat holders as full-trust callers for the hook ingress surface, not as a separate non-owner role. Use environment substitution and rotate regularly when webhook endpoints are internet-accessible.",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
@ -46269,6 +46371,127 @@
|
||||
"help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.chutes",
|
||||
"kind": "plugin",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"advanced"
|
||||
],
|
||||
"label": "@openclaw/chutes-provider",
|
||||
"help": "OpenClaw Chutes.ai provider plugin (plugin: chutes)",
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.chutes.config",
|
||||
"kind": "plugin",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"advanced"
|
||||
],
|
||||
"label": "@openclaw/chutes-provider Config",
|
||||
"help": "Plugin-defined config payload for chutes.",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.chutes.enabled",
|
||||
"kind": "plugin",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"advanced"
|
||||
],
|
||||
"label": "Enable @openclaw/chutes-provider",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.chutes.hooks",
|
||||
"kind": "plugin",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"advanced"
|
||||
],
|
||||
"label": "Plugin Hook Policy",
|
||||
"help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.",
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.chutes.hooks.allowPromptInjection",
|
||||
"kind": "plugin",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"access"
|
||||
],
|
||||
"label": "Allow Prompt Injection Hooks",
|
||||
"help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.chutes.subagent",
|
||||
"kind": "plugin",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"advanced"
|
||||
],
|
||||
"label": "Plugin Subagent Policy",
|
||||
"help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.",
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.chutes.subagent.allowedModels",
|
||||
"kind": "plugin",
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"access"
|
||||
],
|
||||
"label": "Plugin Subagent Allowed Models",
|
||||
"help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.",
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.chutes.subagent.allowedModels.*",
|
||||
"kind": "plugin",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.chutes.subagent.allowModelOverride",
|
||||
"kind": "plugin",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"access"
|
||||
],
|
||||
"label": "Allow Plugin Subagent Model Override",
|
||||
"help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "plugins.entries.cloudflare-ai-gateway",
|
||||
"kind": "plugin",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5457}
|
||||
{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5476}
|
||||
{"recordType":"path","path":"acp","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"ACP","help":"ACP runtime controls for enabling dispatch, selecting backends, constraining allowed agent targets, and tuning streamed turn projection behavior.","hasChildren":true}
|
||||
{"recordType":"path","path":"acp.allowedAgents","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"ACP Allowed Agents","help":"Allowlist of ACP target agent ids permitted for ACP runtime sessions. Empty means no additional allowlist restriction.","hasChildren":true}
|
||||
{"recordType":"path","path":"acp.allowedAgents.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@ -2086,6 +2086,11 @@
|
||||
{"recordType":"path","path":"channels.mattermost.accounts.*.commands.nativeSkills","kind":"channel","type":["boolean","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.mattermost.accounts.*.configWrites","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.mattermost.accounts.*.dangerouslyAllowNameMatching","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.mattermost.accounts.*.dmChannelRetry","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.mattermost.accounts.*.dmChannelRetry.initialDelayMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.mattermost.accounts.*.dmChannelRetry.maxDelayMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.mattermost.accounts.*.dmChannelRetry.maxRetries","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.mattermost.accounts.*.dmChannelRetry.timeoutMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.mattermost.accounts.*.dmPolicy","kind":"channel","type":"string","required":true,"enumValues":["pairing","allowlist","open","disabled"],"defaultValue":"pairing","deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.mattermost.accounts.*.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.mattermost.accounts.*.groupAllowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
@ -2130,6 +2135,11 @@
|
||||
{"recordType":"path","path":"channels.mattermost.configWrites","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Mattermost Config Writes","help":"Allow Mattermost to write config in response to channel events/commands (default: true).","hasChildren":false}
|
||||
{"recordType":"path","path":"channels.mattermost.dangerouslyAllowNameMatching","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.mattermost.defaultAccount","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.mattermost.dmChannelRetry","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.mattermost.dmChannelRetry.initialDelayMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.mattermost.dmChannelRetry.maxDelayMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.mattermost.dmChannelRetry.maxRetries","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.mattermost.dmChannelRetry.timeoutMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.mattermost.dmPolicy","kind":"channel","type":"string","required":true,"enumValues":["pairing","allowlist","open","disabled"],"defaultValue":"pairing","deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.mattermost.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.mattermost.groupAllowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
@ -3398,7 +3408,7 @@
|
||||
{"recordType":"path","path":"channels.zalouser.accounts.*.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.zalouser.accounts.*.groupAllowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.zalouser.accounts.*.groupAllowFrom.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.zalouser.accounts.*.groupPolicy","kind":"channel","type":"string","required":false,"enumValues":["open","disabled","allowlist"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.zalouser.accounts.*.groupPolicy","kind":"channel","type":"string","required":true,"enumValues":["open","disabled","allowlist"],"defaultValue":"allowlist","deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.zalouser.accounts.*.groups","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.zalouser.accounts.*.groups.*","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.zalouser.accounts.*.groups.*.allow","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@ -3426,7 +3436,7 @@
|
||||
{"recordType":"path","path":"channels.zalouser.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.zalouser.groupAllowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.zalouser.groupAllowFrom.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.zalouser.groupPolicy","kind":"channel","type":"string","required":false,"enumValues":["open","disabled","allowlist"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.zalouser.groupPolicy","kind":"channel","type":"string","required":true,"enumValues":["open","disabled","allowlist"],"defaultValue":"allowlist","deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.zalouser.groups","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.zalouser.groups.*","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.zalouser.groups.*.allow","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@ -3563,7 +3573,7 @@
|
||||
{"recordType":"path","path":"gateway.channelMaxRestartsPerHour","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["network","performance"],"label":"Gateway Channel Max Restarts Per Hour","help":"Maximum number of health-monitor-initiated channel restarts allowed within a rolling one-hour window. Once hit, further restarts are skipped until the window expires. Default: 10.","hasChildren":false}
|
||||
{"recordType":"path","path":"gateway.channelStaleEventThresholdMinutes","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["network"],"label":"Gateway Channel Stale Event Threshold (min)","help":"How many minutes a connected channel can go without receiving any event before the health monitor treats it as a stale socket and triggers a restart. Default: 30.","hasChildren":false}
|
||||
{"recordType":"path","path":"gateway.controlUi","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["network"],"label":"Control UI","help":"Control UI hosting settings including enablement, pathing, and browser-origin/auth hardening behavior. Keep UI exposure minimal and pair with strong auth controls before internet-facing deployments.","hasChildren":true}
|
||||
{"recordType":"path","path":"gateway.controlUi.allowedOrigins","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access","network"],"label":"Control UI Allowed Origins","help":"Allowed browser origins for Control UI/WebChat websocket connections (full origins only, e.g. https://control.example.com). Required for non-loopback Control UI deployments unless dangerous Host-header fallback is explicitly enabled.","hasChildren":true}
|
||||
{"recordType":"path","path":"gateway.controlUi.allowedOrigins","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access","network"],"label":"Control UI Allowed Origins","help":"Allowed browser origins for Control UI/WebChat websocket connections (full origins only, e.g. https://control.example.com). Required for non-loopback Control UI deployments unless dangerous Host-header fallback is explicitly enabled. Setting [\"*\"] means allow any browser origin and should be avoided outside tightly controlled local testing.","hasChildren":true}
|
||||
{"recordType":"path","path":"gateway.controlUi.allowedOrigins.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"gateway.controlUi.allowInsecureAuth","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access","advanced","network","security"],"label":"Insecure Control UI Auth Toggle","help":"Loosens strict browser auth checks for Control UI when you must run a non-standard setup. Keep this off unless you trust your network and proxy path, because impersonation risk is higher.","hasChildren":false}
|
||||
{"recordType":"path","path":"gateway.controlUi.basePath","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["network","storage"],"label":"Control UI Base Path","help":"Optional URL prefix where the Control UI is served (e.g. /openclaw).","hasChildren":false}
|
||||
@ -3667,7 +3677,7 @@
|
||||
{"recordType":"path","path":"gateway.trustedProxies","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["network"],"label":"Gateway Trusted Proxy CIDRs","help":"CIDR/IP allowlist of upstream proxies permitted to provide forwarded client identity headers. Keep this list narrow so untrusted hops cannot impersonate users.","hasChildren":true}
|
||||
{"recordType":"path","path":"gateway.trustedProxies.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"hooks","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Hooks","help":"Inbound webhook automation surface for mapping external events into wake or agent actions in OpenClaw. Keep this locked down with explicit token/session/agent controls before exposing it beyond trusted networks.","hasChildren":true}
|
||||
{"recordType":"path","path":"hooks.allowedAgentIds","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Hooks Allowed Agent IDs","help":"Allowlist of agent IDs that hook mappings are allowed to target when selecting execution agents. Use this to constrain automation events to dedicated service agents.","hasChildren":true}
|
||||
{"recordType":"path","path":"hooks.allowedAgentIds","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Hooks Allowed Agent IDs","help":"Allowlist of agent IDs that hook mappings are allowed to target when selecting execution agents. Use this to constrain automation events to dedicated service agents and reduce blast radius if a hook token is exposed.","hasChildren":true}
|
||||
{"recordType":"path","path":"hooks.allowedAgentIds.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"hooks.allowedSessionKeyPrefixes","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access","storage"],"label":"Hooks Allowed Session Key Prefixes","help":"Allowlist of accepted session-key prefixes for inbound hook requests when caller-provided keys are enabled. Use narrow prefixes to prevent arbitrary session-key injection.","hasChildren":true}
|
||||
{"recordType":"path","path":"hooks.allowedSessionKeyPrefixes.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@ -3754,7 +3764,7 @@
|
||||
{"recordType":"path","path":"hooks.path","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["storage"],"label":"Hooks Endpoint Path","help":"HTTP path used by the hooks endpoint (for example `/hooks`) on the gateway control server. Use a non-guessable path and combine it with token validation for defense in depth.","hasChildren":false}
|
||||
{"recordType":"path","path":"hooks.presets","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Hooks Presets","help":"Named hook preset bundles applied at load time to seed standard mappings and behavior defaults. Keep preset usage explicit so operators can audit which automations are active.","hasChildren":true}
|
||||
{"recordType":"path","path":"hooks.presets.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"hooks.token","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":true,"tags":["auth","security"],"label":"Hooks Auth Token","help":"Shared bearer token checked by hooks ingress for request authentication before mappings run. Use environment substitution and rotate regularly when webhook endpoints are internet-accessible.","hasChildren":false}
|
||||
{"recordType":"path","path":"hooks.token","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":true,"tags":["auth","security"],"label":"Hooks Auth Token","help":"Shared bearer token checked by hooks ingress for request authentication before mappings run. Treat holders as full-trust callers for the hook ingress surface, not as a separate non-owner role. Use environment substitution and rotate regularly when webhook endpoints are internet-accessible.","hasChildren":false}
|
||||
{"recordType":"path","path":"hooks.transformsDir","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["storage"],"label":"Hooks Transforms Directory","help":"Base directory for hook transform modules referenced by mapping transform.module paths. Use a controlled repo directory so dynamic imports remain reviewable and predictable.","hasChildren":false}
|
||||
{"recordType":"path","path":"logging","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Logging","help":"Logging behavior controls for severity, output destinations, formatting, and sensitive-data redaction. Keep levels and redaction strict enough for production while preserving useful diagnostics.","hasChildren":true}
|
||||
{"recordType":"path","path":"logging.consoleLevel","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["observability"],"label":"Console Log Level","help":"Console-specific log threshold: \"silent\", \"fatal\", \"error\", \"warn\", \"info\", \"debug\", or \"trace\" for terminal output control. Use this to keep local console quieter while retaining richer file logging if needed.","hasChildren":false}
|
||||
@ -4093,6 +4103,15 @@
|
||||
{"recordType":"path","path":"plugins.entries.byteplus.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.byteplus.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.byteplus.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.chutes","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/chutes-provider","help":"OpenClaw Chutes.ai provider plugin (plugin: chutes)","hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.chutes.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/chutes-provider Config","help":"Plugin-defined config payload for chutes.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.chutes.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/chutes-provider","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.chutes.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.chutes.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.chutes.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.chutes.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.chutes.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.chutes.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.cloudflare-ai-gateway","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/cloudflare-ai-gateway-provider","help":"OpenClaw Cloudflare AI Gateway provider plugin (plugin: cloudflare-ai-gateway)","hasChildren":true}
|
||||
{"recordType":"path","path":"plugins.entries.cloudflare-ai-gateway.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/cloudflare-ai-gateway-provider Config","help":"Plugin-defined config payload for cloudflare-ai-gateway.","hasChildren":false}
|
||||
{"recordType":"path","path":"plugins.entries.cloudflare-ai-gateway.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/cloudflare-ai-gateway-provider","hasChildren":false}
|
||||
|
||||
@ -176,19 +176,31 @@ openclaw config set channels.discord.token \
|
||||
--ref-id DISCORD_BOT_TOKEN \
|
||||
--dry-run \
|
||||
--json
|
||||
|
||||
openclaw config set channels.discord.token \
|
||||
--ref-provider vault \
|
||||
--ref-source exec \
|
||||
--ref-id discord/token \
|
||||
--dry-run \
|
||||
--allow-exec
|
||||
```
|
||||
|
||||
Dry-run behavior:
|
||||
|
||||
- Builder mode: requires full SecretRef resolvability for changed refs/providers.
|
||||
- JSON mode (`--strict-json`, `--json`, or batch mode): requires full resolvability and schema validation.
|
||||
- Builder mode: runs SecretRef resolvability checks for changed refs/providers.
|
||||
- JSON mode (`--strict-json`, `--json`, or batch mode): runs schema validation plus SecretRef resolvability checks.
|
||||
- Exec SecretRef checks are skipped by default during dry-run to avoid command side effects.
|
||||
- Use `--allow-exec` with `--dry-run` to opt in to exec SecretRef checks (this may execute provider commands).
|
||||
- `--allow-exec` is dry-run only and errors if used without `--dry-run`.
|
||||
|
||||
`--dry-run --json` prints a machine-readable report:
|
||||
|
||||
- `ok`: whether dry-run passed
|
||||
- `operations`: number of assignments evaluated
|
||||
- `checks`: whether schema/resolvability checks ran
|
||||
- `refsChecked`: number of refs resolved during dry-run
|
||||
- `checks.resolvabilityComplete`: whether resolvability checks ran to completion (false when exec refs are skipped)
|
||||
- `refsChecked`: number of refs actually resolved during dry-run
|
||||
- `skippedExecRefs`: number of exec refs skipped because `--allow-exec` was not set
|
||||
- `errors`: structured schema/resolvability failures when `ok=false`
|
||||
|
||||
### JSON Output Shape
|
||||
@ -202,8 +214,10 @@ Dry-run behavior:
|
||||
checks: {
|
||||
schema: boolean,
|
||||
resolvability: boolean,
|
||||
resolvabilityComplete: boolean,
|
||||
},
|
||||
refsChecked: number,
|
||||
skippedExecRefs: number,
|
||||
errors?: [
|
||||
{
|
||||
kind: "schema" | "resolvability",
|
||||
@ -224,9 +238,11 @@ Success example:
|
||||
"inputModes": ["builder"],
|
||||
"checks": {
|
||||
"schema": false,
|
||||
"resolvability": true
|
||||
"resolvability": true,
|
||||
"resolvabilityComplete": true
|
||||
},
|
||||
"refsChecked": 1
|
||||
"refsChecked": 1,
|
||||
"skippedExecRefs": 0
|
||||
}
|
||||
```
|
||||
|
||||
@ -240,9 +256,11 @@ Failure example:
|
||||
"inputModes": ["builder"],
|
||||
"checks": {
|
||||
"schema": false,
|
||||
"resolvability": true
|
||||
"resolvability": true,
|
||||
"resolvabilityComplete": true
|
||||
},
|
||||
"refsChecked": 1,
|
||||
"skippedExecRefs": 0,
|
||||
"errors": [
|
||||
{
|
||||
"kind": "resolvability",
|
||||
@ -257,6 +275,7 @@ If dry-run fails:
|
||||
|
||||
- `config schema validation failed`: your post-change config shape is invalid; fix path/value or provider/ref object shape.
|
||||
- `SecretRef assignment(s) could not be resolved`: referenced provider/ref currently cannot resolve (missing env var, invalid file pointer, exec provider failure, or provider/source mismatch).
|
||||
- `Dry run note: skipped <n> exec SecretRef resolvability check(s)`: dry-run skipped exec refs; rerun with `--allow-exec` if you need exec resolvability validation.
|
||||
- For batch mode, fix failing entries and rerun `--dry-run` before writing.
|
||||
|
||||
## Subcommands
|
||||
|
||||
@ -400,8 +400,9 @@ Subcommands:
|
||||
- SecretRef builder mode: `config set <path> --ref-provider <provider> --ref-source <source> --ref-id <id>`
|
||||
- provider builder mode: `config set secrets.providers.<alias> --provider-source <env|file|exec> ...`
|
||||
- batch mode: `config set --batch-json '<json>'` or `config set --batch-file <path>`
|
||||
- `config set --dry-run`: validate assignments without writing `openclaw.json`.
|
||||
- `config set --dry-run --json`: emit machine-readable dry-run output (checks, operations, errors).
|
||||
- `config set --dry-run`: validate assignments without writing `openclaw.json` (exec SecretRef checks are skipped by default).
|
||||
- `config set --allow-exec --dry-run`: opt in to exec SecretRef dry-run checks (may execute provider commands).
|
||||
- `config set --dry-run --json`: emit machine-readable dry-run output (checks + completeness signal, operations, refs checked/skipped, errors).
|
||||
- `config set --strict-json`: require JSON5 parsing for path/value input. `--json` remains a legacy alias for strict parsing outside dry-run output mode.
|
||||
- `config unset <path>`: remove a value.
|
||||
- `config file`: print the active config file path.
|
||||
|
||||
@ -52,6 +52,17 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost):
|
||||
- Runs in CI
|
||||
- No real keys required
|
||||
- Should be fast and stable
|
||||
- Embedded runner note:
|
||||
- When you change message-tool discovery inputs or compaction runtime context,
|
||||
keep both levels of coverage.
|
||||
- Add focused helper regressions for pure routing/normalization seams.
|
||||
- Also keep the embedded runner integration suites healthy:
|
||||
`src/agents/pi-embedded-runner/compact.hooks.test.ts`,
|
||||
`src/agents/pi-embedded-runner/run.overflow-compaction.test.ts`, and
|
||||
`src/agents/pi-embedded-runner/run.overflow-compaction.loop.test.ts`.
|
||||
- Those suites verify that scoped ids and compaction behavior still flow
|
||||
through the real `run.ts` / `compact.ts` paths; helper-only tests are not a
|
||||
sufficient substitute for those seams.
|
||||
- Pool note:
|
||||
- OpenClaw uses Vitest `vmForks` on Node 22, 23, and 24 for faster unit shards.
|
||||
- On Node 25+, OpenClaw automatically falls back to regular `forks` until the repo is re-validated there.
|
||||
|
||||
13
docs/pi.md
13
docs/pi.md
@ -119,19 +119,24 @@ src/agents/
|
||||
│ ├── browser-tool.ts
|
||||
│ ├── canvas-tool.ts
|
||||
│ ├── cron-tool.ts
|
||||
│ ├── discord-actions*.ts
|
||||
│ ├── gateway-tool.ts
|
||||
│ ├── image-tool.ts
|
||||
│ ├── message-tool.ts
|
||||
│ ├── nodes-tool.ts
|
||||
│ ├── session*.ts
|
||||
│ ├── slack-actions.ts
|
||||
│ ├── telegram-actions.ts
|
||||
│ ├── web-*.ts
|
||||
│ └── whatsapp-actions.ts
|
||||
│ └── ...
|
||||
└── ...
|
||||
```
|
||||
|
||||
Channel-specific message action runtimes now live in the plugin-owned extension
|
||||
directories instead of under `src/agents/tools`, for example:
|
||||
|
||||
- `extensions/discord/src/actions/runtime*.ts`
|
||||
- `extensions/slack/src/action-runtime.ts`
|
||||
- `extensions/telegram/src/action-runtime.ts`
|
||||
- `extensions/whatsapp/src/action-runtime.ts`
|
||||
|
||||
## Core Integration Flow
|
||||
|
||||
### 1. Running an Embedded Agent
|
||||
|
||||
@ -188,6 +188,64 @@ The important design boundary:
|
||||
That split lets OpenClaw validate config, explain missing/disabled plugins, and
|
||||
build UI/schema hints before the full runtime is active.
|
||||
|
||||
### Channel plugins and the shared message tool
|
||||
|
||||
Channel plugins do not need to register a separate send/edit/react tool for
|
||||
normal chat actions. OpenClaw keeps one shared `message` tool in core, and
|
||||
channel plugins own the channel-specific discovery and execution behind it.
|
||||
|
||||
The current boundary is:
|
||||
|
||||
- core owns the shared `message` tool host, prompt wiring, session/thread
|
||||
bookkeeping, and execution dispatch
|
||||
- channel plugins own scoped action discovery, capability discovery, and any
|
||||
channel-specific schema fragments
|
||||
- channel plugins execute the final action through their action adapter
|
||||
|
||||
For channel plugins, the preferred SDK surface is
|
||||
`ChannelMessageActionAdapter.describeMessageTool(...)`. That unified discovery
|
||||
call lets a plugin return its visible actions, capabilities, and schema
|
||||
contributions together so those pieces do not drift apart.
|
||||
|
||||
Core passes runtime scope into that discovery step. Important fields include:
|
||||
|
||||
- `accountId`
|
||||
- `currentChannelId`
|
||||
- `currentThreadTs`
|
||||
- `currentMessageId`
|
||||
- `sessionKey`
|
||||
- `sessionId`
|
||||
- `agentId`
|
||||
- trusted inbound `requesterSenderId`
|
||||
|
||||
That matters for context-sensitive plugins. A channel can hide or expose
|
||||
message actions based on the active account, current room/thread/message, or
|
||||
trusted requester identity without hardcoding channel-specific branches in the
|
||||
core `message` tool.
|
||||
|
||||
This is why embedded-runner routing changes are still plugin work: the runner is
|
||||
responsible for forwarding the current chat/session identity into the plugin
|
||||
discovery boundary so the shared `message` tool exposes the right channel-owned
|
||||
surface for the current turn.
|
||||
|
||||
For channel-owned execution helpers, bundled plugins should keep the execution
|
||||
runtime inside their own extension modules. Core no longer owns the Discord,
|
||||
Slack, Telegram, or WhatsApp message-action runtimes under `src/agents/tools`.
|
||||
We do not publish separate `plugin-sdk/*-action-runtime` subpaths, and bundled
|
||||
plugins should import their own local runtime code directly from their
|
||||
extension-owned modules.
|
||||
|
||||
For polls specifically, there are two execution paths:
|
||||
|
||||
- `outbound.sendPoll` is the shared baseline for channels that fit the common
|
||||
poll model
|
||||
- `actions.handleAction("poll")` is the preferred path for channel-specific
|
||||
poll semantics or extra poll parameters
|
||||
|
||||
Core now defers shared poll parsing until after plugin poll dispatch declines
|
||||
the action, so plugin-owned poll handlers can accept channel-specific poll
|
||||
fields without being blocked by the generic poll parser first.
|
||||
|
||||
## Capability ownership model
|
||||
|
||||
OpenClaw treats a native plugin as the ownership boundary for a **company** or a
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/chutes-provider",
|
||||
"version": "2026.3.17",
|
||||
"version": "2026.3.14",
|
||||
"private": true,
|
||||
"description": "OpenClaw Chutes.ai provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
export * from "./src/audit.js";
|
||||
export * from "./src/actions/runtime.js";
|
||||
export * from "./src/actions/runtime.moderation-shared.js";
|
||||
export * from "./src/actions/runtime.shared.js";
|
||||
export * from "./src/channel-actions.js";
|
||||
export * from "./src/directory-live.js";
|
||||
export * from "./src/monitor.js";
|
||||
|
||||
@ -5,12 +5,12 @@ import {
|
||||
readStringArrayParam,
|
||||
readStringParam,
|
||||
} from "openclaw/plugin-sdk/agent-runtime";
|
||||
import type { ChannelMessageActionContext } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { handleDiscordAction } from "./runtime.js";
|
||||
import {
|
||||
isDiscordModerationAction,
|
||||
readDiscordModerationCommand,
|
||||
} from "openclaw/plugin-sdk/agent-runtime";
|
||||
import { handleDiscordAction } from "openclaw/plugin-sdk/agent-runtime";
|
||||
import type { ChannelMessageActionContext } from "openclaw/plugin-sdk/channel-runtime";
|
||||
} from "./runtime.moderation-shared.js";
|
||||
|
||||
type Ctx = Pick<
|
||||
ChannelMessageActionContext,
|
||||
|
||||
@ -4,8 +4,6 @@ import {
|
||||
readStringArrayParam,
|
||||
readStringParam,
|
||||
} from "openclaw/plugin-sdk/agent-runtime";
|
||||
import { readDiscordParentIdParam } from "openclaw/plugin-sdk/agent-runtime";
|
||||
import { handleDiscordAction } from "openclaw/plugin-sdk/agent-runtime";
|
||||
import { readBooleanParam } from "openclaw/plugin-sdk/boolean-param";
|
||||
import { resolveReactionMessageId } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import type { ChannelMessageActionContext } from "openclaw/plugin-sdk/channel-runtime";
|
||||
@ -13,6 +11,8 @@ import { normalizeInteractiveReply } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { buildDiscordInteractiveComponents } from "../shared-interactive.js";
|
||||
import { resolveDiscordChannelId } from "../targets.js";
|
||||
import { tryHandleDiscordMessageActionGuildAdmin } from "./handle-action.guild-admin.js";
|
||||
import { handleDiscordAction } from "./runtime.js";
|
||||
import { readDiscordParentIdParam } from "./runtime.shared.js";
|
||||
|
||||
const providerId = "discord";
|
||||
|
||||
|
||||
@ -1,5 +1,14 @@
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import type { DiscordActionConfig } from "../../config/config.js";
|
||||
import {
|
||||
type ActionGate,
|
||||
jsonResult,
|
||||
parseAvailableTags,
|
||||
readNumberParam,
|
||||
readStringArrayParam,
|
||||
readStringParam,
|
||||
} from "../../../../src/agents/tools/common.js";
|
||||
import type { DiscordActionConfig } from "../../../../src/config/types.discord.js";
|
||||
import { getPresence } from "../monitor/presence-cache.js";
|
||||
import {
|
||||
addRoleDiscord,
|
||||
createChannelDiscord,
|
||||
@ -19,17 +28,29 @@ import {
|
||||
setChannelPermissionDiscord,
|
||||
uploadEmojiDiscord,
|
||||
uploadStickerDiscord,
|
||||
} from "../../plugin-sdk/discord.js";
|
||||
import { getPresence } from "../../plugin-sdk/discord.js";
|
||||
import {
|
||||
type ActionGate,
|
||||
jsonResult,
|
||||
parseAvailableTags,
|
||||
readNumberParam,
|
||||
readStringArrayParam,
|
||||
readStringParam,
|
||||
} from "./common.js";
|
||||
import { readDiscordParentIdParam } from "./discord-actions-shared.js";
|
||||
} from "../send.js";
|
||||
import { readDiscordParentIdParam } from "./runtime.shared.js";
|
||||
|
||||
export const discordGuildActionRuntime = {
|
||||
addRoleDiscord,
|
||||
createChannelDiscord,
|
||||
createScheduledEventDiscord,
|
||||
deleteChannelDiscord,
|
||||
editChannelDiscord,
|
||||
fetchChannelInfoDiscord,
|
||||
fetchMemberInfoDiscord,
|
||||
fetchRoleInfoDiscord,
|
||||
fetchVoiceStatusDiscord,
|
||||
listGuildChannelsDiscord,
|
||||
listGuildEmojisDiscord,
|
||||
listScheduledEventsDiscord,
|
||||
moveChannelDiscord,
|
||||
removeChannelPermissionDiscord,
|
||||
removeRoleDiscord,
|
||||
setChannelPermissionDiscord,
|
||||
uploadEmojiDiscord,
|
||||
uploadStickerDiscord,
|
||||
};
|
||||
|
||||
type DiscordRoleMutation = (params: {
|
||||
guildId: string;
|
||||
@ -85,8 +106,8 @@ export async function handleDiscordGuildAction(
|
||||
required: true,
|
||||
});
|
||||
const member = accountId
|
||||
? await fetchMemberInfoDiscord(guildId, userId, { accountId })
|
||||
: await fetchMemberInfoDiscord(guildId, userId);
|
||||
? await discordGuildActionRuntime.fetchMemberInfoDiscord(guildId, userId, { accountId })
|
||||
: await discordGuildActionRuntime.fetchMemberInfoDiscord(guildId, userId);
|
||||
const presence = getPresence(accountId, userId);
|
||||
const activities = presence?.activities ?? undefined;
|
||||
const status = presence?.status ?? undefined;
|
||||
@ -100,8 +121,8 @@ export async function handleDiscordGuildAction(
|
||||
required: true,
|
||||
});
|
||||
const roles = accountId
|
||||
? await fetchRoleInfoDiscord(guildId, { accountId })
|
||||
: await fetchRoleInfoDiscord(guildId);
|
||||
? await discordGuildActionRuntime.fetchRoleInfoDiscord(guildId, { accountId })
|
||||
: await discordGuildActionRuntime.fetchRoleInfoDiscord(guildId);
|
||||
return jsonResult({ ok: true, roles });
|
||||
}
|
||||
case "emojiList": {
|
||||
@ -112,8 +133,8 @@ export async function handleDiscordGuildAction(
|
||||
required: true,
|
||||
});
|
||||
const emojis = accountId
|
||||
? await listGuildEmojisDiscord(guildId, { accountId })
|
||||
: await listGuildEmojisDiscord(guildId);
|
||||
? await discordGuildActionRuntime.listGuildEmojisDiscord(guildId, { accountId })
|
||||
: await discordGuildActionRuntime.listGuildEmojisDiscord(guildId);
|
||||
return jsonResult({ ok: true, emojis });
|
||||
}
|
||||
case "emojiUpload": {
|
||||
@ -129,7 +150,7 @@ export async function handleDiscordGuildAction(
|
||||
});
|
||||
const roleIds = readStringArrayParam(params, "roleIds");
|
||||
const emoji = accountId
|
||||
? await uploadEmojiDiscord(
|
||||
? await discordGuildActionRuntime.uploadEmojiDiscord(
|
||||
{
|
||||
guildId,
|
||||
name,
|
||||
@ -138,7 +159,7 @@ export async function handleDiscordGuildAction(
|
||||
},
|
||||
{ accountId },
|
||||
)
|
||||
: await uploadEmojiDiscord({
|
||||
: await discordGuildActionRuntime.uploadEmojiDiscord({
|
||||
guildId,
|
||||
name,
|
||||
mediaUrl,
|
||||
@ -162,7 +183,7 @@ export async function handleDiscordGuildAction(
|
||||
required: true,
|
||||
});
|
||||
const sticker = accountId
|
||||
? await uploadStickerDiscord(
|
||||
? await discordGuildActionRuntime.uploadStickerDiscord(
|
||||
{
|
||||
guildId,
|
||||
name,
|
||||
@ -172,7 +193,7 @@ export async function handleDiscordGuildAction(
|
||||
},
|
||||
{ accountId },
|
||||
)
|
||||
: await uploadStickerDiscord({
|
||||
: await discordGuildActionRuntime.uploadStickerDiscord({
|
||||
guildId,
|
||||
name,
|
||||
description,
|
||||
@ -185,14 +206,22 @@ export async function handleDiscordGuildAction(
|
||||
if (!isActionEnabled("roles", false)) {
|
||||
throw new Error("Discord role changes are disabled.");
|
||||
}
|
||||
await runRoleMutation({ accountId, values: params, mutate: addRoleDiscord });
|
||||
await runRoleMutation({
|
||||
accountId,
|
||||
values: params,
|
||||
mutate: discordGuildActionRuntime.addRoleDiscord,
|
||||
});
|
||||
return jsonResult({ ok: true });
|
||||
}
|
||||
case "roleRemove": {
|
||||
if (!isActionEnabled("roles", false)) {
|
||||
throw new Error("Discord role changes are disabled.");
|
||||
}
|
||||
await runRoleMutation({ accountId, values: params, mutate: removeRoleDiscord });
|
||||
await runRoleMutation({
|
||||
accountId,
|
||||
values: params,
|
||||
mutate: discordGuildActionRuntime.removeRoleDiscord,
|
||||
});
|
||||
return jsonResult({ ok: true });
|
||||
}
|
||||
case "channelInfo": {
|
||||
@ -203,8 +232,8 @@ export async function handleDiscordGuildAction(
|
||||
required: true,
|
||||
});
|
||||
const channel = accountId
|
||||
? await fetchChannelInfoDiscord(channelId, { accountId })
|
||||
: await fetchChannelInfoDiscord(channelId);
|
||||
? await discordGuildActionRuntime.fetchChannelInfoDiscord(channelId, { accountId })
|
||||
: await discordGuildActionRuntime.fetchChannelInfoDiscord(channelId);
|
||||
return jsonResult({ ok: true, channel });
|
||||
}
|
||||
case "channelList": {
|
||||
@ -215,8 +244,8 @@ export async function handleDiscordGuildAction(
|
||||
required: true,
|
||||
});
|
||||
const channels = accountId
|
||||
? await listGuildChannelsDiscord(guildId, { accountId })
|
||||
: await listGuildChannelsDiscord(guildId);
|
||||
? await discordGuildActionRuntime.listGuildChannelsDiscord(guildId, { accountId })
|
||||
: await discordGuildActionRuntime.listGuildChannelsDiscord(guildId);
|
||||
return jsonResult({ ok: true, channels });
|
||||
}
|
||||
case "voiceStatus": {
|
||||
@ -230,8 +259,10 @@ export async function handleDiscordGuildAction(
|
||||
required: true,
|
||||
});
|
||||
const voice = accountId
|
||||
? await fetchVoiceStatusDiscord(guildId, userId, { accountId })
|
||||
: await fetchVoiceStatusDiscord(guildId, userId);
|
||||
? await discordGuildActionRuntime.fetchVoiceStatusDiscord(guildId, userId, {
|
||||
accountId,
|
||||
})
|
||||
: await discordGuildActionRuntime.fetchVoiceStatusDiscord(guildId, userId);
|
||||
return jsonResult({ ok: true, voice });
|
||||
}
|
||||
case "eventList": {
|
||||
@ -242,8 +273,8 @@ export async function handleDiscordGuildAction(
|
||||
required: true,
|
||||
});
|
||||
const events = accountId
|
||||
? await listScheduledEventsDiscord(guildId, { accountId })
|
||||
: await listScheduledEventsDiscord(guildId);
|
||||
? await discordGuildActionRuntime.listScheduledEventsDiscord(guildId, { accountId })
|
||||
: await discordGuildActionRuntime.listScheduledEventsDiscord(guildId);
|
||||
return jsonResult({ ok: true, events });
|
||||
}
|
||||
case "eventCreate": {
|
||||
@ -274,8 +305,10 @@ export async function handleDiscordGuildAction(
|
||||
privacy_level: 2,
|
||||
};
|
||||
const event = accountId
|
||||
? await createScheduledEventDiscord(guildId, payload, { accountId })
|
||||
: await createScheduledEventDiscord(guildId, payload);
|
||||
? await discordGuildActionRuntime.createScheduledEventDiscord(guildId, payload, {
|
||||
accountId,
|
||||
})
|
||||
: await discordGuildActionRuntime.createScheduledEventDiscord(guildId, payload);
|
||||
return jsonResult({ ok: true, event });
|
||||
}
|
||||
case "channelCreate": {
|
||||
@ -290,7 +323,7 @@ export async function handleDiscordGuildAction(
|
||||
const position = readNumberParam(params, "position", { integer: true });
|
||||
const nsfw = params.nsfw as boolean | undefined;
|
||||
const channel = accountId
|
||||
? await createChannelDiscord(
|
||||
? await discordGuildActionRuntime.createChannelDiscord(
|
||||
{
|
||||
guildId,
|
||||
name,
|
||||
@ -302,7 +335,7 @@ export async function handleDiscordGuildAction(
|
||||
},
|
||||
{ accountId },
|
||||
)
|
||||
: await createChannelDiscord({
|
||||
: await discordGuildActionRuntime.createChannelDiscord({
|
||||
guildId,
|
||||
name,
|
||||
type: type ?? undefined,
|
||||
@ -348,8 +381,8 @@ export async function handleDiscordGuildAction(
|
||||
availableTags,
|
||||
};
|
||||
const channel = accountId
|
||||
? await editChannelDiscord(editPayload, { accountId })
|
||||
: await editChannelDiscord(editPayload);
|
||||
? await discordGuildActionRuntime.editChannelDiscord(editPayload, { accountId })
|
||||
: await discordGuildActionRuntime.editChannelDiscord(editPayload);
|
||||
return jsonResult({ ok: true, channel });
|
||||
}
|
||||
case "channelDelete": {
|
||||
@ -360,8 +393,8 @@ export async function handleDiscordGuildAction(
|
||||
required: true,
|
||||
});
|
||||
const result = accountId
|
||||
? await deleteChannelDiscord(channelId, { accountId })
|
||||
: await deleteChannelDiscord(channelId);
|
||||
? await discordGuildActionRuntime.deleteChannelDiscord(channelId, { accountId })
|
||||
: await discordGuildActionRuntime.deleteChannelDiscord(channelId);
|
||||
return jsonResult(result);
|
||||
}
|
||||
case "channelMove": {
|
||||
@ -375,7 +408,7 @@ export async function handleDiscordGuildAction(
|
||||
const parentId = readDiscordParentIdParam(params);
|
||||
const position = readNumberParam(params, "position", { integer: true });
|
||||
if (accountId) {
|
||||
await moveChannelDiscord(
|
||||
await discordGuildActionRuntime.moveChannelDiscord(
|
||||
{
|
||||
guildId,
|
||||
channelId,
|
||||
@ -385,7 +418,7 @@ export async function handleDiscordGuildAction(
|
||||
{ accountId },
|
||||
);
|
||||
} else {
|
||||
await moveChannelDiscord({
|
||||
await discordGuildActionRuntime.moveChannelDiscord({
|
||||
guildId,
|
||||
channelId,
|
||||
parentId,
|
||||
@ -402,7 +435,7 @@ export async function handleDiscordGuildAction(
|
||||
const name = readStringParam(params, "name", { required: true });
|
||||
const position = readNumberParam(params, "position", { integer: true });
|
||||
const channel = accountId
|
||||
? await createChannelDiscord(
|
||||
? await discordGuildActionRuntime.createChannelDiscord(
|
||||
{
|
||||
guildId,
|
||||
name,
|
||||
@ -411,7 +444,7 @@ export async function handleDiscordGuildAction(
|
||||
},
|
||||
{ accountId },
|
||||
)
|
||||
: await createChannelDiscord({
|
||||
: await discordGuildActionRuntime.createChannelDiscord({
|
||||
guildId,
|
||||
name,
|
||||
type: 4,
|
||||
@ -429,7 +462,7 @@ export async function handleDiscordGuildAction(
|
||||
const name = readStringParam(params, "name");
|
||||
const position = readNumberParam(params, "position", { integer: true });
|
||||
const channel = accountId
|
||||
? await editChannelDiscord(
|
||||
? await discordGuildActionRuntime.editChannelDiscord(
|
||||
{
|
||||
channelId: categoryId,
|
||||
name: name ?? undefined,
|
||||
@ -437,7 +470,7 @@ export async function handleDiscordGuildAction(
|
||||
},
|
||||
{ accountId },
|
||||
)
|
||||
: await editChannelDiscord({
|
||||
: await discordGuildActionRuntime.editChannelDiscord({
|
||||
channelId: categoryId,
|
||||
name: name ?? undefined,
|
||||
position: position ?? undefined,
|
||||
@ -452,8 +485,8 @@ export async function handleDiscordGuildAction(
|
||||
required: true,
|
||||
});
|
||||
const result = accountId
|
||||
? await deleteChannelDiscord(categoryId, { accountId })
|
||||
: await deleteChannelDiscord(categoryId);
|
||||
? await discordGuildActionRuntime.deleteChannelDiscord(categoryId, { accountId })
|
||||
: await discordGuildActionRuntime.deleteChannelDiscord(categoryId);
|
||||
return jsonResult(result);
|
||||
}
|
||||
case "channelPermissionSet": {
|
||||
@ -468,7 +501,7 @@ export async function handleDiscordGuildAction(
|
||||
const allow = readStringParam(params, "allow");
|
||||
const deny = readStringParam(params, "deny");
|
||||
if (accountId) {
|
||||
await setChannelPermissionDiscord(
|
||||
await discordGuildActionRuntime.setChannelPermissionDiscord(
|
||||
{
|
||||
channelId,
|
||||
targetId,
|
||||
@ -479,7 +512,7 @@ export async function handleDiscordGuildAction(
|
||||
{ accountId },
|
||||
);
|
||||
} else {
|
||||
await setChannelPermissionDiscord({
|
||||
await discordGuildActionRuntime.setChannelPermissionDiscord({
|
||||
channelId,
|
||||
targetId,
|
||||
targetType,
|
||||
@ -495,9 +528,11 @@ export async function handleDiscordGuildAction(
|
||||
}
|
||||
const { channelId, targetId } = readChannelPermissionTarget(params);
|
||||
if (accountId) {
|
||||
await removeChannelPermissionDiscord(channelId, targetId, { accountId });
|
||||
await discordGuildActionRuntime.removeChannelPermissionDiscord(channelId, targetId, {
|
||||
accountId,
|
||||
});
|
||||
} else {
|
||||
await removeChannelPermissionDiscord(channelId, targetId);
|
||||
await discordGuildActionRuntime.removeChannelPermissionDiscord(channelId, targetId);
|
||||
}
|
||||
return jsonResult({ ok: true });
|
||||
}
|
||||
@ -1,7 +1,19 @@
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import type { DiscordActionConfig } from "../../config/config.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { readBooleanParam } from "../../plugin-sdk/boolean-param.js";
|
||||
import { readBooleanParam } from "openclaw/plugin-sdk/boolean-param";
|
||||
import { withNormalizedTimestamp } from "../../../../src/agents/date-time.js";
|
||||
import { assertMediaNotDataUrl } from "../../../../src/agents/sandbox-paths.js";
|
||||
import {
|
||||
type ActionGate,
|
||||
jsonResult,
|
||||
readNumberParam,
|
||||
readReactionParams,
|
||||
readStringArrayParam,
|
||||
readStringParam,
|
||||
} from "../../../../src/agents/tools/common.js";
|
||||
import type { OpenClawConfig } from "../../../../src/config/config.js";
|
||||
import type { DiscordActionConfig } from "../../../../src/config/types.discord.js";
|
||||
import { resolvePollMaxSelections } from "../../../../src/polls.js";
|
||||
import { readDiscordComponentSpec } from "../components.js";
|
||||
import {
|
||||
createThreadDiscord,
|
||||
deleteMessageDiscord,
|
||||
@ -23,20 +35,34 @@ import {
|
||||
sendStickerDiscord,
|
||||
sendVoiceMessageDiscord,
|
||||
unpinMessageDiscord,
|
||||
} from "../../plugin-sdk/discord.js";
|
||||
import type { DiscordSendComponents, DiscordSendEmbeds } from "../../plugin-sdk/discord.js";
|
||||
import { readDiscordComponentSpec, resolveDiscordChannelId } from "../../plugin-sdk/discord.js";
|
||||
import { resolvePollMaxSelections } from "../../polls.js";
|
||||
import { withNormalizedTimestamp } from "../date-time.js";
|
||||
import { assertMediaNotDataUrl } from "../sandbox-paths.js";
|
||||
import {
|
||||
type ActionGate,
|
||||
jsonResult,
|
||||
readNumberParam,
|
||||
readReactionParams,
|
||||
readStringArrayParam,
|
||||
readStringParam,
|
||||
} from "./common.js";
|
||||
} from "../send.js";
|
||||
import type { DiscordSendComponents, DiscordSendEmbeds } from "../send.shared.js";
|
||||
import { resolveDiscordChannelId } from "../targets.js";
|
||||
|
||||
export const discordMessagingActionRuntime = {
|
||||
createThreadDiscord,
|
||||
deleteMessageDiscord,
|
||||
editMessageDiscord,
|
||||
fetchChannelPermissionsDiscord,
|
||||
fetchMessageDiscord,
|
||||
fetchReactionsDiscord,
|
||||
listPinsDiscord,
|
||||
listThreadsDiscord,
|
||||
pinMessageDiscord,
|
||||
reactMessageDiscord,
|
||||
readDiscordComponentSpec,
|
||||
readMessagesDiscord,
|
||||
removeOwnReactionsDiscord,
|
||||
removeReactionDiscord,
|
||||
resolveDiscordChannelId,
|
||||
searchMessagesDiscord,
|
||||
sendDiscordComponentMessage,
|
||||
sendMessageDiscord,
|
||||
sendPollDiscord,
|
||||
sendStickerDiscord,
|
||||
sendVoiceMessageDiscord,
|
||||
unpinMessageDiscord,
|
||||
};
|
||||
|
||||
function parseDiscordMessageLink(link: string) {
|
||||
const normalized = link.trim();
|
||||
@ -65,7 +91,7 @@ export async function handleDiscordMessagingAction(
|
||||
cfg?: OpenClawConfig,
|
||||
): Promise<AgentToolResult<unknown>> {
|
||||
const resolveChannelId = () =>
|
||||
resolveDiscordChannelId(
|
||||
discordMessagingActionRuntime.resolveDiscordChannelId(
|
||||
readStringParam(params, "channelId", {
|
||||
required: true,
|
||||
}),
|
||||
@ -95,28 +121,45 @@ export async function handleDiscordMessagingAction(
|
||||
});
|
||||
if (remove) {
|
||||
if (accountId) {
|
||||
await removeReactionDiscord(channelId, messageId, emoji, {
|
||||
await discordMessagingActionRuntime.removeReactionDiscord(channelId, messageId, emoji, {
|
||||
...cfgOptions,
|
||||
accountId,
|
||||
});
|
||||
} else {
|
||||
await removeReactionDiscord(channelId, messageId, emoji, cfgOptions);
|
||||
await discordMessagingActionRuntime.removeReactionDiscord(
|
||||
channelId,
|
||||
messageId,
|
||||
emoji,
|
||||
cfgOptions,
|
||||
);
|
||||
}
|
||||
return jsonResult({ ok: true, removed: emoji });
|
||||
}
|
||||
if (isEmpty) {
|
||||
const removed = accountId
|
||||
? await removeOwnReactionsDiscord(channelId, messageId, { ...cfgOptions, accountId })
|
||||
: await removeOwnReactionsDiscord(channelId, messageId, cfgOptions);
|
||||
? await discordMessagingActionRuntime.removeOwnReactionsDiscord(channelId, messageId, {
|
||||
...cfgOptions,
|
||||
accountId,
|
||||
})
|
||||
: await discordMessagingActionRuntime.removeOwnReactionsDiscord(
|
||||
channelId,
|
||||
messageId,
|
||||
cfgOptions,
|
||||
);
|
||||
return jsonResult({ ok: true, removed: removed.removed });
|
||||
}
|
||||
if (accountId) {
|
||||
await reactMessageDiscord(channelId, messageId, emoji, {
|
||||
await discordMessagingActionRuntime.reactMessageDiscord(channelId, messageId, emoji, {
|
||||
...cfgOptions,
|
||||
accountId,
|
||||
});
|
||||
} else {
|
||||
await reactMessageDiscord(channelId, messageId, emoji, cfgOptions);
|
||||
await discordMessagingActionRuntime.reactMessageDiscord(
|
||||
channelId,
|
||||
messageId,
|
||||
emoji,
|
||||
cfgOptions,
|
||||
);
|
||||
}
|
||||
return jsonResult({ ok: true, added: emoji });
|
||||
}
|
||||
@ -129,11 +172,15 @@ export async function handleDiscordMessagingAction(
|
||||
required: true,
|
||||
});
|
||||
const limit = readNumberParam(params, "limit");
|
||||
const reactions = await fetchReactionsDiscord(channelId, messageId, {
|
||||
...cfgOptions,
|
||||
...(accountId ? { accountId } : {}),
|
||||
limit,
|
||||
});
|
||||
const reactions = await discordMessagingActionRuntime.fetchReactionsDiscord(
|
||||
channelId,
|
||||
messageId,
|
||||
{
|
||||
...cfgOptions,
|
||||
...(accountId ? { accountId } : {}),
|
||||
limit,
|
||||
},
|
||||
);
|
||||
return jsonResult({ ok: true, reactions });
|
||||
}
|
||||
case "sticker": {
|
||||
@ -146,7 +193,7 @@ export async function handleDiscordMessagingAction(
|
||||
required: true,
|
||||
label: "stickerIds",
|
||||
});
|
||||
await sendStickerDiscord(to, stickerIds, {
|
||||
await discordMessagingActionRuntime.sendStickerDiscord(to, stickerIds, {
|
||||
...cfgOptions,
|
||||
...(accountId ? { accountId } : {}),
|
||||
content,
|
||||
@ -169,7 +216,7 @@ export async function handleDiscordMessagingAction(
|
||||
const allowMultiselect = readBooleanParam(params, "allowMultiselect");
|
||||
const durationHours = readNumberParam(params, "durationHours");
|
||||
const maxSelections = resolvePollMaxSelections(answers.length, allowMultiselect);
|
||||
await sendPollDiscord(
|
||||
await discordMessagingActionRuntime.sendPollDiscord(
|
||||
to,
|
||||
{ question, options: answers, maxSelections, durationHours },
|
||||
{ ...cfgOptions, ...(accountId ? { accountId } : {}), content },
|
||||
@ -182,8 +229,11 @@ export async function handleDiscordMessagingAction(
|
||||
}
|
||||
const channelId = resolveChannelId();
|
||||
const permissions = accountId
|
||||
? await fetchChannelPermissionsDiscord(channelId, { ...cfgOptions, accountId })
|
||||
: await fetchChannelPermissionsDiscord(channelId, cfgOptions);
|
||||
? await discordMessagingActionRuntime.fetchChannelPermissionsDiscord(channelId, {
|
||||
...cfgOptions,
|
||||
accountId,
|
||||
})
|
||||
: await discordMessagingActionRuntime.fetchChannelPermissionsDiscord(channelId, cfgOptions);
|
||||
return jsonResult({ ok: true, permissions });
|
||||
}
|
||||
case "fetchMessage": {
|
||||
@ -206,8 +256,11 @@ export async function handleDiscordMessagingAction(
|
||||
);
|
||||
}
|
||||
const message = accountId
|
||||
? await fetchMessageDiscord(channelId, messageId, { ...cfgOptions, accountId })
|
||||
: await fetchMessageDiscord(channelId, messageId, cfgOptions);
|
||||
? await discordMessagingActionRuntime.fetchMessageDiscord(channelId, messageId, {
|
||||
...cfgOptions,
|
||||
accountId,
|
||||
})
|
||||
: await discordMessagingActionRuntime.fetchMessageDiscord(channelId, messageId, cfgOptions);
|
||||
return jsonResult({
|
||||
ok: true,
|
||||
message: normalizeMessage(message),
|
||||
@ -228,8 +281,11 @@ export async function handleDiscordMessagingAction(
|
||||
around: readStringParam(params, "around"),
|
||||
};
|
||||
const messages = accountId
|
||||
? await readMessagesDiscord(channelId, query, { ...cfgOptions, accountId })
|
||||
: await readMessagesDiscord(channelId, query, cfgOptions);
|
||||
? await discordMessagingActionRuntime.readMessagesDiscord(channelId, query, {
|
||||
...cfgOptions,
|
||||
accountId,
|
||||
})
|
||||
: await discordMessagingActionRuntime.readMessagesDiscord(channelId, query, cfgOptions);
|
||||
return jsonResult({
|
||||
ok: true,
|
||||
messages: messages.map((message) => normalizeMessage(message)),
|
||||
@ -245,7 +301,7 @@ export async function handleDiscordMessagingAction(
|
||||
const rawComponents = params.components;
|
||||
const componentSpec =
|
||||
rawComponents && typeof rawComponents === "object" && !Array.isArray(rawComponents)
|
||||
? readDiscordComponentSpec(rawComponents)
|
||||
? discordMessagingActionRuntime.readDiscordComponentSpec(rawComponents)
|
||||
: null;
|
||||
const components: DiscordSendComponents | undefined =
|
||||
Array.isArray(rawComponents) || typeof rawComponents === "function"
|
||||
@ -279,16 +335,20 @@ export async function handleDiscordMessagingAction(
|
||||
const payload = componentSpec.text
|
||||
? componentSpec
|
||||
: { ...componentSpec, text: normalizedContent };
|
||||
const result = await sendDiscordComponentMessage(to, payload, {
|
||||
...cfgOptions,
|
||||
...(accountId ? { accountId } : {}),
|
||||
silent,
|
||||
replyTo: replyTo ?? undefined,
|
||||
sessionKey: sessionKey ?? undefined,
|
||||
agentId: agentId ?? undefined,
|
||||
mediaUrl: mediaUrl ?? undefined,
|
||||
filename: filename ?? undefined,
|
||||
});
|
||||
const result = await discordMessagingActionRuntime.sendDiscordComponentMessage(
|
||||
to,
|
||||
payload,
|
||||
{
|
||||
...cfgOptions,
|
||||
...(accountId ? { accountId } : {}),
|
||||
silent,
|
||||
replyTo: replyTo ?? undefined,
|
||||
sessionKey: sessionKey ?? undefined,
|
||||
agentId: agentId ?? undefined,
|
||||
mediaUrl: mediaUrl ?? undefined,
|
||||
filename: filename ?? undefined,
|
||||
},
|
||||
);
|
||||
return jsonResult({ ok: true, result, components: true });
|
||||
}
|
||||
|
||||
@ -305,7 +365,7 @@ export async function handleDiscordMessagingAction(
|
||||
);
|
||||
}
|
||||
assertMediaNotDataUrl(mediaUrl);
|
||||
const result = await sendVoiceMessageDiscord(to, mediaUrl, {
|
||||
const result = await discordMessagingActionRuntime.sendVoiceMessageDiscord(to, mediaUrl, {
|
||||
...cfgOptions,
|
||||
...(accountId ? { accountId } : {}),
|
||||
replyTo,
|
||||
@ -314,7 +374,7 @@ export async function handleDiscordMessagingAction(
|
||||
return jsonResult({ ok: true, result, voiceMessage: true });
|
||||
}
|
||||
|
||||
const result = await sendMessageDiscord(to, content ?? "", {
|
||||
const result = await discordMessagingActionRuntime.sendMessageDiscord(to, content ?? "", {
|
||||
...cfgOptions,
|
||||
...(accountId ? { accountId } : {}),
|
||||
mediaUrl,
|
||||
@ -338,8 +398,18 @@ export async function handleDiscordMessagingAction(
|
||||
required: true,
|
||||
});
|
||||
const message = accountId
|
||||
? await editMessageDiscord(channelId, messageId, { content }, { ...cfgOptions, accountId })
|
||||
: await editMessageDiscord(channelId, messageId, { content }, cfgOptions);
|
||||
? await discordMessagingActionRuntime.editMessageDiscord(
|
||||
channelId,
|
||||
messageId,
|
||||
{ content },
|
||||
{ ...cfgOptions, accountId },
|
||||
)
|
||||
: await discordMessagingActionRuntime.editMessageDiscord(
|
||||
channelId,
|
||||
messageId,
|
||||
{ content },
|
||||
cfgOptions,
|
||||
);
|
||||
return jsonResult({ ok: true, message });
|
||||
}
|
||||
case "deleteMessage": {
|
||||
@ -351,9 +421,12 @@ export async function handleDiscordMessagingAction(
|
||||
required: true,
|
||||
});
|
||||
if (accountId) {
|
||||
await deleteMessageDiscord(channelId, messageId, { ...cfgOptions, accountId });
|
||||
await discordMessagingActionRuntime.deleteMessageDiscord(channelId, messageId, {
|
||||
...cfgOptions,
|
||||
accountId,
|
||||
});
|
||||
} else {
|
||||
await deleteMessageDiscord(channelId, messageId, cfgOptions);
|
||||
await discordMessagingActionRuntime.deleteMessageDiscord(channelId, messageId, cfgOptions);
|
||||
}
|
||||
return jsonResult({ ok: true });
|
||||
}
|
||||
@ -375,8 +448,11 @@ export async function handleDiscordMessagingAction(
|
||||
appliedTags: appliedTags ?? undefined,
|
||||
};
|
||||
const thread = accountId
|
||||
? await createThreadDiscord(channelId, payload, { ...cfgOptions, accountId })
|
||||
: await createThreadDiscord(channelId, payload, cfgOptions);
|
||||
? await discordMessagingActionRuntime.createThreadDiscord(channelId, payload, {
|
||||
...cfgOptions,
|
||||
accountId,
|
||||
})
|
||||
: await discordMessagingActionRuntime.createThreadDiscord(channelId, payload, cfgOptions);
|
||||
return jsonResult({ ok: true, thread });
|
||||
}
|
||||
case "threadList": {
|
||||
@ -391,7 +467,7 @@ export async function handleDiscordMessagingAction(
|
||||
const before = readStringParam(params, "before");
|
||||
const limit = readNumberParam(params, "limit");
|
||||
const threads = accountId
|
||||
? await listThreadsDiscord(
|
||||
? await discordMessagingActionRuntime.listThreadsDiscord(
|
||||
{
|
||||
guildId,
|
||||
channelId,
|
||||
@ -401,7 +477,7 @@ export async function handleDiscordMessagingAction(
|
||||
},
|
||||
{ ...cfgOptions, accountId },
|
||||
)
|
||||
: await listThreadsDiscord(
|
||||
: await discordMessagingActionRuntime.listThreadsDiscord(
|
||||
{
|
||||
guildId,
|
||||
channelId,
|
||||
@ -423,13 +499,17 @@ export async function handleDiscordMessagingAction(
|
||||
});
|
||||
const mediaUrl = readStringParam(params, "mediaUrl");
|
||||
const replyTo = readStringParam(params, "replyTo");
|
||||
const result = await sendMessageDiscord(`channel:${channelId}`, content, {
|
||||
...cfgOptions,
|
||||
...(accountId ? { accountId } : {}),
|
||||
mediaUrl,
|
||||
mediaLocalRoots: options?.mediaLocalRoots,
|
||||
replyTo,
|
||||
});
|
||||
const result = await discordMessagingActionRuntime.sendMessageDiscord(
|
||||
`channel:${channelId}`,
|
||||
content,
|
||||
{
|
||||
...cfgOptions,
|
||||
...(accountId ? { accountId } : {}),
|
||||
mediaUrl,
|
||||
mediaLocalRoots: options?.mediaLocalRoots,
|
||||
replyTo,
|
||||
},
|
||||
);
|
||||
return jsonResult({ ok: true, result });
|
||||
}
|
||||
case "pinMessage": {
|
||||
@ -441,9 +521,12 @@ export async function handleDiscordMessagingAction(
|
||||
required: true,
|
||||
});
|
||||
if (accountId) {
|
||||
await pinMessageDiscord(channelId, messageId, { ...cfgOptions, accountId });
|
||||
await discordMessagingActionRuntime.pinMessageDiscord(channelId, messageId, {
|
||||
...cfgOptions,
|
||||
accountId,
|
||||
});
|
||||
} else {
|
||||
await pinMessageDiscord(channelId, messageId, cfgOptions);
|
||||
await discordMessagingActionRuntime.pinMessageDiscord(channelId, messageId, cfgOptions);
|
||||
}
|
||||
return jsonResult({ ok: true });
|
||||
}
|
||||
@ -456,9 +539,12 @@ export async function handleDiscordMessagingAction(
|
||||
required: true,
|
||||
});
|
||||
if (accountId) {
|
||||
await unpinMessageDiscord(channelId, messageId, { ...cfgOptions, accountId });
|
||||
await discordMessagingActionRuntime.unpinMessageDiscord(channelId, messageId, {
|
||||
...cfgOptions,
|
||||
accountId,
|
||||
});
|
||||
} else {
|
||||
await unpinMessageDiscord(channelId, messageId, cfgOptions);
|
||||
await discordMessagingActionRuntime.unpinMessageDiscord(channelId, messageId, cfgOptions);
|
||||
}
|
||||
return jsonResult({ ok: true });
|
||||
}
|
||||
@ -468,8 +554,11 @@ export async function handleDiscordMessagingAction(
|
||||
}
|
||||
const channelId = resolveChannelId();
|
||||
const pins = accountId
|
||||
? await listPinsDiscord(channelId, { ...cfgOptions, accountId })
|
||||
: await listPinsDiscord(channelId, cfgOptions);
|
||||
? await discordMessagingActionRuntime.listPinsDiscord(channelId, {
|
||||
...cfgOptions,
|
||||
accountId,
|
||||
})
|
||||
: await discordMessagingActionRuntime.listPinsDiscord(channelId, cfgOptions);
|
||||
return jsonResult({ ok: true, pins: pins.map((pin) => normalizeMessage(pin)) });
|
||||
}
|
||||
case "searchMessages": {
|
||||
@ -490,7 +579,7 @@ export async function handleDiscordMessagingAction(
|
||||
const channelIdList = [...(channelIds ?? []), ...(channelId ? [channelId] : [])];
|
||||
const authorIdList = [...(authorIds ?? []), ...(authorId ? [authorId] : [])];
|
||||
const results = accountId
|
||||
? await searchMessagesDiscord(
|
||||
? await discordMessagingActionRuntime.searchMessagesDiscord(
|
||||
{
|
||||
guildId,
|
||||
content,
|
||||
@ -500,7 +589,7 @@ export async function handleDiscordMessagingAction(
|
||||
},
|
||||
{ ...cfgOptions, accountId },
|
||||
)
|
||||
: await searchMessagesDiscord(
|
||||
: await discordMessagingActionRuntime.searchMessagesDiscord(
|
||||
{
|
||||
guildId,
|
||||
content,
|
||||
@ -1,5 +1,5 @@
|
||||
import { PermissionFlagsBits } from "discord-api-types/v10";
|
||||
import { readNumberParam, readStringParam } from "./common.js";
|
||||
import { readNumberParam, readStringParam } from "../../../../src/agents/tools/common.js";
|
||||
|
||||
export type DiscordModerationAction = "timeout" | "kick" | "ban";
|
||||
|
||||
@ -1,25 +1,30 @@
|
||||
import { PermissionFlagsBits } from "discord-api-types/v10";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { DiscordActionConfig } from "../../config/config.js";
|
||||
import { handleDiscordModerationAction } from "./discord-actions-moderation.js";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { DiscordActionConfig } from "../../../../src/config/types.discord.js";
|
||||
import {
|
||||
discordModerationActionRuntime,
|
||||
handleDiscordModerationAction,
|
||||
} from "./runtime.moderation.js";
|
||||
|
||||
const discordSendMocks = vi.hoisted(() => ({
|
||||
banMemberDiscord: vi.fn(async () => ({ ok: true })),
|
||||
kickMemberDiscord: vi.fn(async () => ({ ok: true })),
|
||||
timeoutMemberDiscord: vi.fn(async () => ({ id: "user-1" })),
|
||||
hasAnyGuildPermissionDiscord: vi.fn(async () => false),
|
||||
}));
|
||||
|
||||
const { banMemberDiscord, kickMemberDiscord, timeoutMemberDiscord, hasAnyGuildPermissionDiscord } =
|
||||
discordSendMocks;
|
||||
|
||||
vi.mock("../../../extensions/discord/src/send.js", () => ({
|
||||
...discordSendMocks,
|
||||
}));
|
||||
const originalDiscordModerationActionRuntime = { ...discordModerationActionRuntime };
|
||||
const banMemberDiscord = vi.fn(async () => ({ ok: true }));
|
||||
const kickMemberDiscord = vi.fn(async () => ({ ok: true }));
|
||||
const timeoutMemberDiscord = vi.fn(async () => ({ id: "user-1" }));
|
||||
const hasAnyGuildPermissionDiscord = vi.fn(async () => false);
|
||||
|
||||
const enableAllActions = (_key: keyof DiscordActionConfig, _defaultValue = true) => true;
|
||||
|
||||
describe("discord moderation sender authorization", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
Object.assign(discordModerationActionRuntime, originalDiscordModerationActionRuntime, {
|
||||
banMemberDiscord,
|
||||
kickMemberDiscord,
|
||||
timeoutMemberDiscord,
|
||||
hasAnyGuildPermissionDiscord,
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects ban when sender lacks BAN_MEMBERS", async () => {
|
||||
hasAnyGuildPermissionDiscord.mockResolvedValueOnce(false);
|
||||
|
||||
@ -1,17 +1,28 @@
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import type { DiscordActionConfig } from "../../config/config.js";
|
||||
import {
|
||||
type ActionGate,
|
||||
jsonResult,
|
||||
readStringParam,
|
||||
} from "../../../../src/agents/tools/common.js";
|
||||
import type { DiscordActionConfig } from "../../../../src/config/types.discord.js";
|
||||
import {
|
||||
banMemberDiscord,
|
||||
hasAnyGuildPermissionDiscord,
|
||||
kickMemberDiscord,
|
||||
timeoutMemberDiscord,
|
||||
} from "../../plugin-sdk/discord.js";
|
||||
import { type ActionGate, jsonResult, readStringParam } from "./common.js";
|
||||
} from "../send.js";
|
||||
import {
|
||||
isDiscordModerationAction,
|
||||
readDiscordModerationCommand,
|
||||
requiredGuildPermissionForModerationAction,
|
||||
} from "./discord-actions-moderation-shared.js";
|
||||
} from "./runtime.moderation-shared.js";
|
||||
|
||||
export const discordModerationActionRuntime = {
|
||||
banMemberDiscord,
|
||||
hasAnyGuildPermissionDiscord,
|
||||
kickMemberDiscord,
|
||||
timeoutMemberDiscord,
|
||||
};
|
||||
|
||||
async function verifySenderModerationPermission(params: {
|
||||
guildId: string;
|
||||
@ -23,7 +34,7 @@ async function verifySenderModerationPermission(params: {
|
||||
if (!params.senderUserId) {
|
||||
return;
|
||||
}
|
||||
const hasPermission = await hasAnyGuildPermissionDiscord(
|
||||
const hasPermission = await discordModerationActionRuntime.hasAnyGuildPermissionDiscord(
|
||||
params.guildId,
|
||||
params.senderUserId,
|
||||
[params.requiredPermission],
|
||||
@ -57,7 +68,7 @@ export async function handleDiscordModerationAction(
|
||||
switch (command.action) {
|
||||
case "timeout": {
|
||||
const member = accountId
|
||||
? await timeoutMemberDiscord(
|
||||
? await discordModerationActionRuntime.timeoutMemberDiscord(
|
||||
{
|
||||
guildId: command.guildId,
|
||||
userId: command.userId,
|
||||
@ -67,7 +78,7 @@ export async function handleDiscordModerationAction(
|
||||
},
|
||||
{ accountId },
|
||||
)
|
||||
: await timeoutMemberDiscord({
|
||||
: await discordModerationActionRuntime.timeoutMemberDiscord({
|
||||
guildId: command.guildId,
|
||||
userId: command.userId,
|
||||
durationMinutes: command.durationMinutes,
|
||||
@ -78,7 +89,7 @@ export async function handleDiscordModerationAction(
|
||||
}
|
||||
case "kick": {
|
||||
if (accountId) {
|
||||
await kickMemberDiscord(
|
||||
await discordModerationActionRuntime.kickMemberDiscord(
|
||||
{
|
||||
guildId: command.guildId,
|
||||
userId: command.userId,
|
||||
@ -87,7 +98,7 @@ export async function handleDiscordModerationAction(
|
||||
{ accountId },
|
||||
);
|
||||
} else {
|
||||
await kickMemberDiscord({
|
||||
await discordModerationActionRuntime.kickMemberDiscord({
|
||||
guildId: command.guildId,
|
||||
userId: command.userId,
|
||||
reason: command.reason,
|
||||
@ -97,7 +108,7 @@ export async function handleDiscordModerationAction(
|
||||
}
|
||||
case "ban": {
|
||||
if (accountId) {
|
||||
await banMemberDiscord(
|
||||
await discordModerationActionRuntime.banMemberDiscord(
|
||||
{
|
||||
guildId: command.guildId,
|
||||
userId: command.userId,
|
||||
@ -107,7 +118,7 @@ export async function handleDiscordModerationAction(
|
||||
{ accountId },
|
||||
);
|
||||
} else {
|
||||
await banMemberDiscord({
|
||||
await discordModerationActionRuntime.banMemberDiscord({
|
||||
guildId: command.guildId,
|
||||
userId: command.userId,
|
||||
reason: command.reason,
|
||||
@ -1,12 +1,9 @@
|
||||
import type { GatewayPlugin } from "@buape/carbon/gateway";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
clearGateways,
|
||||
registerGateway,
|
||||
} from "../../../extensions/discord/src/monitor/gateway-registry.js";
|
||||
import type { DiscordActionConfig } from "../../config/config.js";
|
||||
import type { ActionGate } from "./common.js";
|
||||
import { handleDiscordPresenceAction } from "./discord-actions-presence.js";
|
||||
import type { ActionGate } from "../../../../src/agents/tools/common.js";
|
||||
import type { DiscordActionConfig } from "../../../../src/config/types.discord.js";
|
||||
import { clearGateways, registerGateway } from "../monitor/gateway-registry.js";
|
||||
import { handleDiscordPresenceAction } from "./runtime.presence.js";
|
||||
|
||||
const mockUpdatePresence = vi.fn();
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
import type { Activity, UpdatePresenceData } from "@buape/carbon/gateway";
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import type { DiscordActionConfig } from "../../config/config.js";
|
||||
import { getGateway } from "../../plugin-sdk/discord.js";
|
||||
import { type ActionGate, jsonResult, readStringParam } from "./common.js";
|
||||
import {
|
||||
type ActionGate,
|
||||
jsonResult,
|
||||
readStringParam,
|
||||
} from "../../../../src/agents/tools/common.js";
|
||||
import type { DiscordActionConfig } from "../../../../src/config/types.discord.js";
|
||||
import { getGateway } from "../monitor/gateway-registry.js";
|
||||
|
||||
const ACTIVITY_TYPE_MAP: Record<string, number> = {
|
||||
playing: 0,
|
||||
@ -1,4 +1,4 @@
|
||||
import { readStringParam } from "./common.js";
|
||||
import { readStringParam } from "../../../../src/agents/tools/common.js";
|
||||
|
||||
export function readDiscordParentIdParam(
|
||||
params: Record<string, unknown>,
|
||||
@ -1,11 +1,22 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { DiscordActionConfig, OpenClawConfig } from "../../config/config.js";
|
||||
import { handleDiscordGuildAction } from "./discord-actions-guild.js";
|
||||
import { handleDiscordMessagingAction } from "./discord-actions-messaging.js";
|
||||
import { handleDiscordModerationAction } from "./discord-actions-moderation.js";
|
||||
import { handleDiscordAction } from "./discord-actions.js";
|
||||
import type { OpenClawConfig } from "../../../../src/config/config.js";
|
||||
import type { DiscordActionConfig } from "../../../../src/config/types.discord.js";
|
||||
import { discordGuildActionRuntime, handleDiscordGuildAction } from "./runtime.guild.js";
|
||||
import { handleDiscordAction } from "./runtime.js";
|
||||
import {
|
||||
discordMessagingActionRuntime,
|
||||
handleDiscordMessagingAction,
|
||||
} from "./runtime.messaging.js";
|
||||
import {
|
||||
discordModerationActionRuntime,
|
||||
handleDiscordModerationAction,
|
||||
} from "./runtime.moderation.js";
|
||||
|
||||
const discordSendMocks = vi.hoisted(() => ({
|
||||
const originalDiscordMessagingActionRuntime = { ...discordMessagingActionRuntime };
|
||||
const originalDiscordGuildActionRuntime = { ...discordGuildActionRuntime };
|
||||
const originalDiscordModerationActionRuntime = { ...discordModerationActionRuntime };
|
||||
|
||||
const discordSendMocks = {
|
||||
banMemberDiscord: vi.fn(async () => ({})),
|
||||
createChannelDiscord: vi.fn(async () => ({
|
||||
id: "new-channel",
|
||||
@ -42,7 +53,7 @@ const discordSendMocks = vi.hoisted(() => ({
|
||||
setChannelPermissionDiscord: vi.fn(async () => ({ ok: true })),
|
||||
timeoutMemberDiscord: vi.fn(async () => ({})),
|
||||
unpinMessageDiscord: vi.fn(async () => ({})),
|
||||
}));
|
||||
};
|
||||
|
||||
const {
|
||||
createChannelDiscord,
|
||||
@ -67,21 +78,28 @@ const {
|
||||
timeoutMemberDiscord,
|
||||
} = discordSendMocks;
|
||||
|
||||
vi.mock("../../../extensions/discord/src/send.js", () => ({
|
||||
...discordSendMocks,
|
||||
}));
|
||||
|
||||
const enableAllActions = () => true;
|
||||
|
||||
const disabledActions = (key: keyof DiscordActionConfig) => key !== "reactions";
|
||||
const channelInfoEnabled = (key: keyof DiscordActionConfig) => key === "channelInfo";
|
||||
const moderationEnabled = (key: keyof DiscordActionConfig) => key === "moderation";
|
||||
|
||||
describe("handleDiscordMessagingAction", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
Object.assign(
|
||||
discordMessagingActionRuntime,
|
||||
originalDiscordMessagingActionRuntime,
|
||||
discordSendMocks,
|
||||
);
|
||||
Object.assign(discordGuildActionRuntime, originalDiscordGuildActionRuntime, discordSendMocks);
|
||||
Object.assign(
|
||||
discordModerationActionRuntime,
|
||||
originalDiscordModerationActionRuntime,
|
||||
discordSendMocks,
|
||||
);
|
||||
});
|
||||
|
||||
describe("handleDiscordMessagingAction", () => {
|
||||
it.each([
|
||||
{
|
||||
name: "without account",
|
||||
@ -1,11 +1,11 @@
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { createDiscordActionGate } from "../../plugin-sdk/discord.js";
|
||||
import { readStringParam } from "./common.js";
|
||||
import { handleDiscordGuildAction } from "./discord-actions-guild.js";
|
||||
import { handleDiscordMessagingAction } from "./discord-actions-messaging.js";
|
||||
import { handleDiscordModerationAction } from "./discord-actions-moderation.js";
|
||||
import { handleDiscordPresenceAction } from "./discord-actions-presence.js";
|
||||
import { readStringParam } from "../../../../src/agents/tools/common.js";
|
||||
import type { OpenClawConfig } from "../../../../src/config/config.js";
|
||||
import { createDiscordActionGate } from "../accounts.js";
|
||||
import { handleDiscordGuildAction } from "./runtime.guild.js";
|
||||
import { handleDiscordMessagingAction } from "./runtime.messaging.js";
|
||||
import { handleDiscordModerationAction } from "./runtime.moderation.js";
|
||||
import { handleDiscordPresenceAction } from "./runtime.presence.js";
|
||||
|
||||
const messagingActions = new Set([
|
||||
"react",
|
||||
@ -79,12 +79,6 @@ function formatDiscordIntents(intents?: {
|
||||
const discordMessageActions: ChannelMessageActionAdapter = {
|
||||
describeMessageTool: (ctx) =>
|
||||
getDiscordRuntime().channel.discord.messageActions?.describeMessageTool?.(ctx) ?? null,
|
||||
listActions: (ctx) =>
|
||||
getDiscordRuntime().channel.discord.messageActions?.listActions?.(ctx) ?? [],
|
||||
getCapabilities: (ctx) =>
|
||||
getDiscordRuntime().channel.discord.messageActions?.getCapabilities?.(ctx) ?? [],
|
||||
getToolSchema: (ctx) =>
|
||||
getDiscordRuntime().channel.discord.messageActions?.getToolSchema?.(ctx) ?? null,
|
||||
extractToolSend: (ctx) =>
|
||||
getDiscordRuntime().channel.discord.messageActions?.extractToolSend?.(ctx) ?? null,
|
||||
handleAction: async (ctx) => {
|
||||
@ -366,6 +360,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: normalizeDiscordMessagingTarget,
|
||||
resolveSessionTarget: ({ id }) => normalizeDiscordMessagingTarget(`channel:${id}`),
|
||||
parseExplicitTarget: ({ raw }) => parseDiscordExplicitTarget(raw),
|
||||
inferTargetChatType: ({ to }) => parseDiscordExplicitTarget(to)?.chatType,
|
||||
buildCrossContextComponents: buildDiscordCrossContextComponents,
|
||||
|
||||
@ -54,6 +54,10 @@ vi.mock("./channel.runtime.js", () => ({
|
||||
|
||||
import { feishuPlugin } from "./channel.js";
|
||||
|
||||
function getDescribedActions(cfg: OpenClawConfig): string[] {
|
||||
return [...(feishuPlugin.actions?.describeMessageTool?.({ cfg })?.actions ?? [])];
|
||||
}
|
||||
|
||||
describe("feishuPlugin.status.probeAccount", () => {
|
||||
it("uses current account credentials for multi-account config", async () => {
|
||||
const cfg = {
|
||||
@ -112,7 +116,7 @@ describe("feishuPlugin actions", () => {
|
||||
});
|
||||
|
||||
it("advertises the expanded Feishu action surface", () => {
|
||||
expect(feishuPlugin.actions?.listActions?.({ cfg })).toEqual([
|
||||
expect(getDescribedActions(cfg)).toEqual([
|
||||
"send",
|
||||
"read",
|
||||
"edit",
|
||||
@ -142,7 +146,7 @@ describe("feishuPlugin actions", () => {
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(feishuPlugin.actions?.listActions?.({ cfg: disabledCfg })).toEqual([
|
||||
expect(getDescribedActions(disabledCfg)).toEqual([
|
||||
"send",
|
||||
"read",
|
||||
"edit",
|
||||
|
||||
@ -2,7 +2,10 @@ import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
|
||||
import { mapAllowFromEntries } from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { collectAllowlistProviderRestrictSendersWarnings } from "openclaw/plugin-sdk/channel-policy";
|
||||
import { createMessageToolCardSchema } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import type { ChannelMessageActionAdapter } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import type {
|
||||
ChannelMessageActionAdapter,
|
||||
ChannelMessageToolDiscovery,
|
||||
} from "openclaw/plugin-sdk/channel-runtime";
|
||||
import type { ChannelMeta, ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
@ -49,6 +52,56 @@ const loadFeishuChannelRuntime = createLazyRuntimeNamedExport(
|
||||
"feishuChannelRuntime",
|
||||
);
|
||||
|
||||
function describeFeishuMessageTool({
|
||||
cfg,
|
||||
}: Parameters<
|
||||
NonNullable<ChannelMessageActionAdapter["describeMessageTool"]>
|
||||
>[0]): ChannelMessageToolDiscovery {
|
||||
const enabled =
|
||||
cfg.channels?.feishu?.enabled !== false &&
|
||||
Boolean(resolveFeishuCredentials(cfg.channels?.feishu as FeishuConfig | undefined));
|
||||
if (listEnabledFeishuAccounts(cfg).length === 0) {
|
||||
return {
|
||||
actions: [],
|
||||
capabilities: enabled ? ["cards"] : [],
|
||||
schema: enabled
|
||||
? {
|
||||
properties: {
|
||||
card: createMessageToolCardSchema(),
|
||||
},
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
const actions = new Set<ChannelMessageActionName>([
|
||||
"send",
|
||||
"read",
|
||||
"edit",
|
||||
"thread-reply",
|
||||
"pin",
|
||||
"list-pins",
|
||||
"unpin",
|
||||
"member-info",
|
||||
"channel-info",
|
||||
"channel-list",
|
||||
]);
|
||||
if (areAnyFeishuReactionActionsEnabled(cfg)) {
|
||||
actions.add("react");
|
||||
actions.add("reactions");
|
||||
}
|
||||
return {
|
||||
actions: Array.from(actions),
|
||||
capabilities: enabled ? ["cards"] : [],
|
||||
schema: enabled
|
||||
? {
|
||||
properties: {
|
||||
card: createMessageToolCardSchema(),
|
||||
},
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
function setFeishuNamedAccountEnabled(
|
||||
cfg: ClawdbotConfig,
|
||||
accountId: string,
|
||||
@ -396,53 +449,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
||||
formatAllowFrom: ({ allowFrom }) => formatAllowFromLowercase({ allowFrom }),
|
||||
},
|
||||
actions: {
|
||||
describeMessageTool: ({
|
||||
cfg,
|
||||
}: Parameters<NonNullable<ChannelMessageActionAdapter["describeMessageTool"]>>[0]) => {
|
||||
const enabled =
|
||||
cfg.channels?.feishu?.enabled !== false &&
|
||||
Boolean(resolveFeishuCredentials(cfg.channels?.feishu as FeishuConfig | undefined));
|
||||
if (listEnabledFeishuAccounts(cfg).length === 0) {
|
||||
return {
|
||||
actions: [],
|
||||
capabilities: enabled ? ["cards"] : [],
|
||||
schema: enabled
|
||||
? {
|
||||
properties: {
|
||||
card: createMessageToolCardSchema(),
|
||||
},
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
const actions = new Set<ChannelMessageActionName>([
|
||||
"send",
|
||||
"read",
|
||||
"edit",
|
||||
"thread-reply",
|
||||
"pin",
|
||||
"list-pins",
|
||||
"unpin",
|
||||
"member-info",
|
||||
"channel-info",
|
||||
"channel-list",
|
||||
]);
|
||||
if (areAnyFeishuReactionActionsEnabled(cfg)) {
|
||||
actions.add("react");
|
||||
actions.add("reactions");
|
||||
}
|
||||
return {
|
||||
actions: Array.from(actions),
|
||||
capabilities: enabled ? ["cards"] : [],
|
||||
schema: enabled
|
||||
? {
|
||||
properties: {
|
||||
card: createMessageToolCardSchema(),
|
||||
},
|
||||
}
|
||||
: null,
|
||||
};
|
||||
},
|
||||
describeMessageTool: describeFeishuMessageTool,
|
||||
handleAction: async (ctx) => {
|
||||
const account = resolveFeishuAccount({ cfg: ctx.cfg, accountId: ctx.accountId ?? undefined });
|
||||
if (
|
||||
|
||||
1
extensions/mattermost/runtime-api.ts
Normal file
1
extensions/mattermost/runtime-api.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "openclaw/plugin-sdk/mattermost";
|
||||
@ -17,6 +17,10 @@ import {
|
||||
withMockedGlobalFetch,
|
||||
} from "./mattermost/reactions.test-helpers.js";
|
||||
|
||||
function getDescribedActions(cfg: OpenClawConfig): string[] {
|
||||
return [...(mattermostPlugin.actions?.describeMessageTool?.({ cfg })?.actions ?? [])];
|
||||
}
|
||||
|
||||
describe("mattermostPlugin", () => {
|
||||
beforeEach(() => {
|
||||
sendMessageMattermostMock.mockReset();
|
||||
@ -132,7 +136,7 @@ describe("mattermostPlugin", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const actions = mattermostPlugin.actions?.listActions?.({ cfg }) ?? [];
|
||||
const actions = getDescribedActions(cfg);
|
||||
expect(actions).toContain("react");
|
||||
expect(actions).toContain("send");
|
||||
expect(mattermostPlugin.actions?.supportsAction?.({ action: "react" })).toBe(true);
|
||||
@ -148,7 +152,7 @@ describe("mattermostPlugin", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const actions = mattermostPlugin.actions?.listActions?.({ cfg }) ?? [];
|
||||
const actions = getDescribedActions(cfg);
|
||||
expect(actions).toEqual([]);
|
||||
});
|
||||
|
||||
@ -164,7 +168,7 @@ describe("mattermostPlugin", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const actions = mattermostPlugin.actions?.listActions?.({ cfg }) ?? [];
|
||||
const actions = getDescribedActions(cfg);
|
||||
expect(actions).not.toContain("react");
|
||||
expect(actions).toContain("send");
|
||||
});
|
||||
@ -187,7 +191,7 @@ describe("mattermostPlugin", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const actions = mattermostPlugin.actions?.listActions?.({ cfg }) ?? [];
|
||||
const actions = getDescribedActions(cfg);
|
||||
expect(actions).toContain("react");
|
||||
});
|
||||
|
||||
|
||||
@ -5,19 +5,7 @@ import {
|
||||
collectAllowlistProviderRestrictSendersWarnings,
|
||||
} from "openclaw/plugin-sdk/channel-policy";
|
||||
import { createMessageToolButtonsSchema } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import {
|
||||
buildComputedAccountStatusSnapshot,
|
||||
buildChannelConfigSchema,
|
||||
createAccountStatusSink,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
deleteAccountFromConfigSection,
|
||||
resolveAllowlistProviderRuntimeGroupPolicy,
|
||||
resolveDefaultGroupPolicy,
|
||||
setAccountEnabledInConfigSection,
|
||||
type ChannelMessageActionAdapter,
|
||||
type ChannelMessageActionName,
|
||||
type ChannelPlugin,
|
||||
} from "openclaw/plugin-sdk/mattermost";
|
||||
import type { ChannelMessageToolDiscovery } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { buildPassiveProbedChannelStatusSummary } from "../../shared/channel-status-summary.js";
|
||||
import { MattermostConfigSchema } from "./config-schema.js";
|
||||
import { resolveMattermostGroupRequireMention } from "./group-mentions.js";
|
||||
@ -38,50 +26,65 @@ import { addMattermostReaction, removeMattermostReaction } from "./mattermost/re
|
||||
import { sendMessageMattermost } from "./mattermost/send.js";
|
||||
import { resolveMattermostOpaqueTarget } from "./mattermost/target-resolution.js";
|
||||
import { looksLikeMattermostTargetId, normalizeMattermostMessagingTarget } from "./normalize.js";
|
||||
import {
|
||||
buildComputedAccountStatusSnapshot,
|
||||
buildChannelConfigSchema,
|
||||
createAccountStatusSink,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
deleteAccountFromConfigSection,
|
||||
resolveAllowlistProviderRuntimeGroupPolicy,
|
||||
resolveDefaultGroupPolicy,
|
||||
setAccountEnabledInConfigSection,
|
||||
type ChannelMessageActionAdapter,
|
||||
type ChannelMessageActionName,
|
||||
type ChannelPlugin,
|
||||
} from "./runtime-api.js";
|
||||
import { getMattermostRuntime } from "./runtime.js";
|
||||
import { mattermostSetupAdapter } from "./setup-core.js";
|
||||
import { mattermostSetupWizard } from "./setup-surface.js";
|
||||
|
||||
function describeMattermostMessageTool({
|
||||
cfg,
|
||||
}: Parameters<
|
||||
NonNullable<ChannelMessageActionAdapter["describeMessageTool"]>
|
||||
>[0]): ChannelMessageToolDiscovery {
|
||||
const enabledAccounts = listMattermostAccountIds(cfg)
|
||||
.map((accountId) => resolveMattermostAccount({ cfg, accountId }))
|
||||
.filter((account) => account.enabled)
|
||||
.filter((account) => Boolean(account.botToken?.trim() && account.baseUrl?.trim()));
|
||||
|
||||
const actions: ChannelMessageActionName[] = [];
|
||||
|
||||
if (enabledAccounts.length > 0) {
|
||||
actions.push("send");
|
||||
}
|
||||
|
||||
const actionsConfig = cfg.channels?.mattermost?.actions as { reactions?: boolean } | undefined;
|
||||
const baseReactions = actionsConfig?.reactions;
|
||||
const hasReactionCapableAccount = enabledAccounts.some((account) => {
|
||||
const accountActions = account.config.actions as { reactions?: boolean } | undefined;
|
||||
return (accountActions?.reactions ?? baseReactions ?? true) !== false;
|
||||
});
|
||||
if (hasReactionCapableAccount) {
|
||||
actions.push("react");
|
||||
}
|
||||
|
||||
return {
|
||||
actions,
|
||||
capabilities: enabledAccounts.length > 0 ? ["buttons"] : [],
|
||||
schema:
|
||||
enabledAccounts.length > 0
|
||||
? {
|
||||
properties: {
|
||||
buttons: createMessageToolButtonsSchema(),
|
||||
},
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
const mattermostMessageActions: ChannelMessageActionAdapter = {
|
||||
describeMessageTool: ({
|
||||
cfg,
|
||||
}: Parameters<NonNullable<ChannelMessageActionAdapter["describeMessageTool"]>>[0]) => {
|
||||
const enabledAccounts = listMattermostAccountIds(cfg)
|
||||
.map((accountId) => resolveMattermostAccount({ cfg, accountId }))
|
||||
.filter((account) => account.enabled)
|
||||
.filter((account) => Boolean(account.botToken?.trim() && account.baseUrl?.trim()));
|
||||
|
||||
const actions: ChannelMessageActionName[] = [];
|
||||
|
||||
// Send (buttons) is available whenever there's at least one enabled account
|
||||
if (enabledAccounts.length > 0) {
|
||||
actions.push("send");
|
||||
}
|
||||
|
||||
// React requires per-account reactions config check
|
||||
const actionsConfig = cfg.channels?.mattermost?.actions as { reactions?: boolean } | undefined;
|
||||
const baseReactions = actionsConfig?.reactions;
|
||||
const hasReactionCapableAccount = enabledAccounts.some((account) => {
|
||||
const accountActions = account.config.actions as { reactions?: boolean } | undefined;
|
||||
return (accountActions?.reactions ?? baseReactions ?? true) !== false;
|
||||
});
|
||||
if (hasReactionCapableAccount) {
|
||||
actions.push("react");
|
||||
}
|
||||
|
||||
return {
|
||||
actions,
|
||||
capabilities: enabledAccounts.length > 0 ? ["buttons"] : [],
|
||||
schema:
|
||||
enabledAccounts.length > 0
|
||||
? {
|
||||
properties: {
|
||||
buttons: createMessageToolButtonsSchema(),
|
||||
},
|
||||
}
|
||||
: null,
|
||||
};
|
||||
},
|
||||
describeMessageTool: describeMattermostMessageTool,
|
||||
supportsAction: ({ action }) => {
|
||||
return action === "send" || action === "react";
|
||||
},
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { z } from "zod";
|
||||
import { requireChannelOpenAllowFrom } from "../../shared/config-schema-helpers.js";
|
||||
import {
|
||||
BlockStreamingCoalesceSchema,
|
||||
DmPolicySchema,
|
||||
GroupPolicySchema,
|
||||
MarkdownConfigSchema,
|
||||
requireOpenAllowFrom,
|
||||
} from "openclaw/plugin-sdk/mattermost";
|
||||
import { z } from "zod";
|
||||
import { requireChannelOpenAllowFrom } from "../../shared/config-schema-helpers.js";
|
||||
} from "./runtime-api.js";
|
||||
import { buildSecretInputSchema } from "./secret-input.js";
|
||||
|
||||
const DmChannelRetrySchema = z
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { resolveChannelGroupRequireMention } from "openclaw/plugin-sdk/channel-policy";
|
||||
import type { ChannelGroupContext } from "openclaw/plugin-sdk/mattermost";
|
||||
import { resolveMattermostAccount } from "./mattermost/accounts.js";
|
||||
import type { ChannelGroupContext } from "./runtime-api.js";
|
||||
|
||||
export function resolveMattermostGroupRequireMention(
|
||||
params: ChannelGroupContext & { requireMentionOverride?: boolean },
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import { createAccountListHelpers, type OpenClawConfig } from "openclaw/plugin-sdk/mattermost";
|
||||
import { createAccountListHelpers, type OpenClawConfig } from "../runtime-api.js";
|
||||
import { normalizeResolvedSecretInputString, normalizeSecretInputString } from "../secret-input.js";
|
||||
import type {
|
||||
MattermostAccountConfig,
|
||||
|
||||
@ -1,8 +1,4 @@
|
||||
import type {
|
||||
ChannelDirectoryEntry,
|
||||
OpenClawConfig,
|
||||
RuntimeEnv,
|
||||
} from "openclaw/plugin-sdk/mattermost";
|
||||
import type { ChannelDirectoryEntry, OpenClawConfig, RuntimeEnv } from "../runtime-api.js";
|
||||
import { listMattermostAccountIds, resolveMattermostAccount } from "./accounts.js";
|
||||
import {
|
||||
createMattermostClient,
|
||||
|
||||
@ -1,10 +1,6 @@
|
||||
import { createHmac, timingSafeEqual } from "node:crypto";
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import {
|
||||
isTrustedProxyAddress,
|
||||
resolveClientIp,
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/mattermost";
|
||||
import { isTrustedProxyAddress, resolveClientIp, type OpenClawConfig } from "../runtime-api.js";
|
||||
import { getMattermostRuntime } from "../runtime.js";
|
||||
import { updateMattermostPost, type MattermostClient, type MattermostPost } from "./client.js";
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ import {
|
||||
resolveStoredModelOverride,
|
||||
type ModelsProviderData,
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/mattermost";
|
||||
} from "../runtime-api.js";
|
||||
import type { MattermostInteractiveButtonInput } from "./interactions.js";
|
||||
|
||||
const MATTERMOST_MODEL_PICKER_CONTEXT_KEY = "oc_model_picker";
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/mattermost";
|
||||
import type { OpenClawConfig } from "../runtime-api.js";
|
||||
import {
|
||||
evaluateSenderGroupAccessForPolicy,
|
||||
isDangerousNameMatchingEnabled,
|
||||
resolveAllowlistMatchSimple,
|
||||
resolveControlCommandGate,
|
||||
resolveEffectiveAllowFromLists,
|
||||
} from "openclaw/plugin-sdk/mattermost";
|
||||
} from "../runtime-api.js";
|
||||
import type { ResolvedMattermostAccount } from "./accounts.js";
|
||||
import type { MattermostChannel } from "./client.js";
|
||||
|
||||
|
||||
@ -2,8 +2,8 @@ import {
|
||||
formatInboundFromLabel as formatInboundFromLabelShared,
|
||||
resolveThreadSessionKeys as resolveThreadSessionKeysShared,
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/mattermost";
|
||||
export { createDedupeCache, rawDataToString } from "openclaw/plugin-sdk/mattermost";
|
||||
} from "../runtime-api.js";
|
||||
export { createDedupeCache, rawDataToString } from "../runtime-api.js";
|
||||
|
||||
export type ResponsePrefixContext = {
|
||||
model?: string;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { ChannelAccountSnapshot, RuntimeEnv } from "openclaw/plugin-sdk/mattermost";
|
||||
import WebSocket from "ws";
|
||||
import type { ChannelAccountSnapshot, RuntimeEnv } from "../runtime-api.js";
|
||||
import type { MattermostPost } from "./client.js";
|
||||
import { rawDataToString } from "./monitor-helpers.js";
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ import type {
|
||||
OpenClawConfig,
|
||||
ReplyPayload,
|
||||
RuntimeEnv,
|
||||
} from "openclaw/plugin-sdk/mattermost";
|
||||
} from "../runtime-api.js";
|
||||
import {
|
||||
buildAgentMediaPayload,
|
||||
buildModelsProviderData,
|
||||
@ -30,7 +30,7 @@ import {
|
||||
warnMissingProviderGroupPolicyFallbackOnce,
|
||||
listSkillCommandsForAgents,
|
||||
type HistoryEntry,
|
||||
} from "openclaw/plugin-sdk/mattermost";
|
||||
} from "../runtime-api.js";
|
||||
import { getMattermostRuntime } from "../runtime.js";
|
||||
import { resolveMattermostAccount, resolveMattermostReplyToMode } from "./accounts.js";
|
||||
import {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { BaseProbeResult } from "openclaw/plugin-sdk/mattermost";
|
||||
import type { BaseProbeResult } from "../runtime-api.js";
|
||||
import { normalizeMattermostBaseUrl, readMattermostError, type MattermostUser } from "./client.js";
|
||||
|
||||
export type MattermostProbe = BaseProbeResult & {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/mattermost";
|
||||
import type { OpenClawConfig } from "../runtime-api.js";
|
||||
import { resolveMattermostAccount } from "./accounts.js";
|
||||
import { createMattermostClient, fetchMattermostMe, type MattermostClient } from "./client.js";
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { OpenClawConfig, PluginRuntime, ReplyPayload } from "openclaw/plugin-sdk/mattermost";
|
||||
import { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/mattermost";
|
||||
import type { OpenClawConfig, PluginRuntime, ReplyPayload } from "../runtime-api.js";
|
||||
import { getAgentScopedMediaLocalRoots } from "../runtime-api.js";
|
||||
|
||||
type MarkdownTableMode = Parameters<PluginRuntime["channel"]["text"]["convertMarkdownTables"]>[1];
|
||||
|
||||
|
||||
1
extensions/mattermost/src/mattermost/runtime-api.ts
Normal file
1
extensions/mattermost/src/mattermost/runtime-api.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "../../runtime-api.js";
|
||||
@ -1,4 +1,4 @@
|
||||
import { loadOutboundMediaFromUrl, type OpenClawConfig } from "openclaw/plugin-sdk/mattermost";
|
||||
import { loadOutboundMediaFromUrl, type OpenClawConfig } from "../runtime-api.js";
|
||||
import { getMattermostRuntime } from "../runtime.js";
|
||||
import { resolveMattermostAccount } from "./accounts.js";
|
||||
import {
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import type { ResolvedMattermostAccount } from "../mattermost/accounts.js";
|
||||
import {
|
||||
buildModelsProviderData,
|
||||
createReplyPrefixOptions,
|
||||
@ -16,8 +17,7 @@ import {
|
||||
type OpenClawConfig,
|
||||
type ReplyPayload,
|
||||
type RuntimeEnv,
|
||||
} from "openclaw/plugin-sdk/mattermost";
|
||||
import type { ResolvedMattermostAccount } from "../mattermost/accounts.js";
|
||||
} from "../runtime-api.js";
|
||||
import { getMattermostRuntime } from "../runtime.js";
|
||||
import {
|
||||
createMattermostClient,
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
*/
|
||||
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/mattermost";
|
||||
import type { OpenClawPluginApi } from "../runtime-api.js";
|
||||
import type { ResolvedMattermostAccount } from "./accounts.js";
|
||||
import { resolveSlashCommandConfig, type MattermostRegisteredCommand } from "./slash-commands.js";
|
||||
import { createSlashCommandHttpHandler } from "./slash-http.js";
|
||||
@ -86,8 +86,8 @@ export function activateSlashCommands(params: {
|
||||
registeredCommands: MattermostRegisteredCommand[];
|
||||
triggerMap?: Map<string, string>;
|
||||
api: {
|
||||
cfg: import("openclaw/plugin-sdk/mattermost").OpenClawConfig;
|
||||
runtime: import("openclaw/plugin-sdk/mattermost").RuntimeEnv;
|
||||
cfg: import("../runtime-api.js").OpenClawConfig;
|
||||
runtime: import("../runtime-api.js").RuntimeEnv;
|
||||
};
|
||||
log?: (msg: string) => void;
|
||||
}) {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/mattermost";
|
||||
import type { OpenClawConfig } from "../runtime-api.js";
|
||||
import { resolveMattermostAccount } from "./accounts.js";
|
||||
import {
|
||||
createMattermostClient,
|
||||
|
||||
1
extensions/mattermost/src/runtime-api.ts
Normal file
1
extensions/mattermost/src/runtime-api.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "../runtime-api.js";
|
||||
@ -1,5 +1,5 @@
|
||||
import type { PluginRuntime } from "openclaw/plugin-sdk/mattermost";
|
||||
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
||||
import type { PluginRuntime } from "./runtime-api.js";
|
||||
|
||||
const { setRuntime: setMattermostRuntime, getRuntime: getMattermostRuntime } =
|
||||
createPluginRuntimeStore<PluginRuntime>("Mattermost runtime not initialized");
|
||||
|
||||
@ -3,7 +3,7 @@ import {
|
||||
hasConfiguredSecretInput,
|
||||
normalizeResolvedSecretInputString,
|
||||
normalizeSecretInputString,
|
||||
} from "openclaw/plugin-sdk/mattermost";
|
||||
} from "./runtime-api.js";
|
||||
|
||||
export {
|
||||
buildSecretInputSchema,
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import type { ChannelSetupAdapter } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { resolveMattermostAccount, type ResolvedMattermostAccount } from "./mattermost/accounts.js";
|
||||
import { normalizeMattermostBaseUrl } from "./mattermost/client.js";
|
||||
import {
|
||||
applyAccountNameToChannelSection,
|
||||
applySetupAccountConfigPatch,
|
||||
@ -7,9 +9,7 @@ import {
|
||||
migrateBaseNameToDefaultAccount,
|
||||
normalizeAccountId,
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/mattermost";
|
||||
import { resolveMattermostAccount, type ResolvedMattermostAccount } from "./mattermost/accounts.js";
|
||||
import { normalizeMattermostBaseUrl } from "./mattermost/client.js";
|
||||
} from "./runtime-api.js";
|
||||
|
||||
const channel = "mattermost" as const;
|
||||
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { type ChannelSetupWizard } from "openclaw/plugin-sdk/setup";
|
||||
import { formatDocsLink } from "openclaw/plugin-sdk/setup";
|
||||
import { listMattermostAccountIds } from "./mattermost/accounts.js";
|
||||
import { normalizeMattermostBaseUrl } from "./mattermost/client.js";
|
||||
import {
|
||||
applySetupAccountConfigPatch,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
hasConfiguredSecretInput,
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/mattermost";
|
||||
import { type ChannelSetupWizard } from "openclaw/plugin-sdk/setup";
|
||||
import { formatDocsLink } from "openclaw/plugin-sdk/setup";
|
||||
import { listMattermostAccountIds } from "./mattermost/accounts.js";
|
||||
import { normalizeMattermostBaseUrl } from "./mattermost/client.js";
|
||||
} from "./runtime-api.js";
|
||||
import {
|
||||
isMattermostConfigured,
|
||||
mattermostSetupAdapter,
|
||||
|
||||
@ -3,7 +3,7 @@ import type {
|
||||
DmPolicy,
|
||||
GroupPolicy,
|
||||
SecretInput,
|
||||
} from "openclaw/plugin-sdk/mattermost";
|
||||
} from "./runtime-api.js";
|
||||
|
||||
export type MattermostReplyToMode = "off" | "first" | "all";
|
||||
export type MattermostChatTypeKey = "direct" | "channel" | "group";
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
|
||||
import { collectAllowlistProviderRestrictSendersWarnings } from "openclaw/plugin-sdk/channel-policy";
|
||||
import { createMessageToolCardSchema } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import type { ChannelMessageActionAdapter } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import type {
|
||||
ChannelMessageActionAdapter,
|
||||
ChannelMessageToolDiscovery,
|
||||
} from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { createLazyRuntimeNamedExport } from "openclaw/plugin-sdk/lazy-runtime";
|
||||
import type {
|
||||
ChannelMessageActionName,
|
||||
@ -64,6 +67,27 @@ const loadMSTeamsChannelRuntime = createLazyRuntimeNamedExport(
|
||||
"msTeamsChannelRuntime",
|
||||
);
|
||||
|
||||
function describeMSTeamsMessageTool({
|
||||
cfg,
|
||||
}: Parameters<
|
||||
NonNullable<ChannelMessageActionAdapter["describeMessageTool"]>
|
||||
>[0]): ChannelMessageToolDiscovery {
|
||||
const enabled =
|
||||
cfg.channels?.msteams?.enabled !== false &&
|
||||
Boolean(resolveMSTeamsCredentials(cfg.channels?.msteams));
|
||||
return {
|
||||
actions: enabled ? (["poll"] satisfies ChannelMessageActionName[]) : [],
|
||||
capabilities: enabled ? ["cards"] : [],
|
||||
schema: enabled
|
||||
? {
|
||||
properties: {
|
||||
card: createMessageToolCardSchema(),
|
||||
},
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
|
||||
id: "msteams",
|
||||
meta: {
|
||||
@ -370,24 +394,7 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
describeMessageTool: ({
|
||||
cfg,
|
||||
}: Parameters<NonNullable<ChannelMessageActionAdapter["describeMessageTool"]>>[0]) => {
|
||||
const enabled =
|
||||
cfg.channels?.msteams?.enabled !== false &&
|
||||
Boolean(resolveMSTeamsCredentials(cfg.channels?.msteams));
|
||||
return {
|
||||
actions: enabled ? (["poll"] satisfies ChannelMessageActionName[]) : [],
|
||||
capabilities: enabled ? ["cards"] : [],
|
||||
schema: enabled
|
||||
? {
|
||||
properties: {
|
||||
card: createMessageToolCardSchema(),
|
||||
},
|
||||
}
|
||||
: null,
|
||||
};
|
||||
},
|
||||
describeMessageTool: describeMSTeamsMessageTool,
|
||||
handleAction: async (ctx) => {
|
||||
// Handle send action with card parameter
|
||||
if (ctx.action === "send" && ctx.params.card) {
|
||||
|
||||
@ -1,7 +1,3 @@
|
||||
import {
|
||||
buildPassiveChannelStatusSummary,
|
||||
buildTrafficStatusSummary,
|
||||
} from "../../shared/channel-status-summary.js";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
collectStatusIssuesFromLastError,
|
||||
@ -10,7 +6,11 @@ import {
|
||||
formatPairingApproveHint,
|
||||
mapAllowFromEntries,
|
||||
type ChannelPlugin,
|
||||
} from "../api.js";
|
||||
} from "openclaw/plugin-sdk/nostr";
|
||||
import {
|
||||
buildPassiveChannelStatusSummary,
|
||||
buildTrafficStatusSummary,
|
||||
} from "../../shared/channel-status-summary.js";
|
||||
import type { NostrProfile } from "./config-schema.js";
|
||||
import { NostrConfigSchema } from "./config-schema.js";
|
||||
import type { MetricEvent, MetricsSnapshot } from "./metrics.js";
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { AllowFromListSchema, DmPolicySchema } from "openclaw/plugin-sdk/channel-config-schema";
|
||||
import { MarkdownConfigSchema, buildChannelConfigSchema } from "openclaw/plugin-sdk/nostr";
|
||||
import { z } from "zod";
|
||||
import { MarkdownConfigSchema, buildChannelConfigSchema } from "../api.js";
|
||||
|
||||
/**
|
||||
* Validates https:// URLs only (no javascript:, data:, file:, etc.)
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
export * from "./src/action-runtime.js";
|
||||
export * from "./src/directory-live.js";
|
||||
export * from "./src/index.js";
|
||||
export * from "./src/resolve-channels.js";
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { handleSlackAction } from "./slack-actions.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import { handleSlackAction, slackActionRuntime } from "./action-runtime.js";
|
||||
import { parseSlackBlocksInput } from "./blocks-input.js";
|
||||
|
||||
const originalSlackActionRuntime = { ...slackActionRuntime };
|
||||
const deleteSlackMessage = vi.fn(async (..._args: unknown[]) => ({}));
|
||||
const downloadSlackFile = vi.fn(async (..._args: unknown[]) => null);
|
||||
const editSlackMessage = vi.fn(async (..._args: unknown[]) => ({}));
|
||||
@ -14,31 +16,10 @@ const reactSlackMessage = vi.fn(async (..._args: unknown[]) => ({}));
|
||||
const readSlackMessages = vi.fn(async (..._args: unknown[]) => ({}));
|
||||
const removeOwnSlackReactions = vi.fn(async (..._args: unknown[]) => ["thumbsup"]);
|
||||
const removeSlackReaction = vi.fn(async (..._args: unknown[]) => ({}));
|
||||
const sendSlackMessage = vi.fn(async (..._args: unknown[]) => ({}));
|
||||
const recordSlackThreadParticipation = vi.fn();
|
||||
const sendSlackMessage = vi.fn(async (..._args: unknown[]) => ({ channelId: "C123" }));
|
||||
const unpinSlackMessage = vi.fn(async (..._args: unknown[]) => ({}));
|
||||
|
||||
vi.mock("../../../extensions/slack/src/actions.js", () => ({
|
||||
deleteSlackMessage: (...args: Parameters<typeof deleteSlackMessage>) =>
|
||||
deleteSlackMessage(...args),
|
||||
downloadSlackFile: (...args: Parameters<typeof downloadSlackFile>) => downloadSlackFile(...args),
|
||||
editSlackMessage: (...args: Parameters<typeof editSlackMessage>) => editSlackMessage(...args),
|
||||
getSlackMemberInfo: (...args: Parameters<typeof getSlackMemberInfo>) =>
|
||||
getSlackMemberInfo(...args),
|
||||
listSlackEmojis: (...args: Parameters<typeof listSlackEmojis>) => listSlackEmojis(...args),
|
||||
listSlackPins: (...args: Parameters<typeof listSlackPins>) => listSlackPins(...args),
|
||||
listSlackReactions: (...args: Parameters<typeof listSlackReactions>) =>
|
||||
listSlackReactions(...args),
|
||||
pinSlackMessage: (...args: Parameters<typeof pinSlackMessage>) => pinSlackMessage(...args),
|
||||
reactSlackMessage: (...args: Parameters<typeof reactSlackMessage>) => reactSlackMessage(...args),
|
||||
readSlackMessages: (...args: Parameters<typeof readSlackMessages>) => readSlackMessages(...args),
|
||||
removeOwnSlackReactions: (...args: Parameters<typeof removeOwnSlackReactions>) =>
|
||||
removeOwnSlackReactions(...args),
|
||||
removeSlackReaction: (...args: Parameters<typeof removeSlackReaction>) =>
|
||||
removeSlackReaction(...args),
|
||||
sendSlackMessage: (...args: Parameters<typeof sendSlackMessage>) => sendSlackMessage(...args),
|
||||
unpinSlackMessage: (...args: Parameters<typeof unpinSlackMessage>) => unpinSlackMessage(...args),
|
||||
}));
|
||||
|
||||
describe("handleSlackAction", () => {
|
||||
function slackConfig(overrides?: Record<string, unknown>): OpenClawConfig {
|
||||
return {
|
||||
@ -105,6 +86,24 @@ describe("handleSlackAction", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
Object.assign(slackActionRuntime, originalSlackActionRuntime, {
|
||||
deleteSlackMessage,
|
||||
downloadSlackFile,
|
||||
editSlackMessage,
|
||||
getSlackMemberInfo,
|
||||
listSlackEmojis,
|
||||
listSlackPins,
|
||||
listSlackReactions,
|
||||
parseSlackBlocksInput,
|
||||
pinSlackMessage,
|
||||
reactSlackMessage,
|
||||
readSlackMessages,
|
||||
recordSlackThreadParticipation,
|
||||
removeOwnSlackReactions,
|
||||
removeSlackReaction,
|
||||
sendSlackMessage,
|
||||
unpinSlackMessage,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
@ -261,6 +260,7 @@ describe("handleSlackAction", () => {
|
||||
{
|
||||
action: "sendMessage",
|
||||
to: "channel:C123",
|
||||
content: "",
|
||||
blocks,
|
||||
},
|
||||
slackConfig(),
|
||||
@ -275,7 +275,7 @@ describe("handleSlackAction", () => {
|
||||
it.each([
|
||||
{
|
||||
name: "invalid blocks JSON",
|
||||
blocks: "{bad-json",
|
||||
blocks: "{not json",
|
||||
expectedError: /blocks must be valid JSON/i,
|
||||
},
|
||||
{ name: "empty blocks arrays", blocks: "[]", expectedError: /at least one block/i },
|
||||
@ -285,6 +285,7 @@ describe("handleSlackAction", () => {
|
||||
{
|
||||
action: "sendMessage",
|
||||
to: "channel:C123",
|
||||
content: "",
|
||||
blocks,
|
||||
},
|
||||
slackConfig(),
|
||||
@ -311,8 +312,9 @@ describe("handleSlackAction", () => {
|
||||
{
|
||||
action: "sendMessage",
|
||||
to: "channel:C123",
|
||||
blocks: [{ type: "divider" }],
|
||||
mediaUrl: "https://example.com/image.png",
|
||||
content: "hello",
|
||||
mediaUrl: "https://example.com/file.png",
|
||||
blocks: JSON.stringify([{ type: "divider" }]),
|
||||
},
|
||||
slackConfig(),
|
||||
),
|
||||
@ -322,13 +324,13 @@ describe("handleSlackAction", () => {
|
||||
it.each([
|
||||
{
|
||||
name: "JSON blocks",
|
||||
blocks: JSON.stringify([{ type: "section", text: { type: "mrkdwn", text: "Updated" } }]),
|
||||
expectedBlocks: [{ type: "section", text: { type: "mrkdwn", text: "Updated" } }],
|
||||
blocks: JSON.stringify([{ type: "divider" }]),
|
||||
expectedBlocks: [{ type: "divider" }],
|
||||
},
|
||||
{
|
||||
name: "array blocks",
|
||||
blocks: [{ type: "divider" }],
|
||||
expectedBlocks: [{ type: "divider" }],
|
||||
blocks: [{ type: "section", text: { type: "mrkdwn", text: "updated" } }],
|
||||
expectedBlocks: [{ type: "section", text: { type: "mrkdwn", text: "updated" } }],
|
||||
},
|
||||
])("passes $name to editSlackMessage", async ({ blocks, expectedBlocks }) => {
|
||||
await handleSlackAction(
|
||||
@ -336,6 +338,7 @@ describe("handleSlackAction", () => {
|
||||
action: "editMessage",
|
||||
channelId: "C123",
|
||||
messageId: "123.456",
|
||||
content: "",
|
||||
blocks,
|
||||
},
|
||||
slackConfig(),
|
||||
@ -360,40 +363,32 @@ describe("handleSlackAction", () => {
|
||||
});
|
||||
|
||||
it("auto-injects threadTs from context when replyToMode=all", async () => {
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig;
|
||||
sendSlackMessage.mockClear();
|
||||
await handleSlackAction(
|
||||
{
|
||||
action: "sendMessage",
|
||||
to: "channel:C123",
|
||||
content: "Auto-threaded",
|
||||
content: "Threaded reply",
|
||||
},
|
||||
cfg,
|
||||
slackConfig(),
|
||||
{
|
||||
currentChannelId: "C123",
|
||||
currentThreadTs: "1111111111.111111",
|
||||
replyToMode: "all",
|
||||
},
|
||||
);
|
||||
expect(sendSlackMessage).toHaveBeenCalledWith("channel:C123", "Auto-threaded", {
|
||||
mediaUrl: undefined,
|
||||
threadTs: "1111111111.111111",
|
||||
blocks: undefined,
|
||||
});
|
||||
expectLastSlackSend("Threaded reply", "1111111111.111111");
|
||||
});
|
||||
|
||||
it("replyToMode=first threads first message then stops", async () => {
|
||||
const { cfg, context, hasRepliedRef } = createReplyToFirstScenario();
|
||||
const { cfg, context } = createReplyToFirstScenario();
|
||||
|
||||
// First message should be threaded
|
||||
await handleSlackAction(
|
||||
{ action: "sendMessage", to: "channel:C123", content: "First" },
|
||||
cfg,
|
||||
context,
|
||||
);
|
||||
expectLastSlackSend("First", "1111111111.111111");
|
||||
expect(hasRepliedRef.value).toBe(true);
|
||||
|
||||
expectLastSlackSend("First", "1111111111.111111");
|
||||
await sendSecondMessageAndExpectNoThread({ cfg, context });
|
||||
});
|
||||
|
||||
@ -405,73 +400,54 @@ describe("handleSlackAction", () => {
|
||||
action: "sendMessage",
|
||||
to: "channel:C123",
|
||||
content: "Explicit",
|
||||
threadTs: "2222222222.222222",
|
||||
threadTs: "9999999999.999999",
|
||||
},
|
||||
cfg,
|
||||
context,
|
||||
);
|
||||
expectLastSlackSend("Explicit", "2222222222.222222");
|
||||
expect(hasRepliedRef.value).toBe(true);
|
||||
|
||||
expectLastSlackSend("Explicit", "9999999999.999999");
|
||||
expect(hasRepliedRef.value).toBe(true);
|
||||
await sendSecondMessageAndExpectNoThread({ cfg, context });
|
||||
});
|
||||
|
||||
it("replyToMode=first without hasRepliedRef does not thread", async () => {
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig;
|
||||
sendSlackMessage.mockClear();
|
||||
await handleSlackAction({ action: "sendMessage", to: "channel:C123", content: "No ref" }, cfg, {
|
||||
currentChannelId: "C123",
|
||||
currentThreadTs: "1111111111.111111",
|
||||
replyToMode: "first",
|
||||
// no hasRepliedRef
|
||||
});
|
||||
expect(sendSlackMessage).toHaveBeenCalledWith("channel:C123", "No ref", {
|
||||
mediaUrl: undefined,
|
||||
threadTs: undefined,
|
||||
blocks: undefined,
|
||||
});
|
||||
await handleSlackAction(
|
||||
{ action: "sendMessage", to: "channel:C123", content: "No ref" },
|
||||
slackConfig(),
|
||||
{
|
||||
currentChannelId: "C123",
|
||||
currentThreadTs: "1111111111.111111",
|
||||
replyToMode: "first",
|
||||
},
|
||||
);
|
||||
expectLastSlackSend("No ref");
|
||||
});
|
||||
|
||||
it("does not auto-inject threadTs when replyToMode=off", async () => {
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig;
|
||||
sendSlackMessage.mockClear();
|
||||
await handleSlackAction(
|
||||
{
|
||||
action: "sendMessage",
|
||||
to: "channel:C123",
|
||||
content: "Off mode",
|
||||
},
|
||||
cfg,
|
||||
{ action: "sendMessage", to: "channel:C123", content: "No thread" },
|
||||
slackConfig(),
|
||||
{
|
||||
currentChannelId: "C123",
|
||||
currentThreadTs: "1111111111.111111",
|
||||
replyToMode: "off",
|
||||
},
|
||||
);
|
||||
expect(sendSlackMessage).toHaveBeenCalledWith("channel:C123", "Off mode", {
|
||||
mediaUrl: undefined,
|
||||
threadTs: undefined,
|
||||
blocks: undefined,
|
||||
});
|
||||
expectLastSlackSend("No thread");
|
||||
});
|
||||
|
||||
it("does not auto-inject threadTs when sending to different channel", async () => {
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig;
|
||||
sendSlackMessage.mockClear();
|
||||
await handleSlackAction(
|
||||
{
|
||||
action: "sendMessage",
|
||||
to: "channel:C999",
|
||||
content: "Different channel",
|
||||
},
|
||||
cfg,
|
||||
{ action: "sendMessage", to: "channel:C999", content: "Other channel" },
|
||||
slackConfig(),
|
||||
{
|
||||
currentChannelId: "C123",
|
||||
currentThreadTs: "1111111111.111111",
|
||||
replyToMode: "all",
|
||||
},
|
||||
);
|
||||
expect(sendSlackMessage).toHaveBeenCalledWith("channel:C999", "Different channel", {
|
||||
expect(sendSlackMessage).toHaveBeenCalledWith("channel:C999", "Other channel", {
|
||||
mediaUrl: undefined,
|
||||
threadTs: undefined,
|
||||
blocks: undefined,
|
||||
@ -479,46 +455,34 @@ describe("handleSlackAction", () => {
|
||||
});
|
||||
|
||||
it("explicit threadTs overrides context threadTs", async () => {
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig;
|
||||
sendSlackMessage.mockClear();
|
||||
await handleSlackAction(
|
||||
{
|
||||
action: "sendMessage",
|
||||
to: "channel:C123",
|
||||
content: "Explicit thread",
|
||||
threadTs: "2222222222.222222",
|
||||
content: "Explicit wins",
|
||||
threadTs: "9999999999.999999",
|
||||
},
|
||||
cfg,
|
||||
slackConfig(),
|
||||
{
|
||||
currentChannelId: "C123",
|
||||
currentThreadTs: "1111111111.111111",
|
||||
replyToMode: "all",
|
||||
},
|
||||
);
|
||||
expect(sendSlackMessage).toHaveBeenCalledWith("channel:C123", "Explicit thread", {
|
||||
mediaUrl: undefined,
|
||||
threadTs: "2222222222.222222",
|
||||
blocks: undefined,
|
||||
});
|
||||
expectLastSlackSend("Explicit wins", "9999999999.999999");
|
||||
});
|
||||
|
||||
it("handles channel target without prefix when replyToMode=all", async () => {
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig;
|
||||
sendSlackMessage.mockClear();
|
||||
await handleSlackAction(
|
||||
{
|
||||
action: "sendMessage",
|
||||
to: "C123",
|
||||
content: "No prefix",
|
||||
},
|
||||
cfg,
|
||||
{ action: "sendMessage", to: "C123", content: "Bare target" },
|
||||
slackConfig(),
|
||||
{
|
||||
currentChannelId: "C123",
|
||||
currentThreadTs: "1111111111.111111",
|
||||
replyToMode: "all",
|
||||
},
|
||||
);
|
||||
expect(sendSlackMessage).toHaveBeenCalledWith("C123", "No prefix", {
|
||||
expect(sendSlackMessage).toHaveBeenCalledWith("C123", "Bare target", {
|
||||
mediaUrl: undefined,
|
||||
threadTs: "1111111111.111111",
|
||||
blocks: undefined,
|
||||
@ -526,104 +490,164 @@ describe("handleSlackAction", () => {
|
||||
});
|
||||
|
||||
it("adds normalized timestamps to readMessages payloads", async () => {
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig;
|
||||
readSlackMessages.mockResolvedValueOnce({
|
||||
messages: [{ ts: "1735689600.456", text: "hi" }],
|
||||
messages: [{ ts: "1712345678.123456", text: "hi" }],
|
||||
hasMore: false,
|
||||
});
|
||||
|
||||
const result = await handleSlackAction({ action: "readMessages", channelId: "C1" }, cfg);
|
||||
const payload = result.details as {
|
||||
messages: Array<{ timestampMs?: number; timestampUtc?: string }>;
|
||||
};
|
||||
const result = await handleSlackAction(
|
||||
{ action: "readMessages", channelId: "C1" },
|
||||
slackConfig(),
|
||||
);
|
||||
|
||||
const expectedMs = Math.round(1735689600.456 * 1000);
|
||||
expect(payload.messages[0].timestampMs).toBe(expectedMs);
|
||||
expect(payload.messages[0].timestampUtc).toBe(new Date(expectedMs).toISOString());
|
||||
expect(result).toMatchObject({
|
||||
details: {
|
||||
ok: true,
|
||||
hasMore: false,
|
||||
messages: [
|
||||
expect.objectContaining({
|
||||
ts: "1712345678.123456",
|
||||
timestampMs: 1712345678123,
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("passes threadId through to readSlackMessages", async () => {
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig;
|
||||
readSlackMessages.mockClear();
|
||||
readSlackMessages.mockResolvedValueOnce({ messages: [], hasMore: false });
|
||||
|
||||
await handleSlackAction(
|
||||
{ action: "readMessages", channelId: "C1", threadId: "12345.6789" },
|
||||
cfg,
|
||||
{ action: "readMessages", channelId: "C1", threadId: "1712345678.123456" },
|
||||
slackConfig(),
|
||||
);
|
||||
|
||||
const opts = readSlackMessages.mock.calls[0]?.[1] as { threadId?: string } | undefined;
|
||||
expect(opts?.threadId).toBe("12345.6789");
|
||||
expect(readSlackMessages).toHaveBeenCalledWith("C1", {
|
||||
threadId: "1712345678.123456",
|
||||
limit: undefined,
|
||||
before: undefined,
|
||||
after: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("adds normalized timestamps to pin payloads", async () => {
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig;
|
||||
listSlackPins.mockResolvedValueOnce([
|
||||
{
|
||||
type: "message",
|
||||
message: { ts: "1735689600.789", text: "pinned" },
|
||||
listSlackPins.mockResolvedValueOnce([{ message: { ts: "1712345678.123456", text: "pin" } }]);
|
||||
|
||||
const result = await handleSlackAction({ action: "listPins", channelId: "C1" }, slackConfig());
|
||||
|
||||
expect(result).toMatchObject({
|
||||
details: {
|
||||
ok: true,
|
||||
pins: [
|
||||
{
|
||||
message: expect.objectContaining({
|
||||
ts: "1712345678.123456",
|
||||
timestampMs: 1712345678123,
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await handleSlackAction({ action: "listPins", channelId: "C1" }, cfg);
|
||||
const payload = result.details as {
|
||||
pins: Array<{ message?: { timestampMs?: number; timestampUtc?: string } }>;
|
||||
};
|
||||
|
||||
const expectedMs = Math.round(1735689600.789 * 1000);
|
||||
expect(payload.pins[0].message?.timestampMs).toBe(expectedMs);
|
||||
expect(payload.pins[0].message?.timestampUtc).toBe(new Date(expectedMs).toISOString());
|
||||
});
|
||||
});
|
||||
|
||||
it("uses user token for reads when available", async () => {
|
||||
const cfg = {
|
||||
channels: { slack: { botToken: "xoxb-1", userToken: "xoxp-1" } },
|
||||
} as OpenClawConfig;
|
||||
expect(await resolveReadToken(cfg)).toBe("xoxp-1");
|
||||
const token = await resolveReadToken(
|
||||
slackConfig({
|
||||
accounts: {
|
||||
default: {
|
||||
botToken: "xoxb-bot",
|
||||
userToken: "xoxp-user",
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(token).toBe("xoxp-user");
|
||||
});
|
||||
|
||||
it("falls back to bot token for reads when user token missing", async () => {
|
||||
const cfg = {
|
||||
channels: { slack: { botToken: "xoxb-1" } },
|
||||
} as OpenClawConfig;
|
||||
expect(await resolveReadToken(cfg)).toBeUndefined();
|
||||
const token = await resolveReadToken(
|
||||
slackConfig({
|
||||
accounts: {
|
||||
default: {
|
||||
botToken: "xoxb-bot",
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(token).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses bot token for writes when userTokenReadOnly is true", async () => {
|
||||
const cfg = {
|
||||
channels: { slack: { botToken: "xoxb-1", userToken: "xoxp-1" } },
|
||||
} as OpenClawConfig;
|
||||
expect(await resolveSendToken(cfg)).toBeUndefined();
|
||||
const token = await resolveSendToken(
|
||||
slackConfig({
|
||||
accounts: {
|
||||
default: {
|
||||
botToken: "xoxb-bot",
|
||||
userToken: "xoxp-user",
|
||||
userTokenReadOnly: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(token).toBeUndefined();
|
||||
});
|
||||
|
||||
it("allows user token writes when bot token is missing", async () => {
|
||||
const cfg = {
|
||||
const token = await resolveSendToken({
|
||||
channels: {
|
||||
slack: { userToken: "xoxp-1", userTokenReadOnly: false },
|
||||
slack: {
|
||||
accounts: {
|
||||
default: {
|
||||
userToken: "xoxp-user",
|
||||
userTokenReadOnly: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
expect(await resolveSendToken(cfg)).toBe("xoxp-1");
|
||||
} as OpenClawConfig);
|
||||
expect(token).toBe("xoxp-user");
|
||||
});
|
||||
|
||||
it("returns all emojis when no limit is provided", async () => {
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig;
|
||||
const emojiMap = { wave: "url1", smile: "url2", heart: "url3" };
|
||||
listSlackEmojis.mockResolvedValueOnce({ ok: true, emoji: emojiMap });
|
||||
const result = await handleSlackAction({ action: "emojiList" }, cfg);
|
||||
const payload = result.details as { ok: boolean; emojis: { emoji: Record<string, string> } };
|
||||
expect(payload.ok).toBe(true);
|
||||
expect(Object.keys(payload.emojis.emoji)).toHaveLength(3);
|
||||
listSlackEmojis.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
emoji: { party: "https://example.com/party.png", wave: "https://example.com/wave.png" },
|
||||
});
|
||||
|
||||
const result = await handleSlackAction({ action: "emojiList" }, slackConfig());
|
||||
|
||||
expect(result).toMatchObject({
|
||||
details: {
|
||||
ok: true,
|
||||
emojis: {
|
||||
emoji: { party: "https://example.com/party.png", wave: "https://example.com/wave.png" },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("applies limit to emoji-list results", async () => {
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig;
|
||||
const emojiMap = { wave: "url1", smile: "url2", heart: "url3", fire: "url4", star: "url5" };
|
||||
listSlackEmojis.mockResolvedValueOnce({ ok: true, emoji: emojiMap });
|
||||
const result = await handleSlackAction({ action: "emojiList", limit: 2 }, cfg);
|
||||
const payload = result.details as { ok: boolean; emojis: { emoji: Record<string, string> } };
|
||||
expect(payload.ok).toBe(true);
|
||||
const emojiKeys = Object.keys(payload.emojis.emoji);
|
||||
expect(emojiKeys).toHaveLength(2);
|
||||
expect(emojiKeys.every((k) => k in emojiMap)).toBe(true);
|
||||
listSlackEmojis.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
emoji: {
|
||||
wave: "https://example.com/wave.png",
|
||||
party: "https://example.com/party.png",
|
||||
tada: "https://example.com/tada.png",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await handleSlackAction({ action: "emojiList", limit: 2 }, slackConfig());
|
||||
|
||||
expect(result).toMatchObject({
|
||||
details: {
|
||||
ok: true,
|
||||
emojis: {
|
||||
emoji: {
|
||||
party: "https://example.com/party.png",
|
||||
tada: "https://example.com/tada.png",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,6 +1,15 @@
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { resolveSlackAccount } from "../../plugin-sdk/account-resolution.js";
|
||||
import { withNormalizedTimestamp } from "../../../src/agents/date-time.js";
|
||||
import {
|
||||
createActionGate,
|
||||
imageResultFromFile,
|
||||
jsonResult,
|
||||
readNumberParam,
|
||||
readReactionParams,
|
||||
readStringParam,
|
||||
} from "../../../src/agents/tools/common.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import { resolveSlackAccount } from "./accounts.js";
|
||||
import {
|
||||
deleteSlackMessage,
|
||||
downloadSlackFile,
|
||||
@ -16,22 +25,10 @@ import {
|
||||
removeSlackReaction,
|
||||
sendSlackMessage,
|
||||
unpinSlackMessage,
|
||||
} from "../../plugin-sdk/slack.js";
|
||||
import {
|
||||
parseSlackBlocksInput,
|
||||
parseSlackTarget,
|
||||
recordSlackThreadParticipation,
|
||||
resolveSlackChannelId,
|
||||
} from "../../plugin-sdk/slack.js";
|
||||
import { withNormalizedTimestamp } from "../date-time.js";
|
||||
import {
|
||||
createActionGate,
|
||||
imageResultFromFile,
|
||||
jsonResult,
|
||||
readNumberParam,
|
||||
readReactionParams,
|
||||
readStringParam,
|
||||
} from "./common.js";
|
||||
} from "./actions.js";
|
||||
import { parseSlackBlocksInput } from "./blocks-input.js";
|
||||
import { recordSlackThreadParticipation } from "./sent-thread-cache.js";
|
||||
import { parseSlackTarget, resolveSlackChannelId } from "./targets.js";
|
||||
|
||||
const messagingActions = new Set([
|
||||
"sendMessage",
|
||||
@ -44,6 +41,25 @@ const messagingActions = new Set([
|
||||
const reactionsActions = new Set(["react", "reactions"]);
|
||||
const pinActions = new Set(["pinMessage", "unpinMessage", "listPins"]);
|
||||
|
||||
export const slackActionRuntime = {
|
||||
deleteSlackMessage,
|
||||
downloadSlackFile,
|
||||
editSlackMessage,
|
||||
getSlackMemberInfo,
|
||||
listSlackEmojis,
|
||||
listSlackPins,
|
||||
listSlackReactions,
|
||||
parseSlackBlocksInput,
|
||||
pinSlackMessage,
|
||||
reactSlackMessage,
|
||||
readSlackMessages,
|
||||
recordSlackThreadParticipation,
|
||||
removeOwnSlackReactions,
|
||||
removeSlackReaction,
|
||||
sendSlackMessage,
|
||||
unpinSlackMessage,
|
||||
};
|
||||
|
||||
export type SlackActionContext = {
|
||||
/** Current channel ID for auto-threading. */
|
||||
currentChannelId?: string;
|
||||
@ -102,7 +118,7 @@ function resolveThreadTsFromContext(
|
||||
}
|
||||
|
||||
function readSlackBlocksParam(params: Record<string, unknown>) {
|
||||
return parseSlackBlocksInput(params.blocks);
|
||||
return slackActionRuntime.parseSlackBlocksInput(params.blocks);
|
||||
}
|
||||
|
||||
export async function handleSlackAction(
|
||||
@ -163,28 +179,28 @@ export async function handleSlackAction(
|
||||
});
|
||||
if (remove) {
|
||||
if (writeOpts) {
|
||||
await removeSlackReaction(channelId, messageId, emoji, writeOpts);
|
||||
await slackActionRuntime.removeSlackReaction(channelId, messageId, emoji, writeOpts);
|
||||
} else {
|
||||
await removeSlackReaction(channelId, messageId, emoji);
|
||||
await slackActionRuntime.removeSlackReaction(channelId, messageId, emoji);
|
||||
}
|
||||
return jsonResult({ ok: true, removed: emoji });
|
||||
}
|
||||
if (isEmpty) {
|
||||
const removed = writeOpts
|
||||
? await removeOwnSlackReactions(channelId, messageId, writeOpts)
|
||||
: await removeOwnSlackReactions(channelId, messageId);
|
||||
? await slackActionRuntime.removeOwnSlackReactions(channelId, messageId, writeOpts)
|
||||
: await slackActionRuntime.removeOwnSlackReactions(channelId, messageId);
|
||||
return jsonResult({ ok: true, removed });
|
||||
}
|
||||
if (writeOpts) {
|
||||
await reactSlackMessage(channelId, messageId, emoji, writeOpts);
|
||||
await slackActionRuntime.reactSlackMessage(channelId, messageId, emoji, writeOpts);
|
||||
} else {
|
||||
await reactSlackMessage(channelId, messageId, emoji);
|
||||
await slackActionRuntime.reactSlackMessage(channelId, messageId, emoji);
|
||||
}
|
||||
return jsonResult({ ok: true, added: emoji });
|
||||
}
|
||||
const reactions = readOpts
|
||||
? await listSlackReactions(channelId, messageId, readOpts)
|
||||
: await listSlackReactions(channelId, messageId);
|
||||
? await slackActionRuntime.listSlackReactions(channelId, messageId, readOpts)
|
||||
: await slackActionRuntime.listSlackReactions(channelId, messageId);
|
||||
return jsonResult({ ok: true, reactions });
|
||||
}
|
||||
|
||||
@ -211,7 +227,7 @@ export async function handleSlackAction(
|
||||
to,
|
||||
context,
|
||||
);
|
||||
const result = await sendSlackMessage(to, content ?? "", {
|
||||
const result = await slackActionRuntime.sendSlackMessage(to, content ?? "", {
|
||||
...writeOpts,
|
||||
mediaUrl: mediaUrl ?? undefined,
|
||||
mediaLocalRoots: context?.mediaLocalRoots,
|
||||
@ -220,7 +236,11 @@ export async function handleSlackAction(
|
||||
});
|
||||
|
||||
if (threadTs && result.channelId && account.accountId) {
|
||||
recordSlackThreadParticipation(account.accountId, result.channelId, threadTs);
|
||||
slackActionRuntime.recordSlackThreadParticipation(
|
||||
account.accountId,
|
||||
result.channelId,
|
||||
threadTs,
|
||||
);
|
||||
}
|
||||
|
||||
// Keep "first" mode consistent even when the agent explicitly provided
|
||||
@ -248,12 +268,12 @@ export async function handleSlackAction(
|
||||
throw new Error("Slack editMessage requires content or blocks.");
|
||||
}
|
||||
if (writeOpts) {
|
||||
await editSlackMessage(channelId, messageId, content ?? "", {
|
||||
await slackActionRuntime.editSlackMessage(channelId, messageId, content ?? "", {
|
||||
...writeOpts,
|
||||
blocks,
|
||||
});
|
||||
} else {
|
||||
await editSlackMessage(channelId, messageId, content ?? "", {
|
||||
await slackActionRuntime.editSlackMessage(channelId, messageId, content ?? "", {
|
||||
blocks,
|
||||
});
|
||||
}
|
||||
@ -265,9 +285,9 @@ export async function handleSlackAction(
|
||||
required: true,
|
||||
});
|
||||
if (writeOpts) {
|
||||
await deleteSlackMessage(channelId, messageId, writeOpts);
|
||||
await slackActionRuntime.deleteSlackMessage(channelId, messageId, writeOpts);
|
||||
} else {
|
||||
await deleteSlackMessage(channelId, messageId);
|
||||
await slackActionRuntime.deleteSlackMessage(channelId, messageId);
|
||||
}
|
||||
return jsonResult({ ok: true });
|
||||
}
|
||||
@ -279,7 +299,7 @@ export async function handleSlackAction(
|
||||
const before = readStringParam(params, "before");
|
||||
const after = readStringParam(params, "after");
|
||||
const threadId = readStringParam(params, "threadId");
|
||||
const result = await readSlackMessages(channelId, {
|
||||
const result = await slackActionRuntime.readSlackMessages(channelId, {
|
||||
...readOpts,
|
||||
limit,
|
||||
before: before ?? undefined,
|
||||
@ -302,7 +322,7 @@ export async function handleSlackAction(
|
||||
const maxBytes = account.config?.mediaMaxMb
|
||||
? account.config.mediaMaxMb * 1024 * 1024
|
||||
: 20 * 1024 * 1024;
|
||||
const downloaded = await downloadSlackFile(fileId, {
|
||||
const downloaded = await slackActionRuntime.downloadSlackFile(fileId, {
|
||||
...readOpts,
|
||||
maxBytes,
|
||||
channelId,
|
||||
@ -336,9 +356,9 @@ export async function handleSlackAction(
|
||||
required: true,
|
||||
});
|
||||
if (writeOpts) {
|
||||
await pinSlackMessage(channelId, messageId, writeOpts);
|
||||
await slackActionRuntime.pinSlackMessage(channelId, messageId, writeOpts);
|
||||
} else {
|
||||
await pinSlackMessage(channelId, messageId);
|
||||
await slackActionRuntime.pinSlackMessage(channelId, messageId);
|
||||
}
|
||||
return jsonResult({ ok: true });
|
||||
}
|
||||
@ -347,15 +367,15 @@ export async function handleSlackAction(
|
||||
required: true,
|
||||
});
|
||||
if (writeOpts) {
|
||||
await unpinSlackMessage(channelId, messageId, writeOpts);
|
||||
await slackActionRuntime.unpinSlackMessage(channelId, messageId, writeOpts);
|
||||
} else {
|
||||
await unpinSlackMessage(channelId, messageId);
|
||||
await slackActionRuntime.unpinSlackMessage(channelId, messageId);
|
||||
}
|
||||
return jsonResult({ ok: true });
|
||||
}
|
||||
const pins = writeOpts
|
||||
? await listSlackPins(channelId, readOpts)
|
||||
: await listSlackPins(channelId);
|
||||
? await slackActionRuntime.listSlackPins(channelId, readOpts)
|
||||
: await slackActionRuntime.listSlackPins(channelId);
|
||||
const normalizedPins = pins.map((pin) => {
|
||||
const message = pin.message
|
||||
? withNormalizedTimestamp(
|
||||
@ -374,8 +394,8 @@ export async function handleSlackAction(
|
||||
}
|
||||
const userId = readStringParam(params, "userId", { required: true });
|
||||
const info = writeOpts
|
||||
? await getSlackMemberInfo(userId, readOpts)
|
||||
: await getSlackMemberInfo(userId);
|
||||
? await slackActionRuntime.getSlackMemberInfo(userId, readOpts)
|
||||
: await slackActionRuntime.getSlackMemberInfo(userId);
|
||||
return jsonResult({ ok: true, info });
|
||||
}
|
||||
|
||||
@ -383,7 +403,9 @@ export async function handleSlackAction(
|
||||
if (!isActionEnabled("emojiList")) {
|
||||
throw new Error("Slack emoji list is disabled.");
|
||||
}
|
||||
const result = readOpts ? await listSlackEmojis(readOpts) : await listSlackEmojis();
|
||||
const result = readOpts
|
||||
? await slackActionRuntime.listSlackEmojis(readOpts)
|
||||
: await slackActionRuntime.listSlackEmojis();
|
||||
const limit = readNumberParam(params, "limit", { integer: true });
|
||||
if (limit != null && limit > 0 && result.emoji != null) {
|
||||
const entries = Object.entries(result.emoji).toSorted(([a], [b]) => a.localeCompare(b));
|
||||
@ -417,6 +417,7 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: normalizeSlackMessagingTarget,
|
||||
resolveSessionTarget: ({ id }) => normalizeSlackMessagingTarget(`channel:${id}`),
|
||||
parseExplicitTarget: ({ raw }) => parseSlackExplicitTarget(raw),
|
||||
inferTargetChatType: ({ to }) => parseSlackExplicitTarget(to)?.chatType,
|
||||
resolveOutboundSessionRoute: async (params) => await resolveSlackOutboundSessionRoute(params),
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
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";
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { captureEnv } from "../../test-utils/env.js";
|
||||
import { handleTelegramAction, readTelegramButtons } from "./telegram-actions.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import { captureEnv } from "../../../test/helpers/extensions/env.js";
|
||||
import {
|
||||
handleTelegramAction,
|
||||
readTelegramButtons,
|
||||
telegramActionRuntime,
|
||||
} from "./action-runtime.js";
|
||||
|
||||
const originalTelegramActionRuntime = { ...telegramActionRuntime };
|
||||
const reactMessageTelegram = vi.fn(async () => ({ ok: true }));
|
||||
const sendMessageTelegram = vi.fn(async () => ({
|
||||
messageId: "789",
|
||||
@ -36,24 +41,6 @@ const createForumTopicTelegram = vi.fn(async () => ({
|
||||
}));
|
||||
let envSnapshot: ReturnType<typeof captureEnv>;
|
||||
|
||||
vi.mock("../../../extensions/telegram/src/send.js", () => ({
|
||||
reactMessageTelegram: (...args: Parameters<typeof reactMessageTelegram>) =>
|
||||
reactMessageTelegram(...args),
|
||||
sendMessageTelegram: (...args: Parameters<typeof sendMessageTelegram>) =>
|
||||
sendMessageTelegram(...args),
|
||||
sendPollTelegram: (...args: Parameters<typeof sendPollTelegram>) => sendPollTelegram(...args),
|
||||
sendStickerTelegram: (...args: Parameters<typeof sendStickerTelegram>) =>
|
||||
sendStickerTelegram(...args),
|
||||
deleteMessageTelegram: (...args: Parameters<typeof deleteMessageTelegram>) =>
|
||||
deleteMessageTelegram(...args),
|
||||
editMessageTelegram: (...args: Parameters<typeof editMessageTelegram>) =>
|
||||
editMessageTelegram(...args),
|
||||
editForumTopicTelegram: (...args: Parameters<typeof editForumTopicTelegram>) =>
|
||||
editForumTopicTelegram(...args),
|
||||
createForumTopicTelegram: (...args: Parameters<typeof createForumTopicTelegram>) =>
|
||||
createForumTopicTelegram(...args),
|
||||
}));
|
||||
|
||||
describe("handleTelegramAction", () => {
|
||||
const defaultReactionAction = {
|
||||
action: "react",
|
||||
@ -107,6 +94,16 @@ describe("handleTelegramAction", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
envSnapshot = captureEnv(["TELEGRAM_BOT_TOKEN"]);
|
||||
Object.assign(telegramActionRuntime, originalTelegramActionRuntime, {
|
||||
reactMessageTelegram,
|
||||
sendMessageTelegram,
|
||||
sendPollTelegram,
|
||||
sendStickerTelegram,
|
||||
deleteMessageTelegram,
|
||||
editMessageTelegram,
|
||||
editForumTopicTelegram,
|
||||
createForumTopicTelegram,
|
||||
});
|
||||
reactMessageTelegram.mockClear();
|
||||
sendMessageTelegram.mockClear();
|
||||
sendPollTelegram.mockClear();
|
||||
@ -1,15 +1,23 @@
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { readBooleanParam } from "../../plugin-sdk/boolean-param.js";
|
||||
import { readBooleanParam } from "openclaw/plugin-sdk/boolean-param";
|
||||
import {
|
||||
createTelegramActionGate,
|
||||
resolveTelegramPollActionGateState,
|
||||
} from "../../plugin-sdk/telegram.js";
|
||||
import type { TelegramButtonStyle, TelegramInlineButtons } from "../../plugin-sdk/telegram.js";
|
||||
jsonResult,
|
||||
readNumberParam,
|
||||
readReactionParams,
|
||||
readStringArrayParam,
|
||||
readStringOrNumberParam,
|
||||
readStringParam,
|
||||
} from "../../../src/agents/tools/common.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import type { TelegramActionConfig } from "../../../src/config/types.telegram.js";
|
||||
import { resolvePollMaxSelections } from "../../../src/polls.js";
|
||||
import { createTelegramActionGate, resolveTelegramPollActionGateState } from "./accounts.js";
|
||||
import type { TelegramButtonStyle, TelegramInlineButtons } from "./button-types.js";
|
||||
import {
|
||||
resolveTelegramInlineButtonsScope,
|
||||
resolveTelegramTargetChatType,
|
||||
} from "../../plugin-sdk/telegram.js";
|
||||
} from "./inline-buttons.js";
|
||||
import { resolveTelegramReactionLevel } from "./reaction-level.js";
|
||||
import {
|
||||
createForumTopicTelegram,
|
||||
deleteMessageTelegram,
|
||||
@ -19,22 +27,22 @@ import {
|
||||
sendMessageTelegram,
|
||||
sendPollTelegram,
|
||||
sendStickerTelegram,
|
||||
} from "../../plugin-sdk/telegram.js";
|
||||
import {
|
||||
} from "./send.js";
|
||||
import { getCacheStats, searchStickers } from "./sticker-cache.js";
|
||||
import { resolveTelegramToken } from "./token.js";
|
||||
|
||||
export const telegramActionRuntime = {
|
||||
createForumTopicTelegram,
|
||||
deleteMessageTelegram,
|
||||
editForumTopicTelegram,
|
||||
editMessageTelegram,
|
||||
getCacheStats,
|
||||
resolveTelegramReactionLevel,
|
||||
resolveTelegramToken,
|
||||
reactMessageTelegram,
|
||||
searchStickers,
|
||||
} from "../../plugin-sdk/telegram.js";
|
||||
import { resolvePollMaxSelections } from "../../polls.js";
|
||||
import {
|
||||
jsonResult,
|
||||
readNumberParam,
|
||||
readReactionParams,
|
||||
readStringArrayParam,
|
||||
readStringOrNumberParam,
|
||||
readStringParam,
|
||||
} from "./common.js";
|
||||
sendMessageTelegram,
|
||||
sendPollTelegram,
|
||||
sendStickerTelegram,
|
||||
};
|
||||
|
||||
const TELEGRAM_BUTTON_STYLES: readonly TelegramButtonStyle[] = ["danger", "success", "primary"];
|
||||
|
||||
@ -155,14 +163,19 @@ export async function handleTelegramAction(
|
||||
hint: "Telegram bot token missing. Do not retry.",
|
||||
});
|
||||
}
|
||||
let reactionResult: Awaited<ReturnType<typeof reactMessageTelegram>>;
|
||||
let reactionResult: Awaited<ReturnType<typeof telegramActionRuntime.reactMessageTelegram>>;
|
||||
try {
|
||||
reactionResult = await reactMessageTelegram(chatId ?? "", messageId ?? 0, emoji ?? "", {
|
||||
cfg,
|
||||
token,
|
||||
remove,
|
||||
accountId: accountId ?? undefined,
|
||||
});
|
||||
reactionResult = await telegramActionRuntime.reactMessageTelegram(
|
||||
chatId ?? "",
|
||||
messageId ?? 0,
|
||||
emoji ?? "",
|
||||
{
|
||||
cfg,
|
||||
token,
|
||||
remove,
|
||||
accountId: accountId ?? undefined,
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
const isInvalid = String(err).includes("REACTION_INVALID");
|
||||
return jsonResult({
|
||||
@ -241,7 +254,7 @@ export async function handleTelegramAction(
|
||||
"Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or channels.telegram.botToken.",
|
||||
);
|
||||
}
|
||||
const result = await sendMessageTelegram(to, content, {
|
||||
const result = await telegramActionRuntime.sendMessageTelegram(to, content, {
|
||||
cfg,
|
||||
token,
|
||||
accountId: accountId ?? undefined,
|
||||
@ -290,7 +303,7 @@ export async function handleTelegramAction(
|
||||
"Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or channels.telegram.botToken.",
|
||||
);
|
||||
}
|
||||
const result = await sendPollTelegram(
|
||||
const result = await telegramActionRuntime.sendPollTelegram(
|
||||
to,
|
||||
{
|
||||
question,
|
||||
@ -334,7 +347,7 @@ export async function handleTelegramAction(
|
||||
"Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or channels.telegram.botToken.",
|
||||
);
|
||||
}
|
||||
await deleteMessageTelegram(chatId ?? "", messageId ?? 0, {
|
||||
await telegramActionRuntime.deleteMessageTelegram(chatId ?? "", messageId ?? 0, {
|
||||
cfg,
|
||||
token,
|
||||
accountId: accountId ?? undefined,
|
||||
@ -375,12 +388,17 @@ export async function handleTelegramAction(
|
||||
"Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or channels.telegram.botToken.",
|
||||
);
|
||||
}
|
||||
const result = await editMessageTelegram(chatId ?? "", messageId ?? 0, content, {
|
||||
cfg,
|
||||
token,
|
||||
accountId: accountId ?? undefined,
|
||||
buttons,
|
||||
});
|
||||
const result = await telegramActionRuntime.editMessageTelegram(
|
||||
chatId ?? "",
|
||||
messageId ?? 0,
|
||||
content,
|
||||
{
|
||||
cfg,
|
||||
token,
|
||||
accountId: accountId ?? undefined,
|
||||
buttons,
|
||||
},
|
||||
);
|
||||
return jsonResult({
|
||||
ok: true,
|
||||
messageId: result.messageId,
|
||||
@ -408,7 +426,7 @@ export async function handleTelegramAction(
|
||||
"Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or channels.telegram.botToken.",
|
||||
);
|
||||
}
|
||||
const result = await sendStickerTelegram(to, fileId, {
|
||||
const result = await telegramActionRuntime.sendStickerTelegram(to, fileId, {
|
||||
cfg,
|
||||
token,
|
||||
accountId: accountId ?? undefined,
|
||||
@ -430,7 +448,7 @@ export async function handleTelegramAction(
|
||||
}
|
||||
const query = readStringParam(params, "query", { required: true });
|
||||
const limit = readNumberParam(params, "limit", { integer: true }) ?? 5;
|
||||
const results = searchStickers(query, limit);
|
||||
const results = telegramActionRuntime.searchStickers(query, limit);
|
||||
return jsonResult({
|
||||
ok: true,
|
||||
count: results.length,
|
||||
@ -444,7 +462,7 @@ export async function handleTelegramAction(
|
||||
}
|
||||
|
||||
if (action === "stickerCacheStats") {
|
||||
const stats = getCacheStats();
|
||||
const stats = telegramActionRuntime.getCacheStats();
|
||||
return jsonResult({ ok: true, ...stats });
|
||||
}
|
||||
|
||||
@ -464,7 +482,7 @@ export async function handleTelegramAction(
|
||||
"Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or channels.telegram.botToken.",
|
||||
);
|
||||
}
|
||||
const result = await createForumTopicTelegram(chatId ?? "", name, {
|
||||
const result = await telegramActionRuntime.createForumTopicTelegram(chatId ?? "", name, {
|
||||
cfg,
|
||||
token,
|
||||
accountId: accountId ?? undefined,
|
||||
@ -500,13 +518,17 @@ export async function handleTelegramAction(
|
||||
"Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or channels.telegram.botToken.",
|
||||
);
|
||||
}
|
||||
const result = await editForumTopicTelegram(chatId ?? "", messageThreadId, {
|
||||
cfg,
|
||||
token,
|
||||
accountId: accountId ?? undefined,
|
||||
name: name ?? undefined,
|
||||
iconCustomEmojiId: iconCustomEmojiId ?? undefined,
|
||||
});
|
||||
const result = await telegramActionRuntime.editForumTopicTelegram(
|
||||
chatId ?? "",
|
||||
messageThreadId,
|
||||
{
|
||||
cfg,
|
||||
token,
|
||||
accountId: accountId ?? undefined,
|
||||
name: name ?? undefined,
|
||||
iconCustomEmojiId: iconCustomEmojiId ?? undefined,
|
||||
},
|
||||
);
|
||||
return jsonResult(result);
|
||||
}
|
||||
|
||||
@ -4,7 +4,6 @@ import {
|
||||
readStringOrNumberParam,
|
||||
readStringParam,
|
||||
} from "openclaw/plugin-sdk/agent-runtime";
|
||||
import { handleTelegramAction } from "openclaw/plugin-sdk/agent-runtime";
|
||||
import { readBooleanParam } from "openclaw/plugin-sdk/boolean-param";
|
||||
import { resolveReactionMessageId } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import {
|
||||
@ -27,6 +26,7 @@ import {
|
||||
listEnabledTelegramAccounts,
|
||||
resolveTelegramPollActionGateState,
|
||||
} from "./accounts.js";
|
||||
import { handleTelegramAction } from "./action-runtime.js";
|
||||
import { resolveTelegramInlineButtons } from "./button-types.js";
|
||||
import { isTelegramInlineButtonsEnabled } from "./inline-buttons.js";
|
||||
|
||||
|
||||
@ -250,12 +250,6 @@ function hasTelegramExecApprovalDmRoute(cfg: OpenClawConfig): boolean {
|
||||
const telegramMessageActions: ChannelMessageActionAdapter = {
|
||||
describeMessageTool: (ctx) =>
|
||||
getTelegramRuntime().channel.telegram.messageActions?.describeMessageTool?.(ctx) ?? null,
|
||||
listActions: (ctx) =>
|
||||
getTelegramRuntime().channel.telegram.messageActions?.listActions?.(ctx) ?? [],
|
||||
getCapabilities: (ctx) =>
|
||||
getTelegramRuntime().channel.telegram.messageActions?.getCapabilities?.(ctx) ?? [],
|
||||
getToolSchema: (ctx) =>
|
||||
getTelegramRuntime().channel.telegram.messageActions?.getToolSchema?.(ctx) ?? null,
|
||||
extractToolSend: (ctx) =>
|
||||
getTelegramRuntime().channel.telegram.messageActions?.extractToolSend?.(ctx) ?? null,
|
||||
handleAction: async (ctx) => {
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "openclaw/plugin-sdk/tlon";
|
||||
export * from "./src/setup-core.js";
|
||||
export * from "./src/setup-surface.js";
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import crypto from "node:crypto";
|
||||
import { configureClient } from "@tloncorp/api";
|
||||
import type {
|
||||
ChannelAccountSnapshot,
|
||||
ChannelOutboundAdapter,
|
||||
ChannelPlugin,
|
||||
OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/tlon";
|
||||
} from "../api.js";
|
||||
import { createLoggerBackedRuntime, createReplyPrefixOptions } from "../api.js";
|
||||
import { monitorTlonProvider } from "./monitor/index.js";
|
||||
import { tlonSetupWizard } from "./setup-surface.js";
|
||||
import {
|
||||
@ -230,7 +232,7 @@ export async function startTlonGatewayAccount(
|
||||
accountId: account.accountId,
|
||||
ship: account.ship,
|
||||
url: account.url,
|
||||
} as import("openclaw/plugin-sdk/tlon").ChannelAccountSnapshot);
|
||||
} as ChannelAccountSnapshot);
|
||||
ctx.log?.info(`[${account.accountId}] starting Tlon provider for ${account.ship ?? "tlon"}`);
|
||||
return monitorTlonProvider({
|
||||
runtime: ctx.runtime,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
|
||||
import type { ChannelPlugin, OpenClawConfig } from "openclaw/plugin-sdk/tlon";
|
||||
import type { ChannelAccountSnapshot, ChannelPlugin, OpenClawConfig } from "../api.js";
|
||||
import { tlonChannelConfigSchema } from "./config-schema.js";
|
||||
import {
|
||||
applyTlonSetupConfig,
|
||||
@ -214,7 +214,7 @@ export const tlonPlugin: ChannelPlugin = {
|
||||
lastError: runtime?.lastError ?? null,
|
||||
probe,
|
||||
};
|
||||
return snapshot as import("openclaw/plugin-sdk/tlon").ChannelAccountSnapshot;
|
||||
return snapshot as ChannelAccountSnapshot;
|
||||
},
|
||||
},
|
||||
gateway: {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { buildChannelConfigSchema } from "openclaw/plugin-sdk/tlon";
|
||||
import { z } from "zod";
|
||||
import { buildChannelConfigSchema } from "../api.js";
|
||||
|
||||
const ShipSchema = z.string().min(1);
|
||||
const ChannelNestSchema = z.string().min(1);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { RuntimeEnv } from "openclaw/plugin-sdk/tlon";
|
||||
import type { RuntimeEnv } from "../../api.js";
|
||||
import type { Foreigns } from "../urbit/foreigns.js";
|
||||
import { formatChangesDate } from "./utils.js";
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { RuntimeEnv } from "openclaw/plugin-sdk/tlon";
|
||||
import type { RuntimeEnv } from "../../api.js";
|
||||
import { extractMessageText } from "./utils.js";
|
||||
|
||||
/**
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { RuntimeEnv, ReplyPayload, OpenClawConfig } from "openclaw/plugin-sdk/tlon";
|
||||
import { createLoggerBackedRuntime, createReplyPrefixOptions } from "openclaw/plugin-sdk/tlon";
|
||||
import type { RuntimeEnv, ReplyPayload, OpenClawConfig } from "../../api.js";
|
||||
import { createLoggerBackedRuntime, createReplyPrefixOptions } from "../../api.js";
|
||||
import { getTlonRuntime } from "../runtime.js";
|
||||
import { createSettingsManager, type TlonSettingsStore } from "../settings.js";
|
||||
import { normalizeShip, parseChannelNest } from "../targets.js";
|
||||
|
||||
@ -5,7 +5,7 @@ import { homedir } from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { Readable } from "node:stream";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/tlon";
|
||||
import { fetchWithSsrFGuard } from "../../api.js";
|
||||
import { getDefaultSsrFPolicy } from "../urbit/context.js";
|
||||
|
||||
// Default to OpenClaw workspace media directory
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { createDedupeCache } from "openclaw/plugin-sdk/tlon";
|
||||
import { createDedupeCache } from "../../api.js";
|
||||
|
||||
export type ProcessedMessageTracker = {
|
||||
mark: (id?: string | null) => boolean;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
||||
import type { PluginRuntime } from "openclaw/plugin-sdk/tlon";
|
||||
import type { PluginRuntime } from "../api.js";
|
||||
|
||||
const { setRuntime: setTlonRuntime, getRuntime: getTlonRuntime } =
|
||||
createPluginRuntimeStore<PluginRuntime>("Tlon runtime not initialized");
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/tlon";
|
||||
import type { OpenClawConfig } from "../api.js";
|
||||
|
||||
export type TlonResolvedAccount = {
|
||||
accountId: string;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { LookupFn, SsrFPolicy } from "openclaw/plugin-sdk/tlon";
|
||||
import type { LookupFn, SsrFPolicy } from "../../api.js";
|
||||
import { UrbitAuthError } from "./errors.js";
|
||||
import { urbitFetch } from "./fetch.js";
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { isBlockedHostnameOrIp } from "openclaw/plugin-sdk/tlon";
|
||||
import { isBlockedHostnameOrIp } from "../../api.js";
|
||||
|
||||
export type UrbitBaseUrlValidation =
|
||||
| { ok: true; baseUrl: string; hostname: string }
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { LookupFn, SsrFPolicy } from "openclaw/plugin-sdk/tlon";
|
||||
import type { LookupFn, SsrFPolicy } from "../../api.js";
|
||||
import { UrbitHttpError } from "./errors.js";
|
||||
import { urbitFetch } from "./fetch.js";
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { SsrFPolicy } from "openclaw/plugin-sdk/tlon";
|
||||
import type { SsrFPolicy } from "../../api.js";
|
||||
import { validateUrbitBaseUrl } from "./base-url.js";
|
||||
import { UrbitUrlError } from "./errors.js";
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { LookupFn, SsrFPolicy } from "openclaw/plugin-sdk/tlon";
|
||||
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/tlon";
|
||||
import type { LookupFn, SsrFPolicy } from "../../api.js";
|
||||
import { fetchWithSsrFGuard } from "../../api.js";
|
||||
import { validateUrbitBaseUrl } from "./base-url.js";
|
||||
import { UrbitUrlError } from "./errors.js";
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { Readable } from "node:stream";
|
||||
import type { LookupFn, SsrFPolicy } from "openclaw/plugin-sdk/tlon";
|
||||
import type { LookupFn, SsrFPolicy } from "../../api.js";
|
||||
import { ensureUrbitChannelOpen, pokeUrbitChannel, scryUrbitPath } from "./channel-ops.js";
|
||||
import { getUrbitContext, normalizeUrbitCookie } from "./context.js";
|
||||
import { urbitFetch } from "./fetch.js";
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
* Upload an image from a URL to Tlon storage.
|
||||
*/
|
||||
import { uploadFile } from "@tloncorp/api";
|
||||
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/tlon";
|
||||
import { fetchWithSsrFGuard } from "../../api.js";
|
||||
import { getDefaultSsrFPolicy } from "./context.js";
|
||||
|
||||
/**
|
||||
|
||||
1
extensions/whatsapp/action-runtime.runtime.ts
Normal file
1
extensions/whatsapp/action-runtime.runtime.ts
Normal file
@ -0,0 +1 @@
|
||||
export { handleWhatsAppAction } from "./src/action-runtime.js";
|
||||
@ -1,4 +1,5 @@
|
||||
export * from "./src/active-listener.js";
|
||||
export * from "./src/action-runtime.js";
|
||||
export * from "./src/agent-tools-login.js";
|
||||
export * from "./src/auth-store.js";
|
||||
export * from "./src/auto-reply.js";
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { resolveWhatsAppAccount } from "../../plugin-sdk/whatsapp.js";
|
||||
import { resolveWhatsAppOutboundTarget } from "../../whatsapp/resolve-outbound-target.js";
|
||||
import { ToolAuthorizationError } from "./common.js";
|
||||
import { ToolAuthorizationError } from "../../../src/agents/tools/common.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import { resolveWhatsAppOutboundTarget } from "../../../src/whatsapp/resolve-outbound-target.js";
|
||||
import { resolveWhatsAppAccount } from "./accounts.js";
|
||||
|
||||
export function resolveAuthorizedWhatsAppOutboundTarget(params: {
|
||||
cfg: OpenClawConfig;
|
||||
@ -1,17 +1,10 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js";
|
||||
import { handleWhatsAppAction } from "./whatsapp-actions.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js";
|
||||
import { handleWhatsAppAction, whatsAppActionRuntime } from "./action-runtime.js";
|
||||
|
||||
const { sendReactionWhatsApp, sendPollWhatsApp } = vi.hoisted(() => ({
|
||||
sendReactionWhatsApp: vi.fn(async () => undefined),
|
||||
sendPollWhatsApp: vi.fn(async () => ({ messageId: "poll-1", toJid: "jid-1" })),
|
||||
}));
|
||||
|
||||
vi.mock("../../../extensions/whatsapp/src/send.js", () => ({
|
||||
sendReactionWhatsApp,
|
||||
sendPollWhatsApp,
|
||||
}));
|
||||
const originalWhatsAppActionRuntime = { ...whatsAppActionRuntime };
|
||||
const sendReactionWhatsApp = vi.fn(async () => undefined);
|
||||
|
||||
const enabledConfig = {
|
||||
channels: { whatsapp: { actions: { reactions: true } } },
|
||||
@ -20,6 +13,9 @@ const enabledConfig = {
|
||||
describe("handleWhatsAppAction", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
Object.assign(whatsAppActionRuntime, originalWhatsAppActionRuntime, {
|
||||
sendReactionWhatsApp,
|
||||
});
|
||||
});
|
||||
|
||||
it("adds reactions", async () => {
|
||||
@ -1,8 +1,18 @@
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { sendReactionWhatsApp } from "../../plugin-sdk/whatsapp.js";
|
||||
import { createActionGate, jsonResult, readReactionParams, readStringParam } from "./common.js";
|
||||
import { resolveAuthorizedWhatsAppOutboundTarget } from "./whatsapp-target-auth.js";
|
||||
import {
|
||||
createActionGate,
|
||||
jsonResult,
|
||||
readReactionParams,
|
||||
readStringParam,
|
||||
} from "../../../src/agents/tools/common.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import { resolveAuthorizedWhatsAppOutboundTarget } from "./action-runtime-target-auth.js";
|
||||
import { sendReactionWhatsApp } from "./send.js";
|
||||
|
||||
export const whatsAppActionRuntime = {
|
||||
resolveAuthorizedWhatsAppOutboundTarget,
|
||||
sendReactionWhatsApp,
|
||||
};
|
||||
|
||||
export async function handleWhatsAppAction(
|
||||
params: Record<string, unknown>,
|
||||
@ -26,7 +36,7 @@ export async function handleWhatsAppAction(
|
||||
const fromMe = typeof fromMeRaw === "boolean" ? fromMeRaw : undefined;
|
||||
|
||||
// Resolve account + allowFrom via shared account logic so auth and routing stay aligned.
|
||||
const resolved = resolveAuthorizedWhatsAppOutboundTarget({
|
||||
const resolved = whatsAppActionRuntime.resolveAuthorizedWhatsAppOutboundTarget({
|
||||
cfg,
|
||||
chatJid,
|
||||
accountId,
|
||||
@ -34,7 +44,7 @@ export async function handleWhatsAppAction(
|
||||
});
|
||||
|
||||
const resolvedEmoji = remove ? "" : emoji;
|
||||
await sendReactionWhatsApp(resolved.to, messageId, resolvedEmoji, {
|
||||
await whatsAppActionRuntime.sendReactionWhatsApp(resolved.to, messageId, resolvedEmoji, {
|
||||
verbose: false,
|
||||
fromMe,
|
||||
participant: participant ?? undefined,
|
||||
@ -714,7 +714,7 @@
|
||||
"overrides": {
|
||||
"hono": "4.12.7",
|
||||
"@hono/node-server": "1.19.10",
|
||||
"fast-xml-parser": "5.3.8",
|
||||
"fast-xml-parser": "5.5.6",
|
||||
"request": "npm:@cypress/request@3.0.10",
|
||||
"request-promise": "npm:@cypress/request-promise@5.0.0",
|
||||
"file-type": "21.3.2",
|
||||
|
||||
59
pnpm-lock.yaml
generated
59
pnpm-lock.yaml
generated
@ -7,7 +7,7 @@ settings:
|
||||
overrides:
|
||||
hono: 4.12.7
|
||||
'@hono/node-server': 1.19.10
|
||||
fast-xml-parser: 5.3.8
|
||||
fast-xml-parser: 5.5.6
|
||||
request: npm:@cypress/request@3.0.10
|
||||
request-promise: npm:@cypress/request-promise@5.0.0
|
||||
file-type: 21.3.2
|
||||
@ -1411,8 +1411,8 @@ packages:
|
||||
peerDependencies:
|
||||
hono: 4.12.7
|
||||
|
||||
'@huggingface/jinja@0.5.5':
|
||||
resolution: {integrity: sha512-xRlzazC+QZwr6z4ixEqYHo9fgwhTZ3xNSdljlKfUFGZSdlvt166DljRELFUfFytlYOYvo3vTisA/AFOuOAzFQQ==}
|
||||
'@huggingface/jinja@0.5.6':
|
||||
resolution: {integrity: sha512-MyMWyLnjqo+KRJYSH7oWNbsOn5onuIvfXYPcc0WOGxU0eHUV7oAYUoQTl2BMdu7ml+ea/bu11UM+EshbeHwtIA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@img/colour@1.0.0':
|
||||
@ -3823,8 +3823,8 @@ packages:
|
||||
audio-decode@2.2.3:
|
||||
resolution: {integrity: sha512-Z0lHvMayR/Pad9+O9ddzaBJE0DrhZkQlStrC1RwcAHF3AhQAsdwKHeLGK8fYKyp2DDU6xHxzGb4CLMui12yVrg==}
|
||||
|
||||
audio-type@2.2.1:
|
||||
resolution: {integrity: sha512-En9AY6EG1qYqEy5L/quryzbA4akBpJrnBZNxeKTqGHC2xT9Qc4aZ8b7CcbOMFTTc/MGdoNyp+SN4zInZNKxMYA==}
|
||||
audio-type@2.4.0:
|
||||
resolution: {integrity: sha512-ugYMgxLpH6gyWUhFWFl2HCJboFL5z/GoqSdonx8ZycfNP8JDHBhRNzYWzrCRa/6htOWfvJAq7qpRloxvx06sRA==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
aws-sign2@0.7.0:
|
||||
@ -4504,8 +4504,11 @@ packages:
|
||||
fast-uri@3.1.0:
|
||||
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
|
||||
|
||||
fast-xml-parser@5.3.8:
|
||||
resolution: {integrity: sha512-53jIF4N6u/pxvaL1eb/hEZts/cFLWZ92eCfLrNyCI0k38lettCG/Bs40W9pPwoPXyHQlKu2OUbQtiEIZK/J6Vw==}
|
||||
fast-xml-builder@1.1.4:
|
||||
resolution: {integrity: sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==}
|
||||
|
||||
fast-xml-parser@5.5.6:
|
||||
resolution: {integrity: sha512-3+fdZyBRVg29n4rXP0joHthhcHdPUHaIC16cuyyd1iLsuaO6Vea36MPrxgAzbZna8lhvZeRL8Bc9GP56/J9xEw==}
|
||||
hasBin: true
|
||||
|
||||
fastq@1.20.1:
|
||||
@ -5426,8 +5429,8 @@ packages:
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
nanoid@5.1.6:
|
||||
resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==}
|
||||
nanoid@5.1.7:
|
||||
resolution: {integrity: sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ==}
|
||||
engines: {node: ^18 || >=20}
|
||||
hasBin: true
|
||||
|
||||
@ -5717,6 +5720,10 @@ packages:
|
||||
partial-json@0.1.7:
|
||||
resolution: {integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==}
|
||||
|
||||
path-expression-matcher@1.1.3:
|
||||
resolution: {integrity: sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
path-is-absolute@1.0.1:
|
||||
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -6206,8 +6213,8 @@ packages:
|
||||
peerDependencies:
|
||||
signal-polyfill: ^0.2.0
|
||||
|
||||
simple-git@3.32.3:
|
||||
resolution: {integrity: sha512-56a5oxFdWlsGygOXHWrG+xjj5w9ZIt2uQbzqiIGdR/6i5iococ7WQ/bNPzWxCJdEUGUCmyMH0t9zMpRJTaKxmw==}
|
||||
simple-git@3.33.0:
|
||||
resolution: {integrity: sha512-D4V/tGC2sjsoNhoMybKyGoE+v8A60hRawKQ1iFRA1zwuDgGZCBJ4ByOzZ5J8joBbi4Oam0qiPH+GhzmSBwbJng==}
|
||||
|
||||
simple-yenc@1.0.4:
|
||||
resolution: {integrity: sha512-5gvxpSd79e9a3V4QDYUqnqxeD4HGlhCakVpb6gMnDD7lexJggSBJRBO5h52y/iJrdXRilX9UCuDaIJhSWm5OWw==}
|
||||
@ -7788,13 +7795,13 @@ snapshots:
|
||||
'@aws-sdk/xml-builder@3.972.11':
|
||||
dependencies:
|
||||
'@smithy/types': 4.13.1
|
||||
fast-xml-parser: 5.3.8
|
||||
fast-xml-parser: 5.5.6
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/xml-builder@3.972.8':
|
||||
dependencies:
|
||||
'@smithy/types': 4.13.0
|
||||
fast-xml-parser: 5.3.8
|
||||
fast-xml-parser: 5.5.6
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws/lambda-invoke-store@0.2.3': {}
|
||||
@ -8244,7 +8251,7 @@ snapshots:
|
||||
dependencies:
|
||||
hono: 4.12.7
|
||||
|
||||
'@huggingface/jinja@0.5.5': {}
|
||||
'@huggingface/jinja@0.5.6': {}
|
||||
|
||||
'@img/colour@1.0.0': {}
|
||||
|
||||
@ -10942,14 +10949,14 @@ snapshots:
|
||||
'@wasm-audio-decoders/flac': 0.2.10
|
||||
'@wasm-audio-decoders/ogg-vorbis': 0.1.20
|
||||
audio-buffer: 5.0.0
|
||||
audio-type: 2.2.1
|
||||
audio-type: 2.4.0
|
||||
mpg123-decoder: 1.0.3
|
||||
node-wav: 0.0.2
|
||||
ogg-opus-decoder: 1.7.3
|
||||
qoa-format: 1.0.1
|
||||
optional: true
|
||||
|
||||
audio-type@2.2.1:
|
||||
audio-type@2.4.0:
|
||||
optional: true
|
||||
|
||||
aws-sign2@0.7.0: {}
|
||||
@ -11667,8 +11674,14 @@ snapshots:
|
||||
|
||||
fast-uri@3.1.0: {}
|
||||
|
||||
fast-xml-parser@5.3.8:
|
||||
fast-xml-builder@1.1.4:
|
||||
dependencies:
|
||||
path-expression-matcher: 1.1.3
|
||||
|
||||
fast-xml-parser@5.5.6:
|
||||
dependencies:
|
||||
fast-xml-builder: 1.1.4
|
||||
path-expression-matcher: 1.1.3
|
||||
strnum: 2.2.0
|
||||
|
||||
fastq@1.20.1:
|
||||
@ -12681,7 +12694,7 @@ snapshots:
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
nanoid@5.1.6: {}
|
||||
nanoid@5.1.7: {}
|
||||
|
||||
negotiator@0.6.3: {}
|
||||
|
||||
@ -12717,7 +12730,7 @@ snapshots:
|
||||
|
||||
node-llama-cpp@3.16.2(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@huggingface/jinja': 0.5.5
|
||||
'@huggingface/jinja': 0.5.6
|
||||
async-retry: 1.3.3
|
||||
bytes: 3.1.2
|
||||
chalk: 5.6.2
|
||||
@ -12732,14 +12745,14 @@ snapshots:
|
||||
is-unicode-supported: 2.1.0
|
||||
lifecycle-utils: 3.1.1
|
||||
log-symbols: 7.0.1
|
||||
nanoid: 5.1.6
|
||||
nanoid: 5.1.7
|
||||
node-addon-api: 8.6.0
|
||||
octokit: 5.0.5
|
||||
ora: 9.3.0
|
||||
pretty-ms: 9.3.0
|
||||
proper-lockfile: 4.1.2
|
||||
semver: 7.7.4
|
||||
simple-git: 3.32.3
|
||||
simple-git: 3.33.0
|
||||
slice-ansi: 8.0.0
|
||||
stdout-update: 4.0.1
|
||||
strip-ansi: 7.2.0
|
||||
@ -13109,6 +13122,8 @@ snapshots:
|
||||
|
||||
partial-json@0.1.7: {}
|
||||
|
||||
path-expression-matcher@1.1.3: {}
|
||||
|
||||
path-is-absolute@1.0.1:
|
||||
optional: true
|
||||
|
||||
@ -13727,7 +13742,7 @@ snapshots:
|
||||
dependencies:
|
||||
signal-polyfill: 0.2.2
|
||||
|
||||
simple-git@3.32.3:
|
||||
simple-git@3.33.0:
|
||||
dependencies:
|
||||
'@kwsites/file-exists': 1.1.1
|
||||
'@kwsites/promise-deferred': 1.1.1
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
import { getChannelPlugin, listChannelPlugins } from "../channels/plugins/index.js";
|
||||
import {
|
||||
createMessageActionDiscoveryContext,
|
||||
resolveMessageActionDiscoveryChannelId,
|
||||
} from "../channels/plugins/message-action-discovery.js";
|
||||
import {
|
||||
__testing as messageActionTesting,
|
||||
resolveMessageActionDiscoveryForPlugin,
|
||||
} from "../channels/plugins/message-actions.js";
|
||||
resolveMessageActionDiscoveryChannelId,
|
||||
__testing as messageActionTesting,
|
||||
} from "../channels/plugins/message-action-discovery.js";
|
||||
import type { ChannelAgentTool, ChannelMessageActionName } from "../channels/plugins/types.js";
|
||||
import { normalizeAnyChannelId } from "../channels/registry.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
@ -32,7 +30,7 @@ export function listChannelSupportedActions(params: {
|
||||
return [];
|
||||
}
|
||||
const plugin = getChannelPlugin(channelId as Parameters<typeof getChannelPlugin>[0]);
|
||||
if (!plugin?.actions?.listActions) {
|
||||
if (!plugin?.actions) {
|
||||
return [];
|
||||
}
|
||||
return resolveMessageActionDiscoveryForPlugin({
|
||||
|
||||
417
src/agents/pi-embedded-runner/compact.hooks.harness.ts
Normal file
417
src/agents/pi-embedded-runner/compact.hooks.harness.ts
Normal file
@ -0,0 +1,417 @@
|
||||
import { vi, type Mock } from "vitest";
|
||||
|
||||
type MockResolvedModel = {
|
||||
model: { provider: string; api: string; id: string; input: unknown[] };
|
||||
error: null;
|
||||
authStorage: { setRuntimeApiKey: Mock<(provider?: string, apiKey?: string) => void> };
|
||||
modelRegistry: Record<string, never>;
|
||||
};
|
||||
type MockMemorySearchManager = {
|
||||
manager: {
|
||||
sync: (params?: unknown) => Promise<void>;
|
||||
};
|
||||
};
|
||||
|
||||
export const contextEngineCompactMock = vi.fn(async () => ({
|
||||
ok: true as boolean,
|
||||
compacted: true as boolean,
|
||||
reason: undefined as string | undefined,
|
||||
result: { summary: "engine-summary", tokensAfter: 50 } as
|
||||
| { summary: string; tokensAfter: number }
|
||||
| undefined,
|
||||
}));
|
||||
|
||||
export const hookRunner = {
|
||||
hasHooks: vi.fn<(hookName?: string) => boolean>(),
|
||||
runBeforeCompaction: vi.fn(async () => undefined),
|
||||
runAfterCompaction: vi.fn(async () => undefined),
|
||||
};
|
||||
|
||||
export const ensureRuntimePluginsLoaded: Mock<(params?: unknown) => void> = vi.fn();
|
||||
export const resolveContextEngineMock = vi.fn(async () => ({
|
||||
info: { ownsCompaction: true as boolean },
|
||||
compact: contextEngineCompactMock,
|
||||
}));
|
||||
export const resolveModelMock: Mock<
|
||||
(provider?: string, modelId?: string, agentDir?: string, cfg?: unknown) => MockResolvedModel
|
||||
> = vi.fn((_provider?: string, _modelId?: string, _agentDir?: string, _cfg?: unknown) => ({
|
||||
model: { provider: "openai", api: "responses", id: "fake", input: [] },
|
||||
error: null,
|
||||
authStorage: { setRuntimeApiKey: vi.fn() },
|
||||
modelRegistry: {},
|
||||
}));
|
||||
export const sessionCompactImpl = vi.fn(async () => ({
|
||||
summary: "summary",
|
||||
firstKeptEntryId: "entry-1",
|
||||
tokensBefore: 120,
|
||||
details: { ok: true },
|
||||
}));
|
||||
export const triggerInternalHook: Mock<(event?: unknown) => void> = vi.fn();
|
||||
export const sanitizeSessionHistoryMock = vi.fn(
|
||||
async (params: { messages: unknown[] }) => params.messages,
|
||||
);
|
||||
export const getMemorySearchManagerMock: Mock<
|
||||
(params?: unknown) => Promise<MockMemorySearchManager>
|
||||
> = vi.fn(async () => ({
|
||||
manager: {
|
||||
sync: vi.fn(async (_params?: unknown) => {}),
|
||||
},
|
||||
}));
|
||||
export const resolveMemorySearchConfigMock = vi.fn(() => ({
|
||||
sources: ["sessions"],
|
||||
sync: {
|
||||
sessions: {
|
||||
postCompactionForce: true,
|
||||
},
|
||||
},
|
||||
}));
|
||||
export const resolveSessionAgentIdMock = vi.fn(() => "main");
|
||||
export const estimateTokensMock = vi.fn((_message?: unknown) => 10);
|
||||
export const sessionAbortCompactionMock: Mock<(reason?: unknown) => void> = vi.fn();
|
||||
export const createOpenClawCodingToolsMock = vi.fn(() => []);
|
||||
|
||||
export function resetCompactHooksHarnessMocks(): void {
|
||||
hookRunner.hasHooks.mockReset();
|
||||
hookRunner.hasHooks.mockReturnValue(false);
|
||||
hookRunner.runBeforeCompaction.mockReset();
|
||||
hookRunner.runBeforeCompaction.mockResolvedValue(undefined);
|
||||
hookRunner.runAfterCompaction.mockReset();
|
||||
hookRunner.runAfterCompaction.mockResolvedValue(undefined);
|
||||
|
||||
ensureRuntimePluginsLoaded.mockReset();
|
||||
|
||||
resolveContextEngineMock.mockReset();
|
||||
resolveContextEngineMock.mockResolvedValue({
|
||||
info: { ownsCompaction: true },
|
||||
compact: contextEngineCompactMock,
|
||||
});
|
||||
contextEngineCompactMock.mockReset();
|
||||
contextEngineCompactMock.mockResolvedValue({
|
||||
ok: true,
|
||||
compacted: true,
|
||||
reason: undefined,
|
||||
result: { summary: "engine-summary", tokensAfter: 50 },
|
||||
});
|
||||
|
||||
resolveModelMock.mockReset();
|
||||
resolveModelMock.mockReturnValue({
|
||||
model: { provider: "openai", api: "responses", id: "fake", input: [] },
|
||||
error: null,
|
||||
authStorage: { setRuntimeApiKey: vi.fn() },
|
||||
modelRegistry: {},
|
||||
});
|
||||
|
||||
sessionCompactImpl.mockReset();
|
||||
sessionCompactImpl.mockResolvedValue({
|
||||
summary: "summary",
|
||||
firstKeptEntryId: "entry-1",
|
||||
tokensBefore: 120,
|
||||
details: { ok: true },
|
||||
});
|
||||
|
||||
triggerInternalHook.mockReset();
|
||||
sanitizeSessionHistoryMock.mockReset();
|
||||
sanitizeSessionHistoryMock.mockImplementation(async (params: { messages: unknown[] }) => {
|
||||
return params.messages;
|
||||
});
|
||||
|
||||
getMemorySearchManagerMock.mockReset();
|
||||
getMemorySearchManagerMock.mockResolvedValue({
|
||||
manager: {
|
||||
sync: vi.fn(async () => {}),
|
||||
},
|
||||
});
|
||||
resolveMemorySearchConfigMock.mockReset();
|
||||
resolveMemorySearchConfigMock.mockReturnValue({
|
||||
sources: ["sessions"],
|
||||
sync: {
|
||||
sessions: {
|
||||
postCompactionForce: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
resolveSessionAgentIdMock.mockReset();
|
||||
resolveSessionAgentIdMock.mockReturnValue("main");
|
||||
estimateTokensMock.mockReset();
|
||||
estimateTokensMock.mockReturnValue(10);
|
||||
sessionAbortCompactionMock.mockReset();
|
||||
createOpenClawCodingToolsMock.mockReset();
|
||||
createOpenClawCodingToolsMock.mockReturnValue([]);
|
||||
}
|
||||
|
||||
export async function loadCompactHooksHarness(): Promise<{
|
||||
compactEmbeddedPiSessionDirect: typeof import("./compact.js").compactEmbeddedPiSessionDirect;
|
||||
compactEmbeddedPiSession: typeof import("./compact.js").compactEmbeddedPiSession;
|
||||
onSessionTranscriptUpdate: typeof import("../../sessions/transcript-events.js").onSessionTranscriptUpdate;
|
||||
}> {
|
||||
resetCompactHooksHarnessMocks();
|
||||
vi.resetModules();
|
||||
|
||||
vi.doMock("../../plugins/hook-runner-global.js", () => ({
|
||||
getGlobalHookRunner: () => hookRunner,
|
||||
}));
|
||||
|
||||
vi.doMock("../runtime-plugins.js", () => ({
|
||||
ensureRuntimePluginsLoaded,
|
||||
}));
|
||||
|
||||
vi.doMock("../../hooks/internal-hooks.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../../hooks/internal-hooks.js")>(
|
||||
"../../hooks/internal-hooks.js",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
triggerInternalHook,
|
||||
};
|
||||
});
|
||||
|
||||
vi.doMock("@mariozechner/pi-ai/oauth", () => ({
|
||||
getOAuthApiKey: vi.fn(),
|
||||
getOAuthProviders: vi.fn(() => []),
|
||||
}));
|
||||
|
||||
vi.doMock("@mariozechner/pi-coding-agent", () => ({
|
||||
AuthStorage: class AuthStorage {},
|
||||
ModelRegistry: class ModelRegistry {},
|
||||
createAgentSession: vi.fn(async () => {
|
||||
const session = {
|
||||
sessionId: "session-1",
|
||||
messages: [
|
||||
{ role: "user", content: "hello", timestamp: 1 },
|
||||
{ role: "assistant", content: [{ type: "text", text: "hi" }], timestamp: 2 },
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "t1",
|
||||
toolName: "exec",
|
||||
content: [{ type: "text", text: "output" }],
|
||||
isError: false,
|
||||
timestamp: 3,
|
||||
},
|
||||
],
|
||||
agent: {
|
||||
replaceMessages: vi.fn((messages: unknown[]) => {
|
||||
session.messages = [...(messages as typeof session.messages)];
|
||||
}),
|
||||
streamFn: vi.fn(),
|
||||
},
|
||||
compact: vi.fn(async () => {
|
||||
session.messages.splice(1);
|
||||
return await sessionCompactImpl();
|
||||
}),
|
||||
abortCompaction: sessionAbortCompactionMock,
|
||||
dispose: vi.fn(),
|
||||
};
|
||||
return { session };
|
||||
}),
|
||||
DefaultResourceLoader: class DefaultResourceLoader {},
|
||||
SessionManager: {
|
||||
open: vi.fn(() => ({})),
|
||||
},
|
||||
SettingsManager: {
|
||||
create: vi.fn(() => ({})),
|
||||
},
|
||||
estimateTokens: estimateTokensMock,
|
||||
}));
|
||||
|
||||
vi.doMock("../session-tool-result-guard-wrapper.js", () => ({
|
||||
guardSessionManager: vi.fn(() => ({
|
||||
flushPendingToolResults: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.doMock("../pi-settings.js", () => ({
|
||||
ensurePiCompactionReserveTokens: vi.fn(),
|
||||
resolveCompactionReserveTokensFloor: vi.fn(() => 0),
|
||||
}));
|
||||
|
||||
vi.doMock("../models-config.js", () => ({
|
||||
ensureOpenClawModelsJson: vi.fn(async () => {}),
|
||||
}));
|
||||
|
||||
vi.doMock("../model-auth.js", () => ({
|
||||
applyLocalNoAuthHeaderOverride: vi.fn((model: unknown) => model),
|
||||
getApiKeyForModel: vi.fn(async () => ({ apiKey: "test", mode: "env" })),
|
||||
resolveModelAuthMode: vi.fn(() => "env"),
|
||||
}));
|
||||
|
||||
vi.doMock("../sandbox.js", () => ({
|
||||
resolveSandboxContext: vi.fn(async () => null),
|
||||
}));
|
||||
|
||||
vi.doMock("../session-file-repair.js", () => ({
|
||||
repairSessionFileIfNeeded: vi.fn(async () => {}),
|
||||
}));
|
||||
|
||||
vi.doMock("../session-write-lock.js", () => ({
|
||||
acquireSessionWriteLock: vi.fn(async () => ({ release: vi.fn(async () => {}) })),
|
||||
resolveSessionLockMaxHoldFromTimeout: vi.fn(() => 0),
|
||||
}));
|
||||
|
||||
vi.doMock("../../context-engine/index.js", () => ({
|
||||
ensureContextEnginesInitialized: vi.fn(),
|
||||
resolveContextEngine: resolveContextEngineMock,
|
||||
}));
|
||||
|
||||
vi.doMock("../../process/command-queue.js", () => ({
|
||||
enqueueCommandInLane: vi.fn((_lane: unknown, task: () => unknown) => task()),
|
||||
}));
|
||||
|
||||
vi.doMock("./lanes.js", () => ({
|
||||
resolveSessionLane: vi.fn(() => "test-session-lane"),
|
||||
resolveGlobalLane: vi.fn(() => "test-global-lane"),
|
||||
}));
|
||||
|
||||
vi.doMock("../context-window-guard.js", () => ({
|
||||
resolveContextWindowInfo: vi.fn(() => ({ tokens: 128_000 })),
|
||||
}));
|
||||
|
||||
vi.doMock("../bootstrap-files.js", () => ({
|
||||
makeBootstrapWarn: vi.fn(() => () => {}),
|
||||
resolveBootstrapContextForRun: vi.fn(async () => ({ contextFiles: [] })),
|
||||
}));
|
||||
|
||||
vi.doMock("../docs-path.js", () => ({
|
||||
resolveOpenClawDocsPath: vi.fn(async () => undefined),
|
||||
}));
|
||||
|
||||
vi.doMock("../channel-tools.js", () => ({
|
||||
listChannelSupportedActions: vi.fn(() => undefined),
|
||||
resolveChannelMessageToolHints: vi.fn(() => undefined),
|
||||
}));
|
||||
|
||||
vi.doMock("../pi-tools.js", () => ({
|
||||
createOpenClawCodingTools: createOpenClawCodingToolsMock,
|
||||
}));
|
||||
|
||||
vi.doMock("./google.js", () => ({
|
||||
logToolSchemasForGoogle: vi.fn(),
|
||||
sanitizeSessionHistory: sanitizeSessionHistoryMock,
|
||||
sanitizeToolsForGoogle: vi.fn(({ tools }: { tools: unknown[] }) => tools),
|
||||
}));
|
||||
|
||||
vi.doMock("./tool-split.js", () => ({
|
||||
splitSdkTools: vi.fn(() => ({ builtInTools: [], customTools: [] })),
|
||||
}));
|
||||
|
||||
vi.doMock("../transcript-policy.js", () => ({
|
||||
resolveTranscriptPolicy: vi.fn(() => ({
|
||||
allowSyntheticToolResults: false,
|
||||
validateGeminiTurns: false,
|
||||
validateAnthropicTurns: false,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.doMock("./extensions.js", () => ({
|
||||
buildEmbeddedExtensionFactories: vi.fn(() => ({ factories: [] })),
|
||||
}));
|
||||
|
||||
vi.doMock("./history.js", () => ({
|
||||
getDmHistoryLimitFromSessionKey: vi.fn(() => undefined),
|
||||
limitHistoryTurns: vi.fn((msgs: unknown[]) => msgs.slice(0, 2)),
|
||||
}));
|
||||
|
||||
vi.doMock("../skills.js", () => ({
|
||||
applySkillEnvOverrides: vi.fn(() => () => {}),
|
||||
applySkillEnvOverridesFromSnapshot: vi.fn(() => () => {}),
|
||||
loadWorkspaceSkillEntries: vi.fn(() => []),
|
||||
resolveSkillsPromptForRun: vi.fn(() => undefined),
|
||||
}));
|
||||
|
||||
vi.doMock("../agent-paths.js", () => ({
|
||||
resolveOpenClawAgentDir: vi.fn(() => "/tmp"),
|
||||
}));
|
||||
|
||||
vi.doMock("../agent-scope.js", () => ({
|
||||
resolveSessionAgentId: resolveSessionAgentIdMock,
|
||||
resolveSessionAgentIds: vi.fn(() => ({ defaultAgentId: "main", sessionAgentId: "main" })),
|
||||
}));
|
||||
|
||||
vi.doMock("../memory-search.js", () => ({
|
||||
resolveMemorySearchConfig: resolveMemorySearchConfigMock,
|
||||
}));
|
||||
|
||||
vi.doMock("../../memory/index.js", () => ({
|
||||
getMemorySearchManager: getMemorySearchManagerMock,
|
||||
}));
|
||||
|
||||
vi.doMock("../date-time.js", () => ({
|
||||
formatUserTime: vi.fn(() => ""),
|
||||
resolveUserTimeFormat: vi.fn(() => ""),
|
||||
resolveUserTimezone: vi.fn(() => ""),
|
||||
}));
|
||||
|
||||
vi.doMock("../defaults.js", () => ({
|
||||
DEFAULT_MODEL: "fake-model",
|
||||
DEFAULT_PROVIDER: "openai",
|
||||
DEFAULT_CONTEXT_TOKENS: 128_000,
|
||||
}));
|
||||
|
||||
vi.doMock("../utils.js", () => ({
|
||||
resolveUserPath: vi.fn((p: string) => p),
|
||||
}));
|
||||
|
||||
vi.doMock("../../infra/machine-name.js", () => ({
|
||||
getMachineDisplayName: vi.fn(async () => "machine"),
|
||||
}));
|
||||
|
||||
vi.doMock("../../config/channel-capabilities.js", () => ({
|
||||
resolveChannelCapabilities: vi.fn(() => undefined),
|
||||
}));
|
||||
|
||||
vi.doMock("../../utils/message-channel.js", () => ({
|
||||
INTERNAL_MESSAGE_CHANNEL: "webchat",
|
||||
normalizeMessageChannel: vi.fn(() => undefined),
|
||||
}));
|
||||
|
||||
vi.doMock("../pi-embedded-helpers.js", () => ({
|
||||
ensureSessionHeader: vi.fn(async () => {}),
|
||||
validateAnthropicTurns: vi.fn((m: unknown[]) => m),
|
||||
validateGeminiTurns: vi.fn((m: unknown[]) => m),
|
||||
}));
|
||||
|
||||
vi.doMock("../pi-project-settings.js", () => ({
|
||||
createPreparedEmbeddedPiSettingsManager: vi.fn(() => ({
|
||||
getGlobalSettings: vi.fn(() => ({})),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.doMock("./sandbox-info.js", () => ({
|
||||
buildEmbeddedSandboxInfo: vi.fn(() => undefined),
|
||||
}));
|
||||
|
||||
vi.doMock("./model.js", () => ({
|
||||
buildModelAliasLines: vi.fn(() => []),
|
||||
resolveModel: resolveModelMock,
|
||||
resolveModelAsync: vi.fn(
|
||||
async (provider: string, modelId: string, agentDir?: string, cfg?: unknown) =>
|
||||
resolveModelMock(provider, modelId, agentDir, cfg),
|
||||
),
|
||||
}));
|
||||
|
||||
vi.doMock("./session-manager-cache.js", () => ({
|
||||
prewarmSessionFile: vi.fn(async () => {}),
|
||||
trackSessionManagerAccess: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.doMock("./system-prompt.js", () => ({
|
||||
applySystemPromptOverrideToSession: vi.fn(),
|
||||
buildEmbeddedSystemPrompt: vi.fn(() => ""),
|
||||
createSystemPromptOverride: vi.fn(() => () => ""),
|
||||
}));
|
||||
|
||||
vi.doMock("./utils.js", () => ({
|
||||
describeUnknownError: vi.fn((err: unknown) => String(err)),
|
||||
mapThinkingLevel: vi.fn(() => "off"),
|
||||
resolveExecToolDefaults: vi.fn(() => undefined),
|
||||
}));
|
||||
|
||||
const [compactModule, transcriptEvents] = await Promise.all([
|
||||
import("./compact.js"),
|
||||
import("../../sessions/transcript-events.js"),
|
||||
]);
|
||||
|
||||
return {
|
||||
...compactModule,
|
||||
onSessionTranscriptUpdate: transcriptEvents.onSessionTranscriptUpdate,
|
||||
};
|
||||
}
|
||||
@ -1,348 +1,57 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { onSessionTranscriptUpdate } from "../../sessions/transcript-events.js";
|
||||
|
||||
const {
|
||||
hookRunner,
|
||||
import { getApiProvider, unregisterApiProviders } from "@mariozechner/pi-ai";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { getCustomApiRegistrySourceId } from "../custom-api-registry.js";
|
||||
import {
|
||||
contextEngineCompactMock,
|
||||
createOpenClawCodingToolsMock,
|
||||
ensureRuntimePluginsLoaded,
|
||||
estimateTokensMock,
|
||||
getMemorySearchManagerMock,
|
||||
hookRunner,
|
||||
loadCompactHooksHarness,
|
||||
resolveContextEngineMock,
|
||||
resolveMemorySearchConfigMock,
|
||||
resolveModelMock,
|
||||
resolveSessionAgentIdMock,
|
||||
resetCompactHooksHarnessMocks,
|
||||
sanitizeSessionHistoryMock,
|
||||
sessionAbortCompactionMock,
|
||||
sessionCompactImpl,
|
||||
triggerInternalHook,
|
||||
sanitizeSessionHistoryMock,
|
||||
contextEngineCompactMock,
|
||||
getMemorySearchManagerMock,
|
||||
resolveMemorySearchConfigMock,
|
||||
resolveSessionAgentIdMock,
|
||||
estimateTokensMock,
|
||||
sessionAbortCompactionMock,
|
||||
createOpenClawCodingToolsMock,
|
||||
} = vi.hoisted(() => {
|
||||
const contextEngineCompactMock = vi.fn(async () => ({
|
||||
ok: true as boolean,
|
||||
compacted: true as boolean,
|
||||
reason: undefined as string | undefined,
|
||||
result: { summary: "engine-summary", tokensAfter: 50 } as
|
||||
| { summary: string; tokensAfter: number }
|
||||
| undefined,
|
||||
}));
|
||||
} from "./compact.hooks.harness.js";
|
||||
|
||||
return {
|
||||
hookRunner: {
|
||||
hasHooks: vi.fn(),
|
||||
runBeforeCompaction: vi.fn(),
|
||||
runAfterCompaction: vi.fn(),
|
||||
},
|
||||
ensureRuntimePluginsLoaded: vi.fn(),
|
||||
resolveContextEngineMock: vi.fn(async () => ({
|
||||
info: { ownsCompaction: true },
|
||||
compact: contextEngineCompactMock,
|
||||
})),
|
||||
resolveModelMock: vi.fn(
|
||||
(_provider?: string, _modelId?: string, _agentDir?: string, _cfg?: unknown) => ({
|
||||
model: { provider: "openai", api: "responses", id: "fake", input: [] },
|
||||
error: null,
|
||||
authStorage: { setRuntimeApiKey: vi.fn() },
|
||||
modelRegistry: {},
|
||||
}),
|
||||
),
|
||||
sessionCompactImpl: vi.fn(async () => ({
|
||||
summary: "summary",
|
||||
firstKeptEntryId: "entry-1",
|
||||
tokensBefore: 120,
|
||||
details: { ok: true },
|
||||
})),
|
||||
triggerInternalHook: vi.fn(),
|
||||
sanitizeSessionHistoryMock: vi.fn(async (params: { messages: unknown[] }) => params.messages),
|
||||
contextEngineCompactMock,
|
||||
getMemorySearchManagerMock: vi.fn(async () => ({
|
||||
manager: {
|
||||
sync: vi.fn(async () => {}),
|
||||
},
|
||||
})),
|
||||
resolveMemorySearchConfigMock: vi.fn(() => ({
|
||||
sources: ["sessions"],
|
||||
sync: {
|
||||
sessions: {
|
||||
postCompactionForce: true,
|
||||
},
|
||||
},
|
||||
})),
|
||||
resolveSessionAgentIdMock: vi.fn(() => "main"),
|
||||
estimateTokensMock: vi.fn((_message?: unknown) => 10),
|
||||
sessionAbortCompactionMock: vi.fn(),
|
||||
createOpenClawCodingToolsMock: vi.fn(() => []),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../plugins/hook-runner-global.js", () => ({
|
||||
getGlobalHookRunner: () => hookRunner,
|
||||
}));
|
||||
|
||||
vi.mock("../runtime-plugins.js", () => ({
|
||||
ensureRuntimePluginsLoaded,
|
||||
}));
|
||||
|
||||
vi.mock("../../hooks/internal-hooks.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../../hooks/internal-hooks.js")>(
|
||||
"../../hooks/internal-hooks.js",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
triggerInternalHook,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@mariozechner/pi-ai/oauth", () => ({
|
||||
getOAuthApiKey: vi.fn(),
|
||||
getOAuthProviders: vi.fn(() => []),
|
||||
}));
|
||||
|
||||
vi.mock("@mariozechner/pi-coding-agent", () => {
|
||||
return {
|
||||
AuthStorage: class AuthStorage {},
|
||||
ModelRegistry: class ModelRegistry {},
|
||||
createAgentSession: vi.fn(async () => {
|
||||
const session = {
|
||||
sessionId: "session-1",
|
||||
messages: [
|
||||
{ role: "user", content: "hello", timestamp: 1 },
|
||||
{ role: "assistant", content: [{ type: "text", text: "hi" }], timestamp: 2 },
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "t1",
|
||||
toolName: "exec",
|
||||
content: [{ type: "text", text: "output" }],
|
||||
isError: false,
|
||||
timestamp: 3,
|
||||
},
|
||||
],
|
||||
agent: {
|
||||
replaceMessages: vi.fn((messages: unknown[]) => {
|
||||
session.messages = [...(messages as typeof session.messages)];
|
||||
}),
|
||||
streamFn: vi.fn(),
|
||||
},
|
||||
compact: vi.fn(async () => {
|
||||
// simulate compaction trimming to a single message
|
||||
session.messages.splice(1);
|
||||
return await sessionCompactImpl();
|
||||
}),
|
||||
abortCompaction: sessionAbortCompactionMock,
|
||||
dispose: vi.fn(),
|
||||
};
|
||||
return { session };
|
||||
}),
|
||||
SessionManager: {
|
||||
open: vi.fn(() => ({})),
|
||||
},
|
||||
SettingsManager: {
|
||||
create: vi.fn(() => ({})),
|
||||
},
|
||||
estimateTokens: estimateTokensMock,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../session-tool-result-guard-wrapper.js", () => ({
|
||||
guardSessionManager: vi.fn(() => ({
|
||||
flushPendingToolResults: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("../pi-settings.js", () => ({
|
||||
ensurePiCompactionReserveTokens: vi.fn(),
|
||||
resolveCompactionReserveTokensFloor: vi.fn(() => 0),
|
||||
}));
|
||||
|
||||
vi.mock("../models-config.js", () => ({
|
||||
ensureOpenClawModelsJson: vi.fn(async () => {}),
|
||||
}));
|
||||
|
||||
vi.mock("../model-auth.js", () => ({
|
||||
applyLocalNoAuthHeaderOverride: vi.fn((model: unknown) => model),
|
||||
getApiKeyForModel: vi.fn(async () => ({ apiKey: "test", mode: "env" })),
|
||||
resolveModelAuthMode: vi.fn(() => "env"),
|
||||
}));
|
||||
|
||||
vi.mock("../sandbox.js", () => ({
|
||||
resolveSandboxContext: vi.fn(async () => null),
|
||||
}));
|
||||
|
||||
vi.mock("../session-file-repair.js", () => ({
|
||||
repairSessionFileIfNeeded: vi.fn(async () => {}),
|
||||
}));
|
||||
|
||||
vi.mock("../session-write-lock.js", () => ({
|
||||
acquireSessionWriteLock: vi.fn(async () => ({ release: vi.fn(async () => {}) })),
|
||||
resolveSessionLockMaxHoldFromTimeout: vi.fn(() => 0),
|
||||
}));
|
||||
|
||||
vi.mock("../../context-engine/index.js", () => ({
|
||||
ensureContextEnginesInitialized: vi.fn(),
|
||||
resolveContextEngine: resolveContextEngineMock,
|
||||
}));
|
||||
|
||||
vi.mock("../../process/command-queue.js", () => ({
|
||||
enqueueCommandInLane: vi.fn((_lane: unknown, task: () => unknown) => task()),
|
||||
}));
|
||||
|
||||
vi.mock("./lanes.js", () => ({
|
||||
resolveSessionLane: vi.fn(() => "test-session-lane"),
|
||||
resolveGlobalLane: vi.fn(() => "test-global-lane"),
|
||||
}));
|
||||
|
||||
vi.mock("../context-window-guard.js", () => ({
|
||||
resolveContextWindowInfo: vi.fn(() => ({ tokens: 128_000 })),
|
||||
}));
|
||||
|
||||
vi.mock("../bootstrap-files.js", () => ({
|
||||
makeBootstrapWarn: vi.fn(() => () => {}),
|
||||
resolveBootstrapContextForRun: vi.fn(async () => ({ contextFiles: [] })),
|
||||
}));
|
||||
|
||||
vi.mock("../docs-path.js", () => ({
|
||||
resolveOpenClawDocsPath: vi.fn(async () => undefined),
|
||||
}));
|
||||
|
||||
vi.mock("../channel-tools.js", () => ({
|
||||
listChannelSupportedActions: vi.fn(() => undefined),
|
||||
resolveChannelMessageToolHints: vi.fn(() => undefined),
|
||||
}));
|
||||
|
||||
vi.mock("../pi-tools.js", () => ({
|
||||
createOpenClawCodingTools: createOpenClawCodingToolsMock,
|
||||
}));
|
||||
|
||||
vi.mock("./google.js", () => ({
|
||||
logToolSchemasForGoogle: vi.fn(),
|
||||
sanitizeSessionHistory: sanitizeSessionHistoryMock,
|
||||
sanitizeToolsForGoogle: vi.fn(({ tools }: { tools: unknown[] }) => tools),
|
||||
}));
|
||||
|
||||
vi.mock("./tool-split.js", () => ({
|
||||
splitSdkTools: vi.fn(() => ({ builtInTools: [], customTools: [] })),
|
||||
}));
|
||||
|
||||
vi.mock("../transcript-policy.js", () => ({
|
||||
resolveTranscriptPolicy: vi.fn(() => ({
|
||||
allowSyntheticToolResults: false,
|
||||
validateGeminiTurns: false,
|
||||
validateAnthropicTurns: false,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("./extensions.js", () => ({
|
||||
buildEmbeddedExtensionFactories: vi.fn(() => ({ factories: [] })),
|
||||
}));
|
||||
|
||||
vi.mock("./history.js", () => ({
|
||||
getDmHistoryLimitFromSessionKey: vi.fn(() => undefined),
|
||||
limitHistoryTurns: vi.fn((msgs: unknown[]) => msgs.slice(0, 2)),
|
||||
}));
|
||||
|
||||
vi.mock("../skills.js", () => ({
|
||||
applySkillEnvOverrides: vi.fn(() => () => {}),
|
||||
applySkillEnvOverridesFromSnapshot: vi.fn(() => () => {}),
|
||||
loadWorkspaceSkillEntries: vi.fn(() => []),
|
||||
resolveSkillsPromptForRun: vi.fn(() => undefined),
|
||||
}));
|
||||
|
||||
vi.mock("../agent-paths.js", () => ({
|
||||
resolveOpenClawAgentDir: vi.fn(() => "/tmp"),
|
||||
}));
|
||||
|
||||
vi.mock("../agent-scope.js", () => ({
|
||||
resolveSessionAgentId: resolveSessionAgentIdMock,
|
||||
resolveSessionAgentIds: vi.fn(() => ({ defaultAgentId: "main", sessionAgentId: "main" })),
|
||||
}));
|
||||
|
||||
vi.mock("../memory-search.js", () => ({
|
||||
resolveMemorySearchConfig: resolveMemorySearchConfigMock,
|
||||
}));
|
||||
|
||||
vi.mock("../../memory/index.js", () => ({
|
||||
getMemorySearchManager: getMemorySearchManagerMock,
|
||||
}));
|
||||
|
||||
vi.mock("../date-time.js", () => ({
|
||||
formatUserTime: vi.fn(() => ""),
|
||||
resolveUserTimeFormat: vi.fn(() => ""),
|
||||
resolveUserTimezone: vi.fn(() => ""),
|
||||
}));
|
||||
|
||||
vi.mock("../defaults.js", () => ({
|
||||
DEFAULT_MODEL: "fake-model",
|
||||
DEFAULT_PROVIDER: "openai",
|
||||
DEFAULT_CONTEXT_TOKENS: 128_000,
|
||||
}));
|
||||
|
||||
vi.mock("../utils.js", () => ({
|
||||
resolveUserPath: vi.fn((p: string) => p),
|
||||
}));
|
||||
|
||||
vi.mock("../../infra/machine-name.js", () => ({
|
||||
getMachineDisplayName: vi.fn(async () => "machine"),
|
||||
}));
|
||||
|
||||
vi.mock("../../config/channel-capabilities.js", () => ({
|
||||
resolveChannelCapabilities: vi.fn(() => undefined),
|
||||
}));
|
||||
|
||||
vi.mock("../../utils/message-channel.js", () => ({
|
||||
INTERNAL_MESSAGE_CHANNEL: "webchat",
|
||||
normalizeMessageChannel: vi.fn(() => undefined),
|
||||
}));
|
||||
|
||||
vi.mock("../pi-embedded-helpers.js", () => ({
|
||||
ensureSessionHeader: vi.fn(async () => {}),
|
||||
validateAnthropicTurns: vi.fn((m: unknown[]) => m),
|
||||
validateGeminiTurns: vi.fn((m: unknown[]) => m),
|
||||
}));
|
||||
|
||||
vi.mock("../pi-project-settings.js", () => ({
|
||||
createPreparedEmbeddedPiSettingsManager: vi.fn(() => ({
|
||||
getGlobalSettings: vi.fn(() => ({})),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("./sandbox-info.js", () => ({
|
||||
buildEmbeddedSandboxInfo: vi.fn(() => undefined),
|
||||
}));
|
||||
|
||||
vi.mock("./model.js", () => ({
|
||||
buildModelAliasLines: vi.fn(() => []),
|
||||
resolveModel: resolveModelMock,
|
||||
resolveModelAsync: vi.fn(
|
||||
async (provider: string, modelId: string, agentDir?: string, cfg?: unknown) =>
|
||||
resolveModelMock(provider, modelId, agentDir, cfg),
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("./session-manager-cache.js", () => ({
|
||||
prewarmSessionFile: vi.fn(async () => {}),
|
||||
trackSessionManagerAccess: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./system-prompt.js", () => ({
|
||||
applySystemPromptOverrideToSession: vi.fn(),
|
||||
buildEmbeddedSystemPrompt: vi.fn(() => ""),
|
||||
createSystemPromptOverride: vi.fn(() => () => ""),
|
||||
}));
|
||||
|
||||
vi.mock("./utils.js", () => ({
|
||||
describeUnknownError: vi.fn((err: unknown) => String(err)),
|
||||
mapThinkingLevel: vi.fn(() => "off"),
|
||||
resolveExecToolDefaults: vi.fn(() => undefined),
|
||||
}));
|
||||
|
||||
import { getApiProvider, unregisterApiProviders } from "@mariozechner/pi-ai";
|
||||
import { getCustomApiRegistrySourceId } from "../custom-api-registry.js";
|
||||
import { compactEmbeddedPiSessionDirect, compactEmbeddedPiSession } from "./compact.js";
|
||||
let compactEmbeddedPiSessionDirect: typeof import("./compact.js").compactEmbeddedPiSessionDirect;
|
||||
let compactEmbeddedPiSession: typeof import("./compact.js").compactEmbeddedPiSession;
|
||||
let onSessionTranscriptUpdate: typeof import("../../sessions/transcript-events.js").onSessionTranscriptUpdate;
|
||||
|
||||
const TEST_SESSION_ID = "session-1";
|
||||
const TEST_SESSION_KEY = "agent:main:session-1";
|
||||
const TEST_SESSION_FILE = "/tmp/session.jsonl";
|
||||
const TEST_WORKSPACE_DIR = "/tmp";
|
||||
const TEST_CUSTOM_INSTRUCTIONS = "focus on decisions";
|
||||
type SessionHookEvent = {
|
||||
type?: string;
|
||||
action?: string;
|
||||
sessionKey?: string;
|
||||
context?: Record<string, unknown>;
|
||||
};
|
||||
type PostCompactionSyncParams = {
|
||||
reason: string;
|
||||
sessionFiles: string[];
|
||||
};
|
||||
type PostCompactionSync = (params?: unknown) => Promise<void>;
|
||||
type Deferred<T> = {
|
||||
promise: Promise<T>;
|
||||
resolve: (value: T) => void;
|
||||
};
|
||||
|
||||
function createDeferred<T>(): Deferred<T> {
|
||||
let resolve!: (value: T) => void;
|
||||
const promise = new Promise<T>((promiseResolve) => {
|
||||
resolve = promiseResolve;
|
||||
});
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
function mockResolvedModel() {
|
||||
resolveModelMock.mockReset();
|
||||
@ -389,10 +98,22 @@ function wrappedCompactionArgs(overrides: Record<string, unknown> = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
const sessionHook = (action: string) =>
|
||||
triggerInternalHook.mock.calls.find(
|
||||
(call) => call[0]?.type === "session" && call[0]?.action === action,
|
||||
)?.[0];
|
||||
const sessionHook = (action: string): SessionHookEvent | undefined =>
|
||||
triggerInternalHook.mock.calls.find((call) => {
|
||||
const event = call[0] as SessionHookEvent | undefined;
|
||||
return event?.type === "session" && event.action === action;
|
||||
})?.[0] as SessionHookEvent | undefined;
|
||||
|
||||
beforeAll(async () => {
|
||||
const loaded = await loadCompactHooksHarness();
|
||||
compactEmbeddedPiSessionDirect = loaded.compactEmbeddedPiSessionDirect;
|
||||
compactEmbeddedPiSession = loaded.compactEmbeddedPiSession;
|
||||
onSessionTranscriptUpdate = loaded.onSessionTranscriptUpdate;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
resetCompactHooksHarnessMocks();
|
||||
});
|
||||
|
||||
describe("compactEmbeddedPiSessionDirect hooks", () => {
|
||||
beforeEach(() => {
|
||||
@ -689,11 +410,12 @@ describe("compactEmbeddedPiSessionDirect hooks", () => {
|
||||
});
|
||||
|
||||
it("awaits post-compaction memory sync in await mode when postCompactionForce is true", async () => {
|
||||
let releaseSync: (() => void) | undefined;
|
||||
const syncGate = new Promise<void>((resolve) => {
|
||||
releaseSync = resolve;
|
||||
const syncStarted = createDeferred<PostCompactionSyncParams>();
|
||||
const syncRelease = createDeferred<void>();
|
||||
const sync = vi.fn<PostCompactionSync>(async (params) => {
|
||||
syncStarted.resolve(params as PostCompactionSyncParams);
|
||||
await syncRelease.promise;
|
||||
});
|
||||
const sync = vi.fn(() => syncGate);
|
||||
getMemorySearchManagerMock.mockResolvedValue({ manager: { sync } });
|
||||
let settled = false;
|
||||
|
||||
@ -706,14 +428,12 @@ describe("compactEmbeddedPiSessionDirect hooks", () => {
|
||||
void resultPromise.then(() => {
|
||||
settled = true;
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
expect(sync).toHaveBeenCalledWith({
|
||||
reason: "post-compaction",
|
||||
sessionFiles: [TEST_SESSION_FILE],
|
||||
});
|
||||
await expect(syncStarted.promise).resolves.toEqual({
|
||||
reason: "post-compaction",
|
||||
sessionFiles: [TEST_SESSION_FILE],
|
||||
});
|
||||
expect(settled).toBe(false);
|
||||
releaseSync?.();
|
||||
syncRelease.resolve(undefined);
|
||||
const result = await resultPromise;
|
||||
expect(result.ok).toBe(true);
|
||||
expect(settled).toBe(true);
|
||||
@ -736,12 +456,17 @@ describe("compactEmbeddedPiSessionDirect hooks", () => {
|
||||
});
|
||||
|
||||
it("fires post-compaction memory sync without awaiting it in async mode", async () => {
|
||||
const sync = vi.fn(async () => {});
|
||||
let resolveManager: ((value: { manager: { sync: typeof sync } }) => void) | undefined;
|
||||
const managerGate = new Promise<{ manager: { sync: typeof sync } }>((resolve) => {
|
||||
resolveManager = resolve;
|
||||
const sync = vi.fn<PostCompactionSync>(async () => {});
|
||||
const managerRequested = createDeferred<void>();
|
||||
const managerGate = createDeferred<{ manager: { sync: PostCompactionSync } }>();
|
||||
const syncStarted = createDeferred<PostCompactionSyncParams>();
|
||||
sync.mockImplementation(async (params) => {
|
||||
syncStarted.resolve(params as PostCompactionSyncParams);
|
||||
});
|
||||
getMemorySearchManagerMock.mockImplementation(async () => {
|
||||
managerRequested.resolve(undefined);
|
||||
return await managerGate.promise;
|
||||
});
|
||||
getMemorySearchManagerMock.mockImplementation(() => managerGate);
|
||||
let settled = false;
|
||||
|
||||
const resultPromise = compactEmbeddedPiSessionDirect(
|
||||
@ -750,26 +475,19 @@ describe("compactEmbeddedPiSessionDirect hooks", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(getMemorySearchManagerMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
await managerRequested.promise;
|
||||
void resultPromise.then(() => {
|
||||
settled = true;
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
expect(settled).toBe(true);
|
||||
});
|
||||
await resultPromise;
|
||||
expect(getMemorySearchManagerMock).toHaveBeenCalledTimes(1);
|
||||
expect(settled).toBe(true);
|
||||
expect(sync).not.toHaveBeenCalled();
|
||||
resolveManager?.({ manager: { sync } });
|
||||
await managerGate;
|
||||
await vi.waitFor(() => {
|
||||
expect(sync).toHaveBeenCalledWith({
|
||||
reason: "post-compaction",
|
||||
sessionFiles: [TEST_SESSION_FILE],
|
||||
});
|
||||
managerGate.resolve({ manager: { sync } });
|
||||
await expect(syncStarted.promise).resolves.toEqual({
|
||||
reason: "post-compaction",
|
||||
sessionFiles: [TEST_SESSION_FILE],
|
||||
});
|
||||
const result = await resultPromise;
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("registers the Ollama api provider before compaction", async () => {
|
||||
|
||||
@ -91,6 +91,7 @@ import {
|
||||
import { getDmHistoryLimitFromSessionKey, limitHistoryTurns } from "./history.js";
|
||||
import { resolveGlobalLane, resolveSessionLane } from "./lanes.js";
|
||||
import { log } from "./logger.js";
|
||||
import { buildEmbeddedMessageActionDiscoveryInput } from "./message-action-discovery-input.js";
|
||||
import { buildModelAliasLines, resolveModelAsync } from "./model.js";
|
||||
import { buildEmbeddedSandboxInfo } from "./sandbox-info.js";
|
||||
import { prewarmSessionFile, trackSessionManagerAccess } from "./session-manager-cache.js";
|
||||
@ -113,6 +114,11 @@ export type CompactEmbeddedPiSessionParams = {
|
||||
messageChannel?: string;
|
||||
messageProvider?: string;
|
||||
agentAccountId?: string;
|
||||
currentChannelId?: string;
|
||||
currentThreadTs?: string;
|
||||
currentMessageId?: string | number;
|
||||
/** Trusted sender id from inbound context for scoped message-tool discovery. */
|
||||
senderId?: string;
|
||||
authProfileId?: string;
|
||||
/** Group id for channel-level tool policy resolution. */
|
||||
groupId?: string | null;
|
||||
@ -655,14 +661,20 @@ export async function compactEmbeddedPiSessionDirect(
|
||||
});
|
||||
// Resolve channel-specific message actions for system prompt
|
||||
const channelActions = runtimeChannel
|
||||
? listChannelSupportedActions({
|
||||
cfg: params.config,
|
||||
channel: runtimeChannel,
|
||||
accountId: params.agentAccountId,
|
||||
sessionKey: params.sessionKey,
|
||||
sessionId: params.sessionId,
|
||||
agentId: sessionAgentId,
|
||||
})
|
||||
? listChannelSupportedActions(
|
||||
buildEmbeddedMessageActionDiscoveryInput({
|
||||
cfg: params.config,
|
||||
channel: runtimeChannel,
|
||||
currentChannelId: params.currentChannelId,
|
||||
currentThreadTs: params.currentThreadTs,
|
||||
currentMessageId: params.currentMessageId,
|
||||
accountId: params.agentAccountId,
|
||||
sessionKey: params.sessionKey,
|
||||
sessionId: params.sessionId,
|
||||
agentId: sessionAgentId,
|
||||
senderId: params.senderId,
|
||||
}),
|
||||
)
|
||||
: undefined;
|
||||
const messageToolHints = runtimeChannel
|
||||
? resolveChannelMessageToolHints({
|
||||
|
||||
@ -0,0 +1,77 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { buildEmbeddedCompactionRuntimeContext } from "./compaction-runtime-context.js";
|
||||
|
||||
describe("buildEmbeddedCompactionRuntimeContext", () => {
|
||||
it("preserves sender and current message routing for compaction", () => {
|
||||
expect(
|
||||
buildEmbeddedCompactionRuntimeContext({
|
||||
sessionKey: "agent:main:thread:1",
|
||||
messageChannel: "slack",
|
||||
messageProvider: "slack",
|
||||
agentAccountId: "acct-1",
|
||||
currentChannelId: "C123",
|
||||
currentThreadTs: "thread-9",
|
||||
currentMessageId: "msg-42",
|
||||
authProfileId: "openai:p1",
|
||||
workspaceDir: "/tmp/workspace",
|
||||
agentDir: "/tmp/agent",
|
||||
config: {} as OpenClawConfig,
|
||||
senderIsOwner: true,
|
||||
senderId: "user-123",
|
||||
provider: "openai-codex",
|
||||
modelId: "gpt-5.3-codex",
|
||||
thinkLevel: "off",
|
||||
reasoningLevel: "on",
|
||||
extraSystemPrompt: "extra",
|
||||
ownerNumbers: ["+15555550123"],
|
||||
}),
|
||||
).toMatchObject({
|
||||
sessionKey: "agent:main:thread:1",
|
||||
messageChannel: "slack",
|
||||
messageProvider: "slack",
|
||||
agentAccountId: "acct-1",
|
||||
currentChannelId: "C123",
|
||||
currentThreadTs: "thread-9",
|
||||
currentMessageId: "msg-42",
|
||||
authProfileId: "openai:p1",
|
||||
workspaceDir: "/tmp/workspace",
|
||||
agentDir: "/tmp/agent",
|
||||
senderId: "user-123",
|
||||
provider: "openai-codex",
|
||||
model: "gpt-5.3-codex",
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes nullable compaction routing fields to undefined", () => {
|
||||
expect(
|
||||
buildEmbeddedCompactionRuntimeContext({
|
||||
sessionKey: null,
|
||||
messageChannel: null,
|
||||
messageProvider: null,
|
||||
agentAccountId: null,
|
||||
currentChannelId: null,
|
||||
currentThreadTs: null,
|
||||
currentMessageId: null,
|
||||
authProfileId: null,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
agentDir: "/tmp/agent",
|
||||
senderId: null,
|
||||
provider: null,
|
||||
modelId: null,
|
||||
}),
|
||||
).toMatchObject({
|
||||
sessionKey: undefined,
|
||||
messageChannel: undefined,
|
||||
messageProvider: undefined,
|
||||
agentAccountId: undefined,
|
||||
currentChannelId: undefined,
|
||||
currentThreadTs: undefined,
|
||||
currentMessageId: undefined,
|
||||
authProfileId: undefined,
|
||||
senderId: undefined,
|
||||
provider: undefined,
|
||||
model: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
76
src/agents/pi-embedded-runner/compaction-runtime-context.ts
Normal file
76
src/agents/pi-embedded-runner/compaction-runtime-context.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import type { ReasoningLevel, ThinkLevel } from "../../auto-reply/thinking.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { ExecElevatedDefaults } from "../bash-tools.js";
|
||||
import type { SkillSnapshot } from "../skills.js";
|
||||
|
||||
export type EmbeddedCompactionRuntimeContext = {
|
||||
sessionKey?: string;
|
||||
messageChannel?: string;
|
||||
messageProvider?: string;
|
||||
agentAccountId?: string;
|
||||
currentChannelId?: string;
|
||||
currentThreadTs?: string;
|
||||
currentMessageId?: string | number;
|
||||
authProfileId?: string;
|
||||
workspaceDir: string;
|
||||
agentDir: string;
|
||||
config?: OpenClawConfig;
|
||||
skillsSnapshot?: SkillSnapshot;
|
||||
senderIsOwner?: boolean;
|
||||
senderId?: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
thinkLevel?: ThinkLevel;
|
||||
reasoningLevel?: ReasoningLevel;
|
||||
bashElevated?: ExecElevatedDefaults;
|
||||
extraSystemPrompt?: string;
|
||||
ownerNumbers?: string[];
|
||||
};
|
||||
|
||||
export function buildEmbeddedCompactionRuntimeContext(params: {
|
||||
sessionKey?: string | null;
|
||||
messageChannel?: string | null;
|
||||
messageProvider?: string | null;
|
||||
agentAccountId?: string | null;
|
||||
currentChannelId?: string | null;
|
||||
currentThreadTs?: string | null;
|
||||
currentMessageId?: string | number | null;
|
||||
authProfileId?: string | null;
|
||||
workspaceDir: string;
|
||||
agentDir: string;
|
||||
config?: OpenClawConfig;
|
||||
skillsSnapshot?: SkillSnapshot;
|
||||
senderIsOwner?: boolean;
|
||||
senderId?: string | null;
|
||||
provider?: string | null;
|
||||
modelId?: string | null;
|
||||
thinkLevel?: ThinkLevel;
|
||||
reasoningLevel?: ReasoningLevel;
|
||||
bashElevated?: ExecElevatedDefaults;
|
||||
extraSystemPrompt?: string;
|
||||
ownerNumbers?: string[];
|
||||
}): EmbeddedCompactionRuntimeContext {
|
||||
return {
|
||||
sessionKey: params.sessionKey ?? undefined,
|
||||
messageChannel: params.messageChannel ?? undefined,
|
||||
messageProvider: params.messageProvider ?? undefined,
|
||||
agentAccountId: params.agentAccountId ?? undefined,
|
||||
currentChannelId: params.currentChannelId ?? undefined,
|
||||
currentThreadTs: params.currentThreadTs ?? undefined,
|
||||
currentMessageId: params.currentMessageId ?? undefined,
|
||||
authProfileId: params.authProfileId ?? undefined,
|
||||
workspaceDir: params.workspaceDir,
|
||||
agentDir: params.agentDir,
|
||||
config: params.config,
|
||||
skillsSnapshot: params.skillsSnapshot,
|
||||
senderIsOwner: params.senderIsOwner,
|
||||
senderId: params.senderId ?? undefined,
|
||||
provider: params.provider ?? undefined,
|
||||
model: params.modelId ?? undefined,
|
||||
thinkLevel: params.thinkLevel,
|
||||
reasoningLevel: params.reasoningLevel,
|
||||
bashElevated: params.bashElevated,
|
||||
extraSystemPrompt: params.extraSystemPrompt,
|
||||
ownerNumbers: params.ownerNumbers,
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildEmbeddedMessageActionDiscoveryInput } from "./message-action-discovery-input.js";
|
||||
|
||||
describe("buildEmbeddedMessageActionDiscoveryInput", () => {
|
||||
it("maps sender and routing scope into message-action discovery context", () => {
|
||||
expect(
|
||||
buildEmbeddedMessageActionDiscoveryInput({
|
||||
channel: "telegram",
|
||||
currentChannelId: "chat-1",
|
||||
currentThreadTs: "thread-9",
|
||||
currentMessageId: "msg-42",
|
||||
accountId: "acct-1",
|
||||
sessionKey: "agent:main:thread:1",
|
||||
sessionId: "session-1",
|
||||
agentId: "main",
|
||||
senderId: "user-123",
|
||||
}),
|
||||
).toEqual({
|
||||
cfg: undefined,
|
||||
channel: "telegram",
|
||||
currentChannelId: "chat-1",
|
||||
currentThreadTs: "thread-9",
|
||||
currentMessageId: "msg-42",
|
||||
accountId: "acct-1",
|
||||
sessionKey: "agent:main:thread:1",
|
||||
sessionId: "session-1",
|
||||
agentId: "main",
|
||||
requesterSenderId: "user-123",
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes nullable routing fields to undefined", () => {
|
||||
expect(
|
||||
buildEmbeddedMessageActionDiscoveryInput({
|
||||
channel: "slack",
|
||||
currentChannelId: null,
|
||||
currentThreadTs: null,
|
||||
currentMessageId: null,
|
||||
accountId: null,
|
||||
sessionKey: null,
|
||||
sessionId: null,
|
||||
agentId: null,
|
||||
senderId: null,
|
||||
}),
|
||||
).toEqual({
|
||||
cfg: undefined,
|
||||
channel: "slack",
|
||||
currentChannelId: undefined,
|
||||
currentThreadTs: undefined,
|
||||
currentMessageId: undefined,
|
||||
accountId: undefined,
|
||||
sessionKey: undefined,
|
||||
sessionId: undefined,
|
||||
agentId: undefined,
|
||||
requesterSenderId: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,27 @@
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
|
||||
export function buildEmbeddedMessageActionDiscoveryInput(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
channel: string;
|
||||
currentChannelId?: string | null;
|
||||
currentThreadTs?: string | null;
|
||||
currentMessageId?: string | number | null;
|
||||
accountId?: string | null;
|
||||
sessionKey?: string | null;
|
||||
sessionId?: string | null;
|
||||
agentId?: string | null;
|
||||
senderId?: string | null;
|
||||
}) {
|
||||
return {
|
||||
cfg: params.cfg,
|
||||
channel: params.channel,
|
||||
currentChannelId: params.currentChannelId ?? undefined,
|
||||
currentThreadTs: params.currentThreadTs ?? undefined,
|
||||
currentMessageId: params.currentMessageId ?? undefined,
|
||||
accountId: params.accountId ?? undefined,
|
||||
sessionKey: params.sessionKey ?? undefined,
|
||||
sessionId: params.sessionId ?? undefined,
|
||||
agentId: params.agentId ?? undefined,
|
||||
requesterSenderId: params.senderId ?? undefined,
|
||||
};
|
||||
}
|
||||
406
src/agents/pi-embedded-runner/run.overflow-compaction.harness.ts
Normal file
406
src/agents/pi-embedded-runner/run.overflow-compaction.harness.ts
Normal file
@ -0,0 +1,406 @@
|
||||
import { vi, type Mock } from "vitest";
|
||||
import type { ThinkLevel } from "../../auto-reply/thinking.js";
|
||||
import type {
|
||||
PluginHookAgentContext,
|
||||
PluginHookBeforeAgentStartResult,
|
||||
PluginHookBeforeModelResolveResult,
|
||||
PluginHookBeforePromptBuildResult,
|
||||
} from "../../plugins/types.js";
|
||||
import type { EmbeddedRunAttemptResult } from "./run/types.js";
|
||||
|
||||
type MockCompactionResult =
|
||||
| {
|
||||
ok: true;
|
||||
compacted: true;
|
||||
result: {
|
||||
summary: string;
|
||||
firstKeptEntryId?: string;
|
||||
tokensBefore?: number;
|
||||
tokensAfter?: number;
|
||||
};
|
||||
reason?: string;
|
||||
}
|
||||
| {
|
||||
ok: false;
|
||||
compacted: false;
|
||||
reason: string;
|
||||
result?: undefined;
|
||||
};
|
||||
|
||||
export const mockedGlobalHookRunner = {
|
||||
hasHooks: vi.fn((_hookName: string) => false),
|
||||
runBeforeAgentStart: vi.fn(
|
||||
async (
|
||||
_event: { prompt: string; messages?: unknown[] },
|
||||
_ctx: PluginHookAgentContext,
|
||||
): Promise<PluginHookBeforeAgentStartResult | undefined> => undefined,
|
||||
),
|
||||
runBeforePromptBuild: vi.fn(
|
||||
async (
|
||||
_event: { prompt: string; messages: unknown[] },
|
||||
_ctx: PluginHookAgentContext,
|
||||
): Promise<PluginHookBeforePromptBuildResult | undefined> => undefined,
|
||||
),
|
||||
runBeforeModelResolve: vi.fn(
|
||||
async (
|
||||
_event: { prompt: string },
|
||||
_ctx: PluginHookAgentContext,
|
||||
): Promise<PluginHookBeforeModelResolveResult | undefined> => undefined,
|
||||
),
|
||||
runBeforeCompaction: vi.fn(async () => undefined),
|
||||
runAfterCompaction: vi.fn(async () => undefined),
|
||||
};
|
||||
|
||||
export const mockedContextEngine = {
|
||||
info: { ownsCompaction: false as boolean },
|
||||
compact: vi.fn<(params: unknown) => Promise<MockCompactionResult>>(async () => ({
|
||||
ok: false as const,
|
||||
compacted: false as const,
|
||||
reason: "nothing to compact",
|
||||
})),
|
||||
};
|
||||
|
||||
export const mockedContextEngineCompact = mockedContextEngine.compact;
|
||||
export const mockedCompactDirect = mockedContextEngine.compact;
|
||||
export const mockedEnsureRuntimePluginsLoaded = vi.fn<(params?: unknown) => void>();
|
||||
export const mockedPrepareProviderRuntimeAuth = vi.fn(async () => undefined);
|
||||
export const mockedRunEmbeddedAttempt =
|
||||
vi.fn<(params: unknown) => Promise<EmbeddedRunAttemptResult>>();
|
||||
export const mockedSessionLikelyHasOversizedToolResults = vi.fn(() => false);
|
||||
export const mockedTruncateOversizedToolResultsInSession = vi.fn<
|
||||
() => Promise<MockTruncateOversizedToolResultsResult>
|
||||
>(async () => ({
|
||||
truncated: false,
|
||||
truncatedCount: 0,
|
||||
reason: "no oversized tool results",
|
||||
}));
|
||||
|
||||
type MockFailoverErrorDescription = {
|
||||
message: string;
|
||||
reason: string | undefined;
|
||||
status: number | undefined;
|
||||
code: string | undefined;
|
||||
};
|
||||
|
||||
type MockCoerceToFailoverError = (
|
||||
err: unknown,
|
||||
params?: { provider?: string; model?: string; profileId?: string },
|
||||
) => unknown;
|
||||
type MockDescribeFailoverError = (err: unknown) => MockFailoverErrorDescription;
|
||||
type MockResolveFailoverStatus = (reason: string) => number | undefined;
|
||||
type MockTruncateOversizedToolResultsResult = {
|
||||
truncated: boolean;
|
||||
truncatedCount: number;
|
||||
reason?: string;
|
||||
};
|
||||
|
||||
export const mockedCoerceToFailoverError = vi.fn<MockCoerceToFailoverError>();
|
||||
export const mockedDescribeFailoverError = vi.fn<MockDescribeFailoverError>(
|
||||
(err: unknown): MockFailoverErrorDescription => ({
|
||||
message: err instanceof Error ? err.message : String(err),
|
||||
reason: undefined,
|
||||
status: undefined,
|
||||
code: undefined,
|
||||
}),
|
||||
);
|
||||
export const mockedResolveFailoverStatus = vi.fn<MockResolveFailoverStatus>();
|
||||
|
||||
export const mockedLog: {
|
||||
debug: Mock<(...args: unknown[]) => void>;
|
||||
info: Mock<(...args: unknown[]) => void>;
|
||||
warn: Mock<(...args: unknown[]) => void>;
|
||||
error: Mock<(...args: unknown[]) => void>;
|
||||
isEnabled: Mock<(level?: string) => boolean>;
|
||||
} = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
isEnabled: vi.fn(() => false),
|
||||
};
|
||||
|
||||
export const mockedClassifyFailoverReason = vi.fn(() => null);
|
||||
export const mockedExtractObservedOverflowTokenCount = vi.fn((msg?: string) => {
|
||||
const match = msg?.match(/prompt is too long:\s*([\d,]+)\s+tokens\s*>\s*[\d,]+\s+maximum/i);
|
||||
return match?.[1] ? Number(match[1].replaceAll(",", "")) : undefined;
|
||||
});
|
||||
export const mockedIsCompactionFailureError = vi.fn(() => false);
|
||||
export const mockedIsLikelyContextOverflowError = vi.fn((msg?: string) => {
|
||||
const lower = (msg ?? "").toLowerCase();
|
||||
return (
|
||||
lower.includes("request_too_large") ||
|
||||
lower.includes("context window exceeded") ||
|
||||
lower.includes("prompt is too long")
|
||||
);
|
||||
});
|
||||
export const mockedPickFallbackThinkingLevel = vi.fn<(params?: unknown) => ThinkLevel | null>(
|
||||
() => null,
|
||||
);
|
||||
|
||||
export const overflowBaseRunParams = {
|
||||
sessionId: "test-session",
|
||||
sessionKey: "test-key",
|
||||
sessionFile: "/tmp/session.json",
|
||||
workspaceDir: "/tmp/workspace",
|
||||
prompt: "hello",
|
||||
timeoutMs: 30000,
|
||||
runId: "run-1",
|
||||
} as const;
|
||||
|
||||
export function resetRunOverflowCompactionHarnessMocks(): void {
|
||||
mockedGlobalHookRunner.hasHooks.mockReset();
|
||||
mockedGlobalHookRunner.hasHooks.mockReturnValue(false);
|
||||
mockedGlobalHookRunner.runBeforeAgentStart.mockReset();
|
||||
mockedGlobalHookRunner.runBeforeAgentStart.mockResolvedValue(undefined);
|
||||
mockedGlobalHookRunner.runBeforePromptBuild.mockReset();
|
||||
mockedGlobalHookRunner.runBeforePromptBuild.mockResolvedValue(undefined);
|
||||
mockedGlobalHookRunner.runBeforeModelResolve.mockReset();
|
||||
mockedGlobalHookRunner.runBeforeModelResolve.mockResolvedValue(undefined);
|
||||
mockedGlobalHookRunner.runBeforeCompaction.mockReset();
|
||||
mockedGlobalHookRunner.runBeforeCompaction.mockResolvedValue(undefined);
|
||||
mockedGlobalHookRunner.runAfterCompaction.mockReset();
|
||||
mockedGlobalHookRunner.runAfterCompaction.mockResolvedValue(undefined);
|
||||
|
||||
mockedContextEngine.info.ownsCompaction = false;
|
||||
mockedContextEngineCompact.mockReset();
|
||||
mockedContextEngineCompact.mockResolvedValue({
|
||||
ok: false,
|
||||
compacted: false,
|
||||
reason: "nothing to compact",
|
||||
});
|
||||
|
||||
mockedEnsureRuntimePluginsLoaded.mockReset();
|
||||
mockedPrepareProviderRuntimeAuth.mockReset();
|
||||
mockedPrepareProviderRuntimeAuth.mockResolvedValue(undefined);
|
||||
mockedRunEmbeddedAttempt.mockReset();
|
||||
mockedSessionLikelyHasOversizedToolResults.mockReset();
|
||||
mockedSessionLikelyHasOversizedToolResults.mockReturnValue(false);
|
||||
mockedTruncateOversizedToolResultsInSession.mockReset();
|
||||
mockedTruncateOversizedToolResultsInSession.mockResolvedValue({
|
||||
truncated: false,
|
||||
truncatedCount: 0,
|
||||
reason: "no oversized tool results",
|
||||
});
|
||||
|
||||
mockedCoerceToFailoverError.mockReset();
|
||||
mockedCoerceToFailoverError.mockReturnValue(null);
|
||||
mockedDescribeFailoverError.mockReset();
|
||||
mockedDescribeFailoverError.mockImplementation(
|
||||
(err: unknown): MockFailoverErrorDescription => ({
|
||||
message: err instanceof Error ? err.message : String(err),
|
||||
reason: undefined,
|
||||
status: undefined,
|
||||
code: undefined,
|
||||
}),
|
||||
);
|
||||
mockedResolveFailoverStatus.mockReset();
|
||||
mockedResolveFailoverStatus.mockReturnValue(undefined);
|
||||
|
||||
mockedLog.debug.mockReset();
|
||||
mockedLog.info.mockReset();
|
||||
mockedLog.warn.mockReset();
|
||||
mockedLog.error.mockReset();
|
||||
mockedLog.isEnabled.mockReset();
|
||||
mockedLog.isEnabled.mockReturnValue(false);
|
||||
|
||||
mockedClassifyFailoverReason.mockReset();
|
||||
mockedClassifyFailoverReason.mockReturnValue(null);
|
||||
mockedExtractObservedOverflowTokenCount.mockReset();
|
||||
mockedExtractObservedOverflowTokenCount.mockImplementation((msg?: string) => {
|
||||
const match = msg?.match(/prompt is too long:\s*([\d,]+)\s+tokens\s*>\s*[\d,]+\s+maximum/i);
|
||||
return match?.[1] ? Number(match[1].replaceAll(",", "")) : undefined;
|
||||
});
|
||||
mockedIsCompactionFailureError.mockReset();
|
||||
mockedIsCompactionFailureError.mockReturnValue(false);
|
||||
mockedIsLikelyContextOverflowError.mockReset();
|
||||
mockedIsLikelyContextOverflowError.mockImplementation((msg?: string) => {
|
||||
const lower = (msg ?? "").toLowerCase();
|
||||
return (
|
||||
lower.includes("request_too_large") ||
|
||||
lower.includes("context window exceeded") ||
|
||||
lower.includes("prompt is too long")
|
||||
);
|
||||
});
|
||||
mockedPickFallbackThinkingLevel.mockReset();
|
||||
mockedPickFallbackThinkingLevel.mockReturnValue(null);
|
||||
}
|
||||
|
||||
export async function loadRunOverflowCompactionHarness(): Promise<{
|
||||
runEmbeddedPiAgent: typeof import("./run.js").runEmbeddedPiAgent;
|
||||
}> {
|
||||
resetRunOverflowCompactionHarnessMocks();
|
||||
vi.resetModules();
|
||||
|
||||
vi.doMock("../../plugins/hook-runner-global.js", () => ({
|
||||
getGlobalHookRunner: vi.fn(() => mockedGlobalHookRunner),
|
||||
}));
|
||||
|
||||
vi.doMock("../../context-engine/index.js", () => ({
|
||||
ensureContextEnginesInitialized: vi.fn(),
|
||||
resolveContextEngine: vi.fn(async () => mockedContextEngine),
|
||||
}));
|
||||
|
||||
vi.doMock("../runtime-plugins.js", () => ({
|
||||
ensureRuntimePluginsLoaded: mockedEnsureRuntimePluginsLoaded,
|
||||
}));
|
||||
|
||||
vi.doMock("../../plugins/provider-runtime.js", () => ({
|
||||
prepareProviderRuntimeAuth: mockedPrepareProviderRuntimeAuth,
|
||||
}));
|
||||
|
||||
vi.doMock("../auth-profiles.js", () => ({
|
||||
isProfileInCooldown: vi.fn(() => false),
|
||||
markAuthProfileFailure: vi.fn(async () => {}),
|
||||
markAuthProfileGood: vi.fn(async () => {}),
|
||||
markAuthProfileUsed: vi.fn(async () => {}),
|
||||
resolveProfilesUnavailableReason: vi.fn(() => undefined),
|
||||
}));
|
||||
|
||||
vi.doMock("../usage.js", () => ({
|
||||
normalizeUsage: vi.fn((usage?: unknown) =>
|
||||
usage && typeof usage === "object" ? usage : undefined,
|
||||
),
|
||||
derivePromptTokens: vi.fn(
|
||||
(usage?: { input?: number; cacheRead?: number; cacheWrite?: number }) =>
|
||||
usage
|
||||
? (() => {
|
||||
const sum = (usage.input ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
|
||||
return sum > 0 ? sum : undefined;
|
||||
})()
|
||||
: undefined,
|
||||
),
|
||||
}));
|
||||
|
||||
vi.doMock("../workspace-run.js", () => ({
|
||||
resolveRunWorkspaceDir: vi.fn((params: { workspaceDir: string }) => ({
|
||||
workspaceDir: params.workspaceDir,
|
||||
usedFallback: false,
|
||||
fallbackReason: undefined,
|
||||
agentId: "main",
|
||||
})),
|
||||
redactRunIdentifier: vi.fn((value?: string) => value ?? ""),
|
||||
}));
|
||||
|
||||
vi.doMock("../pi-embedded-helpers.js", () => ({
|
||||
formatBillingErrorMessage: vi.fn(() => ""),
|
||||
classifyFailoverReason: mockedClassifyFailoverReason,
|
||||
extractObservedOverflowTokenCount: mockedExtractObservedOverflowTokenCount,
|
||||
formatAssistantErrorText: vi.fn(() => ""),
|
||||
isAuthAssistantError: vi.fn(() => false),
|
||||
isBillingAssistantError: vi.fn(() => false),
|
||||
isCompactionFailureError: mockedIsCompactionFailureError,
|
||||
isLikelyContextOverflowError: mockedIsLikelyContextOverflowError,
|
||||
isFailoverAssistantError: vi.fn(() => false),
|
||||
isFailoverErrorMessage: vi.fn(() => false),
|
||||
parseImageSizeError: vi.fn(() => null),
|
||||
parseImageDimensionError: vi.fn(() => null),
|
||||
isRateLimitAssistantError: vi.fn(() => false),
|
||||
isTimeoutErrorMessage: vi.fn(() => false),
|
||||
pickFallbackThinkingLevel: mockedPickFallbackThinkingLevel,
|
||||
}));
|
||||
|
||||
vi.doMock("./run/attempt.js", () => ({
|
||||
runEmbeddedAttempt: mockedRunEmbeddedAttempt,
|
||||
}));
|
||||
|
||||
vi.doMock("./model.js", () => ({
|
||||
resolveModelAsync: vi.fn(async () => ({
|
||||
model: {
|
||||
id: "test-model",
|
||||
provider: "anthropic",
|
||||
contextWindow: 200000,
|
||||
api: "messages",
|
||||
},
|
||||
error: null,
|
||||
authStorage: {
|
||||
setRuntimeApiKey: vi.fn(),
|
||||
},
|
||||
modelRegistry: {},
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.doMock("../model-auth.js", () => ({
|
||||
applyLocalNoAuthHeaderOverride: vi.fn((model: unknown) => model),
|
||||
ensureAuthProfileStore: vi.fn(() => ({})),
|
||||
getApiKeyForModel: vi.fn(async () => ({
|
||||
apiKey: "test-key",
|
||||
profileId: "test-profile",
|
||||
source: "test",
|
||||
})),
|
||||
resolveAuthProfileOrder: vi.fn(() => []),
|
||||
}));
|
||||
|
||||
vi.doMock("../models-config.js", () => ({
|
||||
ensureOpenClawModelsJson: vi.fn(async () => {}),
|
||||
}));
|
||||
|
||||
vi.doMock("../context-window-guard.js", () => ({
|
||||
CONTEXT_WINDOW_HARD_MIN_TOKENS: 1000,
|
||||
CONTEXT_WINDOW_WARN_BELOW_TOKENS: 5000,
|
||||
evaluateContextWindowGuard: vi.fn(() => ({
|
||||
shouldWarn: false,
|
||||
shouldBlock: false,
|
||||
tokens: 200000,
|
||||
source: "model",
|
||||
})),
|
||||
resolveContextWindowInfo: vi.fn(() => ({
|
||||
tokens: 200000,
|
||||
source: "model",
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.doMock("../../process/command-queue.js", () => ({
|
||||
enqueueCommandInLane: vi.fn((_lane: string, task: () => unknown) => task()),
|
||||
}));
|
||||
|
||||
vi.doMock("../../utils/message-channel.js", () => ({
|
||||
isMarkdownCapableMessageChannel: vi.fn(() => true),
|
||||
}));
|
||||
|
||||
vi.doMock("../agent-paths.js", () => ({
|
||||
resolveOpenClawAgentDir: vi.fn(() => "/tmp/agent-dir"),
|
||||
}));
|
||||
|
||||
vi.doMock("../defaults.js", () => ({
|
||||
DEFAULT_CONTEXT_TOKENS: 200000,
|
||||
DEFAULT_MODEL: "test-model",
|
||||
DEFAULT_PROVIDER: "anthropic",
|
||||
}));
|
||||
|
||||
vi.doMock("../failover-error.js", () => ({
|
||||
FailoverError: class extends Error {},
|
||||
coerceToFailoverError: mockedCoerceToFailoverError,
|
||||
describeFailoverError: mockedDescribeFailoverError,
|
||||
resolveFailoverStatus: mockedResolveFailoverStatus,
|
||||
}));
|
||||
|
||||
vi.doMock("./lanes.js", () => ({
|
||||
resolveSessionLane: vi.fn(() => "session-lane"),
|
||||
resolveGlobalLane: vi.fn(() => "global-lane"),
|
||||
}));
|
||||
|
||||
vi.doMock("./logger.js", () => ({
|
||||
log: mockedLog,
|
||||
}));
|
||||
|
||||
vi.doMock("./run/payloads.js", () => ({
|
||||
buildEmbeddedRunPayloads: vi.fn(() => []),
|
||||
}));
|
||||
|
||||
vi.doMock("./tool-result-truncation.js", () => ({
|
||||
truncateOversizedToolResultsInSession: mockedTruncateOversizedToolResultsInSession,
|
||||
sessionLikelyHasOversizedToolResults: mockedSessionLikelyHasOversizedToolResults,
|
||||
}));
|
||||
|
||||
vi.doMock("./utils.js", () => ({
|
||||
describeUnknownError: vi.fn((err: unknown) => {
|
||||
if (err instanceof Error) {
|
||||
return err.message;
|
||||
}
|
||||
return String(err);
|
||||
}),
|
||||
}));
|
||||
|
||||
const { runEmbeddedPiAgent } = await import("./run.js");
|
||||
return { runEmbeddedPiAgent };
|
||||
}
|
||||
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