mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-21 14:32:03 +08:00
Compare commits
1 Commits
vincentkoc
...
vincentkoc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a60b552a2b |
@@ -40,7 +40,6 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Pairing/AllowFrom account fallback: handle omitted `accountId` values in `readChannelAllowFromStore` and `readChannelAllowFromStoreSync` as `default`, while preserving legacy unscoped allowFrom merges for default-account flows. Thanks @Sid-Qin and @vincentkoc.
|
||||
- Agents/Subagent announce cleanup: keep completion-message runs pending while descendants settle, add a 30 minute hard-expiry backstop to avoid indefinite pending state, and keep retry bookkeeping resumable across deferred wakes. (#23970) Thanks @tyler6204.
|
||||
- BlueBubbles/Message metadata: harden send response ID extraction, include sender identity in DM context, and normalize inbound `message_id` selection to avoid duplicate ID metadata. (#23970) Thanks @tyler6204.
|
||||
- Gateway/Control UI method guard: allow POST requests to non-UI routes to fall through when no base path is configured, and add POST regression coverage for fallthrough and base-path 405 behavior. (#23970) Thanks @tyler6204.
|
||||
|
||||
@@ -439,4 +439,4 @@ Planned features:
|
||||
|
||||
- [Multi-Agent Configuration](/tools/multi-agent-sandbox-tools)
|
||||
- [Routing Configuration](/channels/channel-routing)
|
||||
- [Session Management](/concepts/session)
|
||||
- [Session Management](/concepts/sessions)
|
||||
|
||||
@@ -13,28 +13,28 @@ Text is supported everywhere; media and reactions vary by channel.
|
||||
|
||||
## Supported channels
|
||||
|
||||
- [BlueBubbles](/channels/bluebubbles) — **Recommended for iMessage**; uses the BlueBubbles macOS server REST API with full feature support (edit, unsend, effects, reactions, group management — edit currently broken on macOS 26 Tahoe).
|
||||
- [WhatsApp](/channels/whatsapp) — Most popular; uses Baileys and requires QR pairing.
|
||||
- [Telegram](/channels/telegram) — Bot API via grammY; supports groups.
|
||||
- [Discord](/channels/discord) — Discord Bot API + Gateway; supports servers, channels, and DMs.
|
||||
- [IRC](/channels/irc) — Classic IRC servers; channels + DMs with pairing/allowlist controls.
|
||||
- [Slack](/channels/slack) — Bolt SDK; workspace apps.
|
||||
- [Feishu](/channels/feishu) — Feishu/Lark bot via WebSocket (plugin, installed separately).
|
||||
- [Google Chat](/channels/googlechat) — Google Chat API app via HTTP webhook.
|
||||
- [iMessage (legacy)](/channels/imessage) — Legacy macOS integration via imsg CLI (deprecated, use BlueBubbles for new setups).
|
||||
- [IRC](/channels/irc) — Classic IRC servers; channels + DMs with pairing/allowlist controls.
|
||||
- [LINE](/channels/line) — LINE Messaging API bot (plugin, installed separately).
|
||||
- [Matrix](/channels/matrix) — Matrix protocol (plugin, installed separately).
|
||||
- [Mattermost](/channels/mattermost) — Bot API + WebSocket; channels, groups, DMs (plugin, installed separately).
|
||||
- [Microsoft Teams](/channels/msteams) — Bot Framework; enterprise support (plugin, installed separately).
|
||||
- [Nextcloud Talk](/channels/nextcloud-talk) — Self-hosted chat via Nextcloud Talk (plugin, installed separately).
|
||||
- [Nostr](/channels/nostr) — Decentralized DMs via NIP-04 (plugin, installed separately).
|
||||
- [Signal](/channels/signal) — signal-cli; privacy-focused.
|
||||
- [BlueBubbles](/channels/bluebubbles) — **Recommended for iMessage**; uses the BlueBubbles macOS server REST API with full feature support (edit, unsend, effects, reactions, group management — edit currently broken on macOS 26 Tahoe).
|
||||
- [iMessage (legacy)](/channels/imessage) — Legacy macOS integration via imsg CLI (deprecated, use BlueBubbles for new setups).
|
||||
- [Microsoft Teams](/channels/msteams) — Bot Framework; enterprise support (plugin, installed separately).
|
||||
- [Synology Chat](/channels/synology-chat) — Synology NAS Chat via outgoing+incoming webhooks (plugin, installed separately).
|
||||
- [Slack](/channels/slack) — Bolt SDK; workspace apps.
|
||||
- [Telegram](/channels/telegram) — Bot API via grammY; supports groups.
|
||||
- [LINE](/channels/line) — LINE Messaging API bot (plugin, installed separately).
|
||||
- [Nextcloud Talk](/channels/nextcloud-talk) — Self-hosted chat via Nextcloud Talk (plugin, installed separately).
|
||||
- [Matrix](/channels/matrix) — Matrix protocol (plugin, installed separately).
|
||||
- [Nostr](/channels/nostr) — Decentralized DMs via NIP-04 (plugin, installed separately).
|
||||
- [Tlon](/channels/tlon) — Urbit-based messenger (plugin, installed separately).
|
||||
- [Twitch](/channels/twitch) — Twitch chat via IRC connection (plugin, installed separately).
|
||||
- [WebChat](/web/webchat) — Gateway WebChat UI over WebSocket.
|
||||
- [WhatsApp](/channels/whatsapp) — Most popular; uses Baileys and requires QR pairing.
|
||||
- [Zalo](/channels/zalo) — Zalo Bot API; Vietnam's popular messenger (plugin, installed separately).
|
||||
- [Zalo Personal](/channels/zalouser) — Zalo personal account via QR login (plugin, installed separately).
|
||||
- [WebChat](/web/webchat) — Gateway WebChat UI over WebSocket.
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
10
docs/concepts/sessions.md
Normal file
10
docs/concepts/sessions.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
summary: "Alias for session management docs"
|
||||
read_when:
|
||||
- You looked for docs/concepts/sessions.md; canonical doc lives in docs/concepts/session.md
|
||||
title: "Sessions"
|
||||
---
|
||||
|
||||
# Sessions
|
||||
|
||||
Canonical session management docs live in [Session management](/concepts/session).
|
||||
103
docs/docs.json
103
docs/docs.json
@@ -597,7 +597,7 @@
|
||||
},
|
||||
{
|
||||
"source": "/sessions",
|
||||
"destination": "/concepts/session"
|
||||
"destination": "/concepts/sessions"
|
||||
},
|
||||
{
|
||||
"source": "/setup",
|
||||
@@ -832,6 +832,7 @@
|
||||
"group": "First steps",
|
||||
"pages": [
|
||||
"start/getting-started",
|
||||
"start/quickstart",
|
||||
"start/onboarding-overview",
|
||||
"start/wizard",
|
||||
"start/onboarding"
|
||||
@@ -898,25 +899,25 @@
|
||||
{
|
||||
"group": "Messaging platforms",
|
||||
"pages": [
|
||||
"channels/bluebubbles",
|
||||
"channels/whatsapp",
|
||||
"channels/telegram",
|
||||
"channels/discord",
|
||||
"channels/irc",
|
||||
"channels/slack",
|
||||
"channels/feishu",
|
||||
"channels/googlechat",
|
||||
"channels/mattermost",
|
||||
"channels/signal",
|
||||
"channels/imessage",
|
||||
"channels/irc",
|
||||
"channels/bluebubbles",
|
||||
"channels/msteams",
|
||||
"channels/synology-chat",
|
||||
"channels/line",
|
||||
"channels/matrix",
|
||||
"channels/mattermost",
|
||||
"channels/msteams",
|
||||
"channels/nextcloud-talk",
|
||||
"channels/nostr",
|
||||
"channels/signal",
|
||||
"channels/synology-chat",
|
||||
"channels/slack",
|
||||
"channels/telegram",
|
||||
"channels/tlon",
|
||||
"channels/twitch",
|
||||
"channels/whatsapp",
|
||||
"channels/zalo",
|
||||
"channels/zalouser"
|
||||
]
|
||||
@@ -959,6 +960,7 @@
|
||||
"group": "Sessions and memory",
|
||||
"pages": [
|
||||
"concepts/session",
|
||||
"concepts/sessions",
|
||||
"concepts/session-pruning",
|
||||
"concepts/session-tool",
|
||||
"concepts/memory",
|
||||
@@ -990,20 +992,20 @@
|
||||
{
|
||||
"group": "Built-in tools",
|
||||
"pages": [
|
||||
"tools/apply-patch",
|
||||
"brave-search",
|
||||
"perplexity",
|
||||
"tools/lobster",
|
||||
"tools/llm-task",
|
||||
"tools/diffs",
|
||||
"tools/elevated",
|
||||
"tools/exec",
|
||||
"tools/exec-approvals",
|
||||
"tools/firecrawl",
|
||||
"tools/llm-task",
|
||||
"tools/lobster",
|
||||
"tools/loop-detection",
|
||||
"tools/reactions",
|
||||
"tools/web",
|
||||
"tools/apply-patch",
|
||||
"tools/elevated",
|
||||
"tools/thinking",
|
||||
"tools/web"
|
||||
"tools/reactions"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1095,7 +1097,8 @@
|
||||
"group": "Providers",
|
||||
"pages": [
|
||||
"providers/anthropic",
|
||||
"providers/bedrock",
|
||||
"providers/openai",
|
||||
"providers/openrouter",
|
||||
"providers/cloudflare-ai-gateway",
|
||||
"providers/claude-max-api-proxy",
|
||||
"providers/deepgram",
|
||||
@@ -1103,24 +1106,23 @@
|
||||
"providers/huggingface",
|
||||
"providers/kilocode",
|
||||
"providers/litellm",
|
||||
"providers/glm",
|
||||
"providers/minimax",
|
||||
"providers/bedrock",
|
||||
"providers/vercel-ai-gateway",
|
||||
"providers/moonshot",
|
||||
"providers/mistral",
|
||||
"providers/minimax",
|
||||
"providers/nvidia",
|
||||
"providers/ollama",
|
||||
"providers/openai",
|
||||
"providers/opencode",
|
||||
"providers/openrouter",
|
||||
"providers/qianfan",
|
||||
"providers/qwen",
|
||||
"providers/synthetic",
|
||||
"providers/together",
|
||||
"providers/vercel-ai-gateway",
|
||||
"providers/venice",
|
||||
"providers/vllm",
|
||||
"providers/xiaomi",
|
||||
"providers/zai"
|
||||
"providers/glm",
|
||||
"providers/zai",
|
||||
"providers/synthetic",
|
||||
"providers/qianfan"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1430,6 +1432,7 @@
|
||||
"group": "第一步",
|
||||
"pages": [
|
||||
"zh-CN/start/getting-started",
|
||||
"zh-CN/start/quickstart",
|
||||
"zh-CN/start/wizard",
|
||||
"zh-CN/start/onboarding"
|
||||
]
|
||||
@@ -1494,24 +1497,24 @@
|
||||
{
|
||||
"group": "消息平台",
|
||||
"pages": [
|
||||
"zh-CN/channels/bluebubbles",
|
||||
"zh-CN/channels/discord",
|
||||
"zh-CN/channels/feishu",
|
||||
"zh-CN/channels/whatsapp",
|
||||
"zh-CN/channels/telegram",
|
||||
"zh-CN/channels/grammy",
|
||||
"zh-CN/channels/discord",
|
||||
"zh-CN/channels/slack",
|
||||
"zh-CN/channels/feishu",
|
||||
"zh-CN/channels/googlechat",
|
||||
"zh-CN/channels/mattermost",
|
||||
"zh-CN/channels/signal",
|
||||
"zh-CN/channels/imessage",
|
||||
"zh-CN/channels/bluebubbles",
|
||||
"zh-CN/channels/nextcloud-talk",
|
||||
"zh-CN/channels/msteams",
|
||||
"zh-CN/channels/line",
|
||||
"zh-CN/channels/matrix",
|
||||
"zh-CN/channels/mattermost",
|
||||
"zh-CN/channels/msteams",
|
||||
"zh-CN/channels/nextcloud-talk",
|
||||
"zh-CN/channels/nostr",
|
||||
"zh-CN/channels/signal",
|
||||
"zh-CN/channels/slack",
|
||||
"zh-CN/channels/telegram",
|
||||
"zh-CN/channels/tlon",
|
||||
"zh-CN/channels/twitch",
|
||||
"zh-CN/channels/whatsapp",
|
||||
"zh-CN/channels/zalo",
|
||||
"zh-CN/channels/zalouser"
|
||||
]
|
||||
@@ -1554,6 +1557,7 @@
|
||||
"group": "会话与记忆",
|
||||
"pages": [
|
||||
"zh-CN/concepts/session",
|
||||
"zh-CN/concepts/sessions",
|
||||
"zh-CN/concepts/session-pruning",
|
||||
"zh-CN/concepts/session-tool",
|
||||
"zh-CN/concepts/memory",
|
||||
@@ -1585,19 +1589,18 @@
|
||||
{
|
||||
"group": "内置工具",
|
||||
"pages": [
|
||||
"zh-CN/tools/apply-patch",
|
||||
"zh-CN/brave-search",
|
||||
"zh-CN/perplexity",
|
||||
"zh-CN/tools/diffs",
|
||||
"zh-CN/tools/elevated",
|
||||
"zh-CN/tools/lobster",
|
||||
"zh-CN/tools/llm-task",
|
||||
"zh-CN/tools/exec",
|
||||
"zh-CN/tools/exec-approvals",
|
||||
"zh-CN/tools/firecrawl",
|
||||
"zh-CN/tools/llm-task",
|
||||
"zh-CN/tools/lobster",
|
||||
"zh-CN/tools/reactions",
|
||||
"zh-CN/tools/web",
|
||||
"zh-CN/tools/apply-patch",
|
||||
"zh-CN/tools/elevated",
|
||||
"zh-CN/tools/thinking",
|
||||
"zh-CN/tools/web"
|
||||
"zh-CN/tools/reactions"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1687,24 +1690,24 @@
|
||||
"group": "提供商",
|
||||
"pages": [
|
||||
"zh-CN/providers/anthropic",
|
||||
"zh-CN/providers/openai",
|
||||
"zh-CN/providers/openrouter",
|
||||
"zh-CN/providers/bedrock",
|
||||
"zh-CN/providers/vercel-ai-gateway",
|
||||
"zh-CN/providers/claude-max-api-proxy",
|
||||
"zh-CN/providers/deepgram",
|
||||
"zh-CN/providers/github-copilot",
|
||||
"zh-CN/providers/glm",
|
||||
"zh-CN/providers/moonshot",
|
||||
"zh-CN/providers/minimax",
|
||||
"zh-CN/providers/opencode",
|
||||
"zh-CN/providers/ollama",
|
||||
"zh-CN/providers/openai",
|
||||
"zh-CN/providers/openrouter",
|
||||
"zh-CN/providers/qianfan",
|
||||
"zh-CN/providers/opencode",
|
||||
"zh-CN/providers/qwen",
|
||||
"zh-CN/providers/synthetic",
|
||||
"zh-CN/providers/venice",
|
||||
"zh-CN/providers/vercel-ai-gateway",
|
||||
"zh-CN/providers/xiaomi",
|
||||
"zh-CN/providers/zai"
|
||||
"zh-CN/providers/glm",
|
||||
"zh-CN/providers/zai",
|
||||
"zh-CN/providers/synthetic",
|
||||
"zh-CN/providers/qianfan"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -35,29 +35,28 @@ See [Venice AI](/providers/venice).
|
||||
|
||||
## Provider docs
|
||||
|
||||
- [Amazon Bedrock](/providers/bedrock)
|
||||
- [Anthropic (API + Claude Code CLI)](/providers/anthropic)
|
||||
- [Cloudflare AI Gateway](/providers/cloudflare-ai-gateway)
|
||||
- [GLM models](/providers/glm)
|
||||
- [Hugging Face (Inference)](/providers/huggingface)
|
||||
- [Kilocode](/providers/kilocode)
|
||||
- [LiteLLM (unified gateway)](/providers/litellm)
|
||||
- [MiniMax](/providers/minimax)
|
||||
- [Mistral](/providers/mistral)
|
||||
- [Moonshot AI (Kimi + Kimi Coding)](/providers/moonshot)
|
||||
- [NVIDIA](/providers/nvidia)
|
||||
- [Ollama (local models)](/providers/ollama)
|
||||
- [OpenAI (API + Codex)](/providers/openai)
|
||||
- [OpenCode Zen](/providers/opencode)
|
||||
- [OpenRouter](/providers/openrouter)
|
||||
- [Qianfan](/providers/qianfan)
|
||||
- [Anthropic (API + Claude Code CLI)](/providers/anthropic)
|
||||
- [Qwen (OAuth)](/providers/qwen)
|
||||
- [Together AI](/providers/together)
|
||||
- [OpenRouter](/providers/openrouter)
|
||||
- [LiteLLM (unified gateway)](/providers/litellm)
|
||||
- [Vercel AI Gateway](/providers/vercel-ai-gateway)
|
||||
- [Venice (Venice AI, privacy-focused)](/providers/venice)
|
||||
- [vLLM (local models)](/providers/vllm)
|
||||
- [Xiaomi](/providers/xiaomi)
|
||||
- [Together AI](/providers/together)
|
||||
- [Cloudflare AI Gateway](/providers/cloudflare-ai-gateway)
|
||||
- [Moonshot AI (Kimi + Kimi Coding)](/providers/moonshot)
|
||||
- [Mistral](/providers/mistral)
|
||||
- [OpenCode Zen](/providers/opencode)
|
||||
- [Amazon Bedrock](/providers/bedrock)
|
||||
- [Z.AI](/providers/zai)
|
||||
- [Xiaomi](/providers/xiaomi)
|
||||
- [GLM models](/providers/glm)
|
||||
- [MiniMax](/providers/minimax)
|
||||
- [Venice (Venice AI, privacy-focused)](/providers/venice)
|
||||
- [Hugging Face (Inference)](/providers/huggingface)
|
||||
- [Ollama (local models)](/providers/ollama)
|
||||
- [vLLM (local models)](/providers/vllm)
|
||||
- [Qianfan](/providers/qianfan)
|
||||
- [NVIDIA](/providers/nvidia)
|
||||
|
||||
## Transcription providers
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ Use these hubs to discover every page, including deep dives and reference docs t
|
||||
- [Multi-agent routing](/concepts/multi-agent)
|
||||
- [Compaction](/concepts/compaction)
|
||||
- [Sessions](/concepts/session)
|
||||
- [Sessions (alias)](/concepts/sessions)
|
||||
- [Session pruning](/concepts/session-pruning)
|
||||
- [Session tools](/concepts/session-tool)
|
||||
- [Queue](/concepts/queue)
|
||||
|
||||
@@ -446,4 +446,4 @@ interface OpenClawConfig {
|
||||
|
||||
- [多智能体配置](/tools/multi-agent-sandbox-tools)
|
||||
- [路由配置](/channels/channel-routing)
|
||||
- [会话管理](/concepts/session)
|
||||
- [会话管理](/concepts/sessions)
|
||||
|
||||
@@ -20,26 +20,26 @@ OpenClaw 可以在你已经使用的任何聊天应用上与你交流。每个
|
||||
|
||||
## 支持的渠道
|
||||
|
||||
- [BlueBubbles](/channels/bluebubbles) — **推荐用于 iMessage**;使用 BlueBubbles macOS 服务器 REST API,功能完整(编辑、撤回、特效、回应、群组管理——编辑功能在 macOS 26 Tahoe 上目前不可用)。
|
||||
- [WhatsApp](/channels/whatsapp) — 最受欢迎;使用 Baileys,需要二维码配对。
|
||||
- [Telegram](/channels/telegram) — 通过 grammY 使用 Bot API;支持群组。
|
||||
- [Discord](/channels/discord) — Discord Bot API + Gateway;支持服务器、频道和私信。
|
||||
- [Slack](/channels/slack) — Bolt SDK;工作区应用。
|
||||
- [飞书](/channels/feishu) — 飞书(Lark)机器人(插件,需单独安装)。
|
||||
- [Google Chat](/channels/googlechat) — 通过 HTTP webhook 的 Google Chat API 应用。
|
||||
- [iMessage(旧版)](/channels/imessage) — 通过 imsg CLI 的旧版 macOS 集成(已弃用,新设置请使用 BlueBubbles)。
|
||||
- [LINE](/channels/line) — LINE Messaging API 机器人(插件,需单独安装)。
|
||||
- [Matrix](/channels/matrix) — Matrix 协议(插件,需单独安装)。
|
||||
- [Mattermost](/channels/mattermost) — Bot API + WebSocket;频道、群组、私信(插件,需单独安装)。
|
||||
- [Microsoft Teams](/channels/msteams) — Bot Framework;企业支持(插件,需单独安装)。
|
||||
- [Nextcloud Talk](/channels/nextcloud-talk) — 通过 Nextcloud Talk 的自托管聊天(插件,需单独安装)。
|
||||
- [Nostr](/channels/nostr) — 通过 NIP-04 的去中心化私信(插件,需单独安装)。
|
||||
- [Signal](/channels/signal) — signal-cli;注重隐私。
|
||||
- [Slack](/channels/slack) — Bolt SDK;工作区应用。
|
||||
- [Telegram](/channels/telegram) — 通过 grammY 使用 Bot API;支持群组。
|
||||
- [BlueBubbles](/channels/bluebubbles) — **推荐用于 iMessage**;使用 BlueBubbles macOS 服务器 REST API,功能完整(编辑、撤回、特效、回应、群组管理——编辑功能在 macOS 26 Tahoe 上目前不可用)。
|
||||
- [iMessage(旧版)](/channels/imessage) — 通过 imsg CLI 的旧版 macOS 集成(已弃用,新设置请使用 BlueBubbles)。
|
||||
- [Microsoft Teams](/channels/msteams) — Bot Framework;企业支持(插件,需单独安装)。
|
||||
- [LINE](/channels/line) — LINE Messaging API 机器人(插件,需单独安装)。
|
||||
- [Nextcloud Talk](/channels/nextcloud-talk) — 通过 Nextcloud Talk 的自托管聊天(插件,需单独安装)。
|
||||
- [Matrix](/channels/matrix) — Matrix 协议(插件,需单独安装)。
|
||||
- [Nostr](/channels/nostr) — 通过 NIP-04 的去中心化私信(插件,需单独安装)。
|
||||
- [Tlon](/channels/tlon) — 基于 Urbit 的消息应用(插件,需单独安装)。
|
||||
- [Twitch](/channels/twitch) — 通过 IRC 连接的 Twitch 聊天(插件,需单独安装)。
|
||||
- [WebChat](/web/webchat) — 基于 WebSocket 的 Gateway 网关 WebChat 界面。
|
||||
- [WhatsApp](/channels/whatsapp) — 最受欢迎;使用 Baileys,需要二维码配对。
|
||||
- [Zalo](/channels/zalo) — Zalo Bot API;越南流行的消息应用(插件,需单独安装)。
|
||||
- [Zalo Personal](/channels/zalouser) — 通过二维码登录的 Zalo 个人账号(插件,需单独安装)。
|
||||
- [WebChat](/web/webchat) — 基于 WebSocket 的 Gateway 网关 WebChat 界面。
|
||||
|
||||
## 注意事项
|
||||
|
||||
|
||||
17
docs/zh-CN/concepts/sessions.md
Normal file
17
docs/zh-CN/concepts/sessions.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
read_when:
|
||||
- 你查找了 docs/sessions.md;规范文档位于 docs/session.md
|
||||
summary: 会话管理文档的别名
|
||||
title: 会话
|
||||
x-i18n:
|
||||
generated_at: "2026-02-01T20:23:55Z"
|
||||
model: claude-opus-4-5
|
||||
provider: pi
|
||||
source_hash: 7f1e39c3c07b9bb5cdcda361399cf1ce1226ebae3a797d8f93e734aa6a4d00e2
|
||||
source_path: concepts/sessions.md
|
||||
workflow: 14
|
||||
---
|
||||
|
||||
# 会话
|
||||
|
||||
规范的会话管理文档位于[会话管理](/concepts/session)。
|
||||
@@ -41,19 +41,20 @@ Venice 是我们推荐的 Venice AI 设置,用于隐私优先的推理,并
|
||||
|
||||
## 提供商文档
|
||||
|
||||
- [Amazon Bedrock](/providers/bedrock)
|
||||
- [OpenAI(API + Codex)](/providers/openai)
|
||||
- [Anthropic(API + Claude Code CLI)](/providers/anthropic)
|
||||
- [Qwen(OAuth)](/providers/qwen)
|
||||
- [OpenRouter](/providers/openrouter)
|
||||
- [Vercel AI Gateway](/providers/vercel-ai-gateway)
|
||||
- [Moonshot AI(Kimi + Kimi Coding)](/providers/moonshot)
|
||||
- [OpenCode Zen](/providers/opencode)
|
||||
- [Amazon Bedrock](/providers/bedrock)
|
||||
- [Z.AI](/providers/zai)
|
||||
- [Xiaomi](/providers/xiaomi)
|
||||
- [GLM 模型](/providers/glm)
|
||||
- [MiniMax](/providers/minimax)
|
||||
- [Moonshot AI(Kimi + Kimi Coding)](/providers/moonshot)
|
||||
- [Ollama(本地模型)](/providers/ollama)
|
||||
- [OpenAI(API + Codex)](/providers/openai)
|
||||
- [OpenCode Zen](/providers/opencode)
|
||||
- [OpenRouter](/providers/openrouter)
|
||||
- [Qwen(OAuth)](/providers/qwen)
|
||||
- [Venice(Venice AI,注重隐私)](/providers/venice)
|
||||
- [Xiaomi](/providers/xiaomi)
|
||||
- [Z.AI](/providers/zai)
|
||||
- [Ollama(本地模型)](/providers/ollama)
|
||||
|
||||
## 转录提供商
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ x-i18n:
|
||||
- [多智能体路由](/concepts/multi-agent)
|
||||
- [压缩](/concepts/compaction)
|
||||
- [会话](/concepts/session)
|
||||
- [会话(别名)](/concepts/sessions)
|
||||
- [会话修剪](/concepts/session-pruning)
|
||||
- [会话工具](/concepts/session-tool)
|
||||
- [队列](/concepts/queue)
|
||||
|
||||
@@ -44,19 +44,19 @@ function createMergeConfigProvider() {
|
||||
return {
|
||||
baseUrl: "https://config.example/v1",
|
||||
apiKey: "CONFIG_KEY",
|
||||
api: "openai-responses" as const,
|
||||
api: "openai-responses",
|
||||
models: [
|
||||
{
|
||||
id: "config-model",
|
||||
name: "Config model",
|
||||
input: ["text"] as Array<"text" | "image">,
|
||||
input: ["text"],
|
||||
reasoning: false,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 8192,
|
||||
maxTokens: 2048,
|
||||
},
|
||||
],
|
||||
};
|
||||
} as const;
|
||||
}
|
||||
|
||||
async function runCustomProviderMergeTest(seedProvider: {
|
||||
|
||||
@@ -11,6 +11,7 @@ function applyThinkingDefault(thinking: ThinkingLevel) {
|
||||
harness.setSessionsSpawnConfigOverride({
|
||||
session: { mainKey: "main", scope: "per-sender" },
|
||||
agents: { defaults: { subagents: { thinking } } },
|
||||
routing: { sessions: { mainKey: MAIN_SESSION_KEY } },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ function configureDefaultsWithoutTimeout() {
|
||||
setSessionsSpawnConfigOverride({
|
||||
session: { mainKey: "main", scope: "per-sender" },
|
||||
agents: { defaults: { subagents: { maxConcurrent: 8 } } },
|
||||
routing: { sessions: { mainKey: MAIN_SESSION_KEY } },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ function applySubagentTimeoutDefault(seconds: number) {
|
||||
sessionsHarness.setSessionsSpawnConfigOverride({
|
||||
session: { mainKey: "main", scope: "per-sender" },
|
||||
agents: { defaults: { subagents: { runTimeoutSeconds: seconds } } },
|
||||
routing: { sessions: { mainKey: MAIN_SESSION_KEY } },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { vi, type Mock } from "vitest";
|
||||
import { vi } from "vitest";
|
||||
|
||||
type SessionsSpawnTestConfig = ReturnType<(typeof import("../config/config.js"))["loadConfig"]>;
|
||||
type CreateSessionsSpawnTool =
|
||||
@@ -16,6 +16,10 @@ type SessionsSpawnGatewayMockOptions = {
|
||||
agentWaitResult?: { status: "ok" | "timeout"; startedAt: number; endedAt: number };
|
||||
};
|
||||
|
||||
// Avoid exporting vitest mock types (TS2742 under pnpm + d.ts emit).
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
type AnyMock = any;
|
||||
|
||||
const hoisted = vi.hoisted(() => {
|
||||
const callGatewayMock = vi.fn();
|
||||
const defaultConfigOverride = {
|
||||
@@ -28,12 +32,12 @@ const hoisted = vi.hoisted(() => {
|
||||
return { callGatewayMock, defaultConfigOverride, state };
|
||||
});
|
||||
|
||||
export function getCallGatewayMock(): Mock {
|
||||
export function getCallGatewayMock(): AnyMock {
|
||||
return hoisted.callGatewayMock;
|
||||
}
|
||||
|
||||
export function getGatewayRequests(): Array<GatewayRequest> {
|
||||
return getCallGatewayMock().mock.calls.map((call: unknown[]) => call[0] as GatewayRequest);
|
||||
return getCallGatewayMock().mock.calls.map((call: [unknown]) => call[0] as GatewayRequest);
|
||||
}
|
||||
|
||||
export function getGatewayMethods(): Array<string | undefined> {
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
DEFAULT_OPENCLAW_BROWSER_COLOR,
|
||||
DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME,
|
||||
} from "../../browser/constants.js";
|
||||
import { deriveDefaultBrowserCdpPortRange } from "../../config/port-defaults.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { BROWSER_BRIDGES } from "./browser-bridges.js";
|
||||
import { computeSandboxBrowserConfigHash } from "./config-hash.js";
|
||||
@@ -71,16 +70,15 @@ function buildSandboxBrowserResolvedConfig(params: {
|
||||
evaluateEnabled: boolean;
|
||||
}): ResolvedBrowserConfig {
|
||||
const cdpHost = "127.0.0.1";
|
||||
const cdpPortRange = deriveDefaultBrowserCdpPortRange(params.controlPort);
|
||||
return {
|
||||
enabled: true,
|
||||
evaluateEnabled: params.evaluateEnabled,
|
||||
controlPort: params.controlPort,
|
||||
cdpPortRangeStart: params.cdpPort,
|
||||
cdpPortRangeEnd: params.cdpPort,
|
||||
cdpProtocol: "http",
|
||||
cdpHost,
|
||||
cdpIsLoopback: true,
|
||||
cdpPortRangeStart: cdpPortRange.start,
|
||||
cdpPortRangeEnd: cdpPortRange.end,
|
||||
remoteCdpTimeoutMs: 1500,
|
||||
remoteCdpHandshakeTimeoutMs: 3000,
|
||||
color: DEFAULT_OPENCLAW_BROWSER_COLOR,
|
||||
|
||||
@@ -31,19 +31,6 @@ describe("resolveMainSessionAlias", () => {
|
||||
scope: "per-sender",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses session.mainKey over any legacy routing sessions key", () => {
|
||||
const cfg = {
|
||||
session: { mainKey: " work ", scope: "per-sender" },
|
||||
routing: { sessions: { mainKey: "legacy-main" } },
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(resolveMainSessionAlias(cfg)).toEqual({
|
||||
mainKey: "work",
|
||||
alias: "work",
|
||||
scope: "per-sender",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("session key display/internal mapping", () => {
|
||||
|
||||
@@ -11,8 +11,6 @@ function buildResolvedConfig(): ResolvedBrowserConfig {
|
||||
enabled: true,
|
||||
evaluateEnabled: false,
|
||||
controlPort: 0,
|
||||
cdpPortRangeStart: 18800,
|
||||
cdpPortRangeEnd: 18899,
|
||||
cdpProtocol: "http",
|
||||
cdpHost: "127.0.0.1",
|
||||
cdpIsLoopback: true,
|
||||
|
||||
@@ -55,22 +55,6 @@ describe("browser config", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("supports overriding the local CDP auto-allocation range start", () => {
|
||||
const resolved = resolveBrowserConfig({
|
||||
cdpPortRangeStart: 19000,
|
||||
});
|
||||
const openclaw = resolveProfile(resolved, "openclaw");
|
||||
expect(resolved.cdpPortRangeStart).toBe(19000);
|
||||
expect(openclaw?.cdpPort).toBe(19000);
|
||||
expect(openclaw?.cdpUrl).toBe("http://127.0.0.1:19000");
|
||||
});
|
||||
|
||||
it("rejects cdpPortRangeStart values that overflow the CDP range window", () => {
|
||||
expect(() => resolveBrowserConfig({ cdpPortRangeStart: 65535 })).toThrow(
|
||||
/cdpPortRangeStart .* too high/i,
|
||||
);
|
||||
});
|
||||
|
||||
it("normalizes hex colors", () => {
|
||||
const resolved = resolveBrowserConfig({
|
||||
color: "ff4500",
|
||||
|
||||
@@ -20,8 +20,6 @@ export type ResolvedBrowserConfig = {
|
||||
enabled: boolean;
|
||||
evaluateEnabled: boolean;
|
||||
controlPort: number;
|
||||
cdpPortRangeStart: number;
|
||||
cdpPortRangeEnd: number;
|
||||
cdpProtocol: "http" | "https";
|
||||
cdpHost: string;
|
||||
cdpIsLoopback: boolean;
|
||||
@@ -65,27 +63,6 @@ function normalizeTimeoutMs(raw: number | undefined, fallback: number) {
|
||||
return value < 0 ? fallback : value;
|
||||
}
|
||||
|
||||
function resolveCdpPortRangeStart(
|
||||
rawStart: number | undefined,
|
||||
fallbackStart: number,
|
||||
rangeSpan: number,
|
||||
) {
|
||||
const start =
|
||||
typeof rawStart === "number" && Number.isFinite(rawStart)
|
||||
? Math.floor(rawStart)
|
||||
: fallbackStart;
|
||||
if (start < 1 || start > 65535) {
|
||||
throw new Error(`browser.cdpPortRangeStart must be between 1 and 65535, got: ${start}`);
|
||||
}
|
||||
const maxStart = 65535 - rangeSpan;
|
||||
if (start > maxStart) {
|
||||
throw new Error(
|
||||
`browser.cdpPortRangeStart (${start}) is too high for a ${rangeSpan + 1}-port range; max is ${maxStart}.`,
|
||||
);
|
||||
}
|
||||
return start;
|
||||
}
|
||||
|
||||
function normalizeStringList(raw: string[] | undefined): string[] | undefined {
|
||||
if (!Array.isArray(raw) || raw.length === 0) {
|
||||
return undefined;
|
||||
@@ -216,13 +193,6 @@ export function resolveBrowserConfig(
|
||||
);
|
||||
|
||||
const derivedCdpRange = deriveDefaultBrowserCdpPortRange(controlPort);
|
||||
const cdpRangeSpan = derivedCdpRange.end - derivedCdpRange.start;
|
||||
const cdpPortRangeStart = resolveCdpPortRangeStart(
|
||||
cfg?.cdpPortRangeStart,
|
||||
derivedCdpRange.start,
|
||||
cdpRangeSpan,
|
||||
);
|
||||
const cdpPortRangeEnd = cdpPortRangeStart + cdpRangeSpan;
|
||||
|
||||
const rawCdpUrl = (cfg?.cdpUrl ?? "").trim();
|
||||
let cdpInfo:
|
||||
@@ -258,7 +228,7 @@ export function resolveBrowserConfig(
|
||||
// Use legacy cdpUrl port for backward compatibility when no profiles configured
|
||||
const legacyCdpPort = rawCdpUrl ? cdpInfo.port : undefined;
|
||||
const profiles = ensureDefaultChromeExtensionProfile(
|
||||
ensureDefaultProfile(cfg?.profiles, defaultColor, legacyCdpPort, cdpPortRangeStart),
|
||||
ensureDefaultProfile(cfg?.profiles, defaultColor, legacyCdpPort, derivedCdpRange.start),
|
||||
controlPort,
|
||||
);
|
||||
const cdpProtocol = cdpInfo.parsed.protocol === "https:" ? "https" : "http";
|
||||
@@ -284,8 +254,6 @@ export function resolveBrowserConfig(
|
||||
enabled,
|
||||
evaluateEnabled,
|
||||
controlPort,
|
||||
cdpPortRangeStart,
|
||||
cdpPortRangeEnd,
|
||||
cdpProtocol,
|
||||
cdpHost: cdpInfo.parsed.hostname,
|
||||
cdpIsLoopback: isLoopbackHost(cdpInfo.parsed.hostname),
|
||||
|
||||
@@ -61,46 +61,6 @@ describe("BrowserProfilesService", () => {
|
||||
expect(writeConfigFile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to derived CDP range when resolved CDP range is missing", async () => {
|
||||
const base = resolveBrowserConfig({});
|
||||
const baseWithoutRange = { ...base } as {
|
||||
[key: string]: unknown;
|
||||
cdpPortRangeStart?: unknown;
|
||||
cdpPortRangeEnd?: unknown;
|
||||
};
|
||||
delete baseWithoutRange.cdpPortRangeStart;
|
||||
delete baseWithoutRange.cdpPortRangeEnd;
|
||||
const resolved = {
|
||||
...baseWithoutRange,
|
||||
controlPort: 30000,
|
||||
} as BrowserServerState["resolved"];
|
||||
const { ctx, state } = createCtx(resolved);
|
||||
|
||||
vi.mocked(loadConfig).mockReturnValue({ browser: { profiles: {} } });
|
||||
|
||||
const service = createBrowserProfilesService(ctx);
|
||||
const result = await service.createProfile({ name: "work" });
|
||||
|
||||
expect(result.cdpPort).toBe(30009);
|
||||
expect(state.resolved.profiles.work?.cdpPort).toBe(30009);
|
||||
expect(writeConfigFile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allocates from configured cdpPortRangeStart for new local profiles", async () => {
|
||||
const resolved = resolveBrowserConfig({ cdpPortRangeStart: 19000 });
|
||||
const { ctx, state } = createCtx(resolved);
|
||||
|
||||
vi.mocked(loadConfig).mockReturnValue({ browser: { cdpPortRangeStart: 19000, profiles: {} } });
|
||||
|
||||
const service = createBrowserProfilesService(ctx);
|
||||
const result = await service.createProfile({ name: "work" });
|
||||
|
||||
expect(result.cdpPort).toBe(19001);
|
||||
expect(result.isRemote).toBe(false);
|
||||
expect(state.resolved.profiles.work?.cdpPort).toBe(19001);
|
||||
expect(writeConfigFile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("accepts per-profile cdpUrl for remote Chrome", async () => {
|
||||
const resolved = resolveBrowserConfig({});
|
||||
const { ctx } = createCtx(resolved);
|
||||
|
||||
@@ -40,30 +40,6 @@ export type DeleteProfileResult = {
|
||||
|
||||
const HEX_COLOR_RE = /^#[0-9A-Fa-f]{6}$/;
|
||||
|
||||
const cdpPortRange = (resolved: {
|
||||
controlPort: number;
|
||||
cdpPortRangeStart?: number;
|
||||
cdpPortRangeEnd?: number;
|
||||
}): { start: number; end: number } => {
|
||||
const start = resolved.cdpPortRangeStart;
|
||||
const end = resolved.cdpPortRangeEnd;
|
||||
if (
|
||||
typeof start === "number" &&
|
||||
Number.isFinite(start) &&
|
||||
Number.isInteger(start) &&
|
||||
typeof end === "number" &&
|
||||
Number.isFinite(end) &&
|
||||
Number.isInteger(end) &&
|
||||
start > 0 &&
|
||||
end >= start &&
|
||||
end <= 65535
|
||||
) {
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
return deriveDefaultBrowserCdpPortRange(resolved.controlPort);
|
||||
};
|
||||
|
||||
export function createBrowserProfilesService(ctx: BrowserRouteContext) {
|
||||
const listProfiles = async (): Promise<ProfileStatus[]> => {
|
||||
return await ctx.listProfiles();
|
||||
@@ -104,7 +80,7 @@ export function createBrowserProfilesService(ctx: BrowserRouteContext) {
|
||||
};
|
||||
} else {
|
||||
const usedPorts = getUsedPorts(resolvedProfiles);
|
||||
const range = cdpPortRange(state.resolved);
|
||||
const range = deriveDefaultBrowserCdpPortRange(state.resolved.controlPort);
|
||||
const cdpPort = allocateCdpPort(usedPorts, range);
|
||||
if (cdpPort === null) {
|
||||
throw new Error("no available CDP ports in range");
|
||||
|
||||
@@ -12,8 +12,6 @@ function makeBrowserState(): BrowserServerState {
|
||||
resolved: {
|
||||
enabled: true,
|
||||
controlPort: 18791,
|
||||
cdpPortRangeStart: 18800,
|
||||
cdpPortRangeEnd: 18899,
|
||||
cdpProtocol: "http",
|
||||
cdpHost: "127.0.0.1",
|
||||
cdpIsLoopback: true,
|
||||
|
||||
@@ -24,8 +24,6 @@ function makeState(
|
||||
resolved: {
|
||||
enabled: true,
|
||||
controlPort: 18791,
|
||||
cdpPortRangeStart: 18800,
|
||||
cdpPortRangeEnd: 18899,
|
||||
cdpProtocol: profile === "remote" ? "https" : "http",
|
||||
cdpHost: profile === "remote" ? "browserless.example" : "127.0.0.1",
|
||||
cdpIsLoopback: profile !== "remote",
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createTypingCallbacks } from "./typing.js";
|
||||
|
||||
type TypingCallbackOverrides = Partial<Parameters<typeof createTypingCallbacks>[0]>;
|
||||
type TypingHarnessStart = ReturnType<typeof vi.fn<() => Promise<void>>>;
|
||||
type TypingHarnessError = ReturnType<typeof vi.fn<(err: unknown) => void>>;
|
||||
|
||||
const flushMicrotasks = async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
@@ -19,30 +15,16 @@ async function withFakeTimers(run: () => Promise<void>) {
|
||||
}
|
||||
}
|
||||
|
||||
function createTypingHarness(overrides: TypingCallbackOverrides = {}) {
|
||||
const start: TypingHarnessStart = vi.fn<() => Promise<void>>(async () => {});
|
||||
const stop: TypingHarnessStart = vi.fn<() => Promise<void>>(async () => {});
|
||||
const onStartError: TypingHarnessError = vi.fn<(err: unknown) => void>();
|
||||
const onStopError: TypingHarnessError = vi.fn<(err: unknown) => void>();
|
||||
|
||||
if (overrides.start) {
|
||||
start.mockImplementation(overrides.start);
|
||||
}
|
||||
if (overrides.stop) {
|
||||
stop.mockImplementation(overrides.stop);
|
||||
}
|
||||
if (overrides.onStartError) {
|
||||
onStartError.mockImplementation(overrides.onStartError);
|
||||
}
|
||||
if (overrides.onStopError) {
|
||||
onStopError.mockImplementation(overrides.onStopError);
|
||||
}
|
||||
|
||||
function createTypingHarness(overrides: Partial<Parameters<typeof createTypingCallbacks>[0]> = {}) {
|
||||
const start = overrides.start ?? vi.fn().mockResolvedValue(undefined);
|
||||
const stop = overrides.stop ?? vi.fn().mockResolvedValue(undefined);
|
||||
const onStartError = overrides.onStartError ?? vi.fn();
|
||||
const onStopError = overrides.onStopError ?? vi.fn();
|
||||
const callbacks = createTypingCallbacks({
|
||||
start,
|
||||
stop,
|
||||
onStartError,
|
||||
onStopError,
|
||||
...(onStopError ? { onStopError } : {}),
|
||||
...(overrides.maxConsecutiveFailures !== undefined
|
||||
? { maxConsecutiveFailures: overrides.maxConsecutiveFailures }
|
||||
: {}),
|
||||
|
||||
@@ -227,7 +227,7 @@ function createTelegramOutboundPlugin() {
|
||||
};
|
||||
to: string;
|
||||
text: string;
|
||||
accountId?: string | null;
|
||||
accountId?: string;
|
||||
mediaUrl?: string;
|
||||
},
|
||||
mediaUrl?: string,
|
||||
|
||||
@@ -10,20 +10,6 @@ import type { ChannelChoice } from "./onboard-types.js";
|
||||
import { getChannelOnboardingAdapter } from "./onboarding/registry.js";
|
||||
import type { ChannelOnboardingAdapter } from "./onboarding/types.js";
|
||||
|
||||
type ChannelOnboardingAdapterPatch = Partial<
|
||||
Pick<
|
||||
ChannelOnboardingAdapter,
|
||||
"configure" | "configureInteractive" | "configureWhenConfigured" | "getStatus"
|
||||
>
|
||||
>;
|
||||
|
||||
type PatchedOnboardingAdapterFields = {
|
||||
configure?: ChannelOnboardingAdapter["configure"];
|
||||
configureInteractive?: ChannelOnboardingAdapter["configureInteractive"];
|
||||
configureWhenConfigured?: ChannelOnboardingAdapter["configureWhenConfigured"];
|
||||
getStatus?: ChannelOnboardingAdapter["getStatus"];
|
||||
};
|
||||
|
||||
export function setDefaultChannelPluginRegistryForTests(): void {
|
||||
const channels = [
|
||||
{ pluginId: "discord", plugin: discordPlugin, source: "test" },
|
||||
@@ -36,46 +22,23 @@ export function setDefaultChannelPluginRegistryForTests(): void {
|
||||
setActivePluginRegistry(createTestRegistry(channels));
|
||||
}
|
||||
|
||||
export function patchChannelOnboardingAdapter(
|
||||
export function patchChannelOnboardingAdapter<K extends keyof ChannelOnboardingAdapter>(
|
||||
channel: ChannelChoice,
|
||||
patch: ChannelOnboardingAdapterPatch,
|
||||
patch: Pick<ChannelOnboardingAdapter, K>,
|
||||
): () => void {
|
||||
const adapter = getChannelOnboardingAdapter(channel);
|
||||
if (!adapter) {
|
||||
throw new Error(`missing onboarding adapter for ${channel}`);
|
||||
}
|
||||
|
||||
const previous: PatchedOnboardingAdapterFields = {};
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(patch, "getStatus")) {
|
||||
previous.getStatus = adapter.getStatus;
|
||||
adapter.getStatus = patch.getStatus ?? adapter.getStatus;
|
||||
const keys = Object.keys(patch) as K[];
|
||||
const previous = {} as Pick<ChannelOnboardingAdapter, K>;
|
||||
for (const key of keys) {
|
||||
previous[key] = adapter[key];
|
||||
adapter[key] = patch[key];
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(patch, "configure")) {
|
||||
previous.configure = adapter.configure;
|
||||
adapter.configure = patch.configure ?? adapter.configure;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(patch, "configureInteractive")) {
|
||||
previous.configureInteractive = adapter.configureInteractive;
|
||||
adapter.configureInteractive = patch.configureInteractive;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(patch, "configureWhenConfigured")) {
|
||||
previous.configureWhenConfigured = adapter.configureWhenConfigured;
|
||||
adapter.configureWhenConfigured = patch.configureWhenConfigured;
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (Object.prototype.hasOwnProperty.call(patch, "getStatus")) {
|
||||
adapter.getStatus = previous.getStatus!;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(patch, "configure")) {
|
||||
adapter.configure = previous.configure!;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(patch, "configureInteractive")) {
|
||||
adapter.configureInteractive = previous.configureInteractive;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(patch, "configureWhenConfigured")) {
|
||||
adapter.configureWhenConfigured = previous.configureWhenConfigured;
|
||||
for (const key of keys) {
|
||||
adapter[key] = previous[key];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -84,14 +84,12 @@ function createTelegramCfg(botToken: string, enabled?: boolean): OpenClawConfig
|
||||
|
||||
function patchTelegramAdapter(overrides: Parameters<typeof patchChannelOnboardingAdapter>[1]) {
|
||||
return patchChannelOnboardingAdapter("telegram", {
|
||||
getStatus: vi.fn(async ({ cfg }: { cfg: OpenClawConfig }) => ({
|
||||
channel: "telegram",
|
||||
configured: Boolean(cfg.channels?.telegram?.botToken),
|
||||
statusLines: [],
|
||||
})),
|
||||
...overrides,
|
||||
getStatus:
|
||||
overrides.getStatus ??
|
||||
vi.fn(async ({ cfg }: { cfg: OpenClawConfig }) => ({
|
||||
channel: "telegram",
|
||||
configured: Boolean(cfg.channels?.telegram?.botToken),
|
||||
statusLines: [],
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { CONTEXT_WINDOW_HARD_MIN_TOKENS } from "../agents/context-window-guard.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import {
|
||||
applyCustomApiConfig,
|
||||
@@ -79,13 +78,13 @@ function expectOpenAiCompatResult(params: {
|
||||
|
||||
function buildCustomProviderConfig(contextWindow?: number) {
|
||||
if (contextWindow === undefined) {
|
||||
return {} as OpenClawConfig;
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
models: {
|
||||
providers: {
|
||||
custom: {
|
||||
api: "openai-completions" as const,
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://llm.example.com/v1",
|
||||
models: [
|
||||
{
|
||||
@@ -101,7 +100,7 @@ function buildCustomProviderConfig(contextWindow?: number) {
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
};
|
||||
}
|
||||
|
||||
function applyCustomModelConfigWithContextWindow(contextWindow?: number) {
|
||||
|
||||
@@ -220,8 +220,6 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"Disables Chromium sandbox isolation flags for environments where sandboxing fails at runtime. Keep this off whenever possible because process isolation protections are reduced.",
|
||||
"browser.attachOnly":
|
||||
"Restricts browser mode to attach-only behavior without starting local browser processes. Use this when all browser sessions are externally managed by a remote CDP provider.",
|
||||
"browser.cdpPortRangeStart":
|
||||
"Starting local CDP port used for auto-allocated browser profile ports. Increase this when host-level port defaults conflict with other local services.",
|
||||
"browser.defaultProfile":
|
||||
"Default browser profile name selected when callers do not explicitly choose a profile. Use a stable low-privilege profile as the default to reduce accidental cross-context state use.",
|
||||
"browser.profiles":
|
||||
|
||||
@@ -105,7 +105,6 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"browser.headless": "Browser Headless Mode",
|
||||
"browser.noSandbox": "Browser No-Sandbox Mode",
|
||||
"browser.attachOnly": "Browser Attach-only Mode",
|
||||
"browser.cdpPortRangeStart": "Browser CDP Port Range Start",
|
||||
"browser.defaultProfile": "Browser Default Profile",
|
||||
"browser.profiles": "Browser Profiles",
|
||||
"browser.profiles.*.cdpPort": "Browser Profile CDP Port",
|
||||
|
||||
@@ -48,8 +48,6 @@ export type BrowserConfig = {
|
||||
noSandbox?: boolean;
|
||||
/** If true: never launch; only attach to an existing browser. Default: false */
|
||||
attachOnly?: boolean;
|
||||
/** Starting local CDP port for auto-assigned browser profiles. Default derives from gateway port. */
|
||||
cdpPortRangeStart?: number;
|
||||
/** Default profile to use when profile param is omitted. Default: "chrome" */
|
||||
defaultProfile?: string;
|
||||
/** Named browser profiles with explicit CDP ports or URLs. */
|
||||
|
||||
@@ -250,7 +250,6 @@ export const OpenClawSchema = z
|
||||
headless: z.boolean().optional(),
|
||||
noSandbox: z.boolean().optional(),
|
||||
attachOnly: z.boolean().optional(),
|
||||
cdpPortRangeStart: z.number().int().min(1).max(65535).optional(),
|
||||
defaultProfile: z.string().optional(),
|
||||
snapshotDefaults: BrowserSnapshotDefaultsSchema,
|
||||
ssrfPolicy: z
|
||||
|
||||
@@ -81,7 +81,7 @@ describe("runCronIsolatedAgentTurn — cron model override (#21057)", () => {
|
||||
// Hold onto the cron session *object* — the code may reassign its
|
||||
// `sessionEntry` property (e.g. during skills snapshot refresh), so
|
||||
// checking a stale reference would give a false negative.
|
||||
let cronSession: ReturnType<typeof makeCronSession>;
|
||||
let cronSession: { sessionEntry: ReturnType<typeof makeFreshSessionEntry>; [k: string]: unknown };
|
||||
|
||||
beforeEach(() => {
|
||||
previousFastTestEnv = clearFastTestEnv();
|
||||
@@ -103,7 +103,7 @@ describe("runCronIsolatedAgentTurn — cron model override (#21057)", () => {
|
||||
|
||||
cronSession = makeCronSession({
|
||||
sessionEntry: makeFreshSessionEntry(),
|
||||
});
|
||||
}) as { sessionEntry: ReturnType<typeof makeFreshSessionEntry>; [k: string]: unknown };
|
||||
resolveCronSessionMock.mockReturnValue(cronSession);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { vi, type Mock } from "vitest";
|
||||
import { vi } from "vitest";
|
||||
|
||||
type CronSessionEntry = {
|
||||
sessionId: string;
|
||||
updatedAt: number;
|
||||
systemSent: boolean;
|
||||
skillsSnapshot: unknown;
|
||||
model?: string;
|
||||
modelProvider?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
@@ -19,27 +17,23 @@ type CronSession = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
function createMock(): Mock {
|
||||
return vi.fn();
|
||||
}
|
||||
|
||||
export const buildWorkspaceSkillSnapshotMock = createMock();
|
||||
export const resolveAgentConfigMock = createMock();
|
||||
export const resolveAgentModelFallbacksOverrideMock = createMock();
|
||||
export const resolveAgentSkillsFilterMock = createMock();
|
||||
export const getModelRefStatusMock = createMock();
|
||||
export const isCliProviderMock = createMock();
|
||||
export const resolveAllowedModelRefMock = createMock();
|
||||
export const resolveConfiguredModelRefMock = createMock();
|
||||
export const resolveHooksGmailModelMock = createMock();
|
||||
export const resolveThinkingDefaultMock = createMock();
|
||||
export const runWithModelFallbackMock = createMock();
|
||||
export const runEmbeddedPiAgentMock = createMock();
|
||||
export const runCliAgentMock = createMock();
|
||||
export const getCliSessionIdMock = createMock();
|
||||
export const updateSessionStoreMock = createMock();
|
||||
export const resolveCronSessionMock = createMock();
|
||||
export const logWarnMock = createMock();
|
||||
export const buildWorkspaceSkillSnapshotMock = vi.fn();
|
||||
export const resolveAgentConfigMock = vi.fn();
|
||||
export const resolveAgentModelFallbacksOverrideMock = vi.fn();
|
||||
export const resolveAgentSkillsFilterMock = vi.fn();
|
||||
export const getModelRefStatusMock = vi.fn();
|
||||
export const isCliProviderMock = vi.fn();
|
||||
export const resolveAllowedModelRefMock = vi.fn();
|
||||
export const resolveConfiguredModelRefMock = vi.fn();
|
||||
export const resolveHooksGmailModelMock = vi.fn();
|
||||
export const resolveThinkingDefaultMock = vi.fn();
|
||||
export const runWithModelFallbackMock = vi.fn();
|
||||
export const runEmbeddedPiAgentMock = vi.fn();
|
||||
export const runCliAgentMock = vi.fn();
|
||||
export const getCliSessionIdMock = vi.fn();
|
||||
export const updateSessionStoreMock = vi.fn();
|
||||
export const resolveCronSessionMock = vi.fn();
|
||||
export const logWarnMock = vi.fn();
|
||||
|
||||
vi.mock("../../agents/agent-scope.js", () => ({
|
||||
resolveAgentConfig: resolveAgentConfigMock,
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, type Mock, vi } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { saveExecApprovals } from "../infra/exec-approvals.js";
|
||||
import type { ExecHostResponse } from "../infra/exec-host.js";
|
||||
import { handleSystemRunInvoke, formatSystemRunAllowlistMissMessage } from "./invoke-system-run.js";
|
||||
import type { HandleSystemRunInvokeOptions } from "./invoke-system-run.js";
|
||||
|
||||
type MockedRunCommand = Mock<HandleSystemRunInvokeOptions["runCommand"]>;
|
||||
type MockedRunViaMacAppExecHost = Mock<HandleSystemRunInvokeOptions["runViaMacAppExecHost"]>;
|
||||
type MockedSendInvokeResult = Mock<HandleSystemRunInvokeOptions["sendInvokeResult"]>;
|
||||
type MockedSendExecFinishedEvent = Mock<HandleSystemRunInvokeOptions["sendExecFinishedEvent"]>;
|
||||
type MockedSendNodeEvent = Mock<HandleSystemRunInvokeOptions["sendNodeEvent"]>;
|
||||
|
||||
describe("formatSystemRunAllowlistMissMessage", () => {
|
||||
it("returns legacy allowlist miss message by default", () => {
|
||||
@@ -41,7 +34,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
|
||||
}
|
||||
|
||||
function expectInvokeOk(
|
||||
sendInvokeResult: MockedSendInvokeResult,
|
||||
sendInvokeResult: ReturnType<typeof vi.fn>,
|
||||
params?: { payloadContains?: string },
|
||||
) {
|
||||
expect(sendInvokeResult).toHaveBeenCalledWith(
|
||||
@@ -55,7 +48,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
|
||||
}
|
||||
|
||||
function expectInvokeErrorMessage(
|
||||
sendInvokeResult: MockedSendInvokeResult,
|
||||
sendInvokeResult: ReturnType<typeof vi.fn>,
|
||||
params: { message: string; exact?: boolean },
|
||||
) {
|
||||
expect(sendInvokeResult).toHaveBeenCalledWith(
|
||||
@@ -69,8 +62,8 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
|
||||
}
|
||||
|
||||
function expectApprovalRequiredDenied(params: {
|
||||
sendNodeEvent: MockedSendNodeEvent;
|
||||
sendInvokeResult: MockedSendInvokeResult;
|
||||
sendNodeEvent: ReturnType<typeof vi.fn>;
|
||||
sendInvokeResult: ReturnType<typeof vi.fn>;
|
||||
}) {
|
||||
expect(params.sendNodeEvent).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
@@ -132,7 +125,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
|
||||
}
|
||||
|
||||
function expectCommandPinnedToCanonicalPath(params: {
|
||||
runCommand: MockedRunCommand;
|
||||
runCommand: ReturnType<typeof vi.fn>;
|
||||
expected: string;
|
||||
commandTail: string[];
|
||||
cwd?: string;
|
||||
@@ -153,50 +146,23 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
|
||||
security?: "full" | "allowlist";
|
||||
ask?: "off" | "on-miss" | "always";
|
||||
approved?: boolean;
|
||||
runCommand?: HandleSystemRunInvokeOptions["runCommand"];
|
||||
runViaMacAppExecHost?: HandleSystemRunInvokeOptions["runViaMacAppExecHost"];
|
||||
sendInvokeResult?: HandleSystemRunInvokeOptions["sendInvokeResult"];
|
||||
sendExecFinishedEvent?: HandleSystemRunInvokeOptions["sendExecFinishedEvent"];
|
||||
sendNodeEvent?: HandleSystemRunInvokeOptions["sendNodeEvent"];
|
||||
runCommand?: ReturnType<typeof vi.fn>;
|
||||
runViaMacAppExecHost?: ReturnType<typeof vi.fn>;
|
||||
sendInvokeResult?: ReturnType<typeof vi.fn>;
|
||||
sendExecFinishedEvent?: ReturnType<typeof vi.fn>;
|
||||
sendNodeEvent?: ReturnType<typeof vi.fn>;
|
||||
skillBinsCurrent?: () => Promise<Array<{ name: string; resolvedPath: string }>>;
|
||||
}): Promise<{
|
||||
runCommand: MockedRunCommand;
|
||||
runViaMacAppExecHost: MockedRunViaMacAppExecHost;
|
||||
sendInvokeResult: MockedSendInvokeResult;
|
||||
sendNodeEvent: MockedSendNodeEvent;
|
||||
sendExecFinishedEvent: MockedSendExecFinishedEvent;
|
||||
}> {
|
||||
const runCommand: MockedRunCommand = vi.fn<HandleSystemRunInvokeOptions["runCommand"]>(
|
||||
async () => createLocalRunResult(),
|
||||
);
|
||||
const runViaMacAppExecHost: MockedRunViaMacAppExecHost = vi.fn<
|
||||
HandleSystemRunInvokeOptions["runViaMacAppExecHost"]
|
||||
>(async () => params.runViaResponse ?? null);
|
||||
const sendInvokeResult: MockedSendInvokeResult = vi.fn<
|
||||
HandleSystemRunInvokeOptions["sendInvokeResult"]
|
||||
>(async () => {});
|
||||
const sendNodeEvent: MockedSendNodeEvent = vi.fn<HandleSystemRunInvokeOptions["sendNodeEvent"]>(
|
||||
async () => {},
|
||||
);
|
||||
const sendExecFinishedEvent: MockedSendExecFinishedEvent = vi.fn<
|
||||
HandleSystemRunInvokeOptions["sendExecFinishedEvent"]
|
||||
>(async () => {});
|
||||
|
||||
if (params.runCommand !== undefined) {
|
||||
runCommand.mockImplementation(params.runCommand);
|
||||
}
|
||||
if (params.runViaMacAppExecHost !== undefined) {
|
||||
runViaMacAppExecHost.mockImplementation(params.runViaMacAppExecHost);
|
||||
}
|
||||
if (params.sendInvokeResult !== undefined) {
|
||||
sendInvokeResult.mockImplementation(params.sendInvokeResult);
|
||||
}
|
||||
if (params.sendNodeEvent !== undefined) {
|
||||
sendNodeEvent.mockImplementation(params.sendNodeEvent);
|
||||
}
|
||||
if (params.sendExecFinishedEvent !== undefined) {
|
||||
sendExecFinishedEvent.mockImplementation(params.sendExecFinishedEvent);
|
||||
}
|
||||
}) {
|
||||
const runCommand =
|
||||
params.runCommand ??
|
||||
vi.fn(async (_command: string[], _cwd?: string, _env?: Record<string, string>) =>
|
||||
createLocalRunResult(),
|
||||
);
|
||||
const runViaMacAppExecHost =
|
||||
params.runViaMacAppExecHost ?? vi.fn(async () => params.runViaResponse ?? null);
|
||||
const sendInvokeResult = params.sendInvokeResult ?? vi.fn(async () => {});
|
||||
const sendExecFinishedEvent = params.sendExecFinishedEvent ?? vi.fn(async () => {});
|
||||
const sendNodeEvent = params.sendNodeEvent ?? vi.fn(async () => {});
|
||||
|
||||
await handleSystemRunInvoke({
|
||||
client: {} as never,
|
||||
@@ -224,13 +190,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
|
||||
preferMacAppExecHost: params.preferMacAppExecHost,
|
||||
});
|
||||
|
||||
return {
|
||||
runCommand,
|
||||
runViaMacAppExecHost,
|
||||
sendInvokeResult,
|
||||
sendNodeEvent,
|
||||
sendExecFinishedEvent,
|
||||
};
|
||||
return { runCommand, runViaMacAppExecHost, sendInvokeResult, sendExecFinishedEvent };
|
||||
}
|
||||
|
||||
it("uses local execution by default when mac app exec host preference is disabled", async () => {
|
||||
|
||||
@@ -395,21 +395,4 @@ describe("pairing store", () => {
|
||||
expect(scoped).toEqual(["1002", "1001"]);
|
||||
});
|
||||
});
|
||||
|
||||
it("uses default-account allowFrom when account id is omitted", async () => {
|
||||
await withTempStateDir(async (stateDir) => {
|
||||
await writeAllowFromFixture({ stateDir, channel: "telegram", allowFrom: ["1001"] });
|
||||
await writeAllowFromFixture({
|
||||
stateDir,
|
||||
channel: "telegram",
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
allowFrom: ["1002"],
|
||||
});
|
||||
|
||||
const asyncScoped = await readChannelAllowFromStore("telegram", process.env);
|
||||
const syncScoped = readChannelAllowFromStoreSync("telegram", process.env);
|
||||
expect(asyncScoped).toEqual(["1002", "1001"]);
|
||||
expect(syncScoped).toEqual(["1002", "1001"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -225,10 +225,6 @@ function shouldIncludeLegacyAllowFromEntries(normalizedAccountId: string): boole
|
||||
return !normalizedAccountId || normalizedAccountId === DEFAULT_ACCOUNT_ID;
|
||||
}
|
||||
|
||||
function resolveAllowFromAccountId(accountId?: string): string {
|
||||
return normalizePairingAccountId(accountId) || DEFAULT_ACCOUNT_ID;
|
||||
}
|
||||
|
||||
function normalizeId(value: string | number): string {
|
||||
return String(value).trim();
|
||||
}
|
||||
@@ -399,9 +395,10 @@ export async function readLegacyChannelAllowFromStore(
|
||||
export async function readChannelAllowFromStore(
|
||||
channel: PairingChannel,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
accountId?: string,
|
||||
accountId: string,
|
||||
): Promise<string[]> {
|
||||
const resolvedAccountId = resolveAllowFromAccountId(accountId);
|
||||
const normalizedAccountId = accountId.trim().toLowerCase();
|
||||
const resolvedAccountId = normalizedAccountId || DEFAULT_ACCOUNT_ID;
|
||||
|
||||
if (!shouldIncludeLegacyAllowFromEntries(resolvedAccountId)) {
|
||||
return await readNonDefaultAccountAllowFrom({
|
||||
@@ -430,9 +427,10 @@ export function readLegacyChannelAllowFromStoreSync(
|
||||
export function readChannelAllowFromStoreSync(
|
||||
channel: PairingChannel,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
accountId?: string,
|
||||
accountId: string,
|
||||
): string[] {
|
||||
const resolvedAccountId = resolveAllowFromAccountId(accountId);
|
||||
const normalizedAccountId = accountId.trim().toLowerCase();
|
||||
const resolvedAccountId = normalizedAccountId || DEFAULT_ACCOUNT_ID;
|
||||
|
||||
if (!shouldIncludeLegacyAllowFromEntries(resolvedAccountId)) {
|
||||
return readNonDefaultAccountAllowFromSync({
|
||||
|
||||
@@ -3,7 +3,6 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { SecretProviderConfig } from "../config/types.secrets.js";
|
||||
import { resolveSecretRefString, resolveSecretRefValue } from "./resolve.js";
|
||||
|
||||
async function writeSecureFile(filePath: string, content: string, mode = 0o600): Promise<void> {
|
||||
@@ -29,7 +28,7 @@ describe("secret ref resolver", () => {
|
||||
|
||||
function createProviderConfig(
|
||||
providerId: string,
|
||||
provider: SecretProviderConfig,
|
||||
provider: Record<string, unknown>,
|
||||
): OpenClawConfig {
|
||||
return {
|
||||
secrets: {
|
||||
@@ -43,7 +42,7 @@ describe("secret ref resolver", () => {
|
||||
async function resolveWithProvider(params: {
|
||||
ref: Parameters<typeof resolveSecretRefString>[0];
|
||||
providerId: string;
|
||||
provider: SecretProviderConfig;
|
||||
provider: Record<string, unknown>;
|
||||
}) {
|
||||
return await resolveSecretRefString(params.ref, {
|
||||
config: createProviderConfig(params.providerId, params.provider),
|
||||
@@ -53,17 +52,17 @@ describe("secret ref resolver", () => {
|
||||
function createExecProvider(
|
||||
command: string,
|
||||
overrides?: Record<string, unknown>,
|
||||
): SecretProviderConfig {
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
source: "exec",
|
||||
command,
|
||||
passEnv: ["PATH"],
|
||||
...overrides,
|
||||
} as SecretProviderConfig;
|
||||
};
|
||||
}
|
||||
|
||||
async function expectExecResolveRejects(
|
||||
provider: SecretProviderConfig,
|
||||
provider: Record<string, unknown>,
|
||||
message: string,
|
||||
): Promise<void> {
|
||||
await expect(
|
||||
|
||||
@@ -72,7 +72,7 @@ async function runMemberCase(args: MemberCaseArgs = {}): Promise<void> {
|
||||
}
|
||||
|
||||
describe("registerSlackMemberEvents", () => {
|
||||
const cases: Array<{ name: string; args: MemberCaseArgs; calls: number }> = [
|
||||
it.each([
|
||||
{
|
||||
name: "enqueues DM member events when dmPolicy is open",
|
||||
args: { overrides: { dmPolicy: "open" } },
|
||||
@@ -112,8 +112,7 @@ describe("registerSlackMemberEvents", () => {
|
||||
},
|
||||
calls: 0,
|
||||
},
|
||||
];
|
||||
it.each(cases)("$name", async ({ args, calls }) => {
|
||||
])("$name", async ({ args, calls }) => {
|
||||
await runMemberCase(args);
|
||||
expect(memberMocks.enqueue).toHaveBeenCalledTimes(calls);
|
||||
});
|
||||
|
||||
@@ -87,7 +87,7 @@ async function runMessageCase(input: MessageCase = {}): Promise<void> {
|
||||
}
|
||||
|
||||
describe("registerSlackMessageEvents", () => {
|
||||
const cases: Array<{ name: string; input: MessageCase; calls: number }> = [
|
||||
it.each([
|
||||
{
|
||||
name: "enqueues message_changed system events when dmPolicy is open",
|
||||
input: { overrides: { dmPolicy: "open" }, event: makeChangedEvent() },
|
||||
@@ -130,8 +130,7 @@ describe("registerSlackMessageEvents", () => {
|
||||
},
|
||||
calls: 0,
|
||||
},
|
||||
];
|
||||
it.each(cases)("$name", async ({ input, calls }) => {
|
||||
])("$name", async ({ input, calls }) => {
|
||||
await runMessageCase(input);
|
||||
expect(messageQueueMock).toHaveBeenCalledTimes(calls);
|
||||
});
|
||||
|
||||
@@ -75,36 +75,32 @@ async function runPinCase(input: PinCase = {}): Promise<void> {
|
||||
}
|
||||
|
||||
describe("registerSlackPinEvents", () => {
|
||||
const cases: Array<{ name: string; args: PinCase; expectedCalls: number }> = [
|
||||
{
|
||||
name: "enqueues DM pin system events when dmPolicy is open",
|
||||
args: { overrides: { dmPolicy: "open" } },
|
||||
expectedCalls: 1,
|
||||
},
|
||||
{
|
||||
name: "blocks DM pin system events when dmPolicy is disabled",
|
||||
args: { overrides: { dmPolicy: "disabled" } },
|
||||
expectedCalls: 0,
|
||||
},
|
||||
{
|
||||
name: "blocks DM pin system events for unauthorized senders in allowlist mode",
|
||||
args: {
|
||||
it.each([
|
||||
["enqueues DM pin system events when dmPolicy is open", { overrides: { dmPolicy: "open" } }, 1],
|
||||
[
|
||||
"blocks DM pin system events when dmPolicy is disabled",
|
||||
{ overrides: { dmPolicy: "disabled" } },
|
||||
0,
|
||||
],
|
||||
[
|
||||
"blocks DM pin system events for unauthorized senders in allowlist mode",
|
||||
{
|
||||
overrides: { dmPolicy: "allowlist", allowFrom: ["U2"] },
|
||||
event: makePinEvent({ user: "U1" }),
|
||||
},
|
||||
expectedCalls: 0,
|
||||
},
|
||||
{
|
||||
name: "allows DM pin system events for authorized senders in allowlist mode",
|
||||
args: {
|
||||
0,
|
||||
],
|
||||
[
|
||||
"allows DM pin system events for authorized senders in allowlist mode",
|
||||
{
|
||||
overrides: { dmPolicy: "allowlist", allowFrom: ["U1"] },
|
||||
event: makePinEvent({ user: "U1" }),
|
||||
},
|
||||
expectedCalls: 1,
|
||||
},
|
||||
{
|
||||
name: "blocks channel pin events for users outside channel users allowlist",
|
||||
args: {
|
||||
1,
|
||||
],
|
||||
[
|
||||
"blocks channel pin events for users outside channel users allowlist",
|
||||
{
|
||||
overrides: {
|
||||
dmPolicy: "open",
|
||||
channelType: "channel",
|
||||
@@ -112,10 +108,9 @@ describe("registerSlackPinEvents", () => {
|
||||
},
|
||||
event: makePinEvent({ channel: "C1", user: "U_ATTACKER" }),
|
||||
},
|
||||
expectedCalls: 0,
|
||||
},
|
||||
];
|
||||
it.each(cases)("$name", async ({ args, expectedCalls }) => {
|
||||
0,
|
||||
],
|
||||
])("%s", async (_name, args: PinCase, expectedCalls: number) => {
|
||||
await runPinCase(args);
|
||||
expect(pinEnqueueMock).toHaveBeenCalledTimes(expectedCalls);
|
||||
});
|
||||
|
||||
@@ -78,20 +78,20 @@ async function executeReactionCase(input: ReactionRunInput = {}) {
|
||||
}
|
||||
|
||||
describe("registerSlackReactionEvents", () => {
|
||||
const cases: Array<{ name: string; input: ReactionRunInput; expectedCalls: number }> = [
|
||||
it.each([
|
||||
{
|
||||
name: "enqueues DM reaction system events when dmPolicy is open",
|
||||
input: { overrides: { dmPolicy: "open" } },
|
||||
args: { overrides: { dmPolicy: "open" } },
|
||||
expectedCalls: 1,
|
||||
},
|
||||
{
|
||||
name: "blocks DM reaction system events when dmPolicy is disabled",
|
||||
input: { overrides: { dmPolicy: "disabled" } },
|
||||
args: { overrides: { dmPolicy: "disabled" } },
|
||||
expectedCalls: 0,
|
||||
},
|
||||
{
|
||||
name: "blocks DM reaction system events for unauthorized senders in allowlist mode",
|
||||
input: {
|
||||
args: {
|
||||
overrides: { dmPolicy: "allowlist", allowFrom: ["U2"] },
|
||||
event: buildReactionEvent({ user: "U1" }),
|
||||
},
|
||||
@@ -99,7 +99,7 @@ describe("registerSlackReactionEvents", () => {
|
||||
},
|
||||
{
|
||||
name: "allows DM reaction system events for authorized senders in allowlist mode",
|
||||
input: {
|
||||
args: {
|
||||
overrides: { dmPolicy: "allowlist", allowFrom: ["U1"] },
|
||||
event: buildReactionEvent({ user: "U1" }),
|
||||
},
|
||||
@@ -107,8 +107,8 @@ describe("registerSlackReactionEvents", () => {
|
||||
},
|
||||
{
|
||||
name: "enqueues channel reaction events regardless of dmPolicy",
|
||||
input: {
|
||||
handler: "removed",
|
||||
args: {
|
||||
handler: "removed" as const,
|
||||
overrides: { dmPolicy: "disabled", channelType: "channel" },
|
||||
event: {
|
||||
...buildReactionEvent({ channel: "C1" }),
|
||||
@@ -119,7 +119,7 @@ describe("registerSlackReactionEvents", () => {
|
||||
},
|
||||
{
|
||||
name: "blocks channel reaction events for users outside channel users allowlist",
|
||||
input: {
|
||||
args: {
|
||||
overrides: {
|
||||
dmPolicy: "open",
|
||||
channelType: "channel",
|
||||
@@ -129,10 +129,8 @@ describe("registerSlackReactionEvents", () => {
|
||||
},
|
||||
expectedCalls: 0,
|
||||
},
|
||||
];
|
||||
|
||||
it.each(cases)("$name", async ({ input, expectedCalls }) => {
|
||||
await executeReactionCase(input);
|
||||
])("$name", async ({ args, expectedCalls }) => {
|
||||
await executeReactionCase(args);
|
||||
expect(reactionQueueMock).toHaveBeenCalledTimes(expectedCalls);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user