mirror of
https://github.com/ValueCell-ai/ClawX.git
synced 2026-08-14 17:02:22 +00:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ddbba55c3 | ||
|
|
9cadb552d9 | ||
|
|
77c5d45b63 | ||
|
|
47108a7249 | ||
|
|
3a63b10bd5 | ||
|
|
2abe824afd | ||
|
|
c0794bb3b9 | ||
|
|
242a532aa4 | ||
|
|
5621164d1b | ||
|
|
0072082e31 |
@@ -23,7 +23,6 @@ jobs:
|
||||
- windows-latest
|
||||
env:
|
||||
CI: 'true'
|
||||
CLAWX_E2E_WORKERS: '2'
|
||||
# Linux runners cannot use Electron's setuid chrome-sandbox; harmless on macOS/Windows.
|
||||
ELECTRON_DISABLE_SANDBOX: '1'
|
||||
|
||||
|
||||
@@ -87,4 +87,3 @@ resources/openclaw-plugins/skillshub/
|
||||
|
||||
.opencode
|
||||
.superpowers
|
||||
.playwright-mcp
|
||||
@@ -30,7 +30,6 @@ Standard dev commands are in `package.json` scripts and `README.md`. Key ones:
|
||||
- **pnpm version**: The exact pnpm version is pinned via `packageManager` in `package.json`. Use `corepack enable && corepack prepare` to activate the correct version before installing.
|
||||
- **Electron on headless Linux**: The dbus errors (`Failed to connect to the bus`) are expected and harmless in a headless/cloud environment. The app still runs fine with `$DISPLAY` set (e.g., `:1` via Xvfb/VNC).
|
||||
- **Performance profiling**: `pnpm run perf:chat` writes synthetic Renderer/Main CPU profiles and versioned metrics under ignored Playwright `test-results/`. For live Renderer CDP use `CLAWX_REMOTE_DEBUGGING_PORT=9223 pnpm dev`; for live Main inspection use `pnpm run profile:main` and port 9229.
|
||||
- **E2E parallel isolation**: Functional Electron specs run concurrently with `CLAWX_E2E_WORKERS=2` by default. Keep tests parallel-safe and test-scoped; apply `E2E_EXCLUSIVE_TAG` from `tests/e2e/parallel-policy.ts` to tests that use the real clipboard or other OS-global state, and `E2E_PERFORMANCE_TAG` to host performance profiles. Extend `tests/unit/e2e-parallel-policy.test.ts` for recognizable new global APIs.
|
||||
- **`pnpm run lint` race condition**: If `pnpm run uv:download` was recently run, ESLint may fail with `ENOENT: no such file or directory, scandir '/workspace/temp_uv_extract'` because the temp directory was created and removed during download. Simply re-run lint after the download script finishes.
|
||||
- **Build scripts warning**: `pnpm install` may warn about ignored build scripts for `@discordjs/opus` and `koffi`. These are optional messaging-channel dependencies and the warnings are safe to ignore.
|
||||
- **`pnpm run init`**: This is a convenience script that runs `pnpm install` followed by `pnpm run uv:download`. Either run `pnpm run init` or run the two steps separately.
|
||||
|
||||
+366
-98
@@ -10,7 +10,8 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#clawxを選ぶ理由">ClawXを選ぶ理由</a> •
|
||||
<a href="#機能">機能</a> •
|
||||
<a href="#なぜclawxなのか">なぜClawXなのか</a> •
|
||||
<a href="#はじめに">はじめに</a> •
|
||||
<a href="#アーキテクチャ">アーキテクチャ</a> •
|
||||
<a href="#開発">開発</a> •
|
||||
@@ -36,78 +37,133 @@
|
||||
|
||||
## 概要
|
||||
|
||||
**ClawX**は、強力なAIエージェントと日常のユーザーとの間のギャップを埋めます。[OpenClaw](https://github.com/OpenClaw)をベースに構築されており、コマンドラインによるAIオーケストレーションを、使いやすく美しいデスクトップ体験に変換します。ターミナルは必要ありません。
|
||||
**ClawX**は、強力なAIエージェントと日常のユーザーとの間のギャップを埋めます。[OpenClaw](https://github.com/OpenClaw)をベースに構築されており、コマンドラインによるAIオーケストレーションを、アクセスしやすく美しいデスクトップ体験に変換します。ターミナルは不要です。
|
||||
|
||||
ワークフローの自動化、AI搭載チャネルの管理、インテリジェントなタスクのスケジューリングなど、ClawXはAIエージェントを効果的に活用するために必要なインターフェースを提供します。
|
||||
|
||||
ClawXにはベストプラクティスに基づくモデルプロバイダーがあらかじめ設定されており、Windowsと多言語設定をネイティブにサポートしています。高度な設定は **設定 → 詳細設定 → 開発者モード** から調整できます。
|
||||
ClawXはベストプラクティスのモデルプロバイダーが事前設定されており、Windowsおよび多言語設定をネイティブにサポートしています。もちろん、**設定 → 詳細設定 → 開発者モード**から高度な設定を微調整することもできます。
|
||||
|
||||
<p align="center"><strong style="font-size:1.1em; text-decoration: underline;">完全なエンタープライズ版、専用サービスサポート、またはビジネスシナリオに合わせた導入支援が必要な場合は、<a href="mailto:public@valuecell.ai">public@valuecell.ai</a> までお問い合わせください。</strong></p>
|
||||
<p align="center"><strong style="font-size:1.1em; text-decoration: underline;">完全なエンタープライズ版、専用のサービスサポート、または御社のビジネスシナリオに合わせた導入支援が必要な場合は、<a href="mailto:public@valuecell.ai">public@valuecell.ai</a> までお問い合わせください。</strong></p>
|
||||
|
||||
---
|
||||
## スクリーンショット
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><img src="resources/screenshot/jp/chat.png" alt="Chat"><br><em>チャット</em></td>
|
||||
<td align="center"><img src="resources/screenshot/jp/cron.png" alt="Cron"><br><em>スケジュールタスク</em></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><img src="resources/screenshot/jp/skills.png" alt="Skills"><br><em>スキル</em></td>
|
||||
<td align="center"><img src="resources/screenshot/jp/channels.png" alt="Channels"><br><em>チャネル</em></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><img src="resources/screenshot/jp/models.png" alt="Models"><br><em>モデル</em></td>
|
||||
<td align="center"><img src="resources/screenshot/jp/settings.png" alt="Settings"><br><em>設定</em></td>
|
||||
</tr>
|
||||
</table>
|
||||
<p align="center">
|
||||
<img src="resources/screenshot/jp/chat.png" style="width: 100%; height: auto;">
|
||||
</p>
|
||||
|
||||
## ClawXを選ぶ理由
|
||||
<p align="center">
|
||||
<img src="resources/screenshot/jp/cron.png" style="width: 100%; height: auto;">
|
||||
</p>
|
||||
|
||||
AIエージェントの構築にコマンドラインの習得は不要であるべきです。ClawXはシンプルな哲学のもとに設計されました:**強力な技術には、あなたの時間を尊重するインターフェースがふさわしい。** ClawXは公式の **OpenClaw** コアを直接ベースに構築されています。別途インストールする必要はなく、ランタイムをアプリケーション内に組み込むことで、シームレスな「すべて込み」の体験を提供します。上流のOpenClawと緊密に連携し、公式の最新機能、安定性の改善、エコシステムとの互換性を利用できるようにしています。
|
||||
<p align="center">
|
||||
<img src="resources/screenshot/jp/skills.png" style="width: 100%; height: auto;">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="resources/screenshot/jp/channels.png" style="width: 100%; height: auto;">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="resources/screenshot/jp/models.png" style="width: 100%; height: auto;">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="resources/screenshot/jp/settings.png" style="width: 100%; height: auto;">
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## なぜClawXなのか
|
||||
|
||||
AIエージェントの構築にコマンドラインの習得は不要であるべきです。ClawXはシンプルな哲学のもとに設計されました:**強力な技術には、あなたの時間を尊重するインターフェースがふさわしい。**
|
||||
|
||||
| 課題 | ClawXのソリューション |
|
||||
|------|----------------------|
|
||||
| 複雑なCLIセットアップ | ガイド付きセットアップウィザードによるワンクリックインストール |
|
||||
| 設定ファイル | リアルタイム検証付きのビジュアル設定 |
|
||||
| プロセス管理 | Gatewayライフサイクルの自動管理 |
|
||||
| アプリの更新 | 起動時に更新を確認し、ダウンロードまたはインストール前に通知 |
|
||||
| 複雑なCLIセットアップ | ワンクリックインストールとガイド付きセットアップウィザード |
|
||||
| 設定ファイル | リアルタイムバリデーション付きのビジュアル設定 |
|
||||
| プロセス管理 | ゲートウェイライフサイクルの自動管理 |
|
||||
| アプリ更新 | 起動時に更新を確認し、ダウンロードやインストール前に通知 |
|
||||
| 複数のAIプロバイダー | 統合プロバイダー設定パネル |
|
||||
| スキル/プラグインのインストール | オプションの拡張機能マーケットプレイスにも対応したローカル優先のスキル管理 |
|
||||
| スキル/プラグインのインストール | 組み込みのスキルマーケットプレイスと管理機能 |
|
||||
|
||||
### 機能
|
||||
### OpenClaw内蔵
|
||||
|
||||
- **🎯 ゼロ設定バリア**:直感的なグラフィカルインターフェースでセットアップを完了できます。ターミナルコマンド、YAMLファイル、環境変数の探索は不要です。
|
||||
- **💬 インテリジェントチャットインターフェース**:複数セッションのコンテキストと履歴、シンタックスハイライト付きストリーミングMarkdown、CJK対応解析、テーブル、KaTeX数式、`@agent` による直接ルーティング、インライン `/skill` カード、ワークスペース優先のセッション、Markdown・`.docx`・`.pptx`・ローカルHTMLの読み取り専用プレビューに対応します。
|
||||
- **📡 マルチチャネル管理**:複数アカウント、アカウント単位のAgent紐付け、既定アカウントの切り替え、Tencent公式個人WeChatチャネルプラグインを備えた独立したAIチャネルを設定・監視できます。
|
||||
- **⏰ Cronベースの自動化**:繰り返しまたは1回限りのスケジュールを定義し、スケジュール済みプロンプトにスキルを挿入し、結果を外部チャネルへ配信できます。
|
||||
- **🧩 拡張可能なスキルシステム**:Gatewayに依存せずスキルをローカルで管理できます。複数のOpenClawソースからスキルを検出し、`pdf`、`xlsx`、`docx`、`pptx` の文書処理スキルも利用できます。
|
||||
- **🔐 セキュアなプロバイダー統合**:OpenAI、Anthropic、Z.AI / GLMなどに接続し、認証情報をOSのネイティブキーチェーンに安全に保存できます。OAuth、カスタムプロバイダー、画像生成エンドポイント、互換性フォールバックにも対応します。
|
||||
- **🌙 アダプティブテーマ**:ライト、ダーク、システム同期テーマを選択できます。
|
||||
- **🚀 自動起動設定**:**設定 → 一般** で **システム起動時に自動起動** を有効にできます。
|
||||
- **🔔 更新通知**:起動時に新しいバージョンを確認し、ダウンロードまたはインストールするかを選択できます。
|
||||
ClawXは公式の**OpenClaw**コアを直接ベースに構築されています。別途インストールを必要とせず、アプリケーション内にランタイムを組み込むことで、シームレスな「バッテリー同梱」体験を提供します。
|
||||
|
||||
> 機能の詳細は [docs/ja-JP/features.md](docs/ja-JP/features.md) を参照してください。
|
||||
私たちはアップストリームのOpenClawプロジェクトとの厳密な整合性を維持することにコミットしており、公式リリースが提供する最新の機能、安定性の改善、エコシステムの互換性に常にアクセスできることを保証します。
|
||||
|
||||
### 主なユースケース
|
||||
---
|
||||
|
||||
- **🤖 パーソナルAIアシスタント**:質問への回答、メールの下書き、ドキュメントの要約、日常タスクの支援を行う汎用AIエージェントを、クリーンなデスクトップインターフェースから設定できます。
|
||||
- **📊 自動モニタリング**:ニュースフィード、価格、特定のイベントを監視するスケジュールエージェントを設定し、結果を希望する通知チャネルへ届けられます。
|
||||
- **💻 開発者の生産性向上**:AIを開発ワークフローに統合し、コードレビュー、ドキュメント生成、繰り返しのコーディング作業を行えます。
|
||||
- **🔄 ワークフロー自動化**:複数のスキルをビジュアルな自動化パイプラインに組み合わせ、データ処理、コンテンツ変換、アクションの実行を行えます。
|
||||
## 機能
|
||||
|
||||
### 🎯 ゼロ設定バリア
|
||||
インストールから最初のAIインタラクションまで、すべてのセットアップを直感的なグラフィカルインターフェースで完了できます。ターミナルコマンド不要、YAMLファイル不要、環境変数の探索も不要です。
|
||||
|
||||
### 💬 インテリジェントチャットインターフェース
|
||||
モダンなチャット体験を通じてAIエージェントとコミュニケーションできます。複数の会話コンテキスト、メッセージ履歴に加え、シンタックスハイライト付きのフェンスコード、CJK 対応の解析、GitHub 風テーブル、KaTeX による LaTeX 数式(`$インライン$`、`$$ブロック$$`、`\(インライン\)`、`\[ブロック\]`)を含む、エージェント応答のストリーミング Markdown レンダリングをサポートします。ユーザー入力は常にプレーンテキストとして表示します。さらに、マルチエージェント構成ではメイン入力欄の `@agent` から対象エージェントへ直接ルーティングできます。フェンスコードはソースの改行を保持し、長い行はソフトラップされ、ストリーミング完了後にローカライズされたコピー操作を利用できます。
|
||||
コンポーザーから挿入した Skill は `/skill-name` 形式のチップとして表示され、チップをクリックすると右側のプレビュー側欄でその Skill の `SKILL.md` を開けます。
|
||||
`@agent` で別のエージェントを選ぶと、ClawX はデフォルトエージェントを経由せず、そのエージェント自身の会話コンテキストへ直接切り替えます。各エージェントのワークスペースは既定で分離されていますが、より強い実行時分離は OpenClaw の sandbox 設定に依存します。
|
||||
セッション側欄はワークスペース優先で整理され、既定ワークスペースを先頭に固定し、その他のワークスペースは自然順に並べます。各ワークスペースは折りたたみや追加読み込みができます。AI の返信中は行にスピナーが表示され、未確認の返信が完了すると青い点に変わり、会話を開くと相対アクティビティ時刻に戻ります。ホバーすると引き続き操作ボタンが表示されます。インポートしたワークスペースは側欄の見出しから名前を変更でき、新しい名前はチャット入力欄の下にも反映されます。見出しにホバーすると引き続きファイルシステムのパスを確認できます。選択中の会話に有効なワークスペースがある場合、新しいチャットはそれを引き継ぎ、最初の送信までは変更できます。編集可能な新規または未バインドのチャットでは、コンポーザーのワークスペースチップから最近使用したワークスペースと既存セッションのワークスペースの一覧を開き、既定ワークスペースへ戻すか別フォルダーを選べます。保存済みのワークスペースフォルダーが移動または削除されている場合、Chat はセッション作成を一時停止し、無効なパスを繰り返し再試行せずに既存のフォルダーを選ぶよう案内します。利用できない既定以外のグループには側欄で印が付き、確認後に削除できます。この操作ではグループ内の全セッションが完全に削除されます。セッション行の削除と画面遷移は完全削除が成功した後にのみ行われ、失敗した場合は会話と確認ダイアログが保持されるため再試行できます。OpenClaw が生成する UUID と日付のフォールバックタイトルは、そのセッション ID と一致する場合に限って欠落タイトルとして扱い、セッション名として保存せず、会話の最初のユーザーメッセージに置き換えて表示します。
|
||||
各 Agent は `provider/model` の実行時設定を個別に上書きできます。上書きしていない Agent は引き続きグローバルの既定モデルを継承します。
|
||||
コンポーザーのモデルメニューでは、Off、Low、Medium、High の4段階の推論強度を専用サブメニューから直接選択できます。カスタムモデルで新しいチャットを作成した場合も、最初の送信前は Gateway が公開する現在の Agent の既定値を使ってこのコントロールを表示し、セッション作成後はセッションメタデータを優先します。選択した強度は現在のセッションに適用され、各 ACP prompt にそのまま渡されます。Off を選ぶと個別の「思考」トグルなしで推論を無効にできます。Provider が応答前にリクエストを予期せず中断した場合、チャットはターンを黙って終了せず、再試行可能なエラーを表示します。
|
||||
|
||||
Chat の右パネルにあるワークスペースとプレビューの各タブでは、Markdown、`.docx`、`.pptx` ファイルを読み取り専用でプレビューできます。Markdown ファイルのプレビューでは、同じシンタックスハイライト、ソフトラップ、コピー操作付きのフェンスコード、CJK 対応の解析、KaTeX 数式を静的レンダリングモードでサポートします。プレビューのヘッダーから選択中のファイルを ClawX の表示領域全体に拡大でき、同じボタンまたは Esc で右パネルへ戻れます。従来形式の `.doc` と `.ppt` はアプリ内ではプレビューせず、引き続き OS 経由で開きます。DOCX のページ区切りは Microsoft Word と異なる場合があり、PPTX プレビューではアニメーション、画面切り替え、メディア再生をサポートしません。20 MB を超える Office ファイルはアプリ内でプレビューされません。
|
||||
|
||||
### ローカル HTML プレビュー
|
||||
Chat の右パネルにはワークスペース、プレビュー、変更だけがあり、汎用ウェブブラウザ、ホーム画面、アドレスバーはありません。許可済みのローカル `.html` / `.htm` 添付ファイル、ファイルアクティビティ、ワークスペースファイルは既定でプレビューに開きます。ファイル操作では ClawX 内蔵プレビューまたはシステムアプリを選択でき、プレビューのヘッダーから現在の HTML ファイルをシステムブラウザで開くこともできます。
|
||||
|
||||
すべてのリンクはクリックできません。ClawX が描画するリンクは通常のテキストとして表示され、HTML プレビュー内のリンクからもリンク装飾とポインター操作が除去されます。フォーム、スクリプトによる移動、リダイレクト、ページ内移動、ポップアップ、ダウンロード、ネットワーク要求、デバイス権限もブロックされます。自己完結したローカル HTML は表示できますが、選択中の文書から移動することはできません。
|
||||
|
||||
### 📡 マルチチャネル管理
|
||||
複数のAIチャネルを同時に設定・監視できます。各チャネルは独立して動作するため、異なるタスクに特化したエージェントを実行できます。
|
||||
現在は各チャンネルで複数アカウントを扱え、Channels ページでアカウントの Agent 紐付けやデフォルトアカウント切替を直接管理できます。
|
||||
カスタムのチャンネルアカウント ID には、ルーティング不一致を防ぐため OpenClaw 互換の正規形式(`[a-z0-9_-]`、英小文字、最大 64 文字、先頭は英小文字または数字)を必須にしています。
|
||||
ClawX には Tencent 公式の個人 WeChat チャンネルプラグインも同梱されており、Channels ページからアプリ内 QR フローで直接 WeChat を連携できます。
|
||||
|
||||
### ⏰ Cronベースの自動化
|
||||
AIタスクを自動的に実行するようスケジュール設定できます。トリガーを定義し、間隔を設定することで、手動介入なしにAIエージェントを24時間稼働させることができます。
|
||||
定期タスク画面では外部配信を「送信アカウント」と「受信先ターゲット」の 2 段階セレクターで設定できるようになりました。対応チャネルでは、受信先候補をチャネルのディレクトリ機能や既知セッション履歴から自動検出するため、`jobs.json` を手で編集する必要はありません。タスクのメッセージ入力欄でも、メインのチャット入力と同じインライン `/skill` トークン記法でスキルを挿入できるようになりました(選択中のエージェントに応じて読み込み)。スケジュールされたプロンプトから直接スキルを起動できます。スケジュール選択は**繰り返し**と**1回のみ**のタブに分かれました。繰り返しは毎時・毎日・平日・毎週・カスタム(生の cron)の頻度を時刻/曜日コントロール付きで選べ、1回のみは選択した日付(曜日を表示)と時刻に一度だけ実行します。1回のみのタスクは未来の時刻を指定する必要があり、実行後はランタイムにより自動的に削除されます。
|
||||
|
||||
|
||||
### 🧩 拡張可能なスキルシステム
|
||||
事前構築されたスキルでAIエージェントを拡張できます。統合 Skills ページはローカル優先で、管理ディレクトリや workspace のスキルをスキャンし、Gateway に依存せず有効/無効を切り替えられます。エンタープライズ拡張がある場合は、その拡張が提供する marketplace も表示できます。
|
||||
ClawX はドキュメント処理スキル(`pdf`、`xlsx`、`docx`、`pptx`)もフル内容で同梱し、起動時に管理スキルディレクトリ(既定 `~/.openclaw/skills`)へ自動配備し、初回インストール時に既定で有効化します。
|
||||
Skills ページでは OpenClaw の複数ソース(管理ディレクトリ、workspace、追加スキルディレクトリ)から検出されたスキルを表示でき、各スキルの実際のパスを確認して実フォルダを直接開けます。OpenClaw 同梱の bundled skill については、コミュニティ版ではパッケージにも表示にも `skill-creator` のみを残し、dev 起動時と packaged 起動時の両方で他の bundled skill を物理的に削除します。さらに、削除済み bundled skill の古い `openclaw.json` エントリも一緒に掃除します。
|
||||
|
||||
### 🔐 セキュアなプロバイダー統合
|
||||
複数のAIプロバイダー(OpenAI、Anthropic、Z.AI / GLMなど)に接続でき、資格情報はシステムのネイティブキーチェーンに安全に保存されます。OpenAI は API キーとブラウザ OAuth(Codex サブスクリプション)の両方に対応しています。
|
||||
開発者モードでは、専用の Image Generation ページで、独立した OpenAI 互換の画像生成エンドポイント(Base URL、API キー、`gpt-image-2` などのモデル名)を設定でき、画像生成だけ専用の `/v1/images/generations` サービスを使い、チャットは通常の OpenAI Provider のまま継続できます。
|
||||
OpenAI-compatible ゲートウェイを **Custom プロバイダー** で使う場合、**設定 → AI Providers → Provider 編集** でカスタム `User-Agent` を設定でき、互換性が必要なエンドポイントで有効です。
|
||||
プロバイダーの編集や切り替え時、ClawX は `input: ["text", "image"]` など既存のモデル単位の能力メタデータを保持します。新しく選択した Custom プロバイダーのモデルには OpenClaw onboarding と同等の画像入力推論を適用し、不明なモデルはテキスト専用として扱います。
|
||||
Custom プロバイダーのモデル行には明示的な `contextWindow` も書き込まれ(モデルファミリーから推定、例:`gpt-5.x` → 272k)、旧バージョンで保存された行は起動時に自動補完されます。これにより OpenClaw は長いセッションを "Context overflow" エラーになる前に圧縮できます。Custom プロバイダーのプライマリモデルは推論を有効化し、Low / Medium / High の固定ラダーを同期するため、チャットでセッションの thinking を設定できます。ClawX はモデル名から別のレベル一覧を推測しません。compaction 未設定の場合は `agents.defaults.compaction.mode = "safeguard"` と `reserveTokensFloor = 50000` が既定値として設定されますが、ユーザーが設定したフィールドは上書きされず、欠けている推論可能なフィールドだけが補完されます。
|
||||
Z.AI(CN / Global)は OpenClaw 組み込みの `zai` プロバイダー(`ZAI_API_KEY`)に対応し、既定モデルは `glm-5.2` です。Code Plan プリセットで Coding Plan エンドポイント(`…/api/coding/paas/v4`)へ切り替え、通常 API(`…/api/paas/v4`)も利用できます。CN と Global は同じ OpenClaw ランタイムキーを共有するため同時追加できません。
|
||||
互換ゲートウェイで `/models` が認証以外の理由で使えない場合、ClawX は API キー検証時に設定済みモデルを使った軽量な `/chat/completions` または `/responses` プローブへ自動フォールバックします。
|
||||
|
||||
### 🌙 アダプティブテーマ
|
||||
ライトモード、ダークモード、またはシステム同期テーマ。ClawXはあなたの好みに自動的に適応します。
|
||||
|
||||
### 🚀 自動起動設定
|
||||
**設定 → 通用** から **システム起動時に自動起動** を有効化すると、ログイン後に ClawX が自動的に起動します。
|
||||
|
||||
### 🔔 更新通知
|
||||
ClawX は起動時に新しいバージョンを自動確認できます。更新が見つかるとアプリ内通知を表示し、ダウンロードやインストールはユーザーが選択した後にのみ実行されます。
|
||||
|
||||
---
|
||||
|
||||
## はじめに
|
||||
|
||||
### システム要件
|
||||
|
||||
- **オペレーティングシステム**:macOS 11以上、Windows 10以上、またはLinux(Ubuntu 20.04以上)
|
||||
- **メモリ**:最低4GB RAM(8GB推奨)
|
||||
- **ストレージ**:1GBの空きディスク容量
|
||||
- **オペレーティングシステム**: macOS 11以上、Windows 10以上、またはLinux(Ubuntu 20.04以上)
|
||||
- **メモリ**: 最低4GB RAM(8GB推奨)
|
||||
- **ストレージ**: 1GBの空きディスク容量
|
||||
|
||||
### インストール
|
||||
|
||||
#### ビルド済みリリース(推奨)
|
||||
|
||||
[Releases](https://github.com/ValueCell-ai/ClawX/releases) ページから、お使いのプラットフォーム向けの最新リリースをダウンロードしてください。
|
||||
[Releases](https://github.com/ValueCell-ai/ClawX/releases)ページから、お使いのプラットフォーム向けの最新リリースをダウンロードしてください。
|
||||
|
||||
#### ソースからビルド
|
||||
|
||||
@@ -116,126 +172,338 @@ AIエージェントの構築にコマンドラインの習得は不要である
|
||||
git clone https://github.com/ValueCell-ai/ClawX.git
|
||||
cd ClawX
|
||||
|
||||
# プロジェクトを初期化
|
||||
# プロジェクトの初期化
|
||||
pnpm run init
|
||||
|
||||
# 開発モードで起動
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### 初回起動
|
||||
|
||||
ClawXを初めて起動すると、**セットアップウィザード**が次の手順を案内します。
|
||||
ClawXを初めて起動すると、**セットアップウィザード**が以下の手順をガイドします:
|
||||
|
||||
1. **言語と地域**:使用するロケールを設定
|
||||
2. **AIプロバイダー**:ブラウザまたはデバイスログインに対応したプロバイダーでは、APIキーまたはOAuthで追加
|
||||
3. **スキルバンドル**:一般的なユースケース向けの事前設定スキルを選択
|
||||
4. **検証**:メインインターフェースに入る前に設定をテスト
|
||||
1. **言語と地域** – 使用する言語・地域の設定
|
||||
2. **AIプロバイダー** – APIキーまたは OAuth(ブラウザ/デバイスログイン対応プロバイダー)で追加
|
||||
3. **スキルバンドル** – 一般的なユースケース向けの事前設定スキルを選択
|
||||
4. **検証** – メインインターフェースに入る前に設定をテスト
|
||||
|
||||
サポートされている場合、ウィザードはシステム言語を初期選択し、対応していない場合は英語にフォールバックします。
|
||||
サポート対象のシステム言語がある場合、ウィザードはその言語を初期選択し、未対応の場合は英語にフォールバックします。
|
||||
|
||||
> Web検索について:ClawXはAgentとGatewayの両方のポリシーレイヤーで、OpenClawの汎用 `web_search` ツールを無効にします。Moonshot(Kimi)検索も対象です。管理対象のブラウザ自動化と `web_fetch` は引き続き利用できます。
|
||||
>
|
||||
> 内部ツールについて:ClawXは両方のポリシーレイヤーで、Agentに対して `gateway`、`nodes`、`create_goal`、`get_goal`、`update_goal` も無効にします。ClawXアプリケーション自身のGateway RPCに加え、メッセージング、セッションオーケストレーション、Agent検出ツールは引き続き利用できます。
|
||||
> Web searchについて:ClawXは、AgentとGatewayの両方のポリシーレイヤーでOpenClawの汎用`web_search`ツールを無効にします。
|
||||
> Moonshot(Kimi)検索も対象です。管理対象のブラウザ自動化と`web_fetch`は引き続き利用できます。
|
||||
|
||||
### プロキシ設定
|
||||
|
||||
ClawXには、Electron、OpenClaw Gateway、Telegramなどのチャネルがローカルプロキシクライアント経由でインターネットにアクセスする必要がある環境向けの、組み込みプロキシ設定があります。
|
||||
ClawXには、Electron、OpenClaw Gateway、またはTelegramなどのチャネルがローカルプロキシクライアントを介してインターネットにアクセスする必要がある環境向けに、組み込みのプロキシ設定が含まれています。
|
||||
|
||||
**設定 → Gateway → プロキシ**を開き、既定のプロキシ、バイパスルール、開発者モードでのHTTP・HTTPS・`ALL_PROXY` / SOCKSの上書きを設定します。ローカル設定の例は `http://127.0.0.1:7890` です。
|
||||
**設定 → ゲートウェイ → プロキシ**を開いて以下を設定します:
|
||||
|
||||
> プロキシのフォールバック動作、Telegramとの同期、**OpenClaw Doctor**については [docs/ja-JP/proxy-settings.md](docs/ja-JP/proxy-settings.md) を参照してください。
|
||||
- **プロキシサーバー**: すべてのリクエストのデフォルトプロキシ
|
||||
- **バイパスルール**: 直接接続すべきホスト(セミコロン、カンマ、または改行で区切る)
|
||||
- **開発者モード**では、オプションで以下をオーバーライドできます:
|
||||
- **HTTP プロキシ**
|
||||
- **HTTPS プロキシ**
|
||||
- **ALL_PROXY / SOCKS**
|
||||
|
||||
推奨されるローカル設定例:
|
||||
|
||||
```text
|
||||
プロキシサーバー: http://127.0.0.1:7890
|
||||
```
|
||||
注意事項:
|
||||
|
||||
- `host:port`のみの値はHTTPとして扱われます。
|
||||
- 高度なプロキシフィールドが空の場合、ClawXは`プロキシサーバー`にフォールバックします。
|
||||
- プロキシ設定を保存すると、Electronのネットワーク設定が即座に再適用され、ゲートウェイが自動的に再起動されます。
|
||||
- ClawXはTelegramが有効な場合、プロキシをOpenClawのTelegramチャネル設定にも同期します。
|
||||
- ClawXのプロキシが無効な状態では、Gatewayの通常再起動時に既存のTelegramチャネルプロキシ設定を保持します。
|
||||
- OpenClaw設定のTelegramプロキシを明示的に消したい場合は、プロキシ無効の状態で一度「保存」を実行してください。
|
||||
- **設定 → 詳細 → 開発者** では **OpenClaw Doctor** を実行でき、`openclaw doctor --json` の診断出力をアプリ内で確認できます。
|
||||
- Windows のパッケージ版では、同梱された `openclaw` CLI/TUI は端末入力を安定させるため、同梱の `node.exe` エントリーポイント経由で実行されます。
|
||||
|
||||
---
|
||||
|
||||
## アーキテクチャ
|
||||
|
||||
ClawXは **Host API統一レイヤーを備えたデュアルプロセスアーキテクチャ**を採用しています。React Rendererは単一のクライアント抽象を呼び出し、Electron Mainがプロトコル選択、Gatewayのライフサイクル、ACP Chatのstdio bridgeを管理します。
|
||||
ClawXは、**デュアルプロセス + Host API 統一アクセス**構成を採用しています。Renderer は単一クライアント抽象を呼び出し、プロトコル選択とライフサイクルは Main が管理します:
|
||||
|
||||
- **プロセスモデル**:Electron Mainがウィンドウ、Gateway監視、システム統合、更新を管理します。OpenClaw GatewayはAIオーケストレーション、チャネル、スキル機能を提供し、Rendererはローカルエンドポイントへ直接アクセスしません。
|
||||
- **設定の配信**:Gateway実行中は `config.get` / `config.set` を使い、停止中または起動中は解決済みJSON5設定を更新します。通常のプロバイダー、Agent、スキル、モデル変更ではプロセスを置き換えず、認証情報は `secrets.reload` でホットリロードされます。ハートビートが4回連続で失敗した場合は、ライフサイクルで保護された復旧を要求します。
|
||||
- **ACP Chat**:Chat UIは [ACP(Agent Client Protocol)](https://agentclientprotocol.com) を介してOpenClawとやり取りし、頻繁に反復されるOpenClawの前に比較的安定したチャットプロトコル面を確保します。ACPはMainが所有するstdio bridge経由で動作し、設定リロード後の認証済み履歴リプレイ、ページ移動中のストリーミング、Mainが検証したメディア・添付ファイル・ファイルアクティビティに対応します。保護されたGateway再起動によって受理済みターンが中断された場合、パッチ済みOpenClawランタイムは復旧runを元のACP promptへ明示的に関連付け、後続のテキストとツールアクティビティを同じメモリ内ターンで継続します。その後の履歴リプレイでも、永続化されたツール境界をネイティブACP updateとして復元します。
|
||||
- **設計原則**:フロントエンドの単一入口、Mainによるトランスポート管理、再接続・タイムアウト・バックオフによるグレースフルリカバリ、安全なストレージ、CORSセーフな境界を採用しています。
|
||||
OpenClaw の設定配信も Electron Main が一元管理します。Gateway の実行中は `config.get` の正規スナップショットを基準にし、変更を `config.set` でコミットします。Gateway が停止中または起動中の場合は、同じコーディネーターが解決済みの JSON5 設定ファイルだけを更新し、Gateway を起動しません。そのため、通常の Provider、Agent、Channel、バインディング、Skill、モデル変更では Gateway プロセスを置き換えません。完全な再起動は、プロキシなどのプロセス起動環境の変更とユーザーによる明示的な操作に限定されます。確認済みのプロセス終了と WebSocket 切断では、既存の自動再接続経路が引き続き使用されます。WebSocket ハートビートの連続 9 回目までの欠落は診断のみとし、短い pong 遅延で長時間実行中の処理を中断しません。pong または任意のメッセージを受信するとカウントをリセットし、10 回連続で応答がない場合にのみ、ライフサイクル保護付きの Gateway 自動再起動を要求します。認証プロファイルを SQLite に書き込んだ後は OpenClaw の `secrets.reload` を呼び出し、実行中の Agent がプロセス再起動なしで新しい認証情報を読み取れるようにします。
|
||||
|
||||
> プロセス図、設定の調整、ACPファイルアクティビティのセマンティクス、Gatewayのトラブルシューティングについては [docs/ja-JP/architecture.md](docs/ja-JP/architecture.md) を参照してください。
|
||||
Chat は Electron Main が所有する ACP stdio bridge を使用します。Renderer は型付き host event を受け取り、メモリ上の ACP timeline を描画します。Gateway は providers、models、skills、workspace、settings、diagnostics、media configuration などの非 Chat 機能を引き続き担当します。
|
||||
|
||||
別の会話やページを開いても、未完了の ACP 応答はストリーミングを継続します。完了前に戻ると最新のメモリ内 timeline が復元され、ライブ応答の表示が続きます。完了後は通常の ACP 履歴リプレイが引き続き唯一の正となります。
|
||||
|
||||
ACP の assistant ターンにはターン全体の所要時間が表示されます。ライブ計時はクライアントが観測した prompt ライフサイクルに従い、アプリ内を移動しても継続します。履歴の所要時間は Electron Main が範囲を限定した OpenClaw transcript のタイムスタンプから算出し、ACP リプレイですでに復元されたターンだけに付与します。
|
||||
|
||||
ライブの assistant 応答中は、ツール実行が会話表示を妨げないようツール表示を既定で非表示にします。応答が完了すると、連続した呼び出しは 1 つの折りたたみグループとして、単独の呼び出しは詳細を折りたたんだ状態で表示され、必要に応じて展開できます。履歴から復元された完了済みツールも既定で折りたたまれます。
|
||||
|
||||
ACP Chat は標準 ACP resource を添付ファイルとして表示します。ユーザーが選択した画像は、ホバー時のオーバーレイにファイル名を表示するサムネイルとして描画され、その他の利用可能な添付カードはファイル名に続いて、淡色で省略可能なソースパスを表示します。現在の OpenClaw ACP adapter が assistant のメディアを省略した場合も、OpenClaw が永続化した正規メディア情報と明示的な assistant の `MEDIA:` ディレクティブを、transcript 専用メタデータを表示せずに添付カードとして復元できます。現在の workspace 外を含む既存のローカルファイル参照は、プレビューまたはオープンのたびに Electron Main で正確な session と generation に対して再検証されます。AI が生成したプレビュー可能なローカル添付ファイル(20 MB 以下の `.docx` と `.pptx` を含む)は、読み取り専用のアプリ内プレビューを主要操作として維持し、対応アプリで開く操作と Finder、エクスプローラー、またはシステムのファイルマネージャーで表示する操作を副次メニューから利用できます。ローカル HTML 添付ファイルでは、そのメニューの先頭項目が右側のプレビューでファイルを開きます。ここでも Office プレビューには同じ制限があります。`.doc` と `.ppt` はシステムアプリで開く形式のままで、DOCX のページ区切りは Microsoft Word と異なる場合があり、PPTX のアニメーション、画面切り替え、メディア再生はサポートされません。対応アプリの検出は macOS と Windows のみで利用でき、Linux または検出失敗時には通知せず、ファイルの場所を表示する操作だけに切り替わります。それ以外のローカルファイル(20 MB を超える Office ファイルを含む)はユーザーのクリック後にシステムアプリで開かれます。ユーザーが選択したフォルダー添付も送信後に利用可能なまま保持され、クリックするとシステムのファイルマネージャーで開きます。ClawX はフォルダー内を読み取りまたはプレビューしません。リモートの HTTP/HTTPS 添付ファイルはクリック後に外部で開かれます。正規メディア情報がない通常の文章内の単独またはインラインパスは添付ファイルとして扱われません。
|
||||
|
||||
ACP Chat は、runtime が画像生成メディアを信頼できる構造化メディアとして配信した場合に、生成画像のプレビューも表示できます。信頼できる OpenClaw internal-UI 配信と画像生成タスクに関連付けられた最終返信では、テキストのみの失敗説明を含む元のユーザー向け完了テキストを保持し、汎用の画像キャプションへ置き換えません。OpenClaw の履歴リプレイ中は、同じセッションで画像生成タスク開始が記録されている場合に限り、assistant の画像 `MEDIA:` マーカーがインライン画像表示へ昇格されます。ClawX は Renderer から任意にファイルシステムへアクセスするのではなく、Electron Main のホストメディア処理を通じてプレビューを読み込みます。標準 ACP の画像と resource コンテンツは引き続き推奨パスであり、そのまま描画されます。
|
||||
|
||||
### ACP ファイルアクティビティのセマンティクス
|
||||
|
||||
- ファイルアクティビティは、成功して完了した OpenClaw の `write`、`edit`、`apply_patch` 呼び出しから投影されます。ツールの認識方法は公式 OpenClaw Chat UI に準拠し、完了した呼び出しだけに絞る処理は ClawX 固有です。
|
||||
- 作成・変更されたアクティビティ行は、プレビュー可能な assistant 添付ファイルと同じファイルカードと**アプリで開く**メニューを使い、状態表示と利用可能な `+/-` 集計も保持します。HTML ファイルでは、メニューの先頭項目が右側の**プレビュー**でファイルを開きます。削除された行には **Changes** 操作だけを残します。アプリ一覧、選択アプリで開く操作、ファイル位置の表示は、workspace ルートと相対パスから Electron Main が毎回個別に再検証します。ツール由来のパスが添付ファイルに変換されたり、Renderer に正規化済みのネイティブパスが渡されたりすることはありません。
|
||||
- `write` はツールが宣言したとおり、作成および全行追加の差分として表示されます。対象パスがすでに存在する可能性がある場合も同様です。
|
||||
- **Changes** は、ツールが宣言したアクティビティを時系列に並べたセッション単位の記録です。Git の出力でも、検証済みソースベースラインに対する差分でもありません。
|
||||
- 各ファイルについて、Changes はアシスタントの各ターンに最大 1 つの diff エディターを表示します。安全に連結できるフラグメントは合成し、独立したフラグメントは 1 つのエディターに連結しますが、完全なファイルベースラインとの差分であるとはみなしません。
|
||||
- シェルコマンド、スクリプト、ユーザー、IDE による副作用は検出されません。
|
||||
- 完全な ACP リプレイからは記録済みのファイルアクティビティを復元できます。リプレイが不完全な場合、ClawX はフォールバック推論で欠落したアクティビティを補いません。
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────┐
|
||||
│ ClawX デスクトップアプリ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Electron メインプロセス │ │
|
||||
│ │ • ウィンドウ&アプリケーションライフサイクル管理 │ │
|
||||
│ │ • ゲートウェイプロセスの監視 │ │
|
||||
│ │ • システム統合(トレイ、通知、キーチェーン) │ │
|
||||
│ │ • 自動アップデートオーケストレーション │ │
|
||||
│ └──────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ │ IPC(権威ある制御プレーン) │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||||
│ │ React レンダラープロセス │ │
|
||||
│ │ • モダンなコンポーネントベースUI(React 19) │ │
|
||||
│ │ • Zustandによるステート管理 │ │
|
||||
│ │ • 統一 host-api/api-client 呼び出し │ │
|
||||
│ │ • 応答はMarkdown、ユーザー入力はプレーンテキスト │ │
|
||||
│ └──────────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────┬─────────────────────────────────────┘
|
||||
│
|
||||
│ 型付き IPC リクエスト
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Main Host Services と Gateway Manager │
|
||||
│ │
|
||||
│ • host:invoke 型付きサービスディスパッチ │
|
||||
│ • 設定、ファイル、セッション、スキル、プロバイダー、診断サービス │
|
||||
│ • Main が Gateway WebSocket とプロセス監視を所有 │
|
||||
└──────────────────────────────┬──────────────────────────────────┘
|
||||
│
|
||||
│ Main 所有 WebSocket
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ OpenClaw ゲートウェイ │
|
||||
│ │
|
||||
│ • AIエージェントランタイムとオーケストレーション │
|
||||
│ • メッセージチャネル管理 │
|
||||
│ • スキル/プラグイン実行環境 │
|
||||
│ • プロバイダー抽象化レイヤー │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
### 設計原則
|
||||
|
||||
- **プロセス分離**: AIランタイムは別プロセスで動作し、重い計算処理中でもUIの応答性を確保します
|
||||
- **フロントエンド呼び出しの単一入口**: Renderer は host-api/api-client を通じて呼び出し、下位プロトコルに依存しません
|
||||
- **Mainによるトランスポート制御**: ACP Chat stdio bridge と Gateway トランスポートは Electron Main が所有し、Renderer は型付き IPC で Main と通信します
|
||||
- **拡張 IPC コントリビューション**: Main プロセス拡張は HTTP route ではなく、型付き IPC レジストリを通じて host-api action を提供します
|
||||
- **グレースフルリカバリ**: 再接続・タイムアウト・バックオフで一時的障害を自動処理します
|
||||
- **セキュアストレージ**: APIキーや機密データは、OSのネイティブセキュアストレージ機構を活用します
|
||||
- **CORSセーフ設計**: Renderer はローカル Gateway や Host API HTTP エンドポイントを直接呼び出しません
|
||||
|
||||
### プロセスモデルと Gateway トラブルシューティング
|
||||
|
||||
- ClawX は Electron アプリのため、**1つのアプリインスタンスでも複数プロセス(main/renderer/zygote/utility)が表示される**のが正常です。
|
||||
- 単一起動保護は Electron のロックに加え、ローカルのプロセスロックファイルも併用し、デスクトップ IPC / セッションバスが不安定な環境でも重複起動を防ぎます。
|
||||
- ローリングアップグレード中に旧版/新版が混在すると、単一起動保護の挙動が非対称になる場合があります。安定運用のため、デスクトップクライアントは可能な限り同一バージョンへ揃えてください。
|
||||
- ただし OpenClaw Gateway の待受は常に**単一**であるべきです。`127.0.0.1:18789` を Listen しているプロセスは1つだけです。
|
||||
- Gateway の readiness は `system-presence`、`health`、`status` などの OpenClaw コア信号を基準にし、memory またはチャネルの失敗はグローバルな Gateway 障害ではなく capability degradation として表示します。
|
||||
- Listen プロセスの確認例:
|
||||
- macOS/Linux: `lsof -nP -iTCP:18789 -sTCP:LISTEN`
|
||||
- Windows (PowerShell): `Get-NetTCPConnection -LocalPort 18789 -State Listen`
|
||||
- ウィンドウの閉じるボタン(`X`)は既定でトレイへ最小化する動作で、完全終了ではありません。完全終了する場合はトレイメニューの **Quit ClawX** を使用してください。
|
||||
|
||||
---
|
||||
|
||||
## ユースケース
|
||||
|
||||
### 🤖 パーソナルAIアシスタント
|
||||
質問への回答、メールの下書き、ドキュメントの要約、日常タスクのサポートなど、汎用的なAIエージェントを設定できます。すべてクリーンなデスクトップインターフェースから操作できます。
|
||||
|
||||
### 📊 自動モニタリング
|
||||
ニュースフィード、価格追跡、特定イベントの監視などを行うスケジュールエージェントを設定できます。結果はお好みの通知チャネルに配信されます。
|
||||
|
||||
### 💻 開発者の生産性向上
|
||||
AI を開発ワークフローに統合できます。エージェントを使用して、コードレビュー、ドキュメント生成、反復的なコーディングタスクの自動化が可能です。
|
||||
|
||||
### 🔄 ワークフロー自動化
|
||||
複数のスキルを連鎖させて、高度な自動化パイプラインを作成できます。データの処理、コンテンツの変換、アクションのトリガーを、すべてビジュアルにオーケストレーションできます。
|
||||
|
||||
---
|
||||
|
||||
## 開発
|
||||
|
||||
### 前提条件
|
||||
|
||||
- **Node.js**:対応するメジャー系列の22.22.3以上、24.15.0以上、または25.9.0以上(Node 24 LTS推奨)
|
||||
- **パッケージマネージャー**:pnpm 9以上(npmも対応)
|
||||
- **Linux(Ubuntu/Debian)**:Electronの実行前に必要なシステムライブラリをインストールしてください。詳細は [docs/ja-JP/development.md](docs/ja-JP/development.md) を参照してください。
|
||||
- **Node.js**: 対応するメジャー系列の 22.22.3以上、24.15.0以上、または25.9.0以上(Node 24 LTS推奨)
|
||||
- **パッケージマネージャー**: pnpm 9以上(推奨)またはnpm
|
||||
- **Linux(Ubuntu/Debian)**: Electron を実行する前に、必要なシステムライブラリをインストールしてください:
|
||||
```bash
|
||||
sudo apt-get install -y libnss3 libgtk-3-0 libxss1 libxtst6 libatspi2.0-0 libnotify4 xdg-utils
|
||||
```
|
||||
Ubuntu 24.04以降では、一部のパッケージに `t64` サフィックスが付いています。上記コマンドを実行すると `apt` が自動的に適切なバリアントを選択します。
|
||||
|
||||
### よく使うコマンド
|
||||
### プロジェクト構成
|
||||
|
||||
```ClawX/
|
||||
├── electron/ # Electron メインプロセス
|
||||
│ ├── services/ # 型付き Host API、Provider/Secrets/ランタイムサービス
|
||||
│ │ ├── providers/ # provider/account モデル同期ロジック
|
||||
│ │ └── secrets/ # OS キーチェーンと秘密情報管理
|
||||
│ ├── shared/ # 共通 Provider スキーマ/定数
|
||||
│ │ └── providers/
|
||||
│ ├── main/ # アプリ入口、ウィンドウ、IPC 登録
|
||||
│ ├── gateway/ # OpenClaw ゲートウェイプロセスマネージャー
|
||||
│ ├── preload/ # セキュア IPC ブリッジ
|
||||
│ └── utils/ # ユーティリティ(ストレージ、認証、パス)
|
||||
├── src/ # React レンダラープロセス
|
||||
│ ├── lib/ # フロントエンド統一 API とエラーモデル
|
||||
│ ├── stores/ # Zustand ストア(settings/chat/gateway)
|
||||
│ ├── components/ # 再利用可能な UI コンポーネント
|
||||
│ ├── pages/ # Setup/Dashboard/Chat/Channels/Skills/Cron/Settings
|
||||
│ ├── i18n/ # ローカライズリソース
|
||||
│ └── types/ # TypeScript 型定義
|
||||
├── tests/
|
||||
│ ├── e2e/ # Playwright による Electron E2E スモークテスト
|
||||
│ └── unit/ # Vitest ユニット/統合寄りテスト
|
||||
├── resources/ # 静的アセット(アイコン、画像)
|
||||
└── scripts/ # ビルド/ユーティリティスクリプト
|
||||
```
|
||||
### 利用可能なコマンド
|
||||
|
||||
```bash
|
||||
pnpm run init # 依存関係をインストールし、バンドルランタイムをダウンロード
|
||||
pnpm dev # ホットリロード付きで開発モードを起動
|
||||
pnpm lint # ESLintを実行
|
||||
pnpm typecheck # TypeScriptを検証
|
||||
pnpm test # ユニットテストを実行
|
||||
pnpm run test:e2e # Electron E2Eスモークテストを実行
|
||||
pnpm build # 本番ビルドを実行
|
||||
pnpm package # 現在のプラットフォーム向けにパッケージ化(:mac / :win / :linux)
|
||||
# 開発
|
||||
pnpm run init # 依存関係のインストール + バンドルバイナリ(uv、agent-browser)のダウンロード
|
||||
pnpm dev # ホットリロードで起動(不足時は同梱スキルを自動準備)
|
||||
|
||||
# コード品質
|
||||
pnpm lint # ESLintを実行
|
||||
pnpm typecheck # TypeScriptの型チェック
|
||||
|
||||
# テスト
|
||||
pnpm test # ユニットテストを実行
|
||||
pnpm run test:e2e # Electron E2E スモークテストを実行
|
||||
pnpm run test:e2e:headed # 表示付きウィンドウで Electron E2E を実行
|
||||
pnpm run perf:chat # 合成 Chat の Renderer/Main CPU プロファイルを取得
|
||||
pnpm run profile:main # ビルド済みアプリを Main inspector の 9229 番ポートで起動
|
||||
pnpm run comms:replay # 通信リプレイ指標を算出
|
||||
pnpm run comms:baseline # 通信ベースラインを更新
|
||||
pnpm run comms:compare # リプレイ指標をベースライン閾値と比較
|
||||
|
||||
# ビルド&パッケージ
|
||||
pnpm run build:vite # フロントエンドのみビルド
|
||||
pnpm build # フルプロダクションビルド(パッケージアセット含む)
|
||||
pnpm package # 現在のプラットフォーム向けにパッケージ化(同梱プリインストールスキルを含む)
|
||||
pnpm package:mac # macOS向けにパッケージ化
|
||||
pnpm package:win # Windows向けにパッケージ化
|
||||
pnpm package:linux # Linux向けにパッケージ化
|
||||
```
|
||||
|
||||
> プロジェクト構成、完全なコマンド一覧、E2Eの並列実行ポリシー、パフォーマンス診断、通信回帰チェック、技術スタックについては [docs/ja-JP/development.md](docs/ja-JP/development.md) を参照してください。
|
||||
ヘッドレス Linux では Electron テストに表示サーバーが必要です。`xvfb-run -a pnpm run test:e2e` を利用してください。
|
||||
|
||||
### Electron パフォーマンス診断
|
||||
|
||||
`pnpm run perf:chat` は隔離された合成 ACP 負荷を実行し、ストリーミング応答と、リッチな静的 Markdown 会話でのサイドバーおよびスクロール操作を測定します。Playwright の `test-results/` には、バージョン付きメトリクスと Renderer/Main CPU プロファイルが出力されます。Renderer プロファイルは本番の store/render 経路とフレームペーシングを対象とします。ストリーミング Main プロファイルは Main から Renderer への IPC fanout を測定し、操作時の Main プロファイルは Renderer 操作中に Main がアイドルのままかを確認します。どちらも上流の OpenClaw/ACP サブプロセスや GPU プロセスの経路は含みません。CPU プロファイルは Chrome DevTools で開けます。アーティファクトには生成されたテスト文字列だけが含まれ、製品テレメトリーには送信されません。測定値はハードウェアに依存するため、共通の絶対閾値ではなく同じマシン上の複数回の結果を比較してください。
|
||||
|
||||
実際の Renderer を記録する場合は `CLAWX_REMOTE_DEBUGGING_PORT=9223 pnpm dev` で開発環境を起動し、Playwright または Chrome DevTools を `localhost:9223` に接続します。Electron Main を記録する場合は `pnpm run profile:main` を実行し、`chrome://inspect` で `localhost:9229` を設定して Electron Main target を選択します。WebSocket trace 自体を測定する場合を除き、`CLAWX_GATEWAY_WS_TRACE` は設定しないでください。
|
||||
|
||||
ClawX は Chromium のハードウェアアクセラレーションを既定で有効なままにし、長い文書、スクロール、レイアウトアニメーションで GPU コンポジットとラスタライズを利用します。グラフィックスドライバーに問題があるマシンでは、Chromium 標準の `--disable-gpu` コマンドラインスイッチをトラブルシューティング用のフォールバックとして利用できます。
|
||||
|
||||
### 通信回帰チェック
|
||||
|
||||
PR が通信経路(Gateway イベント、ACP Chat bridge の送受信フロー、Channel 配信、トランスポートのフォールバック)に触れる場合は、次を実行してください。
|
||||
|
||||
```bash
|
||||
pnpm run comms:replay
|
||||
pnpm run comms:compare
|
||||
```
|
||||
|
||||
CI の `comms-regression` が必須シナリオと閾値を検証します。
|
||||
### 技術スタック
|
||||
|
||||
| レイヤー | 技術 |
|
||||
|---------|------|
|
||||
| ランタイム | Electron 40以上 |
|
||||
| UIフレームワーク | React 19 + TypeScript |
|
||||
| スタイリング | Tailwind CSS + shadcn/ui |
|
||||
| ステート管理 | Zustand |
|
||||
| ビルド | Vite + electron-builder |
|
||||
| テスト | Vitest + Playwright |
|
||||
| アニメーション | Framer Motion |
|
||||
| アイコン | Lucide React |
|
||||
|
||||
---
|
||||
|
||||
## コントリビューション
|
||||
|
||||
コミュニティからの貢献を歓迎します。バグ修正、新機能、ドキュメントの改善、翻訳など、あらゆる貢献がClawXをより良くします。
|
||||
コミュニティからのコントリビューションを歓迎します!バグ修正、新機能、ドキュメントの改善、翻訳など、あらゆる貢献がClawXをより良くするのに役立ちます。
|
||||
|
||||
### 貢献方法
|
||||
### コントリビューション方法
|
||||
|
||||
1. リポジトリを**フォーク**する
|
||||
2. フィーチャーブランチを**作成**する(`git checkout -b feature/amazing-feature`)
|
||||
3. 明確なメッセージで変更を**コミット**する
|
||||
4. ブランチに**プッシュ**する
|
||||
5. **Pull Request**を作成する
|
||||
5. **プルリクエスト**を作成する
|
||||
|
||||
### ガイドライン
|
||||
|
||||
- 既存のコードスタイル(ESLint + Prettier)に従う
|
||||
- 既存のコードスタイルに従う(ESLint + Prettier)
|
||||
- 新機能にはテストを書く
|
||||
- 必要に応じてドキュメントを更新する
|
||||
- コミットはアトミックかつ説明的に保つ
|
||||
|
||||
---
|
||||
|
||||
## 謝辞
|
||||
|
||||
ClawXは次の優れたオープンソースプロジェクトの上に構築されています。
|
||||
ClawXは優れたオープンソースプロジェクトの上に構築されています:
|
||||
|
||||
- [OpenClaw](https://github.com/OpenClaw) - AIエージェントランタイム
|
||||
- [Electron](https://www.electronjs.org/) - クロスプラットフォームデスクトップフレームワーク
|
||||
- [React](https://react.dev/) - UIコンポーネントライブラリ
|
||||
- [shadcn/ui](https://ui.shadcn.com/) - 美しく設計されたコンポーネント
|
||||
- [Zustand](https://github.com/pmndrs/zustand) - 軽量な状態管理
|
||||
- [OpenClaw](https://github.com/OpenClaw) – AIエージェントランタイム
|
||||
- [Electron](https://www.electronjs.org/) – クロスプラットフォームデスクトップフレームワーク
|
||||
- [React](https://react.dev/) – UIコンポーネントライブラリ
|
||||
- [shadcn/ui](https://ui.shadcn.com/) – 美しくデザインされたコンポーネント
|
||||
- [Zustand](https://github.com/pmndrs/zustand) – 軽量ステート管理
|
||||
|
||||
---
|
||||
|
||||
## コミュニティ
|
||||
|
||||
コミュニティに参加して、他のユーザーと交流し、サポートを受け、体験を共有しましょう。
|
||||
コミュニティに参加して、他のユーザーとつながり、サポートを受け、体験を共有しましょう。
|
||||
|
||||
| 企業WeChat | Feishuグループ | Discord |
|
||||
| 企業微信 | Feishuグループ | Discord |
|
||||
| :---: | :---: | :---: |
|
||||
| <img src="src/assets/community/wecom-qr.png" width="150" alt="WeChat QRコード" /> | <img src="src/assets/community/feishu-qr.png" width="150" alt="Feishu QRコード" /> | <img src="src/assets/community/20260212-185822.png" width="150" alt="Discord QRコード" /> |
|
||||
|
||||
### ClawXパートナープログラム
|
||||
### ClawX パートナープログラム 🚀
|
||||
|
||||
ClawXをより多くのお客様、特にカスタムAIエージェントや自動化のニーズを持つお客様に紹介してくださるパートナーを募集しています。
|
||||
ClawX パートナープログラムを開始します。特に、カスタム AI エージェントや自動化ニーズを持つより多くの顧客に ClawX を紹介してくださるパートナーを募集しています。
|
||||
|
||||
パートナーは見込みユーザーやプロジェクトとの接点づくりを担い、ClawXチームは技術サポート、カスタマイズ、統合を全面的に提供します。AIツールや自動化に関心のあるお客様と仕事をされている方は、ぜひご一緒ください。
|
||||
パートナーの皆さまには、見込みユーザーや案件との接点づくりを担っていただき、ClawX チームは技術サポート、カスタマイズ、統合を全面的に提供します。
|
||||
|
||||
詳細はDM、または [public@valuecell.ai](mailto:public@valuecell.ai) までお問い合わせください。
|
||||
AI ツールや自動化に関心のある顧客とお仕事をされている方は、ぜひご一緒できればうれしいです。
|
||||
|
||||
## Star History
|
||||
詳細は DM いただくか、[public@valuecell.ai](mailto:public@valuecell.ai) までメールでご連絡ください。
|
||||
|
||||
---
|
||||
|
||||
## スター履歴
|
||||
|
||||
<p align="center">
|
||||
<img src="https://api.star-history.com/svg?repos=ValueCell-ai/ClawX&type=Date" alt="Star History Chart" />
|
||||
<img src="https://api.star-history.com/svg?repos=ValueCell-ai/ClawX&type=Date" alt="スター履歴チャート" />
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## ライセンス
|
||||
|
||||
ClawXは [MITライセンス](LICENSE) のもとで公開されています。本ソフトウェアは自由に使用、変更、配布できます。
|
||||
ClawXは[MITライセンス](LICENSE)の下でリリースされています。本ソフトウェアの使用、変更、配布は自由に行えます。
|
||||
|
||||
<hr>
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<sub>ValueCell Teamが❤️を込めて開発</sub>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#features">Features</a> •
|
||||
<a href="#why-clawx">Why ClawX</a> •
|
||||
<a href="#getting-started">Getting Started</a> •
|
||||
<a href="#architecture">Architecture</a> •
|
||||
@@ -36,64 +37,119 @@
|
||||
|
||||
## Overview
|
||||
|
||||
**ClawX** bridges the gap between powerful AI agents and everyday users. Built on top of [OpenClaw](https://github.com/OpenClaw), it transforms command-line AI orchestration into an accessible, beautiful desktop experience - no terminal required.
|
||||
**ClawX** bridges the gap between powerful AI agents and everyday users. Built on top of [OpenClaw](https://github.com/OpenClaw), it transforms command-line AI orchestration into an accessible, beautiful desktop experience—no terminal required.
|
||||
|
||||
Whether you're automating workflows, managing AI-powered channels, or scheduling intelligent tasks, ClawX provides the interface you need to harness AI agents effectively.
|
||||
|
||||
ClawX comes pre-configured with best-practice model providers and natively supports Windows as well as multi-language settings. You can also fine-tune advanced configurations via **Settings -> Advanced -> Developer Mode**.
|
||||
ClawX comes pre-configured with best-practice model providers and natively supports Windows as well as multi-language settings. Of course, you can also fine-tune advanced configurations via **Settings → Advanced → Developer Mode**.
|
||||
|
||||
<p align="center"><strong style="font-size:1.1em; text-decoration: underline;">For a full enterprise edition, dedicated service support, or tailored deployment guidance for your business scenario, contact us at <a href="mailto:public@valuecell.ai">public@valuecell.ai</a>.</strong></p>
|
||||
|
||||
## Screenshots
|
||||
---
|
||||
## Screenshot
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><img src="resources/screenshot/en/chat.png" alt="Chat"><br><em>Chat</em></td>
|
||||
<td align="center"><img src="resources/screenshot/en/cron.png" alt="Cron"><br><em>Scheduled tasks</em></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><img src="resources/screenshot/en/skills.png" alt="Skills"><br><em>Skills</em></td>
|
||||
<td align="center"><img src="resources/screenshot/en/channels.png" alt="Channels"><br><em>Channels</em></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><img src="resources/screenshot/en/models.png" alt="Models"><br><em>Models</em></td>
|
||||
<td align="center"><img src="resources/screenshot/en/settings.png" alt="Settings"><br><em>Settings</em></td>
|
||||
</tr>
|
||||
</table>
|
||||
<p align="center">
|
||||
<img src="resources/screenshot/en/chat.png" style="width: 100%; height: auto;">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="resources/screenshot/en/cron.png" style="width: 100%; height: auto;">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="resources/screenshot/en/skills.png" style="width: 100%; height: auto;">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="resources/screenshot/en/channels.png" style="width: 100%; height: auto;">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="resources/screenshot/en/models.png" style="width: 100%; height: auto;">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="resources/screenshot/en/settings.png" style="width: 100%; height: auto;">
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## Why ClawX
|
||||
|
||||
Building AI agents shouldn't require mastering the command line. ClawX was designed with a simple philosophy: **powerful technology deserves an interface that respects your time.** ClawX is built directly upon the official **OpenClaw** core. Instead of requiring a separate installation, we embed the runtime within the application for a seamless, battery-included experience. We stay closely aligned with upstream OpenClaw so you can benefit from the latest official capabilities, stability improvements, and ecosystem compatibility.
|
||||
Building AI agents shouldn't require mastering the command line. ClawX was designed with a simple philosophy: **powerful technology deserves an interface that respects your time.**
|
||||
|
||||
| Challenge | ClawX Solution |
|
||||
|-----------|----------------|
|
||||
| Complex CLI setup | One-click installation with a guided setup wizard |
|
||||
| Complex CLI setup | One-click installation with guided setup wizard |
|
||||
| Configuration files | Visual settings with real-time validation |
|
||||
| Process management | Automatic Gateway lifecycle management |
|
||||
| Process management | Automatic gateway lifecycle management |
|
||||
| App updates | Startup update checks with a prompt before downloading or installing |
|
||||
| Multiple AI providers | Unified provider configuration panel |
|
||||
| Skill/plugin installation | Local-first skill management with an optional extension-provided marketplace |
|
||||
| Skill/plugin installation | Local-first skill management with optional extension-provided marketplace |
|
||||
|
||||
### Features
|
||||
### OpenClaw Inside
|
||||
|
||||
- **🎯 Zero Configuration Barrier**: Complete setup through an intuitive graphical interface - no terminal commands, YAML files, or environment-variable hunting.
|
||||
- **💬 Intelligent Chat Interface**: Multi-session context and history, streaming Markdown with syntax highlighting, CJK-aware parsing, tables, KaTeX math, direct `@agent` routing, inline `/skill` cards, workspace-first sessions, and read-only previews for Markdown, `.docx`, `.pptx`, and local HTML.
|
||||
- **📡 Multi-Channel Management**: Configure and monitor independent AI channels with multiple accounts, per-account agent binding, default-account switching, and the bundled official Tencent personal WeChat channel plugin.
|
||||
- **⏰ Cron-Based Automation**: Define recurring or one-time schedules, insert skills into scheduled prompts, and deliver results to external channels.
|
||||
- **🧩 Extensible Skill System**: Manage skills locally without depending on the Gateway, discover skills from multiple OpenClaw sources, and use bundled document-processing skills for `pdf`, `xlsx`, `docx`, and `pptx`.
|
||||
- **🔐 Secure Provider Integration**: Connect OpenAI, Anthropic, Z.AI / GLM, and other providers with credentials stored in the native system keychain; supports OAuth, custom providers, image-generation endpoints, and compatibility fallbacks.
|
||||
- **🌙 Adaptive Theming**: Choose light mode, dark mode, or system-synchronized themes.
|
||||
- **🚀 Startup Launch Control**: Enable **Launch at system startup** in **Settings -> General**.
|
||||
- **🔔 Update Prompts**: Check for new versions at startup and choose whether to download or install them.
|
||||
ClawX is built directly upon the official **OpenClaw** core. Instead of requiring a separate installation, we embed the runtime within the application to provide a seamless "battery-included" experience.
|
||||
|
||||
> For full feature details, see [docs/en-US/features.md](docs/en-US/features.md).
|
||||
We are committed to maintaining strict alignment with the upstream OpenClaw project, ensuring that you always have access to the latest capabilities, stability improvements, and ecosystem compatibility provided by the official releases.
|
||||
|
||||
### Typical Use Cases
|
||||
---
|
||||
|
||||
- **🤖 Personal AI Assistant**: Configure a general-purpose AI agent to answer questions, draft emails, summarize documents, and help with everyday tasks from a clean desktop interface.
|
||||
- **📊 Automated Monitoring**: Schedule agents to monitor news feeds, track prices, or watch for specific events, with results delivered to your preferred notification channel.
|
||||
- **💻 Developer Productivity**: Integrate AI into your development workflow for code review, documentation generation, and repetitive coding tasks.
|
||||
- **🔄 Workflow Automation**: Chain multiple skills into visual automation pipelines that process data, transform content, and trigger actions.
|
||||
## Features
|
||||
|
||||
### 🎯 Zero Configuration Barrier
|
||||
Complete the entire setup—from installation to your first AI interaction—through an intuitive graphical interface. No terminal commands, no YAML files, no environment variable hunting.
|
||||
|
||||
### 💬 Intelligent Chat Interface
|
||||
Communicate with AI agents through a modern chat experience. Support for multiple conversation contexts, message history, assistant replies rendered as streaming Markdown with syntax-highlighted fenced code, CJK-aware parsing, GitHub-flavored tables, and KaTeX-powered LaTeX math (`$inline$`, `$$block$$`, `\(inline\)`, and `\[block\]`) while user input remains literal text, and direct `@agent` routing in the main composer for multi-agent setups. Fenced code preserves source line breaks, soft-wraps long lines, and provides a localized copy action after streaming completes.
|
||||
Skills you insert from the composer appear as `/skill-name` chips; click a chip to open the preview sidebar and read that skill's `SKILL.md`.
|
||||
When you target another agent with `@agent`, ClawX switches into that agent's own conversation context directly instead of relaying through the default agent. Agent workspaces stay separate by default, and stronger isolation depends on OpenClaw sandbox settings.
|
||||
The session sidebar is workspace-first: the default workspace stays at the top, other workspaces sort naturally, and each workspace can collapse or load more sessions. A row shows a spinner while the AI is replying, a blue dot when an unseen reply finishes, and its relative activity time after the conversation is opened; hovering still reveals row actions. Imported workspaces can be renamed from their sidebar header; the custom name is reflected in the chat composer while hovering the header still reveals the filesystem path. When available, a new chat inherits the selected conversation's workspace while remaining editable until first send. Editable new or unbound chats expose the composer workspace chip as a small menu that lists recent and known-session workspaces, returns to the default workspace, or chooses another folder. If a saved workspace folder was moved or deleted, Chat pauses session creation and prompts you to choose an existing folder instead of repeatedly retrying the missing path. Unavailable non-default groups are marked in the sidebar and can be removed after confirmation; this permanently deletes every session in that group. A session row is removed and navigation changes only after permanent deletion succeeds; failed deletions leave the conversation and confirmation open for retry. Synthetic OpenClaw UUID-date fallback titles are treated as missing only when they match the session ID, then replaced with the conversation's first user prompt instead of being persisted as the session name.
|
||||
Each agent can also override its own `provider/model` runtime setting; agents without overrides continue inheriting the global default model.
|
||||
The composer model menu opens reasoning effort in a dedicated submenu with four directly selectable levels: Off, Low, Medium, and High. This control is also available on a new custom-model chat before its first message, using the current agent defaults advertised by Gateway until the session is created. A selected level applies to the current session and is forwarded unchanged with each ACP prompt; Off disables reasoning without a separate Thinking toggle. If a provider unexpectedly aborts a prompt before replying, Chat now shows a retryable error instead of silently ending the turn.
|
||||
|
||||
The Workspace and Preview tabs in Chat's right panel provide read-only previews for Markdown, `.docx`, and `.pptx` files. Markdown file previews use the same syntax-highlighted, soft-wrapped, copyable fenced code, CJK-aware parsing, and KaTeX math support in static rendering mode. The Preview header can expand the selected file to the full ClawX viewport; use the same control or Escape to return to the panel. Legacy `.doc` and `.ppt` files continue to open through the operating system instead of inline. DOCX pagination may differ from Microsoft Word, and PPTX previews do not support animations, transitions, or media playback. Office files larger than 20 MB are not previewed inline.
|
||||
|
||||
### Local HTML Preview
|
||||
The Chat right panel has Workspace, Preview, and Changes tabs; it no longer includes a general Web Browser, Home page, or address bar. Authorized local `.html` and `.htm` attachments, file activities, and Workspace files open in Preview by default. Their file actions let you choose the built-in Preview or a system application, and the Preview header can open the current HTML file in the system browser.
|
||||
|
||||
All links are non-clickable. Links rendered by ClawX appear as ordinary text, and links inside HTML Preview have their styling and pointer interaction removed. HTML Preview also blocks forms, script navigation, redirects, hash navigation, popups, downloads, network requests, and device permissions. It can render self-contained local HTML but cannot leave the selected document.
|
||||
|
||||
### 📡 Multi-Channel Management
|
||||
Configure and monitor multiple AI channels simultaneously. Each channel operates independently, allowing you to run specialized agents for different tasks.
|
||||
Each channel now supports multiple accounts, per-account agent binding, and switching the channel default account directly from the Channels page.
|
||||
For custom channel account IDs, ClawX enforces OpenClaw-compatible canonical IDs (`[a-z0-9_-]`, lowercase, max 64 chars, must start with a letter/number) to prevent routing mismatches.
|
||||
ClawX now also bundles Tencent's official personal WeChat channel plugin, so you can link WeChat directly from the Channels page with an in-app QR flow.
|
||||
|
||||
### ⏰ Cron-Based Automation
|
||||
Schedule AI tasks to run automatically. Define triggers, set intervals, and let your AI agents work around the clock without manual intervention.
|
||||
The Cron page now lets you configure external delivery directly in the task form with separate sender-account and recipient-target selectors. For supported channels, recipient targets are discovered automatically from channel directories or known session history, so you no longer need to edit `jobs.json` by hand. The task message field also supports inserting skills with the same inline `/skill` token syntax as the main chat composer (scoped to the selected agent), so scheduled prompts can trigger skills directly. The schedule picker is split into **Recurring** and **Once** tabs: Recurring offers Hourly, Daily, Weekdays, Weekly, and Custom (raw cron) frequencies with inline time/weekday controls, while Once runs the task a single time at a chosen date (with weekday shown) and time. One-time tasks must be scheduled for a future moment and are automatically removed by the runtime once they finish.
|
||||
|
||||
|
||||
### 🧩 Extensible Skill System
|
||||
Extend your AI agents with pre-built skills. The integrated Skills page is local-first: it scans managed/workspace skill directories, lets you enable or disable skills without depending on the Gateway, and can optionally expose an extension-provided marketplace in enterprise builds.
|
||||
ClawX also pre-bundles full document-processing skills (`pdf`, `xlsx`, `docx`, `pptx`), deploys them automatically to the managed skills directory (default `~/.openclaw/skills`) on startup, and enables them by default on first install.
|
||||
The Skills page can display skills discovered from multiple OpenClaw sources (managed dir, workspace, and extra skill dirs), and now shows each skill's actual location so you can open the real folder directly. For bundled OpenClaw skills, community builds now ship and expose only `skill-creator`; non-allowlisted bundled skills are physically trimmed in both dev and packaged startup, and any stale `openclaw.json` entries left behind for those removed bundled skills are pruned.
|
||||
|
||||
### 🔐 Secure Provider Integration
|
||||
Connect to multiple AI providers (OpenAI, Anthropic, Z.AI / GLM, and more) with credentials stored securely in your system's native keychain. OpenAI supports both API key and browser OAuth (Codex subscription) sign-in.
|
||||
In developer mode, the dedicated Image Generation page supports an independent OpenAI-compatible image-generation endpoint (Base URL, API key, and model name such as `gpt-image-2`) so image generation can use a dedicated `/v1/images/generations` service while chat continues using the normal OpenAI provider.
|
||||
For **Custom** providers used with OpenAI-compatible gateways, you can set a custom `User-Agent` in **Settings → AI Providers → Edit Provider** for compatibility-sensitive endpoints.
|
||||
When you edit or switch providers, ClawX preserves existing per-model capability metadata such as `input: ["text", "image"]`. Newly selected Custom-provider models use OpenClaw onboarding-compatible image-input inference, with unknown models defaulting to text-only.
|
||||
Custom-provider model rows also receive an explicit `contextWindow` (inferred from the model family, e.g. `gpt-5.x` → 272k), and rows saved by older versions are backfilled on startup, so OpenClaw can compact long sessions before they fail with "Context overflow" errors. Custom-provider primary models are synced with reasoning enabled and a fixed Low / Medium / High ladder so Chat can offer session thinking controls; ClawX does not invent a different list from the model name. When you have no compaction config, ClawX seeds `agents.defaults.compaction.mode = "safeguard"` and `reserveTokensFloor = 50000`; rows or configs you authored yourself are never modified (except missing inferred fields may be backfilled).
|
||||
Z.AI (CN / Global) maps to OpenClaw's built-in `zai` provider (`ZAI_API_KEY`). Default model is `glm-5.2`. Use the Code Plan preset for Coding Plan endpoints (`…/api/coding/paas/v4`) or the normal API endpoints (`…/api/paas/v4`); CN and Global are mutually exclusive because they share one OpenClaw runtime key.
|
||||
When a compatible gateway rejects `/models` for non-auth reasons, ClawX automatically falls back to a lightweight `/chat/completions` or `/responses` probe using the configured model during API key validation.
|
||||
|
||||
### 🌙 Adaptive Theming
|
||||
Light mode, dark mode, or system-synchronized themes. ClawX adapts to your preferences automatically.
|
||||
|
||||
### 🚀 Startup Launch Control
|
||||
In **Settings → General**, you can enable **Launch at system startup** so ClawX starts automatically after login.
|
||||
|
||||
### 🔔 Update Prompts
|
||||
ClawX can automatically check for new versions on startup. When an update is available, it shows an in-app prompt; downloading and installing only happen after you choose the action.
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -122,67 +178,291 @@ pnpm run init
|
||||
# Start in development mode
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### First Launch
|
||||
|
||||
When you launch ClawX for the first time, the **Setup Wizard** will guide you through:
|
||||
|
||||
1. **Language & Region** - Configure your preferred locale
|
||||
2. **AI Provider** - Add providers with API keys or OAuth for providers that support browser or device login
|
||||
3. **Skill Bundles** - Select pre-configured skills for common use cases
|
||||
4. **Verification** - Test your configuration before entering the main interface
|
||||
1. **Language & Region** – Configure your preferred locale
|
||||
2. **AI Provider** – Add providers with API keys or OAuth (for providers that support browser/device login)
|
||||
3. **Skill Bundles** – Select pre-configured skills for common use cases
|
||||
4. **Verification** – Test your configuration before entering the main interface
|
||||
|
||||
The wizard preselects your system language when it is supported, and falls back to English otherwise.
|
||||
|
||||
> Web search note: ClawX disables OpenClaw's general-purpose `web_search` tool at both the agent and Gateway policy layers. This includes Moonshot (Kimi) search; managed browser automation and `web_fetch` remain available.
|
||||
>
|
||||
> Internal tool note: ClawX also disables `gateway`, `nodes`, `create_goal`, `get_goal`, and `update_goal` for agents at both policy layers. Application-owned Gateway RPCs remain available, as do messaging, session orchestration, and agent discovery tools.
|
||||
> Web search note: ClawX disables OpenClaw's general-purpose `web_search` tool at both the agent and Gateway policy layers.
|
||||
> This includes Moonshot (Kimi) search; managed browser automation and `web_fetch` remain available.
|
||||
|
||||
### Proxy Settings
|
||||
|
||||
ClawX includes built-in proxy settings for Electron, the OpenClaw Gateway, and channels such as Telegram that need to reach the internet through a local proxy client.
|
||||
ClawX includes built-in proxy settings for environments where Electron, the OpenClaw Gateway, or channels such as Telegram need to reach the internet through a local proxy client.
|
||||
|
||||
Open **Settings -> Gateway -> Proxy** to configure the default proxy, bypass rules, and optional developer-mode overrides for HTTP, HTTPS, and `ALL_PROXY` / SOCKS. A local example is `http://127.0.0.1:7890`.
|
||||
Open **Settings → Gateway → Proxy** and configure:
|
||||
|
||||
> For proxy fallback behavior, Telegram synchronization, and **OpenClaw Doctor**, see [docs/en-US/proxy-settings.md](docs/en-US/proxy-settings.md).
|
||||
- **Proxy Server**: the default proxy for all requests
|
||||
- **Bypass Rules**: hosts that should connect directly, separated by semicolons, commas, or new lines
|
||||
- In **Developer Mode**, you can optionally override:
|
||||
- **HTTP Proxy**
|
||||
- **HTTPS Proxy**
|
||||
- **ALL_PROXY / SOCKS**
|
||||
|
||||
Recommended local examples:
|
||||
|
||||
```text
|
||||
Proxy Server: http://127.0.0.1:7890
|
||||
```
|
||||
Notes:
|
||||
|
||||
- A bare `host:port` value is treated as HTTP.
|
||||
- If advanced proxy fields are left empty, ClawX falls back to `Proxy Server`.
|
||||
- Saving proxy settings reapplies Electron networking immediately and restarts the Gateway automatically.
|
||||
- ClawX also syncs the proxy to OpenClaw's Telegram channel config when Telegram is enabled.
|
||||
- Gateway restarts preserve an existing Telegram channel proxy if ClawX proxy is currently disabled.
|
||||
- To explicitly clear Telegram channel proxy from OpenClaw config, save proxy settings with proxy disabled.
|
||||
- In **Settings → Advanced → Developer**, you can run **OpenClaw Doctor** to execute `openclaw doctor --json` and inspect the diagnostic output without leaving the app.
|
||||
- On packaged Windows builds, the bundled `openclaw` CLI/TUI runs via the shipped `node.exe` entrypoint to keep terminal input behavior stable.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
ClawX uses a **dual-process architecture with a unified Host API layer**: the React renderer calls one client abstraction, while Electron Main owns protocol selection, Gateway lifecycle, and the ACP Chat stdio bridge.
|
||||
ClawX employs a **dual-process architecture** with a unified host API layer. The renderer talks to a single client abstraction, while Electron Main owns protocol selection and process lifecycle:
|
||||
|
||||
- **Process model**: Electron Main owns the window, Gateway supervision, system integration, and updates; the OpenClaw Gateway provides AI orchestration, channel, and skill capabilities; the renderer does not access local endpoints directly.
|
||||
- **Configuration delivery**: Main uses `config.get`/`config.set` while the Gateway is running and updates the resolved JSON5 config while it is stopped or starting; ordinary provider, agent, skill, and model changes do not replace the process, credentials are hot-reloaded through `secrets.reload`, and guarded recovery starts after four consecutive heartbeat misses.
|
||||
- **ACP Chat**: Chat UI talks to OpenClaw via [ACP (Agent Client Protocol)](https://agentclientprotocol.com), providing a relatively stable chat protocol surface in front of the rapidly iterating OpenClaw. ACP runs through a Main-owned stdio bridge, supporting authenticated history replay after config reloads, streaming across navigation, and Main-validated media, attachments, and file activity. When a guarded Gateway restart interrupts an accepted turn, the patched OpenClaw runtime explicitly links its recovery run to the original ACP prompt so subsequent text and tool activity continue in the same in-memory turn; later history replay restores persisted tool boundaries as native ACP updates.
|
||||
- **Design principles**: One frontend entry point, Main-owned transport, graceful recovery with reconnect/timeout/backoff, secure storage, and CORS-safe boundaries.
|
||||
Electron Main also owns OpenClaw configuration delivery. While the Gateway is running, ClawX reads the authoritative `config.get` snapshot and commits changes with `config.set`; while it is stopped or starting, the same coordinator updates the resolved JSON5 config file without starting the Gateway. Ordinary provider, agent, channel, binding, skill, and model changes therefore do not replace the Gateway process. Full restarts remain for process-launch environment changes such as proxy settings and explicit user actions. Confirmed process exits and WebSocket closes retain their existing automatic reconnect paths. The first nine consecutive WebSocket heartbeat misses remain diagnostic-only so brief pong delays do not interrupt long-running work; a pong or any incoming message resets the count, while a tenth consecutive miss requests guarded automatic Gateway recovery. Auth-profile SQLite updates use OpenClaw's `secrets.reload` RPC so running agents see new credentials without a process restart.
|
||||
|
||||
> For the process diagram, configuration coordination, ACP file activity semantics, and Gateway troubleshooting, see [docs/en-US/architecture.md](docs/en-US/architecture.md).
|
||||
Chat uses an ACP stdio bridge owned by Electron Main. Renderer receives typed host events and renders an in-memory ACP timeline. Gateway remains responsible for non-Chat capabilities such as providers, models, skills, workspace, settings, diagnostics, and media configuration.
|
||||
|
||||
An unfinished ACP response keeps streaming when you open another conversation or page. Returning before it finishes restores the latest in-memory timeline and continues the live response; once it finishes, normal ACP history replay remains the source of truth.
|
||||
|
||||
ACP assistant turns show whole-turn duration. Live timing follows the client-observed prompt lifecycle and survives in-app navigation; historical timing is derived in Electron Main from bounded OpenClaw transcript timestamps and only annotates a turn already restored by ACP replay.
|
||||
|
||||
While a live assistant response is running, its tool activity stays hidden so tool execution does not interrupt the conversation display. When the response finishes, every tool is shown collapsed by default—consecutive calls as one collapsed group and a single call with collapsed details—and can be expanded on demand. Completed tools restored from history also start collapsed.
|
||||
|
||||
ACP Chat renders standard ACP resources as attachments. User-selected images appear as thumbnails with a filename hover overlay, while other available attachment cards show the filename and a muted, truncating source path. When the current OpenClaw ACP adapter omits assistant media, canonical persisted OpenClaw media facts and explicit assistant `MEDIA:` directives can also be recovered as attachment cards without displaying transcript-only metadata. Existing local file references, including paths outside the active workspace, are revalidated in Electron Main for the exact session and generation before every preview or open. Previewable local attachments produced by the AI, including `.docx` and `.pptx` files within the 20 MB inline-preview limit, keep their primary read-only in-app preview action and provide a secondary menu for opening with compatible applications or revealing the file in Finder, File Explorer, or the system file manager. For local HTML attachments, that menu starts with an action that opens the file in the right-side Preview tab. The same Office limitations apply here: `.doc` and `.ppt` remain system-open formats, DOCX pagination may differ from Microsoft Word, and PPTX animations, transitions, and media playback are unsupported. Compatible-application discovery is available only on macOS and Windows and silently degrades to reveal-only behavior on Linux or when discovery fails. Other local files, including Office files larger than 20 MB, open in the system application after a user click. User-selected folder attachments also remain available after send and open in the system file manager; ClawX does not read or preview their contents. Remote HTTP and HTTPS attachments open externally after a user click. Bare or inline prose paths without canonical media facts are not treated as attachments.
|
||||
|
||||
ACP Chat can also display generated image previews when image-generation media is delivered by the runtime as trusted structured media. Trusted OpenClaw internal-UI deliveries and task-correlated final replies preserve the original user-facing completion text, including text-only failure explanations, rather than replacing it with a generic image caption. During historical OpenClaw replay, assistant image `MEDIA:` markers are promoted to the inline image experience only when they follow a recorded image-generation task start for that session. ClawX loads previews through host media handling in Electron Main, not arbitrary Renderer filesystem access. Standard ACP image and resource content remains the preferred path and renders directly.
|
||||
|
||||
### ACP File Activity Semantics
|
||||
|
||||
- File activity is projected from successful, completed OpenClaw `write`, `edit`, and `apply_patch` calls. Tool recognition follows the official OpenClaw Chat UI; filtering to completed calls is specific to ClawX.
|
||||
- Created and modified activity rows use the same file-card shell and **Open with** menu as previewable assistant attachments while retaining their status and optional `+/-` summary. For HTML files, the first menu item opens the file in the right-side Preview tab. Deleted rows keep only the **Changes** action. Every application-list, selected-application, and reveal request is independently revalidated in Electron Main from the workspace root and relative path; tool-derived paths never become attachments or expose canonical native paths to Renderer.
|
||||
- A `write` is shown as the tool declares it: a creation with an all-added diff, even if the path may already exist.
|
||||
- **Changes** is a chronological, session-level record of tool-declared activity. It is not Git output or a verified diff against a source baseline.
|
||||
- For each file, Changes renders at most one diff editor per assistant turn. Sequential fragments are composed when safe; independent fragments share one concatenated editor without claiming a complete-file baseline.
|
||||
- Side effects made by shell commands, scripts, users, or IDEs are not detected.
|
||||
- A full ACP replay can restore recorded file activity. If replay is incomplete, ClawX does not infer missing activity through fallback behavior.
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ ClawX Desktop App │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Electron Main Process │ │
|
||||
│ │ • Window & application lifecycle management │ │
|
||||
│ │ • Gateway process supervision │ │
|
||||
│ │ • System integration (tray, notifications, keychain) │ │
|
||||
│ │ • Auto-update orchestration │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ │ IPC (authoritative control plane) │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ React Renderer Process │ │
|
||||
│ │ • Modern component-based UI (React 19) │ │
|
||||
│ │ • State management with Zustand │ │
|
||||
│ │ • Unified host-api/api-client calls │ │
|
||||
│ │ • Markdown assistant replies, literal user input │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
│ Typed IPC requests
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ Main Host Services & Gateway Manager │
|
||||
│ │
|
||||
│ • host:invoke typed service dispatcher │
|
||||
│ • Settings, files, sessions, skills, providers, diagnostics │
|
||||
│ • Main-owned Gateway WebSocket and process supervision │
|
||||
└──────────────────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
│ Main-owned WebSocket
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ OpenClaw Gateway │
|
||||
│ │
|
||||
│ • AI agent runtime and orchestration │
|
||||
│ • Message channel management │
|
||||
│ • Skill/plugin execution environment │
|
||||
│ • Provider abstraction layer │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
### Design Principles
|
||||
|
||||
- **Process Isolation**: The AI runtime operates in a separate process, ensuring UI responsiveness even during heavy computation
|
||||
- **Single Entry for Frontend Calls**: Renderer requests go through host-api/api-client; protocol details are hidden behind a stable interface
|
||||
- **Main-Process Transport Ownership**: Electron Main owns the ACP Chat stdio bridge and Gateway transports; the renderer talks to Main over typed IPC
|
||||
- **Extension IPC Contributions**: Main-process extensions contribute host-api actions through the typed IPC registry instead of HTTP routes
|
||||
- **Graceful Recovery**: Built-in reconnect, timeout, and backoff logic handles transient failures automatically
|
||||
- **Secure Storage**: API keys and sensitive data leverage the operating system's native secure storage mechanisms
|
||||
- **CORS-Safe by Design**: The renderer does not call local Gateway or Host API HTTP endpoints directly
|
||||
|
||||
### Process Model & Gateway Troubleshooting
|
||||
|
||||
- ClawX is an Electron app, so **one app instance normally appears as multiple OS processes** (main/renderer/zygote/utility). This is expected.
|
||||
- Single-instance protection uses Electron's lock plus a local process-file lock fallback, preventing duplicate app launch in environments where desktop IPC/session bus is unstable.
|
||||
- During rolling upgrades, mixed old/new app versions can still have asymmetric protection behavior. For best reliability, upgrade all desktop clients to the same version.
|
||||
- The OpenClaw Gateway listener should still be **single-owner**: only one process should listen on `127.0.0.1:18789`.
|
||||
- Gateway readiness is based on OpenClaw core signals such as `system-presence`, `health`, and `status`; memory or channel failures are shown as capability degradation instead of global Gateway failure.
|
||||
- To verify the active listener:
|
||||
- macOS/Linux: `lsof -nP -iTCP:18789 -sTCP:LISTEN`
|
||||
- Windows (PowerShell): `Get-NetTCPConnection -LocalPort 18789 -State Listen`
|
||||
- Clicking the window close button (`X`) hides ClawX to tray; it does **not** fully quit the app. Use tray menu **Quit ClawX** for complete shutdown.
|
||||
|
||||
---
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 🤖 Personal AI Assistant
|
||||
Configure a general-purpose AI agent that can answer questions, draft emails, summarize documents, and help with everyday tasks—all from a clean desktop interface.
|
||||
|
||||
### 📊 Automated Monitoring
|
||||
Set up scheduled agents to monitor news feeds, track prices, or watch for specific events. Results are delivered to your preferred notification channel.
|
||||
|
||||
### 💻 Developer Productivity
|
||||
Integrate AI into your development workflow. Use agents to review code, generate documentation, or automate repetitive coding tasks.
|
||||
|
||||
### 🔄 Workflow Automation
|
||||
Chain multiple skills together to create sophisticated automation pipelines. Process data, transform content, and trigger actions—all orchestrated visually.
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Node.js**: 22.22.3+, 24.15.0+, or 25.9.0+ within the corresponding supported major line (Node 24 LTS recommended)
|
||||
- **Package Manager**: pnpm 9+ (npm is also supported)
|
||||
- **Linux (Ubuntu/Debian)**: Install required system libraries before running Electron; see [docs/en-US/development.md](docs/en-US/development.md)
|
||||
- **Package Manager**: pnpm 9+ (recommended) or npm
|
||||
- **Linux (Ubuntu/Debian)**: Install required system libraries before running Electron:
|
||||
```bash
|
||||
sudo apt-get install -y libnss3 libgtk-3-0 libxss1 libxtst6 libatspi2.0-0 libnotify4 xdg-utils
|
||||
```
|
||||
On Ubuntu 24.04+, some packages use a `t64` suffix; run the above command and `apt` will automatically select the correct variant.
|
||||
|
||||
### Common Commands
|
||||
### Project Structure
|
||||
|
||||
```ClawX/
|
||||
├── electron/ # Electron Main Process
|
||||
│ ├── services/ # Typed host APIs, provider, secrets and runtime services
|
||||
│ │ ├── providers/ # Provider/account model sync logic
|
||||
│ │ └── secrets/ # OS keychain and secret storage
|
||||
│ ├── shared/ # Shared provider schemas/constants
|
||||
│ │ └── providers/
|
||||
│ ├── main/ # App entry, windows, IPC registration
|
||||
│ ├── gateway/ # OpenClaw Gateway process manager
|
||||
│ ├── preload/ # Secure IPC bridge
|
||||
│ └── utils/ # Utilities (storage, auth, paths)
|
||||
├── src/ # React Renderer Process
|
||||
│ ├── lib/ # Unified frontend API + error model
|
||||
│ ├── stores/ # Zustand stores (settings/chat/gateway)
|
||||
│ ├── components/ # Reusable UI components
|
||||
│ ├── pages/ # Setup/Dashboard/Chat/Channels/Skills/Cron/Settings
|
||||
│ ├── i18n/ # Localization resources
|
||||
│ └── types/ # TypeScript type definitions
|
||||
├── tests/
|
||||
│ ├── e2e/ # Playwright Electron end-to-end smoke tests
|
||||
│ └── unit/ # Vitest unit/integration-like tests
|
||||
├── resources/ # Static assets (icons/images)
|
||||
└── scripts/ # Build and utility scripts
|
||||
```
|
||||
### Available Commands
|
||||
|
||||
```bash
|
||||
pnpm run init # Install dependencies and download bundled runtimes
|
||||
pnpm dev # Start in development mode with hot reload
|
||||
pnpm lint # Run ESLint
|
||||
pnpm typecheck # TypeScript validation
|
||||
pnpm test # Run unit tests
|
||||
pnpm run test:e2e # Run Electron E2E smoke tests
|
||||
pnpm build # Full production build
|
||||
pnpm package # Package for the current platform (:mac / :win / :linux)
|
||||
# Development
|
||||
pnpm run init # Install dependencies + download bundled binaries (uv, agent-browser)
|
||||
pnpm dev # Start with hot reload (auto-prepares bundled skills if missing)
|
||||
|
||||
# Quality
|
||||
pnpm lint # Run ESLint
|
||||
pnpm typecheck # TypeScript validation
|
||||
|
||||
# Testing
|
||||
pnpm test # Run unit tests
|
||||
pnpm run test:e2e # Run Electron E2E smoke tests with Playwright
|
||||
pnpm run test:e2e:headed # Run Electron E2E tests with a visible window
|
||||
pnpm run perf:chat # Capture synthetic Chat Renderer/Main CPU profiles
|
||||
pnpm run profile:main # Launch the built app with Main inspector on port 9229
|
||||
pnpm run comms:replay # Compute communication replay metrics
|
||||
pnpm run comms:baseline # Refresh communication baseline snapshot
|
||||
pnpm run comms:compare # Compare replay metrics against baseline thresholds
|
||||
|
||||
# Build & Package
|
||||
pnpm run build:vite # Build frontend only
|
||||
pnpm build # Full production build (with packaging assets)
|
||||
pnpm package # Package for current platform (includes bundled preinstalled skills)
|
||||
pnpm package:mac # Package for macOS
|
||||
pnpm package:win # Package for Windows
|
||||
pnpm package:linux # Package for Linux
|
||||
```
|
||||
|
||||
> For the project structure, complete command list, E2E parallel policy, performance diagnostics, communication regression checks, and tech stack, see [docs/en-US/development.md](docs/en-US/development.md).
|
||||
On headless Linux, run Electron tests under a display server such as `xvfb-run -a pnpm run test:e2e`.
|
||||
|
||||
### Electron Performance Diagnostics
|
||||
|
||||
`pnpm run perf:chat` runs isolated synthetic ACP workloads for streaming and for rich static Markdown sidebar/scroll interaction. It writes versioned metrics plus Renderer and Main CPU profiles under the Playwright `test-results/` directory. The Renderer profiles cover the production store/render path and frame pacing. The streaming Main profile measures Main-to-Renderer IPC fanout; the interaction Main profile shows whether Main remains idle while Renderer interactions run. Neither includes the upstream OpenClaw/ACP subprocess or GPU-process paths. Open a CPU profile in Chrome DevTools; the artifacts contain generated fixture text only and are not product telemetry. Results are hardware-dependent, so compare repeated runs on the same machine instead of applying one cross-platform absolute threshold.
|
||||
|
||||
For a live Renderer recording, start development with `CLAWX_REMOTE_DEBUGGING_PORT=9223 pnpm dev` and attach Playwright or Chrome DevTools to `localhost:9223`. For a live Electron Main recording, run `pnpm run profile:main`, open `chrome://inspect`, configure `localhost:9229`, and select the Electron Main target. Leave `CLAWX_GATEWAY_WS_TRACE` unset unless WebSocket tracing itself is being measured.
|
||||
|
||||
ClawX leaves Chromium hardware acceleration enabled by default so long documents, scrolling, and layout animations can use GPU compositing and rasterization. Chromium still honors the native `--disable-gpu` command-line switch as a troubleshooting fallback for a machine with a broken graphics driver.
|
||||
|
||||
### Communication Regression Checks
|
||||
|
||||
When a PR changes communication paths (gateway events, ACP Chat bridge send/receive flow, channel delivery, or transport fallback), run:
|
||||
|
||||
```bash
|
||||
pnpm run comms:replay
|
||||
pnpm run comms:compare
|
||||
```
|
||||
|
||||
`comms-regression` in CI enforces required scenarios and threshold checks.
|
||||
|
||||
### Electron E2E Tests
|
||||
|
||||
The Playwright Electron suite launches the packaged renderer and main process
|
||||
from `dist/` and `dist-electron/`, so it does not require manually running
|
||||
`pnpm dev` first.
|
||||
|
||||
`pnpm run test:e2e` automatically:
|
||||
|
||||
- builds the renderer and Electron bundles with `pnpm run build:vite`
|
||||
- starts Electron in an isolated E2E mode with a temporary `HOME`
|
||||
- uses a temporary ClawX `userData` directory
|
||||
- skips heavy startup side effects such as gateway auto-start, bundled skill
|
||||
installation, tray creation, and CLI auto-install
|
||||
|
||||
The first two baseline specs cover:
|
||||
|
||||
- first-launch setup wizard visibility on a fresh profile
|
||||
- skipping setup and navigating to the Models page inside the Electron app
|
||||
|
||||
Add future Electron flows under `tests/e2e/` and reuse the shared fixture in
|
||||
`tests/e2e/fixtures/electron.ts`.
|
||||
### Tech Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|------------|
|
||||
| Runtime | Electron 40+ |
|
||||
| UI Framework | React 19 + TypeScript |
|
||||
| Styling | Tailwind CSS + shadcn/ui |
|
||||
| State | Zustand |
|
||||
| Build | Vite + electron-builder |
|
||||
| Testing | Vitest + Playwright |
|
||||
| Animation | Framer Motion |
|
||||
| Icons | Lucide React |
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions from the community! Whether it's bug fixes, new features, documentation improvements, or translations, every contribution helps make ClawX better.
|
||||
We welcome contributions from the community! Whether it's bug fixes, new features, documentation improvements, or translations—every contribution helps make ClawX better.
|
||||
|
||||
### How to Contribute
|
||||
|
||||
@@ -199,15 +479,19 @@ We welcome contributions from the community! Whether it's bug fixes, new feature
|
||||
- Update documentation as needed
|
||||
- Keep commits atomic and descriptive
|
||||
|
||||
---
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
ClawX is built on the shoulders of excellent open-source projects:
|
||||
|
||||
- [OpenClaw](https://github.com/OpenClaw) - The AI agent runtime
|
||||
- [Electron](https://www.electronjs.org/) - Cross-platform desktop framework
|
||||
- [React](https://react.dev/) - UI component library
|
||||
- [shadcn/ui](https://ui.shadcn.com/) - Beautifully designed components
|
||||
- [Zustand](https://github.com/pmndrs/zustand) - Lightweight state management
|
||||
- [OpenClaw](https://github.com/OpenClaw) – The AI agent runtime
|
||||
- [Electron](https://www.electronjs.org/) – Cross-platform desktop framework
|
||||
- [React](https://react.dev/) – UI component library
|
||||
- [shadcn/ui](https://ui.shadcn.com/) – Beautifully designed components
|
||||
- [Zustand](https://github.com/pmndrs/zustand) – Lightweight state management
|
||||
|
||||
---
|
||||
|
||||
## Community
|
||||
|
||||
@@ -217,25 +501,31 @@ Join our community to connect with other users, get support, and share your expe
|
||||
| :---: | :---: | :---: |
|
||||
| <img src="src/assets/community/wecom-qr.png" width="150" alt="WeChat QR Code" /> | <img src="src/assets/community/feishu-qr.png" width="150" alt="Feishu QR Code" /> | <img src="src/assets/community/20260212-185822.png" width="150" alt="Discord QR Code" /> |
|
||||
|
||||
### ClawX Partner Program
|
||||
### ClawX Partner Program 🚀
|
||||
|
||||
We're launching the ClawX Partner Program and looking for partners who can help introduce ClawX to more clients, especially those with custom AI agent or automation needs.
|
||||
|
||||
Partners help connect us with potential users and projects, while the ClawX team provides full technical support, customization, and integration. If you work with clients interested in AI tools or automation, we'd love to collaborate.
|
||||
Partners help connect us with potential users and projects, while the ClawX team provides full technical support, customization, and integration.
|
||||
|
||||
If you work with clients interested in AI tools or automation, we'd love to collaborate.
|
||||
|
||||
DM us or email [public@valuecell.ai](mailto:public@valuecell.ai) to learn more.
|
||||
|
||||
---
|
||||
|
||||
## Star History
|
||||
|
||||
<p align="center">
|
||||
<img src="https://api.star-history.com/svg?repos=ValueCell-ai/ClawX&type=Date" alt="Star History Chart" />
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
ClawX is released under the [MIT License](LICENSE). You're free to use, modify, and distribute this software.
|
||||
|
||||
<hr>
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<sub>Built with ❤️ by the ValueCell Team</sub>
|
||||
|
||||
+339
-100
@@ -1,99 +1,144 @@
|
||||
|
||||
<p align="center">
|
||||
<img src="src/assets/logo.svg" width="128" height="128" alt="ClawX Logo" />
|
||||
<img src="src/assets/logo.svg" width="128" height="128" alt="ClawX Logo" />
|
||||
</p>
|
||||
|
||||
<h1 align="center">ClawX</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>Десктоп-интерфейс для AI-агентов OpenClaw</strong>
|
||||
<strong>Десктоп-интерфейс для AI-агентов OpenClaw</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#почему-clawx">Почему ClawX</a> •
|
||||
<a href="#быстрый-старт">Быстрый старт</a> •
|
||||
<a href="#архитектура">Архитектура</a> •
|
||||
<a href="#разработка">Разработка</a> •
|
||||
<a href="#участие">Участие</a>
|
||||
<a href="#возможности">Возможности</a> •
|
||||
<a href="#почему-clawx">Почему ClawX</a> •
|
||||
<a href="#быстрый-старт">Быстрый старт</a> •
|
||||
<a href="#архитектура">Архитектура</a> •
|
||||
<a href="#разработка">Разработка</a> •
|
||||
<a href="#участие">Участие</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/platform-MacOS%20%7C%20Windows%20%7C%20Linux-blue" alt="Platform" />
|
||||
<img src="https://img.shields.io/badge/electron-40+-47848F?logo=electron" alt="Electron" />
|
||||
<img src="https://img.shields.io/badge/react-19-61DAFB?logo=react" alt="React" />
|
||||
<a href="https://discord.com/invite/84Kex3GGAh" target="_blank">
|
||||
<img src="https://img.shields.io/discord/1399603591471435907?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb" alt="chat on Discord" />
|
||||
</a>
|
||||
<img src="https://img.shields.io/github/downloads/ValueCell-ai/ClawX/total?color=%23027DEB" alt="Downloads" />
|
||||
<img src="https://img.shields.io/badge/license-MIT-green" alt="License" />
|
||||
<img src="https://img.shields.io/badge/platform-MacOS%20%7C%20Windows%20%7C%20Linux-blue" alt="Platform" />
|
||||
<img src="https://img.shields.io/badge/electron-40+-47848F?logo=electron" alt="Electron" />
|
||||
<img src="https://img.shields.io/badge/react-19-61DAFB?logo=react" alt="React" />
|
||||
<a href="https://discord.com/invite/84Kex3GGAh" target="_blank">
|
||||
<img src="https://img.shields.io/discord/1399603591471435907?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb" alt="chat on Discord" />
|
||||
</a>
|
||||
<img src="https://img.shields.io/github/downloads/ValueCell-ai/ClawX/total?color=%23027DEB" alt="Downloads" />
|
||||
<img src="https://img.shields.io/badge/license-MIT-green" alt="License" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.ja-JP.md">日本語</a> | Русский
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.ja-JP.md">日本語</a> | Русский
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## Обзор
|
||||
|
||||
**ClawX** — это мост между мощными AI-агентами и повседневными пользователями. Построенный на базе [OpenClaw](https://github.com/OpenClaw), он превращает управление AI через командную строку в доступный и красивый десктопный интерфейс — терминал не нужен.
|
||||
**ClawX** — это мост между мощными AI-агентами и повседневными пользователями. Построенный на базе [OpenClaw](https://github.com/OpenClaw), он превращает управление AI через командную строку в доступный и красивый десктоп-опыт — терминал не нужен.
|
||||
|
||||
Автоматизация рабочих процессов, управление AI-каналами или планирование интеллектуальных задач — ClawX предоставляет интерфейс для эффективного использования AI-агентов.
|
||||
|
||||
ClawX поставляется с предварительно настроенными провайдерами моделей, соответствующими лучшим практикам, и нативно поддерживает Windows и многоязычные настройки. Расширенные параметры можно настроить через **Настройки → Дополнительно → Режим разработчика**.
|
||||
ClawX поставляется с предустановленными лучшими практиками для провайдеров моделей и нативно поддерживает Windows, а также многоязычные настройки. Вы можете тонко настроить расширенные параметры через **Настройки → Дополнительно → Режим разработчика**.
|
||||
|
||||
<p align="center"><strong style="font-size:1.1em; text-decoration: underline;">Для получения полной корпоративной версии, специализированной поддержки или индивидуального сопровождения внедрения под ваш бизнес-сценарий свяжитесь с нами по адресу <a href="mailto:public@valuecell.ai">public@valuecell.ai</a>.</strong></p>
|
||||
<p align="center"><strong style="font-size:1.1em; text-decoration: underline;">Для получения полной корпоративной версии, специализированной поддержки или индивидуального сопровождения внедрения под ваш бизнес-сценарий, свяжитесь с нами по адресу <a href="mailto:public@valuecell.ai">public@valuecell.ai</a>.</strong></p>
|
||||
|
||||
---
|
||||
|
||||
## Скриншоты
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><img src="resources/screenshot/ru/chat.png" alt="Chat"><br><em>Чат</em></td>
|
||||
<td align="center"><img src="resources/screenshot/ru/cron.png" alt="Cron"><br><em>Запланированные задачи</em></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><img src="resources/screenshot/ru/skills.png" alt="Skills"><br><em>Навыки</em></td>
|
||||
<td align="center"><img src="resources/screenshot/ru/channels.png" alt="Channels"><br><em>Каналы</em></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><img src="resources/screenshot/ru/models.png" alt="Models"><br><em>Модели</em></td>
|
||||
<td align="center"><img src="resources/screenshot/ru/settings.png" alt="Settings"><br><em>Настройки</em></td>
|
||||
</tr>
|
||||
</table>
|
||||
<p align="center">
|
||||
<img src="resources/screenshot/ru/chat.png" style="width: 100%; height: auto;">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="resources/screenshot/ru/cron.png" style="width: 100%; height: auto;">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="resources/screenshot/ru/skills.png" style="width: 100%; height: auto;">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="resources/screenshot/ru/channels.png" style="width: 100%; height: auto;">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="resources/screenshot/ru/models.png" style="width: 100%; height: auto;">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="resources/screenshot/ru/settings.png" style="width: 100%; height: auto;">
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## Почему ClawX
|
||||
|
||||
Создание AI-агентов не должно требовать владения командной строкой. Философия ClawX проста: **мощные технологии заслуживают интерфейса, который уважает ваше время.** ClawX построен непосредственно на официальном ядре **OpenClaw**. Вместо отдельной установки среда выполнения встроена в приложение, что обеспечивает бесшовный опыт «всё включено». Мы поддерживаем тесное соответствие с upstream-проектом OpenClaw, чтобы вы всегда имели доступ к официальным новейшим возможностям, улучшениям стабильности и совместимости с экосистемой.
|
||||
Создание AI-агентов не должно требовать владения командной строкой. Философия ClawX проста: **мощные технологии заслуживают интерфейса, который уважает ваше время.**
|
||||
|
||||
| Проблема | Решение ClawX |
|
||||
|----------|---------------|
|
||||
| Сложная настройка через CLI | Установка в один клик с мастером настройки |
|
||||
| Конфигурационные файлы | Визуальные настройки с проверкой в реальном времени |
|
||||
| Управление процессами | Автоматическое управление жизненным циклом Gateway |
|
||||
| Обновления приложения | Проверка обновлений при запуске с запросом перед скачиванием или установкой |
|
||||
| Редактирование конфигурационных файлов | Визуальные настройки с проверкой в реальном времени |
|
||||
| Управление процессами | Автоматическое управление жизненным циклом шлюза |
|
||||
| Несколько AI-провайдеров | Единая панель настройки провайдеров |
|
||||
| Установка навыков/плагинов | Локальное управление навыками с опциональным маркетплейсом от расширения |
|
||||
| Установка навыков/плагинов | Встроенный маркетплейс и управление навыками |
|
||||
|
||||
### Возможности
|
||||
### OpenClaw внутри
|
||||
|
||||
- **🎯 Нулевой порог настройки**: Весь процесс выполняется через интуитивный графический интерфейс — без терминальных команд, YAML-файлов и поиска переменных окружения.
|
||||
- **💬 Интеллектуальный интерфейс чата**: Несколько контекстов и история разговоров, потоковый Markdown с подсветкой синтаксиса, разбором CJK, таблицами и формулами KaTeX, прямая маршрутизация через `@agent`, встроенные карточки `/skill`, рабочие пространства с приоритетом и предпросмотр только для чтения Markdown, `.docx`, `.pptx` и локального HTML.
|
||||
- **📡 Управление несколькими каналами**: Настраивайте и отслеживайте независимые AI-каналы с несколькими аккаунтами, привязкой агента к аккаунту, переключением аккаунта по умолчанию и встроенным официальным плагином личного WeChat от Tencent.
|
||||
- **⏰ Автоматизация по расписанию**: Создавайте повторяющиеся или одноразовые расписания, вставляйте навыки в запланированные запросы и доставляйте результаты во внешние каналы.
|
||||
- **🧩 Расширяемая система навыков**: Управляйте навыками локально без зависимости от Gateway, обнаруживайте навыки из нескольких источников OpenClaw и используйте встроенные навыки обработки документов для `pdf`, `xlsx`, `docx` и `pptx`.
|
||||
- **🔐 Безопасная интеграция провайдеров**: Подключайте OpenAI, Anthropic, Z.AI / GLM и другие провайдеры; учётные данные хранятся в нативном системном хранилище ключей. Поддерживаются OAuth, пользовательские провайдеры, эндпоинты генерации изображений и совместимые резервные проверки.
|
||||
- **🌙 Адаптивные темы**: Выбирайте светлую, тёмную или синхронизированную с системой тему.
|
||||
- **🚀 Управление автозапуском**: Включите **Запускать при старте системы** в разделе **Настройки → Общие**.
|
||||
- **🔔 Уведомления об обновлениях**: Проверяйте новые версии при запуске и сами решайте, скачивать или устанавливать обновление.
|
||||
ClawX построен непосредственно на официальном ядре **OpenClaw**. Вместо отдельной установки мы встраиваем среду выполнения в приложение для бесшовного опыта "всё включено".
|
||||
|
||||
> Полное описание возможностей доступно в [docs/ru-RU/features.md](docs/ru-RU/features.md).
|
||||
Мы стремимся поддерживать строгое соответствие с проектом OpenClaw, чтобы вы всегда имели доступ к новейшим возможностям, улучшениям стабильности и совместимости с экосистемой.
|
||||
|
||||
### Типичные сценарии использования
|
||||
---
|
||||
|
||||
- **🤖 Персональный AI-ассистент**: Настройте универсального AI-агента для ответов на вопросы, составления писем, резюмирования документов и помощи с повседневными задачами через чистый десктопный интерфейс.
|
||||
- **📊 Автоматизированный мониторинг**: Планируйте агентов для отслеживания новостных лент, цен или определённых событий и доставляйте результаты в предпочитаемый канал уведомлений.
|
||||
- **💻 Производительность разработчика**: Интегрируйте AI в рабочий процесс разработки для проверки кода, генерации документации и автоматизации повторяющихся задач.
|
||||
- **🔄 Автоматизация рабочих процессов**: Объединяйте несколько навыков в визуальные конвейеры, которые обрабатывают данные, преобразуют контент и запускают действия.
|
||||
## Возможности
|
||||
|
||||
### 🎯 Нулевой порог настройки
|
||||
Весь процесс — от установки до первого взаимодействия с AI — выполняется через интуитивный графический интерфейс. Без терминальных команд, без YAML-файлов, без поиска переменных окружения.
|
||||
|
||||
### 💬 Интеллектуальный интерфейс чата
|
||||
Общайтесь с AI-агентами через современный чат. Поддержка нескольких контекстов разговора, истории сообщений и рендеринга ответов агента в Markdown (включая таблицы GitHub-flavored и математические формулы LaTeX через KaTeX: `$строчные$`, `$$блочные$$`, `\(строчные\)` и `\[блочные\]`), при этом пользовательский ввод всегда отображается как обычный текст. Для мультиагентных конфигураций также доступна прямая маршрутизация через `@agent` в главном поле ввода.
|
||||
Навыки, вставляемые из поля ввода, отображаются как чипы `/skill-name`; нажмите на чип, чтобы открыть боковую панель предпросмотра и прочитать `SKILL.md` соответствующего навыка.
|
||||
При выборе другого агента через `@agent` ClawX переключается непосредственно в контекст этого агента вместо ретрансляции через агента по умолчанию. Рабочие пространства агентов по умолчанию разделены, но более строгая изоляция зависит от настроек песочницы OpenClaw.
|
||||
Каждый агент может переопределить свои настройки `provider/model`; агенты без переопределения продолжают наследовать глобальную модель по умолчанию.
|
||||
|
||||
### Предпросмотр локального HTML
|
||||
На правой панели Chat остаются только вкладки «Рабочая область», «Просмотр» и «Изменения»; универсального веб-браузера, домашней страницы и адресной строки больше нет. Разрешённые локальные вложения `.html` / `.htm`, файловые операции и файлы рабочей области по умолчанию открываются в «Просмотре». В действиях файла можно выбрать встроенный просмотр ClawX или системное приложение, а кнопка в заголовке просмотра открывает текущий HTML-файл в системном браузере.
|
||||
|
||||
Все ссылки некликабельны. Ссылки, отображаемые ClawX, выглядят как обычный текст; в HTML-просмотре также удаляются оформление ссылок и взаимодействие указателем. Формы, переходы из скриптов, перенаправления, переходы внутри страницы, всплывающие окна, загрузки, сетевые запросы и разрешения устройств блокируются. Самодостаточный локальный HTML отображается, но не может покинуть выбранный документ.
|
||||
|
||||
### 📡 Управление несколькими каналами
|
||||
Настраивайте и отслеживайте несколько AI-каналов одновременно. Каждый канал работает независимо, позволяя запускать специализированных агентов для разных задач.
|
||||
Каждый канал теперь поддерживает несколько учётных записей, привязку агента к учётной записи и переключение канала по умолчанию прямо на странице Каналы.
|
||||
Для пользовательских идентификаторов учётных записей каналов ClawX требует совместимый с OpenClaw канонический формат (`[a-z0-9_-]`, строчные буквы, максимум 64 символа, должен начинаться с буквы или цифры) для предотвращения ошибок маршрутизации.
|
||||
ClawX также включает официальный плагин личного WeChat от Tencent, позволяя подключить WeChat напрямую со страницы Каналы через встроенный QR-код.
|
||||
|
||||
### ⏰ Автоматизация по расписанию
|
||||
Планируйте автоматический запуск AI-задач. Определяйте триггеры, устанавливайте интервалы и позволяйте AI-агентам работать круглосуточно без ручного вмешательства.
|
||||
На странице Cron теперь можно настроить внешнюю доставку непосредственно в форме задачи с отдельными селекторами учётной записи отправителя и цели получателя. Для поддерживаемых каналов цели получателей автоматически обнаруживаются из каталогов каналов или известной истории сессий, поэтому больше не нужно редактировать `jobs.json` вручную. Поле сообщения задачи также поддерживает вставку навыков с помощью того же синтаксиса встроенных токенов `/skill`, что и в основном окне чата (с учётом выбранного агента), поэтому запланированные подсказки могут запускать навыки напрямую. Выбор расписания разделён на вкладки **Повтор** и **Однократно**: повтор предлагает частоты «Ежечасно», «Ежедневно», «По будням», «Еженедельно» и «Свой» (произвольный cron) со встроенными элементами выбора времени/дня недели, а однократно запускает задачу один раз в выбранную дату (с показом дня недели) и время. Однократные задачи должны быть запланированы на будущее и автоматически удаляются средой выполнения после завершения.
|
||||
|
||||
### 🧩 Расширяемая система навыков
|
||||
Расширяйте возможности AI-агентов готовыми навыками. Просматривайте, устанавливайте и управляйте навыками через встроенную панель — менеджеры пакетов не нужны.
|
||||
ClawX также предварительно упаковывает полные навыки обработки документов (`pdf`, `xlsx`, `docx`, `pptx`), автоматически развёртывает их в управляемый каталог навыков (по умолчанию `~/.openclaw/skills`) при запуске и включает по умолчанию при первой установке.
|
||||
На странице Навыки отображаются навыки из нескольких источников OpenClaw (управляемый каталог, workspace и дополнительные каталоги навыков), а также показывается фактическое расположение каждого навыка для прямого открытия папки.
|
||||
|
||||
### 🔐 Безопасная интеграция провайдеров
|
||||
Подключайтесь к нескольким AI-провайдерам (OpenAI, Anthropic, Z.AI / GLM и др.) с учётными данными, безопасно хранящимися в системной связке ключей. OpenAI поддерживает как API-ключи, так и OAuth через браузер (подписка Codex).
|
||||
Для провайдеров **Custom**, используемых с OpenAI-совместимыми шлюзами, вы можете установить пользовательский `User-Agent` в **Настройки → AI Провайдеры → Редактировать провайдера** для совместимости с чувствительными эндпоинтами.
|
||||
Z.AI (CN / Global) соответствует встроенному провайдеру OpenClaw `zai` (`ZAI_API_KEY`). Модель по умолчанию — `glm-5.2`. Пресет Code Plan переключает на эндпоинты Coding Plan (`…/api/coding/paas/v4`); также доступны обычные API (`…/api/paas/v4`). CN и Global взаимоисключающие, так как используют один и тот же runtime-ключ OpenClaw.
|
||||
Когда совместимый шлюз отклоняет `/models` по причинам, не связанным с аутентификацией, ClawX автоматически переключается на легковесный зонд `/chat/completions` или `/responses` при проверке API-ключа.
|
||||
|
||||
### 🌙 Адаптивные темы
|
||||
Светлая тема, тёмная тема или синхронизация с системой. ClawX автоматически адаптируется к вашим предпочтениям.
|
||||
|
||||
### 🚀 Управление автозапуском
|
||||
В **Настройки → Общие** вы можете включить **Запускать при старте системы**, чтобы ClawX автоматически запускался после входа в систему.
|
||||
|
||||
---
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
@@ -125,71 +170,255 @@ pnpm dev
|
||||
|
||||
### Первый запуск
|
||||
|
||||
При первом запуске ClawX **Мастер настройки** проведёт вас через следующие шаги:
|
||||
При первом запуске ClawX **Мастер настройки** проведёт вас через:
|
||||
|
||||
1. **Язык и регион** — настройка предпочитаемой локали
|
||||
2. **AI-провайдер** — добавление провайдеров с API-ключами или OAuth для провайдеров, поддерживающих вход через браузер или устройство
|
||||
1. **Язык и регион** — настройка предпочтительного языка и региона
|
||||
2. **AI-провайдер** — добавление провайдеров с API-ключами или OAuth (для провайдеров, поддерживающих вход через браузер/устройство)
|
||||
3. **Пакеты навыков** — выбор предустановленных навыков для распространённых сценариев
|
||||
4. **Проверка** — тестирование конфигурации перед входом в основной интерфейс
|
||||
|
||||
Мастер предварительно выбирает системный язык, если он поддерживается, иначе переключается на английский.
|
||||
|
||||
> Примечание о веб-поиске: ClawX отключает универсальный инструмент OpenClaw `web_search` на уровнях политик агента и Gateway. Это также относится к поиску Moonshot (Kimi); управляемая автоматизация браузера и `web_fetch` остаются доступными.
|
||||
>
|
||||
> Примечание о внутренних инструментах: ClawX также отключает для агентов `gateway`, `nodes`, `create_goal`, `get_goal` и `update_goal` на обоих уровнях политик. RPC Gateway самого приложения ClawX, а также инструменты сообщений, оркестрации сессий и обнаружения агентов остаются доступными.
|
||||
|
||||
### Настройки прокси
|
||||
|
||||
ClawX включает встроенные настройки прокси для Electron, OpenClaw Gateway и таких каналов, как Telegram, которым требуется доступ в интернет через локальный прокси-клиент.
|
||||
ClawX включает встроенные настройки прокси для сред, где Electron, шлюз OpenClaw или каналы вроде Telegram должны выходить в интернет через локальный прокси-клиент.
|
||||
|
||||
Откройте **Настройки → Gateway → Прокси**, чтобы настроить прокси по умолчанию, правила обхода и дополнительные переопределения HTTP, HTTPS и `ALL_PROXY` / SOCKS в режиме разработчика. Пример локального адреса: `http://127.0.0.1:7890`.
|
||||
Откройте **Настройки → Шлюз → Прокси** и настройте:
|
||||
|
||||
> Подробности о резервном поведении прокси, синхронизации с Telegram и **OpenClaw Doctor** см. в [docs/ru-RU/proxy-settings.md](docs/ru-RU/proxy-settings.md).
|
||||
- **Прокси-сервер**: прокси по умолчанию для всех запросов
|
||||
- **Правила обхода**: хосты, которые должны подключаться напрямую, разделённые точкой с запятой, запятыми или новыми строками
|
||||
- В **Режиме разработчика** можно дополнительно переопределить:
|
||||
- **HTTP Прокси**
|
||||
- **HTTPS Прокси**
|
||||
- **ALL_PROXY / SOCKS**
|
||||
|
||||
Рекомендуемые примеры локальных настроек:
|
||||
|
||||
```text
|
||||
Прокси-сервер: http://127.0.0.1:7890
|
||||
```
|
||||
Примечания:
|
||||
|
||||
- Значение `host:port` рассматривается как HTTP.
|
||||
- Если расширенные поля прокси пусты, ClawX использует `Прокси-сервер`.
|
||||
- Сохранение настроек прокси немедленно повторно применяет сеть Electron и автоматически перезапускает шлюз.
|
||||
- ClawX также синхронизирует прокси с конфигурацией канала Telegram в OpenClaw, когда Telegram включён.
|
||||
- При перезапуске шлюза существующий прокси канала Telegram сохраняется, если прокси ClawX отключен.
|
||||
- Чтобы явно очистить прокси Telegram из конфигурации OpenClaw, сохраните настройки прокси с отключенным прокси.
|
||||
- В **Настройки → Дополнительно → Разработчик** можно запустить **OpenClaw Doctor** для выполнения `openclaw doctor --json` и просмотра диагностического вывода, не покидая приложение.
|
||||
- В упакованных сборках Windows встроенный `openclaw` CLI/TUI запускается через поставляемый `node.exe` для стабильного поведения ввода в терминале.
|
||||
|
||||
---
|
||||
|
||||
## Архитектура
|
||||
|
||||
ClawX использует **двухпроцессную архитектуру с унифицированным уровнем Host API**: React Renderer обращается к единой абстракции клиента, а Electron Main управляет выбором протокола, жизненным циклом Gateway и stdio bridge для ACP Chat.
|
||||
ClawX использует **двухпроцессную архитектуру с унифицированным уровнем Host API**. Рендерер обращается к единой абстракции клиента, а Electron Main управляет выбором протокола и жизненным циклом процессов:
|
||||
|
||||
- **Модель процессов**: Electron Main управляет окном, наблюдением за Gateway, системной интеграцией и обновлениями; OpenClaw Gateway предоставляет возможности AI-оркестрации, каналов и навыков; Renderer не обращается к локальным эндпоинтам напрямую.
|
||||
- **Доставка конфигурации**: изменения среды выполнения используют авторитетный снимок `config.set`, поэтому обычные изменения провайдера, агента, навыка и модели не заменяют процесс Gateway; учётные данные обновляются без перезапуска через `secrets.reload`, а защищённое восстановление запускается после четырёх последовательных пропусков heartbeat.
|
||||
- **ACP Chat**: Chat UI взаимодействует с OpenClaw через [ACP (Agent Client Protocol)](https://agentclientprotocol.com), обеспечивая относительно стабильную поверхность чат-протокола поверх часто итерируемого OpenClaw. ACP работает через stdio bridge под управлением Main, поддерживая аутентифицированное воспроизведение истории после перезагрузки конфигурации, потоковую выдачу при навигации, а также медиа, вложения и файловые операции, проверенные Main. Если защищённый перезапуск Gateway прерывает уже принятый ход, исправленная среда OpenClaw явно связывает восстановительный run с исходным ACP prompt, чтобы последующий текст и активность инструментов продолжались в том же ходе в памяти; последующее воспроизведение истории восстанавливает сохранённые границы инструментов как нативные обновления ACP.
|
||||
- **Принципы проектирования**: единая точка входа фронтенда, транспорт под управлением Main, корректное восстановление с переподключением/таймаутом/повтором, безопасное хранение и границы, защищённые от CORS.
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Десктоп-приложение ClawX │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Главный процесс Electron │ │
|
||||
│ │ • Управление жизненным циклом окна и приложения │ │
|
||||
│ │ • Наблюдение за процессом шлюза │ │
|
||||
│ │ • Интеграция с системой (трей, уведомления, связка ключей)│ │
|
||||
│ │ • Оркестрация автообновлений │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ IPC (авторитетная плоскость управления) │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Процесс рендерера React │ │
|
||||
│ │ • Современный UI на компонентах (React 19) │ │
|
||||
│ │ • Управление состоянием с Zustand │ │
|
||||
│ │ • Унифицированные вызовы host-api/api-client │ │
|
||||
│ │ • Ответы в Markdown, ввод как обычный текст │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────┬──────────────────────────────────┘
|
||||
│
|
||||
Стратегия транспорта, управляемая Main
|
||||
(Сначала WS, затем HTTP, затем IPC)
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ Host API и прокси-уровень Main │
|
||||
│ │
|
||||
│ • hostapi:fetch (прокси Main, избегает CORS в dev/prod) │
|
||||
│ • gateway:httpProxy (Рендерер не вызывает Gateway HTTP напрямую)│
|
||||
│ • Унифицированное отображение ошибок и повторные попытки │
|
||||
└──────────────────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
Резерв WS / HTTP / IPC
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Шлюз OpenClaw │
|
||||
│ │
|
||||
│ • Среда выполнения AI-агентов и оркестрация │
|
||||
│ • Управление каналами сообщений │
|
||||
│ • Среда выполнения навыков/плагинов │
|
||||
│ • Уровень абстракции провайдеров │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
> Схема процессов, координация конфигурации, семантика файловых операций ACP и устранение неполадок Gateway описаны в [docs/ru-RU/architecture.md](docs/ru-RU/architecture.md).
|
||||
### Принципы проектирования
|
||||
|
||||
- **Изоляция процессов**: Среда выполнения AI работает в отдельном процессе, обеспечивая отзывчивость UI даже при тяжёлых вычислениях
|
||||
- **Единая точка входа для фронтенда**: Запросы рендерера проходят через host-api/api-client; детали протокола скрыты за стабильным интерфейсом
|
||||
- **Владение транспортом в Main**: Electron Main управляет использованием WS/HTTP и откатом к IPC для надёжности
|
||||
- **Корректное восстановление**: Встроенная логика переподключения, таймаутов и отката автоматически обрабатывает временные сбои
|
||||
- **Безопасное хранение**: API-ключи и конфиденциальные данные используют нативные механизмы безопасного хранения ОС
|
||||
- **Безопасность CORS**: Локальный HTTP-доступ проксируется через Main, предотвращая CORS-проблемы на стороне рендерера
|
||||
|
||||
### Модель процессов и устранение неполадок шлюза
|
||||
|
||||
- ClawX — это приложение Electron, поэтому **один экземпляр приложения обычно отображается как несколько процессов ОС** (main/renderer/zygote/utility). Это нормально.
|
||||
- Защита единственного экземпляра использует блокировку Electron плюс локальный файл блокировки процессов, предотвращая дублирование запуска приложения в средах с нестабильным IPC/сессионной шиной.
|
||||
- При последовательных обновлениях смешанные старые/новые версии могут иметь асимметричное поведение защиты. Для лучшей надёжности обновите все десктоп-клиенты до одной версии.
|
||||
- Слушатель шлюза OpenClaw должен быть **единственным владельцем**: только один процесс должен слушать `127.0.0.1:18789`.
|
||||
- Для проверки активного слушателя:
|
||||
- macOS/Linux: `lsof -nP -iTCP:18789 -sTCP:LISTEN`
|
||||
- Windows (PowerShell): `Get-NetTCPConnection -LocalPort 18789 -State Listen`
|
||||
- Нажатие кнопки закрытия окна (`X`) скрывает ClawX в трей; это **не** полностью закрывает приложение. Используйте меню трея **Quit ClawX** для полного завершения.
|
||||
|
||||
---
|
||||
|
||||
## Варианты использования
|
||||
|
||||
### 🤖 Персональный AI-ассистент
|
||||
Настройте универсального AI-агента, который может отвечать на вопросы, составлять письма, резюмировать документы и помогать с повседневными задачами — всё через чистый десктоп-интерфейс.
|
||||
|
||||
### 📊 Автоматизированный мониторинг
|
||||
Настройте запланированных агентов для отслеживания новостных лент, цен или определённых событий. Результаты доставляются в ваш предпочтительный канал уведомлений.
|
||||
|
||||
### 💻 Производительность разработчика
|
||||
Интегрируйте AI в рабочий процесс разработки. Используйте агентов для проверки кода, генерации документации или автоматизации повторяющихся задач кодирования.
|
||||
|
||||
### 🔄 Автоматизация рабочих процессов
|
||||
Связывайте несколько навыков для создания сложных конвейеров автоматизации. Обрабатывайте данные, преобразовывайте контент и запускайте действия — всё визуально оркестрируется.
|
||||
|
||||
---
|
||||
|
||||
## Разработка
|
||||
|
||||
### Требования
|
||||
|
||||
- **Node.js**: 22.22.3+, 24.15.0+ или 25.9.0+ в пределах соответствующей основной версии (рекомендуется Node 24 LTS)
|
||||
- **Менеджер пакетов**: pnpm 9+ (npm также поддерживается)
|
||||
- **Linux (Ubuntu/Debian)**: перед запуском Electron установите необходимые системные библиотеки; см. [docs/ru-RU/development.md](docs/ru-RU/development.md)
|
||||
- **Менеджер пакетов**: pnpm 9+ (рекомендуется) или npm
|
||||
|
||||
### Основные команды
|
||||
### Структура проекта
|
||||
|
||||
```bash
|
||||
pnpm run init # Установить зависимости и скачать встроенные среды выполнения
|
||||
pnpm dev # Запустить режим разработки с горячей перезагрузкой
|
||||
pnpm lint # Запустить ESLint
|
||||
pnpm typecheck # Проверить типы TypeScript
|
||||
pnpm test # Запустить модульные тесты
|
||||
pnpm run test:e2e # Запустить дымовые E2E-тесты Electron
|
||||
pnpm build # Выполнить полную production-сборку
|
||||
pnpm package # Упаковать для текущей платформы (:mac / :win / :linux)
|
||||
```
|
||||
ClawX/
|
||||
├── electron/ # Главный процесс Electron
|
||||
│ ├── api/ # Маршрутизатор API и обработчики Main
|
||||
│ │ └── routes/ # Модули маршрутов RPC/HTTP прокси
|
||||
│ ├── services/ # Службы провайдеров, секретов и среды выполнения
|
||||
│ │ ├── providers/ # Логика синхронизации моделей provider/account
|
||||
│ │ └── secrets/ # Связка ключей ОС и хранилище секретов
|
||||
│ ├── shared/ # Общие схемы провайдеров и константы
|
||||
│ │ └── providers/
|
||||
│ ├── main/ # Точка входа приложения, окна, регистрация IPC
|
||||
│ ├── gateway/ # Менеджер процесса шлюза OpenClaw
|
||||
│ ├── preload/ # Безопасный IPC-мост
|
||||
│ └── utils/ # Утилиты (хранилище, аутентификация, пути)
|
||||
├── src/ # Процесс рендерера React
|
||||
│ ├── lib/ # Унифицированный фронтенд API и модель ошибок
|
||||
│ ├── stores/ # Хранилища Zustand (settings/chat/gateway)
|
||||
│ ├── components/ # Переиспользуемые UI-компоненты
|
||||
│ ├── pages/ # Setup/Dashboard/Chat/Channels/Skills/Cron/Settings
|
||||
│ ├── i18n/ # Ресурсы локализации
|
||||
│ └── types/ # Определения типов TypeScript
|
||||
├── tests/
|
||||
│ ├── e2e/ # Сквозные дымовые тесты Playwright Electron
|
||||
│ └── unit/ # Модульные/интеграционные тесты Vitest
|
||||
├── resources/ # Статические ресуры (иконки, изображения)
|
||||
└── scripts/ # Скрипты сборки и утилит
|
||||
```
|
||||
|
||||
> Структура проекта, полный список команд, политика параллельности E2E, диагностика производительности, проверки регрессий коммуникаций и технологический стек описаны в [docs/ru-RU/development.md](docs/ru-RU/development.md).
|
||||
### Доступные команды
|
||||
|
||||
```bash
|
||||
# Разработка
|
||||
pnpm run init # Установить зависимости + скачать uv
|
||||
pnpm dev # Запуск с горячей перезагрузкой (автоподготовка упакованных навыков при отсутствии)
|
||||
|
||||
# Качество кода
|
||||
pnpm lint # Запустить ESLint
|
||||
pnpm typecheck # Проверка типов TypeScript
|
||||
|
||||
# Тестирование
|
||||
pnpm test # Запустить модульные тесты
|
||||
pnpm run test:e2e # Запустить E2E дымовые тесты Electron с Playwright
|
||||
pnpm run test:e2e:headed # Запустить E2E тесты Electron с видимым окном
|
||||
pnpm run comms:replay # Вычислить метрики повторного воспроизведения коммуникаций
|
||||
pnpm run comms:baseline # Обновить базовый снимок коммуникаций
|
||||
pnpm run comms:compare # Сравнить метрики воспроизведения с базовыми порогами
|
||||
|
||||
# Сборка и упаковка
|
||||
pnpm run build:vite # Собрать только фронтенд
|
||||
pnpm build # Полная production-сборка (с ресурсами упаковки)
|
||||
pnpm package # Упаковать для текущей платформы (включает предустановленные навыки)
|
||||
pnpm package:mac # Упаковать для macOS
|
||||
pnpm package:win # Упаковать для Windows
|
||||
pnpm package:linux # Упаковать для Linux
|
||||
```
|
||||
|
||||
На headless Linux запускайте тесты Electron под сервером отображения, например `xvfb-run -a pnpm run test:e2e`.
|
||||
|
||||
### Проверка регрессии коммуникаций
|
||||
|
||||
Когда PR изменяет пути коммуникации (события шлюза, поток отправки/получения чата, доставка каналов или откат транспорта), запустите:
|
||||
|
||||
```bash
|
||||
pnpm run comms:replay
|
||||
pnpm run comms:compare
|
||||
```
|
||||
|
||||
`comms-regression` в CI проверяет обязательные сценарии и пороги.
|
||||
|
||||
### E2E-тесты Electron
|
||||
|
||||
Сьют Playwright Electron запускает упакованный рендерер и главный процесс из `dist/` и `dist-electron/`, поэтому не требует предварительного ручного запуска `pnpm dev`.
|
||||
|
||||
`pnpm run test:e2e` автоматически:
|
||||
|
||||
- собирает рендерер и пакеты Electron с `pnpm run build:vite`
|
||||
- запускает Electron в изолированном режиме E2E с временным `HOME`
|
||||
- использует временный каталог `userData` ClawX
|
||||
- пропускает тяжёлые побочные эффекты запуска, такие как автозапуск шлюза, установку упакованных навыков, создание трея и автоустановку CLI
|
||||
|
||||
Первые два базовых спецификации покрывают:
|
||||
|
||||
- видимость мастера настройки при первом запуске на чистом профиле
|
||||
- пропуск настройки и навигация на страницу Models внутри приложения Electron
|
||||
|
||||
Добавляйте будущие потоки Electron в `tests/e2e/` и переиспользуйте общий fixture в `tests/e2e/fixtures/electron.ts`.
|
||||
|
||||
### Технологический стек
|
||||
|
||||
| Уровень | Технология |
|
||||
|----------------|-------------------------------|
|
||||
| Среда выполнения | Electron 40+ |
|
||||
| UI-фреймворк | React 19 + TypeScript |
|
||||
| Стилизация | Tailwind CSS + shadcn/ui |
|
||||
| Состояние | Zustand |
|
||||
| Сборка | Vite + electron-builder |
|
||||
| Тестирование | Vitest + Playwright |
|
||||
| Анимация | Framer Motion |
|
||||
| Иконки | Lucide React |
|
||||
|
||||
---
|
||||
|
||||
## Участие
|
||||
|
||||
Мы приветствуем вклад сообщества! Исправления ошибок, новые функции, улучшения документации и переводы помогают сделать ClawX лучше.
|
||||
Мы приветствуем вклад сообщества! Исправления багов, новые функции, улучшения документации или переводы — каждый вклад делает ClawX лучше.
|
||||
|
||||
### Как внести вклад
|
||||
|
||||
1. **Сделайте форк** репозитория
|
||||
2. **Создайте** ветку функции (`git checkout -b feature/amazing-feature`)
|
||||
3. **Зафиксируйте** изменения с понятными сообщениями
|
||||
4. **Отправьте** изменения в свою ветку
|
||||
4. **Отправьте** в свою ветку
|
||||
5. **Откройте** Pull Request
|
||||
|
||||
### Руководящие принципы
|
||||
@@ -199,44 +428,54 @@ pnpm package # Упаковать для текущей платформ
|
||||
- Обновляйте документацию по мере необходимости
|
||||
- Держите коммиты атомарными и описательными
|
||||
|
||||
---
|
||||
|
||||
## Благодарности
|
||||
|
||||
ClawX построен на основе следующих отличных проектов с открытым исходным кодом:
|
||||
ClawX построен на плечах отличных проектов с открытым исходным кодом:
|
||||
|
||||
- [OpenClaw](https://github.com/OpenClaw) - Среда выполнения AI-агентов
|
||||
- [Electron](https://www.electronjs.org/) - Кроссплатформенный десктоп-фреймворк
|
||||
- [React](https://react.dev/) - Библиотека UI-компонентов
|
||||
- [shadcn/ui](https://ui.shadcn.com/) - Красиво спроектированные компоненты
|
||||
- [Zustand](https://github.com/pmndrs/zustand) - Лёгкое управление состоянием
|
||||
- [OpenClaw](https://github.com/OpenClaw) – Среда выполнения AI-агентов
|
||||
- [Electron](https://www.electronjs.org/) – Кроссплатформенный десктоп-фреймворк
|
||||
- [React](https://react.dev/) – Библиотека UI-компонентов
|
||||
- [shadcn/ui](https://ui.shadcn.com/) – Красиво спроектированные компоненты
|
||||
- [Zustand](https://github.com/pmndrs/zustand) – Легковесное управление состоянием
|
||||
|
||||
---
|
||||
|
||||
## Сообщество
|
||||
|
||||
Присоединяйтесь к нашему сообществу, чтобы общаться с другими пользователями, получать поддержку и делиться опытом.
|
||||
|
||||
| WeChat Enterprise | Группа Feishu | Discord |
|
||||
| WeChat Enterprise | Feishu Group | Discord |
|
||||
| :---: | :---: | :---: |
|
||||
| <img src="src/assets/community/wecom-qr.png" width="150" alt="WeChat QR Code" /> | <img src="src/assets/community/feishu-qr.png" width="150" alt="Feishu QR Code" /> | <img src="src/assets/community/20260212-185822.png" width="150" alt="Discord QR Code" /> |
|
||||
|
||||
### Партнёрская программа ClawX
|
||||
### Партнёрская программа ClawX 🚀
|
||||
|
||||
Мы запускаем Партнёрскую программу ClawX и ищем партнёров, которые помогут представить ClawX большему числу клиентов, особенно клиентам с потребностями в кастомных AI-агентах или автоматизации.
|
||||
Мы запускаем Партнёрскую программу ClawX и ищем партнёров, которые могут помочь представить ClawX большему числу клиентов, особенно тем, у кого есть потребности в кастомных AI-агентах или автоматизации.
|
||||
|
||||
Партнёры помогают связывать нас с потенциальными пользователями и проектами, а команда ClawX предоставляет полную техническую поддержку, кастомизацию и интеграцию. Если вы работаете с клиентами, заинтересованными в AI-инструментах или автоматизации, мы будем рады сотрудничеству.
|
||||
Партнёры помогают связывать нас с потенциальными пользователями и проектами, а команда ClawX предоставляет полную техническую поддержку, кастомизацию и интеграцию.
|
||||
|
||||
Напишите нам в DM или на [public@valuecell.ai](mailto:public@valuecell.ai), чтобы узнать больше.
|
||||
Если вы работаете с клиентами, заинтересованными в AI-инструментах или автоматизации, мы будем рады сотрудничеству.
|
||||
|
||||
Напишите нам в DM или на [public@valuecell.ai](mailto:public@valuecell.ai) для получения дополнительной информации.
|
||||
|
||||
---
|
||||
|
||||
## История звёзд
|
||||
|
||||
<p align="center">
|
||||
<img src="https://api.star-history.com/svg?repos=ValueCell-ai/ClawX&type=Date" alt="Star History Chart" />
|
||||
<img src="https://api.star-history.com/svg?repos=ValueCell-ai/ClawX&type=Date" alt="Star History Chart" />
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## Лицензия
|
||||
|
||||
ClawX выпускается под [лицензией MIT](LICENSE). Вы можете свободно использовать, изменять и распространять это программное обеспечение.
|
||||
ClawX выпускается под [лицензией MIT](LICENSE). Вы можете свободно использовать, модифицировать и распространять это программное обеспечение.
|
||||
|
||||
<hr>
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<sub>Создано с ❤️ командой ValueCell</sub>
|
||||
<sub>Создано с ❤️ командой ValueCell</sub>
|
||||
</p>
|
||||
|
||||
+325
-60
@@ -10,6 +10,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#功能特性">功能特性</a> •
|
||||
<a href="#为什么选择-clawx">为什么选择 ClawX</a> •
|
||||
<a href="#快速上手">快速上手</a> •
|
||||
<a href="#系统架构">系统架构</a> •
|
||||
@@ -44,25 +45,39 @@ ClawX 预置了最佳实践的模型供应商配置,原生支持 Windows 平
|
||||
|
||||
<p align="center"><strong style="font-size:1.1em; text-decoration: underline;">如需完整的企业版、专属服务支持或面向您业务场景的定制化落地辅导,请联系 <a href="mailto:public@valuecell.ai">public@valuecell.ai</a>。</strong></p>
|
||||
|
||||
---
|
||||
|
||||
## 截图预览
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><img src="resources/screenshot/zh/chat.png" alt="Chat"><br><em>聊天界面</em></td>
|
||||
<td align="center"><img src="resources/screenshot/zh/cron.png" alt="Cron"><br><em>定时任务</em></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><img src="resources/screenshot/zh/skills.png" alt="Skills"><br><em>技能管理</em></td>
|
||||
<td align="center"><img src="resources/screenshot/zh/channels.png" alt="Channels"><br><em>频道管理</em></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><img src="resources/screenshot/zh/models.png" alt="Models"><br><em>模型配置</em></td>
|
||||
<td align="center"><img src="resources/screenshot/zh/settings.png" alt="Settings"><br><em>设置</em></td>
|
||||
</tr>
|
||||
</table>
|
||||
<p align="center">
|
||||
<img src="resources/screenshot/zh/chat.png" style="width: 100%; height: auto;">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="resources/screenshot/zh/cron.png" style="width: 100%; height: auto;">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="resources/screenshot/zh/skills.png" style="width: 100%; height: auto;">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="resources/screenshot/zh/channels.png" style="width: 100%; height: auto;">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="resources/screenshot/zh/models.png" style="width: 100%; height: auto;">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="resources/screenshot/zh/settings.png" style="width: 100%; height: auto;">
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 为什么选择 ClawX
|
||||
|
||||
构建 AI 智能体不应该需要精通命令行。ClawX 的设计理念很简单:**强大的技术值得拥有一个尊重用户时间的界面**。ClawX 直接基于官方 OpenClaw 核心构建。无需单独安装,我们将运行时嵌入应用内部,提供开箱即用的无缝体验,并致力于与上游 OpenClaw 项目保持严格同步,确保你始终可以使用官方发布的最新功能、稳定性改进和生态兼容性。
|
||||
构建 AI 智能体不应该需要精通命令行。ClawX 的设计理念很简单:**强大的技术值得拥有一个尊重用户时间的界面。**
|
||||
|
||||
| 痛点 | ClawX 解决方案 |
|
||||
|------|----------------|
|
||||
@@ -73,26 +88,69 @@ ClawX 预置了最佳实践的模型供应商配置,原生支持 Windows 平
|
||||
| 多 AI 供应商切换 | 统一的供应商配置面板 |
|
||||
| 技能/插件安装复杂 | 内置技能市场与管理界面 |
|
||||
|
||||
### 功能特性
|
||||
### 内置 OpenClaw 核心
|
||||
|
||||
- **🎯 零配置门槛**:从安装到第一次 AI 对话,全程指引式图形界面,无需终端命令、YAML 配置或环境变量。
|
||||
- **💬 智能聊天界面**:多会话上下文与历史记录,流式 Markdown 渲染(语法高亮、CJK 排版、表格、KaTeX 公式)、`@agent` 直接路由与 `/技能` 内联卡片,工作空间优先的会话侧边栏,以及 Markdown、`.docx`、`.pptx` 和本地 HTML 的只读预览。
|
||||
- **📡 多频道管理**:同时配置和监控多个 AI 频道,每个频道独立运行并支持多账号;内置腾讯官方个人微信渠道插件。
|
||||
- **⏰ 定时任务自动化**:可视化定义触发器与时间间隔,让 AI 智能体 7×24 小时自动运行;支持周期(每小时/每天/工作日/每周/自定义 cron)与单次执行,并可将结果自动投递到外部频道。
|
||||
- **🧩 可扩展技能系统**:本地优先的技能管理,扫描托管与 workspace 技能目录,无需依赖 Gateway 即可启用或停用技能;预装文档处理技能(`pdf`、`xlsx`、`docx`、`pptx`)。
|
||||
- **🔐 安全的供应商集成**:支持 OpenAI、Anthropic、Z.AI / GLM 等供应商,凭证经系统原生密钥链安全存储;提供自定义 Provider、OAuth 登录、图像生成端点与兼容网关的降级探测。
|
||||
- **🌙 自适应主题**:支持浅色、深色与跟随系统主题。
|
||||
- **🚀 开机启动控制**:在 设置 → 通用 中开启开机自动启动。
|
||||
- **🔔 更新提示**:启动时自动检查新版本,由你决定是否下载或安装更新。
|
||||
ClawX 直接基于官方 **OpenClaw** 核心构建。无需单独安装,我们将运行时嵌入应用内部,提供开箱即用的无缝体验。
|
||||
|
||||
> 对于功能细节的完整说明,请参阅 [docs/zh-CN/features.md](docs/zh-CN/features.md)。
|
||||
我们致力于与上游 OpenClaw 项目保持严格同步,确保你始终可以使用官方发布的最新功能、稳定性改进和生态兼容性。
|
||||
|
||||
### 典型使用场景
|
||||
---
|
||||
|
||||
- **🤖 个人 AI 助手**:配置一个通用 AI 智能体,可以回答问题、撰写邮件、总结文档并协助处理日常任务——全部通过简洁的桌面界面完成。
|
||||
- **📊 自动化监控**:设置定时智能体来监控新闻动态、追踪价格变动或监听特定事件,结果将推送到你偏好的通知渠道。
|
||||
- **💻 开发者效率工具**:将 AI 融入你的开发工作流,使用智能体进行代码审查、生成文档或自动化重复性编码任务。
|
||||
- **🔄 工作流自动化**:将多个技能串联起来,创建复杂的自动化流水线——处理数据、转换内容、触发操作,全部通过可视化方式编排。
|
||||
## 功能特性
|
||||
|
||||
### 🎯 零配置门槛
|
||||
从安装到第一次 AI 对话,全程通过直观的图形界面完成。无需终端命令,无需 YAML 文件,无需到处寻找环境变量。
|
||||
|
||||
### 💬 智能聊天界面
|
||||
通过现代化的聊天体验与 AI 智能体交互。支持多会话上下文、消息历史记录,并以流式 Markdown 渲染智能体回复,支持带语法高亮的围栏代码块、面向中日韩文本的解析、GitHub 风格表格,以及由 KaTeX 渲染的 LaTeX 数学公式(`$行内$`、`$$块级$$`、`\(行内\)` 和 `\[块级\]`);用户输入则始终按原始文本显示。同时支持在多 Agent 场景下通过主输入框中的 `@agent` 直接路由到目标智能体。围栏代码会保留源码换行、自动软换行,并在流式输出结束后提供本地化的复制操作。
|
||||
从输入框插入的技能会以 `/技能名` 卡片形式显示;点击卡片可在右侧预览栏打开并阅读该技能的 `SKILL.md`。
|
||||
当你使用 `@agent` 选择其他智能体时,ClawX 会直接切换到该智能体自己的对话上下文,而不是经过默认智能体转发。各 Agent 工作区默认彼此分离,但更强的运行时隔离仍取决于 OpenClaw 的 sandbox 配置。
|
||||
会话侧边栏现在以工作空间优先组织:默认工作空间固定在最上方,其它工作空间按自然顺序排列,每个工作空间都可折叠或继续加载更多会话。AI 回复期间,会话行显示加载指示器;未查看的回复完成后显示蓝点;打开会话后恢复显示相对活跃时间,悬停时仍会露出操作按钮。导入的工作空间可从侧边栏标题处重命名,新名称会同步显示在对话输入框下方,同时悬浮标题仍可查看文件系统路径。如果当前所选会话存在有效工作空间,新对话会继承该工作空间,并在首次发送前保持可编辑。对于可编辑的新对话或未绑定对话,输入框的工作空间卡片会打开一个小菜单,列出最近使用及现有会话中的工作空间,并可切回默认工作空间或选择其它目录。如果保存的工作空间文件夹已被移动或删除,Chat 会暂停创建会话并提示选择现有文件夹,而不会持续重试失效路径。不可用的非默认工作空间会在侧边栏显示标记,并可在确认后删除;该操作会永久删除分组中的全部会话。只有永久删除成功后,会话行才会移除且页面才会跳转;删除失败时会保留会话与确认框,方便重试。OpenClaw 生成的 UUID 加日期兜底标题只有在与该会话 ID 匹配时才会被视为缺失标题,随后改用会话的首条用户消息展示,而不会被持久化为会话名称。
|
||||
每个 Agent 还可以单独覆盖自己的 `provider/model` 运行时设置;未覆盖的 Agent 会继续继承全局默认模型。
|
||||
输入框的模型菜单会在独立子菜单中提供四个可直接选择的推理强度:Off、Low、Medium 和 High。使用自定义模型新建对话时,首次发送前也会根据 Gateway 提供的当前 Agent 默认值显示该控件;会话创建后则以会话元数据为准。所选强度仅对当前会话持续生效,并会原样随每次 ACP prompt 传递;选择 Off 即可关闭推理,不再提供单独的“思考”开关。如果 Provider 在回复前意外中止请求,聊天界面会显示可重试错误,而不是静默结束本轮。
|
||||
|
||||
Chat 右侧面板的工作空间和预览选项卡支持以只读方式预览 Markdown、`.docx` 和 `.pptx` 文件。Markdown 文件预览以静态渲染模式提供相同的围栏代码语法高亮、软换行与复制操作、面向中日韩文本的解析和 KaTeX 数学公式支持。预览栏顶部可将当前文件展开至 ClawX 的整个可视区域;再次点击该按钮或按 Esc 即可返回侧栏。旧版 `.doc` 和 `.ppt` 文件不会在应用内预览,而是继续通过操作系统打开。DOCX 的分页效果可能与 Microsoft Word 不同;PPTX 预览不支持动画、切换效果或媒体播放。超过 20 MB 的 Office 文件不会在应用内预览。
|
||||
|
||||
### 本地 HTML 预览
|
||||
Chat 右侧面板只包含工作空间、预览和变更,不再提供通用网页浏览器、主页或地址栏。已授权的本地 `.html` 和 `.htm` 附件、文件活动及工作空间文件默认在预览中打开。文件操作可以选择 ClawX 内置预览或系统应用,预览标题栏也可将当前 HTML 文件交给系统浏览器打开。
|
||||
|
||||
所有链接都不可点击。ClawX 渲染的链接显示为普通文本,HTML 预览中的链接也会移除链接样式和指针交互。HTML 预览同时阻止表单、脚本跳转、重定向、页内跳转、弹窗、下载、网络请求和设备权限;它可以显示自包含的本地 HTML,但无法离开当前选中的文档。
|
||||
|
||||
### 📡 多频道管理
|
||||
同时配置和监控多个 AI 频道。每个频道独立运行,允许你为不同任务运行专门的智能体。
|
||||
现在每个频道支持多个账号,并可在 Channels 页面直接完成账号绑定到 Agent 与默认账号切换。
|
||||
对于自定义频道账号 ID,ClawX 现在会强制校验 OpenClaw 兼容的规范格式(`[a-z0-9_-]`、小写、最长 64 位、且必须以字母或数字开头),避免路由匹配异常。
|
||||
ClawX 现在还内置了腾讯官方个人微信渠道插件,可直接在 Channels 页面通过内置二维码流程完成微信连接。
|
||||
|
||||
### ⏰ 定时任务自动化
|
||||
调度 AI 任务自动执行。定义触发器、设置时间间隔,让 AI 智能体 7×24 小时不间断工作。
|
||||
现在定时任务页面已经可以直接配置外部投递,统一拆成“发送账号”和“接收目标”两个下拉选择。对于已支持的通道,接收目标会从通道目录能力或已知会话历史中自动发现,不需要再手动修改 `jobs.json`。任务的消息输入框也支持像主对话框那样以内联 `/skill` 令牌的方式插入技能(按所选智能体范围加载),让定时提示词可以直接触发技能。调度选择器现在分为**周期**和**单次**两个选项卡:周期支持每小时、每天、工作日、每周、自定义(原始 cron)等频率,并内置时间/星期选择;单次则在所选日期(显示星期)和时间执行一次。单次任务必须设置为未来时间,并会在执行完成后由运行时自动清除。
|
||||
|
||||
|
||||
### 🧩 可扩展技能系统
|
||||
通过预构建的技能扩展 AI 智能体的能力。集成的 Skills 页面采用“本地优先”方式:会扫描托管目录与 workspace 技能目录,并且无需依赖 Gateway 即可启用或停用技能;在企业扩展接管时,也可以显示扩展提供的 marketplace。
|
||||
ClawX 还会内置预装完整的文档处理技能(`pdf`、`xlsx`、`docx`、`pptx`),在启动时自动部署到托管技能目录(默认 `~/.openclaw/skills`),并在首次安装时默认启用。
|
||||
Skills 页面可展示来自多个 OpenClaw 来源的技能(托管目录、workspace、额外技能目录),并显示每个技能的实际路径,便于直接打开真实安装位置。对于 OpenClaw 自带的 bundled skills,社区版现在在打包产物里只保留并展示 `skill-creator`;开发模式和打包版启动时都会直接清理其它 bundled skill,同时把这些已删除 bundled skill 在 `openclaw.json` 中残留的旧配置一并移除。
|
||||
|
||||
### 🔐 安全的供应商集成
|
||||
连接多个 AI 供应商(OpenAI、Anthropic、Z.AI / GLM 等),凭证安全存储在系统原生密钥链中。OpenAI 同时支持 API Key 与浏览器 OAuth(Codex 订阅)登录。
|
||||
在开发者模式下,独立的“图像生成”页面支持配置 OpenAI 兼容生图端点(Base URL、API Key 和模型名,例如 `gpt-image-2`),生图请求会走专用的 `/v1/images/generations` 服务,聊天仍继续使用正常的 OpenAI Provider。
|
||||
如果你通过 **自定义(Custom)Provider** 对接 OpenAI-compatible 网关,可以在 **设置 → AI Providers → 编辑 Provider** 中配置自定义 `User-Agent`,以提高兼容性。
|
||||
编辑或切换 Provider 时,ClawX 会保留已有的模型级能力元数据,例如 `input: ["text", "image"]`。新选择的自定义 Provider 模型会使用与 OpenClaw onboarding 一致的图片输入能力推断;未知模型默认按纯文本模型处理。
|
||||
自定义 Provider 的模型行还会写入显式的 `contextWindow`(按模型系列推断,例如 `gpt-5.x` → 272k),旧版本保存的模型行会在启动时自动回填,使 OpenClaw 能在长会话超限前主动压缩上下文,避免出现 "Context overflow" 报错。自定义 Provider 的主模型会同步开启推理,并写入固定的低 / 中 / 高档位,以便聊天侧可设置会话 thinking;ClawX 不会根据模型名称推断另一套档位。当你没有配置 compaction 时,ClawX 会默认写入 `agents.defaults.compaction.mode = "safeguard"` 和 `reserveTokensFloor = 50000`;你手动配置过的字段不会被覆盖,仅会回填缺失的推断字段。
|
||||
Z.AI(国内站 / 国际站)会映射到 OpenClaw 内置的 `zai` 供应商(`ZAI_API_KEY`),默认模型为 `glm-5.2`。可通过 Code Plan 预设切换到编码套餐端点(`…/api/coding/paas/v4`),或使用普通 API 端点(`…/api/paas/v4`);国内站与国际站互斥,因为它们共享同一个 OpenClaw 运行时 key。
|
||||
如果兼容网关的 `/models` 因非鉴权原因不可用,ClawX 会在校验 API Key 时使用已配置的模型,自动降级为轻量的 `/chat/completions` 或 `/responses` 探测。
|
||||
|
||||
### 🌙 自适应主题
|
||||
支持浅色模式、深色模式或跟随系统主题。ClawX 自动适应你的偏好设置。
|
||||
|
||||
### 🚀 开机启动控制
|
||||
在 **设置 → 通用** 中,你可以开启 **开机自动启动**,让 ClawX 在系统登录后自动启动。
|
||||
|
||||
### 🔔 更新提示
|
||||
ClawX 可以在启动时自动检查新版本。发现更新后会显示应用内提示;只有在你选择操作后,才会下载或安装更新。
|
||||
|
||||
---
|
||||
|
||||
## 快速上手
|
||||
|
||||
@@ -108,7 +166,7 @@ ClawX 预置了最佳实践的模型供应商配置,原生支持 Windows 平
|
||||
|
||||
从 [Releases](https://github.com/ValueCell-ai/ClawX/releases) 页面下载适用于你平台的最新版本。
|
||||
|
||||
#### 从源码开始
|
||||
#### 从源码构建
|
||||
|
||||
```bash
|
||||
# 克隆仓库
|
||||
@@ -130,10 +188,10 @@ pnpm dev
|
||||
3. **技能包** – 选择适用于常见场景的预配置技能
|
||||
4. **验证** – 在进入主界面前测试你的配置
|
||||
|
||||
如果系统语言在支持列表中,向导会默认选中该语言;否则回退到英文。
|
||||
|
||||
> Web search 说明:ClawX 会在 Agent 和 Gateway 两层策略中禁用 OpenClaw 的通用 `web_search` 工具。
|
||||
> 这也包括 Moonshot(Kimi)搜索;受管浏览器自动化和 `web_fetch` 仍然可用。
|
||||
>
|
||||
> 内部工具说明:ClawX 还会在两层策略中对 Agent 禁用 `gateway`、`nodes`、`create_goal`、`get_goal` 和 `update_goal`。ClawX 应用自身的 Gateway RPC 不受影响,消息、会话编排和 Agent 发现工具仍然可用。
|
||||
|
||||
### 代理设置
|
||||
|
||||
@@ -141,45 +199,245 @@ ClawX 内置了代理设置,适用于需要通过本地代理客户端访问
|
||||
|
||||
打开 **设置 → 网关 → 代理**,配置以下内容:
|
||||
|
||||
- **代理服务器**:所有请求默认使用的代理,填写例如 `http://127.0.0.1:7890`
|
||||
- **代理服务器**:所有请求默认使用的代理
|
||||
- **绕过规则**:需要直连的主机,使用分号、逗号或换行分隔
|
||||
- 在 **开发者模式** 下,还可以单独覆盖:HTTP 代理、HTTPS 代理、ALL_PROXY / SOCKS
|
||||
- 在 **开发者模式** 下,还可以单独覆盖:
|
||||
- **HTTP 代理**
|
||||
- **HTTPS 代理**
|
||||
- **ALL_PROXY / SOCKS**
|
||||
|
||||
> 开发者模式覆盖项、Telegram 代理同步与 **OpenClaw Doctor** 等详细行为说明,请参阅 [docs/zh-CN/proxy-settings.md](docs/zh-CN/proxy-settings.md)。
|
||||
本地代理的常见填写示例:
|
||||
|
||||
```text
|
||||
代理服务器: http://127.0.0.1:7890
|
||||
```
|
||||
说明:
|
||||
|
||||
- 只填写 `host:port` 时,会按 HTTP 代理处理。
|
||||
- 高级代理项留空时,会自动回退到“代理服务器”。
|
||||
- 保存代理设置后,Electron 网络层会立即重新应用代理,并自动重启 Gateway。
|
||||
- 如果启用了 Telegram,ClawX 还会把代理同步到 OpenClaw 的 Telegram 频道配置中。
|
||||
- 当 ClawX 代理处于关闭状态时,Gateway 的常规重启会保留已有的 Telegram 频道代理配置。
|
||||
- 如果你要明确清空 OpenClaw 中的 Telegram 代理,请在关闭代理后点一次“保存代理设置”。
|
||||
- 在 **设置 → 高级 → 开发者** 中,可以直接运行 **OpenClaw Doctor**,执行 `openclaw doctor --json` 并在应用内查看诊断输出。
|
||||
- 在 Windows 打包版本中,内置的 `openclaw` CLI/TUI 会通过随包分发的 `node.exe` 入口运行,以保证终端输入行为稳定。
|
||||
|
||||
---
|
||||
|
||||
## 系统架构
|
||||
|
||||
ClawX 采用 **双进程 + Host API 统一接入架构**:React 渲染进程只通过统一的 host-api/api-client 抽象与后端交互,协议选择、Gateway 生命周期与 ACP Chat stdio bridge 全部由 Electron 主进程统一管理。
|
||||
ClawX 采用 **双进程 + Host API 统一接入架构**。渲染进程只调用统一客户端抽象,协议选择与进程生命周期由 Electron 主进程统一管理:
|
||||
|
||||
- **进程模型**:Electron 主进程负责窗口、网关进程监控、系统集成与自动更新;OpenClaw Gateway 作为独立运行时进程提供 AI 编排、频道和技能能力;渲染层不直接访问本地端点。
|
||||
- **配置交付**:Gateway 运行时由 Main 使用 `config.get` / `config.set`,停止或启动中则更新解析后的 JSON5 配置;普通 Provider/Agent/Skill/模型修改不会替换进程,凭据通过 `secrets.reload` 热更新;连续 4 次心跳无响应后才会请求受生命周期保护的自动恢复。
|
||||
- **ACP Chat**:Chat UI 基于 ACP ([Agent Client Protocol](https://agentclientprotocol.com)) 与 OpenClaw 交互,从而在高速迭代的 OpenClaw 前找到相对稳定的聊天协议面。ACP 走 Main 持有的 stdio bridge,支持配置热重载后的历史回放认证、跨页面持续流式输出,以及由 Main 验证和加载的媒体/附件/文件活动(Changes)展示。当受保护的 Gateway 重启中断已接收的对话轮次时,补丁后的 OpenClaw 运行时会将恢复 run 显式关联到原 ACP prompt,使后续文本和工具活动继续进入同一个内存轮次;之后的历史回放也会以原生 ACP 更新恢复持久化的工具边界。
|
||||
- **设计原则**:前端调用单一入口、Main 掌控传输策略、优雅恢复(重连/超时/退避)、安全存储与 CORS 安全。
|
||||
OpenClaw 配置交付也统一由 Electron Main 管理。Gateway 运行时,ClawX 以 `config.get` 返回的权威快照为基线,并通过 `config.set` 提交修改;Gateway 停止或启动中时,同一个协调器只更新解析后的 JSON5 配置文件,不会因此启动 Gateway。因此,普通的 Provider、Agent、Channel、绑定、Skill 和模型修改不会替换 Gateway 进程。完整重启仅保留给代理等进程启动环境变化和用户显式操作。已确认的进程退出与 WebSocket 关闭继续使用现有的自动重连路径。连续前 9 次 WebSocket 心跳无响应只更新诊断,不会因短暂的 pong 延迟中断长时间运行的任务;收到 pong 或任意消息会重置计数,连续第 10 次无响应时才请求受生命周期保护的 Gateway 自动重启。认证配置写入 SQLite 后,ClawX 会调用 OpenClaw 的 `secrets.reload`,让运行中的 Agent 无需重启即可读取新凭据。
|
||||
|
||||
> 完整架构说明(进程图、配置协调、ACP 文件活动语义与 Gateway 排障)请参阅 [docs/zh-CN/architecture.md](docs/zh-CN/architecture.md)。
|
||||
Chat 使用由 Electron Main 持有的 ACP stdio bridge。Renderer 接收类型化 host events,并渲染内存中的 ACP timeline。Gateway 仍负责 providers、models、skills、workspace、settings、diagnostics 和 media configuration 等非 Chat 能力。
|
||||
|
||||
打开其它会话或页面时,尚未完成的 ACP 回复仍会继续流式接收。若在回复完成前返回,ClawX 会恢复最新的内存 timeline 并继续显示实时输出;回复完成后,普通 ACP 历史回放仍是唯一事实来源。
|
||||
|
||||
ACP assistant 回合会显示整轮耗时。Live 计时跟随客户端观测到的 prompt 生命周期,并在应用内导航后保持连续;历史耗时由 Electron Main 根据有界的 OpenClaw transcript 时间戳计算,而且只能标注 ACP 回放已经恢复出的回合。
|
||||
|
||||
实时 assistant 回复进行时,工具活动默认隐藏,避免工具执行过程打断对话显示。回复结束后,所有工具都会以折叠状态显示——连续调用合并为一个折叠分组,单个调用则折叠其详情——并可按需展开;从历史记录恢复的已完成工具也默认折叠。
|
||||
|
||||
ACP Chat 会将标准 ACP resource 渲染为附件。用户选择的图片会显示为缩略图,并在悬停蒙层中显示文件名;其它可用的附件卡片会显示文件名,以及灰色、可截断的来源路径。当前 OpenClaw ACP adapter 遗漏 assistant 媒体时,OpenClaw 持久化的规范媒体事实和显式 assistant `MEDIA:` 指令也可恢复为附件卡片,且不会显示仅用于 transcript 的元数据。现有本地文件引用(包括当前 workspace 外的路径)在每次预览或打开前,都会由 Electron Main 按精确的 session 和 generation 重新验证。AI 生成且可预览的本地附件(包括不超过 20 MB 的 `.docx` 和 `.pptx` 文件)会保留主要的只读应用内预览操作,并提供次级菜单,可通过兼容应用打开,或在 Finder、文件资源管理器或系统文件管理器中显示。对于本地 HTML 附件,该菜单第一项会在右侧预览中打开文件。Office 预览在此处也有相同限制:`.doc` 和 `.ppt` 仍通过系统应用打开,DOCX 的分页效果可能与 Microsoft Word 不同,PPTX 的动画、切换效果和媒体播放不受支持。兼容应用发现仅在 macOS 和 Windows 上可用;在 Linux 上或发现失败时,会静默降级为仅显示文件位置。其它本地文件(包括超过 20 MB 的 Office 文件)会在用户点击后通过系统应用打开。用户选择的文件夹附件在发送后也会保持可用,点击后交给系统文件管理器打开;ClawX 不会读取或预览其中内容。远程 HTTP 和 HTTPS 附件会在用户点击后从外部打开。没有规范媒体事实佐证的普通文本裸路径或行内路径不会被当作附件。
|
||||
|
||||
ACP Chat 也可在 runtime 以可信结构化媒体投递图像生成结果时显示生成图片预览。对于可信的 OpenClaw internal-UI 投递和与生图任务关联的最终回复,ClawX 会保留原始的用户可见完成文案,包括只有文本的失败说明,而不会统一替换成通用图片文案。历史 OpenClaw 回放中,assistant 的图片 `MEDIA:` 标记只有在同一会话已记录图像生成任务启动后才会进入内联图片体验。ClawX 通过 Electron Main 的主机媒体处理加载预览,而不是让 Renderer 任意访问文件系统。标准 ACP 图片和 resource 内容仍是首选路径,并会直接渲染。
|
||||
|
||||
### ACP 文件活动语义
|
||||
|
||||
- 文件活动由成功且已完成的 OpenClaw `write`、`edit` 和 `apply_patch` 调用投影而来。工具识别方式与 OpenClaw 官方 Chat UI 保持一致;仅接收已完成调用的筛选规则是 ClawX 特有的。
|
||||
- 已创建和已修改的活动行与可预览的 assistant 附件共用同一种文件卡片外壳和**打开方式**菜单,同时保留状态文字及可用的 `+/-` 统计。对于 HTML 文件,菜单第一项会在右侧**预览**中打开文件;已删除的活动行只保留 **Changes** 操作。应用列表、指定应用打开和显示文件位置都会由 Electron Main 根据 workspace 根目录与相对路径分别重新验证;工具路径不会因此变成附件,Renderer 也不会获得规范化系统路径。
|
||||
- `write` 按工具声明的语义显示:视为创建,并展示为全部新增的差异,即使该路径可能已经存在。
|
||||
- **Changes** 是按时间顺序记录工具声明活动的会话级记录,不是 Git 输出,也不是相对于已验证源码基线的差异。
|
||||
- 对每个文件,Changes 在每轮助手回复中最多展示一个 diff 编辑器。可安全串联的片段会合并,独立片段会拼接到同一个编辑器中,但不会被描述为基于完整文件基线的差异。
|
||||
- Shell 命令、脚本、用户或 IDE 产生的副作用不会被检测。
|
||||
- 完整的 ACP 回放可以恢复已记录的文件活动;如果回放不完整,ClawX 不会通过回退推断来补造缺失活动。
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────────────────────┐
|
||||
│ ClawX 桌面应用 │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Electron 主进程 │ │
|
||||
│ │ • 窗口与应用生命周期管理 │ │
|
||||
│ │ • 网关进程监控 │ │
|
||||
│ │ • 系统集成(托盘、通知、密钥链) │ │
|
||||
│ │ • 自动更新编排 │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ │ IPC (权威控制面) │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ React 渲染进程 │ │
|
||||
│ │ • 现代组件化 UI(React 19) │ │
|
||||
│ │ • Zustand 状态管理 │ │
|
||||
│ │ • 统一 host-api/api-client 调用 │ │
|
||||
│ │ • 回复使用 Markdown,用户输入按原文显示 │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
│ 类型化 IPC 请求
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 主进程 Host Services 与 Gateway Manager │
|
||||
│ │
|
||||
│ • host:invoke 类型化服务分发 │
|
||||
│ • 设置、文件、会话、技能、供应商、诊断服务 │
|
||||
│ • 主进程持有 Gateway WebSocket 并负责进程监控 │
|
||||
└──────────────────────────────┬──────────────────────────────────┘
|
||||
│
|
||||
│ 主进程持有 WebSocket
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ OpenClaw 网关 │
|
||||
│ │
|
||||
│ • AI 智能体运行时与编排 │
|
||||
│ • 消息频道管理 │
|
||||
│ • 技能/插件执行环境 │
|
||||
│ • 供应商抽象层 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
### 设计原则
|
||||
|
||||
- **进程隔离**:AI 运行时在独立进程中运行,确保即使在高负载计算期间 UI 也能保持响应
|
||||
- **前端调用单一入口**:渲染层统一走 host-api/api-client,不感知底层协议细节
|
||||
- **主进程掌控传输策略**:ACP Chat stdio bridge 与 Gateway 传输都由 Electron Main 持有,渲染进程通过类型化 IPC 调用 Main
|
||||
- **扩展 IPC 贡献点**:主进程扩展通过类型化 IPC 注册表贡献 host-api action,而不是挂载 HTTP route
|
||||
- **优雅恢复**:内置重连、超时、退避逻辑,自动处理瞬时故障
|
||||
- **安全存储**:API 密钥和敏感数据利用操作系统原生的安全存储机制
|
||||
- **CORS 安全**:渲染进程不直接请求本地 Gateway 或 Host API HTTP 端点
|
||||
|
||||
### 进程模型与 Gateway 排障
|
||||
|
||||
- ClawX 基于 Electron,**单个应用实例出现多个系统进程是正常现象**(main/renderer/zygote/utility)。
|
||||
- 单实例保护同时使用 Electron 自带锁与本地进程文件锁回退机制,可在桌面会话总线异常时避免重复启动。
|
||||
- 滚动升级期间若新旧版本混跑,单实例保护仍可能出现不对称行为。为保证稳定性,建议桌面客户端尽量统一升级到同一版本。
|
||||
- 但 OpenClaw Gateway 监听应始终保持**单实例**:`127.0.0.1:18789` 只能有一个监听者。
|
||||
- Gateway readiness 以 OpenClaw 的 `system-presence`、`health`、`status` 等核心信号为准;memory 或频道失败会显示为能力降级,而不是全局 Gateway 故障。
|
||||
- 可用以下命令确认监听进程:
|
||||
- macOS/Linux:`lsof -nP -iTCP:18789 -sTCP:LISTEN`
|
||||
- Windows(PowerShell):`Get-NetTCPConnection -LocalPort 18789 -State Listen`
|
||||
- 点击窗口关闭按钮(`X`)默认只是最小化到托盘,并不会完全退出应用。请在托盘菜单中选择 **Quit ClawX** 执行完整退出。
|
||||
|
||||
---
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 🤖 个人 AI 助手
|
||||
配置一个通用 AI 智能体,可以回答问题、撰写邮件、总结文档并协助处理日常任务——全部通过简洁的桌面界面完成。
|
||||
|
||||
### 📊 自动化监控
|
||||
设置定时智能体来监控新闻动态、追踪价格变动或监听特定事件。结果将推送到你偏好的通知渠道。
|
||||
|
||||
### 💻 开发者效率工具
|
||||
将 AI 融入你的开发工作流。使用智能体进行代码审查、生成文档或自动化重复性编码任务。
|
||||
|
||||
### 🔄 工作流自动化
|
||||
将多个技能串联起来,创建复杂的自动化流水线。处理数据、转换内容、触发操作——全部通过可视化方式编排。
|
||||
|
||||
---
|
||||
|
||||
## 开发指南
|
||||
|
||||
### 前置要求
|
||||
|
||||
- **Node.js**:22.22.3+ / 24.15.0+(推荐) / 25.9.0+
|
||||
- **包管理器**:pnpm 9+
|
||||
- **Linux(Ubuntu/Debian)**:运行 Electron 前需先安装系统库,见 [docs/zh-CN/development.md](docs/zh-CN/development.md)
|
||||
- **Node.js**:对应主版本范围内的 22.22.3+、24.15.0+ 或 25.9.0+(推荐 Node 24 LTS)
|
||||
- **包管理器**:pnpm 9+(推荐)或 npm
|
||||
- **Linux(Ubuntu/Debian)**:运行 Electron 前,请先安装所需系统库:
|
||||
```bash
|
||||
sudo apt-get install -y libnss3 libgtk-3-0 libxss1 libxtst6 libatspi2.0-0 libnotify4 xdg-utils
|
||||
```
|
||||
在 Ubuntu 24.04+ 上,部分软件包使用 `t64` 后缀,运行上述命令后 `apt` 会自动选择正确版本。
|
||||
|
||||
### 项目结构
|
||||
|
||||
```ClawX/
|
||||
├── electron/ # Electron 主进程
|
||||
│ ├── services/ # 类型化 Host API、Provider、Secrets 与运行时服务
|
||||
│ │ ├── providers/ # Provider/account 模型同步逻辑
|
||||
│ │ └── secrets/ # 系统钥匙串与密钥存储
|
||||
│ ├── shared/ # 共享 Provider schema/常量
|
||||
│ │ └── providers/
|
||||
│ ├── main/ # 应用入口、窗口、IPC 注册
|
||||
│ ├── gateway/ # OpenClaw 网关进程管理
|
||||
│ ├── preload/ # 安全 IPC 桥接
|
||||
│ └── utils/ # 工具模块(存储、认证、路径)
|
||||
├── src/ # React 渲染进程
|
||||
│ ├── lib/ # 前端统一 API 与错误模型
|
||||
│ ├── stores/ # Zustand 状态仓库(settings/chat/gateway)
|
||||
│ ├── components/ # 可复用 UI 组件
|
||||
│ ├── pages/ # Setup/Dashboard/Chat/Channels/Skills/Cron/Settings
|
||||
│ ├── i18n/ # 国际化资源
|
||||
│ └── types/ # TypeScript 类型定义
|
||||
├── tests/
|
||||
│ ├── e2e/ # Playwright Electron 端到端冒烟测试
|
||||
│ └── unit/ # Vitest 单元/集成型测试
|
||||
├── resources/ # 静态资源(图标、图片)
|
||||
└── scripts/ # 构建与工具脚本
|
||||
```
|
||||
### 常用命令
|
||||
|
||||
```bash
|
||||
pnpm run init # 初始化开发环境(安装依赖并下载捆绑运行时)
|
||||
pnpm dev # 以热重载模式启动
|
||||
pnpm lint # ESLint 检查
|
||||
pnpm typecheck # TypeScript 类型检查
|
||||
pnpm test # 单元测试
|
||||
pnpm run test:e2e # Electron E2E 冒烟测试
|
||||
pnpm build # 完整生产构建
|
||||
pnpm package # 为当前平台打包(可用 :mac / :win / :linux 后缀)
|
||||
# 开发
|
||||
pnpm run init # 安装依赖并下载捆绑二进制(uv、agent-browser)
|
||||
pnpm dev # 以热重载模式启动(若缺失会自动准备预装技能包)
|
||||
|
||||
# 代码质量
|
||||
pnpm lint # 运行 ESLint 检查
|
||||
pnpm typecheck # TypeScript 类型检查
|
||||
|
||||
# 测试
|
||||
pnpm test # 运行单元测试
|
||||
pnpm run test:e2e # 运行 Electron E2E 冒烟测试
|
||||
pnpm run test:e2e:headed # 以可见窗口运行 Electron E2E 测试
|
||||
pnpm run perf:chat # 采集合成 Chat 场景的 Renderer/Main CPU Profile
|
||||
pnpm run profile:main # 启动构建产物并在 9229 端口调试 Main
|
||||
pnpm run comms:replay # 计算通信回放指标
|
||||
pnpm run comms:baseline # 刷新通信基线快照
|
||||
pnpm run comms:compare # 将回放指标与基线阈值对比
|
||||
|
||||
# 构建与打包
|
||||
pnpm run build:vite # 仅构建前端
|
||||
pnpm build # 完整生产构建(含打包资源)
|
||||
pnpm package # 为当前平台打包(包含预装技能资源)
|
||||
pnpm package:mac # 为 macOS 打包
|
||||
pnpm package:win # 为 Windows 打包
|
||||
pnpm package:linux # 为 Linux 打包
|
||||
```
|
||||
|
||||
> 项目结构、技术栈、完整命令列表、E2E 并行策略、性能诊断与通信回归检查等细节,请参阅 [docs/zh-CN/development.md](docs/zh-CN/development.md)。
|
||||
在无头 Linux 环境下,Electron 测试需要显示服务;可使用 `xvfb-run -a pnpm run test:e2e`。
|
||||
|
||||
### Electron 性能诊断
|
||||
|
||||
`pnpm run perf:chat` 会运行隔离的合成 ACP 负载,分别覆盖流式响应,以及富 Markdown 静态会话中的侧栏和滚动交互,并在 Playwright 的 `test-results/` 目录输出版本化指标与 Renderer/Main CPU Profile。Renderer Profile 覆盖生产 store/render 路径和帧节奏;流式 Main Profile 测量 Main 到 Renderer 的 IPC fanout,交互 Main Profile 用于确认 Renderer 交互期间 Main 是否保持空闲。两者都不包含上游 OpenClaw/ACP 子进程或 GPU 进程路径。CPU Profile 可直接用 Chrome DevTools 打开;其中只包含生成的测试文本,不会上报为产品遥测。性能数据依赖硬件,应在同一机器上多次运行后对比,不应使用统一的跨平台绝对阈值。
|
||||
|
||||
录制真实 Renderer 时,使用 `CLAWX_REMOTE_DEBUGGING_PORT=9223 pnpm dev` 启动开发环境,再让 Playwright 或 Chrome DevTools 连接 `localhost:9223`。录制真实 Electron Main 时,运行 `pnpm run profile:main`,在 `chrome://inspect` 中配置 `localhost:9229` 并选择 Electron Main target。除非正在测量 WebSocket trace 本身,否则不要设置 `CLAWX_GATEWAY_WS_TRACE`。
|
||||
|
||||
ClawX 默认保留 Chromium 硬件加速,使长文档、滚动和布局动画能够使用 GPU 合成与光栅化。若某台机器的显卡驱动存在问题,仍可使用 Chromium 原生的 `--disable-gpu` 命令行参数作为排障回退。
|
||||
|
||||
### 通信回归检查
|
||||
|
||||
当 PR 涉及通信链路(Gateway 事件、ACP Chat bridge 收发流程、Channel 投递、传输回退)时,建议执行:
|
||||
|
||||
```bash
|
||||
pnpm run comms:replay
|
||||
pnpm run comms:compare
|
||||
```
|
||||
|
||||
CI 中的 `comms-regression` 会校验必选场景与阈值。
|
||||
### 技术栈
|
||||
|
||||
| 层级 | 技术 |
|
||||
|------|------|
|
||||
| 运行时 | Electron 40+ |
|
||||
| UI 框架 | React 19 + TypeScript |
|
||||
| 样式 | Tailwind CSS + shadcn/ui |
|
||||
| 状态管理 | Zustand |
|
||||
| 构建工具 | Vite + electron-builder |
|
||||
| 测试 | Vitest + Playwright |
|
||||
| 动画 | Framer Motion |
|
||||
| 图标 | Lucide React |
|
||||
|
||||
---
|
||||
|
||||
## 参与贡献
|
||||
|
||||
@@ -188,8 +446,10 @@ pnpm package # 为当前平台打包(可用 :mac / :win / :linux 后
|
||||
### 如何贡献
|
||||
|
||||
1. **Fork** 本仓库
|
||||
2. **创建** 功能分支(`git checkout -b feature/amazing-feature`),进行开发
|
||||
3. **提交** 清晰描述的变更,**推送** 到你的分支,并**创建** Pull Request
|
||||
2. **创建** 功能分支(`git checkout -b feature/amazing-feature`)
|
||||
3. **提交** 清晰描述的变更
|
||||
4. **推送** 到你的分支
|
||||
5. **创建** Pull Request
|
||||
|
||||
### 贡献规范
|
||||
|
||||
@@ -198,6 +458,7 @@ pnpm package # 为当前平台打包(可用 :mac / :win / :linux 后
|
||||
- 按需更新文档
|
||||
- 保持提交原子化且描述清晰
|
||||
|
||||
---
|
||||
|
||||
## 致谢
|
||||
|
||||
@@ -209,6 +470,7 @@ ClawX 构建于以下优秀的开源项目之上:
|
||||
- [shadcn/ui](https://ui.shadcn.com/) – 精美设计的组件库
|
||||
- [Zustand](https://github.com/pmndrs/zustand) – 轻量级状态管理
|
||||
|
||||
---
|
||||
|
||||
## 社区
|
||||
|
||||
@@ -222,10 +484,13 @@ ClawX 构建于以下优秀的开源项目之上:
|
||||
|
||||
我们正在启动 ClawX 合作伙伴计划,寻找能够帮助我们将 ClawX 介绍给更多客户的合作伙伴,尤其是那些有定制化 AI 智能体或自动化需求的客户。
|
||||
|
||||
合作伙伴负责帮助我们连接潜在用户和项目,ClawX 团队则提供完整的技术支持、定制开发与集成服务。如果你服务的客户对 AI 工具或自动化方案感兴趣,欢迎与我们合作。
|
||||
合作伙伴负责帮助我们连接潜在用户和项目,ClawX 团队则提供完整的技术支持、定制开发与集成服务。
|
||||
|
||||
如果你服务的客户对 AI 工具或自动化方案感兴趣,欢迎与我们合作。
|
||||
|
||||
欢迎私信我们,或发送邮件至 [public@valuecell.ai](mailto:public@valuecell.ai) 了解更多。
|
||||
|
||||
---
|
||||
|
||||
## Stars 历史
|
||||
|
||||
@@ -233,13 +498,13 @@ ClawX 构建于以下优秀的开源项目之上:
|
||||
<img src="https://api.star-history.com/svg?repos=ValueCell-ai/ClawX&type=Date" alt="Stars 历史图表" />
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 许可证
|
||||
|
||||
ClawX 基于 [MIT 许可证](LICENSE) 发布。你可以自由地使用、修改和分发本软件。
|
||||
|
||||
<hr>
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<sub>由 ValueCell 团队用 ❤️ 打造</sub>
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
# ClawX Architecture
|
||||
|
||||
This document provides the detailed version of the Architecture section in the README.
|
||||
|
||||
ClawX uses a **dual-process architecture with a unified Host API layer**. The renderer calls one client abstraction, while protocol selection and process lifecycle are managed by Electron Main:
|
||||
|
||||
OpenClaw configuration delivery is also managed by Electron Main. While the Gateway is running, ClawX uses the authoritative snapshot returned by `config.get` as its baseline and commits changes with `config.set`. While the Gateway is stopped or starting, the same coordinator updates the resolved JSON5 configuration file without starting the Gateway. Ordinary provider, agent, channel, binding, skill, and model changes therefore do not replace the Gateway process. Full restarts are reserved for process-launch environment changes such as proxy settings and explicit user actions. Confirmed process exits and WebSocket closes retain their existing automatic reconnect paths. The first three consecutive WebSocket heartbeat misses remain diagnostic-only so brief pong delays do not interrupt long-running work; a pong or any incoming message resets the count, while a fourth consecutive miss requests guarded automatic Gateway recovery when the lifecycle is in an auto-recoverable running state. After authentication configuration is written to SQLite, ClawX calls OpenClaw's `secrets.reload` so running agents can read new credentials without a process restart.
|
||||
|
||||
Chat uses an ACP stdio bridge owned by Electron Main. Main passes the same app-managed Gateway token to this local child through its private process environment, so ACP history replay remains authenticated when the runtime configuration reloads. If guarded Gateway recovery interrupts an accepted main-session run, the patched OpenClaw runtime starts a distinct recovery run carrying the interrupted run id as explicit lineage. Chat and agent events preserve that lineage; the reconnecting ACP bridge adopts the new run for its pending prompt, resets per-run stream cursors, and subscribes to session-scoped tool events. The renderer remains unaware of Gateway runtime identity and continues to receive typed host events for one in-memory ACP timeline. The Gateway remains responsible for non-Chat capabilities such as providers, models, skills, workspace, settings, diagnostics, and media configuration.
|
||||
|
||||
### ACP Semantic Authority
|
||||
|
||||
ACP is the preferred semantic authority for every Chat meaning and context that it exposes. This includes session identity and routing where applicable, workspace and execution `cwd`, prompt and timeline state, and standard resource or attachment semantics. When ACP provides a value or event, Main and Renderer must use it rather than replace it with a Gateway snapshot, transcript inference, local configuration, or a parallel projection.
|
||||
|
||||
A bypass is allowed only when upstream ACP has no equivalent. Such a compatibility path must be narrow, bounded, session- and generation-scoped, and documented with its rationale, source of truth, limits, reconciliation behavior, and removal condition in the relevant Harness reference or rule. It must not silently become a competing authority.
|
||||
|
||||
### ACP History Authority and Bounded Transcript Supplements
|
||||
|
||||
ACP `session/load` replay is the primary authority for Chat history. ClawX does not persist a second ACP ledger, reduced timeline, replay cache, or reconstructed tool history. If OpenClaw's structured ACP event ledger is unavailable, its ACP adapter reconstructs persisted transcript `toolCall` and `toolResult` records as native tool updates in transcript order, preserving text-tool-text boundaries; ClawX does not infer those records itself. Some OpenClaw capabilities do not yet have fully corresponding ACP implementations; for example, assistant media may be omitted from ACP and Gateway processing may remove assistant `MEDIA:` directives from the visible live reply. ClawX therefore keeps only bounded, marked, memory-only compatibility supplements:
|
||||
|
||||
- Asynchronous image-generation completions may be restored only when the same session has proven `image_generate` context and the completion evidence is trusted or approved transcript evidence.
|
||||
- General attachments may be recovered from canonical persisted assistant `__openclaw.media` facts or explicit line-leading assistant `MEDIA:` directives. This recovers attachment references and declared metadata, not the surrounding assistant message.
|
||||
- Main may add metadata-only whole-turn timing from bounded transcript JSONL records because ACP replay does not provide the original event timestamps. It can annotate only an already restored ACP turn.
|
||||
- If a cron session has completely empty ACP replay, Main's typed cron-history API may provide the scheduled prompt and completion summary. When an identified run summary contains OpenClaw's truncation marker, Main may recover the final assistant text from that run's transcript only when the transcript is longer and shares the complete persisted summary prefix.
|
||||
|
||||
Historical reads are bounded to the newest 1000 transcript messages. A successful live prompt performs one immediate read and one retry after 1500 ms. Every supplement is scoped to the exact session, ACP generation, operation, and live user turn where applicable; stale, missing, duplicate, or ambiguous matches are discarded. These paths must not reconstruct ordinary assistant messages, thoughts, tools, plans, permissions, file activity, missing turns, or a parallel Chat history, and Main must not manufacture native ACP events from transcript evidence. Standard ACP resources remain preferred, and these compatibility exceptions should be removed when upstream emits equivalent content.
|
||||
|
||||
An unfinished ACP response continues streaming when you open another conversation or page. Returning before it finishes restores the latest in-memory timeline and continues the live response. Once it finishes, normal ACP history replay remains the source of truth.
|
||||
|
||||
ACP assistant turns show whole-turn duration. Live timing follows the client-observed prompt lifecycle and survives in-app navigation. Historical timing is derived in Electron Main from bounded OpenClaw transcript timestamps and only annotates a turn already restored by ACP replay.
|
||||
|
||||
ACP Chat renders standard ACP resources as attachments. User-selected images appear as thumbnails with a filename hover overlay, while other available attachment cards show the filename and a muted, truncating source path. When the current OpenClaw ACP adapter omits assistant media, canonical persisted OpenClaw media facts and explicit assistant `MEDIA:` directives can also be recovered as attachment cards without displaying transcript-only metadata.
|
||||
|
||||
Existing local file references, including paths outside the active workspace, are revalidated in Electron Main for the exact session and generation before every preview or open. Previewable local attachments produced by the AI, including `.docx` and `.pptx` files within the 20 MB inline-preview limit, keep their primary read-only in-app preview action and provide a secondary menu for opening with compatible applications or revealing the file in Finder, File Explorer, or the system file manager. For local HTML attachments, that menu starts with an action that opens the file in the right-side Preview tab.
|
||||
|
||||
The same Office limitations apply here: `.doc` and `.ppt` remain system-open formats, DOCX pagination may differ from Microsoft Word, and PPTX animations, transitions, and media playback are unsupported. Compatible-application discovery is available only on macOS and Windows and silently degrades to reveal-only behavior on Linux or when discovery fails. Other local files, including Office files larger than 20 MB, open in the system application after a user click. User-selected folder attachments remain available after send and open in the system file manager; ClawX does not read or preview their contents. Remote HTTP and HTTPS attachments open externally after a user click. Bare or inline prose paths without canonical media facts are not treated as attachments.
|
||||
|
||||
ACP Chat can also display generated image previews when image-generation media is delivered by the runtime as trusted structured media. Trusted OpenClaw internal-UI deliveries and task-correlated final replies preserve the original user-facing completion text, including text-only failure explanations, rather than replacing it with a generic image caption. During historical OpenClaw replay, assistant image `MEDIA:` markers are promoted to the inline image experience only when they follow a recorded image-generation task start for that session. ClawX loads previews through host media handling in Electron Main, not arbitrary renderer filesystem access. Standard ACP image and resource content remains the preferred path and renders directly.
|
||||
|
||||
### ACP File Activity Semantics
|
||||
|
||||
- File activity is projected from successful, completed OpenClaw `write`, `edit`, and `apply_patch` calls. Tool recognition follows the official OpenClaw Chat UI; filtering to completed calls is specific to ClawX.
|
||||
- Created and modified activity rows use the same file-card shell and **Open with** menu as previewable assistant attachments while retaining their status and optional `+/-` summary. For HTML files, the first menu item opens the file in the right-side **Preview** tab. Deleted rows keep only the **Changes** action. Every application-list, selected-application, and reveal request is independently revalidated in Electron Main from the workspace root and relative path. Tool-derived paths never become attachments or expose canonical native paths to the renderer.
|
||||
- A `write` is shown as the tool declares it: a creation with an all-added diff, even if the path may already exist.
|
||||
- **Changes** is a chronological, session-level record of tool-declared activity. It is not Git output or a verified diff against a source baseline.
|
||||
- For each file, Changes renders at most one diff editor per assistant turn. Sequential fragments are composed when safe; independent fragments share one concatenated editor without claiming a complete-file baseline.
|
||||
- Side effects made by shell commands, scripts, users, or IDEs are not detected.
|
||||
- A full ACP replay can restore recorded file activity. If replay is incomplete, ClawX does not infer missing activity through fallback behavior.
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ ClawX Desktop App │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Electron Main Process │ │
|
||||
│ │ • Window and application lifecycle management │ │
|
||||
│ │ • Gateway process supervision │ │
|
||||
│ │ • System integration (tray, notifications, keychain) │ │
|
||||
│ │ • Auto-update orchestration │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
│ IPC (authoritative control plane)
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ React Renderer Process │
|
||||
│ • Modern component-based UI (React 19) │
|
||||
│ • State management with Zustand │
|
||||
│ • Unified host-api/api-client calls │
|
||||
│ • Markdown assistant replies, literal user input │
|
||||
└──────────────────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
│ Typed IPC requests
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ Main Host Services and Gateway Manager │
|
||||
│ • host:invoke typed service dispatcher │
|
||||
│ • Settings, files, sessions, skills, providers, diagnostics │
|
||||
│ • Main-owned Gateway WebSocket and process supervision │
|
||||
└──────────────────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
│ Main-owned WebSocket
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ OpenClaw Gateway │
|
||||
│ • AI agent runtime and orchestration │
|
||||
│ • Message channel management │
|
||||
│ • Skill/plugin execution environment │
|
||||
│ • Provider abstraction layer │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Design Principles
|
||||
|
||||
- **Process Isolation**: The AI runtime operates in a separate process, keeping the UI responsive even during heavy computation.
|
||||
- **Single Entry for Frontend Calls**: Renderer requests go through `host-api` / `api-client`; protocol details are hidden behind a stable interface.
|
||||
- **Main-Process Transport Ownership**: Electron Main owns the ACP Chat stdio bridge and Gateway transports; the renderer talks to Main over typed IPC.
|
||||
- **Extension IPC Contributions**: Main-process extensions contribute host-api actions through the typed IPC registry instead of HTTP routes.
|
||||
- **Graceful Recovery**: Built-in reconnect, timeout, and backoff logic handles transient failures automatically.
|
||||
- **Secure Storage**: API keys and sensitive data use the operating system's native secure storage mechanisms.
|
||||
- **CORS-Safe by Design**: The renderer does not call local Gateway or Host API HTTP endpoints directly.
|
||||
|
||||
### Process Model and Gateway Troubleshooting
|
||||
|
||||
- ClawX is an Electron app, so **one app instance normally appears as multiple OS processes** (main/renderer/zygote/utility). This is expected.
|
||||
- Single-instance protection uses Electron's lock plus a local process-file lock fallback, preventing duplicate app launches in environments where desktop IPC or the session bus is unstable.
|
||||
- During rolling upgrades, mixed old and new app versions can still have asymmetric protection behavior. For best reliability, upgrade all desktop clients to the same version.
|
||||
- The OpenClaw Gateway listener should still be **single-owner**: only one process should listen on `127.0.0.1:18789`.
|
||||
- Gateway readiness is based on OpenClaw core signals such as `system-presence`, `health`, and `status`. Memory or channel failures are shown as capability degradation rather than global Gateway failure.
|
||||
- To verify the active listener:
|
||||
- macOS/Linux: `lsof -nP -iTCP:18789 -sTCP:LISTEN`
|
||||
- Windows (PowerShell): `Get-NetTCPConnection -LocalPort 18789 -State Listen`
|
||||
- Clicking the window close button (`X`) hides ClawX to the tray; it does not fully quit the app. Use **Quit ClawX** in the tray menu for a complete shutdown.
|
||||
@@ -1,130 +0,0 @@
|
||||
# ClawX Development Guide
|
||||
|
||||
This document provides the detailed version of the Development section in the README.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Node.js**: 22.22.3+, 24.15.0+, or 25.9.0+ within the corresponding supported major line (Node 24 LTS recommended)
|
||||
- **Package Manager**: pnpm 9+ (npm is also supported)
|
||||
- **Linux (Ubuntu/Debian)**: Install the required system libraries before running Electron:
|
||||
```bash
|
||||
sudo apt-get install -y libnss3 libgtk-3-0 libxss1 libxtst6 libatspi2.0-0 libnotify4 xdg-utils
|
||||
```
|
||||
On Ubuntu 24.04+, some packages use a `t64` suffix; `apt` automatically selects the correct variant when you run the command above.
|
||||
|
||||
### Project Structure
|
||||
|
||||
```text
|
||||
ClawX/
|
||||
├── electron/ # Electron Main Process
|
||||
│ ├── services/ # Typed Host API, provider, secrets, and runtime services
|
||||
│ │ ├── providers/ # Provider/account model sync logic
|
||||
│ │ └── secrets/ # OS keychain and secret storage
|
||||
│ ├── shared/ # Shared provider schemas/constants
|
||||
│ │ └── providers/
|
||||
│ ├── main/ # App entry, windows, and IPC registration
|
||||
│ ├── gateway/ # OpenClaw Gateway process manager
|
||||
│ ├── preload/ # Secure IPC bridge
|
||||
│ └── utils/ # Utilities for storage, auth, and paths
|
||||
├── src/ # React Renderer Process
|
||||
│ ├── lib/ # Unified frontend API and error model
|
||||
│ ├── stores/ # Zustand stores (settings/chat/gateway)
|
||||
│ ├── components/ # Reusable UI components
|
||||
│ ├── pages/ # Setup/Dashboard/Chat/Channels/Skills/Cron/Settings
|
||||
│ ├── i18n/ # Localization resources
|
||||
│ └── types/ # TypeScript type definitions
|
||||
├── tests/
|
||||
│ ├── e2e/ # Playwright Electron end-to-end smoke tests
|
||||
│ └── unit/ # Vitest unit and integration-like tests
|
||||
├── resources/ # Static assets (icons and images)
|
||||
└── scripts/ # Build and utility scripts
|
||||
```
|
||||
|
||||
### Available Commands
|
||||
|
||||
```bash
|
||||
# Development
|
||||
pnpm run init # Install dependencies and download bundled binaries (uv, agent-browser)
|
||||
pnpm dev # Start with hot reload (auto-prepares bundled skills if missing)
|
||||
|
||||
# Quality
|
||||
pnpm lint # Run ESLint
|
||||
pnpm typecheck # TypeScript validation
|
||||
|
||||
# Testing
|
||||
pnpm test # Run unit tests
|
||||
pnpm run test:e2e # Run Electron E2E smoke tests
|
||||
pnpm run test:e2e:headed # Run Electron E2E tests with a visible window
|
||||
pnpm run perf:chat # Capture synthetic Chat Renderer/Main CPU profiles
|
||||
pnpm run profile:main # Launch the built app with Main inspector on port 9229
|
||||
pnpm run comms:replay # Compute communication replay metrics
|
||||
pnpm run comms:baseline # Refresh the communication baseline snapshot
|
||||
pnpm run comms:compare # Compare replay metrics against baseline thresholds
|
||||
|
||||
# Build and Package
|
||||
pnpm run build:vite # Build the frontend only
|
||||
pnpm build # Full production build with packaging assets
|
||||
pnpm package # Package for the current platform with bundled skills
|
||||
pnpm package:mac # Package for macOS
|
||||
pnpm package:win # Package for Windows
|
||||
pnpm package:linux # Package for Linux
|
||||
```
|
||||
|
||||
On headless Linux, Electron tests need a display service. Use `xvfb-run -a pnpm run test:e2e`.
|
||||
|
||||
Electron E2E functional tests use two Playwright workers by default both locally and in CI. Set `CLAWX_E2E_WORKERS=<positive integer>` to tune the ordinary parallel lane for the machine. Tests that touch OS-global state use the one-worker `exclusive` project, and host performance profiles run alone afterward. New E2E tests are parallel by default; apply `E2E_EXCLUSIVE_TAG` from `tests/e2e/parallel-policy.ts` when a test uses the real clipboard or another machine-global resource.
|
||||
|
||||
For a focused ordinary spec that does not need the exclusive prerequisite, run `pnpm exec playwright test <spec> --project=parallel --no-deps`.
|
||||
|
||||
### Electron Performance Diagnostics
|
||||
|
||||
`pnpm run perf:chat` runs isolated synthetic ACP workloads for streaming and for rich static Markdown sidebar and scroll interaction. It writes versioned metrics plus Renderer and Main CPU profiles under the Playwright `test-results/` directory. Renderer profiles cover the production store/render path and frame pacing. The streaming Main profile measures Main-to-Renderer IPC fanout, while the interaction Main profile shows whether Main remains idle while Renderer interactions run. Neither includes the upstream OpenClaw/ACP subprocess or GPU-process paths.
|
||||
|
||||
Open a CPU profile in Chrome DevTools. The artifacts contain generated fixture text only and are not product telemetry. Results are hardware-dependent, so compare repeated runs on the same machine instead of applying one cross-platform absolute threshold.
|
||||
|
||||
For a live Renderer recording, start development with `CLAWX_REMOTE_DEBUGGING_PORT=9223 pnpm dev` and attach Playwright or Chrome DevTools to `localhost:9223`. For a live Electron Main recording, run `pnpm run profile:main`, open `chrome://inspect`, configure `localhost:9229`, and select the Electron Main target. Leave `CLAWX_GATEWAY_WS_TRACE` unset unless WebSocket tracing itself is being measured.
|
||||
|
||||
ClawX leaves Chromium hardware acceleration enabled by default so long documents, scrolling, and layout animations can use GPU compositing and rasterization. Chromium still honors the native `--disable-gpu` command-line switch as a troubleshooting fallback for a machine with a broken graphics driver.
|
||||
|
||||
### Communication Regression Checks
|
||||
|
||||
When a PR changes communication paths such as Gateway events, the ACP Chat bridge send/receive flow, channel delivery, or transport fallback, run:
|
||||
|
||||
```bash
|
||||
pnpm run comms:replay
|
||||
pnpm run comms:compare
|
||||
```
|
||||
|
||||
The `comms-regression` CI job enforces required scenarios and threshold checks.
|
||||
|
||||
### Electron E2E Tests
|
||||
|
||||
The Playwright Electron suite launches the packaged renderer and Main process from `dist/` and `dist-electron/`, so it does not require manually running `pnpm dev` first.
|
||||
|
||||
`pnpm run test:e2e` automatically:
|
||||
|
||||
- builds the renderer and Electron bundles with `pnpm run build:vite`
|
||||
- starts Electron in an isolated E2E mode with a temporary `HOME`
|
||||
- uses a temporary ClawX `userData` directory
|
||||
- runs ordinary spec files concurrently while fencing OS-global and performance tests
|
||||
- skips heavy startup side effects such as Gateway auto-start, bundled skill installation, tray creation, and CLI auto-install
|
||||
|
||||
The first baseline specs cover:
|
||||
|
||||
- first-launch Setup Wizard visibility on a fresh profile
|
||||
- skipping setup and navigating to the Models page inside the Electron app
|
||||
|
||||
Add future Electron flows under `tests/e2e/` and reuse the shared fixture in `tests/e2e/fixtures/electron.ts`. Keep tests parallel-safe by avoiding fixed writable paths, ports, native keychains, and other external shared state. Use `E2E_EXCLUSIVE_TAG` when isolation is not possible.
|
||||
|
||||
### Tech Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|------------|
|
||||
| Runtime | Electron 40+ |
|
||||
| UI Framework | React 19 + TypeScript |
|
||||
| Styling | Tailwind CSS + shadcn/ui |
|
||||
| State | Zustand |
|
||||
| Build | Vite + electron-builder |
|
||||
| Testing | Vitest + Playwright |
|
||||
| Animation | Framer Motion |
|
||||
| Icons | Lucide React |
|
||||
@@ -1,83 +0,0 @@
|
||||
# ClawX Features
|
||||
|
||||
This document provides the detailed version of the Features section in the README.
|
||||
|
||||
### Zero Configuration Barrier
|
||||
|
||||
Complete the entire setup from installation to your first AI conversation through an intuitive graphical interface. No terminal commands, YAML files, or environment-variable hunting are required.
|
||||
|
||||
### Intelligent Chat Interface
|
||||
|
||||
Communicate with AI agents through a modern chat experience. ClawX supports multiple conversation contexts and message history, with assistant replies rendered as streaming Markdown with syntax-highlighted fenced code, CJK-aware parsing, GitHub-flavored tables, and KaTeX-powered LaTeX math (`$inline$`, `$$block$$`, `\(inline\)`, and `\[block\]`). User input remains literal text. The main composer also supports direct `@agent` routing for multi-agent setups. Fenced code preserves source line breaks, soft-wraps long lines, and provides a localized copy action after streaming completes.
|
||||
|
||||
Skills inserted from the composer appear as `/skill-name` cards. Click a card to open the preview sidebar and read that skill's `SKILL.md`.
|
||||
|
||||
When you target another agent with `@agent`, ClawX switches directly to that agent's own conversation context instead of relaying through the default agent. Agent workspaces stay separate by default, while stronger runtime isolation depends on OpenClaw sandbox settings.
|
||||
|
||||
The session sidebar is workspace-first: the default workspace stays at the top, other workspaces sort naturally, and each workspace can collapse or load more sessions. A session row shows a spinner while the AI is replying, a blue dot when an unseen reply finishes, and its relative activity time after the conversation is opened; hovering still reveals row actions. Imported workspaces can be renamed from their sidebar header. The custom name is reflected in the chat composer, while hovering the header still reveals the filesystem path.
|
||||
|
||||
When a valid workspace is selected, a new chat inherits it while remaining editable until the first send. Editable new or unbound chats expose a workspace chip in the composer. Its menu lists recent and known-session workspaces, lets you return to the default workspace, or choose another folder. If a saved workspace folder was moved or deleted, Chat pauses session creation and asks you to choose an existing folder instead of repeatedly retrying the missing path. Unavailable non-default groups are marked in the sidebar and can be removed after confirmation; this permanently deletes every session in that group. A session row is removed and navigation changes only after permanent deletion succeeds. Failed deletions leave the conversation and confirmation open for retry. Synthetic OpenClaw UUID-date fallback titles are treated as missing only when they match the session ID, then replaced with the conversation's first user prompt instead of being persisted as the session name.
|
||||
|
||||
Each agent can override its own `provider/model` runtime setting. Agents without overrides continue inheriting the global default model.
|
||||
|
||||
The Workspace and Preview tabs in Chat's right panel provide read-only previews for Markdown, `.docx`, and `.pptx` files. Markdown previews use the same syntax-highlighted, soft-wrapped, copyable fenced code, CJK-aware parsing, and KaTeX math support in static rendering mode. The Preview header can expand the selected file to the full ClawX viewport; use the same control or press Escape to return to the panel. Legacy `.doc` and `.ppt` files continue to open through the operating system instead of inline. DOCX pagination may differ from Microsoft Word, and PPTX previews do not support animations, transitions, or media playback. Office files larger than 20 MB are not previewed inline.
|
||||
|
||||
### Local HTML Preview
|
||||
|
||||
The Chat right panel contains Workspace, Preview, and Changes tabs. It no longer provides a general Web Browser, Home page, or address bar. Authorized local `.html` and `.htm` attachments, file activities, and Workspace files open in Preview by default. File actions let you choose the built-in Preview or a system application, and the Preview header can open the current HTML file in the system browser.
|
||||
|
||||
All links are non-clickable. Links rendered by ClawX appear as ordinary text, and links inside HTML Preview have their styling and pointer interaction removed. HTML Preview also blocks forms, script navigation, redirects, hash navigation, popups, downloads, network requests, and device permissions. It can render self-contained local HTML but cannot leave the selected document.
|
||||
|
||||
### Multi-Channel Management
|
||||
|
||||
Configure and monitor multiple AI channels simultaneously. Each channel operates independently, allowing you to run specialized agents for different tasks.
|
||||
|
||||
Each channel supports multiple accounts, per-account agent binding, and switching the channel default account directly from the Channels page.
|
||||
|
||||
For custom channel account IDs, ClawX enforces OpenClaw-compatible canonical IDs: `[a-z0-9_-]`, lowercase, a maximum of 64 characters, and starting with a letter or number. This prevents routing mismatches.
|
||||
|
||||
ClawX also bundles Tencent's official personal WeChat channel plugin, so you can link WeChat directly from the Channels page through an in-app QR flow.
|
||||
|
||||
### Cron-Based Automation
|
||||
|
||||
Schedule AI tasks to run automatically. Define triggers and set intervals so AI agents can work around the clock.
|
||||
|
||||
The Cron page lets you configure external delivery directly in the task form with separate sender-account and recipient-target selectors. For supported channels, recipient targets are discovered automatically from channel directories or known session history, so you no longer need to edit `jobs.json` by hand. The task message field supports inserting skills with the same inline `/skill` token syntax as the main chat composer, scoped to the selected agent, so scheduled prompts can trigger skills directly.
|
||||
|
||||
The schedule picker is split into **Recurring** and **Once** tabs. Recurring offers Hourly, Daily, Weekdays, Weekly, and Custom raw cron frequencies with inline time and weekday controls. Once runs the task a single time at a chosen date, with the weekday shown, and time. One-time tasks must be scheduled for a future moment and are automatically removed by the runtime once they finish.
|
||||
|
||||
### Extensible Skill System
|
||||
|
||||
Extend your AI agents with pre-built skills. The integrated Skills page is local-first: it scans managed and workspace skill directories and lets you enable or disable skills without depending on the Gateway. Enterprise extensions may also expose an extension-provided marketplace.
|
||||
|
||||
ClawX pre-bundles full document-processing skills (`pdf`, `xlsx`, `docx`, `pptx`), deploys them automatically to the managed skills directory (default `~/.openclaw/skills`) on startup, and enables them by default on first install.
|
||||
|
||||
The Skills page can display skills discovered from multiple OpenClaw sources, including the managed directory, workspace, and extra skill directories. It shows each skill's actual location so you can open the real folder directly. For bundled OpenClaw skills, community builds ship and expose only `skill-creator`; non-allowlisted bundled skills are physically trimmed in both development and packaged startup, and stale `openclaw.json` entries for removed bundled skills are pruned.
|
||||
|
||||
### Secure Provider Integration
|
||||
|
||||
Connect to multiple AI providers, including OpenAI, Anthropic, and Z.AI / GLM, with credentials stored securely in the native system keychain. OpenAI supports both API keys and browser OAuth for Codex subscriptions.
|
||||
|
||||
In Developer Mode, the dedicated Image Generation page supports an independent OpenAI-compatible image-generation endpoint with a Base URL, API key, and model name such as `gpt-image-2`. Image generation can therefore use a dedicated `/v1/images/generations` service while chat continues using the normal OpenAI provider.
|
||||
|
||||
For **Custom** providers used with OpenAI-compatible gateways, you can set a custom `User-Agent` in **Settings -> AI Providers -> Edit Provider** for compatibility-sensitive endpoints.
|
||||
|
||||
When you edit or switch providers, ClawX preserves existing per-model capability metadata such as `input: ["text", "image"]`. Newly selected Custom-provider models use OpenClaw onboarding-compatible image-input inference, with unknown models defaulting to text-only.
|
||||
|
||||
Custom-provider model rows also receive an explicit `contextWindow`, inferred from the model family, such as `gpt-5.x` -> 272k. Rows saved by older versions are backfilled on startup so OpenClaw can compact long sessions before they fail with "Context overflow" errors. When no compaction configuration exists, ClawX seeds `agents.defaults.compaction.mode = "safeguard"` and `reserveTokensFloor = 50000`; rows or configurations you authored yourself are never modified, except that a missing `reserveTokensFloor` may be backfilled.
|
||||
|
||||
Z.AI (CN / Global) maps to OpenClaw's built-in `zai` provider (`ZAI_API_KEY`). The default model is `glm-5.2`. Use the Code Plan preset for Coding Plan endpoints (`.../api/coding/paas/v4`) or the normal API endpoints (`.../api/paas/v4`). CN and Global are mutually exclusive because they share one OpenClaw runtime key.
|
||||
|
||||
When a compatible gateway rejects `/models` for non-authentication reasons, ClawX automatically falls back to a lightweight `/chat/completions` or `/responses` probe using the configured model during API-key validation.
|
||||
|
||||
### Adaptive Theming
|
||||
|
||||
Choose light mode, dark mode, or a system-synchronized theme. ClawX adapts to your preferences automatically.
|
||||
|
||||
### Startup Launch Control
|
||||
|
||||
In **Settings -> General**, enable **Launch at system startup** so ClawX starts automatically after login.
|
||||
|
||||
### Update Prompts
|
||||
|
||||
ClawX checks for new versions on startup. When an update is available, it shows an in-app prompt; downloading and installing happen only after you choose the action.
|
||||
@@ -1,12 +0,0 @@
|
||||
# ClawX Proxy Settings
|
||||
|
||||
This document provides the detailed version of the Proxy Settings section in the README.
|
||||
|
||||
- A bare `host:port` value is treated as an HTTP proxy.
|
||||
- If advanced proxy fields are left empty, ClawX falls back to **Proxy Server**.
|
||||
- Saving proxy settings reapplies Electron networking immediately and restarts the Gateway automatically.
|
||||
- When Telegram is enabled, ClawX also syncs the proxy to OpenClaw's Telegram channel configuration.
|
||||
- When the ClawX proxy is disabled, a normal Gateway restart preserves an existing Telegram channel proxy.
|
||||
- To explicitly clear the Telegram proxy from OpenClaw configuration, disable the proxy and save the proxy settings once.
|
||||
- In **Settings -> Advanced -> Developer**, you can run **OpenClaw Doctor**, which executes `openclaw doctor --json` and displays the diagnostic output in the app.
|
||||
- In packaged Windows builds, the bundled `openclaw` CLI/TUI runs through the shipped `node.exe` entry point to keep terminal input behavior stable.
|
||||
@@ -1,113 +0,0 @@
|
||||
# ClawXのアーキテクチャ
|
||||
|
||||
このドキュメントは、READMEの「アーキテクチャ」セクションの詳細版です。
|
||||
|
||||
ClawXは **統合Host APIレイヤーを備えたデュアルプロセスアーキテクチャ**を採用しています。Rendererは単一のクライアント抽象を呼び出し、プロトコル選択とプロセスライフサイクルはElectron Mainが管理します。
|
||||
|
||||
OpenClawの設定配信もElectron Mainが管理します。Gateway実行中は`config.get`が返す権威あるスナップショットを基準にし、変更を`config.set`でコミットします。Gatewayが停止中または起動中の場合は、同じコーディネーターが解決済みJSON5設定ファイルを更新しますが、これを理由にGatewayを起動することはありません。そのため、通常のプロバイダー、Agent、チャネル、バインディング、スキル、モデルの変更ではGatewayプロセスを置き換えません。完全な再起動は、プロキシなどのプロセス起動環境の変更と、ユーザーによる明示的な操作に限られます。確認済みのプロセス終了とWebSocket切断では、既存の自動再接続経路が引き続き使用されます。WebSocketハートビートの連続3回までの欠落は診断のみとし、短いpong遅延で長時間実行中の処理を中断しません。pongまたは任意の受信メッセージでカウントをリセットし、4回連続で欠落した場合に、ライフサイクルが自動復旧可能なrunning状態であれば、保護されたGateway自動復旧を要求します。認証設定をSQLiteへ書き込んだ後はOpenClawの`secrets.reload`を呼び出し、実行中のAgentがプロセス再起動なしで新しい認証情報を読み取れるようにします。
|
||||
|
||||
ChatはElectron Mainが所有するACP stdio bridgeを使用します。Mainはアプリが管理するGateway tokenをプライベートなプロセス環境経由でローカルの子プロセスへ渡すため、ランタイム設定の再読み込み後もACP履歴リプレイの認証が維持されます。保護されたGateway復旧が受理済みのメインセッションrunを中断した場合、パッチ済みOpenClawランタイムは別の復旧runを開始し、中断されたrun idを明示的なlineageとして保持します。Chat eventとagent eventはそのlineageを維持し、再接続したACP bridgeはpending promptを新しいrunへ引き継ぎ、run単位のストリームカーソルをリセットしてセッション単位のtool eventを購読します。RendererはGatewayランタイムの識別子を認識せず、型付きhost eventから同じメモリ内ACP timelineを描画し続けます。Gatewayはproviders、models、skills、workspace、settings、diagnostics、media configurationなどの非Chat機能を引き続き担当します。
|
||||
|
||||
### ACPのセマンティック権威
|
||||
|
||||
ACPが提供するすべてのChatの意味とコンテキストでは、`session/load`履歴だけでなくACPを優先的なセマンティック権威として扱います。該当する場合のセッションIDとルーティング、ワークスペースと実行`cwd`、promptとtimelineの状態、標準resourceや添付ファイルのセマンティクスが含まれます。ACPが値やイベントを提供する場合、MainとRendererはGatewayスナップショット、transcriptからの推論、ローカル設定、別の並列投影で置き換えず、ACPの結果を使用します。
|
||||
|
||||
上流ACPに相当する機能がない場合に限り、ACPを迂回できます。その互換性パスは狭く有界で、sessionとgenerationに紐付ける必要があります。また、理由、情報源、制限、調整方法、削除条件を該当するHarness referenceまたはruleに記録し、競合する権威へ暗黙に発展させてはいけません。
|
||||
|
||||
### ACP履歴の権威と有界なtranscript補足
|
||||
|
||||
ACP `session/load` のリプレイがChat履歴の主要な権威です。ClawXは第二のACP ledger、縮約timeline、リプレイキャッシュ、再構成したツール履歴を永続化しません。OpenClawの構造化ACP event ledgerが利用できない場合、そのACP adapterは永続化済みtranscriptの`toolCall`と`toolResult`を順番どおりにネイティブなtool updateへ再構成し、text-tool-textの境界を維持します。ClawX自身はこれらの記録を推論しません。OpenClawの一部の機能にはまだ完全に対応するACP実装がありません。たとえば、assistantメディアがACPから省略されたり、Gateway処理によってassistantの`MEDIA:`ディレクティブが表示中のライブ返信から削除されたりする場合があります。そのため、ClawXは有界で印付き、メモリのみの互換性補足だけを保持します。
|
||||
|
||||
- 非同期の画像生成完了は、同じセッションに確認済みの`image_generate`コンテキストがあり、完了の証拠が信頼できるか、承認済みのtranscript証拠である場合に限り復元できます。
|
||||
- 一般の添付ファイルは、永続化されたassistantの`__openclaw.media`事実、または行頭にある明示的なassistant `MEDIA:`ディレクティブから復元できます。復元されるのは添付ファイルの参照と宣言されたメタデータだけで、周囲のassistantメッセージは復元しません。
|
||||
- ACPリプレイには元のイベントタイムスタンプがないため、Mainは有界のtranscript JSONLレコードからメタデータのみのターン全体の時間を追加できます。これはACPですでに復元されたターンにだけ付与できます。
|
||||
- cronセッションのACPリプレイが完全に空の場合、Mainの型付きcron履歴APIがスケジュール済みプロンプトと完了サマリーを提供できます。識別された実行サマリーにOpenClawの切り詰めマーカーがある場合、対応するrunのtranscriptがより長く、永続化されたサマリーの完全な接頭辞を共有するときに限り、Mainは最終assistantテキストを復元できます。
|
||||
|
||||
履歴の読み取りは最新のtranscriptメッセージ1000件に制限されます。成功したライブpromptでは直ちに1回読み取り、1500ms後に1回だけ再試行します。すべての補足は、正確なsession、ACP generation、操作、必要に応じてライブユーザーターンに紐付けられます。古い、欠落した、重複した、または曖昧な一致は破棄されます。これらの経路で通常のassistantメッセージ、thought、tool、plan、permission、ファイルアクティビティ、欠落したターン、別のChat履歴を再構成してはいけません。Mainはtranscriptの証拠からネイティブACPイベントを生成しません。標準ACP resourceが優先され、上流が同等の内容を提供した場合はこれらの互換性例外を削除します。
|
||||
|
||||
別の会話やページを開いても、未完了のACP応答はストリーミングを継続します。完了前に戻ると最新のメモリ内timelineを復元し、ライブ応答の表示を続けます。完了後は通常のACP履歴リプレイが正の情報源です。
|
||||
|
||||
ACPのassistantターンにはターン全体の所要時間が表示されます。ライブ計時はクライアントが観測したpromptライフサイクルに従い、アプリ内の移動後も継続します。履歴の所要時間はElectron Mainが有界のOpenClaw transcriptタイムスタンプから算出し、ACPリプレイですでに復元されたターンにだけ付与します。
|
||||
|
||||
ACP Chatは標準ACP resourceを添付ファイルとして描画します。ユーザーが選択した画像はファイル名をホバーオーバーレイに表示するサムネイルになり、その他の利用可能な添付カードにはファイル名と淡色で省略可能なソースパスが表示されます。現在のOpenClaw ACP adapterがassistantメディアを省略した場合、正規化された永続OpenClawメディア情報と明示的なassistant `MEDIA:`ディレクティブを、transcript専用メタデータを表示せずに添付カードとして復元できます。
|
||||
|
||||
既存のローカルファイル参照は、アクティブなworkspace外のパスを含め、プレビューやオープンのたびにElectron Mainが正確なsessionとgenerationについて再検証します。AIが生成したプレビュー可能なローカル添付(20 MB以下の`.docx`と`.pptx`を含む)は、読み取り専用のアプリ内プレビューを主操作として保持し、対応アプリで開く操作やFinder、エクスプローラー、システムのファイルマネージャーで表示する操作を副次メニューから選べます。ローカルHTML添付では、そのメニューの先頭項目から右側のPreviewタブでファイルを開けます。
|
||||
|
||||
Officeプレビューには同じ制限があります。`.doc`と`.ppt`はシステムアプリで開き、DOCXのページ区切りはMicrosoft Wordと異なる場合があり、PPTXのアニメーション、画面切り替え、メディア再生はサポートされません。対応アプリの検出はmacOSとWindowsでのみ利用でき、Linuxまたは検出失敗時は通知なしにファイル位置の表示だけへ切り替わります。その他のローカルファイル(20 MBを超えるOfficeファイルを含む)は、クリック後にシステムアプリで開きます。ユーザーが選択したフォルダー添付は送信後も利用でき、クリックするとシステムのファイルマネージャーで開きます。ClawXはその内容を読み取ったりプレビューしたりしません。リモートHTTP/HTTPS添付はクリック後に外部で開きます。正規のメディア情報を伴わない通常の文章中のパスは添付として扱われません。
|
||||
|
||||
ACP Chatは、ランタイムが画像生成メディアを信頼できる構造化メディアとして配信した場合、生成画像のプレビューも表示できます。信頼できるOpenClaw internal-UI配信と画像生成タスクに紐付いた最終返信では、テキストだけの失敗説明を含む元のユーザー向け完了テキストを保持し、汎用画像キャプションに置き換えません。OpenClawの履歴リプレイ中、assistant画像の`MEDIA:`マーカーは、同じセッションで画像生成タスクの開始が記録されている場合に限りインライン画像へ昇格します。プレビューは任意のRendererファイルシステムアクセスではなく、Electron Mainのホストメディア処理で読み込みます。標準ACPの画像とresourceコンテンツが引き続き優先され、そのまま描画されます。
|
||||
|
||||
### ACPファイルアクティビティのセマンティクス
|
||||
|
||||
- ファイルアクティビティは、成功して完了したOpenClawの`write`、`edit`、`apply_patch`呼び出しから投影されます。ツール認識は公式OpenClaw Chat UIに従い、完了した呼び出しだけに絞る処理はClawX固有です。
|
||||
- 作成・変更された行は、プレビュー可能なassistant添付と同じファイルカードと**Open with**メニューを使い、状態と任意の`+/-`概要を保持します。HTMLではメニューの先頭項目が右側の**Preview**タブでファイルを開きます。削除行には **Changes** 操作だけを残します。アプリ一覧、選択アプリで開く操作、表示位置の要求は、workspaceルートと相対パスからElectron Mainが個別に再検証します。ツール由来のパスは添付にならず、Rendererへ正規化済みのネイティブパスも公開されません。
|
||||
- `write` はツールの宣言どおり、対象パスがすでに存在する可能性があっても、作成および全行追加の差分として表示されます。
|
||||
- **Changes** はツールが宣言したアクティビティを時系列に記録するセッション単位の記録です。Gitの出力でも、検証済みソースベースラインとの差分でもありません。
|
||||
- 各ファイルについて、Changesはassistantの各ターンに最大1つのdiffエディターを表示します。安全に連結できる断片は合成し、独立した断片は1つのエディターに連結しますが、完全なファイルベースラインとの差分とはみなしません。
|
||||
- シェルコマンド、スクリプト、ユーザー、IDEによる副作用は検出されません。
|
||||
- 完全なACPリプレイから記録済みのファイルアクティビティを復元できます。リプレイが不完全でも、ClawXはフォールバック推論で欠落を補いません。
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ ClawX デスクトップアプリ │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Electron メインプロセス │ │
|
||||
│ │ • ウィンドウとアプリケーションのライフサイクル管理 │ │
|
||||
│ │ • Gatewayプロセスの監視 │ │
|
||||
│ │ • システム統合(トレイ、通知、キーチェーン) │ │
|
||||
│ │ • 自動更新のオーケストレーション │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
│ IPC(権威ある制御プレーン)
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ React Rendererプロセス │
|
||||
│ • モダンなコンポーネントベースUI(React 19) │
|
||||
│ • Zustandによる状態管理 │
|
||||
│ • 統一host-api/api-client呼び出し │
|
||||
│ • assistant返信はMarkdown、ユーザー入力はプレーンテキスト │
|
||||
└──────────────────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
│ 型付きIPCリクエスト
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ Main Host ServicesとGateway Manager │
|
||||
│ • host:invoke型付きサービスディスパッチ │
|
||||
│ • 設定、ファイル、セッション、スキル、プロバイダー、診断 │
|
||||
│ • Main所有のGateway WebSocketとプロセス監視 │
|
||||
└──────────────────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
│ Main所有WebSocket
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ OpenClaw Gateway │
|
||||
│ • AIエージェントランタイムとオーケストレーション │
|
||||
│ • メッセージチャネル管理 │
|
||||
│ • スキル/プラグイン実行環境 │
|
||||
│ • プロバイダー抽象化レイヤー │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 設計原則
|
||||
|
||||
- **プロセス分離**:AIランタイムは別プロセスで動作し、重い計算中もUIの応答性を保ちます。
|
||||
- **フロントエンド呼び出しの単一入口**:Rendererのリクエストは`host-api` / `api-client`を経由し、プロトコルの詳細は安定したインターフェースの背後に隠されます。
|
||||
- **Mainプロセスによるトランスポート管理**:Electron MainがACP Chat stdio bridgeとGatewayトランスポートを所有し、Rendererは型付きIPCでMainと通信します。
|
||||
- **拡張IPCの貢献点**:Mainプロセス拡張はHTTP routeではなく、型付きIPCレジストリを通じてhost-api actionを提供します。
|
||||
- **グレースフルリカバリ**:再接続、タイムアウト、バックオフを内蔵し、一時的な障害を自動処理します。
|
||||
- **セキュアストレージ**:APIキーや機密データにはOSのネイティブな安全な保存機構を使用します。
|
||||
- **CORSセーフ設計**:RendererはローカルGatewayやHost API HTTPエンドポイントを直接呼び出しません。
|
||||
|
||||
### プロセスモデルとGatewayのトラブルシューティング
|
||||
|
||||
- ClawXはElectronアプリのため、**1つのアプリインスタンスでも複数のOSプロセスが表示される**(main/renderer/zygote/utility)のは正常です。
|
||||
- 単一起動保護にはElectronのロックに加えてローカルのプロセスファイルロックのフォールバックを使用し、デスクトップIPCやセッションバスが不安定な環境での二重起動を防ぎます。
|
||||
- ローリングアップグレード中に旧版と新版が混在すると、単一起動保護が非対称になる場合があります。安定性のため、すべてのデスクトップクライアントを同じバージョンへ更新してください。
|
||||
- OpenClaw Gatewayのリスナーは**単一所有者**である必要があります。`127.0.0.1:18789`をListenするプロセスは1つだけにしてください。
|
||||
- Gatewayのreadinessは`system-presence`、`health`、`status`などOpenClawのコア信号を基準にします。メモリまたはチャネルの失敗は、Gateway全体の障害ではなく機能低下として表示されます。
|
||||
- アクティブなリスナーは次のコマンドで確認できます。
|
||||
- macOS/Linux:`lsof -nP -iTCP:18789 -sTCP:LISTEN`
|
||||
- Windows(PowerShell):`Get-NetTCPConnection -LocalPort 18789 -State Listen`
|
||||
- ウィンドウの閉じるボタン(`X`)はClawXをトレイに隠すだけで、完全終了ではありません。完全終了にはトレイメニューの **Quit ClawX** を使用してください。
|
||||
@@ -1,130 +0,0 @@
|
||||
# ClawX開発ガイド
|
||||
|
||||
このドキュメントは、READMEの「開発」セクションの詳細版です。
|
||||
|
||||
### 前提条件
|
||||
|
||||
- **Node.js**:対応するメジャー系列の22.22.3以上、24.15.0以上、または25.9.0以上(Node 24 LTS推奨)
|
||||
- **パッケージマネージャー**:pnpm 9以上(npmも対応)
|
||||
- **Linux(Ubuntu/Debian)**:Electronを実行する前に必要なシステムライブラリをインストールしてください。
|
||||
```bash
|
||||
sudo apt-get install -y libnss3 libgtk-3-0 libxss1 libxtst6 libatspi2.0-0 libnotify4 xdg-utils
|
||||
```
|
||||
Ubuntu 24.04以降では一部のパッケージに`t64`サフィックスが付きます。上記コマンドを実行すると、`apt`が適切なバリアントを自動選択します。
|
||||
|
||||
### プロジェクト構成
|
||||
|
||||
```text
|
||||
ClawX/
|
||||
├── electron/ # Electron Mainプロセス
|
||||
│ ├── services/ # 型付きHost API、プロバイダー、秘密情報、ランタイムサービス
|
||||
│ │ ├── providers/ # プロバイダー/アカウントのモデル同期ロジック
|
||||
│ │ └── secrets/ # OSキーチェーンと秘密情報の保存
|
||||
│ ├── shared/ # 共有プロバイダースキーマ/定数
|
||||
│ │ └── providers/
|
||||
│ ├── main/ # アプリ入口、ウィンドウ、IPC登録
|
||||
│ ├── gateway/ # OpenClaw Gatewayプロセスマネージャー
|
||||
│ ├── preload/ # セキュアIPCブリッジ
|
||||
│ └── utils/ # ストレージ、認証、パスのユーティリティ
|
||||
├── src/ # React Rendererプロセス
|
||||
│ ├── lib/ # フロントエンド統合APIとエラーモデル
|
||||
│ ├── stores/ # Zustandストア(settings/chat/gateway)
|
||||
│ ├── components/ # 再利用可能なUIコンポーネント
|
||||
│ ├── pages/ # Setup/Dashboard/Chat/Channels/Skills/Cron/Settings
|
||||
│ ├── i18n/ # ローカライズリソース
|
||||
│ └── types/ # TypeScript型定義
|
||||
├── tests/
|
||||
│ ├── e2e/ # Playwright Electron E2Eスモークテスト
|
||||
│ └── unit/ # Vitestユニット/統合系テスト
|
||||
├── resources/ # 静的アセット(アイコン、画像)
|
||||
└── scripts/ # ビルドとユーティリティのスクリプト
|
||||
```
|
||||
|
||||
### 利用可能なコマンド
|
||||
|
||||
```bash
|
||||
# 開発
|
||||
pnpm run init # 依存関係をインストールし、同梱バイナリ(uv、agent-browser)をダウンロード
|
||||
pnpm dev # ホットリロードで起動(不足時は同梱スキルを自動準備)
|
||||
|
||||
# 品質
|
||||
pnpm lint # ESLintを実行
|
||||
pnpm typecheck # TypeScriptを検証
|
||||
|
||||
# テスト
|
||||
pnpm test # ユニットテストを実行
|
||||
pnpm run test:e2e # Electron E2Eスモークテストを実行
|
||||
pnpm run test:e2e:headed # 表示可能なウィンドウでElectron E2Eテストを実行
|
||||
pnpm run perf:chat # 合成Chat Renderer/Main CPUプロファイルを取得
|
||||
pnpm run profile:main # Main inspectorを9229番ポートで起動したビルド済みアプリを実行
|
||||
pnpm run comms:replay # 通信リプレイ指標を算出
|
||||
pnpm run comms:baseline # 通信ベースラインスナップショットを更新
|
||||
pnpm run comms:compare # リプレイ指標をベースラインの閾値と比較
|
||||
|
||||
# ビルドとパッケージ
|
||||
pnpm run build:vite # フロントエンドのみをビルド
|
||||
pnpm build # パッケージアセットを含む本番ビルド
|
||||
pnpm package # 現在のプラットフォーム向けにパッケージ化(同梱スキルを含む)
|
||||
pnpm package:mac # macOS向けにパッケージ化
|
||||
pnpm package:win # Windows向けにパッケージ化
|
||||
pnpm package:linux # Linux向けにパッケージ化
|
||||
```
|
||||
|
||||
ヘッドレスLinuxではElectronテストに表示サービスが必要です。`xvfb-run -a pnpm run test:e2e`を使用してください。
|
||||
|
||||
Electron E2E機能テストはローカルとCIの両方で既定で2つのPlaywright workerを使用します。通常の並列レーンは`CLAWX_E2E_WORKERS=<正の整数>`で調整できます。OS全体の状態に触れるテストは1 workerの`exclusive`プロジェクトを使用し、ホストのパフォーマンスプロファイルはその後単独で実行されます。新しいE2Eテストは既定で並列です。実際のクリップボードなどマシン全体で共有されるリソースを使う場合は、`tests/e2e/parallel-policy.ts`の`E2E_EXCLUSIVE_TAG`を適用してください。
|
||||
|
||||
独占前提を必要としない通常のspecだけを実行する場合は、`pnpm exec playwright test <spec> --project=parallel --no-deps`を使用します。
|
||||
|
||||
### Electronパフォーマンス診断
|
||||
|
||||
`pnpm run perf:chat`は、ストリーミングとリッチな静的Markdownサイドバー/スクロール操作を対象に、分離された合成ACP負荷を実行します。Playwrightの`test-results/`ディレクトリにバージョン付きメトリクスとRenderer/Main CPUプロファイルを書き込みます。Rendererプロファイルは本番のstore/render経路とフレームペーシングを対象とします。ストリーミングMainプロファイルはMainからRendererへのIPC fanoutを測定し、操作用MainプロファイルはRenderer操作中にMainがアイドル状態を保つかを示します。どちらも上流のOpenClaw/ACPサブプロセスやGPUプロセスの経路は含みません。
|
||||
|
||||
CPUプロファイルはChrome DevToolsで開けます。アーティファクトには生成されたfixtureテキストだけが含まれ、製品テレメトリーではありません。結果はハードウェアに依存するため、単一のクロスプラットフォーム絶対閾値ではなく、同じマシンで繰り返した結果を比較してください。
|
||||
|
||||
実際のRendererを記録するには、`CLAWX_REMOTE_DEBUGGING_PORT=9223 pnpm dev`で開発環境を起動し、PlaywrightまたはChrome DevToolsを`localhost:9223`へ接続します。実際のElectron Mainを記録するには`pnpm run profile:main`を実行し、`chrome://inspect`で`localhost:9229`を設定してElectron Mainターゲットを選びます。WebSocket trace自体を測定する場合を除き、`CLAWX_GATEWAY_WS_TRACE`は設定しないでください。
|
||||
|
||||
ClawXは既定でChromiumのハードウェアアクセラレーションを有効にし、長い文書、スクロール、レイアウトアニメーションでGPUコンポジットとラスタライズを利用します。グラフィックスドライバーに問題がある場合のトラブルシューティングには、Chromium標準の`--disable-gpu`コマンドラインスイッチを使用できます。
|
||||
|
||||
### 通信回帰チェック
|
||||
|
||||
Gatewayイベント、ACP Chat bridgeの送受信フロー、チャネル配信、トランスポートフォールバックなどの通信経路をPRで変更した場合は、次を実行してください。
|
||||
|
||||
```bash
|
||||
pnpm run comms:replay
|
||||
pnpm run comms:compare
|
||||
```
|
||||
|
||||
CIの`comms-regression`ジョブが必須シナリオと閾値を検証します。
|
||||
|
||||
### Electron E2Eテスト
|
||||
|
||||
Playwright Electronスイートは`dist/`と`dist-electron/`からパッケージ済みのRendererとMainプロセスを起動するため、事前に`pnpm dev`を手動実行する必要はありません。
|
||||
|
||||
`pnpm run test:e2e`は自動的に次を行います。
|
||||
|
||||
- `pnpm run build:vite`でRendererとElectronのバンドルをビルド
|
||||
- 一時的な`HOME`を使ってElectronを分離E2Eモードで起動
|
||||
- 一時的なClawX `userData`ディレクトリを使用
|
||||
- OS全体のリソースとパフォーマンステストを隔離しながら、通常のspecファイルを並列実行
|
||||
- Gateway自動起動、同梱スキルのインストール、トレイ作成、CLI自動インストールなど、重い起動副作用をスキップ
|
||||
|
||||
最初のベースラインspecは次を対象とします。
|
||||
|
||||
- 新しいプロファイルでの初回起動Setup Wizardの表示
|
||||
- セットアップをスキップし、Electronアプリ内でModelsページへ移動できること
|
||||
|
||||
今後のElectronフローは`tests/e2e/`に追加し、`tests/e2e/fixtures/electron.ts`の共有fixtureを再利用してください。固定の書き込みパス、ポート、ネイティブキーチェーン、その他の外部共有状態を避けてテストを並列安全に保ちます。分離できない場合は`E2E_EXCLUSIVE_TAG`を使用してください。
|
||||
|
||||
### 技術スタック
|
||||
|
||||
| レイヤー | 技術 |
|
||||
|---------|------|
|
||||
| ランタイム | Electron 40+ |
|
||||
| UIフレームワーク | React 19 + TypeScript |
|
||||
| スタイリング | Tailwind CSS + shadcn/ui |
|
||||
| 状態管理 | Zustand |
|
||||
| ビルド | Vite + electron-builder |
|
||||
| テスト | Vitest + Playwright |
|
||||
| アニメーション | Framer Motion |
|
||||
| アイコン | Lucide React |
|
||||
@@ -1,83 +0,0 @@
|
||||
# ClawXの機能
|
||||
|
||||
このドキュメントは、READMEの「機能」セクションの詳細版です。
|
||||
|
||||
### ゼロ設定バリア
|
||||
|
||||
インストールから最初のAI会話まで、直感的なグラフィカルインターフェースですべてのセットアップを完了できます。ターミナルコマンド、YAMLファイル、環境変数の探索は不要です。
|
||||
|
||||
### インテリジェントチャットインターフェース
|
||||
|
||||
モダンなチャット体験を通じてAIエージェントとコミュニケーションできます。複数の会話コンテキストとメッセージ履歴に対応し、エージェントの返信は、シンタックスハイライト付きのフェンスコード、CJK対応の解析、GitHub風テーブル、KaTeXによるLaTeX数式(`$インライン$`、`$$ブロック$$`、`\(インライン\)`、`\[ブロック\]`)を含むストリーミングMarkdownとして描画されます。ユーザー入力は常にプレーンテキストとして扱われます。マルチエージェント構成では、メインのコンポーザーから `@agent` で対象エージェントへ直接ルーティングできます。フェンスコードはソースの改行を保持し、長い行をソフトラップし、ストリーミング完了後にローカライズされたコピー操作を提供します。
|
||||
|
||||
コンポーザーから挿入したスキルは `/skill-name` カードとして表示されます。カードをクリックすると右側のプレビューサイドバーが開き、そのスキルの `SKILL.md` を読めます。
|
||||
|
||||
`@agent` で別のエージェントを指定すると、ClawXはデフォルトエージェントを経由せず、そのエージェント自身の会話コンテキストへ直接切り替えます。エージェントのワークスペースは既定で分離されますが、より強い実行時分離はOpenClawのsandbox設定に依存します。
|
||||
|
||||
セッションサイドバーはワークスペース優先で構成されます。既定のワークスペースが先頭に固定され、その他のワークスペースは自然な順序で並びます。各ワークスペースは折りたたんだり、セッションを追加読み込みしたりできます。AIが返信中のセッション行にはスピナーが表示され、未確認の返信が完了すると青い点が表示されます。会話を開いた後は相対的なアクティビティ時刻が表示され、ホバーすると行の操作が表示されます。インポートしたワークスペースはサイドバーの見出しから名前を変更できます。カスタム名はチャットコンポーザーにも反映され、見出しにホバーするとファイルシステムのパスを確認できます。
|
||||
|
||||
有効なワークスペースが選択されている場合、新しいチャットは最初の送信まで編集可能な状態でそのワークスペースを引き継ぎます。編集可能な新規チャットまたは未バインドのチャットでは、コンポーザーのワークスペースチップから、最近使用したワークスペースと既知のセッションワークスペースの一覧を開けます。既定のワークスペースへ戻ることも、別のフォルダーを選ぶこともできます。保存済みのワークスペースフォルダーが移動または削除された場合、Chatはセッション作成を一時停止し、無効なパスを繰り返し再試行せず、既存のフォルダーを選ぶよう案内します。利用できない既定以外のグループにはサイドバーで印が付き、確認後に削除できます。この操作ではグループ内のすべてのセッションが完全に削除されます。セッション行の削除と画面遷移は完全削除が成功した後にのみ行われます。失敗した場合は会話と確認ダイアログが保持され、再試行できます。OpenClawが生成するUUIDと日付のフォールバックタイトルは、セッションIDと一致する場合に限り欠落タイトルとして扱われ、セッション名として保存せず、会話の最初のユーザーメッセージに置き換えて表示します。
|
||||
|
||||
各エージェントは自身の `provider/model` 実行時設定を上書きできます。上書きしていないエージェントはグローバルの既定モデルを引き続き継承します。
|
||||
|
||||
Chat右側パネルのWorkspaceとPreviewタブでは、Markdown、`.docx`、`.pptx`を読み取り専用でプレビューできます。Markdownのプレビューは静的レンダリングモードで、シンタックスハイライト、ソフトラップ、コピー可能なフェンスコード、CJK対応解析、KaTeX数式をサポートします。プレビューのヘッダーから選択中のファイルをClawXの表示領域全体へ拡大できます。同じボタンまたはEscapeキーでパネルへ戻れます。従来形式の`.doc`と`.ppt`はアプリ内ではなくOS経由で開きます。DOCXのページ区切りはMicrosoft Wordと異なる場合があり、PPTXプレビューではアニメーション、画面切り替え、メディア再生をサポートしません。20 MBを超えるOfficeファイルはアプリ内でプレビューされません。
|
||||
|
||||
### ローカルHTMLプレビュー
|
||||
|
||||
Chat右側パネルにはWorkspace、Preview、Changesタブがあります。汎用Webブラウザ、ホーム画面、アドレスバーは提供されません。許可されたローカル`.html`と`.htm`の添付ファイル、ファイルアクティビティ、Workspaceファイルは既定でPreviewに開きます。ファイル操作ではClawX内蔵のPreviewまたはシステムアプリを選択でき、Previewのヘッダーから現在のHTMLファイルをシステムブラウザで開くこともできます。
|
||||
|
||||
すべてのリンクはクリックできません。ClawXが描画するリンクは通常のテキストとして表示され、HTML Preview内のリンクからもリンク装飾とポインター操作が除去されます。HTML Previewはフォーム、スクリプトによる移動、リダイレクト、ページ内移動、ポップアップ、ダウンロード、ネットワーク要求、デバイス権限もブロックします。自己完結したローカルHTMLは表示できますが、選択中の文書から移動することはできません。
|
||||
|
||||
### マルチチャネル管理
|
||||
|
||||
複数のAIチャネルを同時に設定・監視できます。各チャネルは独立して動作するため、異なるタスクに特化したエージェントを実行できます。
|
||||
|
||||
各チャネルは複数アカウント、アカウント単位のAgent紐付け、Channelsページからの既定アカウント切り替えに対応しています。
|
||||
|
||||
カスタムチャネルアカウントIDには、ルーティング不一致を防ぐため、OpenClaw互換の正規形式(`[a-z0-9_-]`、小文字、最大64文字、先頭は英字または数字)を必須としています。
|
||||
|
||||
ClawXにはTencent公式の個人WeChatチャネルプラグインも同梱されており、Channelsページからアプリ内QRフローでWeChatを直接連携できます。
|
||||
|
||||
### Cronベースの自動化
|
||||
|
||||
AIタスクを自動的に実行するようスケジュール設定できます。トリガーと間隔を定義し、AIエージェントを常時稼働させられます。
|
||||
|
||||
Cronページでは、送信アカウントと受信先ターゲットを別々に選択して、タスクフォームから外部配信を直接設定できます。対応チャネルでは、受信先ターゲットがチャネルディレクトリまたは既知のセッション履歴から自動検出されるため、`jobs.json`を手動編集する必要はありません。タスクメッセージ欄では、メインのチャットコンポーザーと同じインライン `/skill` トークン構文で、選択したエージェントのスキルを挿入できます。これにより、スケジュール済みプロンプトからスキルを直接起動できます。
|
||||
|
||||
スケジュール選択は**繰り返し**と**1回のみ**のタブに分かれています。繰り返しでは毎時、毎日、平日、毎週、カスタム(生のcron)を時刻・曜日コントロール付きで選択できます。1回のみでは、曜日が表示された指定日と時刻に一度だけ実行します。1回のみのタスクは未来の時刻を指定する必要があり、完了後はランタイムによって自動削除されます。
|
||||
|
||||
### 拡張可能なスキルシステム
|
||||
|
||||
事前構築されたスキルでAIエージェントを拡張できます。統合Skillsページはローカル優先で、管理ディレクトリとworkspaceのスキルディレクトリをスキャンし、Gatewayに依存せずスキルを有効化・無効化できます。エンタープライズ拡張では、拡張機能が提供するマーケットプレイスを表示することもできます。
|
||||
|
||||
ClawXはドキュメント処理スキル(`pdf`、`xlsx`、`docx`、`pptx`)を完全な形で同梱し、起動時に管理スキルディレクトリ(既定は`~/.openclaw/skills`)へ自動配備し、初回インストール時に既定で有効化します。
|
||||
|
||||
Skillsページでは、管理ディレクトリ、workspace、追加スキルディレクトリなど、複数のOpenClawソースから検出されたスキルを表示できます。各スキルの実際の場所も表示されるため、実フォルダーを直接開けます。OpenClaw同梱のbundled skillについて、コミュニティ版では`skill-creator`だけをパッケージと画面に残します。許可リストにないbundled skillは開発時とパッケージ版の起動時に物理的に削除され、削除済みスキルに対応する古い`openclaw.json`エントリも整理されます。
|
||||
|
||||
### セキュアなプロバイダー統合
|
||||
|
||||
OpenAI、Anthropic、Z.AI / GLMなど複数のAIプロバイダーに接続でき、認証情報はOSのネイティブキーチェーンに安全に保存されます。OpenAIはAPIキーとブラウザOAuth(Codexサブスクリプション)の両方に対応しています。
|
||||
|
||||
開発者モードの専用Image Generationページでは、Base URL、APIキー、`gpt-image-2`などのモデル名を指定して、独立したOpenAI互換の画像生成エンドポイントを設定できます。画像生成は専用の`/v1/images/generations`サービスを使い、チャットは通常のOpenAIプロバイダーを使い続けられます。
|
||||
|
||||
OpenAI互換ゲートウェイで **Custom** プロバイダーを使う場合、互換性が必要なエンドポイント向けに **設定 → AI Providers → Providerを編集** からカスタム `User-Agent` を設定できます。
|
||||
|
||||
プロバイダーを編集または切り替える際、ClawXは `input: ["text", "image"]` など既存のモデル単位の能力メタデータを保持します。新しく選択したCustomプロバイダーのモデルにはOpenClaw onboarding互換の画像入力推論を適用し、不明なモデルはテキスト専用として扱います。
|
||||
|
||||
Customプロバイダーのモデル行には、モデルファミリーから推定した明示的な `contextWindow`(例:`gpt-5.x` → 272k)も付与されます。旧バージョンで保存された行は起動時に補完されるため、OpenClawは長いセッションが「Context overflow」エラーになる前に圧縮できます。圧縮設定がない場合、ClawXは `agents.defaults.compaction.mode = "safeguard"` と `reserveTokensFloor = 50000` を初期設定します。ユーザーが作成したモデル行や設定は変更されませんが、`reserveTokensFloor` が欠落している場合だけ補完されることがあります。
|
||||
|
||||
Z.AI(CN / Global)はOpenClaw組み込みの `zai` プロバイダー(`ZAI_API_KEY`)に対応し、既定モデルは `glm-5.2` です。Code PlanプリセットではCoding Planエンドポイント(`.../api/coding/paas/v4`)を、通常のAPIでは(`.../api/paas/v4`)を使います。CNとGlobalは同じOpenClawランタイムキーを共有するため相互排他的です。
|
||||
|
||||
互換ゲートウェイが認証以外の理由で`/models`を拒否した場合、ClawXはAPIキー検証時に設定済みモデルを使い、軽量な`/chat/completions`または`/responses`プローブへ自動フォールバックします。
|
||||
|
||||
### アダプティブテーマ
|
||||
|
||||
ライトモード、ダークモード、システム同期テーマを選択できます。ClawXは設定に自動的に適応します。
|
||||
|
||||
### 自動起動設定
|
||||
|
||||
**設定 → 一般**で **システム起動時に自動起動** を有効にすると、ログイン後にClawXが自動的に起動します。
|
||||
|
||||
### 更新通知
|
||||
|
||||
ClawXは起動時に新しいバージョンを確認します。更新が利用可能になるとアプリ内プロンプトを表示し、選択した場合にのみダウンロードとインストールを実行します。
|
||||
@@ -1,12 +0,0 @@
|
||||
# ClawXのプロキシ設定
|
||||
|
||||
このドキュメントは、READMEの「プロキシ設定」セクションの詳細版です。
|
||||
|
||||
- `host:port` だけの値はHTTPプロキシとして扱われます。
|
||||
- 高度なプロキシ項目が空の場合、ClawXは **プロキシサーバー** にフォールバックします。
|
||||
- プロキシ設定を保存すると、Electronのネットワーク設定が即座に再適用され、Gatewayが自動的に再起動します。
|
||||
- Telegramが有効な場合、ClawXはプロキシをOpenClawのTelegramチャネル設定にも同期します。
|
||||
- ClawXのプロキシが無効な状態で通常のGateway再起動が行われても、既存のTelegramチャネルプロキシは保持されます。
|
||||
- OpenClaw設定からTelegramプロキシを明示的に削除するには、プロキシを無効にしてプロキシ設定を一度保存してください。
|
||||
- **設定 → 詳細設定 → 開発者**では **OpenClaw Doctor** を実行できます。`openclaw doctor --json` を実行し、診断結果をアプリ内に表示します。
|
||||
- Windowsのパッケージ版では、同梱の`openclaw` CLI/TUIは同梱の`node.exe`エントリーポイント経由で実行され、ターミナル入力の安定性を保ちます。
|
||||
@@ -1,113 +0,0 @@
|
||||
# Архитектура ClawX
|
||||
|
||||
Этот документ содержит подробную версию раздела «Архитектура» из README.
|
||||
|
||||
ClawX использует **двухпроцессную архитектуру с унифицированным уровнем Host API**. Renderer обращается к единой абстракции клиента, а Electron Main управляет выбором протокола и жизненным циклом процессов.
|
||||
|
||||
Доставка конфигурации OpenClaw также управляется Electron Main. Когда Gateway запущен, ClawX использует авторитетный снимок из `config.get` как основу и применяет изменения через `config.set`. Когда Gateway остановлен или запускается, тот же координатор обновляет разрешённый JSON5-файл конфигурации, не запуская Gateway из-за этого обновления. Поэтому обычные изменения провайдера, агента, канала, привязки, навыка и модели не заменяют процесс Gateway. Полные перезапуски остаются только для изменений среды запуска процесса, например прокси, и явных действий пользователя. Подтверждённые завершения процесса и закрытия WebSocket используют существующие пути автоматического переподключения. Первые три последовательных пропуска WebSocket heartbeat являются только диагностикой, поэтому краткая задержка pong не прерывает долгую операцию; pong или любое входящее сообщение сбрасывает счётчик, а при четвёртом последовательном пропуске запрашивается защищённое автоматическое восстановление Gateway, если его жизненный цикл находится в состоянии running с разрешённым автовосстановлением. После записи конфигурации аутентификации в SQLite ClawX вызывает `secrets.reload` OpenClaw, чтобы работающие агенты получили новые учётные данные без перезапуска процесса.
|
||||
|
||||
Chat использует ACP stdio bridge, принадлежащий Electron Main. Main передаёт тому же локальному дочернему процессу управляемый приложением Gateway token через приватное окружение процесса, поэтому после перезагрузки конфигурации среды выполнения воспроизведение истории ACP остаётся аутентифицированным. Если защищённое восстановление Gateway прерывает принятый run основной сессии, исправленная среда OpenClaw запускает отдельный восстановительный run с явной ссылкой на id прерванного run. События Chat и agent сохраняют эту связь; переподключившийся ACP bridge принимает новый run для ожидающего prompt, сбрасывает потоковые курсоры run и подписывается на события инструментов сессии. Renderer не использует идентификатор среды Gateway и продолжает строить одну ACP timeline в памяти из типизированных host events. Gateway продолжает отвечать за возможности вне Chat: providers, models, skills, workspace, settings, diagnostics и media configuration.
|
||||
|
||||
### Семантический авторитет ACP
|
||||
|
||||
ACP является предпочтительным семантическим источником для каждого значения и контекста Chat, которые он предоставляет, а не только для истории `session/load`. Сюда относятся, где применимо, идентификатор сессии и маршрутизация, рабочее пространство и исполняемый `cwd`, состояние prompt и timeline, а также семантика стандартных resource и вложений. Если ACP предоставляет значение или событие, Main и Renderer должны использовать его, а не заменять снимком Gateway, выводом из transcript, локальной конфигурацией или параллельной проекцией.
|
||||
|
||||
Обход ACP разрешён только тогда, когда в upstream нет эквивалентной возможности. Такой путь совместимости должен быть узким, ограниченным и привязанным к session и generation. В соответствующем Harness reference или rule необходимо указать причину, источник истины, ограничения, поведение согласования и условие удаления; обход не должен незаметно стать конкурирующим источником истины.
|
||||
|
||||
### Авторитет истории ACP и ограниченные дополнения из transcript
|
||||
|
||||
Воспроизведение ACP `session/load` является главным источником истории Chat. ClawX не сохраняет второй ACP ledger, сокращённую timeline, кэш воспроизведения или восстановленную историю инструментов. Если структурированный ACP event ledger OpenClaw недоступен, его ACP adapter преобразует сохранённые записи transcript `toolCall` и `toolResult` в нативные обновления инструментов в исходном порядке, сохраняя границы text-tool-text; сам ClawX эти записи не выводит. Некоторые возможности OpenClaw пока не имеют полного соответствия в ACP. Например, media assistant может отсутствовать в ACP, а обработка Gateway может удалять директивы assistant `MEDIA:` из видимого потокового ответа. Поэтому ClawX хранит только ограниченные, помеченные дополнения совместимости в памяти:
|
||||
|
||||
- Асинхронное завершение генерации изображения можно восстановить только при наличии подтверждённого контекста `image_generate` в той же сессии и доверенного либо разрешённого transcript-доказательства.
|
||||
- Обычные вложения можно восстановить из канонических сохранённых фактов assistant `__openclaw.media` или явных директив assistant `MEDIA:` в начале строки. Восстанавливаются только ссылки на вложения и объявленные метаданные, но не окружающее сообщение assistant.
|
||||
- Поскольку ACP replay не содержит исходных временных меток событий, Main может добавить метаданные длительности всего хода из ограниченных записей transcript JSONL. Они могут быть привязаны только к уже восстановленному ACP-ходу.
|
||||
- Если ACP replay cron-сессии полностью пуст, типизированный API истории cron в Main может предоставить запланированный запрос и сводку завершения. Если сводка идентифицированного запуска содержит маркер усечения OpenClaw, Main может восстановить финальный текст assistant из transcript этого запуска только когда transcript длиннее и содержит полный сохранённый префикс сводки.
|
||||
|
||||
Историческое чтение ограничено последними 1000 сообщениями transcript. Успешный live prompt выполняет одно немедленное чтение и одну повторную попытку через 1500 мс. Каждое дополнение привязано к точной session, ACP generation, операции и, где применимо, текущему пользовательскому ходу; устаревшие, отсутствующие, дублирующиеся и неоднозначные совпадения отбрасываются. Эти пути не должны восстанавливать обычные сообщения assistant, thoughts, tools, plans, permissions, файловые операции, пропущенные ходы или параллельную историю Chat. Main не создаёт нативные события ACP из transcript-доказательств. Стандартные ACP resources остаются предпочтительными, а после появления эквивалентного контента upstream эти исключения совместимости должны быть удалены.
|
||||
|
||||
Незавершённый ответ ACP продолжает потоковую выдачу при открытии другого разговора или страницы. Возврат до завершения восстанавливает последнюю timeline в памяти и продолжает отображение ответа. После завершения обычное воспроизведение истории ACP остаётся источником истины.
|
||||
|
||||
Ходы assistant в ACP показывают длительность всего хода. Живой таймер следует за наблюдаемым клиентом жизненным циклом prompt и сохраняется при навигации внутри приложения. Историческая длительность вычисляется Electron Main по ограниченным временным меткам transcript OpenClaw и добавляется только к ходу, уже восстановленному ACP replay.
|
||||
|
||||
ACP Chat отображает стандартные ACP resources как вложения. Выбранные пользователем изображения показываются как миниатюры с именем файла при наведении, а другие доступные карточки вложений содержат имя файла и приглушённый обрезаемый исходный путь. Если текущий OpenClaw ACP adapter не передаёт media assistant, канонические сохранённые факты media OpenClaw и явные директивы assistant `MEDIA:` также могут быть восстановлены как карточки вложений без отображения метаданных, предназначенных только для transcript.
|
||||
|
||||
Существующие локальные ссылки на файлы, включая пути за пределами активного рабочего пространства, перед каждым предпросмотром или открытием повторно проверяются Electron Main для точной session и generation. Локальные вложения, созданные AI и доступные для предпросмотра, включая `.docx` и `.pptx` размером до 20 МБ, сохраняют основное действие предпросмотра только для чтения внутри приложения и дополнительное меню для открытия совместимым приложением или показа в Finder, File Explorer либо системном файловом менеджере. Для локальных HTML-вложений первый пункт меню открывает файл во вкладке Preview справа.
|
||||
|
||||
Для Office действуют те же ограничения: `.doc` и `.ppt` открываются системным приложением, разбиение DOCX на страницы может отличаться от Microsoft Word, а анимации, переходы и воспроизведение медиа в PPTX не поддерживаются. Поиск совместимых приложений доступен только в macOS и Windows; в Linux или при ошибке поиска происходит незаметный переход к действию показа расположения. Остальные локальные файлы, включая Office-файлы размером более 20 МБ, открываются системным приложением после нажатия пользователя. Выбранные пользователем папки остаются доступными после отправки и открываются системным файловым менеджером; ClawX не читает и не просматривает их содержимое. Вложения HTTP и HTTPS открываются внешне после нажатия. Обычные пути в тексте без канонических media-фактов не считаются вложениями.
|
||||
|
||||
ACP Chat также может показывать предпросмотр сгенерированных изображений, когда среда выполнения доставляет media генерации как доверенные структурированные данные. Доверенные OpenClaw internal-UI доставки и финальные ответы, связанные с задачей генерации, сохраняют исходный пользовательский текст завершения, включая текстовое описание ошибки, вместо замены на общий заголовок изображения. При историческом воспроизведении OpenClaw маркеры assistant `MEDIA:` переводятся в встроенный просмотр изображения только после зарегистрированного запуска задачи генерации в той же сессии. ClawX загружает предпросмотр через обработку media на стороне Electron Main, а не через произвольный доступ Renderer к файловой системе. Стандартные изображения и ресурсы ACP остаются предпочтительным путём и отображаются напрямую.
|
||||
|
||||
### Семантика файловых операций ACP
|
||||
|
||||
- Файловые операции проецируются из успешных завершённых вызовов OpenClaw `write`, `edit` и `apply_patch`. Распознавание инструментов соответствует официальному OpenClaw Chat UI; фильтрация только завершённых вызовов специфична для ClawX.
|
||||
- Строки созданных и изменённых файлов используют ту же оболочку карточки и меню **Open with**, что и предпросматриваемые вложения assistant, сохраняя статус и необязательную сводку `+/-`. Для HTML первый пункт меню открывает файл во вкладке **Preview** справа. Удалённые строки сохраняют только действие **Changes**. Каждый запрос списка приложений, выбора приложения и показа расположения заново проверяется Electron Main по корню рабочего пространства и относительному пути. Пути из инструментов не становятся вложениями и не раскрывают Renderer канонические системные пути.
|
||||
- `write` отображается так, как его объявляет инструмент: как создание с разницей из всех добавленных строк, даже если путь уже может существовать.
|
||||
- **Changes** — это хронологическая запись объявленной инструментом активности на уровне сессии. Это не вывод Git и не проверенная разница относительно исходной базы.
|
||||
- Для каждого файла Changes отображает не более одного diff-редактора на ход assistant. Последовательные фрагменты объединяются, если это безопасно; независимые фрагменты объединяются в один редактор без утверждения, что это полная разница относительно базовой версии файла.
|
||||
- Побочные эффекты shell-команд, скриптов, пользователей или IDE не обнаруживаются.
|
||||
- Полное ACP replay может восстановить записанные файловые операции. При неполном replay ClawX не выводит пропущенную активность через fallback.
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ Десктопное приложение ClawX │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Главный процесс Electron │ │
|
||||
│ │ • Управление жизненным циклом окна и приложения │ │
|
||||
│ │ • Наблюдение за процессом Gateway │ │
|
||||
│ │ • Интеграция с системой (трей, уведомления, связка ключей) │ │
|
||||
│ │ • Оркестрация автообновлений │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
│ IPC (авторитетная плоскость управления)
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ Процесс Renderer на React │
|
||||
│ • Современный компонентный UI (React 19) │
|
||||
│ • Управление состоянием с Zustand │
|
||||
│ • Унифицированные вызовы host-api/api-client │
|
||||
│ • Ответы assistant в Markdown, ввод пользователя как обычный текст│
|
||||
└──────────────────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
│ Типизированные IPC-запросы
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ Main Host Services и Gateway Manager │
|
||||
│ • Типизированный диспетчер сервисов host:invoke │
|
||||
│ • Настройки, файлы, сессии, навыки, провайдеры, диагностика │
|
||||
│ • WebSocket Gateway и наблюдение за процессом принадлежат Main │
|
||||
└──────────────────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
│ WebSocket под управлением Main
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ OpenClaw Gateway │
|
||||
│ • Среда выполнения и оркестрация AI-агентов │
|
||||
│ • Управление каналами сообщений │
|
||||
│ • Среда выполнения навыков/плагинов │
|
||||
│ • Уровень абстракции провайдеров │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Принципы проектирования
|
||||
|
||||
- **Изоляция процессов**: AI-среда выполнения работает в отдельном процессе, сохраняя отзывчивость UI даже при тяжёлых вычислениях.
|
||||
- **Единая точка входа для фронтенда**: запросы Renderer проходят через `host-api` / `api-client`, а детали протокола скрыты за стабильным интерфейсом.
|
||||
- **Транспорт принадлежит Main**: Electron Main владеет ACP Chat stdio bridge и транспортами Gateway; Renderer общается с Main через типизированный IPC.
|
||||
- **Расширения через IPC**: расширения Main-процесса добавляют действия host-api через типизированный IPC-реестр, а не через HTTP routes.
|
||||
- **Корректное восстановление**: встроенные переподключение, таймауты и backoff автоматически обрабатывают временные сбои.
|
||||
- **Безопасное хранение**: API-ключи и конфиденциальные данные используют нативные механизмы безопасного хранения ОС.
|
||||
- **CORS-безопасность**: Renderer не вызывает напрямую локальные HTTP-эндпоинты Gateway или Host API.
|
||||
|
||||
### Модель процессов и устранение неполадок Gateway
|
||||
|
||||
- ClawX — приложение Electron, поэтому **один экземпляр обычно отображается как несколько процессов ОС** (main/renderer/zygote/utility). Это нормально.
|
||||
- Защита единственного экземпляра использует блокировку Electron и резервный локальный файл блокировки процесса, предотвращая дублирование запуска при нестабильном desktop IPC или сессионной шине.
|
||||
- При последовательном обновлении смешанные старые и новые версии могут вести себя асимметрично. Для надёжности обновляйте все десктопные клиенты до одной версии.
|
||||
- Слушатель OpenClaw Gateway должен иметь **единственного владельца**: только один процесс должен слушать `127.0.0.1:18789`.
|
||||
- Готовность Gateway определяется основными сигналами OpenClaw, такими как `system-presence`, `health` и `status`. Ошибки памяти или каналов отображаются как снижение возможностей, а не как общий сбой Gateway.
|
||||
- Проверить активный слушатель можно командами:
|
||||
- macOS/Linux: `lsof -nP -iTCP:18789 -sTCP:LISTEN`
|
||||
- Windows (PowerShell): `Get-NetTCPConnection -LocalPort 18789 -State Listen`
|
||||
- Нажатие кнопки закрытия окна (`X`) скрывает ClawX в трее, но не завершает приложение. Для полного завершения используйте **Quit ClawX** в меню трея.
|
||||
@@ -1,130 +0,0 @@
|
||||
# Руководство по разработке ClawX
|
||||
|
||||
Этот документ содержит подробную версию раздела «Разработка» из README.
|
||||
|
||||
### Требования
|
||||
|
||||
- **Node.js**: 22.22.3+, 24.15.0+ или 25.9.0+ в пределах соответствующей основной версии (рекомендуется Node 24 LTS)
|
||||
- **Менеджер пакетов**: pnpm 9+ (npm также поддерживается)
|
||||
- **Linux (Ubuntu/Debian)**: перед запуском Electron установите необходимые системные библиотеки:
|
||||
```bash
|
||||
sudo apt-get install -y libnss3 libgtk-3-0 libxss1 libxtst6 libatspi2.0-0 libnotify4 xdg-utils
|
||||
```
|
||||
В Ubuntu 24.04+ некоторые пакеты используют суффикс `t64`; после выполнения команды `apt` автоматически выберет подходящий вариант.
|
||||
|
||||
### Структура проекта
|
||||
|
||||
```text
|
||||
ClawX/
|
||||
├── electron/ # Главный процесс Electron
|
||||
│ ├── services/ # Типизированные Host API, провайдеры, секреты и runtime-сервисы
|
||||
│ │ ├── providers/ # Логика синхронизации моделей provider/account
|
||||
│ │ └── secrets/ # Связка ключей ОС и хранилище секретов
|
||||
│ ├── shared/ # Общие схемы провайдеров и константы
|
||||
│ │ └── providers/
|
||||
│ ├── main/ # Точка входа приложения, окна и регистрация IPC
|
||||
│ ├── gateway/ # Менеджер процесса OpenClaw Gateway
|
||||
│ ├── preload/ # Безопасный IPC-мост
|
||||
│ └── utils/ # Утилиты для хранилища, аутентификации и путей
|
||||
├── src/ # Процесс Renderer на React
|
||||
│ ├── lib/ # Унифицированный фронтенд API и модель ошибок
|
||||
│ ├── stores/ # Хранилища Zustand (settings/chat/gateway)
|
||||
│ ├── components/ # Переиспользуемые UI-компоненты
|
||||
│ ├── pages/ # Setup/Dashboard/Chat/Channels/Skills/Cron/Settings
|
||||
│ ├── i18n/ # Ресурсы локализации
|
||||
│ └── types/ # Определения типов TypeScript
|
||||
├── tests/
|
||||
│ ├── e2e/ # Сквозные дымовые тесты Playwright Electron
|
||||
│ └── unit/ # Модульные и интеграционные тесты Vitest
|
||||
├── resources/ # Статические ресурсы (иконки и изображения)
|
||||
└── scripts/ # Скрипты сборки и утилит
|
||||
```
|
||||
|
||||
### Доступные команды
|
||||
|
||||
```bash
|
||||
# Разработка
|
||||
pnpm run init # Установить зависимости и скачать встроенные бинарные файлы (uv, agent-browser)
|
||||
pnpm dev # Запуск с горячей перезагрузкой (автоподготовка bundled skills при отсутствии)
|
||||
|
||||
# Качество
|
||||
pnpm lint # Запустить ESLint
|
||||
pnpm typecheck # Проверить типы TypeScript
|
||||
|
||||
# Тестирование
|
||||
pnpm test # Запустить модульные тесты
|
||||
pnpm run test:e2e # Запустить дымовые E2E-тесты Electron
|
||||
pnpm run test:e2e:headed # Запустить E2E-тесты Electron с видимым окном
|
||||
pnpm run perf:chat # Получить синтетические CPU-профили Chat Renderer/Main
|
||||
pnpm run profile:main # Запустить собранное приложение с Main inspector на порту 9229
|
||||
pnpm run comms:replay # Рассчитать метрики повторного воспроизведения коммуникаций
|
||||
pnpm run comms:baseline # Обновить снимок базовой линии коммуникаций
|
||||
pnpm run comms:compare # Сравнить метрики с порогами базовой линии
|
||||
|
||||
# Сборка и упаковка
|
||||
pnpm run build:vite # Собрать только фронтенд
|
||||
pnpm build # Полная production-сборка с ресурсами упаковки
|
||||
pnpm package # Упаковать для текущей платформы со встроенными навыками
|
||||
pnpm package:mac # Упаковать для macOS
|
||||
pnpm package:win # Упаковать для Windows
|
||||
pnpm package:linux # Упаковать для Linux
|
||||
```
|
||||
|
||||
В headless Linux тестам Electron нужен сервер отображения. Используйте `xvfb-run -a pnpm run test:e2e`.
|
||||
|
||||
Функциональные E2E-тесты Electron локально и в CI по умолчанию используют два worker-процесса Playwright. Обычную параллельную группу можно настроить через `CLAWX_E2E_WORKERS=<положительное целое>`. Тесты, затрагивающие глобальное состояние ОС, используют однопоточный проект `exclusive`, а профили производительности хоста запускаются отдельно после них. Новые E2E-тесты по умолчанию параллельны; при использовании реального буфера обмена или другого общего ресурса машины применяйте `E2E_EXCLUSIVE_TAG` из `tests/e2e/parallel-policy.ts`.
|
||||
|
||||
Для запуска отдельного обычного spec без эксклюзивного предварительного этапа используйте `pnpm exec playwright test <spec> --project=parallel --no-deps`.
|
||||
|
||||
### Диагностика производительности Electron
|
||||
|
||||
`pnpm run perf:chat` запускает изолированные синтетические ACP-нагрузки для потоковой выдачи и взаимодействия с боковой панелью и прокруткой в статическом Markdown-документе. В каталог Playwright `test-results/` записываются версионированные метрики и CPU-профили Renderer и Main. Профили Renderer охватывают production store/render-путь и плавность кадров. Потоковый профиль Main измеряет IPC fanout от Main к Renderer, а профиль интеракций показывает, остаётся ли Main свободным во время действий Renderer. Ни один профиль не включает процессы upstream OpenClaw/ACP или путь GPU-процесса.
|
||||
|
||||
CPU-профиль можно открыть в Chrome DevTools. Артефакты содержат только сгенерированный fixture-текст и не являются телеметрией продукта. Результаты зависят от оборудования, поэтому сравнивайте повторные запуски на одной машине, а не применяйте единый абсолютный порог для разных платформ.
|
||||
|
||||
Для записи реального Renderer запустите разработку командой `CLAWX_REMOTE_DEBUGGING_PORT=9223 pnpm dev` и подключите Playwright или Chrome DevTools к `localhost:9223`. Для записи реального Electron Main выполните `pnpm run profile:main`, откройте `chrome://inspect`, настройте `localhost:9229` и выберите цель Electron Main. Не устанавливайте `CLAWX_GATEWAY_WS_TRACE`, если измеряется не сам WebSocket trace.
|
||||
|
||||
ClawX по умолчанию оставляет аппаратное ускорение Chromium включённым, чтобы длинные документы, прокрутка и анимации layout использовали GPU-композицию и растеризацию. При проблемах с графическим драйвером можно использовать встроенный переключатель Chromium `--disable-gpu` как резервный вариант диагностики.
|
||||
|
||||
### Проверки регрессии коммуникаций
|
||||
|
||||
Если PR изменяет пути коммуникации, включая события Gateway, поток отправки/получения ACP Chat, доставку каналов или транспортный fallback, выполните:
|
||||
|
||||
```bash
|
||||
pnpm run comms:replay
|
||||
pnpm run comms:compare
|
||||
```
|
||||
|
||||
Задача CI `comms-regression` проверяет обязательные сценарии и пороги.
|
||||
|
||||
### E2E-тесты Electron
|
||||
|
||||
Набор Playwright Electron запускает упакованные Renderer и Main-процессы из `dist/` и `dist-electron/`, поэтому заранее вручную запускать `pnpm dev` не требуется.
|
||||
|
||||
`pnpm run test:e2e` автоматически:
|
||||
|
||||
- собирает Renderer и бандлы Electron через `pnpm run build:vite`
|
||||
- запускает Electron в изолированном E2E-режиме с временным `HOME`
|
||||
- использует временный каталог `userData` ClawX
|
||||
- запускает обычные spec-файлы параллельно, изолируя тесты глобальных ресурсов ОС и производительности
|
||||
- пропускает тяжёлые побочные эффекты запуска, такие как автозапуск Gateway, установка bundled skills, создание трея и автоустановка CLI
|
||||
|
||||
Первые базовые spec покрывают:
|
||||
|
||||
- видимость Setup Wizard при первом запуске на чистом профиле
|
||||
- пропуск настройки и переход на страницу Models внутри приложения Electron
|
||||
|
||||
Добавляйте будущие сценарии Electron в `tests/e2e/` и переиспользуйте общий fixture из `tests/e2e/fixtures/electron.ts`. Сохраняйте тесты безопасными для параллельного запуска: избегайте фиксированных доступных для записи путей, портов, нативных хранилищ ключей и другого внешнего общего состояния. Если изоляция невозможна, используйте `E2E_EXCLUSIVE_TAG`.
|
||||
|
||||
### Технологический стек
|
||||
|
||||
| Уровень | Технология |
|
||||
|---------|------------|
|
||||
| Среда выполнения | Electron 40+ |
|
||||
| UI-фреймворк | React 19 + TypeScript |
|
||||
| Стилизация | Tailwind CSS + shadcn/ui |
|
||||
| Состояние | Zustand |
|
||||
| Сборка | Vite + electron-builder |
|
||||
| Тестирование | Vitest + Playwright |
|
||||
| Анимация | Framer Motion |
|
||||
| Иконки | Lucide React |
|
||||
@@ -1,83 +0,0 @@
|
||||
# Возможности ClawX
|
||||
|
||||
Этот документ содержит подробную версию раздела «Возможности» из README.
|
||||
|
||||
### Нулевой порог настройки
|
||||
|
||||
Весь процесс от установки до первого разговора с AI выполняется через интуитивный графический интерфейс. Терминальные команды, YAML-файлы и поиск переменных окружения не требуются.
|
||||
|
||||
### Интеллектуальный интерфейс чата
|
||||
|
||||
Общайтесь с AI-агентами через современный чат. ClawX поддерживает несколько контекстов разговоров и историю сообщений, а ответы агента отображаются как потоковый Markdown с подсветкой синтаксиса для fenced code, разбором CJK, таблицами GitHub-flavored и формулами LaTeX через KaTeX (`$строчные$`, `$$блочные$$`, `\(строчные\)` и `\[блочные\]`). Пользовательский ввод всегда остаётся обычным текстом. В многoагентных конфигурациях основное поле ввода поддерживает прямую маршрутизацию через `@agent`. Fenced code сохраняет исходные переводы строк, переносит длинные строки и предоставляет локализованное действие копирования после завершения потоковой выдачи.
|
||||
|
||||
Навыки, вставленные из композитора, отображаются как карточки `/skill-name`. Нажмите карточку, чтобы открыть боковую панель предпросмотра и прочитать `SKILL.md` этого навыка.
|
||||
|
||||
При выборе другого агента через `@agent` ClawX напрямую переключается в контекст этого агента, не передавая запрос через агента по умолчанию. Рабочие пространства агентов по умолчанию разделены, а более строгая изоляция среды выполнения зависит от настроек sandbox OpenClaw.
|
||||
|
||||
Боковая панель сессий организована по принципу «сначала рабочие пространства»: рабочее пространство по умолчанию находится вверху, остальные сортируются естественным образом, а каждое рабочее пространство можно свернуть или загрузить для него дополнительные сессии. Во время ответа AI в строке сессии отображается индикатор загрузки, после завершения непросмотренного ответа появляется синяя точка, а после открытия разговора снова показывается относительное время активности. При наведении отображаются действия строки. Импортированные рабочие пространства можно переименовать из заголовка боковой панели. Пользовательское имя отображается в композиторе чата, а при наведении на заголовок по-прежнему виден путь в файловой системе.
|
||||
|
||||
Если выбрано доступное рабочее пространство, новый чат наследует его и остаётся редактируемым до первой отправки. В редактируемых новых или ещё не привязанных чатах чип рабочего пространства в композиторе открывает меню с недавно использованными и известными из сессий рабочими пространствами. Можно вернуться к рабочему пространству по умолчанию или выбрать другую папку. Если сохранённая папка была перемещена или удалена, Chat приостанавливает создание сессии и предлагает выбрать существующую папку вместо повторных попыток использовать недоступный путь. Недоступные группы, кроме группы по умолчанию, отмечаются в боковой панели и могут быть удалены после подтверждения; это навсегда удаляет все сессии группы. Строка сессии удаляется, а навигация меняется только после успешного окончательного удаления. При ошибке удаления разговор и подтверждение остаются открытыми для повторной попытки. Синтетические резервные заголовки OpenClaw с UUID и датой считаются отсутствующими только при совпадении с ID сессии, после чего заменяются первым сообщением пользователя и не сохраняются как имя сессии.
|
||||
|
||||
Каждый агент может переопределить собственную настройку среды выполнения `provider/model`. Агенты без переопределения продолжают наследовать глобальную модель по умолчанию.
|
||||
|
||||
Вкладки Workspace и Preview на правой панели Chat предоставляют предпросмотр только для чтения файлов Markdown, `.docx` и `.pptx`. Предпросмотр Markdown в статическом режиме использует ту же подсветку синтаксиса, перенос длинных строк, копирование fenced code, разбор CJK и поддержку формул KaTeX. В заголовке Preview можно развернуть выбранный файл на весь видимый экран ClawX; тем же элементом управления или клавишей Escape можно вернуться на панель. Устаревшие `.doc` и `.ppt` по-прежнему открываются через операционную систему, а не внутри приложения. Разбиение DOCX на страницы может отличаться от Microsoft Word; предпросмотр PPTX не поддерживает анимации, переходы и воспроизведение медиа. Office-файлы размером более 20 МБ не просматриваются внутри приложения.
|
||||
|
||||
### Предпросмотр локального HTML
|
||||
|
||||
На правой панели Chat есть вкладки Workspace, Preview и Changes. Универсального веб-браузера, домашней страницы и адресной строки больше нет. Разрешённые локальные вложения `.html` и `.htm`, файловые операции и файлы рабочего пространства по умолчанию открываются в Preview. В действиях файла можно выбрать встроенный Preview или системное приложение, а заголовок Preview может открыть текущий HTML-файл в системном браузере.
|
||||
|
||||
Все ссылки некликабельны. Ссылки, отображаемые ClawX, выглядят как обычный текст; в HTML Preview также удаляются оформление ссылок и взаимодействие указателем. HTML Preview блокирует формы, переходы из скриптов, перенаправления, переходы внутри страницы, всплывающие окна, загрузки, сетевые запросы и разрешения устройств. Самодостаточный локальный HTML отображается, но не может покинуть выбранный документ.
|
||||
|
||||
### Управление несколькими каналами
|
||||
|
||||
Настраивайте и отслеживайте несколько AI-каналов одновременно. Каждый канал работает независимо, позволяя запускать специализированных агентов для разных задач.
|
||||
|
||||
Каждый канал поддерживает несколько аккаунтов, привязку агента к аккаунту и переключение аккаунта канала по умолчанию прямо на странице Channels.
|
||||
|
||||
Для пользовательских ID аккаунтов каналов ClawX требует совместимый с OpenClaw канонический формат: `[a-z0-9_-]`, строчные буквы, максимум 64 символа, начало с буквы или цифры. Это предотвращает ошибки маршрутизации.
|
||||
|
||||
ClawX также включает официальный плагин личного WeChat от Tencent, поэтому WeChat можно подключить прямо на странице Channels через встроенный QR-код.
|
||||
|
||||
### Автоматизация по расписанию
|
||||
|
||||
Планируйте автоматический запуск AI-задач. Определяйте триггеры и интервалы, чтобы AI-агенты могли работать круглосуточно.
|
||||
|
||||
На странице Cron внешнюю доставку можно настроить непосредственно в форме задачи с отдельными селекторами аккаунта отправителя и цели получателя. Для поддерживаемых каналов цели получателей автоматически обнаруживаются из каталогов каналов или известной истории сессий, поэтому больше не нужно вручную редактировать `jobs.json`. Поле сообщения задачи поддерживает вставку навыков тем же синтаксисом встроенных токенов `/skill`, что и основной композитор чата, с учётом выбранного агента. Запланированные запросы могут запускать навыки напрямую.
|
||||
|
||||
Выбор расписания разделён на вкладки **Повтор** и **Однократно**. Повтор предлагает частоты «Ежечасно», «Ежедневно», «По будням», «Еженедельно» и «Свой» (произвольный cron) со встроенным выбором времени и дня недели. Однократно запускает задачу один раз в выбранные дату и время с отображением дня недели. Одноразовые задачи должны быть запланированы на будущее и автоматически удаляются средой выполнения после завершения.
|
||||
|
||||
### Расширяемая система навыков
|
||||
|
||||
Расширяйте возможности AI-агентов готовыми навыками. Встроенная страница Skills работает по принципу local-first: сканирует управляемые каталоги и каталоги рабочего пространства и позволяет включать или отключать навыки без зависимости от Gateway. В корпоративных сборках расширение также может предоставить собственный маркетплейс.
|
||||
|
||||
ClawX предварительно упаковывает полные навыки обработки документов (`pdf`, `xlsx`, `docx`, `pptx`), автоматически развёртывает их в управляемый каталог навыков (по умолчанию `~/.openclaw/skills`) при запуске и включает их по умолчанию при первой установке.
|
||||
|
||||
На странице Skills можно показывать навыки из нескольких источников OpenClaw, включая управляемый каталог, workspace и дополнительные каталоги навыков. Для каждого навыка отображается фактическое расположение, чтобы папку можно было открыть напрямую. Для bundled skills OpenClaw в community-сборках упаковывается и отображается только `skill-creator`; bundled skills, отсутствующие в списке разрешённых, физически удаляются при запуске в режиме разработки и в packaged-сборках, а устаревшие записи для удалённых навыков в `openclaw.json` очищаются.
|
||||
|
||||
### Безопасная интеграция провайдеров
|
||||
|
||||
Подключайтесь к нескольким AI-провайдерам, включая OpenAI, Anthropic и Z.AI / GLM; учётные данные безопасно хранятся в нативном системном хранилище ключей. OpenAI поддерживает API-ключи и браузерный OAuth для подписок Codex.
|
||||
|
||||
В режиме разработчика отдельная страница Image Generation поддерживает независимый OpenAI-совместимый эндпоинт генерации изображений с Base URL, API-ключом и именем модели, например `gpt-image-2`. Поэтому генерация изображений может использовать отдельный сервис `/v1/images/generations`, а чат продолжает использовать обычный OpenAI-провайдер.
|
||||
|
||||
Для **Custom**-провайдеров, работающих с OpenAI-совместимыми шлюзами, можно задать собственный `User-Agent` в разделе **Настройки → AI-провайдеры → Редактировать провайдер** для эндпоинтов с требованиями к совместимости.
|
||||
|
||||
При редактировании или переключении провайдера ClawX сохраняет существующие метаданные возможностей модели, например `input: ["text", "image"]`. Для новых моделей Custom-провайдера используется совместимая с OpenClaw onboarding логика определения поддержки изображений; неизвестные модели считаются текстовыми.
|
||||
|
||||
Строки моделей Custom-провайдера также получают явный `contextWindow`, рассчитанный по семейству модели, например `gpt-5.x` → 272k. Строки, сохранённые старыми версиями, дополняются при запуске, чтобы OpenClaw мог сжимать длинные сессии до ошибки «Context overflow». Если настройки сжатия отсутствуют, ClawX создаёт `agents.defaults.compaction.mode = "safeguard"` и `reserveTokensFloor = 50000`. Созданные пользователем строки и конфигурации не изменяются, кроме возможного дополнения отсутствующего `reserveTokensFloor`.
|
||||
|
||||
Z.AI (CN / Global) соответствует встроенному провайдеру OpenClaw `zai` (`ZAI_API_KEY`). Модель по умолчанию — `glm-5.2`. Пресет Code Plan использует эндпоинты Coding Plan (`.../api/coding/paas/v4`), а обычные API — (`.../api/paas/v4`). CN и Global взаимоисключающие, поскольку используют один ключ среды выполнения OpenClaw.
|
||||
|
||||
Если совместимый шлюз отклоняет `/models` по причинам, не связанным с аутентификацией, ClawX во время проверки API-ключа автоматически переключается на лёгкий зонд `/chat/completions` или `/responses` с настроенной моделью.
|
||||
|
||||
### Адаптивные темы
|
||||
|
||||
Выбирайте светлую, тёмную или синхронизированную с системой тему. ClawX автоматически адаптируется к вашим предпочтениям.
|
||||
|
||||
### Управление автозапуском
|
||||
|
||||
В разделе **Настройки → Общие** включите **Запускать при старте системы**, чтобы ClawX автоматически запускался после входа в систему.
|
||||
|
||||
### Запросы на обновление
|
||||
|
||||
ClawX проверяет наличие новых версий при запуске. Если обновление доступно, приложение показывает запрос; скачивание и установка выполняются только после вашего выбора.
|
||||
@@ -1,12 +0,0 @@
|
||||
# Настройки прокси ClawX
|
||||
|
||||
Этот документ содержит подробную версию раздела «Настройки прокси» из README.
|
||||
|
||||
- Значение в формате `host:port` рассматривается как HTTP-прокси.
|
||||
- Если расширенные поля прокси пусты, ClawX использует **Прокси-сервер**.
|
||||
- Сохранение настроек прокси немедленно повторно применяет сетевые настройки Electron и автоматически перезапускает Gateway.
|
||||
- При включённом Telegram ClawX также синхронизирует прокси с конфигурацией канала Telegram в OpenClaw.
|
||||
- Если прокси ClawX отключён, обычный перезапуск Gateway сохраняет существующий прокси канала Telegram.
|
||||
- Чтобы явно удалить прокси Telegram из конфигурации OpenClaw, отключите прокси и один раз сохраните настройки прокси.
|
||||
- В разделе **Настройки → Дополнительно → Разработчик** можно запустить **OpenClaw Doctor**. Он выполняет `openclaw doctor --json` и показывает диагностический вывод в приложении.
|
||||
- В упакованных сборках Windows встроенный `openclaw` CLI/TUI запускается через поставляемую точку входа `node.exe`, чтобы сохранить стабильное поведение ввода в терминале.
|
||||
@@ -1,110 +0,0 @@
|
||||
# ClawX 系统架构
|
||||
|
||||
本文档是 README「系统架构」一节的详细说明。
|
||||
|
||||
ClawX 采用 **双进程 + Host API 统一接入架构**。渲染进程只调用统一客户端抽象,协议选择与进程生命周期由 Electron 主进程统一管理:
|
||||
|
||||
OpenClaw 配置交付也统一由 Electron Main 管理。Gateway 运行时,ClawX 以 `config.get` 返回的权威快照为基线,并通过 `config.set` 提交修改;Gateway 停止或启动中时,同一个协调器只更新解析后的 JSON5 配置文件,不会因此启动 Gateway。因此,普通的 Provider、Agent、Channel、绑定、Skill 和模型修改不会替换 Gateway 进程。完整重启仅保留给代理等进程启动环境变化和用户显式操作。已确认的进程退出与 WebSocket 关闭继续使用现有的自动重连路径。连续前 3 次 WebSocket 心跳无响应只更新诊断,不会因短暂的 pong 延迟中断长时间运行的任务;收到 pong 或任意消息会重置计数,连续第 4 次无响应时,只有在生命周期处于可自动恢复的 running 状态时,才会请求受保护的 Gateway 自动恢复。认证配置写入 SQLite 后,ClawX 会调用 OpenClaw 的 `secrets.reload`,让运行中的 Agent 无需重启即可读取新凭据。
|
||||
|
||||
Chat 使用由 Electron Main 持有的 ACP stdio bridge。Main 通过私有进程环境把同一份应用管理的 Gateway token 传给本地子进程,因此运行时配置重载后 ACP 历史回放仍能完成认证。如果受保护的 Gateway 恢复中断了已接收的主会话 run,补丁后的 OpenClaw 运行时会启动独立的恢复 run,并显式携带被中断 run id 作为 lineage。Chat 和 agent events 会保留该 lineage;重连后的 ACP bridge 据此将 pending prompt 接续到新 run,重置该 run 的流式游标,并订阅会话级 tool events。Renderer 不感知 Gateway 运行实例身份,仍通过类型化 host events 渲染同一个内存 ACP timeline。Gateway 继续负责 providers、models、skills、workspace、settings、diagnostics 和 media configuration 等非 Chat 能力。
|
||||
|
||||
### ACP 语义权威
|
||||
|
||||
对于 ACP 能够提供的每一种 Chat 语义和上下文,ACP 都是优先的语义权威。这包括适用时的 session identity 与路由、工作空间和执行 `cwd`、prompt 与 timeline 状态,以及标准 resource 或附件语义。ACP 提供值或事件时,Main 和 Renderer 必须使用 ACP 的结果,不得用 Gateway 快照、transcript 推断、本地配置或另一套并行投影替代。
|
||||
|
||||
只有在上游 ACP 没有对应能力时,才允许绕过 ACP。此类兼容性路径必须保持狭窄、有界,并绑定 session 和 generation;同时必须在相关 Harness reference 或 rule 中记录其原因、事实来源、限制、协调行为和移除条件,不得悄悄演变为竞争性的权威来源。
|
||||
|
||||
### ACP 历史权威与有界 transcript 补充
|
||||
|
||||
ACP `session/load` 回放是 Chat 历史的首要事实来源。ClawX 不会持久化第二套 ACP ledger、精简 timeline、回放缓存或重建的工具历史。当 OpenClaw 的结构化 ACP event ledger 不可用时,其 ACP adapter 会按 transcript 顺序把持久化的 `toolCall` 和 `toolResult` 记录重建为原生工具更新,并保留 text-tool-text 边界;ClawX 本身不会推断这些记录。OpenClaw 的部分能力目前还没有完全对应的 ACP 实现;例如,assistant 媒体可能不会出现在 ACP 中,Gateway 处理也可能从可见的实时回复中移除 assistant `MEDIA:` 指令。因此,ClawX 只保留有界、带标记、仅存于内存的兼容性补充路径:
|
||||
|
||||
- 只有在同一 session 中存在已确认的 `image_generate` 上下文,且完成证据可信或来自获准 transcript 证据时,才可以恢复异步图像生成结果。
|
||||
- 普通附件可以从持久化的 assistant `__openclaw.media` 规范事实或明确的行首 assistant `MEDIA:` 指令中恢复。这只恢复附件引用和声明的元数据,不恢复周围的 assistant 消息。
|
||||
- 由于 ACP 回放不提供原始事件时间戳,Main 可以从有界的 transcript JSONL 记录中补充仅包含元数据的整轮耗时,但只能标注已经由 ACP 回放恢复出的回合。
|
||||
- 如果 cron session 的 ACP 回放完全为空,Main 的类型化 cron-history API 可以提供计划提示词和完成摘要。当已识别的运行摘要带有 OpenClaw 截断标记时,只有在对应 run 的 transcript 更长且共享完整的已持久化摘要前缀时,Main 才可以恢复最终 assistant 文本。
|
||||
|
||||
历史读取最多读取最近 1000 条 transcript 消息。一次成功的实时 prompt 会立即读取一次,并在 1500ms 后重试一次。每个补充路径都必须绑定精确的 session、ACP generation、补充操作,并在适用时绑定当前的用户回合;过期、缺失、重复或有歧义的匹配都会被丢弃。这些路径不得重建普通 assistant 消息、thought、tool、plan、permission、文件活动、缺失回合或另一套 Chat 历史,Main 也不得根据 transcript 伪造原生 ACP 事件。标准 ACP resource 仍是首选;上游提供等价内容后,这些兼容性例外应当移除。
|
||||
|
||||
打开其它会话或页面时,尚未完成的 ACP 回复仍会继续流式接收。若在回复完成前返回,ClawX 会恢复最新的内存 timeline 并继续显示实时输出;回复完成后,普通 ACP 历史回放仍是唯一事实来源。
|
||||
|
||||
ACP assistant 回合会显示整轮耗时。Live 计时跟随客户端观测到的 prompt 生命周期,并在应用内导航后保持连续;历史耗时由 Electron Main 根据有界的 OpenClaw transcript 时间戳计算,而且只能标注 ACP 回放已经恢复出的回合。
|
||||
|
||||
ACP Chat 会将标准 ACP resource 渲染为附件。用户选择的图片会显示为缩略图,并在悬停蒙层中显示文件名;其它可用的附件卡片会显示文件名,以及灰色、可截断的来源路径。当前 OpenClaw ACP adapter 遗漏 assistant 媒体时,OpenClaw 持久化的规范媒体事实和显式 assistant `MEDIA:` 指令也可恢复为附件卡片,且不会显示仅用于 transcript 的元数据。现有本地文件引用(包括当前 workspace 外的路径)在每次预览或打开前,都会由 Electron Main 按精确的 session 和 generation 重新验证。AI 生成且可预览的本地附件(包括不超过 20 MB 的 `.docx` 和 `.pptx` 文件)会保留主要的只读应用内预览操作,并提供次级菜单,可通过兼容应用打开,或在 Finder、文件资源管理器或系统文件管理器中显示。对于本地 HTML 附件,该菜单第一项会在右侧预览中打开文件。Office 预览在此处也有相同限制:`.doc` 和 `.ppt` 仍通过系统应用打开,DOCX 的分页效果可能与 Microsoft Word 不同,PPTX 的动画、切换效果和媒体播放不受支持。兼容应用发现仅在 macOS 和 Windows 上可用;在 Linux 上或发现失败时,会静默降级为仅显示文件位置。其它本地文件(包括超过 20 MB 的 Office 文件)会在用户点击后通过系统应用打开。用户选择的文件夹附件在发送后也会保持可用,点击后交给系统文件管理器打开;ClawX 不会读取或预览其中内容。远程 HTTP 和 HTTPS 附件会在用户点击后从外部打开。没有规范媒体事实佐证的普通文本裸路径或行内路径不会被当作附件。
|
||||
|
||||
ACP Chat 也可在 runtime 以可信结构化媒体投递图像生成结果时显示生成图片预览。对于可信的 OpenClaw internal-UI 投递和与生图任务关联的最终回复,ClawX 会保留原始的用户可见完成文案,包括只有文本的失败说明,而不会统一替换成通用图片文案。历史 OpenClaw 回放中,assistant 的图片 `MEDIA:` 标记只有在同一会话已记录图像生成任务启动后才会进入内联图片体验。ClawX 通过 Electron Main 的主机媒体处理加载预览,而不是让 Renderer 任意访问文件系统。标准 ACP 图片和 resource 内容仍是首选路径,并会直接渲染。
|
||||
|
||||
### ACP 文件活动语义
|
||||
|
||||
- 文件活动由成功且已完成的 OpenClaw `write`、`edit` 和 `apply_patch` 调用投影而来。工具识别方式与 OpenClaw 官方 Chat UI 保持一致;仅接收已完成调用的筛选规则是 ClawX 特有的。
|
||||
- 已创建和已修改的活动行与可预览的 assistant 附件共用同一种文件卡片外壳和**打开方式**菜单,同时保留状态文字及可用的 `+/-` 统计。对于 HTML 文件,菜单第一项会在右侧**预览**中打开文件;已删除的活动行只保留 **Changes** 操作。应用列表、指定应用打开和显示文件位置都会由 Electron Main 根据 workspace 根目录与相对路径分别重新验证;工具路径不会因此变成附件,Renderer 也不会获得规范化系统路径。
|
||||
- `write` 按工具声明的语义显示:视为创建,并展示为全部新增的差异,即使该路径可能已经存在。
|
||||
- **Changes** 是按时间顺序记录工具声明活动的会话级记录,不是 Git 输出,也不是相对于已验证源码基线的差异。
|
||||
- 对每个文件,Changes 在每轮助手回复中最多展示一个 diff 编辑器。可安全串联的片段会合并,独立片段会拼接到同一个编辑器中,但不会被描述为基于完整文件基线的差异。
|
||||
- Shell 命令、脚本、用户或 IDE 产生的副作用不会被检测。
|
||||
- 完整的 ACP 回放可以恢复已记录的文件活动;如果回放不完整,ClawX 不会通过回退推断来补造缺失活动。
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────────────────────┐
|
||||
│ ClawX 桌面应用 │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Electron 主进程 │ │
|
||||
│ │ • 窗口与应用生命周期管理 │ │
|
||||
│ │ • 网关进程监控 │ │
|
||||
│ │ • 系统集成(托盘、通知、密钥链) │ │
|
||||
│ │ • 自动更新编排 │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ │ IPC (权威控制面) │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ React 渲染进程 │ │
|
||||
│ │ • 现代组件化 UI(React 19) │ │
|
||||
│ │ • Zustand 状态管理 │ │
|
||||
│ │ • 统一 host-api/api-client 调用 │ │
|
||||
│ │ • 回复使用 Markdown,用户输入按原文显示 │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
│ 类型化 IPC 请求
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 主进程 Host Services 与 Gateway Manager │
|
||||
│ │
|
||||
│ • host:invoke 类型化服务分发 │
|
||||
│ • 设置、文件、会话、技能、供应商、诊断服务 │
|
||||
│ • 主进程持有 Gateway WebSocket 并负责进程监控 │
|
||||
└──────────────────────────────┬──────────────────────────────────┘
|
||||
│
|
||||
│ 主进程持有 WebSocket
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ OpenClaw 网关 │
|
||||
│ │
|
||||
│ • AI 智能体运行时与编排 │
|
||||
│ • 消息频道管理 │
|
||||
│ • 技能/插件执行环境 │
|
||||
│ • 供应商抽象层 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
### 设计原则
|
||||
|
||||
- **进程隔离**:AI 运行时在独立进程中运行,确保即使在高负载计算期间 UI 也能保持响应
|
||||
- **前端调用单一入口**:渲染层统一走 host-api/api-client,不感知底层协议细节
|
||||
- **主进程掌控传输策略**:ACP Chat stdio bridge 与 Gateway 传输都由 Electron Main 持有,渲染进程通过类型化 IPC 调用 Main
|
||||
- **扩展 IPC 贡献点**:主进程扩展通过类型化 IPC 注册表贡献 host-api action,而不是挂载 HTTP route
|
||||
- **优雅恢复**:内置重连、超时、退避逻辑,自动处理瞬时故障
|
||||
- **安全存储**:API 密钥和敏感数据利用操作系统原生的安全存储机制
|
||||
- **CORS 安全**:渲染进程不直接请求本地 Gateway 或 Host API HTTP 端点
|
||||
|
||||
### 进程模型与 Gateway 排障
|
||||
|
||||
- ClawX 基于 Electron,**单个应用实例出现多个系统进程是正常现象**(main/renderer/zygote/utility)。
|
||||
- 单实例保护同时使用 Electron 自带锁与本地进程文件锁回退机制,可在桌面会话总线异常时避免重复启动。
|
||||
- 滚动升级期间若新旧版本混跑,单实例保护仍可能出现不对称行为。为保证稳定性,建议桌面客户端尽量统一升级到同一版本。
|
||||
- 但 OpenClaw Gateway 监听应始终保持**单实例**:`127.0.0.1:18789` 只能有一个监听者。
|
||||
- Gateway readiness 以 OpenClaw 的 `system-presence`、`health`、`status` 等核心信号为准;memory 或频道失败会显示为能力降级,而不是全局 Gateway 故障。
|
||||
- 可用以下命令确认监听进程:
|
||||
- macOS/Linux:`lsof -nP -iTCP:18789 -sTCP:LISTEN`
|
||||
- Windows(PowerShell):`Get-NetTCPConnection -LocalPort 18789 -State Listen`
|
||||
- 点击窗口关闭按钮(`X`)默认只是最小化到托盘,并不会完全退出应用。请在托盘菜单中选择 **Quit ClawX** 执行完整退出。
|
||||
@@ -1,107 +0,0 @@
|
||||
# ClawX 开发文档
|
||||
|
||||
本文档是 README「开发指南」一节的详细说明。
|
||||
|
||||
### 前置要求
|
||||
|
||||
- **Node.js**:对应主版本范围内的 22.22.3+、24.15.0+ 或 25.9.0+(推荐 Node 24 LTS)
|
||||
- **包管理器**:pnpm 9+(推荐)或 npm
|
||||
- **Linux(Ubuntu/Debian)**:运行 Electron 前,请先安装所需系统库:
|
||||
```bash
|
||||
sudo apt-get install -y libnss3 libgtk-3-0 libxss1 libxtst6 libatspi2.0-0 libnotify4 xdg-utils
|
||||
```
|
||||
在 Ubuntu 24.04+ 上,部分软件包使用 `t64` 后缀,运行上述命令后 `apt` 会自动选择正确版本。
|
||||
|
||||
### 项目结构
|
||||
|
||||
```ClawX/
|
||||
├── electron/ # Electron 主进程
|
||||
│ ├── services/ # 类型化 Host API、Provider、Secrets 与运行时服务
|
||||
│ │ ├── providers/ # Provider/account 模型同步逻辑
|
||||
│ │ └── secrets/ # 系统钥匙串与密钥存储
|
||||
│ ├── shared/ # 共享 Provider schema/常量
|
||||
│ │ └── providers/
|
||||
│ ├── main/ # 应用入口、窗口、IPC 注册
|
||||
│ ├── gateway/ # OpenClaw 网关进程管理
|
||||
│ ├── preload/ # 安全 IPC 桥接
|
||||
│ └── utils/ # 工具模块(存储、认证、路径)
|
||||
├── src/ # React 渲染进程
|
||||
│ ├── lib/ # 前端统一 API 与错误模型
|
||||
│ ├── stores/ # Zustand 状态仓库(settings/chat/gateway)
|
||||
│ ├── components/ # 可复用 UI 组件
|
||||
│ ├── pages/ # Setup/Dashboard/Chat/Channels/Skills/Cron/Settings
|
||||
│ ├── i18n/ # 国际化资源
|
||||
│ └── types/ # TypeScript 类型定义
|
||||
├── tests/
|
||||
│ ├── e2e/ # Playwright Electron 端到端冒烟测试
|
||||
│ └── unit/ # Vitest 单元/集成型测试
|
||||
├── resources/ # 静态资源(图标、图片)
|
||||
└── scripts/ # 构建与工具脚本
|
||||
```
|
||||
### 常用命令
|
||||
|
||||
```bash
|
||||
# 开发
|
||||
pnpm run init # 安装依赖并下载捆绑二进制(uv、agent-browser)
|
||||
pnpm dev # 以热重载模式启动(若缺失会自动准备预装技能包)
|
||||
|
||||
# 代码质量
|
||||
pnpm lint # 运行 ESLint 检查
|
||||
pnpm typecheck # TypeScript 类型检查
|
||||
|
||||
# 测试
|
||||
pnpm test # 运行单元测试
|
||||
pnpm run test:e2e # 运行 Electron E2E 冒烟测试
|
||||
pnpm run test:e2e:headed # 以可见窗口运行 Electron E2E 测试
|
||||
pnpm run perf:chat # 采集合成 Chat 场景的 Renderer/Main CPU Profile
|
||||
pnpm run profile:main # 启动构建产物并在 9229 端口调试 Main
|
||||
pnpm run comms:replay # 计算通信回放指标
|
||||
pnpm run comms:baseline # 刷新通信基线快照
|
||||
pnpm run comms:compare # 将回放指标与基线阈值对比
|
||||
|
||||
# 构建与打包
|
||||
pnpm run build:vite # 仅构建前端
|
||||
pnpm build # 完整生产构建(含打包资源)
|
||||
pnpm package # 为当前平台打包(包含预装技能资源)
|
||||
pnpm package:mac # 为 macOS 打包
|
||||
pnpm package:win # 为 Windows 打包
|
||||
pnpm package:linux # 为 Linux 打包
|
||||
```
|
||||
|
||||
在无头 Linux 环境下,Electron 测试需要显示服务;可使用 `xvfb-run -a pnpm run test:e2e`。
|
||||
|
||||
Electron E2E 功能测试在本地和 CI 中默认使用两个 Playwright worker;可通过 `CLAWX_E2E_WORKERS=<正整数>` 按机器能力调整普通并行通道。访问操作系统全局状态的测试进入单 worker 的 `exclusive` project,主机性能采样则在功能测试结束后独占运行。新增 E2E 测试默认并行;若测试使用真实剪贴板或其他机器级共享资源,请应用 `tests/e2e/parallel-policy.ts` 中的 `E2E_EXCLUSIVE_TAG`。
|
||||
|
||||
如果只需运行一个不依赖独占前置阶段的普通 spec,可使用 `pnpm exec playwright test <spec> --project=parallel --no-deps`。
|
||||
|
||||
### Electron 性能诊断
|
||||
|
||||
`pnpm run perf:chat` 会运行隔离的合成 ACP 负载,分别覆盖流式响应,以及富 Markdown 静态会话中的侧栏和滚动交互,并在 Playwright 的 `test-results/` 目录输出版本化指标与 Renderer/Main CPU Profile。Renderer Profile 覆盖生产 store/render 路径和帧节奏;流式 Main Profile 测量 Main 到 Renderer 的 IPC fanout,交互 Main Profile 用于确认 Renderer 交互期间 Main 是否保持空闲。两者都不包含上游 OpenClaw/ACP 子进程或 GPU 进程路径。CPU Profile 可直接用 Chrome DevTools 打开;其中只包含生成的测试文本,不会上报为产品遥测。性能数据依赖硬件,应在同一机器上多次运行后对比,不应使用统一的跨平台绝对阈值。
|
||||
|
||||
录制真实 Renderer 时,使用 `CLAWX_REMOTE_DEBUGGING_PORT=9223 pnpm dev` 启动开发环境,再让 Playwright 或 Chrome DevTools 连接 `localhost:9223`。录制真实 Electron Main 时,运行 `pnpm run profile:main`,在 `chrome://inspect` 中配置 `localhost:9229` 并选择 Electron Main target。除非正在测量 WebSocket trace 本身,否则不要设置 `CLAWX_GATEWAY_WS_TRACE`。
|
||||
|
||||
ClawX 默认保留 Chromium 硬件加速,使长文档、滚动和布局动画能够使用 GPU 合成与光栅化。若某台机器的显卡驱动存在问题,仍可使用 Chromium 原生的 `--disable-gpu` 命令行参数作为排障回退。
|
||||
|
||||
### 通信回归检查
|
||||
|
||||
当 PR 涉及通信链路(Gateway 事件、ACP Chat bridge 收发流程、Channel 投递、传输回退)时,建议执行:
|
||||
|
||||
```bash
|
||||
pnpm run comms:replay
|
||||
pnpm run comms:compare
|
||||
```
|
||||
|
||||
CI 中的 `comms-regression` 会校验必选场景与阈值。
|
||||
|
||||
### 技术栈
|
||||
|
||||
| 层级 | 技术 |
|
||||
|------|------|
|
||||
| 运行时 | Electron 40+ |
|
||||
| UI 框架 | React 19 + TypeScript |
|
||||
| 样式 | Tailwind CSS + shadcn/ui |
|
||||
| 状态管理 | Zustand |
|
||||
| 构建工具 | Vite + electron-builder |
|
||||
| 测试 | Vitest + Playwright |
|
||||
| 动画 | Framer Motion |
|
||||
| 图标 | Lucide React |
|
||||
@@ -1,54 +0,0 @@
|
||||
# ClawX 功能特性
|
||||
|
||||
本文档是 README「功能特性」一节的详细说明。
|
||||
|
||||
### 🎯 零配置门槛
|
||||
从安装到第一次 AI 对话,全程通过直观的图形界面完成。无需终端命令,无需 YAML 文件,无需到处寻找环境变量。
|
||||
|
||||
### 💬 智能聊天界面
|
||||
通过现代化的聊天体验与 AI 智能体交互。支持多会话上下文、消息历史记录,并以流式 Markdown 渲染智能体回复,支持带语法高亮的围栏代码块、面向中日韩文本的解析、GitHub 风格表格,以及由 KaTeX 渲染的 LaTeX 数学公式(`$行内$`、`$$块级$$`、`\(行内\)` 和 `\[块级\]`);用户输入则始终按原始文本显示。同时支持在多 Agent 场景下通过主输入框中的 `@agent` 直接路由到目标智能体。围栏代码会保留源码换行、自动软换行,并在流式输出结束后提供本地化的复制操作。
|
||||
从输入框插入的技能会以 `/技能名` 卡片形式显示;点击卡片可在右侧预览栏打开并阅读该技能的 `SKILL.md`。
|
||||
当你使用 `@agent` 选择其他智能体时,ClawX 会直接切换到该智能体自己的对话上下文,而不是经过默认智能体转发。各 Agent 工作区默认彼此分离,但更强的运行时隔离仍取决于 OpenClaw 的 sandbox 配置。
|
||||
会话侧边栏现在以工作空间优先组织:默认工作空间固定在最上方,其它工作空间按自然顺序排列,每个工作空间都可折叠或继续加载更多会话。AI 回复期间,会话行显示加载指示器;未查看的回复完成后显示蓝点;打开会话后恢复显示相对活跃时间,悬停时仍会露出操作按钮。导入的工作空间可从侧边栏标题处重命名,新名称会同步显示在对话输入框下方,同时悬浮标题仍可查看文件系统路径。如果当前所选会话存在有效工作空间,新对话会继承该工作空间,并在首次发送前保持可编辑。对于可编辑的新对话或未绑定对话,输入框的工作空间卡片会打开一个小菜单,列出最近使用及现有会话中的工作空间,并可切回默认工作空间或选择其它目录。如果保存的工作空间文件夹已被移动或删除,Chat 会暂停创建会话并提示选择现有文件夹,而不会持续重试失效路径。不可用的非默认工作空间会在侧边栏显示标记,并可在确认后删除;该操作会永久删除分组中的全部会话。只有永久删除成功后,会话行才会移除且页面才会跳转;删除失败时会保留会话与确认框,方便重试。OpenClaw 生成的 UUID 加日期兜底标题只有在与该会话 ID 匹配时才会被视为缺失标题,随后改用会话的首条用户消息展示,而不会被持久化为会话名称。
|
||||
每个 Agent 还可以单独覆盖自己的 `provider/model` 运行时设置;未覆盖的 Agent 会继续继承全局默认模型。
|
||||
|
||||
Chat 右侧面板的工作空间和预览选项卡支持以只读方式预览 Markdown、`.docx` 和 `.pptx` 文件。Markdown 文件预览以静态渲染模式提供相同的围栏代码语法高亮、软换行与复制操作、面向中日韩文本的解析和 KaTeX 数学公式支持。预览栏顶部可将当前文件展开至 ClawX 的整个可视区域;再次点击该按钮或按 Esc 即可返回侧栏。旧版 `.doc` 和 `.ppt` 文件不会在应用内预览,而是继续通过操作系统打开。DOCX 的分页效果可能与 Microsoft Word 不同;PPTX 预览不支持动画、切换效果或媒体播放。超过 20 MB 的 Office 文件不会在应用内预览。
|
||||
|
||||
### 本地 HTML 预览
|
||||
Chat 右侧面板只包含工作空间、预览和变更,不再提供通用网页浏览器、主页或地址栏。已授权的本地 `.html` 和 `.htm` 附件、文件活动及工作空间文件默认在预览中打开。文件操作可以选择 ClawX 内置预览或系统应用,预览标题栏也可将当前 HTML 文件交给系统浏览器打开。
|
||||
|
||||
所有链接都不可点击。ClawX 渲染的链接显示为普通文本,HTML 预览中的链接也会移除链接样式和指针交互。HTML 预览同时阻止表单、脚本跳转、重定向、页内跳转、弹窗、下载、网络请求和设备权限;它可以显示自包含的本地 HTML,但无法离开当前选中的文档。
|
||||
|
||||
### 📡 多频道管理
|
||||
同时配置和监控多个 AI 频道。每个频道独立运行,允许你为不同任务运行专门的智能体。
|
||||
现在每个频道支持多个账号,并可在 Channels 页面直接完成账号绑定到 Agent 与默认账号切换。
|
||||
对于自定义频道账号 ID,ClawX 现在会强制校验 OpenClaw 兼容的规范格式(`[a-z0-9_-]`、小写、最长 64 位、且必须以字母或数字开头),避免路由匹配异常。
|
||||
ClawX 现在还内置了腾讯官方个人微信渠道插件,可直接在 Channels 页面通过内置二维码流程完成微信连接。
|
||||
|
||||
### ⏰ 定时任务自动化
|
||||
调度 AI 任务自动执行。定义触发器、设置时间间隔,让 AI 智能体 7×24 小时不间断工作。
|
||||
现在定时任务页面已经可以直接配置外部投递,统一拆成“发送账号”和“接收目标”两个下拉选择。对于已支持的通道,接收目标会从通道目录能力或已知会话历史中自动发现,不需要再手动修改 `jobs.json`。任务的消息输入框也支持像主对话框那样以内联 `/skill` 令牌的方式插入技能(按所选智能体范围加载),让定时提示词可以直接触发技能。调度选择器现在分为**周期**和**单次**两个选项卡:周期支持每小时、每天、工作日、每周、自定义(原始 cron)等频率,并内置时间/星期选择;单次则在所选日期(显示星期)和时间执行一次。单次任务必须设置为未来时间,并会在执行完成后由运行时自动清除。
|
||||
|
||||
|
||||
### 🧩 可扩展技能系统
|
||||
通过预构建的技能扩展 AI 智能体的能力。集成的 Skills 页面采用“本地优先”方式:会扫描托管目录与 workspace 技能目录,并且无需依赖 Gateway 即可启用或停用技能;在企业扩展接管时,也可以显示扩展提供的 marketplace。
|
||||
ClawX 还会内置预装完整的文档处理技能(`pdf`、`xlsx`、`docx`、`pptx`),在启动时自动部署到托管技能目录(默认 `~/.openclaw/skills`),并在首次安装时默认启用。
|
||||
Skills 页面可展示来自多个 OpenClaw 来源的技能(托管目录、workspace、额外技能目录),并显示每个技能的实际路径,便于直接打开真实安装位置。对于 OpenClaw 自带的 bundled skills,社区版现在在打包产物里只保留并展示 `skill-creator`;开发模式和打包版启动时都会直接清理其它 bundled skill,同时把这些已删除 bundled skill 在 `openclaw.json` 中残留的旧配置一并移除。
|
||||
|
||||
### 🔐 安全的供应商集成
|
||||
连接多个 AI 供应商(OpenAI、Anthropic、Z.AI / GLM 等),凭证安全存储在系统原生密钥链中。OpenAI 同时支持 API Key 与浏览器 OAuth(Codex 订阅)登录。
|
||||
在开发者模式下,独立的“图像生成”页面支持配置 OpenAI 兼容生图端点(Base URL、API Key 和模型名,例如 `gpt-image-2`),生图请求会走专用的 `/v1/images/generations` 服务,聊天仍继续使用正常的 OpenAI Provider。
|
||||
如果你通过 **自定义(Custom)Provider** 对接 OpenAI-compatible 网关,可以在 **设置 → AI Providers → 编辑 Provider** 中配置自定义 `User-Agent`,以提高兼容性。
|
||||
编辑或切换 Provider 时,ClawX 会保留已有的模型级能力元数据,例如 `input: ["text", "image"]`。新选择的自定义 Provider 模型会使用与 OpenClaw onboarding 一致的图片输入能力推断;未知模型默认按纯文本模型处理。
|
||||
自定义 Provider 的模型行还会写入显式的 `contextWindow`(按模型系列推断,例如 `gpt-5.x` → 272k),旧版本保存的模型行会在启动时自动回填,使 OpenClaw 能在长会话超限前主动压缩上下文,避免出现 "Context overflow" 报错。当你没有配置 compaction 时,ClawX 会默认写入 `agents.defaults.compaction.mode = "safeguard"` 和 `reserveTokensFloor = 50000`;你手动配置过的模型行或压缩配置永远不会被修改(仅可能回填缺失的 `reserveTokensFloor`)。
|
||||
Z.AI(国内站 / 国际站)会映射到 OpenClaw 内置的 `zai` 供应商(`ZAI_API_KEY`),默认模型为 `glm-5.2`。可通过 Code Plan 预设切换到编码套餐端点(`…/api/coding/paas/v4`),或使用普通 API 端点(`…/api/paas/v4`);国内站与国际站互斥,因为它们共享同一个 OpenClaw 运行时 key。
|
||||
如果兼容网关的 `/models` 因非鉴权原因不可用,ClawX 会在校验 API Key 时使用已配置的模型,自动降级为轻量的 `/chat/completions` 或 `/responses` 探测。
|
||||
|
||||
### 🌙 自适应主题
|
||||
支持浅色模式、深色模式或跟随系统主题。ClawX 自动适应你的偏好设置。
|
||||
|
||||
### 🚀 开机启动控制
|
||||
在 **设置 → 通用** 中,你可以开启 **开机自动启动**,让 ClawX 在系统登录后自动启动。
|
||||
|
||||
### 🔔 更新提示
|
||||
ClawX 可以在启动时自动检查新版本。发现更新后会显示应用内提示;只有在你选择操作后,才会下载或安装更新。
|
||||
@@ -1,12 +0,0 @@
|
||||
# ClawX 代理设置
|
||||
|
||||
本文档是 README「代理设置」一节的详细说明。
|
||||
|
||||
- 只填写 `host:port` 时,会按 HTTP 代理处理。
|
||||
- 高级代理项留空时,会自动回退到“代理服务器”。
|
||||
- 保存代理设置后,Electron 网络层会立即重新应用代理,并自动重启 Gateway。
|
||||
- 如果启用了 Telegram,ClawX 还会把代理同步到 OpenClaw 的 Telegram 频道配置中。
|
||||
- 当 ClawX 代理处于关闭状态时,Gateway 的常规重启会保留已有的 Telegram 频道代理配置。
|
||||
- 如果你要明确清空 OpenClaw 中的 Telegram 代理,请在关闭代理后点一次“保存代理设置”。
|
||||
- 在 **设置 → 高级 → 开发者** 中,可以直接运行 **OpenClaw Doctor**,执行 `openclaw doctor --json` 并在应用内查看诊断输出。
|
||||
- 在 Windows 打包版本中,内置的 `openclaw` CLI/TUI 会通过随包分发的 `node.exe` 入口运行,以保证终端输入行为稳定。
|
||||
@@ -70,20 +70,6 @@ function isBaseHashConflict(error: unknown): boolean {
|
||||
return /config changed since last load; re-run config\.get and retry/i.test(message);
|
||||
}
|
||||
|
||||
function isConfigSetResponseLost(error: unknown): boolean {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return message.includes('RPC timeout: config.set')
|
||||
|| message.includes('Gateway stopped')
|
||||
|| message.includes('Gateway not connected')
|
||||
|| message.includes('Gateway service restart')
|
||||
|| message.includes('Failed to send RPC request:');
|
||||
}
|
||||
|
||||
async function acceptPersistedConfigSetCommitIfMatched(config: OpenClawConfig): Promise<boolean> {
|
||||
const persisted = await readFileConfig(resolveOpenClawConfigPath());
|
||||
return isDeepStrictEqual(persisted.config, config);
|
||||
}
|
||||
|
||||
async function mutateRunningConfig(
|
||||
manager: ConfigDeliveryGatewayManager,
|
||||
mutator: OpenClawConfigMutator,
|
||||
@@ -106,14 +92,6 @@ async function mutateRunningConfig(
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (attempt === 0 && isBaseHashConflict(error)) continue;
|
||||
|
||||
// config.set may durably replace the file and then close the socket with
|
||||
// code 1012 before its RPC response reaches ClawX. Reconnect can restore
|
||||
// running state before the RPC timeout fires, so verify the persisted
|
||||
// snapshot whenever the response was lost instead of only while stopped.
|
||||
if (manager.getStatus().state !== 'running' || isConfigSetResponseLost(error)) {
|
||||
if (await acceptPersistedConfigSetCommitIfMatched(config)) return true;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,10 +36,7 @@ import { prependPathEntry } from '../utils/env-path';
|
||||
import { copyPluginFromNodeModules, fixupPluginManifest, cpSyncSafe, buildCandidateSources, repairTrustedOfficialPluginInstallRecords, removeTrustedOfficialPluginInstallRecord, resolvePluginNpmPackagePath } from '../utils/plugin-install';
|
||||
import { safeRmSync } from '../utils/safe-fs';
|
||||
import { CLAWX_OPENAI_IMAGE_PROVIDER_KEY } from '../utils/openclaw-image-relay-constants';
|
||||
import {
|
||||
ensureOpenClaw2026_7_1UpgradeSnapshot,
|
||||
quarantineLegacyUpdateCheckState,
|
||||
} from '../utils/openclaw-upgrade-snapshot';
|
||||
import { ensureOpenClaw2026_7_1UpgradeSnapshot } from '../utils/openclaw-upgrade-snapshot';
|
||||
import { stripSystemdSupervisorEnv } from './config-sync-env';
|
||||
import { cleanupAgentsSymlinkedSkills, cleanupStalePluginRuntimeDeps } from './skills-symlink-cleanup';
|
||||
import {
|
||||
@@ -654,19 +651,6 @@ export async function prepareGatewayLaunchContext(port: number): Promise<Gateway
|
||||
}
|
||||
});
|
||||
|
||||
await measureAsync(timingsMs, 'legacyUpdateCheckCleanupMs', async () => {
|
||||
try {
|
||||
const cleanup = await quarantineLegacyUpdateCheckState();
|
||||
if (cleanup.status === 'quarantined') {
|
||||
logger.info(
|
||||
`[upgrade] Quarantined conflicting legacy update-check state: ${cleanup.sourcePath} → ${cleanup.backupPath}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('[upgrade] Failed to quarantine legacy update-check state:', error);
|
||||
}
|
||||
});
|
||||
|
||||
const appSettings = await measureAsync(timingsMs, 'settingsMs', getAllSettings);
|
||||
const prelaunchSummary = await measureAsync(timingsMs, 'prelaunchSyncMs', async () => (
|
||||
await syncGatewayConfigBeforeLaunch(appSettings, openclawDir)
|
||||
|
||||
@@ -196,7 +196,7 @@ export class GatewayManager extends EventEmitter {
|
||||
private reconnectSuccessTotal = 0;
|
||||
private static readonly HEARTBEAT_INTERVAL_MS = 60_000;
|
||||
private static readonly HEARTBEAT_TIMEOUT_MS = 30_000;
|
||||
private static readonly HEARTBEAT_MAX_MISSES = 4;
|
||||
private static readonly HEARTBEAT_MAX_MISSES = 10;
|
||||
public static readonly RESTART_COOLDOWN_MS = 5_000;
|
||||
private static readonly GATEWAY_READY_FALLBACK_PROBE_DELAYS_MS = [1_500, 3_000, 5_000, 8_000, 12_000, 30_000] as const;
|
||||
private lastRestartAt = 0;
|
||||
@@ -411,28 +411,12 @@ export class GatewayManager extends EventEmitter {
|
||||
tSpawned = Date.now();
|
||||
},
|
||||
waitForReady: async (port) => {
|
||||
const recoveringOwnedProcess = tSpawned === 0
|
||||
&& this.process?.pid != null
|
||||
&& this.ownsProcess;
|
||||
await waitForGatewayReady({
|
||||
port,
|
||||
getProcessExitCode: () => this.processExitCode,
|
||||
// A code-1012 in-process reload normally returns within seconds.
|
||||
// Do not hold the lifecycle lock for the general 2400-attempt cold
|
||||
// startup budget when the owned process is alive but no longer serves WS.
|
||||
...(recoveringOwnedProcess ? { retries: 50 } : {}),
|
||||
});
|
||||
tReady = Date.now();
|
||||
},
|
||||
terminateStaleOwnedProcess: async () => {
|
||||
const shouldReconnect = this.shouldReconnect;
|
||||
this.shouldReconnect = false;
|
||||
try {
|
||||
await this.forceTerminateOwnedProcessForQuit();
|
||||
} finally {
|
||||
this.shouldReconnect = shouldReconnect;
|
||||
}
|
||||
},
|
||||
onConnectedToManagedGateway: () => {
|
||||
this.startHealthCheck();
|
||||
const tConnected = Date.now();
|
||||
@@ -751,10 +735,6 @@ export class GatewayManager extends EventEmitter {
|
||||
logger.info('Gateway ready fallback RPC router probe succeeded');
|
||||
this.resetGatewayReadyFallback();
|
||||
this.setStatus({ gatewayReady: true });
|
||||
// A fast Gateway can emit gateway.ready before the WebSocket client is
|
||||
// attached. A successful router probe is equivalent readiness, so it
|
||||
// must also complete the one-time migration snapshot lifecycle.
|
||||
void this.cleanupOpenClawUpgradeSnapshot();
|
||||
}
|
||||
} catch (error) {
|
||||
this.capabilityMonitor.recordCoreProbe({
|
||||
@@ -1074,11 +1054,6 @@ export class GatewayManager extends EventEmitter {
|
||||
this.connectionMonitor.clear();
|
||||
this.recordSocketClose(closeCode);
|
||||
this.diagnostics.consecutiveHeartbeatMisses = 0;
|
||||
if (closeCode === 1012) {
|
||||
for (const id of [...this.pendingRequests.keys()]) {
|
||||
rejectPendingGatewayRequest(this.pendingRequests, id, new Error('Gateway service restart'));
|
||||
}
|
||||
}
|
||||
if (this.status.state === 'running') {
|
||||
this.setStatus({ state: 'stopped' });
|
||||
// On Windows, skip reconnect from WS close. The Gateway is a local
|
||||
|
||||
@@ -23,7 +23,6 @@ type StartupHooks = {
|
||||
waitForPortFree: (port: number) => Promise<void>;
|
||||
startProcess: () => Promise<void>;
|
||||
waitForReady: (port: number) => Promise<void>;
|
||||
terminateStaleOwnedProcess: () => Promise<void>;
|
||||
onConnectedToManagedGateway: () => void;
|
||||
runDoctorRepair: () => Promise<boolean>;
|
||||
onDoctorRepairSuccess: () => void;
|
||||
@@ -76,13 +75,7 @@ export async function runGatewayStartupSequence(hooks: StartupHooks): Promise<vo
|
||||
// become ready and reconnect to it.
|
||||
if (hooks.hasOwnedProcess()) {
|
||||
logger.info('Owned Gateway process still alive (likely in-process restart); waiting for it to become ready');
|
||||
try {
|
||||
await hooks.waitForReady(hooks.port);
|
||||
} catch (error) {
|
||||
logger.warn('Owned Gateway process did not recover after an in-process restart; terminating the stale process');
|
||||
await hooks.terminateStaleOwnedProcess();
|
||||
throw new Error('Gateway process exited before becoming ready after an in-process restart', { cause: error });
|
||||
}
|
||||
await hooks.waitForReady(hooks.port);
|
||||
hooks.assertLifecycle('start/wait-ready-owned');
|
||||
await connectWithStartupRetry(hooks, hooks.port);
|
||||
hooks.assertLifecycle('start/connect-owned');
|
||||
|
||||
@@ -16,6 +16,7 @@ import { HOST_EVENT_CHANNELS } from '@shared/host-events/contract';
|
||||
import type {
|
||||
AcpChatCancelPayload,
|
||||
AcpChatLoadPayload,
|
||||
AcpChatOperationErrorCode,
|
||||
AcpChatOperationResult,
|
||||
AcpChatPromptPayload,
|
||||
AcpChatRespondPermissionPayload,
|
||||
@@ -31,7 +32,6 @@ import { logger } from '../utils/logger';
|
||||
import { recordAcpTrace } from './acp-trace';
|
||||
import { AcpSessionAccessRegistry, type AcpSessionAccessContext } from './acp-session-access-registry';
|
||||
import { expandPath } from '../utils/paths';
|
||||
import { getSetting } from '../utils/store';
|
||||
|
||||
type AcpConnection = Pick<ClientSideConnection, 'initialize' | 'newSession' | 'loadSession' | 'prompt' | 'cancel'>;
|
||||
type MainWindowLike = {
|
||||
@@ -70,8 +70,12 @@ function ok(generation?: number, sessionUpdates?: AcpSessionUpdateEnvelope[]): A
|
||||
};
|
||||
}
|
||||
|
||||
function fail(error: unknown): AcpChatOperationResult {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
||||
function fail(error: unknown, errorCode?: AcpChatOperationErrorCode): AcpChatOperationResult {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
...(errorCode ? { errorCode } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function cancelledPermissionResponse(): RequestPermissionResponse {
|
||||
@@ -144,6 +148,7 @@ export class AcpChatService {
|
||||
private loadQueue: Promise<void> | null = null;
|
||||
private activeLoadBatch: AcpSessionLoadBatch | null = null;
|
||||
private readonly livePrompts = new Map<string, AcpLivePromptContext>();
|
||||
private readonly clientCancelledPromptSessions = new Set<string>();
|
||||
private permissionSeq = 0;
|
||||
private readonly permissionWaiters = new Map<string, PermissionWaiter>();
|
||||
readonly client: Client;
|
||||
@@ -372,6 +377,7 @@ export class AcpChatService {
|
||||
generation,
|
||||
accessGrant,
|
||||
};
|
||||
this.clientCancelledPromptSessions.delete(payload.sessionKey);
|
||||
this.livePrompts.set(payload.sessionKey, promptContext);
|
||||
try {
|
||||
const promptCwd = payload.cwd === accessGrant.executionCwd
|
||||
@@ -396,19 +402,35 @@ export class AcpChatService {
|
||||
this.permissionsEnabled = true;
|
||||
const messageId = payload.messageId ?? randomUUID();
|
||||
const isSlashCommand = payload.message?.trimStart().startsWith('/') === true;
|
||||
await connection.prompt({
|
||||
const response = await connection.prompt({
|
||||
sessionId: acpSessionId,
|
||||
prompt,
|
||||
// ACP 1.1 removed messageId from the PromptRequest wire shape. Keep
|
||||
// ClawX correlation metadata in the protocol extension envelope.
|
||||
// OpenClaw must receive slash commands without its textual cwd prefix
|
||||
// so the Gateway can classify and fold command replies into chat final.
|
||||
_meta: { sessionKey: payload.sessionKey, prefixCwd: !isSlashCommand, messageId },
|
||||
_meta: {
|
||||
sessionKey: payload.sessionKey,
|
||||
prefixCwd: !isSlashCommand,
|
||||
messageId,
|
||||
...(payload.thinkingLevel ? { thinkingLevel: payload.thinkingLevel } : {}),
|
||||
},
|
||||
});
|
||||
const cancelledByClient = this.clientCancelledPromptSessions.has(payload.sessionKey);
|
||||
if (response.stopReason === 'cancelled' && !cancelledByClient) {
|
||||
const error = 'ACP prompt was aborted before producing a response';
|
||||
logger.warn(`[acp-chat] ${error}`);
|
||||
this.trace('session/prompt:failed', {
|
||||
sessionKey: payload.sessionKey,
|
||||
generation,
|
||||
details: { error, stopReason: response.stopReason },
|
||||
});
|
||||
return fail(error, 'prompt_aborted');
|
||||
}
|
||||
this.trace('session/prompt:success', {
|
||||
sessionKey: payload.sessionKey,
|
||||
generation,
|
||||
details: { blockCount: prompt.length, acpSessionId },
|
||||
details: { blockCount: prompt.length, acpSessionId, stopReason: response.stopReason },
|
||||
});
|
||||
return ok(generation);
|
||||
} catch (error) {
|
||||
@@ -423,6 +445,7 @@ export class AcpChatService {
|
||||
this.livePrompts.delete(payload.sessionKey);
|
||||
this.resolvePermissionWaitersForSession(payload.sessionKey, cancelledPermissionResponse());
|
||||
}
|
||||
this.clientCancelledPromptSessions.delete(payload.sessionKey);
|
||||
this.permissionsEnabled = this.activeSessionKey != null && this.livePrompts.has(this.activeSessionKey);
|
||||
}
|
||||
}
|
||||
@@ -431,6 +454,8 @@ export class AcpChatService {
|
||||
if (!isValidSessionKey(payload.sessionKey)) return fail('Invalid ACP cancel payload');
|
||||
if (payload.sessionKey !== this.activeSessionKey || !this.loadedAcpSessionId) return fail('ACP session is not loaded');
|
||||
|
||||
const hasLivePrompt = this.livePrompts.has(payload.sessionKey);
|
||||
if (hasLivePrompt) this.clientCancelledPromptSessions.add(payload.sessionKey);
|
||||
try {
|
||||
this.trace('session/cancel:start', { sessionKey: payload.sessionKey });
|
||||
const connection = await this.ensureConnection();
|
||||
@@ -440,6 +465,7 @@ export class AcpChatService {
|
||||
this.trace('session/cancel:success', { sessionKey: payload.sessionKey });
|
||||
return ok(this.generation);
|
||||
} catch (error) {
|
||||
if (hasLivePrompt) this.clientCancelledPromptSessions.delete(payload.sessionKey);
|
||||
logger.error(`[acp-chat] cancel failed: ${String(error)}`);
|
||||
this.trace('session/cancel:failed', {
|
||||
sessionKey: payload.sessionKey,
|
||||
@@ -495,7 +521,7 @@ export class AcpChatService {
|
||||
}
|
||||
|
||||
private async initializeConnectionOnce(attempt: number): Promise<AcpConnection> {
|
||||
if (!this.connection) this.connection = await this.spawnConnection();
|
||||
if (!this.connection) this.connection = this.spawnConnection();
|
||||
const connection = this.connection;
|
||||
const child = this.child;
|
||||
|
||||
@@ -550,15 +576,9 @@ export class AcpChatService {
|
||||
});
|
||||
}
|
||||
|
||||
private async spawnConnection(): Promise<ClientSideConnection> {
|
||||
const gatewayToken = await getSetting('gatewayToken');
|
||||
private spawnConnection(): ClientSideConnection {
|
||||
const spec = getOpenClawEmbeddedForkSpec(['acp']);
|
||||
const forked = fork(spec.modulePath, spec.args, {
|
||||
...spec.options,
|
||||
// ACP is a local Gateway client, so it must use the token that started
|
||||
// this ClawX-owned Gateway instead of relying on config-file fallback.
|
||||
env: { ...spec.options.env, OPENCLAW_GATEWAY_TOKEN: gatewayToken },
|
||||
});
|
||||
const forked = fork(spec.modulePath, spec.args, spec.options);
|
||||
if (!forked.stdin || !forked.stdout || !forked.stderr) {
|
||||
forked.kill();
|
||||
throw new Error('ACP process did not expose stdio pipes');
|
||||
@@ -600,6 +620,7 @@ export class AcpChatService {
|
||||
this.historicalGeneration = null;
|
||||
this.permissionsEnabled = false;
|
||||
this.livePrompts.clear();
|
||||
this.clientCancelledPromptSessions.clear();
|
||||
}
|
||||
|
||||
private emitSessionUpdate(notification: SessionNotification): void {
|
||||
|
||||
@@ -34,7 +34,6 @@ import {
|
||||
ensureWeChatPluginInstalled,
|
||||
ensureWeComPluginInstalled,
|
||||
ensureWhatsAppPluginInstalled,
|
||||
type PluginInstallResult,
|
||||
} from '../utils/plugin-install';
|
||||
import {
|
||||
computeChannelRuntimeStatus,
|
||||
@@ -966,43 +965,6 @@ function emitChannelEvent(
|
||||
}
|
||||
}
|
||||
|
||||
const CHANNEL_PLUGIN_INSTALLERS: Record<
|
||||
string,
|
||||
() => MaybePromise<PluginInstallResult>
|
||||
> = {
|
||||
dingtalk: ensureDingTalkPluginInstalled,
|
||||
wecom: ensureWeComPluginInstalled,
|
||||
discord: ensureDiscordPluginInstalled,
|
||||
qqbot: ensureQQBotPluginInstalled,
|
||||
whatsapp: ensureWhatsAppPluginInstalled,
|
||||
feishu: ensureFeishuPluginInstalled,
|
||||
[OPENCLAW_WECHAT_CHANNEL_TYPE]: ensureWeChatPluginInstalled,
|
||||
};
|
||||
|
||||
function isPluginBackedChannel(storedChannelType: string): boolean {
|
||||
return Object.hasOwn(CHANNEL_PLUGIN_INSTALLERS, storedChannelType);
|
||||
}
|
||||
|
||||
function shouldRestartRunningGateway(ctx: ChannelsApiContext, storedChannelType: string): boolean {
|
||||
return isPluginBackedChannel(storedChannelType)
|
||||
&& ctx.gatewayManager.getStatus().state === 'running';
|
||||
}
|
||||
|
||||
function scheduleGatewayRestartForPluginChannel(
|
||||
ctx: ChannelsApiContext,
|
||||
storedChannelType: string,
|
||||
reason: 'noChange' | 'peerLinkRepairFailed' = 'noChange',
|
||||
): void {
|
||||
logger.info(
|
||||
`[channels.saveConfig] scheduling Gateway restart to activate plugin channel=${storedChannelType} reason=${reason}`,
|
||||
);
|
||||
// The config and scoped binding are already committed. Let the host request
|
||||
// return while the guarded lifecycle path performs stop/start/readiness.
|
||||
// GatewayManager owns error logging, status propagation, and restart
|
||||
// coalescing, so the Channels page can show the normal connecting state.
|
||||
ctx.gatewayManager.debouncedRestart(0);
|
||||
}
|
||||
|
||||
async function awaitWeChatQrLogin(
|
||||
ctx: ChannelsApiContext,
|
||||
sessionKey: string,
|
||||
@@ -1029,12 +991,8 @@ async function awaitWeChatQrLogin(
|
||||
baseUrl: result.baseUrl,
|
||||
userId: result.userId,
|
||||
});
|
||||
const restartGateway = shouldRestartRunningGateway(ctx, OPENCLAW_WECHAT_CHANNEL_TYPE);
|
||||
await saveChannelConfig(UI_WECHAT_CHANNEL_TYPE, { enabled: true }, normalizedAccountId);
|
||||
await ensureScopedChannelBinding(UI_WECHAT_CHANNEL_TYPE, normalizedAccountId);
|
||||
if (restartGateway) {
|
||||
scheduleGatewayRestartForPluginChannel(ctx, OPENCLAW_WECHAT_CHANNEL_TYPE);
|
||||
}
|
||||
|
||||
if (activeQrLogins.get(loginKey) !== sessionKey) return;
|
||||
emitChannelEvent(ctx, UI_WECHAT_CHANNEL_TYPE, 'success', {
|
||||
@@ -1051,14 +1009,22 @@ async function awaitWeChatQrLogin(
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureChannelPluginInstalled(storedChannelType: string): Promise<{ peerLinkOk: boolean }> {
|
||||
const install = CHANNEL_PLUGIN_INSTALLERS[storedChannelType];
|
||||
if (!install) return { peerLinkOk: true };
|
||||
async function ensureChannelPluginInstalled(storedChannelType: string): Promise<void> {
|
||||
const installers: Record<string, () => MaybePromise<{ installed: boolean; warning?: string }>> = {
|
||||
dingtalk: ensureDingTalkPluginInstalled,
|
||||
wecom: ensureWeComPluginInstalled,
|
||||
discord: ensureDiscordPluginInstalled,
|
||||
qqbot: ensureQQBotPluginInstalled,
|
||||
whatsapp: ensureWhatsAppPluginInstalled,
|
||||
feishu: ensureFeishuPluginInstalled,
|
||||
[OPENCLAW_WECHAT_CHANNEL_TYPE]: ensureWeChatPluginInstalled,
|
||||
};
|
||||
const install = installers[storedChannelType];
|
||||
if (!install) return;
|
||||
const result = await install();
|
||||
if (!result.installed) {
|
||||
throw new Error(result.warning || `${toUiChannelType(storedChannelType)} plugin install failed`);
|
||||
}
|
||||
return { peerLinkOk: result.peerLinkOk !== false };
|
||||
}
|
||||
|
||||
export function createChannelsApi(ctx: ChannelsApiContext): CompleteHostServiceRegistry['channels'] {
|
||||
@@ -1135,31 +1101,15 @@ export function createChannelsApi(ctx: ChannelsApiContext): CompleteHostServiceR
|
||||
const accountId = optionalString(payload, 'accountId');
|
||||
await validateCanonicalAccountId(channelType, accountId, { allowLegacyConfiguredId: true });
|
||||
const storedChannelType = resolveStoredChannelType(channelType);
|
||||
const restartGateway = shouldRestartRunningGateway(ctx, storedChannelType);
|
||||
const [installResult, existingValues] = await Promise.all([
|
||||
ensureChannelPluginInstalled(storedChannelType),
|
||||
getChannelFormValues(channelType, accountId),
|
||||
]);
|
||||
await ensureChannelPluginInstalled(storedChannelType);
|
||||
const existingValues = await getChannelFormValues(channelType, accountId);
|
||||
if (isSameConfigValues(existingValues, config)) {
|
||||
await ensureScopedChannelBinding(channelType, accountId);
|
||||
if (restartGateway) {
|
||||
scheduleGatewayRestartForPluginChannel(ctx, storedChannelType, 'noChange');
|
||||
}
|
||||
return { success: true, noChange: true, ...(restartGateway ? { activationPending: true } : {}) };
|
||||
return { success: true, noChange: true };
|
||||
}
|
||||
await saveChannelConfig(channelType, config, accountId);
|
||||
await ensureScopedChannelBinding(channelType, accountId);
|
||||
if (restartGateway && !installResult.peerLinkOk) {
|
||||
scheduleGatewayRestartForPluginChannel(ctx, storedChannelType, 'peerLinkRepairFailed');
|
||||
return { success: true, activationPending: true };
|
||||
}
|
||||
// A changed running config is delivered through config.set, whose native
|
||||
// reload activates the plugin. Scheduling another full restart here races
|
||||
// that code-1012 reload and can trip OpenClaw's restart-loop breaker.
|
||||
// Keep the explicit restart above only for no-change retries, where no
|
||||
// config.set reload occurs but a newly copied plugin may still need discovery,
|
||||
// and when OpenClaw peer link repair failed after plugin install.
|
||||
return { success: true, ...(restartGateway ? { activationPending: true } : {}) };
|
||||
return { success: true };
|
||||
},
|
||||
setEnabled: async (payload) => {
|
||||
const channelType = requireString(payload, 'channelType');
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getProviderSecret } from '../secrets/secret-store';
|
||||
import type { ProviderConfig } from '../../utils/secure-storage';
|
||||
import { getAllProviders, getApiKey, getDefaultProvider, getProvider } from '../../utils/secure-storage';
|
||||
import { getProviderConfig, getProviderDefaultModel } from '../../utils/provider-registry';
|
||||
import { CUSTOM_PROVIDER_DEFAULT_REASONING_EFFORTS } from '../../shared/providers/types';
|
||||
import {
|
||||
ensureAnthropicMessagesModelMaxTokens,
|
||||
ensureOpenClawProviderAgentRuntimePins,
|
||||
@@ -307,6 +308,16 @@ async function syncRuntimeProviderConfig(
|
||||
});
|
||||
}
|
||||
|
||||
function customProviderModelEntry(_config: ProviderConfig, modelId: string) {
|
||||
return {
|
||||
...piAiModelsJsonModelEntry(modelId),
|
||||
reasoning: true,
|
||||
compat: {
|
||||
supportedReasoningEfforts: [...CUSTOM_PROVIDER_DEFAULT_REASONING_EFFORTS],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function syncCustomProviderAgentModel(
|
||||
config: ProviderConfig,
|
||||
runtimeProviderKey: string,
|
||||
@@ -325,7 +336,7 @@ async function syncCustomProviderAgentModel(
|
||||
await updateAgentModelProvider(runtimeProviderKey, {
|
||||
baseUrl: normalizeProviderBaseUrl(config, config.baseUrl, config.apiProtocol || 'openai-completions'),
|
||||
api: config.apiProtocol || 'openai-completions',
|
||||
models: modelId ? [piAiModelsJsonModelEntry(modelId)] : [],
|
||||
models: modelId ? [customProviderModelEntry(config, modelId)] : [],
|
||||
apiKey: resolvedKey,
|
||||
});
|
||||
}
|
||||
@@ -453,7 +464,9 @@ async function buildAgentModelProviderEntry(
|
||||
return {
|
||||
baseUrl,
|
||||
api,
|
||||
models: [piAiModelsJsonModelEntry(modelId)],
|
||||
models: [config.type === 'custom'
|
||||
? customProviderModelEntry(config, modelId)
|
||||
: piAiModelsJsonModelEntry(modelId)],
|
||||
apiKey,
|
||||
authHeader,
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
ProviderAccount,
|
||||
ProviderConfig,
|
||||
ProviderDefinition,
|
||||
ProviderReasoningEffort,
|
||||
ProviderType,
|
||||
} from '../../shared/providers/types';
|
||||
import { BUILTIN_PROVIDER_TYPES } from '../../shared/providers/types';
|
||||
@@ -105,6 +106,50 @@ function mergeSyncedProviderMetadata(
|
||||
return Object.keys(next).length > 0 ? next : undefined;
|
||||
}
|
||||
|
||||
const PROVIDER_REASONING_EFFORTS = new Set<ProviderReasoningEffort>([
|
||||
'low',
|
||||
'medium',
|
||||
'high',
|
||||
'xhigh',
|
||||
]);
|
||||
|
||||
function readCustomProviderReasoning(
|
||||
providerKey: string,
|
||||
entry: Record<string, unknown>,
|
||||
modelRef: string | undefined,
|
||||
): { reasoningEnabled: boolean; reasoningEfforts: ProviderReasoningEffort[] } {
|
||||
const modelId = modelRef?.startsWith(`${providerKey}/`)
|
||||
? modelRef.slice(providerKey.length + 1)
|
||||
: modelRef;
|
||||
const rows = Array.isArray(entry.models) ? entry.models : [];
|
||||
const row = rows.find((candidate) => (
|
||||
candidate
|
||||
&& typeof candidate === 'object'
|
||||
&& !Array.isArray(candidate)
|
||||
&& (candidate as Record<string, unknown>).id === modelId
|
||||
));
|
||||
if (!row || typeof row !== 'object' || Array.isArray(row)) {
|
||||
return { reasoningEnabled: false, reasoningEfforts: [] };
|
||||
}
|
||||
|
||||
const record = row as Record<string, unknown>;
|
||||
const compat = record.compat && typeof record.compat === 'object' && !Array.isArray(record.compat)
|
||||
? record.compat as Record<string, unknown>
|
||||
: undefined;
|
||||
const reasoningEfforts = Array.isArray(compat?.supportedReasoningEfforts)
|
||||
? compat.supportedReasoningEfforts.filter(
|
||||
(effort): effort is ProviderReasoningEffort => (
|
||||
typeof effort === 'string'
|
||||
&& PROVIDER_REASONING_EFFORTS.has(effort as ProviderReasoningEffort)
|
||||
),
|
||||
)
|
||||
: [];
|
||||
return {
|
||||
reasoningEnabled: record.reasoning === true,
|
||||
reasoningEfforts,
|
||||
};
|
||||
}
|
||||
|
||||
export class ProviderService {
|
||||
async listVendors(): Promise<ProviderDefinition[]> {
|
||||
return PROVIDER_DEFINITIONS;
|
||||
@@ -199,13 +244,23 @@ export class ProviderService {
|
||||
if (syncedAccount) {
|
||||
const nextMetadata = mergeSyncedProviderMetadata(kept.metadata, syncedAccount.metadata);
|
||||
const shouldSyncSelectedModel = defaultModel?.startsWith(`${key}/`) ?? false;
|
||||
const nextModel = shouldSyncSelectedModel ? syncedAccount.model : kept.model;
|
||||
const nextModel = shouldSyncSelectedModel ? syncedAccount.model : (kept.model ?? syncedAccount.model);
|
||||
const reasoning = kept.vendorId === 'custom'
|
||||
? readCustomProviderReasoning(key, entry, nextModel)
|
||||
: {
|
||||
reasoningEnabled: kept.reasoningEnabled,
|
||||
reasoningEfforts: kept.reasoningEfforts,
|
||||
};
|
||||
const shouldSyncModelState = kept.model !== nextModel
|
||||
|| kept.reasoningEnabled !== reasoning.reasoningEnabled
|
||||
|| JSON.stringify(kept.reasoningEfforts ?? []) !== JSON.stringify(reasoning.reasoningEfforts ?? [])
|
||||
|| !providerMetadataEquals(kept.metadata, nextMetadata);
|
||||
if (shouldSyncModelState) {
|
||||
kept = {
|
||||
...kept,
|
||||
model: nextModel,
|
||||
reasoningEnabled: reasoning.reasoningEnabled,
|
||||
reasoningEfforts: reasoning.reasoningEfforts,
|
||||
metadata: nextMetadata,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
@@ -314,7 +369,12 @@ export class ProviderService {
|
||||
model = defaultModel;
|
||||
} else if (definition?.defaultModelId) {
|
||||
model = definition.defaultModelId;
|
||||
} else if (vendorId === 'custom' && customModels?.[0]) {
|
||||
model = customModels[0];
|
||||
}
|
||||
const reasoning = vendorId === 'custom'
|
||||
? readCustomProviderReasoning(key, entry, model)
|
||||
: undefined;
|
||||
|
||||
const account: ProviderAccount = {
|
||||
id: key,
|
||||
@@ -327,6 +387,8 @@ export class ProviderService {
|
||||
? (entry.headers as Record<string, string>)
|
||||
: undefined),
|
||||
model,
|
||||
reasoningEnabled: reasoning?.reasoningEnabled,
|
||||
reasoningEfforts: reasoning?.reasoningEfforts,
|
||||
metadata: customModels && customModels.length > 0
|
||||
? { customModels }
|
||||
: undefined,
|
||||
|
||||
@@ -31,6 +31,8 @@ export function providerConfigToAccount(
|
||||
: getProviderDefinition(config.type)?.providerConfig?.api),
|
||||
headers: config.headers,
|
||||
model: config.model,
|
||||
reasoningEnabled: config.reasoningEnabled,
|
||||
reasoningEfforts: config.reasoningEfforts,
|
||||
fallbackModels: config.fallbackModels,
|
||||
fallbackAccountIds: config.fallbackProviderIds,
|
||||
enabled: config.enabled,
|
||||
@@ -49,6 +51,8 @@ export function providerAccountToConfig(account: ProviderAccount): ProviderConfi
|
||||
apiProtocol: account.apiProtocol,
|
||||
headers: account.headers,
|
||||
model: account.model,
|
||||
reasoningEnabled: account.reasoningEnabled,
|
||||
reasoningEfforts: account.reasoningEfforts,
|
||||
fallbackModels: account.fallbackModels,
|
||||
fallbackProviderIds: account.fallbackAccountIds,
|
||||
enabled: account.enabled,
|
||||
|
||||
@@ -97,6 +97,14 @@ export function assertValidApiProtocol(
|
||||
}
|
||||
|
||||
export type ProviderProtocol = OpenClawApiProtocol;
|
||||
export type ProviderReasoningEffort = 'low' | 'medium' | 'high' | 'xhigh';
|
||||
|
||||
/** Fixed effort ladder written for every custom-provider primary model. */
|
||||
export const CUSTOM_PROVIDER_DEFAULT_REASONING_EFFORTS: ProviderReasoningEffort[] = [
|
||||
'low',
|
||||
'medium',
|
||||
'high',
|
||||
];
|
||||
|
||||
export type ProviderAuthMode =
|
||||
| 'api_key'
|
||||
@@ -118,6 +126,8 @@ export interface ProviderConfig {
|
||||
apiProtocol?: ProviderProtocol;
|
||||
headers?: Record<string, string>;
|
||||
model?: string;
|
||||
reasoningEnabled?: boolean;
|
||||
reasoningEfforts?: ProviderReasoningEffort[];
|
||||
fallbackModels?: string[];
|
||||
fallbackProviderIds?: string[];
|
||||
enabled: boolean;
|
||||
@@ -182,6 +192,8 @@ export interface ProviderAccount {
|
||||
apiProtocol?: ProviderProtocol;
|
||||
headers?: Record<string, string>;
|
||||
model?: string;
|
||||
reasoningEnabled?: boolean;
|
||||
reasoningEfforts?: ProviderReasoningEffort[];
|
||||
fallbackModels?: string[];
|
||||
fallbackAccountIds?: string[];
|
||||
enabled: boolean;
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
|
||||
const OPENCLAW_DIR = join(homedir(), '.openclaw');
|
||||
const WECOM_PLUGIN_ID = 'wecom';
|
||||
// Note: QQBot is a built-in channel since OpenClaw 3.31 — no plugin ID needed.
|
||||
const WECHAT_PLUGIN_ID = OPENCLAW_WECHAT_CHANNEL_TYPE;
|
||||
const FEISHU_PLUGIN_ID_CANDIDATES = ['openclaw-lark', 'feishu-openclaw-plugin'] as const;
|
||||
const DEFAULT_ACCOUNT_ID = 'default';
|
||||
@@ -59,13 +60,23 @@ const WECHAT_ACCOUNTS_DIR = join(WECHAT_STATE_DIR, 'accounts');
|
||||
const LEGACY_WECHAT_CREDENTIALS_DIR = join(OPENCLAW_DIR, 'credentials', WECHAT_PLUGIN_ID);
|
||||
const LEGACY_WECHAT_SYNC_DIR = join(OPENCLAW_DIR, 'agents', 'default', 'sessions', '.openclaw-weixin-sync');
|
||||
|
||||
// External plugins whose activation lives in plugins.entries while account
|
||||
// configuration remains exclusively under channels.<id>.
|
||||
// Channels that are managed as plugins (config goes under plugins.entries, not channels)
|
||||
const PLUGIN_CHANNELS: string[] = ['discord', 'qqbot', 'whatsapp'];
|
||||
const LEGACY_BUILTIN_CHANNEL_PLUGIN_IDS = new Set<string>();
|
||||
// OpenClaw 2026.7.1 bundles only these channel extensions. All other ClawX
|
||||
// channels must retain their explicit external plugin allowlist entries.
|
||||
const BUILTIN_CHANNEL_IDS = new Set(['telegram', 'imessage']);
|
||||
const BUILTIN_CHANNEL_IDS = new Set([
|
||||
'discord',
|
||||
'telegram',
|
||||
'whatsapp',
|
||||
'slack',
|
||||
'signal',
|
||||
'imessage',
|
||||
'matrix',
|
||||
'line',
|
||||
'msteams',
|
||||
'googlechat',
|
||||
'mattermost',
|
||||
'qqbot',
|
||||
]);
|
||||
|
||||
// Unique credential key per channel type – used for duplicate bot detection.
|
||||
// Maps each channel type to the field that uniquely identifies a bot/account.
|
||||
@@ -145,13 +156,6 @@ function sanitizeDiscordGuilds(config: unknown): void {
|
||||
* Call before committing channel-config mutations.
|
||||
*/
|
||||
function sanitizeChannelSectionsBeforeWrite(config: OpenClawConfig): void {
|
||||
for (const pluginId of PLUGIN_CHANNELS) {
|
||||
const pluginEntry = config.plugins?.entries?.[pluginId];
|
||||
if (!pluginEntry) continue;
|
||||
delete pluginEntry.accounts;
|
||||
delete pluginEntry.defaultAccount;
|
||||
}
|
||||
|
||||
if (!config.channels) return;
|
||||
for (const channelType of CHANNELS_OMIT_DEFAULT_ACCOUNT_KEY) {
|
||||
const section = config.channels[channelType];
|
||||
@@ -357,24 +361,52 @@ function ensurePluginRegistration(currentConfig: OpenClawConfig, pluginId: strin
|
||||
if (!currentConfig.plugins.entries[pluginId]) {
|
||||
currentConfig.plugins.entries[pluginId] = {};
|
||||
}
|
||||
const pluginEntry = currentConfig.plugins.entries[pluginId];
|
||||
// PluginEntryConfig contains plugin activation/config metadata, not channel
|
||||
// accounts. Older ClawX versions mirrored credentials here, which OpenClaw
|
||||
// 2026.7.1 rejects as an invalid plugins.entries.<id> shape.
|
||||
delete pluginEntry.accounts;
|
||||
delete pluginEntry.defaultAccount;
|
||||
pluginEntry.enabled = true;
|
||||
currentConfig.plugins.entries[pluginId].enabled = true;
|
||||
}
|
||||
|
||||
function syncPluginChannelRegistration(currentConfig: OpenClawConfig, channelType: string): void {
|
||||
function syncPluginChannelAccountMirror(currentConfig: OpenClawConfig, channelType: string): void {
|
||||
if (!PLUGIN_CHANNELS.includes(channelType)) return;
|
||||
const channelSection = currentConfig.channels?.[channelType];
|
||||
if (!channelSection) {
|
||||
removePluginRegistration(currentConfig, channelType);
|
||||
return;
|
||||
}
|
||||
ensurePluginRegistration(currentConfig, channelType);
|
||||
currentConfig.plugins!.entries![channelType].enabled = channelSection.enabled !== false;
|
||||
const pluginEntry = currentConfig.plugins?.entries?.[channelType];
|
||||
if (!pluginEntry) return;
|
||||
const accounts = getChannelAccountsMap(channelSection);
|
||||
pluginEntry.enabled = channelSection.enabled;
|
||||
pluginEntry.defaultAccount = channelSection.defaultAccount;
|
||||
if (accounts && Object.keys(accounts).length > 0) {
|
||||
pluginEntry.accounts = structuredClone(accounts);
|
||||
} else {
|
||||
delete pluginEntry.accounts;
|
||||
}
|
||||
}
|
||||
|
||||
function deletePluginChannelAccountMirror(
|
||||
currentConfig: OpenClawConfig,
|
||||
channelType: string,
|
||||
accountId: string,
|
||||
): boolean {
|
||||
if (!PLUGIN_CHANNELS.includes(channelType)) return false;
|
||||
const pluginEntry = currentConfig.plugins?.entries?.[channelType];
|
||||
if (!pluginEntry) return false;
|
||||
const accounts = getChannelAccountsMap(pluginEntry);
|
||||
if (!accounts?.[accountId]) return false;
|
||||
|
||||
delete accounts[accountId];
|
||||
const remainingAccountIds = Object.keys(accounts).sort((a, b) => {
|
||||
if (a === DEFAULT_ACCOUNT_ID) return -1;
|
||||
if (b === DEFAULT_ACCOUNT_ID) return 1;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
if (remainingAccountIds.length === 0) {
|
||||
delete pluginEntry.accounts;
|
||||
delete pluginEntry.defaultAccount;
|
||||
} else if (pluginEntry.defaultAccount === accountId) {
|
||||
pluginEntry.defaultAccount = remainingAccountIds[0];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function cleanupLegacyBuiltInChannelPluginRegistration(
|
||||
@@ -484,6 +516,10 @@ async function ensurePluginAllowlist(currentConfig: OpenClawConfig, channelType:
|
||||
ensurePluginRegistration(currentConfig, channelType);
|
||||
}
|
||||
|
||||
if (channelType === 'discord' || channelType === 'qqbot' || channelType === 'whatsapp') {
|
||||
ensurePluginRegistration(currentConfig, channelType);
|
||||
}
|
||||
|
||||
if (channelType === 'feishu') {
|
||||
const feishuPluginId = await resolveFeishuPluginId();
|
||||
if (!currentConfig.plugins) {
|
||||
@@ -574,6 +610,8 @@ async function ensurePluginAllowlist(currentConfig: OpenClawConfig, channelType:
|
||||
}
|
||||
}
|
||||
|
||||
// Note: QQBot is a built-in channel since OpenClaw 3.31 — no plugin registration needed.
|
||||
|
||||
if (channelType === WECHAT_PLUGIN_ID) {
|
||||
if (!currentConfig.plugins) {
|
||||
currentConfig.plugins = {
|
||||
@@ -816,8 +854,8 @@ export async function saveChannelConfig(
|
||||
await ensurePluginAllowlist(currentConfig, resolvedChannelType);
|
||||
syncBuiltinChannelsWithPluginAllowlist(currentConfig, [resolvedChannelType]);
|
||||
|
||||
// Channel credentials always live under channels.<id>. External plugin
|
||||
// entries carry activation metadata only.
|
||||
// Plugin-based channels are mirrored into plugins.entries.<id> below,
|
||||
// but ClawX still keeps channels.<id> as the local account-list source.
|
||||
|
||||
if (!currentConfig.channels) {
|
||||
currentConfig.channels = {};
|
||||
@@ -865,7 +903,20 @@ export async function saveChannelConfig(
|
||||
// read channels.<type>.enabled still work.
|
||||
channelSection.enabled = transformedConfig.enabled ?? channelSection.enabled ?? true;
|
||||
|
||||
syncPluginChannelRegistration(currentConfig, resolvedChannelType);
|
||||
// Plugin-backed channel packages read their activation/config from
|
||||
// plugins.entries.<id>. Mirror the enabled flag and account map there
|
||||
// while preserving channels.<id> for ClawX's account list UI.
|
||||
if (PLUGIN_CHANNELS.includes(resolvedChannelType)) {
|
||||
ensurePluginRegistration(currentConfig, resolvedChannelType);
|
||||
const pluginEntry = currentConfig.plugins!.entries![resolvedChannelType];
|
||||
const pluginAccounts = ensureChannelAccountsMap(pluginEntry);
|
||||
pluginEntry.defaultAccount = channelSection.defaultAccount;
|
||||
pluginEntry.enabled = channelSection.enabled;
|
||||
pluginAccounts[resolvedAccountId] = {
|
||||
...pluginAccounts[resolvedAccountId],
|
||||
...accounts[resolvedAccountId],
|
||||
};
|
||||
}
|
||||
|
||||
// Most OpenClaw channel plugins/built-ins also read the default
|
||||
// account's credentials from the top level of `channels.<type>`
|
||||
@@ -973,13 +1024,21 @@ export async function deleteChannelAccountConfig(channelType: string, accountId:
|
||||
deleteWeChatAccount = false;
|
||||
deletedAccount = false;
|
||||
const currentConfig = snapshot as OpenClawConfig;
|
||||
const deletedPluginAccount = deletePluginChannelAccountMirror(
|
||||
currentConfig,
|
||||
resolvedChannelType,
|
||||
accountId,
|
||||
);
|
||||
const channelSection = currentConfig.channels?.[resolvedChannelType];
|
||||
if (!channelSection) {
|
||||
if (isWechatChannelType(resolvedChannelType)) {
|
||||
removePluginRegistration(currentConfig, WECHAT_PLUGIN_ID);
|
||||
deleteWeChatAccount = true;
|
||||
} else if (PLUGIN_CHANNELS.includes(resolvedChannelType)) {
|
||||
removePluginRegistration(currentConfig, resolvedChannelType);
|
||||
}
|
||||
if (deletedPluginAccount) {
|
||||
deletedAccount = true;
|
||||
syncBuiltinChannelsWithPluginAllowlist(currentConfig);
|
||||
sanitizeChannelSectionsBeforeWrite(currentConfig);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -987,7 +1046,14 @@ export async function deleteChannelAccountConfig(channelType: string, accountId:
|
||||
const existingAccounts = getChannelAccountsMap(channelSection);
|
||||
const targetsLegacyDefault = accountId === DEFAULT_ACCOUNT_ID
|
||||
&& Object.keys(getLegacyChannelPayload(channelSection)).length > 0;
|
||||
if (!existingAccounts?.[accountId] && !targetsLegacyDefault) return;
|
||||
if (!existingAccounts?.[accountId] && !targetsLegacyDefault) {
|
||||
if (deletedPluginAccount) {
|
||||
deletedAccount = true;
|
||||
syncBuiltinChannelsWithPluginAllowlist(currentConfig);
|
||||
sanitizeChannelSectionsBeforeWrite(currentConfig);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const currentDefaultAccountId = typeof channelSection.defaultAccount === 'string'
|
||||
&& channelSection.defaultAccount.trim()
|
||||
? channelSection.defaultAccount.trim()
|
||||
@@ -1003,8 +1069,6 @@ export async function deleteChannelAccountConfig(channelType: string, accountId:
|
||||
delete currentConfig.channels![resolvedChannelType];
|
||||
if (isWechatChannelType(resolvedChannelType)) {
|
||||
removePluginRegistration(currentConfig, WECHAT_PLUGIN_ID);
|
||||
} else if (PLUGIN_CHANNELS.includes(resolvedChannelType)) {
|
||||
removePluginRegistration(currentConfig, resolvedChannelType);
|
||||
}
|
||||
} else {
|
||||
if (channelSection.defaultAccount === accountId) {
|
||||
@@ -1032,7 +1096,7 @@ export async function deleteChannelAccountConfig(channelType: string, accountId:
|
||||
}
|
||||
}
|
||||
|
||||
syncPluginChannelRegistration(currentConfig, resolvedChannelType);
|
||||
syncPluginChannelAccountMirror(currentConfig, resolvedChannelType);
|
||||
syncBuiltinChannelsWithPluginAllowlist(currentConfig);
|
||||
sanitizeChannelSectionsBeforeWrite(currentConfig);
|
||||
if (isWechatChannelType(resolvedChannelType)) {
|
||||
@@ -1080,9 +1144,6 @@ export async function deleteChannelConfig(channelType: string): Promise<void> {
|
||||
if (resolvedChannelType === 'wecom') {
|
||||
removePluginRegistration(currentConfig, WECOM_PLUGIN_ID);
|
||||
}
|
||||
if (PLUGIN_CHANNELS.includes(resolvedChannelType)) {
|
||||
removePluginRegistration(currentConfig, resolvedChannelType);
|
||||
}
|
||||
syncBuiltinChannelsWithPluginAllowlist(currentConfig);
|
||||
if (isWechatChannelType(resolvedChannelType)) {
|
||||
deleteWeChat = true;
|
||||
@@ -1274,41 +1335,6 @@ export async function deleteAgentChannelAccounts(agentId: string, ownedChannelAc
|
||||
modified = false;
|
||||
const currentConfig = snapshot as OpenClawConfig;
|
||||
const channels = currentConfig.channels ?? {};
|
||||
|
||||
// Older ClawX releases could leave the only copy of Discord, QQBot,
|
||||
// or WhatsApp account credentials under plugins.entries.<id>. Migrate
|
||||
// that invalid legacy shape into channels.<id> before deleting the
|
||||
// owned account, so sibling accounts survive while PluginEntryConfig
|
||||
// is normalized back to activation metadata only.
|
||||
const legacyPluginChannelTypes = ownedChannelAccounts
|
||||
? [...ownedChannelAccounts]
|
||||
.filter((channelAccountKey) => channelAccountKey.endsWith(`:${accountId}`))
|
||||
.map((channelAccountKey) => channelAccountKey.slice(0, -accountId.length - 1))
|
||||
: PLUGIN_CHANNELS;
|
||||
for (const channelType of legacyPluginChannelTypes) {
|
||||
if (!PLUGIN_CHANNELS.includes(channelType)) continue;
|
||||
const pluginEntry = currentConfig.plugins?.entries?.[channelType];
|
||||
const pluginAccounts = pluginEntry ? getChannelAccountsMap(pluginEntry) : undefined;
|
||||
if (!pluginEntry || !pluginAccounts?.[accountId]) continue;
|
||||
|
||||
const section = channels[channelType] ?? {
|
||||
enabled: pluginEntry.enabled !== false,
|
||||
};
|
||||
const channelAccounts = ensureChannelAccountsMap(section);
|
||||
for (const [legacyAccountId, legacyAccountConfig] of Object.entries(pluginAccounts)) {
|
||||
if (!channelAccounts[legacyAccountId]) {
|
||||
channelAccounts[legacyAccountId] = structuredClone(legacyAccountConfig);
|
||||
}
|
||||
}
|
||||
if (typeof section.defaultAccount !== 'string' || !section.defaultAccount.trim()) {
|
||||
section.defaultAccount = typeof pluginEntry.defaultAccount === 'string'
|
||||
? pluginEntry.defaultAccount
|
||||
: DEFAULT_ACCOUNT_ID;
|
||||
}
|
||||
channels[channelType] = section;
|
||||
currentConfig.channels = channels;
|
||||
}
|
||||
|
||||
for (const channelType of Object.keys(channels)) {
|
||||
if (ownedChannelAccounts && !ownedChannelAccounts.has(`${channelType}:${accountId}`)) continue;
|
||||
const section = channels[channelType];
|
||||
@@ -1352,10 +1378,21 @@ export async function deleteAgentChannelAccounts(agentId: string, ownedChannelAc
|
||||
}
|
||||
}
|
||||
}
|
||||
syncPluginChannelRegistration(currentConfig, channelType);
|
||||
syncPluginChannelAccountMirror(currentConfig, channelType);
|
||||
modified = true;
|
||||
}
|
||||
|
||||
const pluginChannelTypes = ownedChannelAccounts
|
||||
? [...ownedChannelAccounts]
|
||||
.filter((channelAccountKey) => channelAccountKey.endsWith(`:${accountId}`))
|
||||
.map((channelAccountKey) => channelAccountKey.slice(0, -accountId.length - 1))
|
||||
: Object.keys(currentConfig.plugins?.entries ?? {});
|
||||
for (const channelType of pluginChannelTypes) {
|
||||
if (deletePluginChannelAccountMirror(currentConfig, channelType, accountId)) {
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (modified) {
|
||||
sanitizeChannelSectionsBeforeWrite(currentConfig);
|
||||
}
|
||||
@@ -1384,8 +1421,19 @@ export async function setChannelEnabled(channelType: string, enabled: boolean):
|
||||
|
||||
if (PLUGIN_CHANNELS.includes(resolvedChannelType)) {
|
||||
pluginChannel = true;
|
||||
ensurePluginRegistration(currentConfig, resolvedChannelType);
|
||||
currentConfig.plugins!.entries![resolvedChannelType].enabled = enabled;
|
||||
if (enabled) {
|
||||
ensurePluginRegistration(currentConfig, resolvedChannelType);
|
||||
} else {
|
||||
const plugins = currentConfig.plugins ?? (currentConfig.plugins = {});
|
||||
const entries = plugins.entries ?? (plugins.entries = {});
|
||||
entries[resolvedChannelType] ??= {};
|
||||
}
|
||||
const entries = currentConfig.plugins?.entries;
|
||||
const pluginEntry = entries?.[resolvedChannelType];
|
||||
if (!pluginEntry) throw new Error(`Plugin entry not initialized: ${resolvedChannelType}`);
|
||||
pluginEntry.enabled = enabled;
|
||||
syncBuiltinChannelsWithPluginAllowlist(currentConfig);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentConfig.channels) currentConfig.channels = {};
|
||||
|
||||
@@ -13,7 +13,7 @@ type BuildGatewayHealthSummaryOptions = {
|
||||
};
|
||||
|
||||
const CHANNEL_STATUS_FAILURE_WINDOW_MS = 2 * 60_000;
|
||||
const HEARTBEAT_MISS_THRESHOLD = 4;
|
||||
const HEARTBEAT_MISS_THRESHOLD = 10;
|
||||
|
||||
export function buildGatewayHealthSummary(
|
||||
options: BuildGatewayHealthSummaryOptions,
|
||||
|
||||
+86
-188
@@ -42,9 +42,13 @@ import { PORTS } from './config';
|
||||
import { getSetting, setSetting } from './store';
|
||||
import {
|
||||
assertValidApiProtocol,
|
||||
CUSTOM_PROVIDER_DEFAULT_REASONING_EFFORTS,
|
||||
normalizeOpenClawApiProtocol,
|
||||
} from '../shared/providers/types';
|
||||
import { inferCustomModelContextWindow, inferCustomModelInputModalities } from '../shared/providers/model-capabilities';
|
||||
import {
|
||||
inferCustomModelContextWindow,
|
||||
inferCustomModelInputModalities,
|
||||
} from '../shared/providers/model-capabilities';
|
||||
import {
|
||||
CLAWX_OPENAI_IMAGE_DEFAULT_MODEL,
|
||||
CLAWX_OPENAI_IMAGE_PROVIDER_KEY,
|
||||
@@ -534,10 +538,20 @@ const FEISHU_PLUGIN_ID_CANDIDATES = ['openclaw-lark', 'feishu-openclaw-plugin']
|
||||
const VALID_COMPACTION_MODES = new Set(['default', 'safeguard']);
|
||||
/** Matches OpenClaw's 200k+ context-window recommendation (see computeContextAwareReserveTokensFloor). */
|
||||
const DEFAULT_COMPACTION_RESERVE_TOKENS_FLOOR = 50_000;
|
||||
// OpenClaw 2026.7.1 bundles these channel extensions. Discord, WhatsApp,
|
||||
// QQBot, and the remaining catalog channels are external plugins and their
|
||||
// explicit allowlist registrations must be preserved.
|
||||
const BUILTIN_CHANNEL_IDS = new Set(['telegram', 'imessage']);
|
||||
const BUILTIN_CHANNEL_IDS = new Set([
|
||||
'discord',
|
||||
'telegram',
|
||||
'whatsapp',
|
||||
'slack',
|
||||
'signal',
|
||||
'imessage',
|
||||
'matrix',
|
||||
'line',
|
||||
'msteams',
|
||||
'googlechat',
|
||||
'mattermost',
|
||||
'qqbot',
|
||||
]);
|
||||
const OPTIONAL_PROVIDER_LIKE_BUNDLED_PLUGIN_IDS = new Set([
|
||||
'alibaba',
|
||||
'deepgram',
|
||||
@@ -899,35 +913,34 @@ function backfillCompactionReserveTokensFloor(config: Record<string, unknown>):
|
||||
}
|
||||
|
||||
/**
|
||||
* Self-heal helper: walk `models.providers.custom-*` entries and fill in an
|
||||
* inferred `contextWindow` on model rows that have neither `contextWindow`
|
||||
* nor `contextTokens`. Rows written by older ClawX versions only carried
|
||||
* `{ id, name, input }`, which disables OpenClaw's preemptive compaction and
|
||||
* context-window guard for custom providers.
|
||||
* Self-heal helper: walk `models.providers.custom-*` entries and fill inferred
|
||||
* context-window metadata that older ClawX versions omitted.
|
||||
*
|
||||
* Deliberately scoped to `custom-` keys: registry providers own their
|
||||
* metadata, and small local models (ollama) must not inherit a large window.
|
||||
*/
|
||||
function backfillCustomProviderModelContextWindows(config: Record<string, unknown>): string[] {
|
||||
function backfillCustomProviderModelCapabilities(config: Record<string, unknown>): string[] {
|
||||
const models = (config.models || {}) as Record<string, unknown>;
|
||||
const providers = (models.providers || {}) as Record<string, unknown>;
|
||||
const backfilled: string[] = [];
|
||||
const backfilled = new Set<string>();
|
||||
|
||||
for (const [providerKey, entry] of Object.entries(providers)) {
|
||||
if (!providerKey.startsWith('custom-') || !isPlainRecord(entry)) continue;
|
||||
const rows = Array.isArray(entry.models) ? entry.models : [];
|
||||
for (const row of rows) {
|
||||
if (!isPlainRecord(row) || typeof row.id !== 'string' || !row.id) continue;
|
||||
if (typeof row.contextWindow === 'number' || typeof row.contextTokens === 'number') continue;
|
||||
row.contextWindow = inferCustomModelContextWindow(row.id, {
|
||||
const context = {
|
||||
providerKey,
|
||||
apiProtocol: typeof entry.api === 'string' ? entry.api : undefined,
|
||||
});
|
||||
backfilled.push(`${providerKey}/${row.id}`);
|
||||
};
|
||||
if (typeof row.contextWindow !== 'number' && typeof row.contextTokens !== 'number') {
|
||||
row.contextWindow = inferCustomModelContextWindow(row.id, context);
|
||||
backfilled.add(`${providerKey}/${row.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return backfilled;
|
||||
return [...backfilled];
|
||||
}
|
||||
|
||||
// ── Exported Functions (all async) ───────────────────────────────
|
||||
@@ -1886,6 +1899,21 @@ function applyOpenClawProviderAgentRuntimePinsToConfig(config: Record<string, un
|
||||
return pinned;
|
||||
}
|
||||
|
||||
function applyCustomPrimaryModelReasoning(
|
||||
models: Array<Record<string, unknown>>,
|
||||
primaryModelId: string | undefined,
|
||||
): void {
|
||||
if (!primaryModelId) return;
|
||||
|
||||
const row = models.find((model) => model.id === primaryModelId);
|
||||
if (!row) return;
|
||||
|
||||
row.reasoning = true;
|
||||
const compat = isPlainRecord(row.compat) ? { ...row.compat } : {};
|
||||
compat.supportedReasoningEfforts = [...CUSTOM_PROVIDER_DEFAULT_REASONING_EFFORTS];
|
||||
row.compat = compat;
|
||||
}
|
||||
|
||||
function upsertOpenClawProviderEntry(
|
||||
config: Record<string, unknown>,
|
||||
provider: string,
|
||||
@@ -1923,6 +1951,9 @@ function upsertOpenClawProviderEntry(
|
||||
: {}),
|
||||
}));
|
||||
let mergedModels = mergeProviderModels(registryModels, existingModels, runtimeModels);
|
||||
if (provider.startsWith('custom-')) {
|
||||
applyCustomPrimaryModelReasoning(mergedModels, options.modelIds?.[0]);
|
||||
}
|
||||
if (options.api === 'anthropic-messages') {
|
||||
mergedModels = mergedModels.map((model) => ensureAnthropicMessagesModelEntry(model, provider, existingProvider));
|
||||
}
|
||||
@@ -2599,14 +2630,14 @@ export async function batchSyncConfigFields(token: string): Promise<void> {
|
||||
let pinnedProviderRuntimes: string[] = [];
|
||||
let compactionLog: string | undefined;
|
||||
let memorySearchDefaultResult: 'migrated' | 'seeded' | 'unchanged' = 'unchanged';
|
||||
let backfilledContextWindows: string[] = [];
|
||||
let backfilledModelCapabilities: string[] = [];
|
||||
|
||||
const changed = await mutateOpenClawConfig((config) => {
|
||||
let modified = true;
|
||||
pinnedProviderRuntimes = [];
|
||||
compactionLog = undefined;
|
||||
memorySearchDefaultResult = 'unchanged';
|
||||
backfilledContextWindows = [];
|
||||
backfilledModelCapabilities = [];
|
||||
|
||||
// ── Gateway token + controlUi ──
|
||||
const gateway = (
|
||||
@@ -2718,9 +2749,9 @@ export async function batchSyncConfigFields(token: string): Promise<void> {
|
||||
modified = true;
|
||||
}
|
||||
|
||||
// ── Custom provider contextWindow backfill ──
|
||||
backfilledContextWindows = backfillCustomProviderModelContextWindows(config);
|
||||
if (backfilledContextWindows.length > 0) {
|
||||
// ── Custom provider model-capability backfill ──
|
||||
backfilledModelCapabilities = backfillCustomProviderModelCapabilities(config);
|
||||
if (backfilledModelCapabilities.length > 0) {
|
||||
modified = true;
|
||||
}
|
||||
|
||||
@@ -2740,8 +2771,8 @@ export async function batchSyncConfigFields(token: string): Promise<void> {
|
||||
+ 'agents.defaults.memorySearch to FTS-only mode',
|
||||
);
|
||||
}
|
||||
if (backfilledContextWindows.length > 0) {
|
||||
console.log(`[batch-sync] Backfilled contextWindow for custom provider models: ${backfilledContextWindows.join(', ')}`);
|
||||
if (backfilledModelCapabilities.length > 0) {
|
||||
console.log(`[batch-sync] Backfilled custom provider model capabilities: ${backfilledModelCapabilities.join(', ')}`);
|
||||
}
|
||||
if (changed) {
|
||||
console.log('Synced gateway token, browser config, web_fetch SSRF policy, and session idle to openclaw.json');
|
||||
@@ -2802,8 +2833,19 @@ async function updateModelsJsonProviderEntriesForAgents(
|
||||
const mergedModels = (entry.models ?? []).map((m) => {
|
||||
const prev = existingModels.find((e) => e.id === m.id);
|
||||
const base = prev ? { ...prev, id: m.id, name: m.name } : { ...m };
|
||||
if (typeof m.reasoning === 'boolean') {
|
||||
base.reasoning = m.reasoning;
|
||||
const compat = isPlainRecord(base.compat) ? { ...base.compat } : {};
|
||||
if (isPlainRecord(m.compat) && Array.isArray(m.compat.supportedReasoningEfforts)) {
|
||||
compat.supportedReasoningEfforts = [...m.compat.supportedReasoningEfforts];
|
||||
} else if (!m.reasoning) {
|
||||
delete compat.supportedReasoningEfforts;
|
||||
}
|
||||
if (Object.keys(compat).length > 0) base.compat = compat;
|
||||
else delete base.compat;
|
||||
}
|
||||
// Custom-provider rows need an explicit contextWindow so the embedded
|
||||
// runner can budget compaction (see backfillCustomProviderModelContextWindows).
|
||||
// runner can budget compaction (see backfillCustomProviderModelCapabilities).
|
||||
if (
|
||||
providerType.startsWith('custom-')
|
||||
&& typeof base.contextWindow !== 'number'
|
||||
@@ -2879,13 +2921,6 @@ export async function updateSingleAgentModelProvider(
|
||||
*/
|
||||
const SKILL_WORKSHOP_TOOL_DENY_ENTRY = 'skill_workshop';
|
||||
const WEB_SEARCH_TOOL_DENY_ENTRY = 'web_search';
|
||||
const CONTROL_PLANE_TOOL_DENY_ENTRIES = [
|
||||
'gateway',
|
||||
'nodes',
|
||||
'create_goal',
|
||||
'get_goal',
|
||||
'update_goal',
|
||||
] as const;
|
||||
const SKILL_CREATOR_SKILL_KEY = 'skill-creator';
|
||||
|
||||
function normalizeToolDenyList(value: unknown): string[] {
|
||||
@@ -2904,20 +2939,6 @@ function ensureToolDenyIncludes(
|
||||
return { deny: [...deny, entry], modified: true };
|
||||
}
|
||||
|
||||
function ensureToolDenyIncludesAll(
|
||||
deny: string[],
|
||||
entries: readonly string[],
|
||||
): { deny: string[]; modified: boolean } {
|
||||
let current = deny;
|
||||
let modified = false;
|
||||
for (const entry of entries) {
|
||||
const result = ensureToolDenyIncludes(current, entry);
|
||||
current = result.deny;
|
||||
modified ||= result.modified;
|
||||
}
|
||||
return { deny: current, modified };
|
||||
}
|
||||
|
||||
export async function sanitizeOpenClawConfig(): Promise<void> {
|
||||
// The prelaunch file fallback must not turn a missing or corrupt config into
|
||||
// a valid-looking skeleton. The coordinator performs the successful mutation.
|
||||
@@ -2957,27 +2978,8 @@ export async function sanitizeOpenClawConfig(): Promise<void> {
|
||||
}
|
||||
|
||||
// ── plugins section ──────────────────────────────────────────────
|
||||
// OpenClaw 2026.7.1 moved these formerly bundled channels to external
|
||||
// plugins. Recover old channel-only configs before plugin sanitization.
|
||||
let plugins = config.plugins;
|
||||
if (!plugins && isPlainRecord(config.channels)) {
|
||||
const channels = config.channels as Record<string, unknown>;
|
||||
const externalChannelIds = ['discord', 'whatsapp', 'qqbot'].filter((channelId) => {
|
||||
const section = channels[channelId];
|
||||
return isPlainRecord(section) && section.enabled !== false && Object.keys(section).length > 0;
|
||||
});
|
||||
if (externalChannelIds.length > 0) {
|
||||
plugins = {
|
||||
enabled: true,
|
||||
allow: externalChannelIds,
|
||||
entries: Object.fromEntries(externalChannelIds.map((channelId) => [channelId, { enabled: true }])),
|
||||
};
|
||||
config.plugins = plugins;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove absolute paths in plugins that no longer exist or are bundled (preventing hardlink validation errors)
|
||||
const plugins = config.plugins;
|
||||
if (plugins) {
|
||||
if (Array.isArray(plugins)) {
|
||||
const validPlugins: unknown[] = [];
|
||||
@@ -3137,22 +3139,6 @@ export async function sanitizeOpenClawConfig(): Promise<void> {
|
||||
toolsModified = true;
|
||||
}
|
||||
|
||||
const controlPlaneToolDenyResult = ensureToolDenyIncludesAll(
|
||||
normalizeToolDenyList(toolsConfig.deny),
|
||||
CONTROL_PLANE_TOOL_DENY_ENTRIES,
|
||||
);
|
||||
if (controlPlaneToolDenyResult.modified) {
|
||||
toolsConfig.deny = controlPlaneToolDenyResult.deny;
|
||||
toolsModified = true;
|
||||
console.log('[sanitize] Added control-plane tools to tools.deny for ClawX desktop');
|
||||
} else if (
|
||||
!Array.isArray(toolsConfig.deny)
|
||||
|| toolsConfig.deny.length !== controlPlaneToolDenyResult.deny.length
|
||||
) {
|
||||
toolsConfig.deny = controlPlaneToolDenyResult.deny;
|
||||
toolsModified = true;
|
||||
}
|
||||
|
||||
// ── tools.exec approvals (OpenClaw 3.28+) ──────────────────────
|
||||
// ClawX is a local desktop app where the user is the trusted operator.
|
||||
// Exec approval prompts add unnecessary friction in this context, so we
|
||||
@@ -3230,22 +3216,6 @@ export async function sanitizeOpenClawConfig(): Promise<void> {
|
||||
gatewayModified = true;
|
||||
}
|
||||
|
||||
const gatewayControlPlaneToolDenyResult = ensureToolDenyIncludesAll(
|
||||
normalizeToolDenyList(gatewayTools.deny),
|
||||
CONTROL_PLANE_TOOL_DENY_ENTRIES,
|
||||
);
|
||||
if (gatewayControlPlaneToolDenyResult.modified) {
|
||||
gatewayTools.deny = gatewayControlPlaneToolDenyResult.deny;
|
||||
gatewayModified = true;
|
||||
console.log('[sanitize] Added control-plane tools to gateway.tools.deny for ClawX desktop');
|
||||
} else if (
|
||||
!Array.isArray(gatewayTools.deny)
|
||||
|| gatewayTools.deny.length !== gatewayControlPlaneToolDenyResult.deny.length
|
||||
) {
|
||||
gatewayTools.deny = gatewayControlPlaneToolDenyResult.deny;
|
||||
gatewayModified = true;
|
||||
}
|
||||
|
||||
if (gatewayModified) {
|
||||
gateway.tools = gatewayTools;
|
||||
config.gateway = gateway;
|
||||
@@ -3479,96 +3449,18 @@ export async function sanitizeOpenClawConfig(): Promise<void> {
|
||||
modified = true;
|
||||
}
|
||||
|
||||
// ── external channel plugin registration cleanup ────────────
|
||||
// Channel account configuration belongs under channels.<id>. OpenClaw's
|
||||
// PluginEntryConfig rejects ClawX's legacy accounts/defaultAccount mirror.
|
||||
// Migrate first: some older configs have no channels.<id> copy, and
|
||||
// deleting the plugin account map directly would lose their credentials.
|
||||
for (const pluginId of ['discord', 'whatsapp', 'qqbot'] as const) {
|
||||
const pluginEntry = pEntries[pluginId];
|
||||
if (!pluginEntry) continue;
|
||||
|
||||
const legacyAccounts = isPlainRecord(pluginEntry.accounts)
|
||||
? pluginEntry.accounts as Record<string, Record<string, unknown>>
|
||||
: null;
|
||||
if (legacyAccounts && Object.keys(legacyAccounts).length > 0) {
|
||||
const channels = isPlainRecord(config.channels)
|
||||
? config.channels as Record<string, Record<string, unknown>>
|
||||
: {};
|
||||
const existingSection = isPlainRecord(channels[pluginId])
|
||||
? channels[pluginId]
|
||||
: {};
|
||||
const channelAccounts = isPlainRecord(existingSection.accounts)
|
||||
? existingSection.accounts as Record<string, Record<string, unknown>>
|
||||
: {};
|
||||
let migratedAccount = false;
|
||||
|
||||
for (const [accountId, accountConfig] of Object.entries(legacyAccounts)) {
|
||||
if (!isPlainRecord(accountConfig) || channelAccounts[accountId]) continue;
|
||||
channelAccounts[accountId] = structuredClone(accountConfig);
|
||||
migratedAccount = true;
|
||||
}
|
||||
|
||||
if (migratedAccount) {
|
||||
existingSection.accounts = channelAccounts;
|
||||
if (existingSection.enabled === undefined) {
|
||||
existingSection.enabled = pluginEntry.enabled !== false;
|
||||
}
|
||||
if (typeof existingSection.defaultAccount !== 'string' || !existingSection.defaultAccount.trim()) {
|
||||
const legacyDefaultAccount = typeof pluginEntry.defaultAccount === 'string'
|
||||
&& channelAccounts[pluginEntry.defaultAccount]
|
||||
? pluginEntry.defaultAccount
|
||||
: Object.keys(channelAccounts).sort((a, b) => {
|
||||
if (a === 'default') return -1;
|
||||
if (b === 'default') return 1;
|
||||
return a.localeCompare(b);
|
||||
})[0];
|
||||
if (legacyDefaultAccount) {
|
||||
existingSection.defaultAccount = legacyDefaultAccount;
|
||||
}
|
||||
}
|
||||
channels[pluginId] = existingSection;
|
||||
config.channels = channels;
|
||||
modified = true;
|
||||
console.log(`[sanitize] Migrated legacy plugins.entries.${pluginId}.accounts to channels.${pluginId}.accounts`);
|
||||
}
|
||||
}
|
||||
|
||||
if ('accounts' in pluginEntry) {
|
||||
delete pluginEntry.accounts;
|
||||
modified = true;
|
||||
}
|
||||
if ('defaultAccount' in pluginEntry) {
|
||||
delete pluginEntry.defaultAccount;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
// QQBot is an external @openclaw/qqbot plugin in OpenClaw 2026.7.1.
|
||||
// Migrate the legacy manifest id and keep one canonical active entry.
|
||||
const legacyQQBotId = 'openclaw-qqbot';
|
||||
const legacyQQBotAllowIndex = allowArr.indexOf(legacyQQBotId);
|
||||
if (legacyQQBotAllowIndex !== -1) {
|
||||
allowArr.splice(legacyQQBotAllowIndex, 1);
|
||||
modified = true;
|
||||
}
|
||||
if (pEntries[legacyQQBotId]) {
|
||||
delete pEntries[legacyQQBotId];
|
||||
modified = true;
|
||||
}
|
||||
const qqbotChannel = (config.channels as Record<string, Record<string, unknown>> | undefined)?.qqbot;
|
||||
const isQQBotConfigured = Boolean(
|
||||
qqbotChannel
|
||||
&& qqbotChannel.enabled !== false
|
||||
&& Object.keys(qqbotChannel).length > 0
|
||||
);
|
||||
if (isQQBotConfigured) {
|
||||
if (!allowArr.includes('qqbot')) {
|
||||
allowArr.push('qqbot');
|
||||
modified = true;
|
||||
}
|
||||
if (!pEntries.qqbot || pEntries.qqbot.enabled !== true) {
|
||||
pEntries.qqbot = { ...(pEntries.qqbot || {}), enabled: true };
|
||||
// ── qqbot built-in channel cleanup ──────────────────────────
|
||||
// OpenClaw 3.31 moved qqbot from a third-party plugin to a built-in
|
||||
// channel. Clean up legacy plugin entries (both bare "qqbot" and
|
||||
// manifest-declared "openclaw-qqbot") from plugins.entries.
|
||||
// plugins.allow is left untouched — having openclaw-qqbot there is harmless.
|
||||
// The channel config under channels.qqbot is preserved and works
|
||||
// identically with the built-in channel.
|
||||
const QQBOT_PLUGIN_IDS = ['qqbot', 'openclaw-qqbot'] as const;
|
||||
for (const qqbotId of QQBOT_PLUGIN_IDS) {
|
||||
if (pEntries?.[qqbotId]) {
|
||||
delete pEntries[qqbotId];
|
||||
console.log(`[sanitize] Removed built-in channel plugin from plugins.entries: ${qqbotId}`);
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
@@ -3651,6 +3543,12 @@ export async function sanitizeOpenClawConfig(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
if (pEntries.whatsapp) {
|
||||
delete pEntries.whatsapp;
|
||||
console.log('[sanitize] Removed legacy plugins.entries.whatsapp for built-in channel');
|
||||
modified = true;
|
||||
}
|
||||
|
||||
// Discover all bundled extension IDs so we can clean stale bundled
|
||||
// allowlist entries from older OpenClaw versions. Re-add only the
|
||||
// ClawX-critical bundled plugins, active provider plugins, and explicitly
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { chmod, copyFile, lstat, mkdir, readdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
||||
import { basename, dirname, join, relative, resolve } from 'node:path';
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
import { resolveOpenClawConfigPath, resolveOpenClawStateDir } from './paths';
|
||||
|
||||
const UPGRADE_ID = 'openclaw-2026.7.1';
|
||||
@@ -24,12 +23,6 @@ export type OpenClawUpgradeSnapshotCleanupResult = {
|
||||
snapshotDir: string;
|
||||
};
|
||||
|
||||
export type LegacyUpdateCheckCleanupResult = {
|
||||
status: 'quarantined' | 'missing' | 'deferred';
|
||||
sourcePath: string;
|
||||
backupPath?: string;
|
||||
};
|
||||
|
||||
type SnapshotOptions = {
|
||||
stateDir?: string;
|
||||
configPath?: string;
|
||||
@@ -64,44 +57,6 @@ async function copyFileIfPresent(source: string, destination: string, copied: st
|
||||
copied.push(destination);
|
||||
}
|
||||
|
||||
async function pathExists(path: string): Promise<boolean> {
|
||||
try {
|
||||
await lstat(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveAvailableBackupPath(basePath: string): Promise<string> {
|
||||
if (!await pathExists(basePath)) return basePath;
|
||||
|
||||
const timestamp = Date.now();
|
||||
for (let suffix = 0; suffix < 100; suffix += 1) {
|
||||
const candidate = `${basePath}.${timestamp}${suffix === 0 ? '' : `-${suffix}`}`;
|
||||
if (!await pathExists(candidate)) return candidate;
|
||||
}
|
||||
throw new Error(`Could not allocate backup path for ${basePath}`);
|
||||
}
|
||||
|
||||
function hasCanonicalUpdateCheckState(sqlitePath: string): boolean {
|
||||
let db: DatabaseSync | undefined;
|
||||
try {
|
||||
db = new DatabaseSync(sqlitePath, { readOnly: true });
|
||||
const row = db.prepare(`
|
||||
SELECT 1 AS present
|
||||
FROM update_check_state
|
||||
WHERE state_key = ?
|
||||
LIMIT 1
|
||||
`).get('default') as { present?: number } | undefined;
|
||||
return row?.present === 1;
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
db?.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function copyTree(
|
||||
sourceRoot: string,
|
||||
destinationRoot: string,
|
||||
@@ -206,46 +161,6 @@ export async function ensureOpenClaw2026_7_1UpgradeSnapshot(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenClaw 2026.7.1 refuses Gateway readiness when the legacy update-check JSON
|
||||
* differs from an existing canonical SQLite row. The JSON contains updater
|
||||
* bookkeeping only, and upstream would archive it when both copies match. Once
|
||||
* SQLite has the canonical row, move the legacy file out of the active state
|
||||
* root so a harmless mismatch cannot trap startup or an ineffective doctor
|
||||
* retry loop. If SQLite has no row yet, leave the JSON for upstream to import.
|
||||
*/
|
||||
export async function quarantineLegacyUpdateCheckState(
|
||||
options: Pick<SnapshotOptions, 'stateDir'> = {},
|
||||
): Promise<LegacyUpdateCheckCleanupResult> {
|
||||
const stateDir = resolve(options.stateDir ?? resolveOpenClawStateDir());
|
||||
const sourcePath = join(stateDir, 'update-check.json');
|
||||
let sourceInfo;
|
||||
try {
|
||||
sourceInfo = await lstat(sourcePath);
|
||||
} catch {
|
||||
return { status: 'missing', sourcePath };
|
||||
}
|
||||
if (!sourceInfo.isFile() && !sourceInfo.isSymbolicLink()) {
|
||||
return { status: 'deferred', sourcePath };
|
||||
}
|
||||
|
||||
const sqlitePath = join(stateDir, 'state', 'openclaw.sqlite');
|
||||
if (!hasCanonicalUpdateCheckState(sqlitePath)) {
|
||||
return { status: 'deferred', sourcePath };
|
||||
}
|
||||
|
||||
const backupDir = join(stateDir, 'backups');
|
||||
await mkdir(backupDir, { recursive: true, mode: SNAPSHOT_DIR_MODE });
|
||||
const backupPath = await resolveAvailableBackupPath(
|
||||
join(backupDir, `clawx-${UPGRADE_ID}-legacy-update-check.json`),
|
||||
);
|
||||
await rename(sourcePath, backupPath);
|
||||
if (sourceInfo.isFile()) {
|
||||
await chmod(backupPath, SNAPSHOT_FILE_MODE);
|
||||
}
|
||||
return { status: 'quarantined', sourcePath, backupPath };
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the one-time OpenClaw 2026.7.1 pre-migration snapshot after Gateway
|
||||
* startup succeeds so duplicated config/auth/SQLite secrets do not linger.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
import { app } from 'electron';
|
||||
import path from 'node:path';
|
||||
import { existsSync, cpSync, copyFileSync, statSync, lstatSync, mkdirSync, readFileSync, readlinkSync, writeFileSync, readdirSync, realpathSync, symlinkSync, unlinkSync } from 'node:fs';
|
||||
import { existsSync, cpSync, copyFileSync, statSync, lstatSync, mkdirSync, readFileSync, writeFileSync, readdirSync, realpathSync, symlinkSync, unlinkSync } from 'node:fs';
|
||||
import { readdir, stat, copyFile, mkdir } from 'node:fs/promises';
|
||||
import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
@@ -417,34 +417,6 @@ function canonicalComparablePath(filePath: string): string {
|
||||
return process.platform === 'win32' ? withoutLongPathPrefix.toLowerCase() : withoutLongPathPrefix;
|
||||
}
|
||||
|
||||
function resolveSymlinkTarget(linkPath: string, target: string): string {
|
||||
return path.isAbsolute(target) ? target : path.resolve(path.dirname(linkPath), target);
|
||||
}
|
||||
|
||||
function openClawPeerLinkPointsTo(linkPath: string, openclawDir: string): boolean {
|
||||
try {
|
||||
const stat = lstatSync(fsPath(linkPath));
|
||||
if (stat.isSymbolicLink()) {
|
||||
const target = readlinkSync(fsPath(linkPath));
|
||||
const resolvedTarget = resolveSymlinkTarget(linkPath, target);
|
||||
return canonicalComparablePath(resolvedTarget) === canonicalComparablePath(openclawDir);
|
||||
}
|
||||
if (stat.isDirectory()) {
|
||||
try {
|
||||
const packageJson = JSON.parse(readFileSync(fsPath(join(linkPath, 'package.json')), 'utf-8')) as { name?: unknown };
|
||||
if (packageJson.name === 'openclaw') {
|
||||
return canonicalComparablePath(linkPath) === canonicalComparablePath(openclawDir);
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Materialized mirrors live outside the bundled OpenClaw package tree, so
|
||||
* Node's normal package lookup cannot resolve their declared `openclaw` peer.
|
||||
@@ -486,8 +458,12 @@ export function repairPluginOpenClawPeerLink(
|
||||
return false;
|
||||
}
|
||||
|
||||
if (openClawPeerLinkPointsTo(linkPath, openclawDir)) {
|
||||
return true;
|
||||
try {
|
||||
if (canonicalComparablePath(linkPath) === canonicalComparablePath(openclawDir)) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// Fall through to lstat/creation for a missing or broken link.
|
||||
}
|
||||
|
||||
let existing: ReturnType<typeof lstatSync> | null = null;
|
||||
@@ -519,9 +495,8 @@ export function repairPluginOpenClawPeerLink(
|
||||
}
|
||||
}
|
||||
|
||||
const junctionTarget = path.resolve(openclawDir);
|
||||
symlinkSync(fsPath(junctionTarget), fsPath(linkPath), 'junction');
|
||||
if (!openClawPeerLinkPointsTo(linkPath, openclawDir)) {
|
||||
symlinkSync(openclawDir, fsPath(linkPath), 'junction');
|
||||
if (canonicalComparablePath(linkPath) !== canonicalComparablePath(openclawDir)) {
|
||||
logger.warn(`[plugin] OpenClaw peer link audit failed after creating ${linkPath}`);
|
||||
return false;
|
||||
}
|
||||
@@ -748,37 +723,28 @@ export function copyPluginFromNodeModules(npmPkgPath: string, targetDir: string,
|
||||
|
||||
// ── Core install / upgrade logic ─────────────────────────────────────────────
|
||||
|
||||
export type PluginInstallResult = {
|
||||
installed: boolean;
|
||||
warning?: string;
|
||||
peerLinkOk?: boolean;
|
||||
};
|
||||
|
||||
export async function ensurePluginInstalled(
|
||||
pluginDirName: string,
|
||||
candidateSources: string[],
|
||||
pluginLabel: string,
|
||||
): Promise<PluginInstallResult> {
|
||||
): Promise<{ installed: boolean; warning?: string }> {
|
||||
const targetDir = join(homedir(), '.openclaw', 'extensions', pluginDirName);
|
||||
const targetManifest = join(targetDir, 'openclaw.plugin.json');
|
||||
const targetPkgJson = join(targetDir, 'package.json');
|
||||
|
||||
const sourceDir = candidateSources.find((dir) => existsSync(fsPath(join(dir, 'openclaw.plugin.json'))));
|
||||
|
||||
async function finalizeInstalledMirror(): Promise<{ installed: true; peerLinkOk: boolean }> {
|
||||
await syncTrustedOfficialPluginInstallRecord(pluginDirName, targetDir);
|
||||
return { installed: true, peerLinkOk: repairPluginOpenClawPeerLink(targetDir) };
|
||||
}
|
||||
|
||||
// If already installed, check whether an upgrade is available
|
||||
if (existsSync(fsPath(targetManifest))) {
|
||||
if (!sourceDir) {
|
||||
return await finalizeInstalledMirror(); // no bundled source to compare, keep existing
|
||||
await syncTrustedOfficialPluginInstallRecord(pluginDirName, targetDir);
|
||||
return { installed: true }; // no bundled source to compare, keep existing
|
||||
}
|
||||
const installedVersion = readPluginVersion(targetPkgJson);
|
||||
const sourceVersion = readPluginVersion(join(sourceDir, 'package.json'));
|
||||
if (!sourceVersion || !installedVersion || sourceVersion === installedVersion) {
|
||||
return await finalizeInstalledMirror(); // same version or unable to compare
|
||||
await syncTrustedOfficialPluginInstallRecord(pluginDirName, targetDir);
|
||||
return { installed: true }; // same version or unable to compare
|
||||
}
|
||||
// Version differs — fall through to overwrite install
|
||||
logger.info(
|
||||
@@ -801,9 +767,9 @@ export async function ensurePluginInstalled(
|
||||
return { installed: false, warning: `Failed to install ${pluginLabel} plugin mirror (manifest missing).` };
|
||||
}
|
||||
fixupPluginManifest(targetDir);
|
||||
const installed = await finalizeInstalledMirror();
|
||||
await syncTrustedOfficialPluginInstallRecord(pluginDirName, targetDir);
|
||||
logger.info(`Installed ${pluginLabel} plugin from bundled mirror: ${sourceDir}`);
|
||||
return installed;
|
||||
return { installed: true };
|
||||
} catch (error) {
|
||||
const diagnostic = toErrorDiagnostic(error);
|
||||
attempts.push({ attempt, ...diagnostic });
|
||||
@@ -850,7 +816,8 @@ export async function ensurePluginInstalled(
|
||||
copyPluginFromNodeModules(npmPkgPath, targetDir, npmName);
|
||||
fixupPluginManifest(targetDir);
|
||||
if (existsSync(fsPath(join(targetDir, 'openclaw.plugin.json')))) {
|
||||
return await finalizeInstalledMirror();
|
||||
await syncTrustedOfficialPluginInstallRecord(pluginDirName, targetDir);
|
||||
return { installed: true };
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
@@ -867,7 +834,8 @@ export async function ensurePluginInstalled(
|
||||
);
|
||||
}
|
||||
} else if (existsSync(fsPath(targetManifest))) {
|
||||
return await finalizeInstalledMirror(); // same version, already installed
|
||||
await syncTrustedOfficialPluginInstallRecord(pluginDirName, targetDir);
|
||||
return { installed: true }; // same version, already installed
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -902,15 +870,15 @@ export function buildCandidateSources(pluginDirName: string): string[] {
|
||||
|
||||
// ── Per-channel plugin helpers ───────────────────────────────────────────────
|
||||
|
||||
export function ensureDingTalkPluginInstalled(): Promise<PluginInstallResult> {
|
||||
export function ensureDingTalkPluginInstalled(): Promise<{ installed: boolean; warning?: string }> {
|
||||
return ensurePluginInstalled('dingtalk', buildCandidateSources('dingtalk'), 'DingTalk');
|
||||
}
|
||||
|
||||
export function ensureWeComPluginInstalled(): Promise<PluginInstallResult> {
|
||||
export function ensureWeComPluginInstalled(): Promise<{ installed: boolean; warning?: string }> {
|
||||
return ensurePluginInstalled('wecom', buildCandidateSources('wecom'), 'WeCom');
|
||||
}
|
||||
|
||||
export function ensureFeishuPluginInstalled(): Promise<PluginInstallResult> {
|
||||
export function ensureFeishuPluginInstalled(): Promise<{ installed: boolean; warning?: string }> {
|
||||
return ensurePluginInstalled(
|
||||
'feishu-openclaw-plugin',
|
||||
buildCandidateSources('feishu-openclaw-plugin'),
|
||||
@@ -918,23 +886,25 @@ export function ensureFeishuPluginInstalled(): Promise<PluginInstallResult> {
|
||||
);
|
||||
}
|
||||
|
||||
export function ensureWeChatPluginInstalled(): Promise<PluginInstallResult> {
|
||||
|
||||
|
||||
export function ensureWeChatPluginInstalled(): Promise<{ installed: boolean; warning?: string }> {
|
||||
return ensurePluginInstalled('openclaw-weixin', buildCandidateSources('openclaw-weixin'), 'WeChat');
|
||||
}
|
||||
|
||||
export function ensureDiscordPluginInstalled(): Promise<PluginInstallResult> {
|
||||
export function ensureDiscordPluginInstalled(): Promise<{ installed: boolean; warning?: string }> {
|
||||
return ensurePluginInstalled('discord', buildCandidateSources('discord'), 'Discord');
|
||||
}
|
||||
|
||||
export function ensureQQBotPluginInstalled(): Promise<PluginInstallResult> {
|
||||
export function ensureQQBotPluginInstalled(): Promise<{ installed: boolean; warning?: string }> {
|
||||
return ensurePluginInstalled('qqbot', buildCandidateSources('qqbot'), 'QQBot');
|
||||
}
|
||||
|
||||
export function ensureWhatsAppPluginInstalled(): Promise<PluginInstallResult> {
|
||||
export function ensureWhatsAppPluginInstalled(): Promise<{ installed: boolean; warning?: string }> {
|
||||
return ensurePluginInstalled('whatsapp', buildCandidateSources('whatsapp'), 'WhatsApp');
|
||||
}
|
||||
|
||||
export function ensureClawXOpenAiImagePluginInstalled(): Promise<PluginInstallResult> {
|
||||
export function ensureClawXOpenAiImagePluginInstalled(): Promise<{ installed: boolean; warning?: string }> {
|
||||
return ensurePluginInstalled(
|
||||
'clawx-openai-image',
|
||||
buildCandidateSources('clawx-openai-image'),
|
||||
|
||||
@@ -6,7 +6,7 @@ Related scenario: `acp-chat-experience`
|
||||
|
||||
Related rules: `acp-chat-state-and-history`, `attachment-access-safety`, `renderer-main-boundary`
|
||||
|
||||
Related tasks: `acp-native-chat`, `acp-media-attachments`, `filter-openclaw-heartbeat-session`, `recover-acp-session-after-gateway-restart`
|
||||
Related tasks: `acp-native-chat`, `acp-media-attachments`, `filter-openclaw-heartbeat-session`
|
||||
|
||||
## Ownership
|
||||
|
||||
@@ -21,25 +21,17 @@ session/update -> Main routing envelope -> Renderer reducer -> timeline -> React
|
||||
|
||||
Gateway remains responsible for non-Chat capabilities. Renderer Chat does not call ordinary Gateway `chat.history` or `chat.send`, and Main has no Chat-history polling, coalescing, or backpressure specialization. Generic Gateway RPC requests retain Main-owned validation and timeout handling before direct `GatewayManager.rpc` dispatch. Restricted Gateway host-event evidence may supplement asynchronous image-generation completion, but it is not a source for ordinary Chat messages or tool history.
|
||||
|
||||
## ACP Semantic Authority
|
||||
|
||||
For every Chat semantic and context exposed by ACP, ACP is the preferred authority, not only for `session/load` history. This includes session identity and routing where applicable, workspace and execution `cwd`, prompt and timeline state, and standard resource or attachment semantics. When ACP provides the value or event, Main and Renderer must use it rather than substitute Gateway snapshots, transcript inference, local configuration, or a parallel projection.
|
||||
|
||||
An ACP bypass is allowed only when upstream has no equivalent capability. The exception must be narrow, bounded, session- and generation-scoped, and documented with its rationale, source of truth, limits, reconciliation behavior, and removal condition in a Harness reference or rule. It must never become a second semantic authority.
|
||||
|
||||
## Identity And Race Protection
|
||||
|
||||
Renderer-visible session identity is the OpenClaw Gateway session key. Main may hold a different ACP session id returned by `newSession`; it rewrites downstream routing to the matching Gateway session key. Loads on the shared ACP connection are serialized. A routing envelope carries the session key and the Main-owned generation token for the matching load or live prompt. Renderer uses a separate local request sequence to reject stale load completions; preparing a local-only session must not advance the ACP generation. Renderer ignores updates, permission requests, and asynchronous hydration results whose session or generation matches neither the selected session nor a retained live prompt. Generation is an in-memory race token rather than a durable sequence; Main may restore the previous value when a load fails, so code must compare it together with session and current-operation state rather than assume global monotonicity.
|
||||
|
||||
While `session/prompt` is pending, Main retains a bounded session-id routing context and Renderer retains that prompt's reduced timeline and original client-observed turn start in memory. This lets another page or conversation be viewed without dropping the original stream or resetting elapsed time. Returning to the live conversation reactivates its existing ACP context and restores the memory snapshot without calling `session/load`; updates received during the handoff are still generation-filtered. Prompt settlement releases both live contexts, after which returning uses ordinary ACP replay plus bounded timing metadata. This is live operation state, not a second history ledger, and it is never persisted.
|
||||
|
||||
When guarded Gateway recovery interrupts an accepted main-session run, patched OpenClaw starts a distinct recovery run and marks its Chat and agent events with the interrupted run id as `resumedFromRunId`. The reconnecting ACP bridge keeps the original prompt pending for a bounded 60-second total recovery window, adopts only that explicitly linked recovery run, resets per-run text and tool state, and rebinds cancellation to the new run id. The initial disconnect check still occurs after 5 seconds: prompts whose send was never acknowledged reject then, while acknowledged prompts receive the remaining 55 seconds for Gateway startup backoff and recovery dispatch. ACP also subscribes to session tool events after reconnect, and Gateway mirrors visible recovery tools to that exact session subscription with lineage intact, so recovered tool cards preserve the surrounding text boundaries. Renderer does not reload the session or use Gateway runtime identity for this flow; normal ACP generation, session, and workspace guards remain authoritative.
|
||||
|
||||
`messageId` and `toolCallId` are opaque identities within one loaded timeline. They are not durable UI identities across loads. Timeline sequence values and DOM anchors are also local to the active snapshot.
|
||||
|
||||
## History Authority
|
||||
|
||||
ACP `session/load` replay is the primary source of Chat history. ClawX does not persist an ACP ledger, reduced timeline, replay cache, or reconstructed tool history. Full structured replay restores recorded tools and file activity. When that ledger is unavailable, OpenClaw's ACP adapter maps persisted transcript `toolCall` and `toolResult` records to native ACP tool updates in transcript order, preserving assistant text segments on either side; this is upstream ACP replay, not a ClawX transcript supplement or inference path.
|
||||
ACP `session/load` replay is the primary source of Chat history. ClawX does not persist an ACP ledger, reduced timeline, replay cache, or reconstructed tool history. Full structured replay can restore tools and file activity; transcript-only fallback must not invent them.
|
||||
|
||||
OpenClaw emits replay through ordinary `session/update` notifications and completes the replay before `session/load` returns. Main collects those raw notifications for the active load generation and returns them with the load result instead of forwarding them incrementally. Renderer temporarily groups generation-matching host events that arrive during the IPC result handoff, then runs the normal reducer over the combined batch and publishes the resulting timeline in one state update. This is an in-flight transaction buffer only, not a history cache; after load, each live update continues through the normal host-event route and is applied immediately without a Renderer batching timer. Permission requests are accepted only after the current loaded session starts a prompt, preventing load-time or handoff requests from creating invisible waiters.
|
||||
|
||||
@@ -80,7 +72,7 @@ The protocol timeline remains flat. `src/lib/acp/timeline-groups.ts` derives dis
|
||||
- Assistant-side items before the first user item still form a visible assistant turn.
|
||||
- Grouping never infers ownership from `messageId`, `toolCallId`, `_meta`, or synthetic persisted turn ids.
|
||||
|
||||
An assistant turn has one identity column and one copy action. Copy includes textual assistant segments and excludes tool output. Its footer may show localized whole-turn metadata: live duration runs from optimistic send until prompt settlement, while historical duration runs from the matched transcript user record to the latest assistant or tool-result record before the next real user. Missing or ambiguous timing stays hidden. Tool cards render inline in original order, preserve preformatted whitespace, auto-collapse one second after live completion, respect manual override, and start collapsed when historical and completed.
|
||||
An assistant turn has one identity column and one copy action. Copy includes textual assistant segments and excludes tool output. Its footer may show localized whole-turn metadata: live duration runs from optimistic send until prompt settlement, while historical duration runs from the matched transcript user record to the latest assistant or tool-result record before the next real user. Missing or ambiguous timing stays hidden. Tool items retain their original timeline order and preserve preformatted whitespace, but a live assistant turn hides its tool presentation while the prompt is still running. When the prompt settles, consecutive tool runs appear as collapsed groups and a single tool appears with its details collapsed; users can then expand the group or card. Historical completed tools also start collapsed. This is presentation-only state: hidden or collapsed tools remain in the in-memory timeline, and tool events without live turn timing retain the standalone fallback behavior.
|
||||
|
||||
## Attachments
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# Electron E2E Parallelism
|
||||
|
||||
ClawX launches one Electron process per Playwright test with a test-scoped HOME and user-data directory. Ordinary specs can therefore run in separate workers without sharing application stores or OpenClaw files.
|
||||
|
||||
The Playwright project graph has three ordered lanes:
|
||||
|
||||
1. `exclusive` runs tests tagged `@exclusive` with one worker.
|
||||
2. `parallel` runs all ordinary functional tests with the configured worker count after `exclusive` succeeds.
|
||||
3. `performance` runs tests tagged `@performance` with one worker after functional tests finish.
|
||||
|
||||
Real clipboard tests are exclusive because Electron renderer instances read and write the same OS clipboard. Renderer performance tests run last because concurrent Electron processes distort CPU, GPU, frame-pacing, and elapsed-time evidence even when their files are otherwise isolated. `test.describe.configure({ mode: 'serial' })` is not sufficient for either case because it does not prevent another spec file or project from running at the same time.
|
||||
|
||||
New tests are parallel by default. A test that uses an OS-global resource must import and apply `E2E_EXCLUSIVE_TAG`; a host performance profile must use `E2E_PERFORMANCE_TAG`. Extend `tests/unit/e2e-parallel-policy.test.ts` when another recognizable global API is introduced. No static check can identify every possible external side effect, so reviewers must classify tests that use native dialogs, keychains, fixed ports, fixed writable paths, external runtimes, or other machine-global state.
|
||||
|
||||
Use `CLAWX_E2E_WORKERS` to override the ordinary worker count on constrained or high-capacity machines. Playwright project dependencies make a directly filtered ordinary spec run the exclusive prerequisite first; add `--project=parallel --no-deps` when a focused command intentionally needs only an audited ordinary spec. `pnpm run perf:chat` selects the performance project without running its dependencies.
|
||||
@@ -1,13 +1,13 @@
|
||||
# OpenClaw Config Delivery
|
||||
|
||||
ClawX bundles OpenClaw 2026.7.1-2. OpenClaw owns the field-level decision between a no-op snapshot update, hot application, subsystem restart, and in-process Gateway restart.
|
||||
ClawX bundles OpenClaw 2026.7.1. OpenClaw owns the field-level decision between a no-op snapshot update, hot application, subsystem restart, and in-process Gateway restart.
|
||||
|
||||
Provider, Agent, Channel, skill, proxy, image-generation, and plugin-install helpers express config changes as mutators. One Main-owned coordinator owns selection of the authoritative baseline and the commit:
|
||||
|
||||
1. If Gateway is running, call `config.get` and require its runtime-shaped `config` object and `hash`. The coordinator accepts `raw` only as a compatibility fallback for older responses.
|
||||
2. Clone the runtime-shaped config, apply the mutator, and call `config.set` with the serialized result and `baseHash: hash`. Using source-shaped `raw` as the preferred baseline can misalign redacted secret paths with OpenClaw's runtime-shaped restore baseline.
|
||||
3. Retry one base-hash conflict from a fresh `config.get`; fail other RPC errors without writing around the running Gateway. If `config.set` durably wrote the exact requested snapshot but its response was lost when OpenClaw began a native code-1012 reload, verify that persisted snapshot after Gateway leaves running state and accept the existing commit without replaying it.
|
||||
4. Treat success as converged and do not send `SIGUSR1` or schedule a redundant ClawX process replacement.
|
||||
3. Retry one base-hash conflict from a fresh `config.get`; fail other RPC errors without writing around the running Gateway.
|
||||
4. Treat success as converged and do not send `SIGUSR1` or replace the process.
|
||||
5. If Gateway is stopped or starting, apply the same mutator to `resolveOpenClawConfigPath()` under the shared config lock and do not start the Gateway.
|
||||
|
||||
This is not a write-then-notify design. No provider, Agent, Channel, skill, proxy, image-generation, or plugin-install helper may write the active config independently. The coordinator prevents a locally read stale snapshot from overwriting concurrent Gateway or CLI config changes.
|
||||
@@ -16,8 +16,6 @@ Gateway WebSocket tracing must redact the complete serialized `raw` payload for
|
||||
|
||||
Coordinator-backed reads follow the same authority rule: prefer the runtime-shaped `config.get.config` object while Gateway is running and use JSON5 file parsing while it is not. Compound views derive all config-backed fields from one snapshot.
|
||||
|
||||
OpenClaw 2026.7.1-2 keeps auth-profile SQLite snapshots in memory. After a completed auth-store write batch, ClawX calls `secrets.reload` once when Gateway is running. `config.set` does not replace this refresh. Agent `models.json` needs no explicit RPC because OpenClaw re-reads it when its file fingerprint changes.
|
||||
|
||||
Before launch, upgrade compatibility cleanup checks the canonical `state/openclaw.sqlite` update-check row. If it exists, the SQLite row is authoritative and any legacy root `update-check.json` is moved with restrictive permissions under `backups/`; otherwise the JSON remains in place for OpenClaw to import. This cleanup runs after the one-time upgrade snapshot and prevents harmless updater-bookkeeping differences from blocking Gateway readiness or triggering an ineffective doctor retry. The snapshot is removed after either the native ready event or a successful RPC-router readiness fallback, covering the race where a fast Gateway emits readiness before ClawX attaches its WebSocket client.
|
||||
OpenClaw 2026.7.1 keeps auth-profile SQLite snapshots in memory. After a completed auth-store write batch, ClawX calls `secrets.reload` once when Gateway is running. `config.set` does not replace this refresh. Agent `models.json` needs no explicit RPC because OpenClaw re-reads it when its file fingerprint changes.
|
||||
|
||||
Full ClawX process replacement remains necessary after a successful coordinator commit when values are injected only at process creation, including proxy environment changes, or for explicit manual lifecycle and health/crash recovery. OpenClaw config categories must not be duplicated as a ClawX restart whitelist.
|
||||
|
||||
@@ -8,10 +8,10 @@ appliesTo:
|
||||
- gateway-backend-communication
|
||||
---
|
||||
|
||||
Main owns ACP process, SDK, routing lifecycle, and serialization of operations on the shared ACP connection; Renderer owns semantic reduction into an in-memory timeline. The local ACP child receives the authoritative `gatewayToken` from Electron store through `OPENCLAW_GATEWAY_TOKEN`; it must not rely on a separately resolved OpenClaw config credential or expose the token through CLI arguments or Renderer state. Notifications emitted during `session/load` are returned as one generation-scoped raw batch and reduced in one Renderer state commit. Renderer may temporarily buffer matching host events during the IPC result handoff, while each ordinary live prompt update continues through host events and is applied immediately without a Renderer batching timer. A pending prompt may retain a bounded Main routing context and Renderer timeline snapshot so navigation cannot drop its stream; those contexts must be keyed by session and generation, remain memory-only, and be released when the prompt settles. Gateway restart recovery must use explicit source-run lineage from trusted OpenClaw recovery provenance; ACP may adopt a new run only when `resumedFromRunId` matches its pending prompt, and Renderer must not reload a session based on Gateway runtime identity. An acknowledged prompt may remain pending for at most 60 seconds after disconnect so Gateway startup backoff and restart recovery can complete, while an unacknowledged send retains the 5-second deadline. Permission requests are interactive only for an active prompt. Stale session generations are ignored, and ClawX does not persist a second ACP ledger or reduced Chat history.
|
||||
Main owns ACP process, SDK, routing lifecycle, and serialization of operations on the shared ACP connection; Renderer owns semantic reduction into an in-memory timeline. Renderer presentation may use live whole-turn timing to hide tool UI while a prompt is running and reveal every tool collapsed when that prompt settles, but it must not remove, reorder, or delay reduction of the underlying tool items. Manual post-settlement expansion remains UI-only state, and historical completed tools also start collapsed. Notifications emitted during `session/load` are returned as one generation-scoped raw batch and reduced in one Renderer state commit. Renderer may temporarily buffer matching host events during the IPC result handoff, while each ordinary live prompt update continues through host events and is applied immediately without a Renderer batching timer. A pending prompt may retain a bounded Main routing context and Renderer timeline snapshot so navigation cannot drop its stream; those contexts must be keyed by session and generation, remain memory-only, and be released when the prompt settles. Permission requests are interactive only for an active prompt. Stale session generations are ignored, and ClawX does not persist a second ACP ledger or reduced Chat history.
|
||||
|
||||
ACP is the preferred authority for every Chat semantic it exposes, including session routing, workspace and execution `cwd`, prompt/timeline state, and standard resource or attachment semantics. If ACP provides the value or event, implementations must not replace it with Gateway snapshots, transcript inference, local configuration, or a parallel projection. A bypass is permitted only when upstream ACP has no equivalent; it must be narrow, bounded, session- and generation-scoped, and documented with its rationale, source, limits, reconciliation behavior, and removal condition in a relevant Harness reference or rule.
|
||||
Current-session reasoning effort is Gateway session metadata, not ACP timeline state. The Chat picker must use the selected row's `thinkingLevels` as its capability source, `thinkingDefault` as the effective fallback, and `thinkingLevel` as the explicit override. For a Renderer-local draft that Gateway has not listed yet, it may temporarily use the agent-scoped `sessions.list.defaults` thinking metadata returned by the same catalog request; persisted rows take precedence as soon as they exist. Its effort submenu directly lists the advertised members of the product ladder `off`, `low`, `medium`, and `high` in that order, with no inherited option and no separate Thinking toggle. Selecting `off` patches that explicit level through `sessions.patch`; `null` remains reserved for clearing the override. Renderer must not derive support from provider or model-name patterns, and sending must wait until an in-flight picker patch settles. Once selected, the exact explicit session level must also be forwarded in ACP prompt metadata; a session without an explicit override must omit that prompt field and continue inheriting from Gateway. If ACP reports a cancelled prompt without a matching user-initiated cancel, Main must return a typed prompt-aborted failure so Renderer shows a localized retryable error instead of silently completing an empty turn.
|
||||
|
||||
ACP replay is the primary history authority. OpenClaw's native ACP adapter may reconstruct persisted transcript `toolCall` and `toolResult` records as standard `tool_call` and `tool_call_update` events when its structured event ledger is unavailable; ClawX consumes those events normally and does not perform that reconstruction itself. The only approved ClawX transcript-derived content supplements are best-effort recovery of asynchronous image-generation completions with proven `image_generate` context and recovery of explicit line-leading assistant OpenClaw `MEDIA:` attachment directives omitted by ACP. The general attachment exception does not require image-generation context, but it recovers only attachment references. When ACP replay for a cron session is completely empty, scheduled-task prompt and completion summaries may instead come from Main's typed cron-history host API. This cron exception must be anchored by Gateway `cron.runs` (with a Main-owned legacy file fallback), be generation-scoped and in memory, and never replace or duplicate non-empty ACP replay. When an anchored run summary carries OpenClaw's bounded-summary ellipsis, Main may recover that run's final assistant text from the identified run transcript only when it is longer and shares the complete persisted summary prefix; missing, mismatched, or unbounded summaries remain unchanged. A separate metadata-only supplement may annotate an ACP-replayed assistant turn with whole-turn duration because ACP `session/load` omits the original event timestamps; it cannot create turns or content. These exceptions remain marked and in memory; do not generalize them to bare paths, surrounding transcript prose, arbitrary ordinary messages, tool cards, plans, permissions, thoughts, file activity, or any parallel persisted history.
|
||||
ACP replay is the primary history authority. The only approved transcript-derived content supplements are best-effort recovery of asynchronous image-generation completions with proven `image_generate` context and recovery of explicit line-leading assistant OpenClaw `MEDIA:` attachment directives omitted by ACP. The general attachment exception does not require image-generation context, but it recovers only attachment references. When ACP replay for a cron session is completely empty, scheduled-task prompt and completion summaries may instead come from Main's typed cron-history host API. This cron exception must be anchored by Gateway `cron.runs` (with a Main-owned legacy file fallback), be generation-scoped and in memory, and never replace or duplicate non-empty ACP replay. When an anchored run summary carries OpenClaw's bounded-summary ellipsis, Main may recover that run's final assistant text from the identified run transcript only when it is longer and shares the complete persisted summary prefix; missing, mismatched, or unbounded summaries remain unchanged. A separate metadata-only supplement may annotate an ACP-replayed assistant turn with whole-turn duration because ACP `session/load` omits the original event timestamps; it cannot create turns or content. These exceptions remain marked and in memory; do not generalize them to bare paths, surrounding transcript prose, arbitrary ordinary messages, tool cards, plans, permissions, thoughts, file activity, or any parallel persisted history.
|
||||
|
||||
Historical transcript reads are limited to the newest `1000` message records. A successful live prompt reads content immediately and retries exactly once after `1500 ms`. General attachment and timing alignment treat history as a suffix and match the binary-free OpenClaw prompt-text projection of structured ACP user blocks by duplicate occurrence from the tail; they must not parse or globally remove user-authored resource marker text. Attachment-only empty projections remain eligible, and live content alignment also requires the current optimistic user identity. Every asynchronous result must retain the same active session, generation, supplement operation and attempt, and live turn where applicable. Unmatched, ambiguous, superseded, or stale work cannot mutate the timeline or timing annotations.
|
||||
|
||||
@@ -16,7 +16,6 @@ Rules:
|
||||
- disabling a bundled plugin is required when removing it from an allowlist is not sufficient to stop runtime loading
|
||||
- stale plugin registrations for unconfigured capabilities must be removed during sanitize or recovery paths
|
||||
- ClawX must include `web_search` in both `tools.deny` and `gateway.tools.deny`; existing deny entries remain user-owned and browser automation plus `web_fetch` remain available
|
||||
- ClawX must include `gateway`, `nodes`, `create_goal`, `get_goal`, and `update_goal` in both deny lists without blocking application-owned Gateway RPCs; it must not implicitly deny messaging, session orchestration, or agent discovery tools
|
||||
- when no embedding credentials or user-owned memory-search config exist, preserve `memory_search` through OpenClaw's explicit FTS-only provider instead of disabling the tool
|
||||
- migrations may replace only the exact legacy ClawX-managed memory-search default, must run at most once, and must preserve later user opt-outs
|
||||
- tests for config rewrites should assert the final active config, not only intermediate helper output
|
||||
|
||||
@@ -8,10 +8,6 @@ appliesTo:
|
||||
|
||||
When channel plugin ownership changes between bundled OpenClaw extensions and external `~/.openclaw/extensions/*` installs, ClawX must normalize configuration to one active plugin identity per channel.
|
||||
|
||||
The ClawX channel configuration catalog is intentionally limited to `telegram`, `discord`, `whatsapp`, `wechat`, `dingtalk`, `feishu`, `wecom`, and `qqbot`. OpenClaw may report other channel ids, but the ClawX Channels page must not expose them as configurable or editable channel groups. Filtering an unsupported runtime channel is presentation-only and must not delete or rewrite that channel's underlying OpenClaw configuration.
|
||||
|
||||
Channel credentials and account maps must remain under `channels.<id>`; `plugins.entries.<id>` is activation metadata and must not contain ClawX-generated `accounts` or `defaultAccount` fields. Discord, WhatsApp, and QQBot are external plugins in the pinned OpenClaw runtime and must retain explicit `plugins.allow` and `{ enabled }` entries. Saving changed configuration for a supported external plugin channel while Gateway is running must use the coordinator-owned `config.set` reload without scheduling a second ClawX full restart when OpenClaw peer link repair succeeds. When peer link repair fails after plugin install, Main must schedule the guarded full restart after the config commit instead of relying on the native reload alone. A no-change retry must still start the guarded full restart path after the scoped-binding commit so a newly copied or previously undiscovered plugin is loaded. Successful WeChat QR completion must likewise leave plugin activation on a single lifecycle path. The host save response may return while activation is still pending, provided it explicitly reports that state and failures are caught and surfaced through normal Gateway status/logging. If `config.set` durably commits before its response is lost to a native code-1012 reload, Main may verify that exact persisted config and treat the transaction as committed; it must not perform an out-of-band replay.
|
||||
|
||||
For Feishu/Lark specifically:
|
||||
|
||||
- a configured Feishu channel must not leave both the bundled `feishu` plugin and the legacy external `openclaw-lark` / `feishu-openclaw-plugin` registrations active at the same time
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
id: e2e-parallel-isolation
|
||||
title: E2E Parallel Isolation
|
||||
type: ai-coding-rule
|
||||
appliesTo:
|
||||
- gateway-backend-communication
|
||||
requiredProfiles:
|
||||
- fast
|
||||
- e2e
|
||||
---
|
||||
|
||||
Electron E2E tests are parallel by default because each test owns its HOME, OpenClaw state directory, Electron user-data directory, and Host API configuration. Keep those fixtures test-scoped.
|
||||
|
||||
Tests that mutate OS-global state must use `E2E_EXCLUSIVE_TAG`. Tests that profile shared host CPU, GPU, display, or frame pacing must use `E2E_PERFORMANCE_TAG`. Do not use Playwright serial mode as a cross-file mutex; serial mode only orders tests within its own group.
|
||||
|
||||
When adding another global resource, extend the automated policy check where the resource has a recognizable API. Unknown external resources still require reviewer classification.
|
||||
|
||||
The project graph, environment isolation, and validation commands are documented in `harness/reference/e2e-parallelism.md`.
|
||||
@@ -11,8 +11,8 @@ requiredTests:
|
||||
|
||||
WebSocket heartbeat misses are availability evidence. A short sequence is not proof that the local Gateway process is dead because long-running model, tool, compaction, and scheduled work may temporarily delay Gateway control-plane responses.
|
||||
|
||||
Misses one through three must remain diagnostic-only: they must not terminate the socket, kill the owned Gateway process, or request `GatewayManager.restart`. A pong or any incoming Gateway message resets the sequence.
|
||||
Misses one through nine must remain diagnostic-only: they must not terminate the socket, kill the owned Gateway process, or request `GatewayManager.restart`. A pong or any incoming Gateway message resets the sequence.
|
||||
|
||||
After four consecutive missed responses, ClawX may treat the Gateway as persistently unresponsive and request the guarded `GatewayManager.restart` path only when auto-recovery is enabled and lifecycle state is still `running`. The heartbeat callback must not directly terminate the socket or process, and it must request recovery at most once per uninterrupted miss sequence.
|
||||
After ten consecutive missed responses, ClawX may treat the Gateway as persistently unresponsive and request the guarded `GatewayManager.restart` path only when auto-recovery is enabled and lifecycle state is still `running`. The heartbeat callback must not directly terminate the socket or process, and it must request recovery at most once per uninterrupted miss sequence.
|
||||
|
||||
Authoritative child-process exit, WebSocket close, and Gateway restart close code 1012 signals retain their existing automatic lifecycle paths. Explicit user restart remains available.
|
||||
|
||||
@@ -14,7 +14,7 @@ ClawX must defer runtime config planning to the bundled OpenClaw Gateway.
|
||||
|
||||
The Main-owned config coordinator must own the entire read-modify-write transaction. Production helpers must not write the active OpenClaw config and then notify another layer afterward.
|
||||
|
||||
When the Gateway is running, the coordinator prefers the runtime-shaped `config.get.config` object as the mutation baseline, applies the caller's mutator, and commits through `config.set` with the returned `hash` as `baseHash`. Source-shaped `raw` is only a compatibility fallback because its redacted secret paths may not align with OpenClaw's write-side runtime snapshot. A successful mutation must not be followed by `SIGUSR1` or a redundant ClawX process restart. Base-hash conflicts retry once from a new snapshot; other RPC failures fail closed instead of performing an out-of-band file write. When `config.set` itself durably writes the exact requested config and then a native code-1012 reload drops its response, the coordinator may verify that persisted commit after Gateway leaves running state and accept it without replaying or rewriting the mutation.
|
||||
When the Gateway is running, the coordinator prefers the runtime-shaped `config.get.config` object as the mutation baseline, applies the caller's mutator, and commits through `config.set` with the returned `hash` as `baseHash`. Source-shaped `raw` is only a compatibility fallback because its redacted secret paths may not align with OpenClaw's write-side runtime snapshot. A successful mutation must not be followed by `SIGUSR1` or a ClawX process restart. Base-hash conflicts retry once from a new snapshot; other RPC failures fail closed instead of performing an out-of-band file write.
|
||||
|
||||
Coordinator mutators are replayable transformations. They must not perform filesystem writes, SQLite writes, settings writes, lifecycle actions, or other non-idempotent external effects; preload required external inputs before entering the mutator and perform follow-up effects only after a successful commit.
|
||||
|
||||
|
||||
@@ -23,3 +23,9 @@ existing rows missing both `contextWindow` and `contextTokens` may be
|
||||
backfilled with that default. Rows that already declare either field are
|
||||
user-owned and must never be modified, and non-`custom-` provider entries are
|
||||
never backfilled.
|
||||
|
||||
Custom-provider primary model rows receive `reasoning: true` and the fixed
|
||||
effort ladder `compat.supportedReasoningEfforts: ["low","medium","high"]`
|
||||
so Chat can offer session thinking controls. Provider settings must not expose
|
||||
enable-reasoning controls, and model-name inference must not invent a different
|
||||
effort list. Sync must preserve unrelated compat keys while writing this ladder.
|
||||
|
||||
@@ -59,7 +59,7 @@ requiredRules:
|
||||
- docs-sync
|
||||
---
|
||||
|
||||
ACP Chat covers session load, prompt, cancel, permission, replay, timeline reduction, assistant-turn presentation and whole-turn duration, standard ACP attachments, bounded generated-media and OpenClaw MEDIA compatibility, and Chat-specific diagnostics. The user-visible attachment flow includes attachment-scoped preview, system open, selected-application open, reveal actions, and a first-position built-in Preview action for eligible local HTML, with platform discovery limited to macOS and Windows. Authorized local DOCX/PPTX attachments within the Office limit use scoped Preview; remote, legacy, and over-limit Office attachments retain scoped system/external-open behavior. User-selected directories remain system-open-only targets: Main may open the directory after session-scoped revalidation, but directory contents are not read, enumerated, previewed, or exposed to Open With.
|
||||
ACP Chat covers session load, prompt, cancel, permission, replay, timeline reduction, assistant-turn presentation and whole-turn duration, live tool visibility and completed-turn collapse behavior, standard ACP attachments, bounded generated-media and OpenClaw MEDIA compatibility, and Chat-specific diagnostics. The user-visible attachment flow includes attachment-scoped preview, system open, selected-application open, reveal actions, and a first-position built-in Preview action for eligible local HTML, with platform discovery limited to macOS and Windows. Authorized local DOCX/PPTX attachments within the Office limit use scoped Preview; remote, legacy, and over-limit Office attachments retain scoped system/external-open behavior. User-selected directories remain system-open-only targets: Main may open the directory after session-scoped revalidation, but directory contents are not read, enumerated, previewed, or exposed to Open With.
|
||||
|
||||
Main owns ACP transport, routing, transcript retrieval and timing extraction, workspace grants, and session/generation-scoped attachment authorization. Renderer owns the in-memory timeline, bounded compatibility and timing alignment, attachment presentation, and display grouping, including user-image thumbnails and user-selected source-path labels. ACP replay remains authoritative for historical turns and content; transcript-derived timing may only annotate an unambiguously matched ACP turn. Standard ACP content remains preferred over compatibility projections, and incidental tool paths never enter the attachment pipeline.
|
||||
|
||||
|
||||
@@ -60,7 +60,6 @@ requiredRules:
|
||||
- provider-model-selection-authority
|
||||
- sidebar-session-attention-authority
|
||||
- web-browser-security-and-lifecycle
|
||||
- e2e-parallel-isolation
|
||||
- comms-regression
|
||||
- docs-sync
|
||||
forbiddenPatterns:
|
||||
@@ -91,12 +90,14 @@ Typed generic Gateway RPC requests are validated by `electron/services/gateway-a
|
||||
|
||||
Channel/plugin migration behavior is also part of this scenario when ClawX rewrites OpenClaw config before Gateway launch. Upgrades must preserve single-owner channel registration for migrated plugin-backed channels such as Feishu/Lark.
|
||||
|
||||
ClawX's prelaunch config sanitizer also owns desktop tool policy. It must keep `web_search` in both the agent-level and Gateway-level deny lists without replacing existing deny entries or disabling managed browser automation and `web_fetch`. It must also deny the agent-facing `gateway`, `nodes`, `create_goal`, `get_goal`, and `update_goal` tools at both layers while preserving application-owned Gateway RPCs. Messaging, session orchestration, and agent discovery tools remain available unless another explicit policy denies them.
|
||||
ClawX's prelaunch config sanitizer also owns desktop tool policy. It must keep `web_search` in both the agent-level and Gateway-level deny lists without replacing existing deny entries or disabling managed browser automation and `web_fetch`.
|
||||
|
||||
Scheduled-task history is Main-owned backend data. Current OpenClaw versions must be queried through the Gateway `cron.runs` RPC; direct run-log file reads are allowed only as a compatibility fallback for older file-backed runtimes. When a run's bounded summary ends with OpenClaw's truncation ellipsis, Main may recover the complete final assistant reply from the run transcript identified by that `cron.runs` entry, but only when the transcript reply is longer and shares the entire summary prefix. When a cron base session has no ACP replay, Renderer may project that typed host result into a generation-scoped, in-memory historical ACP timeline, but must not replace or duplicate non-empty ACP replay.
|
||||
|
||||
The local HTML Preview privileged bridge is also Main-owned: Renderer may load a validated local HTML file or open that current file externally through the typed Host API. The guest is an implementation detail of the existing `preview` tab; there is no `web-browser` artifact tab or general address navigation. The durable guest contract is `harness/reference/web-browser.md`.
|
||||
|
||||
Gateway session-catalog subscription, normalization, ordered list/event replay, attention transitions, and reconnect recovery are documented in `harness/reference/sidebar-session-attention.md`. Electron test-process isolation and global-resource scheduling are documented in `harness/reference/e2e-parallelism.md`.
|
||||
Gateway session-catalog subscription, normalization, ordered list/event replay, attention transitions, and reconnect recovery are documented in `harness/reference/sidebar-session-attention.md`.
|
||||
|
||||
Gateway WebSocket heartbeat misses are diagnostic availability signals for the first three consecutive misses and must not interrupt long-running work during that window. A pong or any incoming Gateway message resets the sequence. On the fourth consecutive miss, Main may request the guarded Gateway restart path when auto-recovery is enabled and lifecycle state is still running; the heartbeat callback must not directly terminate the socket or process. Authoritative process-exit and socket-close signals retain their existing automatic lifecycle paths.
|
||||
Gateway session rows are also authoritative for Chat reasoning-effort controls. Renderer may project `thinkingLevel`, `thinkingLevels`, and `thinkingDefault` into the session catalog and may update the explicit current-session override only through the Main-owned `sessions.patch` RPC boundary. The selected explicit override is also copied unchanged into the typed ACP prompt metadata so execution cannot silently use a different prompt-level effort; inherited sessions omit that field. A local draft missing from the catalog may use only the agent-scoped `sessions.list.defaults` thinking metadata from that request until Gateway returns a persisted row. Renderer must not invent thinking levels from model names. Custom-provider primary models are synced with the fixed `low`, `medium`, and `high` OpenClaw reasoning ladder so Gateway can advertise those levels to Chat; the picker adds advertised `off` as the direct way to disable reasoning and does not expose a separate toggle. Provider settings do not expose enable-reasoning controls. An ACP prompt that is unexpectedly reported as cancelled must cross the typed host boundary as a prompt-aborted failure and produce a localized retryable Renderer error; an explicit user cancellation remains a successful stop action.
|
||||
|
||||
Gateway WebSocket heartbeat misses are diagnostic availability signals for the first nine consecutive misses and must not interrupt long-running work during that window. A pong or any incoming Gateway message resets the sequence. On the tenth consecutive miss, Main may request the guarded Gateway restart path when auto-recovery is enabled and lifecycle state is still running; the heartbeat callback must not directly terminate the socket or process. Authoritative process-exit and socket-close signals retain their existing automatic lifecycle paths.
|
||||
|
||||
@@ -33,7 +33,7 @@ ClawX should prefer OpenClaw-native signals over stderr string matching:
|
||||
|
||||
stderr is supporting evidence only. It should not be the primary source for deciding whether the Gateway is ready, blocked, or should be restarted.
|
||||
|
||||
WebSocket heartbeat misses show that the Gateway control plane did not answer within the observation window. The first three consecutive misses remain diagnostic-only so transient pong delays do not interrupt long-running work. A pong or any incoming message resets the sequence. A fourth consecutive miss marks persistent unresponsiveness and may request the guarded Gateway restart path when auto-recovery is enabled and lifecycle state is still running. Process exit and socket close retain their existing automatic recovery paths.
|
||||
WebSocket heartbeat misses show that the Gateway control plane did not answer within the observation window. The first nine consecutive misses remain diagnostic-only so transient pong delays do not interrupt long-running work. A pong or any incoming message resets the sequence. A tenth consecutive miss marks persistent unresponsiveness and may request the guarded Gateway restart path when auto-recovery is enabled and lifecycle state is still running. Process exit and socket close retain their existing automatic recovery paths.
|
||||
|
||||
## Failure Shape
|
||||
|
||||
@@ -278,7 +278,7 @@ pnpm exec openclaw gateway call status >/tmp/clawx-status.json
|
||||
- `health` and `status` are captured in Gateway diagnostics when available.
|
||||
- Memory doctor calls return when the memory capability is available.
|
||||
- `doctor.memory.*` and `channels.status` failures degrade their capability only and do not trigger Gateway restart.
|
||||
- The first three consecutive heartbeat misses do not replace the Gateway process; the fourth records unresponsive diagnostics and requests one guarded restart when lifecycle auto-recovery is allowed.
|
||||
- The first nine consecutive heartbeat misses do not replace the Gateway process; the tenth records unresponsive diagnostics and requests one guarded restart when lifecycle auto-recovery is allowed.
|
||||
- Logs no longer repeat stale runtime cache or escaped managed-skill symlink warnings for entries ClawX can safely clean.
|
||||
|
||||
## Required Regression Coverage
|
||||
|
||||
@@ -43,7 +43,6 @@ touchedAreas:
|
||||
- README.ja-JP.md
|
||||
expectedUserBehavior:
|
||||
- Opening a Chat session loads history through ACP session/load replay.
|
||||
- The ACP child receives the authoritative ClawX Gateway token through its private process environment, so history replay does not depend on a separately resolved config token.
|
||||
- Sending a Chat prompt uses ACP session/prompt, shows an optimistic user segment, and coalesces it with the ACP user echo.
|
||||
- Thinking, tool calls, permission requests, plans, generated files, and generated images appear as inline timeline blocks in ACP event order.
|
||||
- Renderer does not call Gateway HTTP or WebSocket endpoints directly.
|
||||
@@ -73,7 +72,6 @@ requiredTests:
|
||||
- pnpm run comms:compare
|
||||
acceptance:
|
||||
- Main starts and reuses openclaw acp through a spawn-safe CLI spec and @agentclientprotocol/sdk ClientSideConnection.
|
||||
- Main passes the Electron-store Gateway token to the local ACP child with `OPENCLAW_GATEWAY_TOKEN`, without placing the token in CLI arguments or Renderer state.
|
||||
- Main forwards ACP SessionNotification envelopes and permission request envelopes without translating text, thinking, tools, or media into legacy Chat events.
|
||||
- Renderer reduces ACP notifications into an in-memory ordered timeline.
|
||||
- No ClawX ACP replay ledger, Chat history cache, or reduced timeline persistence is introduced.
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
id: acp-tool-card-visibility
|
||||
title: Hide live ACP tool activity and collapse completed turns
|
||||
scenario: acp-chat-experience
|
||||
taskType: renderer-ui
|
||||
intent: Keep tool execution out of the live conversation display, then reveal every tool collapsed after the assistant response settles.
|
||||
touchedAreas:
|
||||
- harness/specs/tasks/acp-tool-card-visibility.md
|
||||
- harness/specs/scenarios/acp-chat-experience.md
|
||||
- harness/specs/rules/acp-chat-state-and-history.md
|
||||
- harness/reference/acp-chat.md
|
||||
- src/pages/Chat/AcpAssistantTurn.tsx
|
||||
- src/pages/Chat/AcpToolCallCard.tsx
|
||||
- src/pages/Chat/AcpToolCallsGroup.tsx
|
||||
- tests/unit/acp-chat-components.test.tsx
|
||||
- tests/e2e/chat-acp-inline-timeline.spec.ts
|
||||
- README.md
|
||||
- README.zh-CN.md
|
||||
- README.ja-JP.md
|
||||
expectedUserBehavior:
|
||||
- Tool UI belonging to a live assistant turn is not shown while that turn is running.
|
||||
- When the assistant response settles, consecutive tool calls appear as a collapsed group and a single tool appears with collapsed details.
|
||||
- Users can expand completed tool groups or individual cards after they appear.
|
||||
- Historical completed tools remain collapsed by default.
|
||||
- Tool events without live whole-turn timing retain the existing standalone rendering fallback.
|
||||
requiredProfiles:
|
||||
- fast
|
||||
- e2e
|
||||
requiredRules:
|
||||
- acp-chat-state-and-history
|
||||
- ui-i18n-design-tokens
|
||||
- electron-rendering-performance
|
||||
- docs-sync
|
||||
requiredTests:
|
||||
- pnpm exec vitest run tests/unit/acp-chat-components.test.tsx
|
||||
- pnpm run typecheck
|
||||
- pnpm run lint:check
|
||||
- pnpm run build:vite
|
||||
- pnpm exec playwright test tests/e2e/chat-acp-inline-timeline.spec.ts
|
||||
- pnpm harness validate --spec harness/specs/tasks/acp-tool-card-visibility.md
|
||||
- pnpm harness run --spec harness/specs/tasks/acp-tool-card-visibility.md
|
||||
acceptance:
|
||||
- Live visibility is derived from the existing running whole-turn timing for the owning user message; no transport, reducer, or persisted history state is added.
|
||||
- Hiding live tool UI does not remove, reorder, or delay its timeline item, output, attachment extraction, or file-activity projection.
|
||||
- A settled live turn mounts consecutive tool calls as one collapsed group and mounts a single tool with collapsed details immediately.
|
||||
- Manual expansion affects only the selected completed group or card.
|
||||
- Historical completed tools preserve their existing collapsed-first behavior.
|
||||
- Focused unit, Electron E2E, typecheck, lint, Vite build, harness validation, and synchronized README checks pass.
|
||||
docs:
|
||||
required: true
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
This task changes only ACP Chat presentation. The existing live whole-turn timing determines whether a turn is still running; the flat timeline remains authoritative and continues to reduce every tool update immediately.
|
||||
|
||||
## Out Of Scope
|
||||
|
||||
- Hiding permission prompts, thoughts, plans, attachments, or file-activity controls.
|
||||
- Changing ACP transport, event reduction, tool status, or history replay.
|
||||
- Adding a global user preference for tool visibility.
|
||||
@@ -0,0 +1,106 @@
|
||||
---
|
||||
id: chat-session-reasoning-effort
|
||||
title: Add a session-scoped reasoning effort picker to Chat
|
||||
scenario: gateway-backend-communication
|
||||
taskType: runtime-bridge
|
||||
intent: Let users select the current OpenClaw session thinking level from the existing Chat model control without duplicating provider capability rules in ClawX.
|
||||
touchedAreas:
|
||||
- harness/specs/tasks/chat-session-reasoning-effort.md
|
||||
- harness/specs/scenarios/gateway-backend-communication.md
|
||||
- harness/specs/rules/acp-chat-state-and-history.md
|
||||
- harness/specs/rules/provider-model-metadata-preservation.md
|
||||
- electron/shared/providers/model-capabilities.ts
|
||||
- electron/shared/providers/types.ts
|
||||
- electron/services/providers/provider-runtime-sync.ts
|
||||
- electron/services/acp-chat-service.ts
|
||||
- electron/utils/openclaw-auth.ts
|
||||
- shared/acp-chat/types.ts
|
||||
- shared/chat/types.ts
|
||||
- shared/host-api/contract.ts
|
||||
- src/components/settings/ProvidersSettings.tsx
|
||||
- src/lib/providers.ts
|
||||
- src/stores/acp-chat-session.ts
|
||||
- src/stores/chat.ts
|
||||
- src/stores/chat/session-catalog.ts
|
||||
- src/stores/providers.ts
|
||||
- src/pages/Chat/ChatInput.tsx
|
||||
- src/pages/Chat/index.tsx
|
||||
- shared/i18n/locales/**/chat.json
|
||||
- tests/unit/session-catalog.test.ts
|
||||
- tests/unit/provider-model-capabilities.test.ts
|
||||
- tests/unit/openclaw-auth.test.ts
|
||||
- tests/unit/chat-input.test.tsx
|
||||
- tests/unit/chat-store-session-label-fetch.test.ts
|
||||
- tests/unit/chat-load-sessions-startup.test.ts
|
||||
- tests/unit/acp-chat-service.test.ts
|
||||
- tests/unit/acp-chat-store.test.ts
|
||||
- tests/unit/gateway-events.test.ts
|
||||
- tests/unit/provider-runtime-sync.test.ts
|
||||
- tests/unit/provider-service-stale-cleanup.test.ts
|
||||
- tests/e2e/chat-model-picker.spec.ts
|
||||
- tests/e2e/provider-lifecycle.spec.ts
|
||||
- README.md
|
||||
- README.zh-CN.md
|
||||
- README.ja-JP.md
|
||||
expectedUserBehavior:
|
||||
- The Chat model button shows the effective reasoning effort for the current session.
|
||||
- The model menu directly offers the advertised members of the Off, Low, Medium, and High product ladder.
|
||||
- A new local draft can use the current agent's Gateway-advertised session defaults before its first prompt creates a persisted session row.
|
||||
- Reasoning effort opens in a compact dedicated submenu without a separate Thinking toggle.
|
||||
- Selecting Off, Low, Medium, or High persists that explicit current-session override through Gateway sessions.patch.
|
||||
- Each ACP prompt carries the same explicit reasoning effort selected for its current session instead of falling back to a different prompt-level effort.
|
||||
- A message cannot be sent while an effort change is still being applied.
|
||||
- A provider-side aborted prompt that was not cancelled by the user surfaces a localized retryable error instead of ending silently.
|
||||
requiredProfiles:
|
||||
- fast
|
||||
- comms
|
||||
- e2e
|
||||
requiredRules:
|
||||
- renderer-main-boundary
|
||||
- backend-communication-boundary
|
||||
- acp-chat-state-and-history
|
||||
- provider-model-metadata-preservation
|
||||
- ui-i18n-design-tokens
|
||||
- comms-regression
|
||||
- docs-sync
|
||||
requiredTests:
|
||||
- tests/unit/session-catalog.test.ts
|
||||
- tests/unit/provider-model-capabilities.test.ts
|
||||
- tests/unit/openclaw-auth.test.ts
|
||||
- tests/unit/chat-input.test.tsx
|
||||
- tests/unit/chat-store-session-label-fetch.test.ts
|
||||
- tests/unit/chat-load-sessions-startup.test.ts
|
||||
- tests/unit/acp-chat-service.test.ts
|
||||
- tests/unit/acp-chat-store.test.ts
|
||||
- tests/unit/gateway-events.test.ts
|
||||
- tests/unit/provider-runtime-sync.test.ts
|
||||
- tests/unit/provider-service-stale-cleanup.test.ts
|
||||
- tests/e2e/chat-model-picker.spec.ts
|
||||
- tests/e2e/provider-lifecycle.spec.ts
|
||||
acceptance:
|
||||
- Renderer uses the typed host-api Gateway RPC boundary and never opens its own Gateway transport.
|
||||
- thinkingLevels, thinkingDefault, and thinkingLevel remain Gateway-owned session metadata.
|
||||
- A local draft falls back only to agent-scoped sessions.list defaults, and a persisted session row always takes precedence.
|
||||
- Custom-provider primary models are synced with a fixed OpenClaw reasoning ladder; provider settings do not expose enable-reasoning controls.
|
||||
- Explicit off is distinct from a cleared override.
|
||||
- The reasoning-effort submenu stays narrower than the parent model menu while preserving readable wrapped guidance.
|
||||
- An explicit user-selected session level is forwarded unchanged in ACP prompt metadata; an inherited session adds no prompt-level override.
|
||||
- Failed patches restore the prior session state and leave the message available to send.
|
||||
- New labels are localized in English, Chinese, Japanese, and Russian.
|
||||
- Focused tests, harness validation, communication replay, and communication compare pass.
|
||||
docs:
|
||||
required: true
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
- Project Gateway session thinking metadata into the Chat session catalog.
|
||||
- Add a combined model and reasoning-effort picker to the composer.
|
||||
- Persist current-session overrides with `sessions.patch`.
|
||||
- Forward the explicit current-session selection through ACP prompt metadata so prompt execution uses that exact level.
|
||||
|
||||
## Out Of Scope
|
||||
|
||||
- Adding an independently selectable per-message or per-agent thinking default.
|
||||
- Maintaining a model/provider capability table in ClawX.
|
||||
- Changing OpenClaw reasoning semantics.
|
||||
@@ -1,40 +0,0 @@
|
||||
---
|
||||
id: disable-internal-agent-tools
|
||||
title: Disable control-plane agent tools in ClawX
|
||||
taskType: runtime-bridge
|
||||
intent: Prevent ClawX agents from invoking Gateway, node, and goal-management control-plane tools while preserving application-owned Gateway RPCs and agent orchestration tools.
|
||||
scenario: gateway-backend-communication
|
||||
touchedAreas:
|
||||
- electron/utils/openclaw-auth.ts
|
||||
- tests/unit/openclaw-auth.test.ts
|
||||
- tests/unit/sanitize-config.test.ts
|
||||
- README.md
|
||||
- README.zh-CN.md
|
||||
- README.ja-JP.md
|
||||
- README.ru-RU.md
|
||||
- harness/specs/rules/active-config-guards.md
|
||||
- harness/specs/scenarios/gateway-backend-communication.md
|
||||
- harness/specs/tasks/disable-internal-agent-tools.md
|
||||
expectedUserBehavior:
|
||||
- ClawX agents cannot invoke gateway, nodes, create_goal, get_goal, or update_goal.
|
||||
- Message, session orchestration, subagent, and agent discovery tools remain available unless another explicit policy denies them.
|
||||
- Gateway-exposed tools cannot bypass the agent-level restriction.
|
||||
- Existing user-defined tool deny entries remain intact and in their original order.
|
||||
- ClawX application-owned Gateway RPCs continue to work because tool policy does not block internal RPC methods.
|
||||
requiredProfiles:
|
||||
- fast
|
||||
- comms
|
||||
requiredTests:
|
||||
- tests/unit/openclaw-auth.test.ts
|
||||
- tests/unit/sanitize-config.test.ts
|
||||
acceptance:
|
||||
- Config sanitization adds every required control-plane tool to tools.deny and gateway.tools.deny without adding message or session-orchestration tools.
|
||||
- Repeated sanitization is idempotent and does not duplicate deny entries.
|
||||
- Existing deny entries are preserved in their original order.
|
||||
- Existing ClawX-required deny entries remain enforced.
|
||||
- Targeted unit tests, type checks, communication regression checks, and harness validation pass.
|
||||
docs:
|
||||
required: true
|
||||
---
|
||||
|
||||
ClawX keeps application-owned Gateway communication and agent orchestration available while preventing models from invoking selected control-plane tools directly. The restriction is enforced at both OpenClaw tool-policy layers.
|
||||
@@ -1,62 +0,0 @@
|
||||
---
|
||||
id: fix-supported-channel-connectivity
|
||||
title: Restore supported channel configuration and plugin activation
|
||||
type: ai-coding-task
|
||||
scenario: gateway-backend-communication
|
||||
taskType: runtime-bridge
|
||||
intent: Make every ClawX-supported plugin channel persist schema-valid configuration and become visible to the running Gateway after save or QR login.
|
||||
touchedAreas:
|
||||
- harness/specs/tasks/fix-supported-channel-connectivity.md
|
||||
- harness/specs/tasks/remove-unsupported-channel-catalog-entries.md
|
||||
- harness/specs/rules/channel-plugin-migration-guards.md
|
||||
- shared/types/channel.ts
|
||||
- shared/i18n/locales/en/channels.json
|
||||
- shared/i18n/locales/zh/channels.json
|
||||
- shared/i18n/locales/ja/channels.json
|
||||
- shared/i18n/locales/ru/channels.json
|
||||
- src/pages/Channels/index.tsx
|
||||
- electron/services/channels-api.ts
|
||||
- electron/utils/channel-config.ts
|
||||
- electron/utils/openclaw-auth.ts
|
||||
- electron/utils/plugin-install-index.ts
|
||||
- tests/unit/channels-page.test.tsx
|
||||
- tests/unit/channel-config.test.ts
|
||||
- tests/unit/openclaw-auth.test.ts
|
||||
- tests/unit/host-services.test.ts
|
||||
- tests/unit/plugin-install-index.test.ts
|
||||
- tests/e2e/channels-supported-catalog.spec.ts
|
||||
- tests/e2e/channels-plugin-save.spec.ts
|
||||
expectedUserBehavior:
|
||||
- Discord, WhatsApp, and QQBot save account credentials under channels.<id> without schema-invalid account mirrors under plugins.entries.<id>.
|
||||
- DingTalk, WeCom, Feishu/Lark, WeChat, Discord, WhatsApp, and QQBot trigger a guarded full Gateway restart when saved while the Gateway is running.
|
||||
- A no-change retry of a plugin-backed channel still performs the restart needed to discover an already copied plugin.
|
||||
- Telegram remains on the native OpenClaw config reload path without an extra ClawX restart.
|
||||
requiredProfiles:
|
||||
- fast
|
||||
- comms
|
||||
- e2e
|
||||
requiredRules:
|
||||
- channel-plugin-migration-guards
|
||||
- openclaw-config-delivery
|
||||
- gateway-readiness-policy
|
||||
- renderer-main-boundary
|
||||
- backend-communication-boundary
|
||||
- e2e-parallel-isolation
|
||||
- comms-regression
|
||||
- docs-sync
|
||||
requiredTests:
|
||||
- tests/unit/channel-config.test.ts
|
||||
- tests/unit/openclaw-auth.test.ts
|
||||
- tests/unit/host-services.test.ts
|
||||
- tests/unit/plugin-install-index.test.ts
|
||||
- tests/e2e/channels-plugin-save.spec.ts
|
||||
acceptance:
|
||||
- Plugin entries contain activation metadata only and never channel account credentials.
|
||||
- Discord, WhatsApp, and QQBot output passes the OpenClaw 2026.7.1 plugin-entry schema shape.
|
||||
- External channel plugin ids are retained in plugins.allow even when no unrelated plugin is present.
|
||||
- Trusted plugin install metadata targets OpenClaw's active state/openclaw.sqlite database.
|
||||
- Plugin-backed saves await the guarded Gateway restart path when the Gateway was running at request start.
|
||||
- No Renderer transport or direct Gateway request is added.
|
||||
docs:
|
||||
required: false
|
||||
---
|
||||
@@ -44,4 +44,4 @@ docs:
|
||||
required: true
|
||||
---
|
||||
|
||||
Historical first-stage heartbeat safety task. Its observability-only recovery policy is superseded by `restore-gateway-heartbeat-recovery-after-four-misses`, which keeps the first three misses diagnostic-only and permits guarded recovery on the fourth consecutive miss.
|
||||
Historical first-stage heartbeat safety task. Its observability-only recovery policy is superseded by `restore-gateway-heartbeat-recovery-after-ten-misses`, which keeps the first nine misses diagnostic-only and permits guarded recovery on the tenth consecutive miss.
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
---
|
||||
id: optimize-channel-delete-latency
|
||||
title: Make channel deletion responsive while preserving durable cleanup
|
||||
type: ai-coding-task
|
||||
scenario: gateway-backend-communication
|
||||
taskType: runtime-bridge
|
||||
intent: Remove a deleted channel or account from the Channels UI immediately after confirmation while Main completes the durable OpenClaw configuration and binding cleanup.
|
||||
touchedAreas:
|
||||
- harness/specs/tasks/optimize-channel-delete-latency.md
|
||||
- harness/specs/tasks/optimize-channel-save-latency.md
|
||||
- harness/specs/tasks/fix-supported-channel-connectivity.md
|
||||
- harness/specs/tasks/remove-unsupported-channel-catalog-entries.md
|
||||
- harness/specs/rules/channel-plugin-migration-guards.md
|
||||
- shared/host-api/contract.ts
|
||||
- shared/types/channel.ts
|
||||
- shared/i18n/locales/en/channels.json
|
||||
- shared/i18n/locales/zh/channels.json
|
||||
- shared/i18n/locales/ja/channels.json
|
||||
- shared/i18n/locales/ru/channels.json
|
||||
- electron/services/channels-api.ts
|
||||
- electron/utils/channel-config.ts
|
||||
- electron/utils/openclaw-auth.ts
|
||||
- src/components/channels/ChannelConfigModal.tsx
|
||||
- src/pages/Channels/index.tsx
|
||||
- tests/unit/agent-config.test.ts
|
||||
- tests/unit/channel-config.test.ts
|
||||
- tests/unit/host-services.test.ts
|
||||
- tests/unit/openclaw-auth.test.ts
|
||||
- tests/unit/channels-page.test.tsx
|
||||
- tests/e2e/channels-plugin-save.spec.ts
|
||||
- tests/e2e/channels-supported-catalog.spec.ts
|
||||
- tests/e2e/channels-delete-latency.spec.ts
|
||||
expectedUserBehavior:
|
||||
- Confirming deletion closes the confirmation dialog and removes the target row immediately instead of blocking on Gateway/OpenClaw configuration delivery.
|
||||
- Main still durably deletes the channel configuration and associated binding.
|
||||
- A failed deletion reports an error and refreshes the file-backed channel view to restore the actual state.
|
||||
- Runtime convergence refresh remains asynchronous and does not block the delete interaction.
|
||||
requiredProfiles:
|
||||
- fast
|
||||
- comms
|
||||
- e2e
|
||||
requiredRules:
|
||||
- channel-plugin-migration-guards
|
||||
- openclaw-config-delivery
|
||||
- gateway-readiness-policy
|
||||
- renderer-main-boundary
|
||||
- backend-communication-boundary
|
||||
- e2e-parallel-isolation
|
||||
- comms-regression
|
||||
- docs-sync
|
||||
requiredTests:
|
||||
- tests/unit/channels-page.test.tsx
|
||||
- tests/e2e/channels-delete-latency.spec.ts
|
||||
acceptance:
|
||||
- The UI applies deletion optimistically before the host delete promise settles.
|
||||
- The host delete request remains the only mutation path; Renderer does not edit OpenClaw configuration directly.
|
||||
- Failure triggers a config-only refresh rather than leaving stale optimistic state.
|
||||
- No direct Renderer Gateway request or new transport path is added.
|
||||
docs:
|
||||
required: false
|
||||
---
|
||||
@@ -1,77 +0,0 @@
|
||||
---
|
||||
id: optimize-channel-save-latency
|
||||
title: Return promptly after durable channel saves while activation continues
|
||||
type: ai-coding-task
|
||||
scenario: gateway-backend-communication
|
||||
taskType: runtime-bridge
|
||||
intent: Reduce the time the channel configuration modal remains blocked by returning after configuration and binding commits, while plugin activation uses one Gateway lifecycle path without racing native config reloads.
|
||||
touchedAreas:
|
||||
- harness/specs/tasks/optimize-channel-save-latency.md
|
||||
- harness/specs/tasks/fix-supported-channel-connectivity.md
|
||||
- harness/specs/tasks/remove-unsupported-channel-catalog-entries.md
|
||||
- harness/specs/rules/channel-plugin-migration-guards.md
|
||||
- harness/specs/rules/openclaw-config-delivery.md
|
||||
- harness/reference/openclaw-config-delivery.md
|
||||
- shared/host-api/contract.ts
|
||||
- shared/types/channel.ts
|
||||
- shared/i18n/locales/en/channels.json
|
||||
- shared/i18n/locales/zh/channels.json
|
||||
- shared/i18n/locales/ja/channels.json
|
||||
- shared/i18n/locales/ru/channels.json
|
||||
- electron/services/channels-api.ts
|
||||
- electron/gateway/config-delivery.ts
|
||||
- electron/gateway/manager.ts
|
||||
- electron/gateway/startup-orchestrator.ts
|
||||
- electron/utils/channel-config.ts
|
||||
- electron/utils/openclaw-auth.ts
|
||||
- electron/utils/plugin-install.ts
|
||||
- src/components/channels/ChannelConfigModal.tsx
|
||||
- src/pages/Channels/index.tsx
|
||||
- tests/unit/channel-config.test.ts
|
||||
- tests/unit/agent-config.test.ts
|
||||
- tests/unit/host-services.test.ts
|
||||
- tests/unit/gateway-config-delivery.test.ts
|
||||
- tests/unit/gateway-startup-orchestrator.test.ts
|
||||
- tests/unit/openclaw-auth.test.ts
|
||||
- tests/unit/plugin-install.test.ts
|
||||
- tests/unit/channels-page.test.tsx
|
||||
- tests/e2e/channels-plugin-save.spec.ts
|
||||
- tests/e2e/channels-supported-catalog.spec.ts
|
||||
expectedUserBehavior:
|
||||
- Saving a plugin-backed channel returns as soon as its configuration and scoped binding are durably committed instead of waiting for Gateway stop, startup, and readiness.
|
||||
- The Channels page immediately reloads the committed local configuration and then converges to runtime connection state after the scheduled Gateway restart.
|
||||
- Changed plugin configuration uses OpenClaw's native config reload without an additional ClawX full restart when OpenClaw peer link repair succeeds; a failed peer link repair schedules the guarded restart path after the config commit, and no-change retries still use that path when plugin discovery is required.
|
||||
- A config commit whose acknowledgement is lost to native reload is verified from the durable config instead of being reported as a false save failure.
|
||||
- A stale owned process that fails to recover from an in-process restart is terminated promptly and replaced instead of holding startup for the full cold-start retry budget.
|
||||
- Restart failures remain visible through normal Gateway status and logging rather than becoming unhandled promise rejections.
|
||||
requiredProfiles:
|
||||
- fast
|
||||
- comms
|
||||
- e2e
|
||||
requiredRules:
|
||||
- channel-plugin-migration-guards
|
||||
- openclaw-config-delivery
|
||||
- gateway-readiness-policy
|
||||
- renderer-main-boundary
|
||||
- backend-communication-boundary
|
||||
- e2e-parallel-isolation
|
||||
- comms-regression
|
||||
- docs-sync
|
||||
requiredTests:
|
||||
- tests/unit/agent-config.test.ts
|
||||
- tests/unit/host-services.test.ts
|
||||
- tests/unit/gateway-config-delivery.test.ts
|
||||
- tests/unit/gateway-startup-orchestrator.test.ts
|
||||
- tests/unit/plugin-install.test.ts
|
||||
- tests/unit/channels-page.test.tsx
|
||||
- tests/e2e/channels-plugin-save.spec.ts
|
||||
acceptance:
|
||||
- The save response exposes when plugin activation is pending.
|
||||
- A changed plugin config relies on the coordinator-owned config.set reload without a redundant full restart when peer link repair succeeds, and schedules a guarded full restart after commit when peer link repair fails.
|
||||
- A no-change plugin save starts a guarded Gateway restart only after the scoped binding commit completes.
|
||||
- The save response does not await Gateway restart readiness.
|
||||
- Immediate post-save refresh is config-only and does not issue an expensive runtime probe while Gateway is restarting.
|
||||
- No Renderer transport or direct Gateway request is added.
|
||||
docs:
|
||||
required: false
|
||||
---
|
||||
@@ -1,57 +0,0 @@
|
||||
---
|
||||
id: parallelize-electron-e2e
|
||||
title: Parallelize Electron E2E safely
|
||||
scenario: gateway-backend-communication
|
||||
taskType: runtime-bridge
|
||||
intent: Run isolated Electron E2E specs concurrently while fencing OS-global resources and host-sensitive performance profiles.
|
||||
touchedAreas:
|
||||
- playwright.config.ts
|
||||
- package.json
|
||||
- .github/workflows/electron-e2e.yml
|
||||
- AGENTS.md
|
||||
- tests/e2e/fixtures/electron.ts
|
||||
- tests/e2e/parallel-policy.ts
|
||||
- tests/e2e/chat-streamdown-rendering.spec.ts
|
||||
- tests/e2e/chat-acp-attachments.spec.ts
|
||||
- tests/e2e/markdown-file-preview.spec.ts
|
||||
- tests/e2e/renderer-performance.spec.ts
|
||||
- tests/unit/e2e-parallel-policy.test.ts
|
||||
- harness/reference/e2e-parallelism.md
|
||||
- harness/specs/rules/e2e-parallel-isolation.md
|
||||
- harness/specs/scenarios/gateway-backend-communication.md
|
||||
- harness/specs/tasks/parallelize-electron-e2e.md
|
||||
- README.md
|
||||
- README.zh-CN.md
|
||||
- README.ja-JP.md
|
||||
- README.ru-RU.md
|
||||
expectedUserBehavior:
|
||||
- Local and CI Electron E2E runs execute independent spec files concurrently.
|
||||
- Tests that use the OS clipboard and host performance profiles never overlap incompatible tests.
|
||||
requiredProfiles:
|
||||
- fast
|
||||
- comms
|
||||
- e2e
|
||||
requiredRules:
|
||||
- backend-communication-boundary
|
||||
- e2e-parallel-isolation
|
||||
- docs-sync
|
||||
requiredTests:
|
||||
- pnpm exec vitest run tests/unit/e2e-parallel-policy.test.ts
|
||||
- pnpm harness validate --spec harness/specs/tasks/parallelize-electron-e2e.md
|
||||
- pnpm run test:e2e
|
||||
- pnpm run typecheck
|
||||
- pnpm run lint:check
|
||||
acceptance:
|
||||
- The ordinary E2E project uses more than one worker by default and can be overridden for constrained machines.
|
||||
- OS-global clipboard tests execute in a one-worker prerequisite project.
|
||||
- Renderer performance profiles execute alone after functional E2E tests.
|
||||
- Per-test HOME, Electron profile, OpenClaw state, and Host API configuration remain isolated.
|
||||
- CI opts into the checked-in parallel worker policy on every supported OS.
|
||||
- A durable policy and automated guard explain how future global-resource tests enter the exclusive lane.
|
||||
docs:
|
||||
required: true
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
This task changes only Electron E2E scheduling and fixture isolation. It does not change application transport behavior, production Host API routing, or user-visible ClawX features.
|
||||
@@ -1,87 +0,0 @@
|
||||
---
|
||||
id: recover-acp-session-after-gateway-restart
|
||||
title: Continue an ACP prompt through Gateway restart recovery
|
||||
scenario: gateway-backend-communication
|
||||
taskType: runtime-bridge
|
||||
intent: Preserve an accepted ACP prompt while OpenClaw replaces an interrupted run with an explicitly linked restart-recovery run.
|
||||
touchedAreas:
|
||||
- .gitignore
|
||||
- README.md
|
||||
- README.zh-CN.md
|
||||
- README.ja-JP.md
|
||||
- README.ru-RU.md
|
||||
- docs/en-US/architecture.md
|
||||
- docs/zh-CN/architecture.md
|
||||
- docs/ja-JP/architecture.md
|
||||
- docs/ru-RU/architecture.md
|
||||
- harness/reference/acp-chat.md
|
||||
- harness/specs/rules/acp-chat-state-and-history.md
|
||||
- harness/specs/tasks/recover-acp-session-after-gateway-restart.md
|
||||
- pnpm-workspace.yaml
|
||||
- pnpm-lock.yaml
|
||||
- patches/openclaw@2026.7.1-2.patch
|
||||
- tests/unit/openclaw-restart-recovery-patch.test.ts
|
||||
- tests/e2e/chat-acp-inline-timeline.spec.ts
|
||||
expectedUserBehavior:
|
||||
- An accepted ACP prompt stays pending for up to 60 seconds while the Gateway reconnects and OpenClaw starts restart recovery; a send that was never acknowledged retains the 5-second disconnect deadline.
|
||||
- Events from the explicitly linked recovery run continue the original in-memory turn and settle its original prompt.
|
||||
- Recovered text, tool activity, approvals, cancellation, and terminal state use the new run id without a Renderer session reload; tool calls keep text segments on either side distinct.
|
||||
- After ClawX restarts, ACP `session/load` restores persisted transcript tool calls and results as native tool updates in their original order between assistant text segments.
|
||||
requiredProfiles:
|
||||
- fast
|
||||
- comms
|
||||
requiredRules:
|
||||
- renderer-main-boundary
|
||||
- backend-communication-boundary
|
||||
- gateway-readiness-policy
|
||||
- acp-chat-state-and-history
|
||||
- comms-regression
|
||||
- docs-sync
|
||||
requiredTests:
|
||||
- pnpm exec vitest run tests/unit/openclaw-restart-recovery-patch.test.ts
|
||||
- pnpm exec playwright test tests/e2e/chat-acp-inline-timeline.spec.ts --project=parallel --grep "renders ledger-style replayed ACP tool events"
|
||||
- pnpm run typecheck
|
||||
- pnpm run comms:replay
|
||||
- pnpm run comms:compare
|
||||
acceptance:
|
||||
- The unique interrupted run id is propagated only by trusted main-session restart recovery and never inferred from a session key.
|
||||
- Chat and agent events expose `resumedFromRunId`, and ACP adopts the new run only when it matches the pending prompt.
|
||||
- Adoption clears the stale disconnect deadline, resets per-run stream state, rebinds cancellation, and retains tool and approval delivery after reconnect.
|
||||
- Visible recovery tool events reach the exact session-message subscription with `resumedFromRunId`, even when no global session-event subscription exists.
|
||||
- Reconnect reconciliation waits for the exact session-message subscription request to settle, so recovery dispatch cannot race ahead of tool-event registration.
|
||||
- OpenClaw's ACP transcript fallback maps persisted assistant `toolCall` blocks and `toolResult` messages to `tool_call` and `tool_call_update` rather than flattening adjacent assistant text.
|
||||
- String-valued and structured transcript tool results retain visible output in the replayed tool card.
|
||||
- The initial 5-second disconnect check extends only acknowledged prompts to a bounded 60-second total recovery window; unacknowledged prompts still reject after 5 seconds.
|
||||
- Renderer remains unchanged and ACP replay remains the source of truth for persisted Chat history.
|
||||
docs:
|
||||
required: true
|
||||
---
|
||||
|
||||
## Original Problem
|
||||
|
||||
If the Gateway restarted while the AI was replying, ClawX could reconnect to the replacement Gateway process but the active ACP conversation did not resume. OpenClaw could start a restart-recovery run and continue producing output, while ClawX remained frozen at the last text received before the disconnect. The original `session/prompt` eventually failed or stayed disconnected from the replacement run because ACP still identified the turn by the interrupted run id and had no trusted lineage proving that the new run continued it.
|
||||
|
||||
The failure was not limited to the live UI. Even when OpenClaw completed and persisted the recovered response, restarting ClawX and selecting the same conversation could still replay only the timeline prefix that existed before the disconnect. The ACP event history associated with the original prompt had not received the replacement run's events, and transcript fallback did not preserve the complete native timeline: it projected text and thinking while dropping persisted `toolCall` and `toolResult` records. Missing tool events also removed the boundaries between assistant text segments around those calls.
|
||||
|
||||
The broken flow therefore had two related symptoms:
|
||||
|
||||
- Live recovery: Gateway connectivity returned, but the replacement run's text, tools, approvals, cancellation target, and terminal state did not settle the original in-memory ACP prompt.
|
||||
- Reload recovery: after restarting ClawX, `session/load` could reproduce the stale pre-disconnect view or an incomplete flattened response instead of the recovered conversation that OpenClaw had persisted.
|
||||
|
||||
## Fix
|
||||
|
||||
`patches/openclaw@2026.7.1-2.patch` is the ClawX-local backport applied to the pinned `openclaw@2026.7.1-2` runtime. It restores one explicit recovery chain from the interrupted run through live ACP delivery and persisted replay:
|
||||
|
||||
- Persist the active lifecycle run id before a restart and allow only trusted `main_session_restart_recovery` provenance to pass it into a distinct replacement run as `internalRestartRecoverySourceRunId`.
|
||||
- Project that source id as `resumedFromRunId` on Chat, agent, tool, and approval events. ACP adopts a replacement run only when this value exactly matches its pending prompt; it never infers lineage from a shared session key.
|
||||
- Keep an acknowledged prompt pending for a bounded 60-second total recovery window while retaining the original 5-second deadline for a send that was never acknowledged. Adoption clears the stale disconnect deadline, resets per-run text, thought, and tool state, rebinds cancellation, and lets the replacement terminal event settle the original prompt.
|
||||
- Re-register the exact `sessions.messages.subscribe` subscription before reconnect reconciliation can dispatch recovery work. Gateway mirrors visible recovery tool events to that exact subscriber, includes `resumedFromRunId`, and deduplicates clients that already receive the run-scoped event.
|
||||
- When a complete structured ACP ledger is unavailable, map persisted assistant `toolCall` blocks and `toolResult` messages to native `tool_call` and `tool_call_update` updates in transcript order. Both structured and string-valued results retain visible output, and a text-tool-text sequence reloads as two distinct assistant text segments around the tool card.
|
||||
|
||||
ClawX Renderer remains unchanged. It continues to reduce standard ACP updates into one in-memory timeline; the repair is in OpenClaw's recovery lineage, Gateway event projection, ACP prompt reconciliation, and ACP replay fallback.
|
||||
|
||||
The same source-level fixes will be submitted to the OpenClaw upstream repository as a pull request. This local generated-dist patch is a temporary compatibility measure: after the upstream PR is merged and ClawX upgrades to an OpenClaw release containing the fixes, `patches/openclaw@2026.7.1-2.patch` should be removed rather than carried forward to a newer generated bundle.
|
||||
|
||||
## Scope
|
||||
|
||||
The dependency patch backports current OpenClaw run-lineage, recovered tool delivery, and native ACP transcript replay behavior to the pinned `openclaw@2026.7.1-2` runtime. It patches generated runtime chunks and declarations, so every OpenClaw version change must regenerate and review the patch rather than carrying it forward by filename.
|
||||
@@ -1,43 +0,0 @@
|
||||
---
|
||||
id: remove-unsupported-channel-catalog-entries
|
||||
title: Remove unsupported channels from the ClawX channel catalog
|
||||
type: ai-coding-task
|
||||
scenario: gateway-backend-communication
|
||||
taskType: runtime-bridge
|
||||
intent: Keep the ClawX channel configuration UI limited to the eight integrations that ClawX installs and supports.
|
||||
touchedAreas:
|
||||
- harness/specs/tasks/remove-unsupported-channel-catalog-entries.md
|
||||
- harness/specs/rules/channel-plugin-migration-guards.md
|
||||
- shared/types/channel.ts
|
||||
- shared/i18n/locales/en/channels.json
|
||||
- shared/i18n/locales/zh/channels.json
|
||||
- shared/i18n/locales/ja/channels.json
|
||||
- shared/i18n/locales/ru/channels.json
|
||||
- src/pages/Channels/index.tsx
|
||||
- tests/unit/channels-page.test.tsx
|
||||
- tests/e2e/channels-supported-catalog.spec.ts
|
||||
expectedUserBehavior:
|
||||
- The Channels page offers only Telegram, Discord, WhatsApp, WeChat, DingTalk, Feishu/Lark, WeCom, and QQBot.
|
||||
- Signal, iMessage, Matrix, LINE, Microsoft Teams, Google Chat, and Mattermost are not shown as configurable or configured ClawX channels.
|
||||
- Runtime reports for unknown OpenClaw channels do not create editable cards in the ClawX Channels page.
|
||||
requiredProfiles:
|
||||
- fast
|
||||
- comms
|
||||
- e2e
|
||||
requiredRules:
|
||||
- channel-plugin-migration-guards
|
||||
- renderer-main-boundary
|
||||
- backend-communication-boundary
|
||||
- e2e-parallel-isolation
|
||||
- docs-sync
|
||||
requiredTests:
|
||||
- tests/unit/channels-page.test.tsx
|
||||
- tests/e2e/channels-supported-catalog.spec.ts
|
||||
acceptance:
|
||||
- The shared ChannelType and channel metadata catalog contain exactly the eight ClawX-supported channel ids.
|
||||
- Unsupported channel metadata and translations are removed from every supported locale.
|
||||
- The Channels page ignores unsupported channel groups returned by the runtime without deleting their OpenClaw configuration.
|
||||
- No new direct IPC or Gateway transport is introduced.
|
||||
docs:
|
||||
required: false
|
||||
---
|
||||
+14
-14
@@ -1,11 +1,11 @@
|
||||
---
|
||||
id: restore-gateway-heartbeat-recovery-after-four-misses
|
||||
title: Restore Gateway heartbeat recovery after four misses
|
||||
id: restore-gateway-heartbeat-recovery-after-ten-misses
|
||||
title: Restore Gateway heartbeat recovery after ten misses
|
||||
scenario: gateway-backend-communication
|
||||
taskType: runtime-bridge
|
||||
intent: Recover a persistently unresponsive local Gateway automatically while giving long-running work a bounded heartbeat window before process replacement.
|
||||
intent: Recover a persistently unresponsive local Gateway automatically while giving long-running work a longer heartbeat window before process replacement.
|
||||
touchedAreas:
|
||||
- harness/specs/tasks/restore-gateway-heartbeat-recovery-after-four-misses.md
|
||||
- harness/specs/tasks/restore-gateway-heartbeat-recovery-after-ten-misses.md
|
||||
- harness/specs/tasks/make-gateway-heartbeat-observability-only.md
|
||||
- harness/specs/rules/gateway-heartbeat-safety.md
|
||||
- harness/specs/scenarios/gateway-backend-communication.md
|
||||
@@ -19,9 +19,9 @@ touchedAreas:
|
||||
- README.zh-CN.md
|
||||
- README.ja-JP.md
|
||||
expectedUserBehavior:
|
||||
- One to three consecutive missed Gateway heartbeat responses remain diagnostic-only and do not interrupt long-running work.
|
||||
- Four consecutive missed heartbeat responses mark the Gateway unresponsive and request an automatic restart when lifecycle auto-recovery is enabled and the Gateway is still running.
|
||||
- A pong or any incoming Gateway message before the fourth miss resets the consecutive-miss counter.
|
||||
- One to nine consecutive missed Gateway heartbeat responses remain diagnostic-only and do not interrupt long-running work.
|
||||
- Ten consecutive missed heartbeat responses mark the Gateway unresponsive and request an automatic restart when lifecycle auto-recovery is enabled and the Gateway is still running.
|
||||
- A pong or any incoming Gateway message before the tenth miss resets the consecutive-miss counter.
|
||||
- Process exit, WebSocket close, explicit restart, and code-1012 reconnect behavior remain unchanged.
|
||||
requiredProfiles:
|
||||
- fast
|
||||
@@ -37,15 +37,15 @@ requiredTests:
|
||||
- tests/unit/gateway-manager-diagnostics.test.ts
|
||||
- tests/unit/gateway-connection-monitor.test.ts
|
||||
acceptance:
|
||||
- The Gateway heartbeat threshold is four consecutive misses.
|
||||
- Misses one through three update monitor state without calling GatewayManager.restart or terminating the socket.
|
||||
- The fourth consecutive miss records timeout diagnostics and calls GatewayManager.restart exactly once when auto-recovery is allowed.
|
||||
- The fourth miss does not restart when auto-reconnect is disabled or the Gateway is not running.
|
||||
- Recovery through a pong or any incoming Gateway message resets the sequence, so only four new consecutive misses can trigger recovery.
|
||||
- The Gateway heartbeat threshold is ten consecutive misses.
|
||||
- Misses one through nine update monitor state without calling GatewayManager.restart or terminating the socket.
|
||||
- The tenth consecutive miss records timeout diagnostics and calls GatewayManager.restart exactly once when auto-recovery is allowed.
|
||||
- The tenth miss does not restart when auto-reconnect is disabled or the Gateway is not running.
|
||||
- Recovery through a pong or any incoming Gateway message resets the sequence, so only ten new consecutive misses can trigger recovery.
|
||||
- Automatic heartbeat recovery behaves consistently on Windows, macOS, and Linux.
|
||||
- Documentation in all maintained README translations describes the four-miss automatic recovery policy.
|
||||
- Documentation in all maintained README translations describes the ten-miss automatic recovery policy.
|
||||
docs:
|
||||
required: true
|
||||
---
|
||||
|
||||
This task supersedes the recovery policy from `make-gateway-heartbeat-observability-only`: heartbeat misses remain non-authoritative during the first three misses, while a fourth consecutive miss is treated as persistent unresponsiveness and may request guarded lifecycle recovery.
|
||||
This task supersedes the recovery policy from `make-gateway-heartbeat-observability-only`: heartbeat misses remain non-authoritative during the first nine misses, while a tenth consecutive miss is treated as persistent unresponsiveness and may request guarded lifecycle recovery.
|
||||
@@ -1,60 +0,0 @@
|
||||
---
|
||||
id: upgrade-openclaw-2026-7-1-2
|
||||
title: Upgrade the bundled OpenClaw runtime to 2026.7.1-2
|
||||
scenario: gateway-backend-communication
|
||||
taskType: runtime-bridge
|
||||
intent: Apply the OpenClaw 2026.7.1 correction releases without regressing ClawX channels, providers, models, ACP chat, or packaged runtime behavior.
|
||||
touchedAreas:
|
||||
- package.json
|
||||
- pnpm-lock.yaml
|
||||
- pnpm-workspace.yaml
|
||||
- patches/openclaw@2026.7.1-2.patch
|
||||
- electron/gateway/config-sync.ts
|
||||
- electron/gateway/manager.ts
|
||||
- electron/utils/openclaw-upgrade-snapshot.ts
|
||||
- tests/unit/gateway-ready-fallback.test.ts
|
||||
- tests/unit/openclaw-bundle-config.test.ts
|
||||
- tests/unit/openclaw-upgrade-snapshot.test.ts
|
||||
- tests/unit/openclaw-restart-recovery-patch.test.ts
|
||||
- harness/reference/openclaw-config-delivery.md
|
||||
- harness/specs/tasks/upgrade-openclaw-2026-7-1.md
|
||||
- harness/specs/tasks/upgrade-openclaw-2026-7-1-2.md
|
||||
expectedUserBehavior:
|
||||
- A direct ClawX 0.5.1 upgrade from OpenClaw 2026.6.10 preserves configuration, authentication, sessions, ClawX-managed selected models, channel credentials, and managed channel plugin identities through the inherited 2026.7.1 migration.
|
||||
- Manual catalog-only model references require explicit upgrade preflight because the upstream unconfigured global model catalog is smaller in 2026.7.1 than in 2026.6.10; explicit ClawX-managed provider models are not dependent on that catalog entry remaining built in.
|
||||
- Existing configuration, authentication, sessions, models, and channel credentials remain usable after upgrading from OpenClaw 2026.7.1.
|
||||
- The supported ClawX channel catalog and effective plugin identities remain unchanged.
|
||||
- Official managed plugin updates tolerate repaired npm lock metadata and singleton-array npm view responses.
|
||||
- Codex turns continue to their authoritative terminal result after progress replies.
|
||||
- Legacy migration residue, Memory Core derived-sidecar conflicts, and guarded WSL EROFS permission results do not cause avoidable Gateway startup failures.
|
||||
- When canonical SQLite update-check state already exists, conflicting legacy update-check JSON is backed up outside the active state root before launch instead of causing a doctor retry loop.
|
||||
requiredProfiles:
|
||||
- fast
|
||||
- comms
|
||||
requiredTests:
|
||||
- tests/unit/gateway-ready-fallback.test.ts
|
||||
- tests/unit/openclaw-bundle-config.test.ts
|
||||
- tests/unit/openclaw-upgrade-snapshot.test.ts
|
||||
- tests/unit/plugin-install.test.ts
|
||||
- tests/unit/plugin-install-index.test.ts
|
||||
- tests/unit/channel-config.test.ts
|
||||
- tests/unit/openclaw-auth.test.ts
|
||||
- tests/unit/acp-chat-service.test.ts
|
||||
- tests/unit/gateway-startup-orchestrator.test.ts
|
||||
- tests/unit/openclaw-restart-recovery-patch.test.ts
|
||||
acceptance:
|
||||
- The OpenClaw runtime is pinned to 2026.7.1-2 and resolves @openclaw/ai 2026.7.1-2.
|
||||
- External channel plugin package versions and ClawX's supported channel catalog remain unchanged because the correction release does not change channel APIs or manifests.
|
||||
- Provider and model configuration behavior remains unchanged because the correction release does not change provider or model catalog sources.
|
||||
- The bundled-runtime patch and pruning pipeline succeeds against OpenClaw 2026.7.1-2.
|
||||
- Prelaunch preserves legacy update-check JSON for upstream import when SQLite has no canonical row, and quarantines it with restrictive permissions when SQLite is already authoritative.
|
||||
- The pre-migration snapshot is removed after either the native ready event or an equivalent successful RPC-router readiness fallback, so a missed early event does not leave duplicated auth/SQLite secrets behind.
|
||||
- Type checks, targeted channel/provider/ACP tests, communication regression checks, and harness validation pass.
|
||||
docs:
|
||||
required: false
|
||||
---
|
||||
|
||||
Use this task spec for the correction-release upgrade from OpenClaw 2026.7.1
|
||||
to 2026.7.1-2. It inherits the runtime and migration compatibility work captured
|
||||
in `upgrade-openclaw-2026-7-1.md` and focuses on proving that the correction
|
||||
release does not widen ClawX's channel or model surface.
|
||||
@@ -29,7 +29,6 @@ touchedAreas:
|
||||
- tests/e2e/cron-run-live-status.spec.ts
|
||||
- tests/unit/gateway-startup-recovery.test.ts
|
||||
- tests/unit/gateway-startup-orchestrator.test.ts
|
||||
- tests/unit/gateway-ready-fallback.test.ts
|
||||
- tests/unit/openclaw-cli.test.ts
|
||||
- tests/unit/openclaw-bundle-config.test.ts
|
||||
- tests/unit/openclaw-upgrade-snapshot.test.ts
|
||||
@@ -40,13 +39,11 @@ touchedAreas:
|
||||
- README.zh-CN.md
|
||||
- README.ja-JP.md
|
||||
- README.ru-RU.md
|
||||
- harness/reference/openclaw-config-delivery.md
|
||||
- harness/specs/scenarios/gateway-backend-communication.md
|
||||
- harness/specs/rules/acp-chat-state-and-history.md
|
||||
- harness/specs/tasks/upgrade-openclaw-2026-7-1.md
|
||||
- harness/specs/tasks/upgrade-openclaw-2026-7-1-2.md
|
||||
expectedUserBehavior:
|
||||
- Existing ClawX-managed OpenClaw 2026.6.10 configuration, authentication, sessions, selected provider models, and channel credentials remain usable after upgrade, with a one-time pre-migration snapshot of migration-critical config/auth/SQLite state that is removed after Gateway startup succeeds.
|
||||
- Existing OpenClaw 2026.6.10 configuration, authentication, sessions, and channel credentials remain usable after upgrade, with a one-time pre-migration snapshot of migration-critical config/auth/SQLite state that is removed after Gateway startup succeeds.
|
||||
- ClawX reconciles old managed channel-plugin install records with its current mirrored extensions, removes records for unconfigured mirrors, and links declared `openclaw` peers to the bundled runtime before OpenClaw's post-core payload smoke check.
|
||||
- ClawX starts and communicates with the bundled OpenClaw 2026.7.1 Gateway, including migration and control-plane safe-mode startup states.
|
||||
- ClawX registers the compatibility-patched WeCom mirror as a local-path install with static channel metadata so OpenClaw startup migration does not replace it with the raw mismatched npm package.
|
||||
@@ -82,7 +79,6 @@ acceptance:
|
||||
- Configured mirrored plugins that declare an `openclaw` peer have a runtime link to the current bundled OpenClaw package before migration validation; stale install records for unconfigured mirrors are removed so missing directories cannot block startup.
|
||||
- Gateway recovery performs at most one doctor repair per startup flow and does not retry fatal runtime, EX_CONFIG, invalid migration, or active migration-lease failures indefinitely.
|
||||
- Electron Main reads current cron history through Gateway `cron.runs`, retains legacy JSONL as a compatibility fallback, and supplements only empty cron ACP replay in memory without replacing non-empty replay.
|
||||
- ClawX-managed selected models remain explicit in provider configuration, but upgrade preflight treats manual catalog-only model references separately: OpenClaw's unconfigured `models list --all` catalog changes from 140 entries in 2026.6.10 to 86 in 2026.7.1, removing the built-in Venice, Fireworks, Tencent TokenHub, and Z.AI catalog groups plus several older Moonshot entries.
|
||||
- ACP 1.1 type checks, targeted runtime tests, communication regression checks, and harness validation pass.
|
||||
docs:
|
||||
required: true
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clawx",
|
||||
"version": "0.5.4",
|
||||
"version": "0.5.3",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@discordjs/opus",
|
||||
@@ -52,7 +52,7 @@
|
||||
"test": "vitest run",
|
||||
"test:e2e": "pnpm run build:vite && playwright test",
|
||||
"test:e2e:headed": "pnpm run build:vite && playwright test --headed",
|
||||
"perf:chat": "pnpm run build:vite && playwright test tests/e2e/renderer-performance.spec.ts --project=performance --no-deps --workers=1",
|
||||
"perf:chat": "pnpm run build:vite && playwright test tests/e2e/renderer-performance.spec.ts --workers=1",
|
||||
"profile:main": "pnpm run build:vite && electron --inspect=9229 .",
|
||||
"harness": "pnpm --filter @clawx/harness start --",
|
||||
"harness:ci": "pnpm harness list && pnpm harness validate --spec harness/specs/scenarios/gateway-backend-communication.md && pnpm harness validate --spec harness/specs/tasks/maintain-session-catalog-reconciliation.example.md --no-diff && pnpm harness run --spec harness/specs/scenarios/gateway-backend-communication.md --dry-run && pnpm exec vitest run tests/unit/harness-specs.test.ts tests/unit/harness-git.test.ts",
|
||||
@@ -175,7 +175,7 @@
|
||||
"monaco-editor": "^0.55.1",
|
||||
"mpg123-decoder": "^1.0.3",
|
||||
"ms": "^2.1.3",
|
||||
"openclaw": "2026.7.1-2",
|
||||
"openclaw": "2026.7.1",
|
||||
"opusscript": "^0.1.1",
|
||||
"pdfjs-dist": "^5.7.284",
|
||||
"playwright-core": "1.59.1",
|
||||
|
||||
@@ -1,695 +0,0 @@
|
||||
diff --git a/dist/acp-cli-BXc5GttU.js b/dist/acp-cli-BXc5GttU.js
|
||||
index 6c7de7d60ac87a733632c36a744ada0e4e48d077..62797972ce78076c545d712809d99f4a13f8e0ee 100644
|
||||
--- a/dist/acp-cli-BXc5GttU.js
|
||||
+++ b/dist/acp-cli-BXc5GttU.js
|
||||
@@ -1622,8 +1622,25 @@ function buildSessionUsageSnapshot(row) {
|
||||
}
|
||||
//#endregion
|
||||
//#region src/acp/translator.replay.ts
|
||||
+function extractToolResultReplay(message) {
|
||||
+ const toolCallId = normalizeOptionalString(message.toolCallId);
|
||||
+ if (!toolCallId) return [];
|
||||
+ const rawOutput = {
|
||||
+ content: message.content,
|
||||
+ ...message.details === void 0 ? {} : { details: message.details }
|
||||
+ };
|
||||
+ return [{
|
||||
+ sessionUpdate: "tool_call_update",
|
||||
+ toolCallId,
|
||||
+ status: message.isError === true ? "failed" : "completed",
|
||||
+ rawOutput,
|
||||
+ content: extractToolCallContent(message.content) ?? extractToolCallContent(rawOutput),
|
||||
+ locations: extractToolCallLocations(rawOutput)
|
||||
+ }];
|
||||
+}
|
||||
function extractReplayChunks(message) {
|
||||
const role = typeof message.role === "string" ? message.role : "";
|
||||
+ if (role === "toolResult") return extractToolResultReplay(message);
|
||||
if (role !== "user" && role !== "assistant") return [];
|
||||
if (typeof message.content === "string") return message.content.length > 0 ? [{
|
||||
sessionUpdate: role === "user" ? "user_message_chunk" : "agent_message_chunk",
|
||||
@@ -1641,6 +1658,22 @@ function extractReplayChunks(message) {
|
||||
});
|
||||
continue;
|
||||
}
|
||||
+ if (role === "assistant" && typedBlock.type === "toolCall") {
|
||||
+ const toolCallId = normalizeOptionalString(typedBlock.id);
|
||||
+ const name = normalizeOptionalString(typedBlock.name);
|
||||
+ if (!toolCallId) continue;
|
||||
+ const args = asOptionalRecord(typedBlock.arguments);
|
||||
+ replayChunks.push({
|
||||
+ sessionUpdate: "tool_call",
|
||||
+ toolCallId,
|
||||
+ title: formatToolTitle(name, args),
|
||||
+ status: "in_progress",
|
||||
+ rawInput: args,
|
||||
+ kind: inferToolKind(name),
|
||||
+ locations: extractToolCallLocations(args)
|
||||
+ });
|
||||
+ continue;
|
||||
+ }
|
||||
if (role === "assistant" && typedBlock.type === "thinking" && typeof typedBlock.thinking === "string" && typedBlock.thinking) replayChunks.push({
|
||||
sessionUpdate: "agent_thought_chunk",
|
||||
text: typedBlock.thinking
|
||||
@@ -1849,6 +1882,7 @@ const ACP_AGENT_INFO = {
|
||||
const MAX_PROMPT_BYTES = 2 * 1024 * 1024;
|
||||
const ACP_LOAD_SESSION_REPLAY_LIMIT = 1e6;
|
||||
const ACP_GATEWAY_DISCONNECT_GRACE_MS = 5e3;
|
||||
+const ACP_GATEWAY_ACCEPTED_PROMPT_RECOVERY_GRACE_MS = 6e4;
|
||||
function normalizedChatSendAckStatus(status) {
|
||||
return typeof status === "string" ? status.trim().toLowerCase() : "";
|
||||
}
|
||||
@@ -1959,10 +1993,13 @@ var AcpGatewayAgent = class {
|
||||
}
|
||||
handleGatewayReconnect() {
|
||||
this.log("gateway reconnected");
|
||||
+ const subscriptionReady = Promise.all([...this.pendingPrompts.values()].map((pending) => this.gateway.request("sessions.messages.subscribe", { key: pending.sessionKey }).catch((err) => {
|
||||
+ this.log(`session message subscription failed for ${pending.sessionKey}: ${String(err)}`);
|
||||
+ })));
|
||||
const disconnectContext = this.activeDisconnectContext;
|
||||
this.activeDisconnectContext = null;
|
||||
- if (!disconnectContext) return;
|
||||
- this.reconcilePendingPrompts(disconnectContext.generation, false);
|
||||
+ if (!disconnectContext) return subscriptionReady;
|
||||
+ return subscriptionReady.then(() => this.reconcilePendingPrompts(disconnectContext.generation, false));
|
||||
}
|
||||
handleGatewayDisconnect(reason) {
|
||||
this.log(`gateway disconnected: ${reason}`);
|
||||
@@ -1985,7 +2022,7 @@ var AcpGatewayAgent = class {
|
||||
this.handleExecApprovalRequestEvent(evt);
|
||||
return;
|
||||
}
|
||||
- if (evt.event === "agent") await this.handleAgentEvent(evt);
|
||||
+ if (evt.event === "agent" || evt.event === "session.tool") await this.handleAgentEvent(evt);
|
||||
}
|
||||
async initialize(params) {
|
||||
this.clientCapabilities = normalizeClientCapabilities(params.clientCapabilities);
|
||||
@@ -2299,7 +2336,8 @@ var AcpGatewayAgent = class {
|
||||
};
|
||||
sendWithProvenanceFallback().catch((err) => {
|
||||
const promptKey = this.pendingPromptKey(params.sessionId, runId);
|
||||
- if (isGatewayCloseError(err) && (this.getPendingPrompt(params.sessionId, runId) || this.settlingPromptKeys.has(promptKey))) return;
|
||||
+ const pending = this.pendingPrompts.get(params.sessionId);
|
||||
+ if (isGatewayCloseError(err) && (this.getPendingPrompt(params.sessionId, runId) || pending?.resumedRunIds?.has(runId) || this.settlingPromptKeys.has(promptKey))) return;
|
||||
this.clearApprovalRelaysForPrompt(params.sessionId, runId, { denyActive: true });
|
||||
this.pendingPrompts.delete(params.sessionId);
|
||||
this.sessionStore.clearActiveRun(params.sessionId);
|
||||
@@ -2336,6 +2374,7 @@ var AcpGatewayAgent = class {
|
||||
const data = payload.data;
|
||||
const sessionKey = payload.sessionKey;
|
||||
if (!stream || !data || !sessionKey) return;
|
||||
+ const pending = this.findPendingBySessionKey(sessionKey, runId, payload.resumedFromRunId);
|
||||
if (stream === "approval") {
|
||||
await this.handleApprovalEvent({
|
||||
sessionKey,
|
||||
@@ -2349,7 +2388,6 @@ var AcpGatewayAgent = class {
|
||||
const name = data.name;
|
||||
const toolCallId = data.toolCallId;
|
||||
if (!toolCallId) return;
|
||||
- const pending = this.findPendingBySessionKey(sessionKey, runId);
|
||||
if (!pending) return;
|
||||
if (phase === "start") {
|
||||
if (!pending.toolCalls) pending.toolCalls = /* @__PURE__ */ new Map();
|
||||
@@ -2442,13 +2480,15 @@ var AcpGatewayAgent = class {
|
||||
if (!sessionKey) return;
|
||||
this.startApprovalRelay({
|
||||
sessionKey,
|
||||
+ runId: normalizeOptionalString(request?.runId),
|
||||
+ resumedFromRunId: normalizeOptionalString(request?.resumedFromRunId),
|
||||
approvalEvent
|
||||
});
|
||||
}
|
||||
startApprovalRelay(params) {
|
||||
const approvalEvent = params.approvalEvent;
|
||||
if (this.approvalRelays.has(approvalEvent.approvalId)) return;
|
||||
- const pending = params.runId ? this.findPendingBySessionKey(params.sessionKey, params.runId) : this.findUniquePendingBySessionKey(params.sessionKey);
|
||||
+ const pending = params.runId ? this.findPendingBySessionKey(params.sessionKey, params.runId, params.resumedFromRunId) : this.findUniquePendingBySessionKey(params.sessionKey);
|
||||
if (!pending) return;
|
||||
const relay = {
|
||||
approvalId: approvalEvent.approvalId,
|
||||
@@ -2526,7 +2566,7 @@ var AcpGatewayAgent = class {
|
||||
const runId = payload.runId;
|
||||
const messageData = payload.message;
|
||||
if (!sessionKey || !state) return;
|
||||
- const pending = this.findPendingBySessionKey(sessionKey, runId);
|
||||
+ const pending = this.findPendingBySessionKey(sessionKey, runId, payload.resumedFromRunId);
|
||||
if (!pending) return;
|
||||
if (messageData && (state === "delta" || state === "final")) {
|
||||
await this.handleDeltaEvent(pending.sessionId, messageData);
|
||||
@@ -2619,18 +2659,55 @@ var AcpGatewayAgent = class {
|
||||
this.settlingPromptKeys.delete(promptKey);
|
||||
}
|
||||
}
|
||||
- findPendingBySessionKey(sessionKey, runId) {
|
||||
+ findPendingBySessionKey(sessionKey, runId, resumedFromRunId) {
|
||||
for (const pending of this.pendingPrompts.values()) {
|
||||
if (pending.sessionKey !== sessionKey) continue;
|
||||
if (runId && pending.idempotencyKey !== runId) continue;
|
||||
return pending;
|
||||
}
|
||||
+ if (runId && resumedFromRunId) for (const pending of this.pendingPrompts.values()) {
|
||||
+ if (pending.idempotencyKey !== resumedFromRunId) continue;
|
||||
+ this.reconcilePendingSessionKey(pending, sessionKey);
|
||||
+ this.adoptResumedRun(pending, runId);
|
||||
+ return pending;
|
||||
+ }
|
||||
if (runId) for (const pending of this.pendingPrompts.values()) {
|
||||
if (pending.idempotencyKey !== runId) continue;
|
||||
this.reconcilePendingSessionKey(pending, sessionKey);
|
||||
return pending;
|
||||
}
|
||||
}
|
||||
+ adoptResumedRun(pending, runId) {
|
||||
+ const previousRunId = pending.idempotencyKey;
|
||||
+ this.log(`prompt run resumed: ${previousRunId} -> ${runId}`);
|
||||
+ this.clearApprovalRelaysForPrompt(pending.sessionId, previousRunId, { denyActive: true });
|
||||
+ pending.resumedRunIds ??= /* @__PURE__ */ new Set();
|
||||
+ pending.resumedRunIds.add(previousRunId);
|
||||
+ pending.idempotencyKey = runId;
|
||||
+ pending.sendAccepted = true;
|
||||
+ pending.disconnectContext = void 0;
|
||||
+ for (const [toolCallId, toolCall] of pending.toolCalls ?? []) this.sessionUpdates.emit({
|
||||
+ sessionId: pending.sessionId,
|
||||
+ sessionKey: pending.sessionKey,
|
||||
+ ...pending.ledgerSessionId ? { ledgerSessionId: pending.ledgerSessionId } : {},
|
||||
+ runId: previousRunId,
|
||||
+ record: true,
|
||||
+ waitForDelivery: false,
|
||||
+ update: {
|
||||
+ sessionUpdate: "tool_call_update",
|
||||
+ toolCallId,
|
||||
+ status: "failed",
|
||||
+ locations: toolCall.locations
|
||||
+ }
|
||||
+ });
|
||||
+ pending.sentTextLength = 0;
|
||||
+ pending.sentText = void 0;
|
||||
+ pending.sentThoughtLength = 0;
|
||||
+ pending.sentThought = void 0;
|
||||
+ pending.toolCalls = void 0;
|
||||
+ const session = this.sessionStore.getSession(pending.sessionId);
|
||||
+ if (session?.abortController) this.sessionStore.setActiveRun(pending.sessionId, runId, session.abortController);
|
||||
+ }
|
||||
findUniquePendingBySessionKey(sessionKey) {
|
||||
let match;
|
||||
for (const pending of this.pendingPrompts.values()) {
|
||||
@@ -2652,12 +2729,12 @@ var AcpGatewayAgent = class {
|
||||
clearTimeout(this.disconnectTimer);
|
||||
this.disconnectTimer = null;
|
||||
}
|
||||
- armDisconnectTimer(disconnectContext) {
|
||||
+ armDisconnectTimer(disconnectContext, deadline = "initial") {
|
||||
this.clearDisconnectTimer();
|
||||
this.disconnectTimer = setTimeout(() => {
|
||||
this.disconnectTimer = null;
|
||||
- this.reconcilePendingPrompts(disconnectContext.generation, true);
|
||||
- }, ACP_GATEWAY_DISCONNECT_GRACE_MS);
|
||||
+ this.reconcilePendingPrompts(disconnectContext.generation, deadline);
|
||||
+ }, deadline === "initial" ? ACP_GATEWAY_DISCONNECT_GRACE_MS : ACP_GATEWAY_ACCEPTED_PROMPT_RECOVERY_GRACE_MS - ACP_GATEWAY_DISCONNECT_GRACE_MS);
|
||||
this.disconnectTimer.unref?.();
|
||||
}
|
||||
rejectPendingPrompt(pending, error) {
|
||||
@@ -2668,14 +2745,10 @@ var AcpGatewayAgent = class {
|
||||
if (this.pendingPrompts.size === 0) this.clearDisconnectTimer();
|
||||
pending.reject(error);
|
||||
}
|
||||
- clearPendingDisconnectState(pending, disconnectContext) {
|
||||
- if (pending.disconnectContext !== disconnectContext) return;
|
||||
- pending.disconnectContext = void 0;
|
||||
+ shouldRejectPendingAtDisconnectDeadline(pending, disconnectContext, deadline) {
|
||||
+ return pending.disconnectContext === disconnectContext && (!pending.sendAccepted || deadline === "accepted-recovery");
|
||||
}
|
||||
- shouldRejectPendingAtDisconnectDeadline(pending, disconnectContext) {
|
||||
- return pending.disconnectContext === disconnectContext && (!pending.sendAccepted || this.activeDisconnectContext?.generation === disconnectContext.generation);
|
||||
- }
|
||||
- async reconcilePendingPrompts(observedDisconnectGeneration, deadlineExpired) {
|
||||
+ async reconcilePendingPrompts(observedDisconnectGeneration, deadline) {
|
||||
if (this.pendingPrompts.size === 0) {
|
||||
if (this.disconnectGeneration === observedDisconnectGeneration) this.clearDisconnectTimer();
|
||||
return;
|
||||
@@ -2685,32 +2758,35 @@ var AcpGatewayAgent = class {
|
||||
for (const [sessionId, pending] of pendingEntries) {
|
||||
if (this.pendingPrompts.get(sessionId) !== pending) continue;
|
||||
if (pending.disconnectContext?.generation !== observedDisconnectGeneration) continue;
|
||||
- if (await this.reconcilePendingPrompt(sessionId, pending, deadlineExpired)) keepDisconnectTimer = true;
|
||||
+ if (await this.reconcilePendingPrompt(sessionId, pending, deadline)) keepDisconnectTimer = true;
|
||||
}
|
||||
- if (!keepDisconnectTimer && this.disconnectGeneration === observedDisconnectGeneration) this.clearDisconnectTimer();
|
||||
+ if (keepDisconnectTimer && deadline === "initial" && this.disconnectGeneration === observedDisconnectGeneration) {
|
||||
+ const disconnectContext = pendingEntries.map(([, pending]) => pending.disconnectContext).find((context) => context?.generation === observedDisconnectGeneration);
|
||||
+ if (disconnectContext) this.armDisconnectTimer(disconnectContext, "accepted-recovery");
|
||||
+ } else if (!keepDisconnectTimer && this.disconnectGeneration === observedDisconnectGeneration) this.clearDisconnectTimer();
|
||||
}
|
||||
- async reconcilePendingPrompt(sessionId, pending, deadlineExpired) {
|
||||
+ async reconcilePendingPrompt(sessionId, pending, deadline) {
|
||||
const disconnectContext = pending.disconnectContext;
|
||||
if (!disconnectContext) return false;
|
||||
+ const waitedRunId = pending.idempotencyKey;
|
||||
let result;
|
||||
try {
|
||||
result = await this.gateway.request("agent.wait", {
|
||||
- runId: pending.idempotencyKey,
|
||||
+ runId: waitedRunId,
|
||||
timeoutMs: 0
|
||||
}, { timeoutMs: null });
|
||||
} catch (err) {
|
||||
- this.log(`agent.wait reconcile failed for ${pending.idempotencyKey}: ${String(err)}`);
|
||||
- if (deadlineExpired) {
|
||||
- if (this.shouldRejectPendingAtDisconnectDeadline(pending, disconnectContext)) {
|
||||
+ this.log(`agent.wait reconcile failed for ${waitedRunId}: ${String(err)}`);
|
||||
+ if (deadline) {
|
||||
+ if (this.shouldRejectPendingAtDisconnectDeadline(pending, disconnectContext, deadline)) {
|
||||
this.rejectPendingPrompt(pending, /* @__PURE__ */ new Error(`Gateway disconnected: ${disconnectContext.reason}`));
|
||||
return false;
|
||||
}
|
||||
- this.clearPendingDisconnectState(pending, disconnectContext);
|
||||
- return false;
|
||||
+ return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
- const currentPending = this.getPendingPrompt(sessionId, pending.idempotencyKey);
|
||||
+ const currentPending = this.getPendingPrompt(sessionId, waitedRunId);
|
||||
if (!currentPending) return false;
|
||||
if (result?.status === "ok") {
|
||||
await this.finishPrompt(sessionId, currentPending, "end_turn");
|
||||
@@ -2720,15 +2796,14 @@ var AcpGatewayAgent = class {
|
||||
this.finishPrompt(sessionId, currentPending, "end_turn");
|
||||
return false;
|
||||
}
|
||||
- if (deadlineExpired) {
|
||||
- if (this.shouldRejectPendingAtDisconnectDeadline(currentPending, disconnectContext)) {
|
||||
+ if (deadline) {
|
||||
+ if (this.shouldRejectPendingAtDisconnectDeadline(currentPending, disconnectContext, deadline)) {
|
||||
const currentDisconnectContext = currentPending.disconnectContext;
|
||||
if (!currentDisconnectContext) return false;
|
||||
this.rejectPendingPrompt(currentPending, /* @__PURE__ */ new Error(`Gateway disconnected: ${currentDisconnectContext.reason}`));
|
||||
return false;
|
||||
}
|
||||
- this.clearPendingDisconnectState(currentPending, disconnectContext);
|
||||
- return false;
|
||||
+ return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -2890,13 +2965,13 @@ var AcpGatewayAgent = class {
|
||||
const replayChunks = extractReplayChunks(message);
|
||||
for (const chunk of replayChunks) await this.sessionUpdates.emit({
|
||||
sessionId,
|
||||
- update: {
|
||||
+ update: chunk.sessionUpdate === "user_message_chunk" || chunk.sessionUpdate === "agent_message_chunk" || chunk.sessionUpdate === "agent_thought_chunk" ? {
|
||||
sessionUpdate: chunk.sessionUpdate,
|
||||
content: {
|
||||
type: "text",
|
||||
text: chunk.text
|
||||
}
|
||||
- }
|
||||
+ } : chunk
|
||||
});
|
||||
}
|
||||
}
|
||||
diff --git a/dist/agent-D6kiZtPt.js b/dist/agent-D6kiZtPt.js
|
||||
index af55bc0d0faa6c4abdcb72e6f905cc3774784508..c9438a1fad0004d7e8a812e17e3af69b52844371 100644
|
||||
--- a/dist/agent-D6kiZtPt.js
|
||||
+++ b/dist/agent-D6kiZtPt.js
|
||||
@@ -941,6 +941,15 @@ const agentHandlers = {
|
||||
let resolvedGroupSpace = normalizedSpawned.groupSpace;
|
||||
let spawnedByValue;
|
||||
const inputProvenance = normalizeInputProvenance(request.inputProvenance);
|
||||
+ const isRestartRecoveryResumeRun = canUseInternalRuntimeHandoff && inputProvenance?.kind === "internal_system" && inputProvenance.sourceTool === "main_session_restart_recovery";
|
||||
+ if (request.internalRestartRecoverySourceRunId !== void 0 && !isRestartRecoveryResumeRun) {
|
||||
+ respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "restart recovery run lineage is reserved for main-session restart recovery."));
|
||||
+ return;
|
||||
+ }
|
||||
+ if (request.internalRestartRecoverySourceRunId !== void 0 && request.internalRestartRecoverySourceRunId === runId) {
|
||||
+ respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "restart recovery must use a distinct run id from its source run."));
|
||||
+ return;
|
||||
+ }
|
||||
const preserveUserFacingSessionModelState = canUseInternalRuntimeHandoff && shouldPreserveUserFacingSessionStateForInputProvenance(inputProvenance);
|
||||
const sessionEffects = requestedInternalSessionEffects ? "internal" : request.sessionEffects;
|
||||
const suppressVisibleSessionEffects = sessionEffects === "internal";
|
||||
@@ -2192,7 +2201,8 @@ const agentHandlers = {
|
||||
lifecycleGeneration
|
||||
} : {
|
||||
sessionKey: resolvedSessionKey,
|
||||
- lifecycleGeneration
|
||||
+ lifecycleGeneration,
|
||||
+ ...request.internalRestartRecoverySourceRunId ? { resumedFromRunId: request.internalRestartRecoverySourceRunId } : {}
|
||||
});
|
||||
}
|
||||
const resolvedThreadId = explicitThreadId ?? deliveryPlan.resolvedThreadId;
|
||||
diff --git a/dist/agent-events-CRggPZCM.js b/dist/agent-events-CRggPZCM.js
|
||||
index d219ef4933d12a485f9f29bc056fd8d39594d0a1..9f6c72ec51940362d08467068aff26a377d15f7b 100644
|
||||
--- a/dist/agent-events-CRggPZCM.js
|
||||
+++ b/dist/agent-events-CRggPZCM.js
|
||||
@@ -56,6 +56,7 @@ function registerAgentRunContext(runId, context) {
|
||||
if (context.lifecycleGeneration && existing.lifecycleGeneration && context.lifecycleGeneration !== existing.lifecycleGeneration) return;
|
||||
if (context.sessionKey && existing.sessionKey !== context.sessionKey) existing.sessionKey = context.sessionKey;
|
||||
if (context.sessionId && existing.sessionId !== context.sessionId) existing.sessionId = context.sessionId;
|
||||
+ if (context.resumedFromRunId && existing.resumedFromRunId !== context.resumedFromRunId) existing.resumedFromRunId = context.resumedFromRunId;
|
||||
if (context.agentId && existing.agentId !== context.agentId) existing.agentId = context.agentId;
|
||||
if (context.verboseLevel && existing.verboseLevel !== context.verboseLevel) existing.verboseLevel = context.verboseLevel;
|
||||
if (context.isControlUiVisible !== void 0) existing.isControlUiVisible = context.isControlUiVisible;
|
||||
@@ -182,11 +183,13 @@ function enrichAgentEvent(event) {
|
||||
const sessionId = event.stream === "lifecycle" ? event.sessionId ?? context?.sessionId : event.sessionId;
|
||||
const lifecycleGeneration = event.stream === "lifecycle" ? ownedLifecycleGeneration ?? state.lifecycleGeneration : ownedLifecycleGeneration;
|
||||
const agentId = event.agentId ?? context?.agentId;
|
||||
+ const resumedFromRunId = context?.resumedFromRunId;
|
||||
const enriched = {
|
||||
...event,
|
||||
sessionKey,
|
||||
...sessionId ? { sessionId } : {},
|
||||
...agentId ? { agentId } : {},
|
||||
+ ...resumedFromRunId ? { resumedFromRunId } : {},
|
||||
seq: nextSeq,
|
||||
ts: Date.now()
|
||||
};
|
||||
diff --git a/dist/agent-tools-BD8WL7ny.js b/dist/agent-tools-BD8WL7ny.js
|
||||
index c16af06126dfcd8070fa0eb203799aba7a16bd95..75fe92b0994ced97fc8f7014a76dacb0c4dedc65 100644
|
||||
--- a/dist/agent-tools-BD8WL7ny.js
|
||||
+++ b/dist/agent-tools-BD8WL7ny.js
|
||||
@@ -1132,0 +1133 @@ function createOpenClawCodingTools(options) {
|
||||
+ runId: options?.runId,
|
||||
diff --git a/dist/bash-tools-DHyGpWCr.js b/dist/bash-tools-DHyGpWCr.js
|
||||
index def4eecc41bf57190eac84a5df1dcf1d7c70664b..ba0013d6148de2410bad04b392e84a8ab3a8529d 100644
|
||||
--- a/dist/bash-tools-DHyGpWCr.js
|
||||
+++ b/dist/bash-tools-DHyGpWCr.js
|
||||
@@ -100,0 +101,3 @@ function buildExecApprovalRequestToolParams(params) {
|
||||
+ sessionId: params.sessionId,
|
||||
+ runId: params.runId,
|
||||
+ toolCallId: params.toolCallId,
|
||||
@@ -214,0 +218,3 @@ async function buildHostApprovalDecisionParams(params) {
|
||||
+ sessionId: params.sessionId,
|
||||
+ runId: params.runId,
|
||||
+ toolCallId: params.toolCallId,
|
||||
@@ -1146,0 +1153,3 @@ async function processGatewayAllowlist(params) {
|
||||
+ sessionId: params.sessionId,
|
||||
+ runId: params.runId,
|
||||
+ toolCallId: params.toolCallId,
|
||||
@@ -1879,0 +1889,2 @@ async function executeNodeHostCommand(params) {
|
||||
+ runId: params.runId,
|
||||
+ toolCallId: params.toolCallId,
|
||||
@@ -3216 +3227 @@ function createExecTool(defaults) {
|
||||
- execute: async (_toolCallId, args, signal, onUpdate) => {
|
||||
+ execute: async (toolCallId, args, signal, onUpdate) => {
|
||||
@@ -3375,0 +3387,2 @@ function createExecTool(defaults) {
|
||||
+ toolCallId,
|
||||
+ runId: defaults?.runId,
|
||||
@@ -3427,0 +3441,2 @@ function createExecTool(defaults) {
|
||||
+ runId: defaults?.runId,
|
||||
+ toolCallId,
|
||||
diff --git a/dist/exec-approval-DRfKKxhu.js b/dist/exec-approval-DRfKKxhu.js
|
||||
index f76d64d4041f579301e4ffacc38a20a7887567ef..eb4243089d8329eab00d9af9b106b46ef260df8c 100644
|
||||
--- a/dist/exec-approval-DRfKKxhu.js
|
||||
+++ b/dist/exec-approval-DRfKKxhu.js
|
||||
@@ -10,6 +10,7 @@ import { i as sanitizeExecApprovalWarningText, n as sanitizeExecApprovalDisplayT
|
||||
import { t as analyzeCommandForPolicy } from "./policy-X6MHg5ni.js";
|
||||
import { a as buildSystemRunApprovalEnvBinding, i as buildSystemRunApprovalBinding } from "./system-run-command-Bd_agqvl.js";
|
||||
import { n as resolveSystemRunApprovalRequestContext } from "./system-run-approval-context-B9ONWoZl.js";
|
||||
+import { f as getAgentRunContext } from "./agent-events-CRggPZCM.js";
|
||||
import { a as handleApprovalWaitDecision, c as listVisiblePendingApprovalRequests, f as resolvePendingApprovalRecord, i as handleApprovalResolve, l as registerPendingApprovalRecord, n as bindApprovalReviewerDeviceIds, o as handlePendingApprovalRequest, p as respondPendingApprovalLookupError, r as buildRequestedApprovalEvent, s as isApprovalRecordVisibleToClient, t as bindApprovalRequesterMetadata, u as resolveApprovalDecisionParams } from "./approval-shared-BKEyXMsJ.js";
|
||||
//#region src/infra/command-analysis/explain.ts
|
||||
function riskLabel(risk) {
|
||||
@@ -225,6 +226,8 @@ function createExecApprovalHandlers(manager, opts) {
|
||||
return;
|
||||
}
|
||||
const unavailableDecisions = normalizeExecApprovalUnavailableDecisions(p.unavailableDecisions);
|
||||
+ const requestRunId = normalizeOptionalString(p.runId);
|
||||
+ const resumedFromRunId = requestRunId ? getAgentRunContext(requestRunId)?.resumedFromRunId : void 0;
|
||||
const request = {
|
||||
command: sanitizedCommandText,
|
||||
commandPreview: host === "node" || !approvalContext.commandPreview ? void 0 : sanitizeExecApprovalDisplayText(approvalContext.commandPreview),
|
||||
@@ -248,6 +251,10 @@ function createExecApprovalHandlers(manager, opts) {
|
||||
agentId: effectiveAgentId ?? null,
|
||||
resolvedPath: p.resolvedPath ?? null,
|
||||
sessionKey: effectiveSessionKey ?? null,
|
||||
+ sessionId: normalizeOptionalString(p.sessionId) ?? null,
|
||||
+ runId: requestRunId ?? null,
|
||||
+ toolCallId: normalizeOptionalString(p.toolCallId) ?? null,
|
||||
+ ...resumedFromRunId ? { resumedFromRunId } : {},
|
||||
turnSourceChannel: normalizeOptionalString(p.turnSourceChannel) ?? null,
|
||||
turnSourceTo: normalizeOptionalString(p.turnSourceTo) ?? null,
|
||||
turnSourceAccountId: normalizeOptionalString(p.turnSourceAccountId) ?? null,
|
||||
diff --git a/dist/exec-approvals-bouecjdj.d.ts b/dist/exec-approvals-bouecjdj.d.ts
|
||||
index c0a416851d957f1edd9e675fb74d69c4f3d1fdbb..30e33fc6fe5f31ee393ad25d0f2b24990ae0b5fe 100644
|
||||
--- a/dist/exec-approvals-bouecjdj.d.ts
|
||||
+++ b/dist/exec-approvals-bouecjdj.d.ts
|
||||
@@ -382,6 +382,10 @@ type ExecApprovalRequestPayload = {
|
||||
agentId?: string | null;
|
||||
resolvedPath?: string | null;
|
||||
sessionKey?: string | null;
|
||||
+ sessionId?: string | null;
|
||||
+ runId?: string | null;
|
||||
+ resumedFromRunId?: string;
|
||||
+ toolCallId?: string | null;
|
||||
turnSourceChannel?: string | null;
|
||||
turnSourceTo?: string | null;
|
||||
turnSourceAccountId?: string | null;
|
||||
diff --git a/dist/main-session-restart-recovery-Ce8fihTV.js b/dist/main-session-restart-recovery-Ce8fihTV.js
|
||||
index ba698afb03ca7043d475948afcbb18643719a011..5c976a1d0f5fc9b85e76f65684eaba2c2b42d887 100644
|
||||
--- a/dist/main-session-restart-recovery-Ce8fihTV.js
|
||||
+++ b/dist/main-session-restart-recovery-Ce8fihTV.js
|
||||
@@ -211,6 +211,8 @@ async function markStartupOrphanedMainSessionsForRecovery(params) {
|
||||
entry,
|
||||
sessionKey
|
||||
})) continue;
|
||||
+ const sourceRunId = normalizeOptionalString(entry.lifecycleRunId);
|
||||
+ if (sourceRunId) entry.restartRecoveryDeliverySourceRunId = sourceRunId;
|
||||
entry.abortedLastRun = true;
|
||||
entry.updatedAt = Date.now();
|
||||
replacements.push({
|
||||
@@ -350,6 +352,8 @@ function resolveRestartRecoveryDeliveryContext(params) {
|
||||
}
|
||||
async function resumeMainSession(params) {
|
||||
const sanitizedPendingText = typeof params.pendingFinalDeliveryText === "string" ? sanitizePendingFinalDeliveryText(params.pendingFinalDeliveryText) : "";
|
||||
+ const sourceRunIds = [...new Set((params.entry.restartRecoveryRuns ?? []).map((run) => typeof run.runId === "string" ? run.runId.trim() : "").filter(Boolean))];
|
||||
+ const sourceRunId = normalizeOptionalString(params.entry.restartRecoveryDeliverySourceRunId) ?? (sourceRunIds.length === 1 ? sourceRunIds[0] : void 0);
|
||||
const deliveryContext = resolveRestartRecoveryDeliveryContext({
|
||||
cfg: params.cfg,
|
||||
entry: params.entry,
|
||||
@@ -359,9 +363,15 @@ async function resumeMainSession(params) {
|
||||
const agentParams = {
|
||||
message: buildResumeMessage(sanitizedPendingText),
|
||||
sessionKey: params.sessionKey,
|
||||
+ ...sourceRunId ? { internalRestartRecoverySourceRunId: sourceRunId } : {},
|
||||
idempotencyKey: crypto.randomUUID(),
|
||||
deliver: Boolean(deliveryContext),
|
||||
- lane: "main"
|
||||
+ lane: "main",
|
||||
+ inputProvenance: {
|
||||
+ kind: "internal_system",
|
||||
+ sourceSessionKey: params.sessionKey,
|
||||
+ sourceTool: "main_session_restart_recovery"
|
||||
+ }
|
||||
};
|
||||
if (deliveryContext) {
|
||||
agentParams.channel = deliveryContext.channel;
|
||||
diff --git a/dist/schema-BuOFpc7K.js b/dist/schema-BuOFpc7K.js
|
||||
index 50b16b4a3f159b698c6485e079e1c38ea41fda78..03ee242b547c3b841cc6a5819a4d0349a300228e 100644
|
||||
--- a/dist/schema-BuOFpc7K.js
|
||||
+++ b/dist/schema-BuOFpc7K.js
|
||||
@@ -138,6 +138,7 @@ const AgentInternalEventSchema = Type.Object({
|
||||
/** Stream event emitted by the agent runtime over the gateway protocol. */
|
||||
const AgentEventSchema = Type.Object({
|
||||
runId: NonEmptyString,
|
||||
+ resumedFromRunId: Type.Optional(NonEmptyString),
|
||||
seq: Type.Integer({ minimum: 0 }),
|
||||
stream: NonEmptyString,
|
||||
ts: Type.Integer({ minimum: 0 }),
|
||||
@@ -280,6 +281,7 @@ const AgentParamsSchema = Type.Object({
|
||||
])),
|
||||
acpTurnSource: Type.Optional(Type.Literal("manual_spawn")),
|
||||
internalRuntimeHandoffId: Type.Optional(NonEmptyString),
|
||||
+ internalRestartRecoverySourceRunId: Type.Optional(NonEmptyString),
|
||||
execApprovalFollowupExpectedSessionId: Type.Optional(NonEmptyString),
|
||||
internalEvents: Type.Optional(Type.Array(AgentInternalEventSchema)),
|
||||
inputProvenance: Type.Optional(InputProvenanceSchema),
|
||||
@@ -2910,0 +2913,3 @@ const ExecApprovalRequestParamsSchema = Type.Object({
|
||||
+ sessionId: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
||||
+ runId: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
||||
+ toolCallId: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
||||
@@ -3304,6 +3309,7 @@ const ChatInjectParamsSchema = Type.Object({
|
||||
/** Shared event fields preserve stream ordering and route events to the right session. */
|
||||
const ChatEventBaseSchema = {
|
||||
runId: NonEmptyString,
|
||||
+ resumedFromRunId: Type.Optional(NonEmptyString),
|
||||
sessionKey: NonEmptyString,
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
spawnedBy: Type.Optional(NonEmptyString),
|
||||
diff --git a/dist/schema-DtyqV_v0.d.ts b/dist/schema-DtyqV_v0.d.ts
|
||||
index ff8cd2829dc9f64d7fc8ec2bf77bb5f05947459d..f2f08299de269d42714b491343006250be0db4aa 100644
|
||||
--- a/dist/schema-DtyqV_v0.d.ts
|
||||
+++ b/dist/schema-DtyqV_v0.d.ts
|
||||
@@ -4,6 +4,7 @@ import { Static, TSchema, Type } from "typebox";
|
||||
/** Stream event emitted by the agent runtime over the gateway protocol. */
|
||||
declare const AgentEventSchema: Type.TObject<{
|
||||
runId: Type.TString;
|
||||
+ resumedFromRunId: Type.TOptional<Type.TString>;
|
||||
seq: Type.TInteger;
|
||||
stream: Type.TString;
|
||||
ts: Type.TInteger;
|
||||
@@ -110,6 +111,7 @@ declare const AgentParamsSchema: Type.TObject<{
|
||||
bootstrapContextRunKind: Type.TOptional<Type.TUnion<[Type.TLiteral<"default">, Type.TLiteral<"heartbeat">, Type.TLiteral<"cron">]>>;
|
||||
acpTurnSource: Type.TOptional<Type.TLiteral<"manual_spawn">>;
|
||||
internalRuntimeHandoffId: Type.TOptional<Type.TString>;
|
||||
+ internalRestartRecoverySourceRunId: Type.TOptional<Type.TString>;
|
||||
execApprovalFollowupExpectedSessionId: Type.TOptional<Type.TString>;
|
||||
internalEvents: Type.TOptional<Type.TArray<Type.TObject<{
|
||||
type: Type.TLiteral<"task_completion">;
|
||||
@@ -3065,6 +3067,7 @@ declare const ProtocolSchemas: {
|
||||
bootstrapContextRunKind: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TLiteral<"default">, import("typebox").TLiteral<"heartbeat">, import("typebox").TLiteral<"cron">]>>;
|
||||
acpTurnSource: import("typebox").TOptional<import("typebox").TLiteral<"manual_spawn">>;
|
||||
internalRuntimeHandoffId: import("typebox").TOptional<import("typebox").TString>;
|
||||
+ internalRestartRecoverySourceRunId: import("typebox").TOptional<import("typebox").TString>;
|
||||
execApprovalFollowupExpectedSessionId: import("typebox").TOptional<import("typebox").TString>;
|
||||
internalEvents: import("typebox").TOptional<import("typebox").TArray<import("typebox").TObject<{
|
||||
type: import("typebox").TLiteral<"task_completion">;
|
||||
@@ -7573,6 +7576,9 @@ declare const ExecApprovalRequestParamsSchema: Type.TObject<{
|
||||
agentId: Type.TOptional<Type.TUnion<[Type.TString, Type.TNull]>>;
|
||||
resolvedPath: Type.TOptional<Type.TUnion<[Type.TString, Type.TNull]>>;
|
||||
sessionKey: Type.TOptional<Type.TUnion<[Type.TString, Type.TNull]>>;
|
||||
+ sessionId: Type.TOptional<Type.TUnion<[Type.TString, Type.TNull]>>;
|
||||
+ runId: Type.TOptional<Type.TUnion<[Type.TString, Type.TNull]>>;
|
||||
+ toolCallId: Type.TOptional<Type.TUnion<[Type.TString, Type.TNull]>>;
|
||||
turnSourceChannel: Type.TOptional<Type.TUnion<[Type.TString, Type.TNull]>>;
|
||||
turnSourceTo: Type.TOptional<Type.TUnion<[Type.TString, Type.TNull]>>;
|
||||
turnSourceAccountId: Type.TOptional<Type.TUnion<[Type.TString, Type.TNull]>>;
|
||||
@@ -7867,6 +7873,7 @@ declare const ChatEventSchema: Type.TUnion<[Type.TObject<{
|
||||
replace: Type.TOptional<Type.TBoolean>;
|
||||
usage: Type.TOptional<Type.TUnknown>;
|
||||
runId: Type.TString;
|
||||
+ resumedFromRunId: Type.TOptional<Type.TString>;
|
||||
sessionKey: Type.TString;
|
||||
agentId: Type.TOptional<Type.TString>;
|
||||
spawnedBy: Type.TOptional<Type.TString>;
|
||||
@@ -7877,6 +7884,7 @@ declare const ChatEventSchema: Type.TUnion<[Type.TObject<{
|
||||
usage: Type.TOptional<Type.TUnknown>;
|
||||
stopReason: Type.TOptional<Type.TString>;
|
||||
runId: Type.TString;
|
||||
+ resumedFromRunId: Type.TOptional<Type.TString>;
|
||||
sessionKey: Type.TString;
|
||||
agentId: Type.TOptional<Type.TString>;
|
||||
spawnedBy: Type.TOptional<Type.TString>;
|
||||
@@ -7887,6 +7895,7 @@ declare const ChatEventSchema: Type.TUnion<[Type.TObject<{
|
||||
errorMessage: Type.TOptional<Type.TString>;
|
||||
stopReason: Type.TOptional<Type.TString>;
|
||||
runId: Type.TString;
|
||||
+ resumedFromRunId: Type.TOptional<Type.TString>;
|
||||
sessionKey: Type.TString;
|
||||
agentId: Type.TOptional<Type.TString>;
|
||||
spawnedBy: Type.TOptional<Type.TString>;
|
||||
@@ -7899,6 +7908,7 @@ declare const ChatEventSchema: Type.TUnion<[Type.TObject<{
|
||||
usage: Type.TOptional<Type.TUnknown>;
|
||||
stopReason: Type.TOptional<Type.TString>;
|
||||
runId: Type.TString;
|
||||
+ resumedFromRunId: Type.TOptional<Type.TString>;
|
||||
sessionKey: Type.TString;
|
||||
agentId: Type.TOptional<Type.TString>;
|
||||
spawnedBy: Type.TOptional<Type.TString>;
|
||||
diff --git a/dist/server-chat-wgxNCdC3.js b/dist/server-chat-wgxNCdC3.js
|
||||
index e03fecec0657ddd63833fd4804efba1d21d2f361..6fa31407a9c30d42cc9d907e2f1d862edf5bba12 100644
|
||||
--- a/dist/server-chat-wgxNCdC3.js
|
||||
+++ b/dist/server-chat-wgxNCdC3.js
|
||||
@@ -544,14 +544,21 @@ function createAgentEventHandler({ broadcast, broadcastToConnIds, nodeSendToSess
|
||||
chatRunState.deltaSentAt.set(clientRunId, now);
|
||||
};
|
||||
const sendChatPayload = (sessionKey, payload, opts) => {
|
||||
+ const payloadRecord = payload && typeof payload === "object" ? payload : void 0;
|
||||
+ const runId = typeof payloadRecord?.runId === "string" ? payloadRecord.runId : void 0;
|
||||
+ const resumedFromRunId = runId ? getAgentRunContext(runId)?.resumedFromRunId : void 0;
|
||||
+ const projectedPayload = resumedFromRunId && payloadRecord ? {
|
||||
+ ...payloadRecord,
|
||||
+ resumedFromRunId
|
||||
+ } : payload;
|
||||
const deliverySessionKey = resolveSessionDeliveryKey(sessionKey, opts?.agentId);
|
||||
if (opts?.controlUiVisible ?? true) {
|
||||
- broadcast("chat", payload, { dropIfSlow: opts?.dropIfSlow });
|
||||
- sendNodeSessionPayloadForAgent(sessionKey, "chat", payload, opts?.agentId);
|
||||
+ broadcast("chat", projectedPayload, { dropIfSlow: opts?.dropIfSlow });
|
||||
+ sendNodeSessionPayloadForAgent(sessionKey, "chat", projectedPayload, opts?.agentId);
|
||||
return;
|
||||
}
|
||||
const recipients = sessionMessageSubscribers.get(deliverySessionKey);
|
||||
- if (recipients.size > 0) broadcastToConnIds("chat", payload, recipients, { dropIfSlow: opts?.dropIfSlow });
|
||||
+ if (recipients.size > 0) broadcastToConnIds("chat", projectedPayload, recipients, { dropIfSlow: opts?.dropIfSlow });
|
||||
};
|
||||
const emitChatTerminal = (sessionKey, clientRunId, sourceRunId, seq, jobState, error, stopReason, errorKind, opts) => {
|
||||
const { text, shouldSuppressSilent } = resolveBufferedChatTextState(clientRunId, sourceRunId, { suppressLeadFragments: false });
|
||||
@@ -721,15 +728,18 @@ function createAgentEventHandler({ broadcast, broadcastToConnIds, nodeSendToSess
|
||||
}
|
||||
if (lifecyclePhase !== null && lifecyclePhase !== "error") clearPendingTerminalLifecycleError(evt.runId);
|
||||
const spawnedBy = sessionKey ? resolveSpawnedBy(sessionKey) : null;
|
||||
+ const resumedFromRunId = runContext?.resumedFromRunId;
|
||||
const agentPayload = sessionKey ? {
|
||||
...eventForClients,
|
||||
sessionKey,
|
||||
...sessionAgentId ? { agentId: sessionAgentId } : {},
|
||||
...spawnedBy && { spawnedBy },
|
||||
- ...isHeartbeat !== void 0 && { isHeartbeat }
|
||||
+ ...isHeartbeat !== void 0 && { isHeartbeat },
|
||||
+ ...resumedFromRunId ? { resumedFromRunId } : {}
|
||||
} : {
|
||||
...eventForClients,
|
||||
- ...isHeartbeat !== void 0 && { isHeartbeat }
|
||||
+ ...isHeartbeat !== void 0 && { isHeartbeat },
|
||||
+ ...resumedFromRunId ? { resumedFromRunId } : {}
|
||||
};
|
||||
const hasSessionMessageSubscribers = sessionKey ? sessionMessageSubscribers.get(resolveSessionDeliveryKey(sessionKey, sessionAgentId)).size > 0 : false;
|
||||
const last = agentRunSeq.get(evt.runId) ?? 0;
|
||||
@@ -799,7 +809,10 @@ function createAgentEventHandler({ broadcast, broadcastToConnIds, nodeSendToSess
|
||||
dropIfSlow: true
|
||||
});
|
||||
if (isControlUiVisible && sessionKey && !suppressHeartbeatToolEvents) {
|
||||
- const sessionSubscribers = excludeConnIds(sessionEventSubscribers.getAll(), runToolRecipients);
|
||||
+ const sessionSubscribers = excludeConnIds(new Set([
|
||||
+ ...sessionEventSubscribers.getAll(),
|
||||
+ ...sessionMessageSubscribers.get(resolveSessionDeliveryKey(sessionKey, sessionAgentId))
|
||||
+ ]), runToolRecipients);
|
||||
if (sessionSubscribers.size > 0) broadcastToConnIds("session.tool", {
|
||||
...agentPayload,
|
||||
...buildSessionEventSnapshot(sessionKey, void 0, sessionAgentId)
|
||||
diff --git a/dist/session-lifecycle-state-Czgc8l0p.js b/dist/session-lifecycle-state-Czgc8l0p.js
|
||||
index db68c120f69a4f2bc4121f3b75a456319fd227ed..21a1198bc7273b8aeeb8c91466a8cf85d32932ef 100644
|
||||
--- a/dist/session-lifecycle-state-Czgc8l0p.js
|
||||
+++ b/dist/session-lifecycle-state-Czgc8l0p.js
|
||||
@@ -97,10 +97,18 @@ function derivePersistedSessionLifecyclePatch(params) {
|
||||
if (remainingRuns.length > 0) return { restartRecoveryRuns: remainingRuns };
|
||||
patch.restartRecoveryRuns = void 0;
|
||||
}
|
||||
- return patch;
|
||||
+ const phase = resolveLifecyclePhase(params.event);
|
||||
+ return {
|
||||
+ ...patch,
|
||||
+ ...phase === "start" ? { lifecycleRunId: runId } : patch.status && patch.status !== "running" ? { lifecycleRunId: void 0 } : {}
|
||||
+ };
|
||||
}
|
||||
function deriveGatewaySessionLifecycleProjectionPatch(params) {
|
||||
- const { restartRecoveryRuns: _restartRecoveryRuns, ...patch } = derivePersistedSessionLifecyclePatch(params);
|
||||
+ const {
|
||||
+ restartRecoveryRuns: _restartRecoveryRuns,
|
||||
+ lifecycleRunId: _lifecycleRunId,
|
||||
+ ...patch
|
||||
+ } = derivePersistedSessionLifecyclePatch(params);
|
||||
return patch;
|
||||
}
|
||||
function isRestartRecoveryLifecycleEvent(params) {
|
||||
diff --git a/dist/store-BJJhlPrk.js b/dist/store-BJJhlPrk.js
|
||||
index 15b9c3b2aea87e1f1782e8669de218d30afd9a00..e62c2a2061b639ba7291d9c19e6e8603beaae19e 100644
|
||||
--- a/dist/store-BJJhlPrk.js
|
||||
+++ b/dist/store-BJJhlPrk.js
|
||||
@@ -147,6 +147,7 @@ const SESSION_ENTRY_RESERVED_SLOT_KEYS = /* @__PURE__ */ new Set([
|
||||
"pluginExtensionSlotKeys",
|
||||
"pluginNextTurnInjections",
|
||||
"sessionId",
|
||||
+ "lifecycleRunId",
|
||||
"lifecycleRevision",
|
||||
"updatedAt",
|
||||
"archivedAt",
|
||||
@@ -229,6 +230,7 @@ const SESSION_ENTRY_RESERVED_SLOT_KEYS = /* @__PURE__ */ new Set([
|
||||
"pendingFinalDeliveryIntentId",
|
||||
"restartRecoveryDeliveryContext",
|
||||
"restartRecoveryDeliveryRunId",
|
||||
+ "restartRecoveryDeliverySourceRunId",
|
||||
"totalTokensFresh",
|
||||
"estimatedCostUsd",
|
||||
"cacheRead",
|
||||
+1
-39
@@ -1,29 +1,9 @@
|
||||
import { defineConfig } from '@playwright/test';
|
||||
import {
|
||||
DEFAULT_E2E_WORKERS,
|
||||
E2E_EXCLUSIVE_TAG,
|
||||
E2E_PERFORMANCE_TAG,
|
||||
} from './tests/e2e/parallel-policy';
|
||||
|
||||
function e2eWorkers(): number {
|
||||
const configured = process.env.CLAWX_E2E_WORKERS?.trim();
|
||||
if (!configured) return DEFAULT_E2E_WORKERS;
|
||||
|
||||
const workers = Number(configured);
|
||||
if (!Number.isInteger(workers) || workers < 1) {
|
||||
throw new Error('CLAWX_E2E_WORKERS must be a positive integer');
|
||||
}
|
||||
return workers;
|
||||
}
|
||||
|
||||
const exclusivePattern = new RegExp(E2E_EXCLUSIVE_TAG);
|
||||
const performancePattern = new RegExp(E2E_PERFORMANCE_TAG);
|
||||
const nonParallelPattern = new RegExp(`${E2E_EXCLUSIVE_TAG}|${E2E_PERFORMANCE_TAG}`);
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests/e2e',
|
||||
fullyParallel: false,
|
||||
workers: e2eWorkers(),
|
||||
workers: 1,
|
||||
forbidOnly: Boolean(process.env.CI),
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
timeout: 90_000,
|
||||
@@ -39,22 +19,4 @@ export default defineConfig({
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'exclusive',
|
||||
grep: exclusivePattern,
|
||||
workers: 1,
|
||||
},
|
||||
{
|
||||
name: 'parallel',
|
||||
grepInvert: nonParallelPattern,
|
||||
dependencies: ['exclusive'],
|
||||
},
|
||||
{
|
||||
name: 'performance',
|
||||
grep: performancePattern,
|
||||
dependencies: ['parallel'],
|
||||
workers: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
Generated
+50
-43
@@ -7,11 +7,6 @@ settings:
|
||||
overrides:
|
||||
isbinaryfile: ^5.0.0
|
||||
|
||||
patchedDependencies:
|
||||
openclaw@2026.7.1-2:
|
||||
hash: fbdd382bdaaebbaa3b1b61bd8329bbd6cd4e7c37cbfc0b6197e696d1d0d40db3
|
||||
path: patches/openclaw@2026.7.1-2.patch
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
@@ -70,7 +65,7 @@ importers:
|
||||
version: 1.3.7
|
||||
'@larksuite/openclaw-lark':
|
||||
specifier: 2026.7.9
|
||||
version: 2026.7.9(openclaw@2026.7.1-2(patch_hash=fbdd382bdaaebbaa3b1b61bd8329bbd6cd4e7c37cbfc0b6197e696d1d0d40db3)(encoding@0.1.13))
|
||||
version: 2026.7.9(openclaw@2026.7.1(encoding@0.1.13))
|
||||
'@larksuiteoapi/node-sdk':
|
||||
specifier: ^1.61.1
|
||||
version: 1.62.0
|
||||
@@ -79,13 +74,13 @@ importers:
|
||||
version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@openclaw/discord':
|
||||
specifier: 2026.7.1
|
||||
version: 2026.7.1(openclaw@2026.7.1-2(patch_hash=fbdd382bdaaebbaa3b1b61bd8329bbd6cd4e7c37cbfc0b6197e696d1d0d40db3)(encoding@0.1.13))
|
||||
version: 2026.7.1(openclaw@2026.7.1(encoding@0.1.13))
|
||||
'@openclaw/qqbot':
|
||||
specifier: 2026.7.1
|
||||
version: 2026.7.1(openclaw@2026.7.1-2(patch_hash=fbdd382bdaaebbaa3b1b61bd8329bbd6cd4e7c37cbfc0b6197e696d1d0d40db3)(encoding@0.1.13))
|
||||
version: 2026.7.1(openclaw@2026.7.1(encoding@0.1.13))
|
||||
'@openclaw/whatsapp':
|
||||
specifier: 2026.7.1
|
||||
version: 2026.7.1(openclaw@2026.7.1-2(patch_hash=fbdd382bdaaebbaa3b1b61bd8329bbd6cd4e7c37cbfc0b6197e696d1d0d40db3)(encoding@0.1.13))
|
||||
version: 2026.7.1(openclaw@2026.7.1(encoding@0.1.13))
|
||||
'@playwright/test':
|
||||
specifier: ^1.56.1
|
||||
version: 1.59.0
|
||||
@@ -130,7 +125,7 @@ importers:
|
||||
version: 0.34.48
|
||||
'@soimy/dingtalk':
|
||||
specifier: 3.6.6
|
||||
version: 3.6.6(openclaw@2026.7.1-2(patch_hash=fbdd382bdaaebbaa3b1b61bd8329bbd6cd4e7c37cbfc0b6197e696d1d0d40db3)(encoding@0.1.13))
|
||||
version: 3.6.6(openclaw@2026.7.1(encoding@0.1.13))
|
||||
'@streamdown/cjk':
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(react@19.2.4)(unified@11.0.5)
|
||||
@@ -145,7 +140,7 @@ importers:
|
||||
version: 1.1.0
|
||||
'@tencent-weixin/openclaw-weixin':
|
||||
specifier: ^2.4.6
|
||||
version: 2.4.6(openclaw@2026.7.1-2(patch_hash=fbdd382bdaaebbaa3b1b61bd8329bbd6cd4e7c37cbfc0b6197e696d1d0d40db3)(encoding@0.1.13))
|
||||
version: 2.4.6(openclaw@2026.7.1(encoding@0.1.13))
|
||||
'@testing-library/jest-dom':
|
||||
specifier: ^6.9.1
|
||||
version: 6.9.1
|
||||
@@ -178,7 +173,7 @@ importers:
|
||||
version: 5.2.0(vite@7.3.1(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.9.0))
|
||||
'@wecom/wecom-openclaw-plugin':
|
||||
specifier: 2026.7.2
|
||||
version: 2026.7.2(openclaw@2026.7.1-2(patch_hash=fbdd382bdaaebbaa3b1b61bd8329bbd6cd4e7c37cbfc0b6197e696d1d0d40db3)(encoding@0.1.13))
|
||||
version: 2026.7.2(openclaw@2026.7.1(encoding@0.1.13))
|
||||
'@whiskeysockets/baileys':
|
||||
specifier: 7.0.0-rc.9
|
||||
version: 7.0.0-rc.9(audio-decode@2.2.3)(jimp@1.6.1)(sharp@0.34.5)
|
||||
@@ -267,8 +262,8 @@ importers:
|
||||
specifier: ^2.1.3
|
||||
version: 2.1.3
|
||||
openclaw:
|
||||
specifier: 2026.7.1-2
|
||||
version: 2026.7.1-2(patch_hash=fbdd382bdaaebbaa3b1b61bd8329bbd6cd4e7c37cbfc0b6197e696d1d0d40db3)(encoding@0.1.13)
|
||||
specifier: 2026.7.1
|
||||
version: 2026.7.1(encoding@0.1.13)
|
||||
opusscript:
|
||||
specifier: ^0.1.1
|
||||
version: 0.1.1
|
||||
@@ -1501,8 +1496,8 @@ packages:
|
||||
resolution: {integrity: sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
|
||||
'@openclaw/ai@2026.7.1-2':
|
||||
resolution: {integrity: sha512-st+NH0cxlQqdbEur//yYqM7WlYBjeEBnop3cztJTSCONKjv6LNoGguI9cH65asZG94FdM/39z857isRGPnZvEw==}
|
||||
'@openclaw/ai@2026.7.1':
|
||||
resolution: {integrity: sha512-FsKy5DXSHf4qyN8Huoz/10HZRgoEwLF4uk8UWaCafaIler+q5Fsl51HcrIqIrEe0S38OT7LOaxnR++MOshAlmw==}
|
||||
engines: {node: '>=22.19.0'}
|
||||
|
||||
'@openclaw/discord@2026.7.1':
|
||||
@@ -4218,7 +4213,7 @@ packages:
|
||||
|
||||
glob@7.2.3:
|
||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||
deprecated: Glob versions prior to v9 are no longer supported
|
||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
|
||||
global-agent@3.0.0:
|
||||
resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==}
|
||||
@@ -4677,8 +4672,8 @@ packages:
|
||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
libsignal@git+https://git@github.com:whiskeysockets/libsignal-node.git#bcea72df9ec34d9d9140ab30619cf479c7c144c7:
|
||||
resolution: {commit: bcea72df9ec34d9d9140ab30619cf479c7c144c7, repo: git@github.com:whiskeysockets/libsignal-node.git, type: git}
|
||||
libsignal@https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/bcea72df9ec34d9d9140ab30619cf479c7c144c7:
|
||||
resolution: {gitHosted: true, tarball: https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/bcea72df9ec34d9d9140ab30619cf479c7c144c7}
|
||||
version: 6.0.0
|
||||
|
||||
lie@3.3.0:
|
||||
@@ -5320,8 +5315,8 @@ packages:
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
openclaw@2026.7.1-2:
|
||||
resolution: {integrity: sha512-ycF3yPcbjN6bUPeaUx6Mh6vze1hQWoD3CT/wWcmD7a8xaHHHRUaAlaq+lFxMHf1ssEgODVAwjlzYqp2twkYZ7g==}
|
||||
openclaw@2026.7.1:
|
||||
resolution: {integrity: sha512-ge/Xss99CHAjPL/ikmH/UFoiOrjcxDB4sW3y9mhyCD+dYW3wzV7TKbAVdkrXFgAG2d2BjpJofP97zUZ+umxo8g==}
|
||||
engines: {node: '>=22.22.3 <23 || >=24.15.0 <25 || >=25.9.0'}
|
||||
hasBin: true
|
||||
|
||||
@@ -6371,6 +6366,10 @@ packages:
|
||||
resolution: {integrity: sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tar@7.5.15:
|
||||
resolution: {integrity: sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tar@7.5.19:
|
||||
resolution: {integrity: sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -7370,7 +7369,7 @@ snapshots:
|
||||
ora: 5.4.1
|
||||
read-binary-file-arch: 1.0.6
|
||||
semver: 7.7.4
|
||||
tar: 7.5.19
|
||||
tar: 7.5.15
|
||||
yargs: 17.7.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -8022,7 +8021,7 @@ snapshots:
|
||||
|
||||
'@kurkle/color@0.3.4': {}
|
||||
|
||||
'@larksuite/openclaw-lark@2026.7.9(openclaw@2026.7.1-2(patch_hash=fbdd382bdaaebbaa3b1b61bd8329bbd6cd4e7c37cbfc0b6197e696d1d0d40db3)(encoding@0.1.13))':
|
||||
'@larksuite/openclaw-lark@2026.7.9(openclaw@2026.7.1(encoding@0.1.13))':
|
||||
dependencies:
|
||||
'@larksuiteoapi/node-sdk': 1.66.1
|
||||
'@sinclair/typebox': 0.34.49
|
||||
@@ -8030,7 +8029,7 @@ snapshots:
|
||||
undici-types: 8.3.0
|
||||
zod: 4.4.3
|
||||
optionalDependencies:
|
||||
openclaw: 2026.7.1-2(patch_hash=fbdd382bdaaebbaa3b1b61bd8329bbd6cd4e7c37cbfc0b6197e696d1d0d40db3)(encoding@0.1.13)
|
||||
openclaw: 2026.7.1(encoding@0.1.13)
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- debug
|
||||
@@ -8282,7 +8281,7 @@ snapshots:
|
||||
dependencies:
|
||||
semver: 7.7.4
|
||||
|
||||
'@openclaw/ai@2026.7.1-2(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)':
|
||||
'@openclaw/ai@2026.7.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)':
|
||||
dependencies:
|
||||
'@anthropic-ai/sdk': 0.109.1(zod@4.4.3)
|
||||
'@google/genai': 2.10.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))
|
||||
@@ -8302,9 +8301,9 @@ snapshots:
|
||||
- ws
|
||||
- zod
|
||||
|
||||
'@openclaw/discord@2026.7.1(openclaw@2026.7.1-2(patch_hash=fbdd382bdaaebbaa3b1b61bd8329bbd6cd4e7c37cbfc0b6197e696d1d0d40db3)(encoding@0.1.13))':
|
||||
'@openclaw/discord@2026.7.1(openclaw@2026.7.1(encoding@0.1.13))':
|
||||
optionalDependencies:
|
||||
openclaw: 2026.7.1-2(patch_hash=fbdd382bdaaebbaa3b1b61bd8329bbd6cd4e7c37cbfc0b6197e696d1d0d40db3)(encoding@0.1.13)
|
||||
openclaw: 2026.7.1(encoding@0.1.13)
|
||||
|
||||
'@openclaw/fs-safe@0.4.1':
|
||||
optionalDependencies:
|
||||
@@ -8315,13 +8314,13 @@ snapshots:
|
||||
dependencies:
|
||||
undici: 8.5.0
|
||||
|
||||
'@openclaw/qqbot@2026.7.1(openclaw@2026.7.1-2(patch_hash=fbdd382bdaaebbaa3b1b61bd8329bbd6cd4e7c37cbfc0b6197e696d1d0d40db3)(encoding@0.1.13))':
|
||||
'@openclaw/qqbot@2026.7.1(openclaw@2026.7.1(encoding@0.1.13))':
|
||||
optionalDependencies:
|
||||
openclaw: 2026.7.1-2(patch_hash=fbdd382bdaaebbaa3b1b61bd8329bbd6cd4e7c37cbfc0b6197e696d1d0d40db3)(encoding@0.1.13)
|
||||
openclaw: 2026.7.1(encoding@0.1.13)
|
||||
|
||||
'@openclaw/whatsapp@2026.7.1(openclaw@2026.7.1-2(patch_hash=fbdd382bdaaebbaa3b1b61bd8329bbd6cd4e7c37cbfc0b6197e696d1d0d40db3)(encoding@0.1.13))':
|
||||
'@openclaw/whatsapp@2026.7.1(openclaw@2026.7.1(encoding@0.1.13))':
|
||||
optionalDependencies:
|
||||
openclaw: 2026.7.1-2(patch_hash=fbdd382bdaaebbaa3b1b61bd8329bbd6cd4e7c37cbfc0b6197e696d1d0d40db3)(encoding@0.1.13)
|
||||
openclaw: 2026.7.1(encoding@0.1.13)
|
||||
|
||||
'@opentelemetry/semantic-conventions@1.43.0': {}
|
||||
|
||||
@@ -9005,7 +9004,7 @@ snapshots:
|
||||
- '@emnapi/core'
|
||||
- '@emnapi/runtime'
|
||||
|
||||
'@soimy/dingtalk@3.6.6(openclaw@2026.7.1-2(patch_hash=fbdd382bdaaebbaa3b1b61bd8329bbd6cd4e7c37cbfc0b6197e696d1d0d40db3)(encoding@0.1.13))':
|
||||
'@soimy/dingtalk@3.6.6(openclaw@2026.7.1(encoding@0.1.13))':
|
||||
dependencies:
|
||||
axios: 1.13.6(debug@4.4.3)
|
||||
dingtalk-stream: 2.1.5
|
||||
@@ -9014,7 +9013,7 @@ snapshots:
|
||||
pdf-parse: 2.4.5
|
||||
zod: 4.4.3
|
||||
optionalDependencies:
|
||||
openclaw: 2026.7.1-2(patch_hash=fbdd382bdaaebbaa3b1b61bd8329bbd6cd4e7c37cbfc0b6197e696d1d0d40db3)(encoding@0.1.13)
|
||||
openclaw: 2026.7.1(encoding@0.1.13)
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- debug
|
||||
@@ -9060,9 +9059,9 @@ snapshots:
|
||||
dependencies:
|
||||
qrcode-terminal: 0.12.0
|
||||
|
||||
'@tencent-weixin/openclaw-weixin@2.4.6(openclaw@2026.7.1-2(patch_hash=fbdd382bdaaebbaa3b1b61bd8329bbd6cd4e7c37cbfc0b6197e696d1d0d40db3)(encoding@0.1.13))':
|
||||
'@tencent-weixin/openclaw-weixin@2.4.6(openclaw@2026.7.1(encoding@0.1.13))':
|
||||
dependencies:
|
||||
openclaw: 2026.7.1-2(patch_hash=fbdd382bdaaebbaa3b1b61bd8329bbd6cd4e7c37cbfc0b6197e696d1d0d40db3)(encoding@0.1.13)
|
||||
openclaw: 2026.7.1(encoding@0.1.13)
|
||||
qrcode-terminal: 0.12.0
|
||||
zod: 4.4.3
|
||||
|
||||
@@ -9551,7 +9550,7 @@ snapshots:
|
||||
- debug
|
||||
- utf-8-validate
|
||||
|
||||
'@wecom/wecom-openclaw-plugin@2026.7.2(openclaw@2026.7.1-2(patch_hash=fbdd382bdaaebbaa3b1b61bd8329bbd6cd4e7c37cbfc0b6197e696d1d0d40db3)(encoding@0.1.13))':
|
||||
'@wecom/wecom-openclaw-plugin@2026.7.2(openclaw@2026.7.1(encoding@0.1.13))':
|
||||
dependencies:
|
||||
'@wecom/aibot-node-sdk': 1.0.6
|
||||
fast-xml-parser: 5.7.3
|
||||
@@ -9559,7 +9558,7 @@ snapshots:
|
||||
undici: 7.24.6
|
||||
zod: 4.4.3
|
||||
optionalDependencies:
|
||||
openclaw: 2026.7.1-2(patch_hash=fbdd382bdaaebbaa3b1b61bd8329bbd6cd4e7c37cbfc0b6197e696d1d0d40db3)(encoding@0.1.13)
|
||||
openclaw: 2026.7.1(encoding@0.1.13)
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- debug
|
||||
@@ -9571,7 +9570,7 @@ snapshots:
|
||||
'@cacheable/node-cache': 1.7.6
|
||||
'@hapi/boom': 9.1.4
|
||||
async-mutex: 0.5.0
|
||||
libsignal: git+https://git@github.com:whiskeysockets/libsignal-node.git#bcea72df9ec34d9d9140ab30619cf479c7c144c7
|
||||
libsignal: https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/bcea72df9ec34d9d9140ab30619cf479c7c144c7
|
||||
lru-cache: 11.2.7
|
||||
music-metadata: 11.12.3
|
||||
p-queue: 9.1.0
|
||||
@@ -9981,7 +9980,7 @@ snapshots:
|
||||
minipass-pipeline: 1.2.4
|
||||
p-map: 7.0.4
|
||||
ssri: 12.0.0
|
||||
tar: 7.5.19
|
||||
tar: 7.5.15
|
||||
unique-filename: 4.0.0
|
||||
|
||||
cacheable-lookup@5.0.4: {}
|
||||
@@ -11829,7 +11828,7 @@ snapshots:
|
||||
prelude-ls: 1.2.1
|
||||
type-check: 0.4.0
|
||||
|
||||
libsignal@git+https://git@github.com:whiskeysockets/libsignal-node.git#bcea72df9ec34d9d9140ab30619cf479c7c144c7:
|
||||
libsignal@https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/bcea72df9ec34d9d9140ab30619cf479c7c144c7:
|
||||
dependencies:
|
||||
curve25519-js: 0.0.4
|
||||
protobufjs: 7.5.8
|
||||
@@ -12626,7 +12625,7 @@ snapshots:
|
||||
nopt: 8.1.0
|
||||
proc-log: 5.0.0
|
||||
semver: 7.7.4
|
||||
tar: 7.5.19
|
||||
tar: 7.5.15
|
||||
tinyglobby: 0.2.15
|
||||
which: 5.0.0
|
||||
transitivePeerDependencies:
|
||||
@@ -12700,7 +12699,7 @@ snapshots:
|
||||
ws: 8.21.0
|
||||
zod: 4.4.3
|
||||
|
||||
openclaw@2026.7.1-2(patch_hash=fbdd382bdaaebbaa3b1b61bd8329bbd6cd4e7c37cbfc0b6197e696d1d0d40db3)(encoding@0.1.13):
|
||||
openclaw@2026.7.1(encoding@0.1.13):
|
||||
dependencies:
|
||||
'@agentclientprotocol/sdk': 1.1.0(zod@4.4.3)
|
||||
'@anthropic-ai/sdk': 0.109.1(zod@4.4.3)
|
||||
@@ -12715,7 +12714,7 @@ snapshots:
|
||||
'@mistralai/mistralai': 2.4.0
|
||||
'@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3)
|
||||
'@mozilla/readability': 0.6.0
|
||||
'@openclaw/ai': 2026.7.1-2(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)
|
||||
'@openclaw/ai': 2026.7.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)
|
||||
'@openclaw/fs-safe': 0.4.1
|
||||
'@openclaw/proxyline': 0.3.3(undici@8.5.0)
|
||||
'@silvia-odwyer/photon-node': 0.3.4
|
||||
@@ -13972,6 +13971,14 @@ snapshots:
|
||||
minizlib: 3.1.0
|
||||
yallist: 5.0.0
|
||||
|
||||
tar@7.5.15:
|
||||
dependencies:
|
||||
'@isaacs/fs-minipass': 4.0.1
|
||||
chownr: 3.0.0
|
||||
minipass: 7.1.3
|
||||
minizlib: 3.1.0
|
||||
yallist: 5.0.0
|
||||
|
||||
tar@7.5.19:
|
||||
dependencies:
|
||||
'@isaacs/fs-minipass': 4.0.1
|
||||
|
||||
@@ -5,6 +5,3 @@ packages:
|
||||
ignoredBuiltDependencies:
|
||||
- electron
|
||||
- esbuild
|
||||
|
||||
patchedDependencies:
|
||||
openclaw@2026.7.1-2: patches/openclaw@2026.7.1-2.patch
|
||||
|
||||
@@ -29,6 +29,8 @@ export type AcpChatPromptPayload = AcpSessionKeyPayload & {
|
||||
message?: string;
|
||||
media?: AcpPromptMediaItem[];
|
||||
messageId?: string;
|
||||
/** Explicit current-session reasoning effort selected by the user. */
|
||||
thinkingLevel?: string;
|
||||
};
|
||||
|
||||
export type AcpChatCancelPayload = AcpSessionKeyPayload;
|
||||
@@ -38,9 +40,12 @@ export type AcpChatRespondPermissionPayload = AcpSessionKeyPayload & {
|
||||
outcome: RequestPermissionResponse['outcome'];
|
||||
};
|
||||
|
||||
export type AcpChatOperationErrorCode = 'prompt_aborted';
|
||||
|
||||
export type AcpChatOperationResult = {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
errorCode?: AcpChatOperationErrorCode;
|
||||
generation?: number;
|
||||
/** The requested session still has a live prompt and was reactivated without history replay. */
|
||||
resumedActivePrompt?: boolean;
|
||||
|
||||
@@ -81,6 +81,20 @@ export interface ContentBlock {
|
||||
content?: unknown;
|
||||
}
|
||||
|
||||
export interface ThinkingLevelOption {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
/** Agent-scoped defaults advertised by Gateway sessions.list. */
|
||||
export interface ChatThinkingDefaults {
|
||||
agentId: string;
|
||||
modelProvider?: string;
|
||||
model?: string;
|
||||
thinkingLevels?: ThinkingLevelOption[];
|
||||
thinkingDefault?: string;
|
||||
}
|
||||
|
||||
/** Session from sessions.list */
|
||||
export interface ChatSession {
|
||||
key: string;
|
||||
@@ -90,7 +104,13 @@ export interface ChatSession {
|
||||
displayName?: string;
|
||||
derivedTitle?: string;
|
||||
lastMessagePreview?: string;
|
||||
/** Explicit session override. Undefined means inherit the Gateway-resolved default. */
|
||||
thinkingLevel?: string;
|
||||
/** Model/runtime-specific options advertised by the Gateway. */
|
||||
thinkingLevels?: ThinkingLevelOption[];
|
||||
/** Effective inherited value when no explicit session override is present. */
|
||||
thinkingDefault?: string;
|
||||
modelProvider?: string;
|
||||
model?: string;
|
||||
updatedAt?: number;
|
||||
status?: string;
|
||||
@@ -138,6 +158,11 @@ export interface ChatState {
|
||||
/** Last message timestamp (ms) per session key, used for sorting */
|
||||
sessionLastActivity: Record<string, number>;
|
||||
|
||||
/** Agent-scoped fallback metadata for a local draft not yet listed by Gateway. */
|
||||
thinkingDefaults: ChatThinkingDefaults | null;
|
||||
/** Session currently persisting a reasoning-effort override. */
|
||||
thinkingLevelUpdatingSessionKey: string | null;
|
||||
|
||||
loadSessions: (options?: LoadSessionsOptions) => Promise<void>;
|
||||
handleSessionsChanged: (payload: GatewaySessionsChangedPayload) => void;
|
||||
switchSession: (key: string) => void;
|
||||
@@ -147,6 +172,7 @@ export interface ChatState {
|
||||
deleteSession: (key: string) => Promise<DeleteSessionResult>;
|
||||
deleteSessions: (keys: string[]) => Promise<DeleteSessionsResult>;
|
||||
renameSession: (key: string, label: string) => Promise<void>;
|
||||
updateSessionThinkingLevel: (key: string, level: string | null) => Promise<void>;
|
||||
}
|
||||
|
||||
export const DEFAULT_CANONICAL_PREFIX = 'agent:main';
|
||||
|
||||
@@ -243,8 +243,6 @@ export type ChannelSaveConfigPayload = ChannelTypePayload & {
|
||||
};
|
||||
export type ChannelSaveConfigResult = HostSuccess & {
|
||||
noChange?: boolean;
|
||||
/** Configuration is committed; a guarded Gateway restart is continuing asynchronously. */
|
||||
activationPending?: boolean;
|
||||
warning?: string;
|
||||
};
|
||||
export type ChannelConfiguredResult = HostSuccess & { channels?: Array<string | JsonRecord> };
|
||||
@@ -311,6 +309,7 @@ export type ProviderProtocol =
|
||||
| 'bedrock-converse-stream'
|
||||
| 'ollama'
|
||||
| 'azure-openai-responses';
|
||||
export type ProviderReasoningEffort = 'low' | 'medium' | 'high' | 'xhigh';
|
||||
export type ProviderConfig = {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -319,6 +318,8 @@ export type ProviderConfig = {
|
||||
apiProtocol?: ProviderProtocol;
|
||||
headers?: Record<string, string>;
|
||||
model?: string;
|
||||
reasoningEnabled?: boolean;
|
||||
reasoningEfforts?: ProviderReasoningEffort[];
|
||||
fallbackModels?: string[];
|
||||
fallbackProviderIds?: string[];
|
||||
enabled: boolean;
|
||||
@@ -367,6 +368,8 @@ export type ProviderAccount = {
|
||||
apiProtocol?: ProviderProtocol;
|
||||
headers?: Record<string, string>;
|
||||
model?: string;
|
||||
reasoningEnabled?: boolean;
|
||||
reasoningEfforts?: ProviderReasoningEffort[];
|
||||
fallbackModels?: string[];
|
||||
fallbackAccountIds?: string[];
|
||||
enabled: boolean;
|
||||
|
||||
@@ -214,6 +214,21 @@
|
||||
"Fill in Client ID (AppKey) and Client Secret (AppSecret)"
|
||||
]
|
||||
},
|
||||
"signal": {
|
||||
"description": "Connect Signal using signal-cli",
|
||||
"docsUrl": "https://docs.openclaw.ai/channels/signal",
|
||||
"fields": {
|
||||
"phoneNumber": {
|
||||
"label": "Phone Number",
|
||||
"placeholder": "+1234567890"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"Install signal-cli on your system",
|
||||
"Register or link your phone number",
|
||||
"Enter your phone number below"
|
||||
]
|
||||
},
|
||||
"feishu": {
|
||||
"description": "Connect Feishu/Lark bot via WebSocket",
|
||||
"docsUrl": "https://icnnp7d0dymg.feishu.cn/wiki/GKn8wOvHnibpPNkNkPzcAvGlnzK#GdHUdp9t9oqyegxwV8ScLvVGn1c",
|
||||
@@ -253,6 +268,118 @@
|
||||
"Enter your Bot ID (or Corp ID) and Secret to establish connection"
|
||||
]
|
||||
},
|
||||
"imessage": {
|
||||
"description": "Connect iMessage via BlueBubbles (macOS)",
|
||||
"docsUrl": "https://docs.openclaw.ai/channels/bluebubbles",
|
||||
"fields": {
|
||||
"serverUrl": {
|
||||
"label": "BlueBubbles Server URL",
|
||||
"placeholder": "http://localhost:1234"
|
||||
},
|
||||
"password": {
|
||||
"label": "Server Password",
|
||||
"placeholder": "Your server password"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"Install BlueBubbles server on your Mac",
|
||||
"Note the server URL and password",
|
||||
"Enter the connection details below"
|
||||
]
|
||||
},
|
||||
"matrix": {
|
||||
"description": "Connect to Matrix protocol",
|
||||
"docsUrl": "https://docs.openclaw.ai/channels/matrix",
|
||||
"fields": {
|
||||
"homeserver": {
|
||||
"label": "Homeserver URL",
|
||||
"placeholder": "https://matrix.org"
|
||||
},
|
||||
"accessToken": {
|
||||
"label": "Access Token",
|
||||
"placeholder": "Your access token"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"Create a Matrix account or use existing",
|
||||
"Get an access token from your client",
|
||||
"Enter the homeserver and token below"
|
||||
]
|
||||
},
|
||||
"line": {
|
||||
"description": "Connect LINE Messaging API",
|
||||
"docsUrl": "https://docs.openclaw.ai/channels/line",
|
||||
"fields": {
|
||||
"channelAccessToken": {
|
||||
"label": "Channel Access Token",
|
||||
"placeholder": "Your LINE channel access token"
|
||||
},
|
||||
"channelSecret": {
|
||||
"label": "Channel Secret",
|
||||
"placeholder": "Your LINE channel secret"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"Go to LINE Developers Console",
|
||||
"Create a Messaging API channel",
|
||||
"Get Channel Access Token and Secret"
|
||||
]
|
||||
},
|
||||
"msteams": {
|
||||
"description": "Connect Microsoft Teams via Bot Framework",
|
||||
"docsUrl": "https://docs.openclaw.ai/channels/msteams",
|
||||
"fields": {
|
||||
"appId": {
|
||||
"label": "App ID",
|
||||
"placeholder": "Your Microsoft App ID"
|
||||
},
|
||||
"appPassword": {
|
||||
"label": "App Password",
|
||||
"placeholder": "Your Microsoft App Password"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"Go to Azure Portal",
|
||||
"Register a new Bot application",
|
||||
"Get App ID and create a password",
|
||||
"Configure Teams channel"
|
||||
]
|
||||
},
|
||||
"googlechat": {
|
||||
"description": "Connect Google Chat via webhook",
|
||||
"docsUrl": "https://docs.openclaw.ai/channels/googlechat",
|
||||
"fields": {
|
||||
"serviceAccountKey": {
|
||||
"label": "Service Account JSON Path",
|
||||
"placeholder": "/path/to/service-account.json"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"Create a Google Cloud project",
|
||||
"Enable Google Chat API",
|
||||
"Create a service account",
|
||||
"Download the JSON key file"
|
||||
]
|
||||
},
|
||||
"mattermost": {
|
||||
"description": "Connect Mattermost via Bot API",
|
||||
"docsUrl": "https://docs.openclaw.ai/channels/mattermost",
|
||||
"fields": {
|
||||
"serverUrl": {
|
||||
"label": "Server URL",
|
||||
"placeholder": "https://your-mattermost.com"
|
||||
},
|
||||
"botToken": {
|
||||
"label": "Bot Access Token",
|
||||
"placeholder": "Your bot access token"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"Go to Mattermost Integrations",
|
||||
"Create a new Bot Account",
|
||||
"Copy the access token"
|
||||
]
|
||||
},
|
||||
"qqbot": {
|
||||
"description": "Connect QQ Bot channel (built-in since OpenClaw 3.31)",
|
||||
"docsUrl": "https://icnnp7d0dymg.feishu.cn/wiki/KPIJwlyiGiupMrkiS9ice39Zn2c",
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"cancelled": "Cancelled",
|
||||
"loadFailed": "Failed to load session",
|
||||
"promptFailed": "Failed to send prompt",
|
||||
"promptAborted": "The model request ended before producing a response. Please try again.",
|
||||
"unsupportedContent": "Unsupported content",
|
||||
"dismiss": "Dismiss",
|
||||
"image": "Image",
|
||||
@@ -216,11 +217,17 @@
|
||||
"skillEmpty": "No matching skills found",
|
||||
"pickAgent": "Choose agent",
|
||||
"pickModel": "Choose model",
|
||||
"modelControlTitle": "Model and reasoning effort",
|
||||
"modelSectionTitle": "Model",
|
||||
"clearTarget": "Clear target agent",
|
||||
"targetChip": "@{{agent}}",
|
||||
"agentPickerTitle": "Route the next message to another agent",
|
||||
"modelPickerTitle": "Switch model for this chat",
|
||||
"modelSwitchFailed": "Failed to switch model: {{error}}",
|
||||
"reasoningEffortTitle": "Reasoning effort",
|
||||
"reasoningEffortDescription": "Higher effort gives more thorough responses, but takes longer and uses context faster.",
|
||||
"reasoningEffortDefault": "Default",
|
||||
"reasoningEffortUpdateFailed": "Failed to update reasoning effort: {{error}}",
|
||||
"gatewayDisconnectedPlaceholder": "Gateway not connected...",
|
||||
"send": "Send",
|
||||
"stop": "Stop",
|
||||
|
||||
@@ -214,6 +214,21 @@
|
||||
"Client ID (AppKey) と Client Secret (AppSecret) を入力します"
|
||||
]
|
||||
},
|
||||
"signal": {
|
||||
"description": "signal-cli を使用して Signal に接続します",
|
||||
"fields": {
|
||||
"phoneNumber": {
|
||||
"label": "電話番号",
|
||||
"placeholder": "+1234567890"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"システムに signal-cli をインストールします",
|
||||
"電話番号を登録またはリンクします",
|
||||
"以下に電話番号を入力します"
|
||||
],
|
||||
"docsUrl": "https://docs.openclaw.ai/channels/signal"
|
||||
},
|
||||
"feishu": {
|
||||
"description": "WebSocket 経由で Feishu/Lark ボットに接続します",
|
||||
"docsUrl": "https://icnnp7d0dymg.feishu.cn/wiki/GKn8wOvHnibpPNkNkPzcAvGlnzK#GdHUdp9t9oqyegxwV8ScLvVGn1c",
|
||||
@@ -253,6 +268,118 @@
|
||||
"ボット ID (または 企業 ID) とシークレットを入力して接続を確立します"
|
||||
]
|
||||
},
|
||||
"imessage": {
|
||||
"description": "BlueBubbles (macOS) 経由で iMessage に接続します",
|
||||
"fields": {
|
||||
"serverUrl": {
|
||||
"label": "BlueBubbles サーバーURL",
|
||||
"placeholder": "http://localhost:1234"
|
||||
},
|
||||
"password": {
|
||||
"label": "サーバーパスワード",
|
||||
"placeholder": "サーバーのパスワード"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"Mac に BlueBubbles サーバーをインストールします",
|
||||
"サーバーURLとパスワードをメモします",
|
||||
"以下に接続詳細を入力します"
|
||||
],
|
||||
"docsUrl": "https://docs.openclaw.ai/channels/bluebubbles"
|
||||
},
|
||||
"matrix": {
|
||||
"description": "Matrix プロトコルに接続します",
|
||||
"fields": {
|
||||
"homeserver": {
|
||||
"label": "ホームサーバー URL",
|
||||
"placeholder": "https://matrix.org"
|
||||
},
|
||||
"accessToken": {
|
||||
"label": "アクセストークン",
|
||||
"placeholder": "アクセストークン"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"Matrix アカウントを作成するか、既存のものを使用します",
|
||||
"クライアントからアクセストークンを取得します",
|
||||
"以下にホームサーバーとトークンを入力します"
|
||||
],
|
||||
"docsUrl": "https://docs.openclaw.ai/channels/matrix"
|
||||
},
|
||||
"line": {
|
||||
"description": "LINE Messaging API に接続します",
|
||||
"fields": {
|
||||
"channelAccessToken": {
|
||||
"label": "チャンネルアクセストークン",
|
||||
"placeholder": "LINE チャンネルアクセストークン"
|
||||
},
|
||||
"channelSecret": {
|
||||
"label": "チャンネルシークレット",
|
||||
"placeholder": "LINE チャンネルシークレット"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"LINE Developers Console に移動します",
|
||||
"Messaging API チャンネルを作成します",
|
||||
"チャンネルアクセストークンとシークレットを取得します"
|
||||
],
|
||||
"docsUrl": "https://docs.openclaw.ai/channels/line"
|
||||
},
|
||||
"msteams": {
|
||||
"description": "Bot Framework 経由で Microsoft Teams に接続します",
|
||||
"fields": {
|
||||
"appId": {
|
||||
"label": "App ID",
|
||||
"placeholder": "Microsoft App ID"
|
||||
},
|
||||
"appPassword": {
|
||||
"label": "App Password",
|
||||
"placeholder": "Microsoft App Password"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"Azure Portal に移動します",
|
||||
"新しい Bot アプリケーションを登録します",
|
||||
"App ID を取得し、パスワードを作成します",
|
||||
"Teams チャンネルを設定します"
|
||||
],
|
||||
"docsUrl": "https://docs.openclaw.ai/channels/msteams"
|
||||
},
|
||||
"googlechat": {
|
||||
"description": "Webhook 経由で Google Chat に接続します",
|
||||
"fields": {
|
||||
"serviceAccountKey": {
|
||||
"label": "サービスアカウント JSON パス",
|
||||
"placeholder": "/path/to/service-account.json"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"Google Cloud プロジェクトを作成します",
|
||||
"Google Chat API を有効にします",
|
||||
"サービスアカウントを作成します",
|
||||
"JSON キーファイルをダウンロードします"
|
||||
],
|
||||
"docsUrl": "https://docs.openclaw.ai/channels/googlechat"
|
||||
},
|
||||
"mattermost": {
|
||||
"description": "Bot API 経由で Mattermost に接続します",
|
||||
"fields": {
|
||||
"serverUrl": {
|
||||
"label": "サーバー URL",
|
||||
"placeholder": "https://your-mattermost.com"
|
||||
},
|
||||
"botToken": {
|
||||
"label": "ボットアクセストークン",
|
||||
"placeholder": "ボットアクセストークン"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"Mattermost Integrations に移動します",
|
||||
"新しい Bot アカウントを作成します",
|
||||
"アクセストークンをコピーします"
|
||||
],
|
||||
"docsUrl": "https://docs.openclaw.ai/channels/mattermost"
|
||||
},
|
||||
"qqbot": {
|
||||
"description": "QQ ボットチャンネルに接続します(OpenClaw 3.31 より内蔵)",
|
||||
"fields": {
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"cancelled": "キャンセル済み",
|
||||
"loadFailed": "セッションの読み込みに失敗しました",
|
||||
"promptFailed": "プロンプトの送信に失敗しました",
|
||||
"promptAborted": "モデルリクエストは応答を生成する前に中断されました。もう一度お試しください。",
|
||||
"unsupportedContent": "未対応のコンテンツ",
|
||||
"dismiss": "閉じる",
|
||||
"image": "画像",
|
||||
@@ -216,8 +217,14 @@
|
||||
"skillEmpty": "一致する Skill がありません",
|
||||
"pickAgent": "Agent を選択",
|
||||
"pickModel": "モデルを選択",
|
||||
"modelControlTitle": "モデルと推論強度",
|
||||
"modelSectionTitle": "モデル",
|
||||
"modelPickerTitle": "モデルを切り替え",
|
||||
"modelSwitchFailed": "モデルの切り替えに失敗しました: {{error}}",
|
||||
"reasoningEffortTitle": "推論強度",
|
||||
"reasoningEffortDescription": "高い強度ほど回答は詳細になりますが、時間がかかり、コンテキストをより速く消費します。",
|
||||
"reasoningEffortDefault": "デフォルト",
|
||||
"reasoningEffortUpdateFailed": "推論強度の更新に失敗しました: {{error}}",
|
||||
"clearTarget": "送信先 Agent をクリア",
|
||||
"targetChip": "@{{agent}}",
|
||||
"agentPickerTitle": "次のメッセージを別の Agent に直接送信します",
|
||||
|
||||
@@ -214,6 +214,21 @@
|
||||
"Заполните Client ID (AppKey) и Client Secret (AppSecret)"
|
||||
]
|
||||
},
|
||||
"signal": {
|
||||
"description": "Подключите Signal через signal-cli",
|
||||
"docsUrl": "https://docs.openclaw.ai/channels/signal",
|
||||
"fields": {
|
||||
"phoneNumber": {
|
||||
"label": "Номер телефона",
|
||||
"placeholder": "+1234567890"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"Установите signal-cli в вашей системе",
|
||||
"Зарегистрируйте или привяжите ваш номер телефона",
|
||||
"Введите ваш номер телефона ниже"
|
||||
]
|
||||
},
|
||||
"feishu": {
|
||||
"description": "Подключите бота Feishu/Lark через WebSocket",
|
||||
"docsUrl": "https://icnnp7d0dymg.feishu.cn/wiki/GKn8wOvHnibpPNkNkPzcAvGlnzK#GdHUdp9t9oqyegxwV8ScLvVGn1c",
|
||||
@@ -253,6 +268,118 @@
|
||||
"Введите ваш Bot ID (или Corp ID) и Secret для установления соединения"
|
||||
]
|
||||
},
|
||||
"imessage": {
|
||||
"description": "Подключите iMessage через BlueBubbles (macOS)",
|
||||
"docsUrl": "https://docs.openclaw.ai/channels/bluebubbles",
|
||||
"fields": {
|
||||
"serverUrl": {
|
||||
"label": "URL сервера BlueBubbles",
|
||||
"placeholder": "http://localhost:1234"
|
||||
},
|
||||
"password": {
|
||||
"label": "Пароль сервера",
|
||||
"placeholder": "Пароль вашего сервера"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"Установите сервер BlueBubbles на ваш Mac",
|
||||
"Запишите URL сервера и пароль",
|
||||
"Введите данные подключения ниже"
|
||||
]
|
||||
},
|
||||
"matrix": {
|
||||
"description": "Подключитесь к протоколу Matrix",
|
||||
"docsUrl": "https://docs.openclaw.ai/channels/matrix",
|
||||
"fields": {
|
||||
"homeserver": {
|
||||
"label": "URL Homeserver",
|
||||
"placeholder": "https://matrix.org"
|
||||
},
|
||||
"accessToken": {
|
||||
"label": "Access Token",
|
||||
"placeholder": "Ваш access token"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"Создайте учётную запись Matrix или используйте существующую",
|
||||
"Получите access token из вашего клиента",
|
||||
"Введите homeserver и token ниже"
|
||||
]
|
||||
},
|
||||
"line": {
|
||||
"description": "Подключите LINE Messaging API",
|
||||
"docsUrl": "https://docs.openclaw.ai/channels/line",
|
||||
"fields": {
|
||||
"channelAccessToken": {
|
||||
"label": "Channel Access Token",
|
||||
"placeholder": "Ваш LINE channel access token"
|
||||
},
|
||||
"channelSecret": {
|
||||
"label": "Channel Secret",
|
||||
"placeholder": "Ваш LINE channel secret"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"Перейдите в LINE Developers Console",
|
||||
"Создайте канал Messaging API",
|
||||
"Получите Channel Access Token и Secret"
|
||||
]
|
||||
},
|
||||
"msteams": {
|
||||
"description": "Подключите Microsoft Teams через Bot Framework",
|
||||
"docsUrl": "https://docs.openclaw.ai/channels/msteams",
|
||||
"fields": {
|
||||
"appId": {
|
||||
"label": "App ID",
|
||||
"placeholder": "Ваш Microsoft App ID"
|
||||
},
|
||||
"appPassword": {
|
||||
"label": "App Password",
|
||||
"placeholder": "Ваш Microsoft App Password"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"Перейдите на Azure Portal",
|
||||
"Зарегистрируйте новое приложение бота",
|
||||
"Получите App ID и создайте пароль",
|
||||
"Настройте канал Teams"
|
||||
]
|
||||
},
|
||||
"googlechat": {
|
||||
"description": "Подключите Google Chat через webhook",
|
||||
"docsUrl": "https://docs.openclaw.ai/channels/googlechat",
|
||||
"fields": {
|
||||
"serviceAccountKey": {
|
||||
"label": "Путь к JSON файлу сервисного аккаунта",
|
||||
"placeholder": "/path/to/service-account.json"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"Создайте проект Google Cloud",
|
||||
"Включите Google Chat API",
|
||||
"Создайте сервисный аккаунт",
|
||||
"Скачайте файл ключа JSON"
|
||||
]
|
||||
},
|
||||
"mattermost": {
|
||||
"description": "Подключите Mattermost через Bot API",
|
||||
"docsUrl": "https://docs.openclaw.ai/channels/mattermost",
|
||||
"fields": {
|
||||
"serverUrl": {
|
||||
"label": "URL сервера",
|
||||
"placeholder": "https://your-mattermost.com"
|
||||
},
|
||||
"botToken": {
|
||||
"label": "Bot Access Token",
|
||||
"placeholder": "Ваш bot access token"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"Перейдите в Mattermost Integrations",
|
||||
"Создайте новую учётную запись бота",
|
||||
"Скопируйте access token"
|
||||
]
|
||||
},
|
||||
"qqbot": {
|
||||
"description": "Подключите канал QQ Bot (встроенный с OpenClaw 3.31)",
|
||||
"docsUrl": "https://icnnp7d0dymg.feishu.cn/wiki/KPIJwlyiGiupMrkiS9ice39Zn2c",
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"cancelled": "Отменено",
|
||||
"loadFailed": "Не удалось загрузить сеанс",
|
||||
"promptFailed": "Не удалось отправить запрос",
|
||||
"promptAborted": "Запрос к модели был прерван до получения ответа. Повторите попытку.",
|
||||
"unsupportedContent": "Неподдерживаемое содержимое",
|
||||
"dismiss": "Закрыть",
|
||||
"image": "Изображение",
|
||||
@@ -216,8 +217,14 @@
|
||||
"skillEmpty": "Подходящие Skill не найдены",
|
||||
"pickAgent": "Выбрать агента",
|
||||
"pickModel": "Выбрать модель",
|
||||
"modelControlTitle": "Модель и глубина рассуждений",
|
||||
"modelSectionTitle": "Модель",
|
||||
"modelPickerTitle": "Переключить модель",
|
||||
"modelSwitchFailed": "Не удалось переключить модель: {{error}}",
|
||||
"reasoningEffortTitle": "Глубина рассуждений",
|
||||
"reasoningEffortDescription": "Более высокий уровень даёт более подробные ответы, но требует больше времени и быстрее расходует контекст.",
|
||||
"reasoningEffortDefault": "По умолчанию",
|
||||
"reasoningEffortUpdateFailed": "Не удалось изменить глубину рассуждений: {{error}}",
|
||||
"clearTarget": "Очистить целевого агента",
|
||||
"targetChip": "@{{agent}}",
|
||||
"agentPickerTitle": "Направить следующее сообщение другому агенту",
|
||||
|
||||
@@ -214,6 +214,21 @@
|
||||
"填写 Client ID (AppKey) 和 Client Secret (AppSecret)"
|
||||
]
|
||||
},
|
||||
"signal": {
|
||||
"description": "使用 signal-cli 连接 Signal",
|
||||
"docsUrl": "https://docs.openclaw.ai/zh-CN/channels/signal",
|
||||
"fields": {
|
||||
"phoneNumber": {
|
||||
"label": "手机号码",
|
||||
"placeholder": "+1234567890"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"在您的系统上安装 signal-cli",
|
||||
"注册或链接您的手机号码",
|
||||
"在下方输入您的手机号码"
|
||||
]
|
||||
},
|
||||
"feishu": {
|
||||
"description": "通过飞书官方推出的 OpenClaw 插件连接飞书/Lark 机器人",
|
||||
"docsUrl": "https://icnnp7d0dymg.feishu.cn/wiki/GKn8wOvHnibpPNkNkPzcAvGlnzK#Py88dTltfoJc1jxAhIBcW3Pkn7b",
|
||||
@@ -254,6 +269,118 @@
|
||||
"填写 Bot ID(可选企业 ID 或者直接使用机器人专属 ID)及 Secret 即可建立连接"
|
||||
]
|
||||
},
|
||||
"imessage": {
|
||||
"description": "通过 BlueBubbles (macOS) 连接 iMessage",
|
||||
"docsUrl": "https://docs.openclaw.ai/zh-CN/channels/bluebubbles",
|
||||
"fields": {
|
||||
"serverUrl": {
|
||||
"label": "BlueBubbles 服务器地址",
|
||||
"placeholder": "http://localhost:1234"
|
||||
},
|
||||
"password": {
|
||||
"label": "服务器密码",
|
||||
"placeholder": "您的服务器密码"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"在您的 Mac 上安装 BlueBubbles 服务器",
|
||||
"记下服务器地址和密码",
|
||||
"在下方输入连接详情"
|
||||
]
|
||||
},
|
||||
"matrix": {
|
||||
"description": "连接到 Matrix 协议",
|
||||
"docsUrl": "https://docs.openclaw.ai/zh-CN/channels/matrix",
|
||||
"fields": {
|
||||
"homeserver": {
|
||||
"label": "Homeserver 地址",
|
||||
"placeholder": "https://matrix.org"
|
||||
},
|
||||
"accessToken": {
|
||||
"label": "访问令牌 (Access Token)",
|
||||
"placeholder": "您的访问令牌"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"创建一个 Matrix 账户或使用现有账户",
|
||||
"从您的客户端获取访问令牌",
|
||||
"在下方输入 Homeserver 地址和令牌"
|
||||
]
|
||||
},
|
||||
"line": {
|
||||
"description": "连接 LINE Messaging API",
|
||||
"docsUrl": "https://docs.openclaw.ai/channels/line",
|
||||
"fields": {
|
||||
"channelAccessToken": {
|
||||
"label": "频道访问令牌",
|
||||
"placeholder": "您的 LINE 频道访问令牌"
|
||||
},
|
||||
"channelSecret": {
|
||||
"label": "频道密钥",
|
||||
"placeholder": "您的 LINE 频道密钥"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"前往 LINE 开发者控制台",
|
||||
"创建一个 Messaging API 频道",
|
||||
"获取频道访问令牌和密钥"
|
||||
]
|
||||
},
|
||||
"msteams": {
|
||||
"description": "通过 Bot Framework 连接 Microsoft Teams",
|
||||
"docsUrl": "https://docs.openclaw.ai/channels/msteams",
|
||||
"fields": {
|
||||
"appId": {
|
||||
"label": "应用 ID",
|
||||
"placeholder": "您的 Microsoft 应用 ID"
|
||||
},
|
||||
"appPassword": {
|
||||
"label": "应用密码",
|
||||
"placeholder": "您的 Microsoft 应用密码"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"前往 Azure 门户",
|
||||
"注册一个新的 Bot 应用",
|
||||
"获取应用 ID 并创建密码",
|
||||
"配置 Teams 频道"
|
||||
]
|
||||
},
|
||||
"googlechat": {
|
||||
"description": "通过 Webhook 连接 Google Chat",
|
||||
"docsUrl": "https://docs.openclaw.ai/channels/googlechat",
|
||||
"fields": {
|
||||
"serviceAccountKey": {
|
||||
"label": "服务账号 JSON 路径",
|
||||
"placeholder": "/path/to/service-account.json"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"创建 Google Cloud 项目",
|
||||
"启用 Google Chat API",
|
||||
"创建服务账号",
|
||||
"下载 JSON 密钥文件"
|
||||
]
|
||||
},
|
||||
"mattermost": {
|
||||
"description": "通过 Bot API 连接 Mattermost",
|
||||
"docsUrl": "https://docs.openclaw.ai/channels/mattermost",
|
||||
"fields": {
|
||||
"serverUrl": {
|
||||
"label": "服务器地址",
|
||||
"placeholder": "https://your-mattermost.com"
|
||||
},
|
||||
"botToken": {
|
||||
"label": "机器人访问令牌",
|
||||
"placeholder": "您的机器人访问令牌"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"前往 Mattermost 集成",
|
||||
"创建一个新的 Bot 账户",
|
||||
"复制访问令牌"
|
||||
]
|
||||
},
|
||||
"qqbot": {
|
||||
"description": "连接 QQ 机器人频道(OpenClaw 3.31 起内置)",
|
||||
"docsUrl": "https://icnnp7d0dymg.feishu.cn/wiki/KPIJwlyiGiupMrkiS9ice39Zn2c",
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"cancelled": "已取消",
|
||||
"loadFailed": "会话加载失败",
|
||||
"promptFailed": "发送提示失败",
|
||||
"promptAborted": "模型请求在生成回复前已中止,请重试。",
|
||||
"unsupportedContent": "不支持的内容",
|
||||
"dismiss": "关闭",
|
||||
"image": "图片",
|
||||
@@ -216,8 +217,14 @@
|
||||
"skillEmpty": "未找到匹配的技能",
|
||||
"pickAgent": "选择 Agent",
|
||||
"pickModel": "选择模型",
|
||||
"modelControlTitle": "模型与推理强度",
|
||||
"modelSectionTitle": "模型",
|
||||
"modelPickerTitle": "切换当前 Agent 模型",
|
||||
"modelSwitchFailed": "模型切换失败:{{error}}",
|
||||
"reasoningEffortTitle": "推理强度",
|
||||
"reasoningEffortDescription": "更高档位会带来更深入的回答,但耗时更长,也会更快消耗上下文。",
|
||||
"reasoningEffortDefault": "默认",
|
||||
"reasoningEffortUpdateFailed": "推理强度更新失败:{{error}}",
|
||||
"clearTarget": "清除目标 Agent",
|
||||
"targetChip": "@{{agent}}",
|
||||
"agentPickerTitle": "将下一条消息直接发送给其他 Agent",
|
||||
|
||||
+236
-16
@@ -6,18 +6,22 @@
|
||||
/**
|
||||
* Supported channel types
|
||||
*/
|
||||
export const SUPPORTED_CHANNEL_TYPES = [
|
||||
'telegram',
|
||||
'discord',
|
||||
'whatsapp',
|
||||
'wechat',
|
||||
'dingtalk',
|
||||
'feishu',
|
||||
'wecom',
|
||||
'qqbot',
|
||||
] as const;
|
||||
|
||||
export type ChannelType = (typeof SUPPORTED_CHANNEL_TYPES)[number];
|
||||
export type ChannelType =
|
||||
| 'whatsapp'
|
||||
| 'wechat'
|
||||
| 'dingtalk'
|
||||
| 'telegram'
|
||||
| 'discord'
|
||||
| 'signal'
|
||||
| 'feishu'
|
||||
| 'wecom'
|
||||
| 'imessage'
|
||||
| 'matrix'
|
||||
| 'line'
|
||||
| 'msteams'
|
||||
| 'googlechat'
|
||||
| 'mattermost'
|
||||
| 'qqbot';
|
||||
|
||||
/**
|
||||
* Channel connection status
|
||||
@@ -82,8 +86,15 @@ export const CHANNEL_ICONS: Record<ChannelType, string> = {
|
||||
dingtalk: '💬',
|
||||
telegram: '✈️',
|
||||
discord: '🎮',
|
||||
signal: '🔒',
|
||||
feishu: '🐦',
|
||||
wecom: '💼',
|
||||
imessage: '💬',
|
||||
matrix: '🔗',
|
||||
line: '🟢',
|
||||
msteams: '👔',
|
||||
googlechat: '💭',
|
||||
mattermost: '💠',
|
||||
qqbot: '🐧',
|
||||
};
|
||||
|
||||
@@ -96,8 +107,15 @@ export const CHANNEL_NAMES: Record<ChannelType, string> = {
|
||||
dingtalk: 'DingTalk',
|
||||
telegram: 'Telegram',
|
||||
discord: 'Discord',
|
||||
signal: 'Signal',
|
||||
feishu: 'Feishu / Lark',
|
||||
wecom: 'WeCom',
|
||||
imessage: 'iMessage',
|
||||
matrix: 'Matrix',
|
||||
line: 'LINE',
|
||||
msteams: 'Microsoft Teams',
|
||||
googlechat: 'Google Chat',
|
||||
mattermost: 'Mattermost',
|
||||
qqbot: 'QQ Bot',
|
||||
};
|
||||
|
||||
@@ -301,6 +319,28 @@ export const CHANNEL_META: Record<ChannelType, ChannelMeta> = {
|
||||
],
|
||||
isPlugin: true,
|
||||
},
|
||||
signal: {
|
||||
id: 'signal',
|
||||
name: 'Signal',
|
||||
icon: '🔒',
|
||||
description: 'channels:meta.signal.description',
|
||||
connectionType: 'token',
|
||||
docsUrl: 'channels:meta.signal.docsUrl',
|
||||
configFields: [
|
||||
{
|
||||
key: 'phoneNumber',
|
||||
label: 'channels:meta.signal.fields.phoneNumber.label',
|
||||
type: 'text',
|
||||
placeholder: 'channels:meta.signal.fields.phoneNumber.placeholder',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
instructions: [
|
||||
'channels:meta.signal.instructions.0',
|
||||
'channels:meta.signal.instructions.1',
|
||||
'channels:meta.signal.instructions.2',
|
||||
],
|
||||
},
|
||||
feishu: {
|
||||
id: 'feishu',
|
||||
name: 'Feishu / Lark',
|
||||
@@ -334,15 +374,195 @@ export const CHANNEL_META: Record<ChannelType, ChannelMeta> = {
|
||||
],
|
||||
isPlugin: true,
|
||||
},
|
||||
imessage: {
|
||||
id: 'imessage',
|
||||
name: 'iMessage',
|
||||
icon: '💬',
|
||||
description: 'channels:meta.imessage.description',
|
||||
connectionType: 'token',
|
||||
docsUrl: 'channels:meta.imessage.docsUrl',
|
||||
configFields: [
|
||||
{
|
||||
key: 'serverUrl',
|
||||
label: 'channels:meta.imessage.fields.serverUrl.label',
|
||||
type: 'text',
|
||||
placeholder: 'channels:meta.imessage.fields.serverUrl.placeholder',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
label: 'channels:meta.imessage.fields.password.label',
|
||||
type: 'password',
|
||||
placeholder: 'channels:meta.imessage.fields.password.placeholder',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
instructions: [
|
||||
'channels:meta.imessage.instructions.0',
|
||||
'channels:meta.imessage.instructions.1',
|
||||
'channels:meta.imessage.instructions.2',
|
||||
],
|
||||
},
|
||||
matrix: {
|
||||
id: 'matrix',
|
||||
name: 'Matrix',
|
||||
icon: '🔗',
|
||||
description: 'channels:meta.matrix.description',
|
||||
connectionType: 'token',
|
||||
docsUrl: 'channels:meta.matrix.docsUrl',
|
||||
configFields: [
|
||||
{
|
||||
key: 'homeserver',
|
||||
label: 'channels:meta.matrix.fields.homeserver.label',
|
||||
type: 'text',
|
||||
placeholder: 'channels:meta.matrix.fields.homeserver.placeholder',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'accessToken',
|
||||
label: 'channels:meta.matrix.fields.accessToken.label',
|
||||
type: 'password',
|
||||
placeholder: 'channels:meta.matrix.fields.accessToken.placeholder',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
instructions: [
|
||||
'channels:meta.matrix.instructions.0',
|
||||
'channels:meta.matrix.instructions.1',
|
||||
'channels:meta.matrix.instructions.2',
|
||||
],
|
||||
isPlugin: true,
|
||||
},
|
||||
line: {
|
||||
id: 'line',
|
||||
name: 'LINE',
|
||||
icon: '🟢',
|
||||
description: 'channels:meta.line.description',
|
||||
connectionType: 'token',
|
||||
docsUrl: 'channels:meta.line.docsUrl',
|
||||
configFields: [
|
||||
{
|
||||
key: 'channelAccessToken',
|
||||
label: 'channels:meta.line.fields.channelAccessToken.label',
|
||||
type: 'password',
|
||||
placeholder: 'channels:meta.line.fields.channelAccessToken.placeholder',
|
||||
required: true,
|
||||
envVar: 'LINE_CHANNEL_ACCESS_TOKEN',
|
||||
},
|
||||
{
|
||||
key: 'channelSecret',
|
||||
label: 'channels:meta.line.fields.channelSecret.label',
|
||||
type: 'password',
|
||||
placeholder: 'channels:meta.line.fields.channelSecret.placeholder',
|
||||
required: true,
|
||||
envVar: 'LINE_CHANNEL_SECRET',
|
||||
},
|
||||
],
|
||||
instructions: [
|
||||
'channels:meta.line.instructions.0',
|
||||
'channels:meta.line.instructions.1',
|
||||
'channels:meta.line.instructions.2',
|
||||
],
|
||||
isPlugin: true,
|
||||
},
|
||||
msteams: {
|
||||
id: 'msteams',
|
||||
name: 'Microsoft Teams',
|
||||
icon: '👔',
|
||||
description: 'channels:meta.msteams.description',
|
||||
connectionType: 'token',
|
||||
docsUrl: 'channels:meta.msteams.docsUrl',
|
||||
configFields: [
|
||||
{
|
||||
key: 'appId',
|
||||
label: 'channels:meta.msteams.fields.appId.label',
|
||||
type: 'text',
|
||||
placeholder: 'channels:meta.msteams.fields.appId.placeholder',
|
||||
required: true,
|
||||
envVar: 'MSTEAMS_APP_ID',
|
||||
},
|
||||
{
|
||||
key: 'appPassword',
|
||||
label: 'channels:meta.msteams.fields.appPassword.label',
|
||||
type: 'password',
|
||||
placeholder: 'channels:meta.msteams.fields.appPassword.placeholder',
|
||||
required: true,
|
||||
envVar: 'MSTEAMS_APP_PASSWORD',
|
||||
},
|
||||
],
|
||||
instructions: [
|
||||
'channels:meta.msteams.instructions.0',
|
||||
'channels:meta.msteams.instructions.1',
|
||||
'channels:meta.msteams.instructions.2',
|
||||
'channels:meta.msteams.instructions.3',
|
||||
],
|
||||
isPlugin: true,
|
||||
},
|
||||
googlechat: {
|
||||
id: 'googlechat',
|
||||
name: 'Google Chat',
|
||||
icon: '💭',
|
||||
description: 'channels:meta.googlechat.description',
|
||||
connectionType: 'webhook',
|
||||
docsUrl: 'channels:meta.googlechat.docsUrl',
|
||||
configFields: [
|
||||
{
|
||||
key: 'serviceAccountKey',
|
||||
label: 'channels:meta.googlechat.fields.serviceAccountKey.label',
|
||||
type: 'text',
|
||||
placeholder: 'channels:meta.googlechat.fields.serviceAccountKey.placeholder',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
instructions: [
|
||||
'channels:meta.googlechat.instructions.0',
|
||||
'channels:meta.googlechat.instructions.1',
|
||||
'channels:meta.googlechat.instructions.2',
|
||||
'channels:meta.googlechat.instructions.3',
|
||||
],
|
||||
},
|
||||
mattermost: {
|
||||
id: 'mattermost',
|
||||
name: 'Mattermost',
|
||||
icon: '💠',
|
||||
description: 'channels:meta.mattermost.description',
|
||||
connectionType: 'token',
|
||||
docsUrl: 'channels:meta.mattermost.docsUrl',
|
||||
configFields: [
|
||||
{
|
||||
key: 'serverUrl',
|
||||
label: 'channels:meta.mattermost.fields.serverUrl.label',
|
||||
type: 'text',
|
||||
placeholder: 'channels:meta.mattermost.fields.serverUrl.placeholder',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'botToken',
|
||||
label: 'channels:meta.mattermost.fields.botToken.label',
|
||||
type: 'password',
|
||||
placeholder: 'channels:meta.mattermost.fields.botToken.placeholder',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
instructions: [
|
||||
'channels:meta.mattermost.instructions.0',
|
||||
'channels:meta.mattermost.instructions.1',
|
||||
'channels:meta.mattermost.instructions.2',
|
||||
],
|
||||
isPlugin: true,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the channel integrations supported by ClawX.
|
||||
* Get primary supported channels (non-plugin, commonly used)
|
||||
*/
|
||||
export function getPrimaryChannels(): ChannelType[] {
|
||||
return [...SUPPORTED_CHANNEL_TYPES];
|
||||
return ['telegram', 'discord', 'whatsapp', 'wechat', 'dingtalk', 'feishu', 'wecom', 'qqbot'];
|
||||
}
|
||||
|
||||
export function isSupportedChannelType(channelType: string): channelType is ChannelType {
|
||||
return (SUPPORTED_CHANNEL_TYPES as readonly string[]).includes(channelType);
|
||||
/**
|
||||
* Get all available channels including plugins
|
||||
*/
|
||||
export function getAllChannels(): ChannelType[] {
|
||||
return Object.keys(CHANNEL_META) as ChannelType[];
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Label } from '@/components/ui/label';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useChannelsStore } from '@/stores/channels';
|
||||
|
||||
import { hostApi } from '@/lib/host-api';
|
||||
import { hostEvents } from '@/lib/host-events';
|
||||
@@ -80,6 +81,7 @@ export function ChannelConfigModal({
|
||||
onChannelSaved,
|
||||
}: ChannelConfigModalProps) {
|
||||
const { t } = useTranslation('channels');
|
||||
const { fetchChannels } = useChannelsStore();
|
||||
const [selectedType, setSelectedType] = useState<ChannelType | null>(initialSelectedType);
|
||||
const [configValues, setConfigValues] = useState<Record<string, string>>({});
|
||||
const [channelName, setChannelName] = useState('');
|
||||
@@ -190,8 +192,9 @@ export function ChannelConfigModal({
|
||||
}, [selectedType, loadingConfig, showChannelName]);
|
||||
|
||||
const finishSave = useCallback(async (channelType: ChannelType) => {
|
||||
await fetchChannels();
|
||||
await onChannelSaved?.(channelType);
|
||||
}, [onChannelSaved]);
|
||||
}, [fetchChannels, onChannelSaved]);
|
||||
|
||||
const finishSaveRef = useRef(finishSave);
|
||||
const onCloseRef = useRef(onClose);
|
||||
@@ -405,6 +408,7 @@ export function ChannelConfigModal({
|
||||
|
||||
toast.success(t('toast.channelSaved', { name: meta.name }));
|
||||
toast.success(t('toast.channelConnecting', { name: meta.name }));
|
||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||
onClose();
|
||||
} catch (error) {
|
||||
toast.error(t('toast.configFailed', { error: String(error) }));
|
||||
|
||||
@@ -708,32 +708,34 @@ function ProviderCard({
|
||||
</div>
|
||||
)}
|
||||
{account.vendorId === 'custom' && (
|
||||
<div className="space-y-1.5 pt-2">
|
||||
<Label className={currentLabelClasses}>{t('aiProviders.dialog.protocol', 'Protocol')}</Label>
|
||||
<div className="flex gap-2 text-meta">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setApiProtocol('openai-completions')}
|
||||
className={cn("flex-1 py-1.5 px-3 rounded-lg border transition-colors", apiProtocol === 'openai-completions' ? "bg-surface-modal border-black/20 dark:border-white/20 shadow-sm font-medium" : "border-transparent bg-black/5 dark:bg-white/5 text-muted-foreground hover:bg-black/10 dark:hover:bg-white/10")}
|
||||
>
|
||||
{t('aiProviders.protocols.openaiCompletions', 'OpenAI Completions')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setApiProtocol('openai-responses')}
|
||||
className={cn("flex-1 py-1.5 px-3 rounded-lg border transition-colors", apiProtocol === 'openai-responses' ? "bg-surface-modal border-black/20 dark:border-white/20 shadow-sm font-medium" : "border-transparent bg-black/5 dark:bg-white/5 text-muted-foreground hover:bg-black/10 dark:hover:bg-white/10")}
|
||||
>
|
||||
{t('aiProviders.protocols.openaiResponses', 'OpenAI Responses')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setApiProtocol('anthropic-messages')}
|
||||
className={cn("flex-1 py-1.5 px-3 rounded-lg border transition-colors", apiProtocol === 'anthropic-messages' ? "bg-surface-modal border-black/20 dark:border-white/20 shadow-sm font-medium" : "border-transparent bg-black/5 dark:bg-white/5 text-muted-foreground hover:bg-black/10 dark:hover:bg-white/10")}
|
||||
>
|
||||
{t('aiProviders.protocols.anthropic', 'Anthropic')}
|
||||
</button>
|
||||
<>
|
||||
<div className="space-y-1.5 pt-2">
|
||||
<Label className={currentLabelClasses}>{t('aiProviders.dialog.protocol', 'Protocol')}</Label>
|
||||
<div className="flex gap-2 text-meta">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setApiProtocol('openai-completions')}
|
||||
className={cn("flex-1 py-1.5 px-3 rounded-lg border transition-colors", apiProtocol === 'openai-completions' ? "bg-surface-modal border-black/20 dark:border-white/20 shadow-sm font-medium" : "border-transparent bg-black/5 dark:bg-white/5 text-muted-foreground hover:bg-black/10 dark:hover:bg-white/10")}
|
||||
>
|
||||
{t('aiProviders.protocols.openaiCompletions', 'OpenAI Completions')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setApiProtocol('openai-responses')}
|
||||
className={cn("flex-1 py-1.5 px-3 rounded-lg border transition-colors", apiProtocol === 'openai-responses' ? "bg-surface-modal border-black/20 dark:border-white/20 shadow-sm font-medium" : "border-transparent bg-black/5 dark:bg-white/5 text-muted-foreground hover:bg-black/10 dark:hover:bg-white/10")}
|
||||
>
|
||||
{t('aiProviders.protocols.openaiResponses', 'OpenAI Responses')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setApiProtocol('anthropic-messages')}
|
||||
className={cn("flex-1 py-1.5 px-3 rounded-lg border transition-colors", apiProtocol === 'anthropic-messages' ? "bg-surface-modal border-black/20 dark:border-white/20 shadow-sm font-medium" : "border-transparent bg-black/5 dark:bg-white/5 text-muted-foreground hover:bg-black/10 dark:hover:bg-white/10")}
|
||||
>
|
||||
{t('aiProviders.protocols.anthropic', 'Anthropic')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{showUserAgentField && (
|
||||
<div className="space-y-1.5 pt-2">
|
||||
@@ -1553,32 +1555,34 @@ function AddProviderDialog({
|
||||
</div>
|
||||
)}
|
||||
{selectedType === 'custom' && (
|
||||
<div className="space-y-2.5">
|
||||
<Label className={labelClasses}>{t('aiProviders.dialog.protocol', 'Protocol')}</Label>
|
||||
<div className="flex gap-2 text-meta">
|
||||
<button
|
||||
type="button"
|
||||
<>
|
||||
<div className="space-y-2.5">
|
||||
<Label className={labelClasses}>{t('aiProviders.dialog.protocol', 'Protocol')}</Label>
|
||||
<div className="flex gap-2 text-meta">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setApiProtocol('openai-completions')}
|
||||
className={cn("flex-1 py-1.5 px-3 rounded-lg border transition-colors", apiProtocol === 'openai-completions' ? "bg-surface-modal border-black/20 dark:border-white/20 shadow-sm font-medium" : "border-transparent bg-black/5 dark:bg-white/5 text-muted-foreground hover:bg-black/10 dark:hover:bg-white/10")}
|
||||
>
|
||||
{t('aiProviders.protocols.openaiCompletions', 'OpenAI Completions')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setApiProtocol('openai-responses')}
|
||||
className={cn("flex-1 py-1.5 px-3 rounded-lg border transition-colors", apiProtocol === 'openai-responses' ? "bg-surface-modal border-black/20 dark:border-white/20 shadow-sm font-medium" : "border-transparent bg-black/5 dark:bg-white/5 text-muted-foreground hover:bg-black/10 dark:hover:bg-white/10")}
|
||||
>
|
||||
{t('aiProviders.protocols.openaiResponses', 'OpenAI Responses')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setApiProtocol('anthropic-messages')}
|
||||
className={cn("flex-1 py-1.5 px-3 rounded-lg border transition-colors", apiProtocol === 'anthropic-messages' ? "bg-surface-modal border-black/20 dark:border-white/20 shadow-sm font-medium" : "border-transparent bg-black/5 dark:bg-white/5 text-muted-foreground hover:bg-black/10 dark:hover:bg-white/10")}
|
||||
>
|
||||
{t('aiProviders.protocols.anthropic', 'Anthropic')}
|
||||
</button>
|
||||
>
|
||||
{t('aiProviders.protocols.openaiCompletions', 'OpenAI Completions')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setApiProtocol('openai-responses')}
|
||||
className={cn("flex-1 py-1.5 px-3 rounded-lg border transition-colors", apiProtocol === 'openai-responses' ? "bg-surface-modal border-black/20 dark:border-white/20 shadow-sm font-medium" : "border-transparent bg-black/5 dark:bg-white/5 text-muted-foreground hover:bg-black/10 dark:hover:bg-white/10")}
|
||||
>
|
||||
{t('aiProviders.protocols.openaiResponses', 'OpenAI Responses')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setApiProtocol('anthropic-messages')}
|
||||
className={cn("flex-1 py-1.5 px-3 rounded-lg border transition-colors", apiProtocol === 'anthropic-messages' ? "bg-surface-modal border-black/20 dark:border-white/20 shadow-sm font-medium" : "border-transparent bg-black/5 dark:bg-white/5 text-muted-foreground hover:bg-black/10 dark:hover:bg-white/10")}
|
||||
>
|
||||
{t('aiProviders.protocols.anthropic', 'Anthropic')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{showUserAgentInAddDialog && (
|
||||
<div className="space-y-2.5">
|
||||
|
||||
@@ -135,12 +135,6 @@ export function WebBrowserHost(): React.ReactElement | null {
|
||||
setLoading(node.isLoading());
|
||||
void navigateToPreview();
|
||||
};
|
||||
const onDomReady = () => {
|
||||
// did-attach can be missed when React mounts during guest initialization.
|
||||
// dom-ready confirms Main has registered the guest and is safe to navigate.
|
||||
attachedWebviewRef.current = node;
|
||||
void navigateToPreview();
|
||||
};
|
||||
const onDidStartLoading = () => setLoading(true);
|
||||
const onDidStopLoading = () => setLoading(false);
|
||||
const onDidFailLoad = (event: DidFailLoadEvent) => {
|
||||
@@ -155,14 +149,12 @@ export function WebBrowserHost(): React.ReactElement | null {
|
||||
};
|
||||
|
||||
node.addEventListener('did-attach', onDidAttach);
|
||||
node.addEventListener('dom-ready', onDomReady);
|
||||
node.addEventListener('did-start-loading', onDidStartLoading);
|
||||
node.addEventListener('did-stop-loading', onDidStopLoading);
|
||||
node.addEventListener('did-fail-load', onDidFailLoad);
|
||||
node.addEventListener('render-process-gone', onRenderProcessGone);
|
||||
removeListenersRef.current = () => {
|
||||
node.removeEventListener('did-attach', onDidAttach);
|
||||
node.removeEventListener('dom-ready', onDomReady);
|
||||
node.removeEventListener('did-start-loading', onDidStartLoading);
|
||||
node.removeEventListener('did-stop-loading', onDidStopLoading);
|
||||
node.removeEventListener('did-fail-load', onDidFailLoad);
|
||||
|
||||
@@ -36,6 +36,7 @@ export type ProviderProtocol =
|
||||
| 'bedrock-converse-stream'
|
||||
| 'ollama'
|
||||
| 'azure-openai-responses';
|
||||
export type ProviderReasoningEffort = 'low' | 'medium' | 'high' | 'xhigh';
|
||||
|
||||
export const BUILTIN_PROVIDER_TYPES = [
|
||||
'anthropic',
|
||||
@@ -65,6 +66,8 @@ export interface ProviderConfig {
|
||||
apiProtocol?: ProviderProtocol;
|
||||
headers?: Record<string, string>;
|
||||
model?: string;
|
||||
reasoningEnabled?: boolean;
|
||||
reasoningEfforts?: ProviderReasoningEffort[];
|
||||
fallbackModels?: string[];
|
||||
fallbackProviderIds?: string[];
|
||||
enabled: boolean;
|
||||
@@ -133,6 +136,8 @@ export interface ProviderAccount {
|
||||
apiProtocol?: ProviderProtocol;
|
||||
headers?: Record<string, string>;
|
||||
model?: string;
|
||||
reasoningEnabled?: boolean;
|
||||
reasoningEfforts?: ProviderReasoningEffort[];
|
||||
fallbackModels?: string[];
|
||||
fallbackAccountIds?: string[];
|
||||
enabled: boolean;
|
||||
|
||||
@@ -10,14 +10,7 @@ import { hostEvents } from '@/lib/host-events';
|
||||
import { ChannelConfigModal } from '@/components/channels/ChannelConfigModal';
|
||||
import { isGatewayStopped } from '@/lib/gateway-status';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
CHANNEL_ICONS,
|
||||
CHANNEL_NAMES,
|
||||
CHANNEL_META,
|
||||
getPrimaryChannels,
|
||||
isSupportedChannelType,
|
||||
type ChannelType,
|
||||
} from '@/types/channel';
|
||||
import { CHANNEL_ICONS, CHANNEL_NAMES, CHANNEL_META, getPrimaryChannels, type ChannelType } from '@/types/channel';
|
||||
import { usesPluginManagedQrAccounts } from '@/lib/channel-alias';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
@@ -138,14 +131,7 @@ export function Channels() {
|
||||
const displayedGatewayHealth = isStaleNotRunningHealthForRunningGateway(gatewayHealth, gatewayStatus.state)
|
||||
? DEFAULT_GATEWAY_HEALTH
|
||||
: gatewayHealth;
|
||||
const visibleChannelGroups = useMemo(
|
||||
() => channelGroups.filter(
|
||||
(group): group is ChannelGroupItem & { channelType: ChannelType } => (
|
||||
isSupportedChannelType(group.channelType)
|
||||
),
|
||||
),
|
||||
[channelGroups],
|
||||
);
|
||||
const visibleChannelGroups = channelGroups;
|
||||
const visibleAgents = agents;
|
||||
const hasStableValue = visibleChannelGroups.length > 0 || visibleAgents.length > 0;
|
||||
const isUsingStableValue = hasStableValue && (loading || Boolean(error));
|
||||
@@ -340,10 +326,14 @@ export function Channels() {
|
||||
}, [visibleChannelGroups]);
|
||||
|
||||
const configuredGroups = useMemo(() => {
|
||||
return displayedChannelTypes
|
||||
const known = displayedChannelTypes
|
||||
.map((type) => groupedByType[type])
|
||||
.filter((group): group is ChannelGroupItem & { channelType: ChannelType } => Boolean(group));
|
||||
}, [displayedChannelTypes, groupedByType]);
|
||||
.filter((group): group is ChannelGroupItem => Boolean(group));
|
||||
const unknown = visibleChannelGroups.filter(
|
||||
(group) => !displayedChannelTypes.includes(group.channelType as ChannelType),
|
||||
);
|
||||
return [...known, ...unknown];
|
||||
}, [visibleChannelGroups, displayedChannelTypes, groupedByType]);
|
||||
|
||||
const unsupportedGroups = displayedChannelTypes.filter((type) => !configuredTypes.includes(type));
|
||||
|
||||
@@ -449,23 +439,19 @@ export function Channels() {
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!deleteTarget) return;
|
||||
const target = deleteTarget;
|
||||
|
||||
// Close the dialog and update the list before waiting for OpenClaw's
|
||||
// coordinated config delivery. Main still owns the durable mutation; on
|
||||
// failure, reload the file-backed view to restore the actual state.
|
||||
setDeleteTarget(null);
|
||||
setChannelGroups((prev) => removeDeletedTarget(prev, target));
|
||||
|
||||
try {
|
||||
await hostApi.channels.deleteConfig(target.channelType, target.accountId);
|
||||
toast.success(target.accountId ? t('toast.accountDeleted') : t('toast.channelDeleted'));
|
||||
await hostApi.channels.deleteConfig(deleteTarget.channelType, deleteTarget.accountId);
|
||||
setChannelGroups((prev) => removeDeletedTarget(prev, deleteTarget));
|
||||
toast.success(deleteTarget.accountId ? t('toast.accountDeleted') : t('toast.channelDeleted'));
|
||||
// Channel reload is debounced in main process; pull again shortly to
|
||||
// converge with runtime state without flashing deleted rows back in.
|
||||
window.setTimeout(() => {
|
||||
void fetchPageData();
|
||||
}, 1200);
|
||||
} catch (deleteError) {
|
||||
toast.error(t('toast.configFailed', { error: String(deleteError) }));
|
||||
void fetchPageData({ configOnly: true });
|
||||
} finally {
|
||||
setDeleteTarget(null);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -855,10 +841,7 @@ export function Channels() {
|
||||
setInitialConfigValuesForModal(undefined);
|
||||
}}
|
||||
onChannelSaved={async () => {
|
||||
// The host may still be restarting Gateway for plugin activation.
|
||||
// Read the committed file-backed view immediately and let the
|
||||
// existing convergence loop refresh runtime status asynchronously.
|
||||
await fetchPageData({ configOnly: true });
|
||||
await fetchPageData({ probe: true });
|
||||
scheduleConvergenceRefresh();
|
||||
setShowConfigModal(false);
|
||||
setSelectedChannelType(null);
|
||||
|
||||
@@ -133,6 +133,8 @@ export function AcpAssistantTurn({
|
||||
}) {
|
||||
const clipboardText = useMemo(() => assistantTurnClipboardText(group), [group]);
|
||||
const renderItems = useMemo(() => partitionTurnItems(group.items), [group.items]);
|
||||
const hideToolPresentation = timing?.status === 'running';
|
||||
const collapseSingleToolsByDefault = timing?.status === 'complete';
|
||||
|
||||
return (
|
||||
<div data-testid="acp-assistant-turn" className="group flex w-full justify-start gap-3">
|
||||
@@ -145,6 +147,7 @@ export function AcpAssistantTurn({
|
||||
<div className="flex min-w-0 flex-1 flex-col items-start gap-3">
|
||||
{renderItems.map((item) => {
|
||||
if (item.kind === 'tool-call-group') {
|
||||
if (hideToolPresentation) return null;
|
||||
return (
|
||||
<div key={item.id} className="w-full">
|
||||
<AcpToolCallsGroup
|
||||
@@ -176,9 +179,10 @@ export function AcpAssistantTurn({
|
||||
}
|
||||
|
||||
if (item.kind === 'tool-call') {
|
||||
if (hideToolPresentation) return null;
|
||||
return (
|
||||
<div key={item.id} data-acp-item-id={item.id} className="-my-1 w-full">
|
||||
<AcpToolCallCard item={item} />
|
||||
<AcpToolCallCard item={item} collapsedByDefault={collapseSingleToolsByDefault} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,11 +39,19 @@ function AcpToolOutputPart({ part }: { part: RenderPart }) {
|
||||
return <AcpRenderPart part={part} tone="process" />;
|
||||
}
|
||||
|
||||
export function AcpToolCallCard({ item, grouped = false }: { item: ToolCallItem; grouped?: boolean }) {
|
||||
export function AcpToolCallCard({
|
||||
item,
|
||||
grouped = false,
|
||||
collapsedByDefault = false,
|
||||
}: {
|
||||
item: ToolCallItem;
|
||||
grouped?: boolean;
|
||||
collapsedByDefault?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation('chat');
|
||||
const hasDetails = Boolean(item.error) || item.outputParts.length > 0;
|
||||
const isFinished = item.status === 'completed' || item.status === 'failed';
|
||||
const shouldStartExpanded = !hasDetails || !(item.historical && isFinished);
|
||||
const shouldStartExpanded = !hasDetails || (!collapsedByDefault && !(item.historical && isFinished));
|
||||
const [expansionState, setExpansionState] = useState<ExpansionState>(() => ({
|
||||
toolCallId: item.toolCallId,
|
||||
expanded: shouldStartExpanded,
|
||||
@@ -57,13 +65,13 @@ export function AcpToolCallCard({ item, grouped = false }: { item: ToolCallItem;
|
||||
if (!hasDetails) return true;
|
||||
if (manualOverride) return currentExpansionState.expanded;
|
||||
if (item.historical && isFinished) return false;
|
||||
if (!isFinished) return true;
|
||||
if (!isFinished && !collapsedByDefault) return true;
|
||||
return currentExpansionState.expanded;
|
||||
})();
|
||||
|
||||
useEffect(() => {
|
||||
if (manualOverride) return;
|
||||
if (!hasDetails || item.historical || !isFinished) return;
|
||||
if (!hasDetails || collapsedByDefault || item.historical || !isFinished) return;
|
||||
|
||||
const timer = window.setTimeout(() => {
|
||||
setExpansionState((state) => {
|
||||
@@ -75,7 +83,7 @@ export function AcpToolCallCard({ item, grouped = false }: { item: ToolCallItem;
|
||||
});
|
||||
}, TOOL_AUTO_COLLAPSE_DELAY_MS);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [hasDetails, item.historical, isFinished, item.toolCallId, manualOverride, shouldStartExpanded]);
|
||||
}, [collapsedByDefault, hasDetails, item.historical, isFinished, item.toolCallId, manualOverride, shouldStartExpanded]);
|
||||
|
||||
const toggleLabel = expanded ? t('acp.collapseTool') : t('acp.expandTool');
|
||||
|
||||
|
||||
+185
-36
@@ -7,7 +7,7 @@
|
||||
* references in the ACP session/prompt request.
|
||||
*/
|
||||
import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
||||
import { SendHorizontal, Square, X, Paperclip, FileText, Film, Music, FileArchive, File, FolderOpen, Loader2, AtSign, Search, ChevronDown, Check } from 'lucide-react';
|
||||
import { SendHorizontal, Square, X, Paperclip, FileText, Film, Music, FileArchive, File, FolderOpen, Loader2, AtSign, Search, ChevronDown, ChevronRight, Check } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
@@ -48,7 +48,12 @@ export interface ChatWorkspaceOption {
|
||||
}
|
||||
|
||||
interface ChatInputProps {
|
||||
onSend: (text: string, attachments?: FileAttachment[], targetAgentId?: string | null) => void;
|
||||
onSend: (
|
||||
text: string,
|
||||
attachments?: FileAttachment[],
|
||||
targetAgentId?: string | null,
|
||||
thinkingLevel?: string,
|
||||
) => void;
|
||||
onStop?: () => void;
|
||||
disabled?: boolean;
|
||||
sending?: boolean;
|
||||
@@ -63,6 +68,7 @@ interface ChatInputProps {
|
||||
// ── Helpers ──────────────────────────────────────────────────────
|
||||
|
||||
const DIRECTORY_MIME_TYPE = 'application/x-directory';
|
||||
const SELECTABLE_THINKING_LEVEL_IDS = ['off', 'low', 'medium', 'high'] as const;
|
||||
|
||||
function formatFileSize(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
@@ -215,6 +221,7 @@ export function ChatInput({
|
||||
const [pickerOpen, setPickerOpen] = useState(false);
|
||||
const [skillPickerOpen, setSkillPickerOpen] = useState(false);
|
||||
const [modelPickerOpen, setModelPickerOpen] = useState(false);
|
||||
const [reasoningMenuOpen, setReasoningMenuOpen] = useState(false);
|
||||
const [workspaceMenuOpen, setWorkspaceMenuOpen] = useState(false);
|
||||
const [skillQuery, setSkillQuery] = useState('');
|
||||
const [quickSkills, setQuickSkills] = useState<QuickAccessSkill[]>([]);
|
||||
@@ -241,6 +248,11 @@ export function ChatInput({
|
||||
const providerError = useProviderStore((s) => s.error);
|
||||
const refreshProviderSnapshot = useProviderStore((s) => s.refreshProviderSnapshot);
|
||||
const currentAgentId = useChatStore((s) => s.currentAgentId);
|
||||
const currentSessionKey = useChatStore((s) => s.currentSessionKey);
|
||||
const sessions = useChatStore((s) => s.sessions);
|
||||
const thinkingDefaults = useChatStore((s) => s.thinkingDefaults);
|
||||
const thinkingLevelUpdatingSessionKey = useChatStore((s) => s.thinkingLevelUpdatingSessionKey);
|
||||
const updateSessionThinkingLevel = useChatStore((s) => s.updateSessionThinkingLevel);
|
||||
const currentAgent = useMemo(
|
||||
() => (agents ?? []).find((agent) => agent.id === currentAgentId) ?? null,
|
||||
[agents, currentAgentId],
|
||||
@@ -263,10 +275,55 @@ export function ChatInput({
|
||||
[currentAgent?.modelRef, defaultModelRef, modelOptions],
|
||||
);
|
||||
const effectiveModelRef = optimisticModelRef || configuredModelRef;
|
||||
const currentModelOption = useMemo(
|
||||
() => modelOptions.find((option) => option.modelRef === effectiveModelRef),
|
||||
[effectiveModelRef, modelOptions],
|
||||
);
|
||||
const currentModelAccount = useMemo(
|
||||
() => providerAccounts.find((account) => account.id === currentModelOption?.accountId),
|
||||
[currentModelOption?.accountId, providerAccounts],
|
||||
);
|
||||
const canConfigureThinking = currentModelAccount?.vendorId === 'custom';
|
||||
const currentModelLabel = useMemo(() => {
|
||||
const matchedOption = modelOptions.find((option) => option.modelRef === effectiveModelRef);
|
||||
return matchedOption?.label || formatModelRefLabel(effectiveModelRef);
|
||||
}, [effectiveModelRef, modelOptions]);
|
||||
return currentModelOption?.label || formatModelRefLabel(effectiveModelRef);
|
||||
}, [currentModelOption?.label, effectiveModelRef]);
|
||||
const currentSession = useMemo(
|
||||
() => sessions.find((session) => session.key === currentSessionKey),
|
||||
[currentSessionKey, sessions],
|
||||
);
|
||||
const canUseThinkingDefaults = currentSession?.createdLocally === true
|
||||
&& thinkingDefaults?.agentId === currentAgentId;
|
||||
const thinkingOptions = useMemo(() => {
|
||||
if (!canConfigureThinking) return [];
|
||||
if (currentSession?.thinkingLevels?.length) return currentSession.thinkingLevels;
|
||||
return canUseThinkingDefaults ? (thinkingDefaults?.thinkingLevels ?? []) : [];
|
||||
}, [
|
||||
canConfigureThinking,
|
||||
canUseThinkingDefaults,
|
||||
currentSession?.thinkingLevels,
|
||||
thinkingDefaults?.thinkingLevels,
|
||||
]);
|
||||
const effortOptions = useMemo(
|
||||
() => SELECTABLE_THINKING_LEVEL_IDS.flatMap((id) => {
|
||||
const option = thinkingOptions.find((candidate) => candidate.id === id);
|
||||
return option ? [option] : [];
|
||||
}),
|
||||
[thinkingOptions],
|
||||
);
|
||||
const effectiveThinkingLevel = currentSession?.thinkingLevel
|
||||
?? currentSession?.thinkingDefault
|
||||
?? (canUseThinkingDefaults ? thinkingDefaults?.thinkingDefault : undefined);
|
||||
const currentThinkingLabel = useMemo(() => {
|
||||
if (!canConfigureThinking || !effectiveThinkingLevel) {
|
||||
return '';
|
||||
}
|
||||
return thinkingOptions.find((option) => option.id === effectiveThinkingLevel)?.label
|
||||
?? effectiveThinkingLevel;
|
||||
}, [canConfigureThinking, effectiveThinkingLevel, thinkingOptions]);
|
||||
const currentModelControlLabel = currentThinkingLabel
|
||||
? `${currentModelLabel} · ${currentThinkingLabel}`
|
||||
: currentModelLabel;
|
||||
const switchingThinkingLevel = thinkingLevelUpdatingSessionKey === currentSessionKey;
|
||||
const mentionableAgents = useMemo(
|
||||
() => (agents ?? []).filter((agent) => agent.id !== currentAgentId),
|
||||
[agents, currentAgentId],
|
||||
@@ -285,11 +342,10 @@ export function ChatInput({
|
||||
);
|
||||
}, [quickSkills, skillQuery]);
|
||||
const showAgentPicker = mentionableAgents.length > 0;
|
||||
const showModelPicker = modelOptions.length > 1;
|
||||
const showModelPicker = modelOptions.length > 1 || effortOptions.length > 0;
|
||||
const chatComposerStatusComponents = rendererExtensionRegistry.getChatComposerStatusComponents();
|
||||
const isGatewayUsable = gatewayStatus.state === 'running' && gatewayStatus.gatewayReady !== false;
|
||||
const inputDisabled = disabled;
|
||||
const gatewayUnavailable = !isGatewayUsable;
|
||||
const workspaceSelectorDisabled = workspaceReadOnly || inputDisabled || sending || !onSelectWorkspace;
|
||||
const skillTokenRanges = useMemo(() => findSkillTokenRanges(input), [input]);
|
||||
const openArtifactPreview = useArtifactPanel((s) => s.openPreview);
|
||||
@@ -327,6 +383,10 @@ export function ChatInput({
|
||||
setOptimisticModelRef(null);
|
||||
}, [currentAgent?.modelRef, currentAgentId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!modelPickerOpen) setReasoningMenuOpen(false);
|
||||
}, [modelPickerOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspaceSelectorDisabled) {
|
||||
setWorkspaceMenuOpen(false);
|
||||
@@ -524,6 +584,29 @@ export function ChatInput({
|
||||
}
|
||||
}, [currentAgent, defaultModelRef, effectiveModelRef, switchingModelRef, t, updateAgentModel]);
|
||||
|
||||
const handleSelectThinkingLevel = useCallback(async (level: string | null) => {
|
||||
if (switchingThinkingLevel || level === (currentSession?.thinkingLevel ?? null)) {
|
||||
setModelPickerOpen(false);
|
||||
textareaRef.current?.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
setModelPickerOpen(false);
|
||||
try {
|
||||
await updateSessionThinkingLevel(currentSessionKey, level);
|
||||
} catch (error) {
|
||||
toast.error(t('composer.reasoningEffortUpdateFailed', { error: String(error) }));
|
||||
} finally {
|
||||
textareaRef.current?.focus();
|
||||
}
|
||||
}, [
|
||||
currentSession?.thinkingLevel,
|
||||
currentSessionKey,
|
||||
switchingThinkingLevel,
|
||||
t,
|
||||
updateSessionThinkingLevel,
|
||||
]);
|
||||
|
||||
const handleWorkspaceButtonClick = useCallback(() => {
|
||||
if (workspaceSelectorDisabled) return;
|
||||
setPickerOpen(false);
|
||||
@@ -689,6 +772,7 @@ export function ChatInput({
|
||||
&& allReady
|
||||
&& !inputDisabled
|
||||
&& !sending
|
||||
&& !switchingThinkingLevel
|
||||
&& !imageGenerating;
|
||||
const canStop = sending && !inputDisabled && !!onStop;
|
||||
|
||||
@@ -728,12 +812,17 @@ export function ChatInput({
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = 'auto';
|
||||
}
|
||||
onSend(textToSend, attachmentsToSend, targetAgentId);
|
||||
onSend(
|
||||
textToSend,
|
||||
attachmentsToSend,
|
||||
targetAgentId,
|
||||
currentSession?.thinkingLevel?.trim() || undefined,
|
||||
);
|
||||
setTargetAgentId(null);
|
||||
setPickerOpen(false);
|
||||
setSkillPickerOpen(false);
|
||||
setWorkspaceMenuOpen(false);
|
||||
}, [input, attachments, canSend, onSend, targetAgentId]);
|
||||
}, [input, attachments, canSend, currentSession?.thinkingLevel, onSend, targetAgentId]);
|
||||
|
||||
const handleStop = useCallback(() => {
|
||||
if (!canStop) return;
|
||||
@@ -978,7 +1067,7 @@ export function ChatInput({
|
||||
isComposingRef.current = false;
|
||||
}}
|
||||
onPaste={handlePaste}
|
||||
placeholder={inputDisabled && gatewayUnavailable ? t('composer.gatewayDisconnectedPlaceholder') : ''}
|
||||
placeholder={inputDisabled ? t('composer.gatewayDisconnectedPlaceholder') : ''}
|
||||
disabled={inputDisabled}
|
||||
data-testid="chat-composer-input"
|
||||
className={cn(
|
||||
@@ -1137,8 +1226,8 @@ export function ChatInput({
|
||||
type="button"
|
||||
data-testid="chat-model-picker-button"
|
||||
className={cn(
|
||||
'inline-flex h-8 max-w-[220px] items-center gap-1 rounded-lg px-1.5 text-meta font-medium text-muted-foreground transition-colors hover:bg-transparent hover:text-foreground focus-visible:outline-none focus-visible:ring-0 disabled:pointer-events-none disabled:opacity-50',
|
||||
(modelPickerOpen || switchingModelRef) && 'text-foreground',
|
||||
'inline-flex h-8 max-w-[280px] items-center gap-1 rounded-lg px-1.5 text-meta font-medium text-muted-foreground transition-colors hover:bg-transparent hover:text-foreground focus-visible:outline-none focus-visible:ring-0 disabled:pointer-events-none disabled:opacity-50',
|
||||
(modelPickerOpen || switchingModelRef || switchingThinkingLevel) && 'text-foreground',
|
||||
)}
|
||||
onClick={() => {
|
||||
setPickerOpen(false);
|
||||
@@ -1146,42 +1235,102 @@ export function ChatInput({
|
||||
setWorkspaceMenuOpen(false);
|
||||
setModelPickerOpen((open) => !open);
|
||||
}}
|
||||
disabled={inputDisabled || sending || !currentAgent || !!switchingModelRef}
|
||||
title={t('composer.pickModel')}
|
||||
disabled={inputDisabled || sending || !currentAgent || !!switchingModelRef || switchingThinkingLevel}
|
||||
title={t('composer.modelControlTitle')}
|
||||
>
|
||||
{switchingModelRef ? (
|
||||
{(switchingModelRef || switchingThinkingLevel) ? (
|
||||
<Loader2 className="h-3.5 w-3.5 shrink-0 animate-spin" />
|
||||
) : null}
|
||||
<span className="truncate">{currentModelLabel}</span>
|
||||
<span className="truncate">{currentModelControlLabel}</span>
|
||||
<ChevronDown className={cn('h-3.5 w-3.5 shrink-0 transition-transform', modelPickerOpen && 'rotate-180')} />
|
||||
</button>
|
||||
{modelPickerOpen && (
|
||||
<div
|
||||
className="absolute left-0 bottom-full z-20 mb-2 w-72 overflow-hidden rounded-2xl border border-black/10 bg-surface-modal p-1.5 shadow-xl dark:border-white/10"
|
||||
className="absolute left-0 bottom-full z-20 mb-2 w-72 overflow-visible rounded-2xl border border-black/10 bg-surface-modal p-1.5 shadow-xl dark:border-white/10"
|
||||
data-testid="chat-model-picker-menu"
|
||||
>
|
||||
<div className="px-3 py-2 text-tiny font-medium text-muted-foreground/80">
|
||||
{t('composer.modelPickerTitle')}
|
||||
{t('composer.modelControlTitle')}
|
||||
</div>
|
||||
<div className="max-h-64 overflow-y-auto">
|
||||
{modelOptions.map((option) => (
|
||||
<button
|
||||
key={option.modelRef}
|
||||
type="button"
|
||||
onClick={() => void handleSelectModel(option.modelRef)}
|
||||
className={cn(
|
||||
'flex w-full items-center justify-between gap-3 rounded-xl px-3 py-2 text-left text-sm font-medium transition-colors',
|
||||
option.modelRef === effectiveModelRef ? 'bg-primary/10 text-foreground' : 'hover:bg-black/5 dark:hover:bg-white/5'
|
||||
)}
|
||||
data-testid={`chat-model-picker-option-${option.label}`}
|
||||
>
|
||||
<span className="truncate">{option.label}</span>
|
||||
{option.modelRef === effectiveModelRef && (
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-primary" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
<div className="max-h-72 overflow-y-auto">
|
||||
{modelOptions.length > 1 && (
|
||||
<>
|
||||
<div className="px-3 pb-1 pt-1 text-tiny font-medium text-muted-foreground/70">
|
||||
{t('composer.modelSectionTitle')}
|
||||
</div>
|
||||
{modelOptions.map((option) => (
|
||||
<button
|
||||
key={option.modelRef}
|
||||
type="button"
|
||||
onClick={() => void handleSelectModel(option.modelRef)}
|
||||
className={cn(
|
||||
'flex w-full items-center justify-between gap-3 rounded-xl px-3 py-2 text-left text-sm font-medium transition-colors',
|
||||
option.modelRef === effectiveModelRef
|
||||
? 'bg-black/5 text-foreground dark:bg-white/10'
|
||||
: 'hover:bg-black/5 dark:hover:bg-white/5',
|
||||
)}
|
||||
data-testid={`chat-model-picker-option-${option.label}`}
|
||||
>
|
||||
<span className="truncate">{option.label}</span>
|
||||
{option.modelRef === effectiveModelRef && (
|
||||
<Check className="h-3.5 w-3.5 shrink-0" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{effortOptions.length > 0 && (
|
||||
<>
|
||||
{modelOptions.length > 1 && <div className="mx-2 my-1.5 h-px bg-border" />}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setReasoningMenuOpen((open) => !open)}
|
||||
className={cn(
|
||||
'flex w-full items-center justify-between gap-3 rounded-xl px-3 py-2 text-left text-sm font-medium transition-colors',
|
||||
reasoningMenuOpen
|
||||
? 'bg-black/5 text-foreground dark:bg-white/10'
|
||||
: 'hover:bg-black/5 dark:hover:bg-white/5',
|
||||
)}
|
||||
data-testid="chat-reasoning-effort-menu-trigger"
|
||||
>
|
||||
<span>{t('composer.reasoningEffortTitle')}</span>
|
||||
<span className="flex min-w-0 items-center gap-1 text-muted-foreground">
|
||||
<span className="truncate">{currentThinkingLabel || t('composer.reasoningEffortDefault')}</span>
|
||||
<ChevronRight className="h-3.5 w-3.5 shrink-0" />
|
||||
</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{reasoningMenuOpen && (
|
||||
<div
|
||||
className="absolute bottom-0 right-full z-30 mr-2 w-52 rounded-2xl border border-black/10 bg-surface-modal p-1.5 shadow-xl dark:border-white/10"
|
||||
data-testid="chat-reasoning-effort-menu"
|
||||
>
|
||||
<p className="px-3 py-2 text-xs leading-5 text-muted-foreground">
|
||||
{t('composer.reasoningEffortDescription')}
|
||||
</p>
|
||||
{effortOptions.map((option) => (
|
||||
<button
|
||||
key={option.id}
|
||||
type="button"
|
||||
onClick={() => void handleSelectThinkingLevel(option.id)}
|
||||
className={cn(
|
||||
'flex w-full items-center justify-between gap-3 rounded-xl px-3 py-2 text-left text-sm font-medium transition-colors',
|
||||
effectiveThinkingLevel === option.id
|
||||
? 'bg-black/5 text-foreground dark:bg-white/10'
|
||||
: 'hover:bg-black/5 dark:hover:bg-white/5',
|
||||
)}
|
||||
data-testid={`chat-reasoning-effort-option-${option.id}`}
|
||||
>
|
||||
<span className="truncate">{option.label}</span>
|
||||
{effectiveThinkingLevel === option.id && (
|
||||
<Check className="h-3.5 w-3.5 shrink-0" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -545,7 +545,12 @@ export function Chat() {
|
||||
</div>
|
||||
|
||||
<ChatInput
|
||||
onSend={(text: string, attachments?: FileAttachment[], targetAgentId?: string | null) => {
|
||||
onSend={(
|
||||
text: string,
|
||||
attachments?: FileAttachment[],
|
||||
targetAgentId?: string | null,
|
||||
thinkingLevel?: string,
|
||||
) => {
|
||||
if (!currentSessionKey || !cwd || !workspaceContextAvailable) return;
|
||||
const targetAgent = targetAgentId
|
||||
? agents.find((agent) => agent.id === targetAgentId) ?? null
|
||||
@@ -554,6 +559,7 @@ export function Chat() {
|
||||
? targetAgent.mainSessionKey || `agent:${targetAgent.id}:main`
|
||||
: currentSessionKey;
|
||||
const existingSession = sessions.find((session) => session.key === sessionKey);
|
||||
const selectedThinkingLevel = targetAgent ? undefined : thinkingLevel?.trim();
|
||||
setLastPromptAttemptSessionKey(sessionKey);
|
||||
const promptCwd = targetAgent?.workspace || cwd;
|
||||
const media = attachments
|
||||
@@ -608,6 +614,7 @@ export function Chat() {
|
||||
cwd: promptCwd,
|
||||
message: text,
|
||||
media,
|
||||
...(selectedThinkingLevel ? { thinkingLevel: selectedThinkingLevel } : {}),
|
||||
});
|
||||
requestAnimationFrame(() => {
|
||||
void scrollToBottom({ animation: 'instant', ignoreEscapes: true });
|
||||
|
||||
@@ -164,6 +164,9 @@ function errorMessage(error: unknown, fallback: string): string {
|
||||
}
|
||||
|
||||
function failedOperationMessage(result: AcpChatOperationResult, fallback: string): string {
|
||||
if (result.errorCode === 'prompt_aborted') {
|
||||
return i18n.t('chat:acp.promptAborted');
|
||||
}
|
||||
return result.error || fallback;
|
||||
}
|
||||
|
||||
|
||||
+112
-1
@@ -13,6 +13,7 @@ import { useGatewayStore } from './gateway';
|
||||
import { pickStartupSessionFallback } from './chat/session-selection';
|
||||
import {
|
||||
applyGatewaySessionsChanged,
|
||||
normalizeGatewaySessionPatch,
|
||||
normalizeGatewaySessionRow,
|
||||
type GatewaySessionsChangedPayload,
|
||||
} from './chat/session-catalog';
|
||||
@@ -486,12 +487,21 @@ async function fetchChatSessionsList(): Promise<Record<string, unknown>> {
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchAgentThinkingDefaults(agentId: string): Promise<Record<string, unknown>> {
|
||||
return useGatewayStore.getState().rpc<Record<string, unknown>>('sessions.list', {
|
||||
agentId,
|
||||
limit: 1,
|
||||
});
|
||||
}
|
||||
|
||||
export const useChatStore = create<ChatState>((set, get) => ({
|
||||
sessions: [],
|
||||
currentSessionKey: DEFAULT_SESSION_KEY,
|
||||
currentAgentId: 'main',
|
||||
sessionLabels: {},
|
||||
sessionLastActivity: {},
|
||||
thinkingDefaults: null,
|
||||
thinkingLevelUpdatingSessionKey: null,
|
||||
|
||||
loadSessions: async (options) => {
|
||||
const requestedGeneration = options?.gatewayGeneration;
|
||||
@@ -546,8 +556,42 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
|
||||
try {
|
||||
const localRevisionBeforeRequest = localSessionCatalogRevision;
|
||||
const data = await fetchChatSessionsList();
|
||||
const stateBeforeRequest = get();
|
||||
const requestedAgentId = stateBeforeRequest.currentAgentId;
|
||||
const selectedSessionBeforeRequest = stateBeforeRequest.sessions.find(
|
||||
(session) => session.key === stateBeforeRequest.currentSessionKey,
|
||||
);
|
||||
const shouldFetchScopedDefaults = requestedAgentId !== 'main'
|
||||
&& selectedSessionBeforeRequest?.createdLocally === true;
|
||||
const [data, scopedDefaultsData] = await Promise.all([
|
||||
fetchChatSessionsList(),
|
||||
shouldFetchScopedDefaults ? fetchAgentThinkingDefaults(requestedAgentId) : null,
|
||||
]);
|
||||
const defaultsData = scopedDefaultsData ?? data;
|
||||
if (generation === sessionCatalogGeneration) {
|
||||
const rawDefaults = defaultsData.defaults
|
||||
&& typeof defaultsData.defaults === 'object'
|
||||
&& !Array.isArray(defaultsData.defaults)
|
||||
? defaultsData.defaults as Record<string, unknown>
|
||||
: null;
|
||||
const normalizedDefaults = rawDefaults
|
||||
? normalizeGatewaySessionRow({ ...rawDefaults, key: '__gateway_defaults__' })
|
||||
: null;
|
||||
const thinkingDefaults = normalizedDefaults
|
||||
? {
|
||||
agentId: requestedAgentId,
|
||||
...(normalizedDefaults.modelProvider
|
||||
? { modelProvider: normalizedDefaults.modelProvider }
|
||||
: {}),
|
||||
...(normalizedDefaults.model ? { model: normalizedDefaults.model } : {}),
|
||||
...(normalizedDefaults.thinkingLevels
|
||||
? { thinkingLevels: normalizedDefaults.thinkingLevels }
|
||||
: {}),
|
||||
...(normalizedDefaults.thinkingDefault
|
||||
? { thinkingDefault: normalizedDefaults.thinkingDefault }
|
||||
: {}),
|
||||
}
|
||||
: null;
|
||||
const rawSessions = Array.isArray(data.sessions) ? data.sessions : [];
|
||||
const normalizedSessions = rawSessions.map((session) => (
|
||||
normalizeGatewaySessionRow(session as Record<string, unknown>)
|
||||
@@ -919,6 +963,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
currentAgentId: getAgentIdFromSessionKey(nextSessionKey),
|
||||
sessionLabels,
|
||||
sessionLastActivity,
|
||||
thinkingDefaults,
|
||||
};
|
||||
});
|
||||
applySessionBackendLabels(set, sessionsWithCurrent);
|
||||
@@ -1374,4 +1419,70 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
sessionLabels: { ...state.sessionLabels, [key]: normalized },
|
||||
}));
|
||||
},
|
||||
|
||||
updateSessionThinkingLevel: async (key: string, level: string | null) => {
|
||||
const state = get();
|
||||
if (state.thinkingLevelUpdatingSessionKey) {
|
||||
throw new Error('A reasoning effort update is already in progress');
|
||||
}
|
||||
const previousSession = state.sessions.find((session) => session.key === key);
|
||||
|
||||
set((current) => ({
|
||||
thinkingLevelUpdatingSessionKey: key,
|
||||
sessions: current.sessions.map((session) => {
|
||||
if (session.key !== key) return session;
|
||||
if (level !== null) return { ...session, thinkingLevel: level };
|
||||
const { thinkingLevel: _thinkingLevel, ...inheritedSession } = session;
|
||||
return inheritedSession;
|
||||
}),
|
||||
}));
|
||||
|
||||
try {
|
||||
const result = await hostApi.gateway.rpc<{
|
||||
resolved?: {
|
||||
thinkingLevel?: unknown;
|
||||
thinkingLevels?: unknown;
|
||||
};
|
||||
}>('sessions.patch', { key, thinkingLevel: level });
|
||||
const resolved = result?.resolved;
|
||||
const resolvedPatch = normalizeGatewaySessionPatch({
|
||||
key,
|
||||
...(Array.isArray(resolved?.thinkingLevels)
|
||||
? { thinkingLevels: resolved.thinkingLevels }
|
||||
: {}),
|
||||
});
|
||||
const inheritedLevel = level === null && typeof resolved?.thinkingLevel === 'string'
|
||||
? resolved.thinkingLevel.trim()
|
||||
: '';
|
||||
|
||||
set((current) => ({
|
||||
sessions: current.sessions.map((session) => {
|
||||
if (session.key !== key) return session;
|
||||
const next = { ...session };
|
||||
if (level === null) delete next.thinkingLevel;
|
||||
else next.thinkingLevel = level;
|
||||
if (resolvedPatch.values.thinkingLevels) {
|
||||
next.thinkingLevels = resolvedPatch.values.thinkingLevels;
|
||||
}
|
||||
if (inheritedLevel) next.thinkingDefault = inheritedLevel;
|
||||
return next;
|
||||
}),
|
||||
}));
|
||||
await get().loadSessions({ force: true });
|
||||
} catch (error) {
|
||||
set((current) => ({
|
||||
sessions: previousSession
|
||||
? current.sessions.map((session) => session.key === key ? previousSession : session)
|
||||
: current.sessions,
|
||||
}));
|
||||
throw error;
|
||||
} finally {
|
||||
set((current) => ({
|
||||
thinkingLevelUpdatingSessionKey:
|
||||
current.thinkingLevelUpdatingSessionKey === key
|
||||
? null
|
||||
: current.thinkingLevelUpdatingSessionKey,
|
||||
}));
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -20,6 +20,8 @@ const STRING_FIELDS = [
|
||||
'derivedTitle',
|
||||
'lastMessagePreview',
|
||||
'thinkingLevel',
|
||||
'thinkingDefault',
|
||||
'modelProvider',
|
||||
'model',
|
||||
'workspacePath',
|
||||
] as const satisfies readonly SessionField[];
|
||||
@@ -70,6 +72,20 @@ export function normalizeGatewaySessionPatch(raw: Record<string, unknown>): Norm
|
||||
}
|
||||
}
|
||||
|
||||
if (hasOwn(raw, 'thinkingLevels')) {
|
||||
present.add('thinkingLevels');
|
||||
if (raw.thinkingLevels === null) {
|
||||
cleared.add('thinkingLevels');
|
||||
} else if (Array.isArray(raw.thinkingLevels)) {
|
||||
values.thinkingLevels = raw.thinkingLevels.flatMap((option) => {
|
||||
if (!isRecord(option)) return [];
|
||||
const id = typeof option.id === 'string' ? option.id.trim() : '';
|
||||
const label = typeof option.label === 'string' ? option.label.trim() : '';
|
||||
return id && label ? [{ id, label }] : [];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (hasOwn(raw, 'updatedAt')) {
|
||||
present.add('updatedAt');
|
||||
if (raw.updatedAt === null) {
|
||||
|
||||
@@ -114,6 +114,8 @@ export const useProviderStore = create<ProviderState>((set, get) => ({
|
||||
apiProtocol: config.apiProtocol,
|
||||
headers: config.headers,
|
||||
model: config.model,
|
||||
reasoningEnabled: config.reasoningEnabled,
|
||||
reasoningEfforts: config.reasoningEfforts,
|
||||
fallbackModels: config.fallbackModels,
|
||||
fallbackAccountIds: config.fallbackProviderIds,
|
||||
enabled: config.enabled,
|
||||
@@ -156,6 +158,8 @@ export const useProviderStore = create<ProviderState>((set, get) => ({
|
||||
if (updates.apiProtocol !== undefined) accountUpdates.apiProtocol = updates.apiProtocol;
|
||||
if (updates.headers !== undefined) accountUpdates.headers = updates.headers;
|
||||
if (updates.model !== undefined) accountUpdates.model = updates.model;
|
||||
if (updates.reasoningEnabled !== undefined) accountUpdates.reasoningEnabled = updates.reasoningEnabled;
|
||||
if (updates.reasoningEfforts !== undefined) accountUpdates.reasoningEfforts = updates.reasoningEfforts;
|
||||
if (updates.fallbackModels !== undefined) accountUpdates.fallbackModels = updates.fallbackModels;
|
||||
if (updates.fallbackProviderIds !== undefined) accountUpdates.fallbackAccountIds = updates.fallbackProviderIds;
|
||||
if (updates.enabled !== undefined) accountUpdates.enabled = updates.enabled;
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
import { completeSetup, expect, test } from './fixtures/electron';
|
||||
|
||||
const configuredChannels = {
|
||||
success: true,
|
||||
channels: [{
|
||||
channelType: 'feishu',
|
||||
defaultAccountId: 'default',
|
||||
status: 'connected',
|
||||
accounts: [{
|
||||
accountId: 'default',
|
||||
name: 'Primary Account',
|
||||
configured: true,
|
||||
status: 'connected',
|
||||
isDefault: true,
|
||||
}],
|
||||
}],
|
||||
};
|
||||
|
||||
test.describe('Channel deletion responsiveness', () => {
|
||||
test('closes the confirmation and removes the channel before host cleanup settles', async ({ electronApp, page }) => {
|
||||
await electronApp.evaluate(({ ipcMain }, response) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(globalThis as any).__clawxDeletePending = false;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(globalThis as any).__clawxResolveDelete = null;
|
||||
const originalHostInvoke = (ipcMain as unknown as {
|
||||
_invokeHandlers?: Map<string, (event: unknown, request: unknown) => Promise<unknown>>;
|
||||
})._invokeHandlers?.get('host:invoke');
|
||||
const respond = (id: unknown, data: unknown) => ({
|
||||
id: typeof id === 'string' ? id : undefined,
|
||||
ok: true,
|
||||
data,
|
||||
});
|
||||
|
||||
ipcMain.removeHandler('host:invoke');
|
||||
ipcMain.handle('host:invoke', async (event, request: {
|
||||
id?: string;
|
||||
module?: string;
|
||||
action?: string;
|
||||
}) => {
|
||||
if (request?.module === 'channels' && request.action === 'accounts') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const pending = (globalThis as any).__clawxDeletePending === true;
|
||||
return respond(request.id, pending ? { success: true, channels: [] } : response);
|
||||
}
|
||||
if (request?.module === 'agents' && request.action === 'list') {
|
||||
return respond(request.id, { success: true, agents: [] });
|
||||
}
|
||||
if (request?.module === 'channels' && request.action === 'deleteConfig') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(globalThis as any).__clawxDeletePending = true;
|
||||
return await new Promise((resolve) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(globalThis as any).__clawxResolveDelete = () => resolve(respond(request.id, { success: true }));
|
||||
});
|
||||
}
|
||||
return originalHostInvoke?.(event, request) ?? respond(request?.id, {});
|
||||
});
|
||||
}, configuredChannels);
|
||||
|
||||
await completeSetup(page);
|
||||
await page.getByTestId('sidebar-nav-channels').click();
|
||||
|
||||
const channelsPage = page.getByTestId('channels-page');
|
||||
await expect(channelsPage.getByTitle('Delete channel')).toBeVisible();
|
||||
await channelsPage.getByTitle('Delete channel').click();
|
||||
await page.getByTestId('confirm-dialog-confirm-button').click();
|
||||
|
||||
await expect(page.getByTestId('confirm-dialog-confirm-button')).toBeHidden();
|
||||
await expect(channelsPage.getByTitle('Delete channel')).toHaveCount(0);
|
||||
await expect.poll(async () => electronApp.evaluate(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return (globalThis as any).__clawxDeletePending;
|
||||
})).toBe(true);
|
||||
|
||||
await electronApp.evaluate(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(globalThis as any).__clawxResolveDelete?.();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
import { completeSetup, expect, test } from './fixtures/electron';
|
||||
|
||||
const responses = {
|
||||
channels: { success: true, channels: [] },
|
||||
agents: { success: true, agents: [] },
|
||||
validation: { success: true, valid: true, warnings: [] },
|
||||
};
|
||||
|
||||
test.describe('Plugin-backed channel save', () => {
|
||||
test('submits QQBot credentials through the typed Channels host API', async ({ electronApp, page }) => {
|
||||
await electronApp.evaluate(({ ipcMain }, fixtures) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(globalThis as any).__clawxPluginChannelSavePayload = null;
|
||||
const originalHostInvoke = (ipcMain as unknown as {
|
||||
_invokeHandlers?: Map<string, (event: unknown, request: unknown) => Promise<unknown>>;
|
||||
})._invokeHandlers?.get('host:invoke');
|
||||
const respond = (id: unknown, data: unknown) => ({
|
||||
id: typeof id === 'string' ? id : undefined,
|
||||
ok: true,
|
||||
data,
|
||||
});
|
||||
|
||||
ipcMain.removeHandler('host:invoke');
|
||||
ipcMain.handle('host:invoke', async (event, request: {
|
||||
id?: string;
|
||||
module?: string;
|
||||
action?: string;
|
||||
payload?: unknown;
|
||||
}) => {
|
||||
if (request?.module === 'channels' && request.action === 'accounts') {
|
||||
return respond(request.id, fixtures.channels);
|
||||
}
|
||||
if (request?.module === 'agents' && request.action === 'list') {
|
||||
return respond(request.id, fixtures.agents);
|
||||
}
|
||||
if (request?.module === 'channels' && request.action === 'validateCredentials') {
|
||||
return respond(request.id, fixtures.validation);
|
||||
}
|
||||
if (request?.module === 'channels' && request.action === 'saveConfig') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(globalThis as any).__clawxPluginChannelSavePayload = request.payload;
|
||||
return respond(request.id, { success: true, activationPending: true });
|
||||
}
|
||||
return originalHostInvoke?.(event, request) ?? respond(request?.id, {});
|
||||
});
|
||||
}, responses);
|
||||
|
||||
await completeSetup(page);
|
||||
await page.getByTestId('sidebar-nav-channels').click();
|
||||
|
||||
const channelsPage = page.getByTestId('channels-page');
|
||||
await expect(channelsPage).toBeVisible();
|
||||
await channelsPage.getByRole('button', { name: /QQ Bot/ }).click();
|
||||
|
||||
await page.locator('#appId').fill('qq-app-id');
|
||||
await page.locator('#clientSecret').fill('qq-client-secret');
|
||||
await page.getByRole('button', { name: /Save & Connect|dialog\.saveAndConnect/i }).click();
|
||||
|
||||
await expect.poll(async () => electronApp.evaluate(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return (globalThis as any).__clawxPluginChannelSavePayload;
|
||||
})).toEqual({
|
||||
channelType: 'qqbot',
|
||||
config: {
|
||||
appId: 'qq-app-id',
|
||||
clientSecret: 'qq-client-secret',
|
||||
},
|
||||
});
|
||||
await expect(page.getByText(/Configure QQ Bot|dialog\.configureTitle/i)).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1,78 +0,0 @@
|
||||
import { completeSetup, expect, test } from './fixtures/electron';
|
||||
|
||||
const unsupportedChannelTypes = [
|
||||
'signal',
|
||||
'imessage',
|
||||
'matrix',
|
||||
'line',
|
||||
'msteams',
|
||||
'googlechat',
|
||||
'mattermost',
|
||||
];
|
||||
|
||||
const channelsResponse = {
|
||||
success: true,
|
||||
channels: [
|
||||
{
|
||||
channelType: 'feishu',
|
||||
defaultAccountId: 'default',
|
||||
status: 'connected',
|
||||
accounts: [],
|
||||
},
|
||||
...unsupportedChannelTypes.map((channelType) => ({
|
||||
channelType,
|
||||
defaultAccountId: 'default',
|
||||
status: 'connected',
|
||||
accounts: [{
|
||||
accountId: 'default',
|
||||
name: `unsupported-${channelType}`,
|
||||
configured: true,
|
||||
status: 'connected',
|
||||
isDefault: true,
|
||||
}],
|
||||
})),
|
||||
],
|
||||
};
|
||||
|
||||
test.describe('ClawX supported channel catalog', () => {
|
||||
test('does not expose unsupported runtime channels as configurable integrations', async ({ electronApp, page }) => {
|
||||
await electronApp.evaluate(({ ipcMain }, response) => {
|
||||
const originalHostInvoke = (ipcMain as unknown as {
|
||||
_invokeHandlers?: Map<string, (event: unknown, request: unknown) => Promise<unknown>>;
|
||||
})._invokeHandlers?.get('host:invoke');
|
||||
const respond = (id: unknown, data: unknown) => ({
|
||||
id: typeof id === 'string' ? id : undefined,
|
||||
ok: true,
|
||||
data,
|
||||
});
|
||||
|
||||
ipcMain.removeHandler('host:invoke');
|
||||
ipcMain.handle('host:invoke', async (event, request: {
|
||||
id?: string;
|
||||
module?: string;
|
||||
action?: string;
|
||||
}) => {
|
||||
if (request?.module === 'channels' && request.action === 'accounts') {
|
||||
return respond(request.id, response);
|
||||
}
|
||||
if (request?.module === 'agents' && request.action === 'list') {
|
||||
return respond(request.id, { success: true, agents: [] });
|
||||
}
|
||||
return originalHostInvoke?.(event, request) ?? respond(request?.id, {});
|
||||
});
|
||||
}, channelsResponse);
|
||||
|
||||
await completeSetup(page);
|
||||
await page.getByTestId('sidebar-nav-channels').click();
|
||||
|
||||
const channelsPage = page.getByTestId('channels-page');
|
||||
await expect(channelsPage).toBeVisible();
|
||||
await expect(channelsPage.getByText('Feishu / Lark')).toBeVisible();
|
||||
await expect(channelsPage.getByText('Telegram', { exact: true })).toBeVisible();
|
||||
|
||||
for (const channelType of unsupportedChannelTypes) {
|
||||
await expect(channelsPage.getByText(channelType, { exact: true })).toHaveCount(0);
|
||||
await expect(channelsPage.getByText(`unsupported-${channelType}`)).toHaveCount(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -76,13 +76,6 @@ function filesActionCalls(
|
||||
return calls.filter((call) => call.module === 'files' && call.action === action);
|
||||
}
|
||||
|
||||
function resolvedAttachmentRefs(calls: RecordedHostInvocation[]): Record<string, unknown>[] {
|
||||
return calls
|
||||
.filter((call) => call.module === 'files' && call.action === 'resolveAttachment')
|
||||
.map((call) => call.payload?.ref)
|
||||
.filter((ref): ref is Record<string, unknown> => ref != null);
|
||||
}
|
||||
|
||||
async function openChat(app: ElectronApplication): Promise<Page> {
|
||||
const page = await getStableWindow(app);
|
||||
try {
|
||||
@@ -234,6 +227,14 @@ test.describe('ACP media attachments', () => {
|
||||
));
|
||||
return resolveCall?.payload?.ref ?? null;
|
||||
}).not.toBeNull();
|
||||
const resolveCall = (await fixture.getHostInvocations()).find((call) => (
|
||||
call.module === 'files'
|
||||
&& call.action === 'resolveAttachment'
|
||||
&& (call.payload?.ref as Record<string, unknown> | undefined)?.uri === spreadsheetPath
|
||||
));
|
||||
const resolvedRef = resolveCall?.payload?.ref as Record<string, unknown>;
|
||||
await fixture.clearInvocations();
|
||||
|
||||
await trigger.click();
|
||||
const menu = page.getByTestId('acp-attachment-open-with-menu');
|
||||
await expect(menu).toBeVisible();
|
||||
@@ -248,17 +249,7 @@ test.describe('ACP media attachments', () => {
|
||||
await expect.poll(async () => filesActionCalls(
|
||||
await fixture.getHostInvocations(),
|
||||
'listAttachmentOpenHandlers',
|
||||
)).toHaveLength(1);
|
||||
const openWithRef = filesActionCalls(
|
||||
await fixture.getHostInvocations(),
|
||||
'listAttachmentOpenHandlers',
|
||||
)[0].payload as Record<string, unknown>;
|
||||
expect(openWithRef).toMatchObject({
|
||||
sessionKey: MAIN_SESSION_KEY,
|
||||
uri: spreadsheetPath,
|
||||
generation: expect.any(Number),
|
||||
});
|
||||
expect(resolvedAttachmentRefs(await fixture.getHostInvocations())).toContainEqual(openWithRef);
|
||||
).map((call) => call.payload)).toEqual([resolvedRef]);
|
||||
const appRows = page.getByTestId('acp-attachment-open-with-app');
|
||||
await expect(appRows).toHaveCount(2);
|
||||
await expect(appRows.nth(0)).toHaveText('Zulu Sheets');
|
||||
@@ -274,7 +265,7 @@ test.describe('ACP media attachments', () => {
|
||||
await expect.poll(async () => filesActionCalls(
|
||||
await fixture.getHostInvocations(),
|
||||
'openAttachmentWith',
|
||||
).map((call) => call.payload)).toEqual([{ ref: openWithRef, handlerId: 'app-alpha' }]);
|
||||
).map((call) => call.payload)).toEqual([{ ref: resolvedRef, handlerId: 'app-alpha' }]);
|
||||
await expect(page.getByTestId('artifact-panel')).toHaveCount(0);
|
||||
await trigger.click();
|
||||
} else {
|
||||
@@ -287,17 +278,7 @@ test.describe('ACP media attachments', () => {
|
||||
await expect.poll(async () => filesActionCalls(
|
||||
await fixture.getHostInvocations(),
|
||||
'revealAttachment',
|
||||
)).toHaveLength(1);
|
||||
const revealRef = filesActionCalls(
|
||||
await fixture.getHostInvocations(),
|
||||
'revealAttachment',
|
||||
)[0].payload as Record<string, unknown>;
|
||||
expect(revealRef).toMatchObject({
|
||||
sessionKey: MAIN_SESSION_KEY,
|
||||
uri: spreadsheetPath,
|
||||
generation: expect.any(Number),
|
||||
});
|
||||
expect(resolvedAttachmentRefs(await fixture.getHostInvocations())).toContainEqual(revealRef);
|
||||
).map((call) => call.payload)).toEqual([resolvedRef]);
|
||||
await expect(page.getByTestId('artifact-panel')).toHaveCount(0);
|
||||
|
||||
await fixture.clearInvocations();
|
||||
|
||||
@@ -779,55 +779,66 @@ test.describe('ClawX ACP inline timeline', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('auto-collapses completed tool cards and respects manual override', async ({ launchElectronApp }) => {
|
||||
test('hides tool activity during a response, then reveals every tool collapsed', async ({ launchElectronApp }) => {
|
||||
const app = await launchElectronApp({ skipSetup: true });
|
||||
|
||||
try {
|
||||
await installAcpChatMocks(app);
|
||||
await installAcpPromptDeferredMock(app);
|
||||
const page = await openChat(app);
|
||||
await expect(page.getByTestId('acp-chat-empty-state')).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
await page.getByTestId('chat-composer-input').fill('Use multiple tools');
|
||||
await page.getByTestId('chat-composer-send').click();
|
||||
await emitAcpSessionUpdates(app, [
|
||||
{
|
||||
sessionUpdate: 'tool_call',
|
||||
toolCallId: 'collapse-tool',
|
||||
title: 'Collapsible tool',
|
||||
toolCallId: 'collapse-tool-1',
|
||||
title: 'First collapsible tool',
|
||||
status: 'in_progress',
|
||||
content: [{ type: 'content', content: { type: 'text', text: 'collapsible output' } }],
|
||||
content: [{ type: 'content', content: { type: 'text', text: 'first output' } }],
|
||||
locations: [],
|
||||
},
|
||||
{
|
||||
sessionUpdate: 'tool_call',
|
||||
toolCallId: 'collapse-tool-2',
|
||||
title: 'Second collapsible tool',
|
||||
status: 'in_progress',
|
||||
content: [{ type: 'content', content: { type: 'text', text: 'second output' } }],
|
||||
locations: [],
|
||||
},
|
||||
]);
|
||||
|
||||
const card = page.getByTestId('acp-tool-call-card');
|
||||
await expect(card).toHaveAttribute('data-expanded', 'true', { timeout: 30_000 });
|
||||
await expect(page.getByTestId('acp-tool-calls-group')).toHaveCount(0);
|
||||
await expect(page.getByTestId('acp-tool-call-card')).toHaveCount(0);
|
||||
|
||||
await emitAcpSessionUpdates(app, [
|
||||
{
|
||||
sessionUpdate: 'tool_call_update',
|
||||
toolCallId: 'collapse-tool',
|
||||
toolCallId: 'collapse-tool-1',
|
||||
status: 'completed',
|
||||
content: [{ type: 'content', content: { type: 'text', text: 'collapsible output' } }],
|
||||
content: [{ type: 'content', content: { type: 'text', text: 'first output' } }],
|
||||
locations: [],
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(card).toHaveAttribute('data-expanded', 'false', { timeout: 30_000 });
|
||||
|
||||
await page.getByTestId('acp-tool-toggle').click();
|
||||
await expect(card).toHaveAttribute('data-expanded', 'true');
|
||||
|
||||
await emitAcpSessionUpdates(app, [
|
||||
{
|
||||
sessionUpdate: 'tool_call_update',
|
||||
toolCallId: 'collapse-tool',
|
||||
toolCallId: 'collapse-tool-2',
|
||||
status: 'completed',
|
||||
content: [{ type: 'content', content: { type: 'text', text: 'collapsible output after override' } }],
|
||||
content: [{ type: 'content', content: { type: 'text', text: 'second output' } }],
|
||||
locations: [],
|
||||
},
|
||||
]);
|
||||
await expect(page.getByTestId('acp-tool-calls-group')).toHaveCount(0);
|
||||
|
||||
await page.waitForTimeout(1_200);
|
||||
await expect(card).toHaveAttribute('data-expanded', 'true');
|
||||
await resolveDeferredAcpPrompt(app);
|
||||
|
||||
const group = page.getByTestId('acp-tool-calls-group');
|
||||
await expect(group).toHaveCount(1, { timeout: 30_000 });
|
||||
await expect(group).toHaveAttribute('data-collapsed', 'true');
|
||||
await expect(page.getByTestId('acp-tool-call-card')).toHaveCount(0);
|
||||
|
||||
await expandAcpToolCallsGroup(page);
|
||||
await expect(page.getByTestId('acp-tool-call-card')).toHaveCount(2);
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
@@ -844,11 +855,6 @@ test.describe('ClawX ACP inline timeline', () => {
|
||||
messageId: 'history-user',
|
||||
content: [{ type: 'text', text: 'Replay the tool call' }],
|
||||
},
|
||||
{
|
||||
sessionUpdate: 'agent_message_chunk',
|
||||
messageId: 'history-assistant-before',
|
||||
content: { type: 'text', text: 'Before the historical tool' },
|
||||
},
|
||||
{
|
||||
sessionUpdate: 'tool_call',
|
||||
toolCallId: 'history-tool',
|
||||
@@ -858,9 +864,9 @@ test.describe('ClawX ACP inline timeline', () => {
|
||||
locations: [],
|
||||
},
|
||||
{
|
||||
sessionUpdate: 'agent_message_chunk',
|
||||
messageId: 'history-assistant-after',
|
||||
content: { type: 'text', text: 'After the historical tool' },
|
||||
sessionUpdate: 'agent_message',
|
||||
messageId: 'history-assistant',
|
||||
content: [{ type: 'text', text: 'Historical answer' }],
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -873,14 +879,7 @@ test.describe('ClawX ACP inline timeline', () => {
|
||||
await page.getByTestId('acp-tool-toggle').click();
|
||||
await expect(card).toHaveAttribute('data-expanded', 'true');
|
||||
await expect(card).toContainText('historical output');
|
||||
const turn = page.getByTestId('acp-assistant-turn');
|
||||
await expect(turn).toContainText('Before the historical tool');
|
||||
await expect(turn).toContainText('After the historical tool');
|
||||
const orderedParts = turn.locator('[data-testid="acp-assistant-message"], [data-testid="acp-tool-call-card"]');
|
||||
await expect(orderedParts).toHaveCount(3);
|
||||
await expect(orderedParts.nth(0)).toContainText('Before the historical tool');
|
||||
await expect(orderedParts.nth(1)).toContainText('Historical tool');
|
||||
await expect(orderedParts.nth(2)).toContainText('After the historical tool');
|
||||
await expect(page.getByTestId('acp-assistant-turn')).toContainText('Historical answer');
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user