diff --git a/docs/README.md b/docs/README.md index 752f10e8473..0c02655db7c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -81,15 +81,6 @@ | [KnowledgeService](./references/knowledge/knowledge-service.md) | Concurrency control and workload management | | [Knowledge Operation Guards](./references/knowledge/operation-guards.md) | Guard, enqueue failure, and recovery semantics for add/delete/reindex | -### CherryClaw (Autonomous Agent) - -| Document | Description | -|----------|-------------| -| [CherryClaw Overview](./references/cherryclaw/overview.md) | Architecture, memory system, API | -| [Channel System](./references/cherryclaw/channels.md) | IM integration (Telegram, etc.) | -| [Claw MCP Server](./references/cherryclaw/mcp-claw.md) | Built-in MCP tools (cron, notify, skills, memory) | -| [Scheduler](./references/cherryclaw/scheduler.md) | Task-based polling scheduler | - ### Components | Document | Description | diff --git a/docs/assets/images/cherryclaw.png b/docs/assets/images/cherryclaw.png deleted file mode 100644 index 06c34bf6900..00000000000 Binary files a/docs/assets/images/cherryclaw.png and /dev/null differ diff --git a/docs/references/ai/README.md b/docs/references/ai/README.md index 8c46514903a..6fe8e84077f 100644 --- a/docs/references/ai/README.md +++ b/docs/references/ai/README.md @@ -50,7 +50,7 @@ src/main/ai/ │ └── claudeCode/ ← Claude Code driver, warm query, SDK adapter ├── agentSession/ ← agent-session topic host │ └── AgentSessionRuntimeService.ts -├── agents/ ← AgentJobsService, AgentTaskJobHandler, runAgentTask, builtin/, cherryclaw/ +├── agents/ ← AgentJobsService, AgentTaskJobHandler, runAgentTask, prompt, heartbeat, builtin/ ├── channels/ ← ChannelManager + IM adapters (discord/feishu/qq/slack/telegram/wechat) + security/ ├── streamManager/ ← AiStreamManager + listeners + persistence backends │ ├── AiStreamManager.ts ← registers the stream IPC (Open/Attach/Detach/Abort) diff --git a/docs/references/ai/core-architecture.md b/docs/references/ai/core-architecture.md index ac860753fce..293b3cfff41 100644 --- a/docs/references/ai/core-architecture.md +++ b/docs/references/ai/core-architecture.md @@ -163,7 +163,7 @@ src/main/ai/ ├── AiService.ts ← lifecycle owner, IPC entry (generate / translate / approval) ├── runtime/ ← execution backends: runtime/aiSdk (Agent + params), runtime/claudeCode ├── agentSession/ ← agent-session topic host -├── agents/ ← AgentJobsService, AgentTaskJobHandler, runAgentTask, cherryclaw +├── agents/ ← AgentJobsService, AgentTaskJobHandler, runAgentTask, prompt, heartbeat ├── channels/ ← ChannelManager + IM adapters (discord/feishu/qq/slack/telegram/wechat) + security/ ├── streamManager/ ← AiStreamManager, listeners, persistence (registers the stream IPC) ├── provider/ ← provider config, endpoint resolution, custom providers diff --git a/docs/references/cherryclaw/channels.md b/docs/references/cherryclaw/channels.md deleted file mode 100644 index c6172334049..00000000000 --- a/docs/references/cherryclaw/channels.md +++ /dev/null @@ -1,139 +0,0 @@ -# CherryClaw Channel System - -The channel system provides IM integration for CherryClaw, allowing users to interact with agents through instant messaging platforms like Telegram. The system uses an abstract adapter pattern, supporting future expansion to Discord, Slack, and other platforms. - -## Architecture - -``` -ChannelManager (singleton, lifecycle management) - ├── adapters Map — Active adapter instances - ├── notifyChannels Set — Channels marked as notification receivers - ├── start() → Load all CherryClaw agents, create adapters for enabled channels - ├── stop() → Disconnect all adapters - └── syncAgent(agentId) → Disconnect old adapters, rebuild from current config - -ChannelAdapter (abstract EventEmitter) - ├── connect() / disconnect() - ├── sendMessage(chatId, text, opts?) - ├── sendMessageDraft(chatId, draftId, text) — Streaming draft updates - ├── sendTypingIndicator(chatId) - └── Events: 'message' → ChannelMessageEvent - 'command' → ChannelCommandEvent - -ChannelMessageHandler (singleton, stateless message router) - ├── handleIncoming(adapter, message) — Route to agent session - ├── handleCommand(adapter, command) — Handle /new /compact /help - └── sessionTracker Map — Active session per agent -``` - -## Adapter Registration - -Adapters self-register via `registerAdapterFactory(type, factory)`. Importing the adapter module triggers registration: - -```typescript -// src/main/services/agents/services/channels/adapters/TelegramAdapter.ts -registerAdapterFactory('telegram', (channel, agentId) => { - return new TelegramAdapter({ channelId: channel.id, agentId, channelConfig: channel.config }) -}) -``` - -`ChannelManager` imports all adapter modules at startup (via `channels/index.ts`); the `registerAdapterFactory` calls execute as module side effects. - -## Message Processing Flow - -### User Messages - -``` -User sends message in Telegram - → TelegramAdapter emits 'message' event - → ChannelManager forwards to ChannelMessageHandler.handleIncoming() - 1. resolveSession(agentId) - → Check sessionTracker → Query existing session → Create new session - 2. Send typing indicator (refreshed every 4s) - 3. Generate random draftId - 4. collectStreamResponse(session, text, abort, onDraft): - - Create session message (persist: true) - - Read stream: - text-delta → Update currentBlockText (accumulated within block) - text-end → Commit to completedText, reset current block - - Send draft every 500ms via sendMessageDraft - 5. sendMessage(chatId, finalText) — Auto-split messages over 4096 characters -``` - -### Command Handling - -| Command | Behavior | -|---|---| -| `/new` | Create new session, update sessionTracker | -| `/compact` | Send `/compact` to current session, collect response | -| `/help` | Return agent name, description, and available commands | - -## Streaming Response - -CherryClaw's streaming response follows these rules: - -- `text-delta` events within the same text block are **cumulative** — each event contains the full text so far, not an increment -- `ChannelMessageHandler` uses `text = value.text` (replace) within a block, commits on `text-end` -- Drafts are sent via `sendMessageDraft` throttled to 500ms -- Typing indicator refreshes every 4s - -## Telegram Adapter - -### Configuration - -```typescript -{ - type: 'telegram', - id: 'unique-channel-id', - enabled: true, - is_notify_receiver: true, - config: { - bot_token: 'YOUR_BOT_TOKEN', - allowed_chat_ids: ['123456789'] - } -} -``` - -### Features - -- Uses **grammY** library, long polling only (desktop apps behind NAT don't support webhooks) -- **Authorization guard**: First middleware checks if chat ID is whitelisted; unauthorized messages are silently dropped -- **Message chunking**: Messages over 4096 characters are automatically split by paragraph/line/hard-split -- **Draft streaming**: Real-time response streaming via Telegram's `sendMessageDraft` API -- **Notification targets**: `notifyChatIds` equals `allowed_chat_ids`; all authorized chats receive notifications - -### Known Limitations - -| Limitation | Description | -|---|---| -| Rate limits | `sendMessage` global 30/s, per-chat 1/s. Draft throttle 500ms, typing 4s | -| Plain text output | Agent responses sent as plain text (no `parse_mode`) to avoid MarkdownV2 escaping issues | -| Long polling only | Desktop apps cannot receive webhooks | - -## Notification Channels - -`ChannelManager` tracks which adapters have channels configured with `is_notify_receiver: true` via the `notifyChannels` Set. `getNotifyAdapters(agentId)` returns all notification adapters for a given agent, used by the `notify` MCP tool and scheduler task notifications. - -## Lifecycle - -- **Start**: `channelManager.start()` is called at app ready alongside the scheduler -- **Stop**: `channelManager.stop()` is called at app exit -- **Sync**: `channelManager.syncAgent(agentId)` is called on agent update/delete, disconnecting old adapters and rebuilding from new config - -## Extending with New Channels - -Adding a new channel type requires: - -1. Implement the `ChannelAdapter` abstract class -2. Call `registerAdapterFactory(type, factory)` in the module -3. Import the module in `channels/index.ts` - -## Key Files - -| File | Description | -|---|---| -| `src/main/services/agents/services/channels/ChannelAdapter.ts` | Abstract interface + event types | -| `src/main/services/agents/services/channels/ChannelManager.ts` | Lifecycle management + adapter factory registration | -| `src/main/services/agents/services/channels/ChannelMessageHandler.ts` | Message routing + streaming response collection | -| `src/main/services/agents/services/channels/adapters/TelegramAdapter.ts` | Telegram adapter implementation | -| `src/main/services/agents/services/channels/index.ts` | Public exports + adapter module imports | diff --git a/docs/references/cherryclaw/mcp-claw.md b/docs/references/cherryclaw/mcp-claw.md deleted file mode 100644 index 9d80674a880..00000000000 --- a/docs/references/cherryclaw/mcp-claw.md +++ /dev/null @@ -1,137 +0,0 @@ -# Claw MCP Server - -The Claw MCP server is a built-in MCP (Model Context Protocol) server automatically injected into every CherryClaw (Soul Mode) session. It provides three self-management tools for the agent: `cron` (task scheduling), `notify` (notifications), and `config` (agent/channel self-configuration). Skill and memory management used to live here too but were extracted into their own standalone MCP servers (see [Related servers](#related-servers-formerly-claw-tools)). - -## Architecture - -``` -CherryClawService.invoke() - → Create ClawServer instance (one new instance per invocation) - → Inject as in-memory MCP server: - _internalMcpServers = { claw: { type: 'inmem', instance: clawServer.mcpServer } } - → ClaudeCodeService merges into SDK options.mcpServers - → SDK auto-discovers tools: mcp__claw__cron, mcp__claw__notify, mcp__claw__config -``` - -ClawServer uses the `@modelcontextprotocol/sdk` `McpServer` class, running in memory mode (no HTTP transport). A new instance is created per CherryClaw session invocation, bound to the current agent's ID. - -## Tool Whitelist - -When an agent has an explicit `allowed_tools` whitelist, `CherryClawService` automatically appends the `mcp__claw__*` wildcard to ensure the SDK doesn't filter out internal MCP tools. When `allowed_tools` is undefined (unrestricted), all tools are already available. - ---- - -## cron Tool - -Manages agent scheduled tasks. The agent can autonomously create, view, and delete periodically executed tasks. - -### Actions - -#### `add` — Create Task - -| Parameter | Type | Required | Description | -|---|---|---|---| -| `name` | string | Yes | Task name | -| `message` | string | Yes | Prompt/instruction to execute | -| `cron` | string | One of three | Cron expression, e.g., `0 9 * * 1-5` | -| `every` | string | One of three | Duration, e.g., `30m`, `2h`, `1h30m` | -| `at` | string | One of three | RFC3339 timestamp for one-time tasks | -| `session_mode` | string | No | `reuse` (default, preserve conversation history) or `new` (new session each time) | - -Only one of `cron`, `every`, `at` can be specified. `every` supports human-friendly duration formats, internally converted to minutes. - -Schedule type mapping: -- `cron` → `schedule_type: 'cron'` -- `every` → `schedule_type: 'interval'` (value in minutes) -- `at` → `schedule_type: 'once'` (value as ISO timestamp) - -Session mode mapping: -- `reuse` → `context_mode: 'session'` -- `new` → `context_mode: 'isolated'` - -#### `list` — List Tasks - -No parameters. Returns all scheduled tasks for the current agent (limit 100), in JSON format. - -#### `remove` — Delete Task - -| Parameter | Type | Required | Description | -|---|---|---|---| -| `id` | string | Yes | Task ID | - ---- - -## notify Tool - -Send notification messages to users through connected channels (e.g., Telegram). The agent can proactively notify users of task results, status updates, or other important information. - -### Parameters - -| Parameter | Type | Required | Description | -|---|---|---|---| -| `message` | string | Yes | Notification content | -| `channel_id` | string | No | Send to specific channel only (omit to send to all notification channels) | - -### Behavior - -1. Get all `is_notify_receiver: true` channel adapters for the current agent -2. If `channel_id` is specified, filter to that channel -3. Send message to all `notifyChatIds` of each adapter -4. Return send count and any errors - -Returns an informational message (not an error) if no notification channels are configured. - ---- - -## config Tool - -Inspect and manage the agent's own configuration — identity, model, and IM channel connections — and drive the onboarding ("bootstrap") ritual. - -### Parameters - -| Parameter | Type | Required | Description | -|---|---|---|---| -| `action` | string | Yes | One of the actions below | -| `type` | string | For `add_channel` | Channel adapter type: `telegram` / `feishu` / `qq` / `wechat` / `discord` / `slack` | -| `name` | string | For `rename` / `add_channel` | New display name (`rename`) or human-readable channel name (`add_channel`) | -| `channel_id` | string | For `update_channel` / `remove_channel` / `reconnect_channel` | Target channel id | -| `config` | object | For `add_channel` | Adapter-specific configuration (optional for `update_channel`) | -| `enabled` | boolean | No | Enable/disable the channel (defaults to true) | - -### Actions - -| Action | Description | -|---|---| -| `status` | Current channels, model, and supported adapter types | -| `rename` | Change the agent's display name | -| `add_channel` / `update_channel` / `remove_channel` | Manage IM channel connections | -| `reconnect_channel` | Re-scan a QR code for a WeChat/Feishu channel (e.g. expired session or failed initial setup) | -| `complete_bootstrap` | Mark the onboarding ritual as done | -| `reset_bootstrap` | Re-run onboarding in the next session | - ---- - -## Related servers (formerly claw tools) - -Skill and memory management were extracted out of claw into their own standalone MCP servers: - -| Capability | Server | Tool | File | -|---|---|---|---| -| Skills (search / install / uninstall / list) | `skills` | `mcp__skills__skills` | `src/main/ai/mcp/servers/skills.ts` | -| Persistent memory (update / append / search) | `agent-memory` | `mcp__agent-memory__memory` | `src/main/ai/mcp/servers/workspaceMemory.ts` | - -> The CherryClaw system prompt and the workspace bootstrap reference memory as `mcp__agent-memory__memory` — **not** `mcp__claw__memory`. - ---- - -## Error Handling - -All tool calls execute within an internal try-catch. On error, returns an `{ isError: true }` MCP response with the error message. Errors are also logged to `loggerService`. - -## Key Files - -| File | Description | -|---|---| -| `src/main/ai/mcp/servers/claw.ts` | ClawServer implementation (`cron` / `notify` / `config` + helpers) | -| `src/main/ai/mcp/servers/__tests__/claw.test.ts` | Unit tests | -| `src/main/ai/runtime/claudeCode/settingsBuilder.ts` | `buildMcpServers` — injects the claw server in Soul Mode | diff --git a/docs/references/cherryclaw/overview.md b/docs/references/cherryclaw/overview.md deleted file mode 100644 index 255047dfbbb..00000000000 --- a/docs/references/cherryclaw/overview.md +++ /dev/null @@ -1,126 +0,0 @@ -# CherryClaw Architecture - -

- CherryClaw -

- -CherryClaw is an autonomous agent type in Cherry Studio, built on the Claude Agent SDK. Unlike standard claude-code agents, CherryClaw has an independent personality system, task-based scheduler, IM channel integration, and a set of self-management tools provided through an internal MCP server. - -## Architecture Overview - -``` -CherryClawService - ├── PromptBuilder — Assembles full system prompt from workspace files - ├── HeartbeatReader — Reads heartbeat file content (for pre-task prompt context) - ├── ClawServer (MCP) — Built-in MCP server providing cron / notify / skills / memory tools - ├── SchedulerService — 60s polling scheduler, queries DB for due tasks and executes - ├── TaskService — Task CRUD + next run time calculation - └── ChannelManager — Channel adapter lifecycle management (Telegram, etc.) -``` - -## Core Design Decisions - -### AgentServiceRegistry Pattern - -`SessionMessageService` no longer hard-codes `ClaudeCodeService`. Instead, it uses `AgentServiceRegistry` to look up the corresponding service implementation by `AgentType`. CherryClaw delegates to claude-code for execution at runtime through the registry. - -```typescript -// src/main/services/agents/services/AgentServiceRegistry.ts -agentServiceRegistry.register('claude-code', new ClaudeCodeService()) -agentServiceRegistry.register('cherry-claw', new CherryClawService()) -``` - -### Custom System Prompt (Replacing Claude Code Presets) - -CherryClaw does not use Claude Code's preset system prompts. `PromptBuilder` assembles a complete custom prompt from workspace files, passed via the `_systemPrompt` field to `ClaudeCodeService`. When this field is present, it serves as the complete system prompt rather than the preset + append mode. - -### Disabling Inapplicable Built-in Tools - -CherryClaw disables a set of SDK built-in tools unsuitable for autonomous operation via `_disallowedTools`: - -| Disabled Tool | Reason | -|---|---| -| `CronCreate` / `CronDelete` / `CronList` | Replaced by internal MCP cron tools | -| `TodoWrite` | Not suitable for autonomous agents | -| `AskUserQuestion` | Autonomous agents should not ask users | -| `EnterPlanMode` / `ExitPlanMode` | Not suitable for autonomous agents | -| `EnterWorktree` / `NotebookEdit` | Not suitable for autonomous agents | - -## Invocation Flow - -``` -CherryClawService.invoke() - 1. PromptBuilder.buildSystemPrompt(workspacePath) - → Load system.md (optional override) + soul.md + user.md + memory/FACT.md - → Assemble into complete system prompt - 2. Create ClawServer instance (in-memory MCP server) - → Inject as _internalMcpServers = { claw: { type: 'inmem', instance } } - 3. Set _disallowedTools (disable inapplicable tools) - 4. If agent has allowed_tools whitelist, append mcp__claw__* wildcard - 5. Delegate to ClaudeCodeService.invoke() - → Use _systemPrompt as complete replacement - → Merge _internalMcpServers into SDK options.mcpServers - → Claude SDK auto-discovers cron / notify / skills / memory tools -``` - -## Memory System - -CherryClaw uses an Anna-inspired three-file memory model, each file with an independent scope: - -``` -{workspace}/ - system.md — Optional system prompt override (replaces default CherryClaw identity) - soul.md — Who you are: personality, tone, communication style - user.md — Who the user is: name, preferences, personal context - memory/ - FACT.md — What you know: persistent project knowledge, technical decisions (6+ months) - JOURNAL.jsonl — Event log: one-off events, completed tasks, session notes (append-only) -``` - -Key rules: -- Each file has independent scope; no cross-file duplication -- `soul.md` and `user.md` are edited directly via Read/Edit tools -- `FACT.md` and `JOURNAL.jsonl` are managed via `memory` MCP tools -- Agent updates autonomously without requesting user approval -- Filenames are case-insensitive - -### PromptBuilder Caching - -`PromptBuilder` uses mtime-based caching for all file reads. Each read performs a single `fs.stat` check — if the file modification time hasn't changed, cached content is returned directly, without persistent file watchers. - -## Database - -CherryClaw uses Drizzle ORM + better-sqlite3 (SQLite) for task data storage: - -| Table | Purpose | -|---|---| -| `scheduled_tasks` | Scheduled tasks (name, prompt, schedule type, next run time, status) | -| `task_run_logs` | Task run logs (run time, duration, status, result/error) | - -Both tables are associated with the agents table via foreign key cascades. - -## API Endpoints - -| Method | Path | Description | -|---|---|---| -| `GET` | `/v1/agents/:agentId/tasks` | List tasks | -| `POST` | `/v1/agents/:agentId/tasks` | Create task | -| `GET` | `/v1/agents/:agentId/tasks/:taskId` | Get task details | -| `PATCH` | `/v1/agents/:agentId/tasks/:taskId` | Update task | -| `DELETE` | `/v1/agents/:agentId/tasks/:taskId` | Delete task | -| `POST` | `/v1/agents/:agentId/tasks/:taskId/run` | Manually trigger run | -| `GET` | `/v1/agents/:agentId/tasks/:taskId/logs` | Get run logs | - -## Key Files - -| File | Description | -|---|---| -| `src/main/services/agents/services/cherryclaw/index.ts` | CherryClawService entry point | -| `src/main/services/agents/services/cherryclaw/prompt.ts` | PromptBuilder system prompt assembly | -| `src/main/services/agents/services/cherryclaw/heartbeat.ts` | HeartbeatReader heartbeat file reading | -| `src/main/services/agents/services/AgentServiceRegistry.ts` | Agent service registry | -| `src/main/services/agents/services/TaskService.ts` | Task CRUD + scheduling calculation | -| `src/main/services/agents/services/SchedulerService.ts` | Polling scheduler | -| `src/main/ai/mcp/servers/claw.ts` | Claw MCP server | -| `src/main/services/agents/services/channels/` | Channel abstraction layer | -| `src/main/services/agents/database/schema/tasks.schema.ts` | Task table schema | diff --git a/docs/references/cherryclaw/scheduler.md b/docs/references/cherryclaw/scheduler.md deleted file mode 100644 index c313e58de84..00000000000 --- a/docs/references/cherryclaw/scheduler.md +++ /dev/null @@ -1,118 +0,0 @@ -# CherryClaw Scheduler - -CherryClaw's scheduler uses a nanoclaw-inspired task-based polling design. The database is the single source of truth — no in-memory timer state is needed, and the system auto-recovers after app restart. - -## Architecture - -``` -SchedulerService (singleton, polling loop) - startLoop() - → Execute tick() every 60s - → taskService.getDueTasks() - → SELECT * FROM scheduled_tasks WHERE status='active' AND next_run <= now() - → For each due task, call runTask(task) (fire-and-forget) - - runTask(task) - 1. Load agent configuration - 2. Read heartbeat file, prepend to task prompt (optional) - 3. Find or create session based on context_mode - 4. sessionMessageService.createSessionMessage({ persist: true }) - 5. Drain stream and wait for completion - 6. Log run to task_run_logs - 7. computeNextRun() to calculate next run time - 8. Send task completion/failure notification via channels (optional) - - stopLoop() - → Clear timer, abort all running tasks -``` - -## Schedule Types - -| Type | `schedule_value` Format | Description | -|---|---|---| -| `cron` | Cron expression, e.g., `0 9 * * 1-5` | Standard cron scheduling (using cron-parser v5) | -| `interval` | Minutes, e.g., `30` | Fixed interval execution | -| `once` | ISO 8601 timestamp | One-time task, auto-marked as completed after execution | - -## Drift-proof Interval Calculation - -`computeNextRun()` anchors to the previous `next_run` timestamp, not the current time. If multiple intervals were missed (e.g., during app shutdown), it skips past expired intervals to calculate the next future time point: - -```typescript -// Anchor to scheduled time to prevent cumulative drift -let next = new Date(task.next_run).getTime() + intervalMs -while (next <= now) { - next += intervalMs -} -``` - -This ensures interval scheduling doesn't accumulate drift from task execution time or polling delays. - -## Context Modes - -Each task can configure `context_mode`: - -| Mode | Behavior | -|---|---| -| `session` | Reuse existing session, maintaining multi-turn conversation context | -| `isolated` | Create a new session each execution, no history context | - -When using `session` mode, `SessionMessageService` captures the SDK's `session_id` (from the `system/init` message) and persists it as `agent_session_id`. On the next run, it's passed as `options.resume`, enabling cross-execution conversation continuity. - -## Heartbeat File - -If an agent has `heartbeat_enabled: true`, the scheduler reads the heartbeat file (path specified by `heartbeat_file` config) before task execution and prepends it as context to the task prompt: - -``` -[Heartbeat] -{heartbeat file content} - -[Task] -{task prompt} -``` - -`HeartbeatReader` includes path traversal protection, ensuring the heartbeat file path cannot escape the workspace directory. - -## Consecutive Error Handling - -The scheduler tracks consecutive error counts per task. After 3 consecutive failures, the task is automatically paused (`status: 'paused'`). The error count resets on the next successful run. This state is tracked in memory, not persisted. - -## Task Completion Notifications - -After each task run, `notifyTaskResult()` sends a status message to all channels with `is_notify_receiver` enabled: - -``` -[Task completed] Task Name -Duration: 12s -``` - -Or on failure: - -``` -[Task failed] Task Name -Duration: 5s -Error: error message -``` - -Notifications are sent fire-and-forget, not blocking the scheduling loop. - -## Manual Triggering - -Besides automatic scheduling, each task can be manually triggered via API or UI: - -- API: `POST /v1/agents/:agentId/tasks/:taskId/run` -- UI: "Run" button in the task settings list - -`runTaskNow()` validates the task exists and isn't already running (returns 409 for duplicates), then triggers execution in the background. - -## Backward Compatibility - -`startScheduler(agent)` and `stopScheduler(agentId)` are preserved as no-ops for compatibility with existing agent handler code. All scheduling logic is driven by the polling loop through database state. - -## Key Files - -| File | Description | -|---|---| -| `src/main/services/agents/services/SchedulerService.ts` | Polling scheduler main logic | -| `src/main/services/agents/services/TaskService.ts` | Task CRUD, getDueTasks, computeNextRun | -| `src/main/services/agents/database/schema/tasks.schema.ts` | scheduled_tasks + task_run_logs table definitions | diff --git a/docs/references/naming-conventions.md b/docs/references/naming-conventions.md index aa481a843a8..99278a0d1f3 100644 --- a/docs/references/naming-conventions.md +++ b/docs/references/naming-conventions.md @@ -251,7 +251,7 @@ Choose number based on what the directory **conceptually contains**, not on whic Decision rule: ask "does this directory hold **many of X**?" — yes → plural; no → singular. When two readings both make sense, pick the one that matches the directory's **default import name** (e.g. `import { ... } from './config'` reads naturally with `config/` singular). -**Same stem, different number — decide by role, not by the word.** A name like `agent` is not inherently singular or plural; its number follows the role the directory plays. The `agents/` listed above is the **collection-bucket** reading — e.g. `src/main/ai/agents/`, which holds many agent implementations (`builtin/`, `cherryclaw/`, …). The same stem is **singular** when the directory is a feature **namespace** that groups one feature's code rather than many agents — `src/renderer/hooks/agent/` (holds the agent feature's hooks, not agents) and `src/renderer/components/chat/agent/` are singular, matching their sibling namespaces (`hooks/chat/`, `hooks/tab/`, `hooks/translate/`). Reading the `agents/` entry as "the word *agent* is always plural" is the trap: apply the decision rule to the directory's actual contents. +**Same stem, different number — decide by role, not by the word.** A name like `agent` is not inherently singular or plural; its number follows the role the directory plays. The `agents/` listed above is the **collection-bucket** reading — e.g. `src/main/ai/agents/`, which holds many agent implementations (`builtin/`, …). The same stem is **singular** when the directory is a feature **namespace** that groups one feature's code rather than many agents — `src/renderer/hooks/agent/` (holds the agent feature's hooks, not agents) and `src/renderer/components/chat/agent/` are singular, matching their sibling namespaces (`hooks/chat/`, `hooks/tab/`, `hooks/translate/`). Reading the `agents/` entry as "the word *agent* is always plural" is the trap: apply the decision rule to the directory's actual contents. ### 4.10 Feature Modules — `features/` vs Type Buckets diff --git a/eslint.config.mjs b/eslint.config.mjs index 2275f0f82cd..3d9d8747652 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -106,8 +106,10 @@ const serviceBarrelZones = serviceTopics.map((topic) => ({ `src/renderer/services/!(${topic})/**/*`, // sibling topic dirs `src/renderer/services/*` // flat files at the services/ root ], - from: `src/renderer/services/${topic}/**/*`, - except: ['**/index.ts'], // the barrel itself stays importable + from: [ + `src/renderer/services/${topic}/!(index).{ts,tsx,js,jsx}`, + `src/renderer/services/${topic}/!(index)/**/*` + ], message: `services/${topic}/ is a topic barrel — import @renderer/services/${topic} (its index.ts), not its internals. renderer-architecture.md §3.1/§5.` })) @@ -636,11 +638,13 @@ export default defineConfig([ from: ['src/renderer/components', 'src/renderer/hooks'], message: 'utils/ is stateless and may call downward infra (data/ipc) but must not import components/hooks or any higher app layer. renderer-architecture.md §3.' }, - // @logger is a §2 primitive that physically lives under services/. `from` uses a glob, so `except` must also glob. + // @logger is a §2 primitive that physically lives under services/; keep it out of the restricted glob. { target: 'src/renderer/utils', - from: ['src/renderer/services/**/*'], - except: ['**/LoggerService.ts'], + from: [ + 'src/renderer/services/!(LoggerService).{ts,tsx,js,jsx}', + 'src/renderer/services/!(LoggerService)/**/*' + ], message: 'utils/ must not import renderer services (except @logger). renderer-architecture.md §3.' }, ...serviceBarrelZones diff --git a/src/main/ai/agentSession/AgentSessionRuntimeService.ts b/src/main/ai/agentSession/AgentSessionRuntimeService.ts index 13b31388e88..ff235da8d4c 100644 --- a/src/main/ai/agentSession/AgentSessionRuntimeService.ts +++ b/src/main/ai/agentSession/AgentSessionRuntimeService.ts @@ -62,6 +62,7 @@ export interface BeginAgentSessionTurnInput { modelId: UniqueModelId assistantMessageId: string userMessage?: AgentSessionMessageEntity + headless?: boolean /** Container-level OTel trace id (one trace per session); cached on the entry. */ traceId?: string } @@ -99,6 +100,7 @@ type AgentSessionTurn = { terminalStatus?: AgentSessionRuntimeTerminalStatus controller?: ReadableStreamDefaultController activeToolIds: Set + headless?: boolean } type AgentSessionRuntimeEntry = { @@ -109,10 +111,15 @@ type AgentSessionRuntimeEntry = { agentId: string agentType: string modelId: UniqueModelId + headless?: boolean status: AgentSessionRuntimeStatus pendingTurns: AgentSessionMessageEntity[] connection?: AgentRuntimeConnection connectionLoop?: Promise + /** The `headless` mode {@link connection} was built with. `beginTurn` compares it against an incoming + * turn's headless flag to detect a baked-policy mismatch (a headless scheduled/channel run reusing a + * primed non-headless connection whose Claude settings still allow AskUserQuestion) and rebuild. */ + connectionHeadless?: boolean /** In-flight {@link ensureConnection} promise — shared by concurrent callers so only one connect runs. */ connecting?: Promise currentTurn?: AgentSessionTurn @@ -122,6 +129,8 @@ type AgentSessionRuntimeEntry = { startingNextTurn?: boolean /** Ids of pending messages that arrived mid-turn (steers) — drives the system-reminder wrap. */ steerMessageIds?: Set + /** Ids of queued follow-ups that must open a responder-less/headless turn. */ + headlessMessageIds?: Set /** Roll in progress: a steer was injected mid-turn (`steer-boundary`), the current row was finalised * as A1a, and the post-steer chunks are buffered until the continuation row (A2) opens its stream. */ rolling?: boolean @@ -130,6 +139,8 @@ type AgentSessionRuntimeEntry = { /** The injected steer(s) carried to the continuation turn for its rename/seed context (U2 is already * persisted by the provider — these do NOT create a new user row). */ rollSteerInputs?: AgentRuntimeUserInput[] + /** Whether the post-steer continuation turn should keep responder-less/headless enforcement. */ + rollHeadless?: boolean compacting?: boolean } @@ -214,28 +225,42 @@ export class AgentSessionRuntimeService extends BaseService { modelId: input.modelId, admitted: false, abortController: new AbortController(), - activeToolIds: new Set() + activeToolIds: new Set(), + headless: input.headless === true } if (existing?.status === 'idle') { - this.clearIdleTimer(existing) - existing.pendingTurns = [] - existing.topicId = input.topicId - existing.sessionTraceId = input.traceId ?? existing.sessionTraceId - existing.agentId = input.agentId - existing.agentType = input.agentType - existing.modelId = input.modelId - existing.status = 'active' - existing.currentTurn = turn - - return { - listeners: [ - this.createPersistenceListener(existing, userMessage), - new AgentSessionRuntimeTerminalListener(this, input.sessionId), - new TraceFlushListener(input.topicId) - ], - turnId, - abortController: turn.abortController + const headless = input.headless === true + // The live (or in-flight) connection bakes `headless` into its Claude settings — notably the + // AskUserQuestion disallow for responder-less runs. A primed connection is always built + // non-headless, so a scheduled/channel run reusing an idle interactive session would inherit the + // permissive policy and could stall on AskUserQuestion. When the turn needs a different mode than + // the connection was built for, drop it and rebuild (fall through to fresh-entry creation) rather + // than reuse; the resume token is re-hydrated on reconnect, so conversation context survives. + const builtHeadless = existing.connection ? existing.connectionHeadless === true : existing.headless === true + const canReuse = !(existing.connection || existing.connecting) || builtHeadless === headless + + if (canReuse) { + this.clearIdleTimer(existing) + existing.pendingTurns = [] + existing.topicId = input.topicId + existing.sessionTraceId = input.traceId ?? existing.sessionTraceId + existing.agentId = input.agentId + existing.agentType = input.agentType + existing.modelId = input.modelId + existing.headless = headless + existing.status = 'active' + existing.currentTurn = turn + + return { + listeners: [ + this.createPersistenceListener(existing, userMessage), + new AgentSessionRuntimeTerminalListener(this, input.sessionId), + new TraceFlushListener(input.topicId) + ], + turnId, + abortController: turn.abortController + } } } @@ -248,6 +273,7 @@ export class AgentSessionRuntimeService extends BaseService { agentId: input.agentId, agentType: input.agentType, modelId: input.modelId, + headless: input.headless === true, status: 'active', pendingTurns: [], currentTurn: turn @@ -435,12 +461,13 @@ export class AgentSessionRuntimeService extends BaseService { }) } - enqueueUserMessage(sessionId: string, message: AgentSessionMessageEntity): void { + enqueueUserMessage(sessionId: string, message: AgentSessionMessageEntity, opts: { headless?: boolean } = {}): void { const entry = this.entries.get(sessionId) if (!entry) return entry.status = 'active' this.clearIdleTimer(entry) + if (opts.headless === true) (entry.headlessMessageIds ??= new Set()).add(message.id) const turn = entry.currentTurn // Live turn + a backend that can steer → inject into the running turn (claude's PreToolUse steer @@ -476,6 +503,7 @@ export class AgentSessionRuntimeService extends BaseService { entry.rolling = false entry.rollBuffer = undefined entry.rollSteerInputs = undefined + entry.rollHeadless = undefined } entry.status = 'idle' @@ -624,6 +652,7 @@ export class AgentSessionRuntimeService extends BaseService { agentId: entry.agentId, modelId: entry.modelId, resumeToken: entry.lastResumeToken, + headless: entry.headless === true, trace: this.sessionTraceContext(entry) }) if (!this.isCurrentEntry(entry)) { @@ -634,10 +663,14 @@ export class AgentSessionRuntimeService extends BaseService { } entry.connection = connection + entry.connectionHeadless = entry.headless === true this.refreshContextUsage(entry, connection) this.refreshSupportedCommands(entry, connection) entry.connectionLoop = this.runConnectionLoop(entry, connection).finally(() => { - if (entry.connection === connection) entry.connection = undefined + if (entry.connection === connection) { + entry.connection = undefined + entry.connectionHeadless = undefined + } if (entry.connectionLoop) entry.connectionLoop = undefined }) return true @@ -683,6 +716,11 @@ export class AgentSessionRuntimeService extends BaseService { entry.rolling = true entry.rollBuffer = [] entry.rollSteerInputs = event.inputs + // A responder exists if the pre-steer turn was interactive or any injected steer came from one. + entry.rollHeadless = + entry.currentTurn?.headless === true && + event.inputs.every((input) => entry.headlessMessageIds?.has(input.message.id) === true) + for (const input of event.inputs) entry.headlessMessageIds?.delete(input.message.id) this.closeCurrentTurn(entry, 'success') break case 'steer-undelivered': @@ -919,6 +957,7 @@ export class AgentSessionRuntimeService extends BaseService { } const assistantMessageId = assistantMessage.id + const headless = entry.headlessMessageIds?.delete(nextMessage.id) === true const turnId = crypto.randomUUID() entry.currentTurn = { @@ -928,7 +967,8 @@ export class AgentSessionRuntimeService extends BaseService { modelId: entry.modelId, admitted: false, abortController: new AbortController(), - activeToolIds: new Set() + activeToolIds: new Set(), + headless } const messages = createRuntimeSeedMessages(nextMessage, assistantMessageId) @@ -985,7 +1025,9 @@ export class AgentSessionRuntimeService extends BaseService { */ private async startContinuationTurn(entry: AgentSessionRuntimeEntry): Promise { const steerMessage = entry.rollSteerInputs?.[0]?.message ?? createSyntheticUserMessage(entry.sessionId) + const headless = entry.rollHeadless === true entry.rollSteerInputs = undefined + entry.rollHeadless = undefined const rootSpan = this.startRuntimeRootSpan(entry) let assistantMessage: Awaited> @@ -1005,6 +1047,7 @@ export class AgentSessionRuntimeService extends BaseService { rootSpan?.end() entry.rolling = false entry.rollBuffer = undefined + entry.rollHeadless = undefined application.get('AiStreamManager').broadcastTopicError(entry.topicId, entry.modelId, serializeError(error)) this.markTurnTerminal(entry.sessionId, 'error') return @@ -1020,7 +1063,8 @@ export class AgentSessionRuntimeService extends BaseService { // Pre-admitted: the steer was already delivered via the hook, so `admitTurn` must NOT re-send it. admitted: true, abortController: new AbortController(), - activeToolIds: new Set() + activeToolIds: new Set(), + headless } const messages = createRuntimeSeedMessages(steerMessage, assistantMessageId) @@ -1066,6 +1110,10 @@ export class AgentSessionRuntimeService extends BaseService { for (const chunk of buffered) this.enqueueTurnChunk(turn, chunk) } + isCurrentTurnHeadless(sessionId: string): boolean { + return this.entries.get(sessionId)?.currentTurn?.headless === true + } + private startRuntimeRootSpan(entry: AgentSessionRuntimeEntry): Span | undefined { const traceId = entry.sessionTraceId if (!traceId) return undefined @@ -1160,6 +1208,7 @@ export class AgentSessionRuntimeService extends BaseService { entry.rolling = false entry.rollBuffer = undefined entry.rollSteerInputs = undefined + entry.rollHeadless = undefined if (entry.compacting) { application.get('CacheService').setShared(AGENT_SESSION_COMPACTION_CACHE_KEY(entry.sessionId), { status: 'idle' @@ -1200,6 +1249,7 @@ export class AgentSessionRuntimeService extends BaseService { private closeConnection(entry: AgentSessionRuntimeEntry): AgentRuntimeConnection | undefined { const connection = entry.connection entry.connection = undefined + entry.connectionHeadless = undefined entry.connectionLoop = undefined return connection } diff --git a/src/main/ai/agentSession/__tests__/AgentSessionRuntimeService.test.ts b/src/main/ai/agentSession/__tests__/AgentSessionRuntimeService.test.ts index 2376b1d983d..381b2794ca1 100644 --- a/src/main/ai/agentSession/__tests__/AgentSessionRuntimeService.test.ts +++ b/src/main/ai/agentSession/__tests__/AgentSessionRuntimeService.test.ts @@ -182,6 +182,92 @@ describe('AgentSessionRuntimeService', () => { }) }) + describe('per-turn headless state', () => { + it('opens a queued busy follow-up as headless when enqueueUserMessage is marked headless', async () => { + const service = new AgentSessionRuntimeService() + service.beginTurn(baseTurnInput) + + service.enqueueUserMessage('session-1', userMessage('user-2'), { headless: true }) + expect(getEntry(service).headlessMessageIds.has('user-2')).toBe(true) + + service.markTurnTerminal('session-1', 'success') + await new Promise((resolve) => setTimeout(resolve, 0)) + + const entry = getEntry(service) + expect(entry.currentTurn.userMessage.id).toBe('user-2') + expect(entry.currentTurn.headless).toBe(true) + expect(entry.headlessMessageIds?.has('user-2')).toBe(false) + expect(service.isCurrentTurnHeadless('session-1')).toBe(true) + }) + + it('opens an unmarked queued busy follow-up as interactive', async () => { + const service = new AgentSessionRuntimeService() + service.beginTurn({ ...baseTurnInput, headless: true }) + + service.enqueueUserMessage('session-1', userMessage('user-2')) + service.markTurnTerminal('session-1', 'success') + await new Promise((resolve) => setTimeout(resolve, 0)) + + expect(getEntry(service).currentTurn.headless).toBe(false) + expect(service.isCurrentTurnHeadless('session-1')).toBe(false) + }) + + it('sets current turn headless from beginTurn input', () => { + const service = new AgentSessionRuntimeService() + + service.beginTurn({ ...baseTurnInput, headless: true }) + + expect(getEntry(service).currentTurn.headless).toBe(true) + expect(service.isCurrentTurnHeadless('session-1')).toBe(true) + }) + + async function rollContinuation(initialHeadless: boolean, steerHeadless: boolean) { + const service = new AgentSessionRuntimeService() + service.beginTurn({ ...baseTurnInput, userMessage: userMessage('user-1'), headless: initialHeadless }) + const entry = getEntry(service) + if (steerHeadless) (entry.headlessMessageIds ??= new Set()).add('user-2') + + ;(service as any).handleRuntimeEvent(entry, { + type: 'steer-boundary', + inputs: [{ message: userMessage('user-2'), systemReminder: true }] + }) + await (service as any).startContinuationTurn(entry) + + return { service, entry } + } + + it('keeps a roll continuation headless when the current turn and injected steer are headless', async () => { + const { service, entry } = await rollContinuation(true, true) + + expect(entry.currentTurn.userMessage.id).toBe('user-2') + expect(entry.currentTurn.headless).toBe(true) + expect(entry.rollHeadless).toBeUndefined() + expect(service.isCurrentTurnHeadless('session-1')).toBe(true) + + service.closeSession('session-1') + }) + + it('opens a headless turn plus interactive steer roll continuation as interactive', async () => { + const { service, entry } = await rollContinuation(true, false) + + expect(entry.currentTurn.userMessage.id).toBe('user-2') + expect(entry.currentTurn.headless).toBe(false) + expect(service.isCurrentTurnHeadless('session-1')).toBe(false) + + service.closeSession('session-1') + }) + + it('opens an interactive turn plus headless steer roll continuation as interactive', async () => { + const { service, entry } = await rollContinuation(false, true) + + expect(entry.currentTurn.userMessage.id).toBe('user-2') + expect(entry.currentTurn.headless).toBe(false) + expect(service.isCurrentTurnHeadless('session-1')).toBe(false) + + service.closeSession('session-1') + }) + }) + describe('reconcileStalePendingMessages — boot crash recovery', () => { it('marks crash-orphaned pending assistant messages as errored on init', async () => { mocks.findPendingAssistantMessageIds.mockReturnValue(['stale-1', 'stale-2']) @@ -354,6 +440,84 @@ describe('AgentSessionRuntimeService', () => { }) }) + it('rebuilds instead of reusing an idle non-headless connection for a headless run', () => { + // A primed/interactive connection is built non-headless (AskUserQuestion allowed). Reusing it for a + // headless (scheduled/channel) run would leave AskUserQuestion enabled and stall on a prompt no one + // answers. beginTurn must drop the stale connection and rebuild with the headless policy. + const service = new AgentSessionRuntimeService() + const first = service.beginTurn(baseTurnInput) + const entry = getEntry(service) + const connection = { close: vi.fn(), send: vi.fn(), events: [] } + entry.lastResumeToken = 'resume-1' + entry.connection = connection + entry.connectionHeadless = false + + void terminalListener(first).onDone({ status: 'success', isTopicDone: true }) + const second = service.beginTurn({ + ...baseTurnInput, + assistantMessageId: 'assistant-2', + userMessage: userMessage('user-2'), + headless: true + }) + + expect(second).not.toBe(first) + expect(connection.close).toHaveBeenCalledOnce() + const rebuilt = getEntry(service) + expect(rebuilt.connection).toBeUndefined() // torn down; ensureConnection rebuilds with headless baked in + expect(rebuilt.headless).toBe(true) + }) + + it('reuses an idle headless connection for another headless run', () => { + const service = new AgentSessionRuntimeService() + const first = service.beginTurn({ ...baseTurnInput, headless: true }) + const entry = getEntry(service) + const connection = { close: vi.fn(), send: vi.fn(), events: [] } + entry.lastResumeToken = 'resume-1' + entry.connection = connection + entry.connectionHeadless = true + + void terminalListener(first).onDone({ status: 'success', isTopicDone: true }) + const second = service.beginTurn({ + ...baseTurnInput, + assistantMessageId: 'assistant-2', + userMessage: userMessage('user-2'), + headless: true + }) + + expect(second).not.toBe(first) + expect(connection.close).not.toHaveBeenCalled() + expect(getEntry(service).connection).toBe(connection) + expect(getEntry(service).headless).toBe(true) + }) + + it('rebuilds instead of reusing an idle in-flight (connecting) non-headless connection for a headless run', () => { + // No live connection yet — a non-headless connect is still in flight (`connecting` set, `connection` + // undefined). A headless run must not latch onto that pending non-headless connection; it should tear + // the entry down and rebuild with the headless policy baked in. + const service = new AgentSessionRuntimeService() + const first = service.beginTurn(baseTurnInput) + const entry = getEntry(service) + entry.lastResumeToken = 'resume-1' + + void terminalListener(first).onDone({ status: 'success', isTopicDone: true }) + // Simulate primeConnection still mid-connect, built non-headless (never resolves in this test). + entry.connection = undefined + entry.connecting = new Promise(() => {}) + entry.headless = false + + const second = service.beginTurn({ + ...baseTurnInput, + assistantMessageId: 'assistant-2', + userMessage: userMessage('user-2'), + headless: true + }) + + expect(second).not.toBe(first) + const rebuilt = getEntry(service) + expect(rebuilt.connecting).toBeUndefined() // stale in-flight connect dropped; fresh entry rebuilds headless + expect(rebuilt.headless).toBe(true) + }) + it('applies tool-policy updates when disabled tools change', async () => { const service = new AgentSessionRuntimeService() service.beginTurn(baseTurnInput) @@ -1175,6 +1339,7 @@ describe('AgentSessionRuntimeService', () => { agentId: 'agent-1', modelId: 'claude-code::claude-sonnet-4-5', resumeToken: undefined, + headless: false, trace: { topicId: 'agent-session:session-1', traceId: 'a'.repeat(32), @@ -1227,6 +1392,7 @@ describe('AgentSessionRuntimeService', () => { agentId: 'agent-1', modelId: 'claude-code::claude-sonnet-4-5', resumeToken: 'resume-db', + headless: false, trace: { topicId: 'agent-session:session-1', traceId: 'a'.repeat(32), diff --git a/src/main/ai/agents/cherryclaw/__tests__/heartbeat.test.ts b/src/main/ai/agents/__tests__/heartbeat.test.ts similarity index 100% rename from src/main/ai/agents/cherryclaw/__tests__/heartbeat.test.ts rename to src/main/ai/agents/__tests__/heartbeat.test.ts diff --git a/src/main/ai/agents/cherryclaw/__tests__/prompt.test.ts b/src/main/ai/agents/__tests__/prompt.test.ts similarity index 73% rename from src/main/ai/agents/cherryclaw/__tests__/prompt.test.ts rename to src/main/ai/agents/__tests__/prompt.test.ts index 1de3da561b7..16c65e1d638 100644 --- a/src/main/ai/agents/cherryclaw/__tests__/prompt.test.ts +++ b/src/main/ai/agents/__tests__/prompt.test.ts @@ -21,8 +21,7 @@ import { PromptBuilder } from '../prompt' const baseConfig: AgentConfiguration = { permission_mode: 'bypassPermissions', max_turns: 100, - env_vars: {}, - soul_enabled: true + env_vars: {} } const mockedStat = vi.mocked(stat) @@ -72,11 +71,31 @@ describe('PromptBuilder', () => { const result = await builder.buildSystemPrompt('/workspace') - expect(result).toContain('You are CherryClaw') - expect(result).toContain('## CherryClaw Tools') + expect(result).toContain('You are a personal assistant running inside Cherry Studio') + expect(result).toContain('## Autonomy Tools') expect(result).not.toContain('## Memories') }) + it('embeds tool guidance sections in order (autonomy, memory, web)', async () => { + setupFiles({}) + + const result = await builder.buildSystemPrompt('/workspace') + + const cherryIdx = result.indexOf('## Autonomy Tools') + const memoryIdx = result.indexOf('## Workspace Memory') + const webIdx = result.indexOf('## Web Search Strategy') + expect(cherryIdx).toBeGreaterThanOrEqual(0) + expect(cherryIdx).toBeLessThan(memoryIdx) + expect(memoryIdx).toBeLessThan(webIdx) + expect(result).toContain('mcp__cherry-tools__cron') + expect(result).not.toContain('mcp__skills__skills') + expect(result).toContain('mcp__agent-memory__memory') + expect(result).toContain('mcp__cherry-tools__web_search') + expect(result).toContain('mcp__cherry-tools__web_fetch') + // Guard against the removed Exa tool name leaking back into the guidance. + expect(result).not.toContain('mcp__exa__web_search_exa') + }) + it('overrides basic prompt with system.md from workspace', async () => { setupFiles({ '/workspace/system.md': 'You are CustomBot, a specialized assistant.' @@ -85,7 +104,7 @@ describe('PromptBuilder', () => { const result = await builder.buildSystemPrompt('/workspace') expect(result).toContain('You are CustomBot') - expect(result).not.toContain('You are CherryClaw') + expect(result).not.toContain('You are a personal assistant running inside Cherry Studio') }) it('includes soul.md in memories section', async () => { @@ -132,7 +151,7 @@ describe('PromptBuilder', () => { setupFiles({ '/workspace/soul.md': 'Be concise.', '/workspace/user.md': 'Name: V', - '/workspace/memory/FACT.md': 'Project: CherryClaw' + '/workspace/memory/FACT.md': 'Project: Cherry Studio' }) const result = await builder.buildSystemPrompt('/workspace') @@ -208,6 +227,16 @@ describe('PromptBuilder', () => { expect(result).toContain('## Bootstrap Mode') }) + it('runs bootstrap when reset (bootstrap_completed false) even if the agent has instructions', async () => { + // `config reset_bootstrap` sets bootstrap_completed=false and promises the next session onboards. + // That explicit reset must override the instruction-based skip, or the tool's promise is a lie. + setupFiles({}) + + const result = await builder.buildSystemPrompt('/workspace', { ...baseConfig, bootstrap_completed: false }, true) + + expect(result).toContain('## Bootstrap Mode') + }) + it('skips bootstrap when bootstrap_completed is true', async () => { setupFiles({}) @@ -216,6 +245,14 @@ describe('PromptBuilder', () => { expect(result).not.toContain('## Bootstrap Mode') }) + it('skips bootstrap when the agent already has non-blank user instructions', async () => { + setupFiles({}) + + const result = await builder.buildSystemPrompt('/workspace', baseConfig, true) + + expect(result).not.toContain('## Bootstrap Mode') + }) + it('skips bootstrap when SOUL.md has substantial content (legacy migration)', async () => { const realContent = 'I am a warm, direct assistant. I lead with answers and prefer concise communication. I respect boundaries and always ask before making assumptions.' @@ -253,83 +290,6 @@ describe('PromptBuilder', () => { }) }) - describe('buildToolGuidance', () => { - it('returns skills, memory, and web sections without claw by default', () => { - const result = builder.buildToolGuidance() - - expect(result).toContain('## Skills') - expect(result).toContain('mcp__skills__skills') - expect(result).toContain('## Workspace Memory') - expect(result).toContain('mcp__agent-memory__memory') - expect(result).toContain('## Web Search Strategy') - expect(result).toContain('mcp__cherry-tools__web_search') - expect(result).toContain('mcp__cherry-tools__web_fetch') - // Guard against the removed Exa tool name leaking back into the guidance. - expect(result).not.toContain('mcp__exa__web_search_exa') - expect(result).not.toContain('## CherryClaw Tools') - expect(result).not.toContain('mcp__claw__cron') - expect(result).not.toContain('mcp__claw__notify') - expect(result).not.toContain('mcp__claw__config') - }) - - it('includes claw section when hasClaw is true', () => { - const result = builder.buildToolGuidance({ hasClaw: true }) - - expect(result).toContain('## CherryClaw Tools') - expect(result).toContain('mcp__claw__cron') - expect(result).toContain('mcp__claw__notify') - expect(result).toContain('mcp__claw__config') - // Skills, memory, and web are still included - expect(result).toContain('mcp__skills__skills') - expect(result).toContain('mcp__agent-memory__memory') - expect(result).toContain('## Web Search Strategy') - }) - - it('places claw guidance before skills/memory when present', () => { - const result = builder.buildToolGuidance({ hasClaw: true }) - - const clawIdx = result.indexOf('## CherryClaw Tools') - const skillsIdx = result.indexOf('## Skills') - const memoryIdx = result.indexOf('## Workspace Memory') - const webIdx = result.indexOf('## Web Search Strategy') - - expect(clawIdx).toBeGreaterThanOrEqual(0) - expect(clawIdx).toBeLessThan(skillsIdx) - expect(skillsIdx).toBeLessThan(memoryIdx) - expect(memoryIdx).toBeLessThan(webIdx) - }) - - it('teaches when to act for skills (init/register and patching)', () => { - const result = builder.buildToolGuidance() - - expect(result).toMatch(/init.*register|register.*init/) - expect(result).toMatch(/edit.*in place|patch|outdated/i) - }) - - it('teaches when to act for memory (search-before-ask, FACT vs JOURNAL)', () => { - const result = builder.buildToolGuidance() - - expect(result).toMatch(/search.*before|before.*ask/i) - expect(result).toContain('FACT.md') - expect(result).toContain('JOURNAL') - expect(result).toMatch(/6 months|durable/i) - }) - - it('returns the same content soul-mode buildSystemPrompt embeds (with claw)', async () => { - setupFiles({}) - const soulPrompt = await builder.buildSystemPrompt('/workspace') - const guidance = builder.buildToolGuidance({ hasClaw: true }) - - // The Soul prompt should embed every section the with-claw guidance has. - expect(soulPrompt).toContain('## CherryClaw Tools') - expect(soulPrompt).toContain('## Skills') - expect(soulPrompt).toContain('## Workspace Memory') - expect(soulPrompt).toContain('## Web Search Strategy') - // And the guidance string is a contiguous substring of the soul prompt. - expect(soulPrompt).toContain(guidance) - }) - }) - describe('buildFactsSection', () => { it('returns undefined when no FACT.md exists', async () => { setupFiles({}) @@ -378,7 +338,7 @@ describe('PromptBuilder', () => { expect(result).toBeUndefined() }) - it('does not include SOUL.md or USER.md content (those are Soul-only)', async () => { + it('does not include SOUL.md or USER.md content (those are persona files)', async () => { setupFiles({ '/workspace/SOUL.md': 'Warm but direct.', '/workspace/user.md': 'Name: V', diff --git a/src/main/ai/agents/__tests__/runAgentTask.test.ts b/src/main/ai/agents/__tests__/runAgentTask.test.ts index c84245b11f6..2c7720314a0 100644 --- a/src/main/ai/agents/__tests__/runAgentTask.test.ts +++ b/src/main/ai/agents/__tests__/runAgentTask.test.ts @@ -58,7 +58,7 @@ vi.mock('@data/services/JobScheduleService', () => ({ vi.mock('@data/services/JobService', () => ({ jobService: { getById: vi.fn() } })) -vi.mock('@main/ai/agents/cherryclaw/heartbeat', () => ({ +vi.mock('@main/ai/agents/heartbeat', () => ({ readHeartbeat: vi.fn() })) @@ -68,7 +68,7 @@ import { agentSessionService } from '@data/services/AgentSessionService' import { agentWorkspaceService } from '@data/services/AgentWorkspaceService' import { jobScheduleService } from '@data/services/JobScheduleService' import { jobService } from '@data/services/JobService' -import { readHeartbeat } from '@main/ai/agents/cherryclaw/heartbeat' +import { readHeartbeat } from '@main/ai/agents/heartbeat' import { buildAgentSessionTopicId } from '@main/ai/agentSession/topic' import { runAgentTask } from '../runAgentTask' @@ -225,6 +225,21 @@ describe('runAgentTask', () => { expect(readHeartbeat).not.toHaveBeenCalled() }) + it('skips assistant heartbeat WITHOUT creating a session', async () => { + vi.mocked(jobService.getById).mockReturnValueOnce(makeJobSnapshot()) + vi.mocked(jobScheduleService.getById).mockReturnValueOnce(makeSchedule('heartbeat')) + vi.mocked(agentService.getAgent).mockReturnValueOnce( + makeAgent({ builtin_role: 'assistant', heartbeat_enabled: true }) + ) + + const out = await runAgentTask(makeCtx()) + + expect(out).toEqual({ sessionId: null, result: 'Skipped (assistant role)' }) + expect(agentSessionService.create).not.toHaveBeenCalled() + expect(agentWorkspaceService.getById).not.toHaveBeenCalled() + expect(readHeartbeat).not.toHaveBeenCalled() + }) + it('skips an enabled heartbeat with system workspace WITHOUT creating a session', async () => { vi.mocked(jobService.getById).mockReturnValueOnce(makeJobSnapshot()) vi.mocked(jobScheduleService.getById).mockReturnValueOnce(makeSchedule('heartbeat')) @@ -303,6 +318,9 @@ describe('runAgentTask', () => { name: 'heartbeat', workspace: { type: 'user', workspaceId: 'ws-1' } }) + // Scheduled runs have no interactive responder — the dispatch must be headless so AskUserQuestion + // stays disallowed and the run can't stall on an approval prompt. + expect(mockStartRun).toHaveBeenCalledWith(expect.objectContaining({ headless: true })) }) // Regular tasks carry the workspace bound at creation time (system by @@ -349,6 +367,37 @@ describe('runAgentTask', () => { expect(out).toEqual({ sessionId: 'sess-new', result: 'Hello world' }) }) + it('builds listeners only for subscribed channels owned by the task agent', async () => { + vi.mocked(jobService.getById).mockReturnValueOnce(makeJobSnapshot('s1')) + vi.mocked(jobScheduleService.getById).mockReturnValueOnce(makeSchedule('daily-summary')) + vi.mocked(agentService.getAgent).mockReturnValueOnce(makeAgent()) + vi.mocked(agentSessionService.create).mockReturnValueOnce(makeSession('/ws/a')) + vi.mocked(agentChannelService.getSubscribedChannels).mockReturnValueOnce([ + { id: 'ch-match', agentId: 'a1' }, + { id: 'ch-foreign', agentId: 'a2' } + ] as never) + + const adapter = { + channelId: 'ch-match', + connected: true, + notifyChatIds: ['chat-1'], + sendMessage: vi.fn(async () => {}), + onTextUpdate: vi.fn(async () => {}), + onStreamComplete: vi.fn(async () => true) + } + mockGetAdapter.mockReturnValue(adapter as never) + + const promise = runAgentTask(makeCtx({ input: { agentId: 'a1', prompt: 'hi', timeoutMinutes: 0 } })) + + await vi.waitFor(() => expect(mockStartRun).toHaveBeenCalled()) + captured.listeners[0].onDone({ status: 'completed' }) + await promise + + expect(mockGetAdapter).toHaveBeenCalledTimes(1) + expect(mockGetAdapter).toHaveBeenCalledWith('ch-match') + expect(captured.listeners).toHaveLength(2) + }) + // agents-jobs-4: on a non-abort error, a subscribed channel must be notified exactly // once. The channel listener's generic `Error: …` is suppressed for task runs so only // the richer `[Task failed]` summary from notifyTaskError is delivered (no double-send). @@ -357,7 +406,7 @@ describe('runAgentTask', () => { vi.mocked(jobScheduleService.getById).mockReturnValueOnce(makeSchedule('daily-summary')) vi.mocked(agentService.getAgent).mockReturnValueOnce(makeAgent()) vi.mocked(agentSessionService.create).mockReturnValueOnce(makeSession('/ws/a')) - vi.mocked(agentChannelService.getSubscribedChannels).mockReturnValueOnce([{ id: 'ch1' }] as never) + vi.mocked(agentChannelService.getSubscribedChannels).mockReturnValueOnce([{ id: 'ch1', agentId: 'a1' }] as never) const adapter = { channelId: 'ch1', diff --git a/src/main/ai/agents/bootstrap.ts b/src/main/ai/agents/bootstrap.ts new file mode 100644 index 00000000000..c1a58af8e04 --- /dev/null +++ b/src/main/ai/agents/bootstrap.ts @@ -0,0 +1,39 @@ +/** + * The bootstrap instruction is embedded as a constant (not written to disk). + * It is injected into the system prompt only when bootstrap detection decides + * the agent still needs first-run onboarding. + */ +export const BOOTSTRAP_INSTRUCTIONS = `## Bootstrap Mode + +You are starting a brand-new relationship with your user. Your SOUL.md and USER.md files may not exist yet, or may be empty templates waiting to be filled. + +Your goal in this conversation is to: + +1. **Introduce yourself** — Explain that you're their personal agent and this is a one-time setup conversation to figure out what role you should play for them. +2. **Discover the role** — Through natural conversation, understand what the user wants you to be: + - What kind of assistant do they need? (coding partner, project manager, research aide, creative collaborator, life assistant, etc.) + - What should your name be? Suggest options that fit the role, or let them choose freely. The name will appear in the app sidebar. + - What tone and personality fits this role? (professional, casual, playful, concise, thorough, etc.) + - Any boundaries, things you should never do, or strong preferences? +3. **Learn about the user** — Naturally weave in questions about: + - Their name and how they'd like to be addressed + - Their timezone and working hours + - Communication preferences (language, verbosity, formality) +4. **Commit the identity** — When you have enough information: + - Rename yourself using \`mcp__cherry-tools__config\` (action: "rename", name: the chosen name) + - Update \`SOUL.md\` with your role definition, personality, tone, principles, and boundaries. Use Write if the file is missing; use Edit if it already exists. + - Update \`USER.md\` with everything you learned about the user. Use Write if the file is missing; use Edit if it already exists. + - Log the bootstrap completion using \`mcp__agent-memory__memory\` (append action, tags: ["bootstrap"]) + - Mark bootstrap as complete using \`mcp__cherry-tools__config\` (action: "complete_bootstrap") + +Guidelines: +- Keep the conversation natural and warm — this is a first impression +- Ask no more than 3-5 questions total; don't interrogate +- It's okay to make reasonable assumptions and let the user correct you +- Write detailed, thoughtful content to SOUL.md and USER.md — these define your relationship +- Always respect the user's language preference — if they write in Chinese, respond in Chinese +- After marking bootstrap complete, future sessions will use your standard mode with the personality you defined +` + +/** Minimum character count for SOUL.md to be considered non-template (already configured). */ +export const SOUL_CONTENT_THRESHOLD = 50 diff --git a/src/main/ai/agents/cherryclaw/__tests__/seedWorkspace.test.ts b/src/main/ai/agents/cherryclaw/__tests__/seedWorkspace.test.ts deleted file mode 100644 index b2ec71b7fe1..00000000000 --- a/src/main/ai/agents/cherryclaw/__tests__/seedWorkspace.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' - -vi.mock('@logger', () => ({ - loggerService: { - withContext: () => ({ info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn() }) - } -})) - -vi.mock('node:fs/promises', () => ({ - mkdir: vi.fn(), - stat: vi.fn(), - writeFile: vi.fn() -})) - -import { mkdir, stat, writeFile } from 'node:fs/promises' - -import { seedWorkspaceTemplates } from '../seedWorkspace' - -const mockedMkdir = vi.mocked(mkdir) -const mockedStat = vi.mocked(stat) -const mockedWriteFile = vi.mocked(writeFile) - -describe('seedWorkspaceTemplates', () => { - beforeEach(() => { - vi.clearAllMocks() - mockedMkdir.mockResolvedValue(undefined) - mockedWriteFile.mockResolvedValue(undefined) - }) - - it('creates directories and seeds templates when files do not exist', async () => { - mockedStat.mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' })) - - await seedWorkspaceTemplates('/workspace') - - expect(mockedMkdir).toHaveBeenCalledWith('/workspace', { recursive: true }) - expect(mockedMkdir).toHaveBeenCalledWith('/workspace/memory', { recursive: true }) - - expect(mockedWriteFile).toHaveBeenCalledTimes(2) - const writeCalls = mockedWriteFile.mock.calls.map((c) => c[0]) - expect(writeCalls).toContain('/workspace/SOUL.md') - expect(writeCalls).toContain('/workspace/USER.md') - - // Verify template content - const soulCall = mockedWriteFile.mock.calls.find((c) => c[0] === '/workspace/SOUL.md') - expect(soulCall![1]).toContain('# Soul') - expect(soulCall![1]).toContain('## Personality') - - const userCall = mockedWriteFile.mock.calls.find((c) => c[0] === '/workspace/USER.md') - expect(userCall![1]).toContain('# User Profile') - expect(userCall![1]).toContain('## Name') - }) - - it('skips writing files that already exist (idempotent)', async () => { - mockedStat.mockResolvedValue({ mtimeMs: 1000 } as any) - - await seedWorkspaceTemplates('/workspace') - - expect(mockedWriteFile).not.toHaveBeenCalled() - }) - - it('writes only missing files', async () => { - mockedStat.mockImplementation(async (filePath) => { - const p = typeof filePath === 'string' ? filePath : filePath.toString() - if (p.includes('SOUL.md')) { - return { mtimeMs: 1000 } as any - } - throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' }) - }) - - await seedWorkspaceTemplates('/workspace') - - expect(mockedWriteFile).toHaveBeenCalledTimes(1) - expect(mockedWriteFile.mock.calls[0][0]).toBe('/workspace/USER.md') - }) -}) diff --git a/src/main/ai/agents/cherryclaw/seedWorkspace.ts b/src/main/ai/agents/cherryclaw/seedWorkspace.ts deleted file mode 100644 index 33c31bd28a3..00000000000 --- a/src/main/ai/agents/cherryclaw/seedWorkspace.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { mkdir, stat, writeFile } from 'node:fs/promises' -import path from 'node:path' - -import { loggerService } from '@logger' - -const logger = loggerService.withContext('SeedWorkspace') - -const SOUL_TEMPLATE = `# Soul - -> This file defines who you are. Update it as your personality evolves. - -## Personality - - -## Tone & Communication Style - - -## Core Principles - - -## Boundaries - -` - -const USER_TEMPLATE = `# User Profile - -> This file describes the user you serve. Update it as you learn more. - -## Name - - -## Preferences - - -## Timezone - - -## Context - -` - -/** - * The bootstrap instruction is embedded as a constant (not written to disk). - * It is injected into the system prompt when `bootstrap_completed` is falsy. - */ -export const BOOTSTRAP_INSTRUCTIONS = `## Bootstrap Mode - -You are starting a brand-new relationship with your user. Your SOUL.md and USER.md files are empty templates waiting to be filled. - -Your goal in this conversation is to: - -1. **Introduce yourself** — Explain that you're their personal CherryClaw agent and this is a one-time setup conversation to figure out what role you should play for them. -2. **Discover the role** — Through natural conversation, understand what the user wants you to be: - - What kind of assistant do they need? (coding partner, project manager, research aide, creative collaborator, life assistant, etc.) - - What should your name be? Suggest options that fit the role, or let them choose freely. The name will appear in the app sidebar. - - What tone and personality fits this role? (professional, casual, playful, concise, thorough, etc.) - - Any boundaries, things you should never do, or strong preferences? -3. **Learn about the user** — Naturally weave in questions about: - - Their name and how they'd like to be addressed - - Their timezone and working hours - - Communication preferences (language, verbosity, formality) -4. **Commit the identity** — When you have enough information: - - Rename yourself using \`mcp__claw__config\` (action: "rename", name: the chosen name) - - Update \`SOUL.md\` with your role definition, personality, tone, principles, and boundaries using the Edit tool - - Update \`USER.md\` with everything you learned about the user using the Edit tool - - Log the bootstrap completion using \`mcp__agent-memory__memory\` (append action, tag: "bootstrap") - - Mark bootstrap as complete using \`mcp__claw__config\` (action: "complete_bootstrap") - -Guidelines: -- Keep the conversation natural and warm — this is a first impression -- Ask no more than 3-5 questions total; don't interrogate -- It's okay to make reasonable assumptions and let the user correct you -- Write detailed, thoughtful content to SOUL.md and USER.md — these define your relationship -- Always respect the user's language preference — if they write in Chinese, respond in Chinese -- After marking bootstrap complete, future sessions will use your standard mode with the personality you defined -` - -/** Minimum character count for SOUL.md to be considered non-template (already configured). */ -export const SOUL_CONTENT_THRESHOLD = 50 - -/** - * Seed workspace with template files for soul mode. - * Idempotent: only writes files that don't already exist. - */ -export async function seedWorkspaceTemplates(workspacePath: string): Promise { - try { - // Ensure workspace and memory directories exist - await mkdir(workspacePath, { recursive: true }) - await mkdir(path.join(workspacePath, 'memory'), { recursive: true }) - - const seeds: Array<{ filePath: string; content: string }> = [ - { filePath: path.join(workspacePath, 'SOUL.md'), content: SOUL_TEMPLATE }, - { filePath: path.join(workspacePath, 'USER.md'), content: USER_TEMPLATE } - ] - - for (const { filePath, content } of seeds) { - const exists = await fileExists(filePath) - if (!exists) { - await writeFile(filePath, content, 'utf-8') - logger.info(`Seeded template: ${path.basename(filePath)}`, { path: filePath }) - } - } - } catch (error) { - logger.error('Failed to seed workspace templates', error as Error) - } -} - -async function fileExists(filePath: string): Promise { - try { - await stat(filePath) - return true - } catch { - return false - } -} diff --git a/src/main/ai/agents/cherryclaw/heartbeat.ts b/src/main/ai/agents/heartbeat.ts similarity index 100% rename from src/main/ai/agents/cherryclaw/heartbeat.ts rename to src/main/ai/agents/heartbeat.ts diff --git a/src/main/ai/agents/cherryclaw/prompt.ts b/src/main/ai/agents/prompt.ts similarity index 68% rename from src/main/ai/agents/cherryclaw/prompt.ts rename to src/main/ai/agents/prompt.ts index a6ee3a770c1..be8dd6e7d78 100644 --- a/src/main/ai/agents/cherryclaw/prompt.ts +++ b/src/main/ai/agents/prompt.ts @@ -4,7 +4,7 @@ import path from 'node:path' import { loggerService } from '@logger' import type { AgentConfiguration } from '@shared/data/types/agent' -import { BOOTSTRAP_INSTRUCTIONS, SOUL_CONTENT_THRESHOLD } from './seedWorkspace' +import { BOOTSTRAP_INSTRUCTIONS, SOUL_CONTENT_THRESHOLD } from './bootstrap' const logger = loggerService.withContext('PromptBuilder') @@ -36,19 +36,10 @@ type CacheEntry = { content: string } -const DEFAULT_BASIC_PROMPT = `You are CherryClaw, a personal assistant running inside CherryStudio. +const DEFAULT_BASIC_PROMPT = `You are a personal assistant running inside Cherry Studio. ` -const SKILLS_GUIDANCE = `## Skills - -You can manage Claude skills via the \`mcp__skills__skills\` tool — search the marketplace, install / remove existing skills, and author new ones via the \`init\` and \`register\` actions. Discovery and runtime activation of installed skills is handled automatically by the agent SDK; this tool is just the management surface. - -When to act: -- When the user asks for a capability you don't already have, search the marketplace before attempting the task from scratch — there is often an existing skill that fits. -- After completing a non-trivial task (5+ tool calls, an iterative fix, a workflow you'd want to repeat), offer to save the approach as a new skill via \`init\` + \`register\`. -- If you find an installed skill is outdated, incomplete, or wrong, fix it in place. Get the skill's \`path\` from \`mcp__skills__skills\` action="list" (or use the path returned by \`init\` if you just created it), then use the native Read / Edit tools on the files in that directory. The live symlink picks up file changes immediately, so no separate "patch" call is needed. Don't wait for the user to ask — patch immediately when you notice the issue.` - const MEMORY_GUIDANCE = `## Workspace Memory You have persistent memory in this agent's workspace via the \`mcp__agent-memory__memory\` tool: \`update\` rewrites \`memory/FACT.md\` (durable knowledge), \`append\` adds a timestamped entry to \`memory/JOURNAL.jsonl\` (one-off events), and \`search\` queries the journal. @@ -62,20 +53,20 @@ When to act: - Before writing to \`FACT.md\`, ask: will this still matter in 6 months? If not, append to the journal instead. - Never write to \`memory/FACT.md\` or \`memory/JOURNAL.jsonl\` via direct file tools — always go through the memory tool so writes stay atomic and searchable.` -const CLAW_GUIDANCE = `## CherryClaw Tools +const CHERRY_GUIDANCE = `## Autonomy Tools You have exclusive access to these tools for interacting with CherryStudio's autonomous features. Always prefer them over manual alternatives. | Tool | Purpose | When to use | |---|---|---| -| \`mcp__claw__cron\` | Schedule recurring or one-time tasks. Supports \`timeout_minutes\` param (default 2). | Creating reminders, periodic checks, scheduled reports. Never use builtin Cron* tools — they are disabled. | -| \`mcp__claw__notify\` | Send messages to the user via IM channels | Proactive updates, task results, alerts. Use when the user is not in the current session. | -| \`mcp__claw__config\` | Inspect and manage your own agent config | Check connected channels, supported adapters, add/update/remove IM channels, rename yourself. | +| \`mcp__cherry-tools__cron\` | Schedule recurring or one-time tasks. Supports \`timeout_minutes\` param (default 2). | Creating reminders, periodic checks, scheduled reports. Never use builtin Cron* tools — they are disabled. | +| \`mcp__cherry-tools__notify\` | Send messages to the user via IM channels | Proactive updates, task results, alerts. Use when the user is not in the current session. | +| \`mcp__cherry-tools__config\` | Inspect and manage your own agent config | Check connected channels, supported adapters, add/update/remove IM channels, rename yourself. | Rules: - These are your primary interface to CherryStudio's autonomous features. Do not attempt workarounds or alternative approaches. -- When creating scheduled tasks, always use \`mcp__claw__cron\`. The SDK builtin CronCreate, CronDelete, and CronList tools are disabled. -- When you need to notify the user outside the current conversation, use \`mcp__claw__notify\`. +- When creating scheduled tasks, always use \`mcp__cherry-tools__cron\`. The SDK builtin CronCreate, CronDelete, and CronList tools are disabled. +- When you need to notify the user outside the current conversation, use \`mcp__cherry-tools__notify\`. - When adding a WeChat channel, the config tool returns a QR code image. Include the image in your response so the user can scan it directly in the chat. - Use \`config status\` to check which channels are actually connected. If a channel shows \`connected: false\`, use \`config reconnect_channel\` to trigger a fresh QR scan.` @@ -90,19 +81,12 @@ You have two web tools: \`mcp__cherry-tools__web_search\` for structured search If the user explicitly needs browser automation (filling forms, clicking, navigating live pages), tell them this capability is not currently available rather than attempting a workaround.` /** - * Compose the tool-strategy guidance for an agent based on which MCP servers - * have actually been injected. The skills, memory, and web-tools sections are - * always present (those servers are injected for every agent); the claw - * section is only included for autonomous (Soul Mode) agents that get the - * cron / notify / config tools. + * Compose the tool-strategy guidance for an agent. Every section is always + * present — the autonomy (cron / notify / config), memory, and web-tools MCP + * servers are injected for every agent. */ -function composeToolGuidance(opts: { hasClaw: boolean }): string { - const parts: string[] = [] - if (opts.hasClaw) parts.push(CLAW_GUIDANCE) - parts.push(SKILLS_GUIDANCE) - parts.push(MEMORY_GUIDANCE) - parts.push(WEB_TOOLS_GUIDANCE) - return parts.join('\n\n') +function composeToolGuidance(): string { + return [CHERRY_GUIDANCE, MEMORY_GUIDANCE, WEB_TOOLS_GUIDANCE].join('\n\n') } function memoriesTemplate(workspacePath: string, sections: string): string { @@ -114,12 +98,13 @@ Persistent files in \`${workspacePath}/\` carry your state across sessions. Upda |---|---|---| | \`SOUL.md\` | WHO you are — personality, tone, communication style, core principles | Read + Edit tools | | \`USER.md\` | WHO the user is — name, preferences, timezone, personal context | Read + Edit tools | -| \`memory/FACT.md\` | WHAT you know — active projects, technical decisions, durable knowledge (6+ months) | Read + Edit tools | +| \`memory/FACT.md\` | WHAT you know — active projects, technical decisions, durable knowledge (6+ months) | Read inline + \`mcp__agent-memory__memory\` update action | | \`memory/JOURNAL.jsonl\` | WHEN things happened — one-time events, session notes (append-only log) | \`mcp__agent-memory__memory\` tool only (actions: append, search) | Rules: - Each file has an exclusive scope — never duplicate information across files. -- \`SOUL.md\`, \`USER.md\`, and \`memory/FACT.md\` are loaded below. Read and edit them directly when updates are needed. +- \`SOUL.md\` and \`USER.md\` are loaded below. Read and edit them directly when updates are needed. +- \`memory/FACT.md\` is loaded below for inline reading. Update it only through \`mcp__agent-memory__memory\` (action: update). - \`memory/JOURNAL.jsonl\` is NOT loaded into context. Use \`mcp__agent-memory__memory\` to append entries or search past events. Never read or write the file directly. - Filenames are case-insensitive. ${sections}` @@ -128,20 +113,12 @@ ${sections}` /** * PromptBuilder assembles the system prompt for CherryStudio agents. * - * Two entry points: + * {@link buildSystemPrompt} — full custom prompt that REPLACES the SDK preset + * entirely. Includes the basic identity, the full tool guidance (autonomy + + * memory + web), bootstrap instructions when needed, and the workspace memory + * files (SOUL.md / USER.md / FACT.md). * - * 1. {@link buildSystemPrompt} — full custom prompt for Soul Mode agents that - * REPLACES the SDK preset entirely. Includes the basic identity, the full - * tool guidance (claw + skills + memory + web), bootstrap instructions when - * needed, and the workspace memory files (SOUL.md / USER.md / FACT.md). - * - * 2. {@link buildToolGuidance} — lightweight tool-strategy suffix for - * non-Soul agents. Does not touch workspace files; intended to be APPENDED - * to the SDK's `claude_code` preset so the model gets cross-tool strategy - * guidance (skills + memory + web) on top of the standard Claude Code - * instructions. Returns a synchronous string — no I/O. - * - * Memory files layout (Soul Mode only): + * Memory files layout: * {workspace}/SOUL.md — personality, tone, communication style * {workspace}/USER.md — user profile, preferences, context * {workspace}/memory/FACT.md — durable project knowledge, technical decisions @@ -150,7 +127,11 @@ ${sections}` export class PromptBuilder { private cache = new Map() - async buildSystemPrompt(workspacePath: string, config?: AgentConfiguration): Promise { + async buildSystemPrompt( + workspacePath: string, + config?: AgentConfiguration, + hasUserInstructions = false + ): Promise { const parts: string[] = [] // Basic prompt: workspace system.md (case-insensitive) > embedded default @@ -158,11 +139,11 @@ export class PromptBuilder { const basicPrompt = systemPath ? await this.readCachedFile(systemPath) : undefined parts.push(basicPrompt ?? DEFAULT_BASIC_PROMPT) - // Tool guidance — Soul Mode gets the full set including claw (cron / notify / config) - parts.push(composeToolGuidance({ hasClaw: true })) + // Tool guidance — the full set including the autonomy tools (cron / notify / config) + parts.push(composeToolGuidance()) // Bootstrap detection: inject bootstrap instructions if not completed - const needsBootstrap = await this.shouldRunBootstrap(workspacePath, config) + const needsBootstrap = await this.shouldRunBootstrap(workspacePath, config, hasUserInstructions) if (needsBootstrap) { parts.push(BOOTSTRAP_INSTRUCTIONS) logger.info('Bootstrap mode active — injecting onboarding instructions') @@ -178,31 +159,18 @@ export class PromptBuilder { } /** - * Build the cross-tool strategy guidance string for a non-Soul agent. The - * returned text is meant to be APPENDED to the Claude Code SDK preset so - * the model gets explicit "when to use which tool" guidance on top of the - * SDK's built-in instructions. The skills + memory + web sections are - * always included (those MCP servers are injected for every agent); the - * claw section is excluded by default (non-Soul agents do not get cron / - * notify / config). - */ - buildToolGuidance(opts: { hasClaw?: boolean } = {}): string { - return composeToolGuidance({ hasClaw: opts.hasClaw ?? false }) - } - - /** - * Build a "## Workspace Knowledge" section for non-Soul agents that loads - * just the workspace's `memory/FACT.md` content. This is the recall side of + * Build a "## Workspace Knowledge" section that loads just the workspace's + * `memory/FACT.md` content. This is the recall side of * the cross-session learning loop — agents write durable knowledge to * FACT.md via \`mcp__agent-memory__memory\` action="update", and this method * loads it back into the system prompt at the start of the next session so * the agent remembers what it learned (e.g. parameter shapes that previously * failed, project conventions, user corrections). * - * Distinct from {@link buildSystemPrompt}'s memories section which is Soul - * Mode only and also includes the SOUL.md / USER.md persona files. Returns - * undefined when no FACT.md exists, so callers can omit the section - * entirely rather than emitting an empty wrapper. + * Distinct from {@link buildSystemPrompt}'s memories section which also + * includes the SOUL.md / USER.md persona files. Returns undefined when no + * FACT.md exists, so callers can omit the section entirely rather than + * emitting an empty wrapper. */ async buildFactsSection(workspacePath: string): Promise { const memoryDir = path.join(workspacePath, 'memory') @@ -224,13 +192,26 @@ ${content} /** * Determine whether bootstrap should run. * - If `bootstrap_completed` is explicitly true, skip. + * - If `bootstrap_completed` is explicitly false (via `config reset_bootstrap`), run — an explicit + * reset overrides the instruction-based skip so the tool's "next session will onboard" holds. + * - If the agent already has non-blank user instructions, skip. * - If SOUL.md has substantial non-template content, skip (legacy agent migration). * - Otherwise, run bootstrap. */ - private async shouldRunBootstrap(workspacePath: string, config?: AgentConfiguration): Promise { + private async shouldRunBootstrap( + workspacePath: string, + config?: AgentConfiguration, + hasUserInstructions = false + ): Promise { if (config?.bootstrap_completed === true) { return false } + if (config?.bootstrap_completed === false) { + return true + } + if (hasUserInstructions) { + return false + } // Legacy migration: if SOUL.md already has real content, treat as completed const soulPath = await resolveFile(workspacePath, 'SOUL.md') diff --git a/src/main/ai/agents/runAgentTask.ts b/src/main/ai/agents/runAgentTask.ts index f9fe7e5f1f6..8e8b93f0551 100644 --- a/src/main/ai/agents/runAgentTask.ts +++ b/src/main/ai/agents/runAgentTask.ts @@ -18,7 +18,7 @@ import { agentWorkspaceService } from '@data/services/AgentWorkspaceService' import { jobScheduleService } from '@data/services/JobScheduleService' import { jobService } from '@data/services/JobService' import { loggerService } from '@logger' -import { readHeartbeat } from '@main/ai/agents/cherryclaw/heartbeat' +import { readHeartbeat } from '@main/ai/agents/heartbeat' import { buildAgentSessionTopicId } from '@main/ai/agentSession/topic' import { ChannelAdapterListener, startAgentSessionRun, type StreamListener } from '@main/ai/streamManager' import type { JobContext } from '@main/core/job/types' @@ -90,6 +90,10 @@ export async function runAgentTask(ctx: JobContext): Promise): Promise channel.agentId === agentId) + : [] const channelManager = application.get('ChannelManager') const channelListeners: StreamListener[] = subscribedChannels.flatMap((ch) => { @@ -217,7 +224,8 @@ export async function runAgentTask(ctx: JobContext): Promise { expect.objectContaining({ sessionId: 'session-1', userParts: [{ type: 'text', text: '/compact' }], + // Channel-triggered runs have no interactive responder — headless keeps AskUserQuestion + // disallowed so the run can't stall on an approval prompt. + headless: true, listeners: expect.arrayContaining([ expect.objectContaining({ id: expect.stringContaining('channel-completion:') }) ]) @@ -520,7 +523,7 @@ describe('ChannelMessageHandler', () => { expect(adapter.sendMessage).toHaveBeenCalledWith( 'oc_123', - 'Current chat ID: `oc_123`\n\nAdd this value to `allow_ids` in settings to receive notifications.', + 'Current chat ID: `oc_123`\n\nAdd this value to `allowed_chat_ids` (or `allowed_channel_ids` for Discord) in settings to receive notifications.', { replyToMessageId: undefined } ) }) diff --git a/src/main/ai/mcp/servers/__tests__/claw.test.ts b/src/main/ai/mcp/servers/__tests__/cherryAutonomyTools.test.ts similarity index 83% rename from src/main/ai/mcp/servers/__tests__/claw.test.ts rename to src/main/ai/mcp/servers/__tests__/cherryAutonomyTools.test.ts index fc632d65c34..7accdf3a9a8 100644 --- a/src/main/ai/mcp/servers/__tests__/claw.test.ts +++ b/src/main/ai/mcp/servers/__tests__/cherryAutonomyTools.test.ts @@ -4,7 +4,7 @@ import path from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -// Mock TaskService before importing ClawServer +// Mock TaskService before importing CherryAutonomyTools const mockCreateTask = vi.fn() const mockListTasks = vi.fn() const mockDeleteTask = vi.fn() @@ -80,49 +80,35 @@ vi.mock('@main/services/MainWindowService', () => ({ } })) -const { default: ClawServer } = await import('../claw') -type ClawServerInstance = InstanceType +const { CherryAutonomyTools } = await import('../cherryAutonomyTools') +type CherryAutonomyToolsInstance = InstanceType const WORKSPACE_SOURCE = { type: 'system' as const } -const WORKSPACE_PATH = '/tmp/claw-test-workspace' +const WORKSPACE_PATH = '/tmp/cherry-test-workspace' function createServer(agentId = 'agent_test', workspacePath = WORKSPACE_PATH) { - return new ClawServer(agentId, WORKSPACE_SOURCE, workspacePath) + return new CherryAutonomyTools({ agentId, workspaceSource: WORKSPACE_SOURCE, workspacePath }) } -// Helper to call tools via the Server's request handlers -async function callTool(server: ClawServerInstance, args: Record, toolName = 'cron') { - // Use the server's internal handler by simulating a CallTool request - const handlers = (server.mcpServer.server as any)._requestHandlers - const callToolHandler = handlers?.get('tools/call') - if (!callToolHandler) { - throw new Error('No tools/call handler registered') - } - - return callToolHandler( - { method: 'tools/call', params: { name: toolName, arguments: args } }, - {} // extra - ) -} - -async function listTools(server: ClawServerInstance) { - const handlers = (server.mcpServer.server as any)._requestHandlers - const listHandler = handlers?.get('tools/list') - if (!listHandler) { - throw new Error('No tools/list handler registered') - } - return listHandler({ method: 'tools/list', params: {} }, {}) +// Helper mirroring how CherryBuiltinToolsServer's CallTool handler routes autonomy calls +// (returns `any` so assertions can poke content items without narrowing the SDK union). +async function callTool( + server: CherryAutonomyToolsInstance, + args: Record, + toolName = 'cron' +): Promise { + return server.call(toolName, args as Record) } -describe('ClawServer', () => { +describe('CherryAutonomyTools', () => { beforeEach(() => { vi.clearAllMocks() }) - it('should list all tools', async () => { + it('should list all tools', () => { const server = createServer() - const result = await listTools(server) - expect(result.tools).toHaveLength(3) - expect(result.tools.map((t: any) => t.name)).toEqual(['cron', 'notify', 'config']) + const tools = server.tools() + expect(tools).toHaveLength(3) + expect(tools.map((t) => t.name)).toEqual(['cron', 'notify', 'config']) }) describe('add action', () => { @@ -234,6 +220,39 @@ describe('ClawServer', () => { expect(result.isError).toBe(true) expect(mockCreateTask).not.toHaveBeenCalled() }) + + it('should subscribe explicit channel_ids owned by this agent', async () => { + mockGetChannel.mockReturnValue({ id: 'ch_own', agentId: 'agent_1' }) + mockCreateTask.mockResolvedValue({ id: 'task_ch' }) + + const server = createServer('agent_1') + await callTool(server, { + action: 'add', + name: 'test', + message: 'test', + cron: '* * * * *', + channel_ids: ['ch_own'] + }) + + expect(mockCreateTask).toHaveBeenCalledWith('agent_1', expect.objectContaining({ channelIds: ['ch_own'] })) + }) + + it('should reject channel_ids owned by another agent without leaking existence', async () => { + mockGetChannel.mockReturnValue({ id: 'ch_foreign', agentId: 'agent_2' }) + + const server = createServer('agent_1') + const result = await callTool(server, { + action: 'add', + name: 'test', + message: 'test', + cron: '* * * * *', + channel_ids: ['ch_foreign'] + }) + + expect(result.isError).toBe(true) + expect(result.content[0].text).toContain('Channel "ch_foreign" not found') + expect(mockCreateTask).not.toHaveBeenCalled() + }) }) describe('list action', () => { @@ -395,8 +414,8 @@ describe('ClawServer', () => { let outside: string beforeEach(async () => { - workspace = await mkdtemp(path.join(tmpdir(), 'claw-notify-')) - outside = await mkdtemp(path.join(tmpdir(), 'claw-outside-')) + workspace = await mkdtemp(path.join(tmpdir(), 'cherry-notify-')) + outside = await mkdtemp(path.join(tmpdir(), 'cherry-outside-')) }) afterEach(async () => { @@ -510,25 +529,25 @@ describe('ClawServer', () => { id: 'ch_1', type: 'telegram', name: 'My Telegram', + agentId: 'agent_1', isActive: true, config: { type: 'telegram', bot_token: 'tok_123', allowed_chat_ids: ['100'] } } const agentWithConfig = { id: 'agent_1', - name: 'CherryClaw', + name: 'Test Agent', model: 'claude-sonnet-4-20250514', configuration: { - soul_enabled: true, heartbeat_enabled: true } } const agentNoConfig = { id: 'agent_1', - name: 'CherryClaw', + name: 'Test Agent', model: 'claude-sonnet-4-20250514', - configuration: { soul_enabled: false } + configuration: {} } beforeEach(() => { @@ -562,7 +581,8 @@ describe('ClawServer', () => { 'discord', 'slack' ]) - expect(parsed.soul_enabled).toBe(true) + expect(parsed.soul_enabled).toBeUndefined() + expect(parsed.heartbeat_enabled).toBe(true) }) it('should return empty channels when none configured', async () => { @@ -728,6 +748,21 @@ describe('ClawServer', () => { expect(result.isError).toBe(true) expect(result.content[0].text).toContain('not found') }) + + it('should hide channels owned by another agent', async () => { + mockGetChannel.mockReturnValue({ ...telegramChannel, agentId: 'agent_2' }) + + const server = createServer('agent_1') + const result = await callTool( + server, + { action: 'update_channel', channel_id: 'ch_1', enabled: false }, + 'config' + ) + + expect(result.isError).toBe(true) + expect(result.content[0].text).toContain('Channel "ch_1" not found') + expect(mockUpdateChannel).not.toHaveBeenCalled() + }) }) describe('remove_channel action', () => { @@ -759,6 +794,58 @@ describe('ClawServer', () => { expect(result.isError).toBe(true) expect(result.content[0].text).toContain('not found') }) + + it('should hide channels owned by another agent', async () => { + mockGetChannel.mockReturnValue({ ...telegramChannel, agentId: 'agent_2' }) + + const server = createServer('agent_1') + const result = await callTool(server, { action: 'remove_channel', channel_id: 'ch_1' }, 'config') + + expect(result.isError).toBe(true) + expect(result.content[0].text).toContain('Channel "ch_1" not found') + expect(mockDeleteChannel).not.toHaveBeenCalled() + }) + }) + + describe('reconnect_channel action', () => { + it('should reconnect an existing non-QR channel', async () => { + mockGetChannel.mockReturnValue(telegramChannel) + + const server = createServer('agent_1') + const result = await callTool(server, { action: 'reconnect_channel', channel_id: 'ch_1' }, 'config') + + expect(result.content[0].text).toContain('reconnected') + expect(mockSyncChannel).toHaveBeenCalledWith('ch_1') + }) + + it('should error when channel_id is missing', async () => { + const server = createServer('agent_1') + const result = await callTool(server, { action: 'reconnect_channel' }, 'config') + + expect(result.isError).toBe(true) + expect(result.content[0].text).toContain("'channel_id' is required") + }) + + it('should error when channel not found', async () => { + mockGetChannel.mockReturnValue(null) + + const server = createServer('agent_1') + const result = await callTool(server, { action: 'reconnect_channel', channel_id: 'ch_999' }, 'config') + + expect(result.isError).toBe(true) + expect(result.content[0].text).toContain('not found') + }) + + it('should hide channels owned by another agent', async () => { + mockGetChannel.mockReturnValue({ ...telegramChannel, agentId: 'agent_2' }) + + const server = createServer('agent_1') + const result = await callTool(server, { action: 'reconnect_channel', channel_id: 'ch_1' }, 'config') + + expect(result.isError).toBe(true) + expect(result.content[0].text).toContain('Channel "ch_1" not found') + expect(mockSyncChannel).not.toHaveBeenCalled() + }) }) it('should handle unknown config action', async () => { diff --git a/src/main/ai/mcp/servers/__tests__/cherryBuiltinTools.test.ts b/src/main/ai/mcp/servers/__tests__/cherryBuiltinTools.test.ts index e96e8d67180..7c4837bd1ba 100644 --- a/src/main/ai/mcp/servers/__tests__/cherryBuiltinTools.test.ts +++ b/src/main/ai/mcp/servers/__tests__/cherryBuiltinTools.test.ts @@ -40,7 +40,9 @@ vi.mock('@application', () => ({ } })) -const { callCherryBuiltinTool, listCherryBuiltinTools } = await import('../cherryBuiltinTools') +const { callCherryBuiltinTool, listCherryBuiltinTools, CherryBuiltinToolsServer } = await import( + '../cherryBuiltinTools' +) const { WEB_LOOKUP_ERROR_NOTE } = await import('@main/ai/tools/webLookup') const signal = new AbortController().signal @@ -439,3 +441,21 @@ describe('cherryBuiltinTools', () => { expect(textOf(result)).toContain('Unknown tool') }) }) + +// The server hosts the stateless builtin tools plus the autonomy tools acting on the session's agent. +describe('CherryBuiltinToolsServer autonomy tool registration', () => { + const agentContext = { + agentId: 'agent_1', + workspaceSource: { type: 'system' as const }, + workspacePath: '/tmp/workspace' + } + + it('exposes the stateless tools plus cron/notify/config', async () => { + const server = new CherryBuiltinToolsServer(agentContext) + const handlers = (server.mcpServer.server as any)._requestHandlers + const result = await handlers.get('tools/list')({ method: 'tools/list', params: {} }, {}) + const names = result.tools.map((t: any) => t.name) + expect(names).toEqual(expect.arrayContaining(['cron', 'notify', 'config'])) + expect(names).toEqual(expect.arrayContaining(listCherryBuiltinTools().map((t) => t.name))) + }) +}) diff --git a/src/main/ai/mcp/servers/claw.ts b/src/main/ai/mcp/servers/cherryAutonomyTools.ts similarity index 83% rename from src/main/ai/mcp/servers/claw.ts rename to src/main/ai/mcp/servers/cherryAutonomyTools.ts index 66c80a85484..5b4c20e08e1 100644 --- a/src/main/ai/mcp/servers/claw.ts +++ b/src/main/ai/mcp/servers/cherryAutonomyTools.ts @@ -1,3 +1,13 @@ +/** + * Agent autonomy tools (cron / notify / config) hosted by the in-process + * `cherry-tools` MCP server (see `cherryBuiltinTools.ts`). + * + * Unlike the stateless builtin lookup tools, these act on behalf of a specific + * agent (schedule its tasks, notify through its channels, manage its own + * configuration), so they take the per-session agent context + * `CherryBuiltinToolsServer` is constructed with. + */ + import { application } from '@application' import { agentChannelService as channelService } from '@data/services/AgentChannelService' import { agentChannelWorkflowService } from '@data/services/AgentChannelWorkflowService' @@ -5,16 +15,24 @@ import { agentService } from '@data/services/AgentService' import { agentTaskService as taskService } from '@data/services/AgentTaskService' import { loggerService } from '@logger' import { type ChannelAdapter, resolveWorkspaceFile, sanitizeChannelOutput } from '@main/ai/channels' -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' -import type { Tool } from '@modelcontextprotocol/sdk/types.js' -import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from '@modelcontextprotocol/sdk/types.js' +import type { CallToolResult, Tool } from '@modelcontextprotocol/sdk/types.js' +import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js' +import { CONFIG_TOOL_NAME, CRON_TOOL_NAME, NOTIFY_TOOL_NAME } from '@shared/ai/builtinTools' import type { AgentConfiguration } from '@shared/data/api/schemas/agents' import type { AgentSessionWorkspaceSource } from '@shared/data/api/schemas/agentWorkspaces' import type { Trigger } from '@shared/data/api/schemas/jobs' import { type ChannelConfig, ChannelConfigSchema } from '@shared/data/types/channel' import QRCode from 'qrcode' -const logger = loggerService.withContext('McpServer:Claw') +const logger = loggerService.withContext('McpServer:CherryAutonomyTools') + +/** Per-session agent context the autonomy tools act on behalf of. */ +export interface CherryAgentContext { + agentId: string + workspaceSource: AgentSessionWorkspaceSource + workspacePath: string + sourceChannelId?: string +} /** * Parse a human-friendly duration string (e.g. '30m', '2h', '1h30m') into minutes. @@ -37,7 +55,7 @@ function parseDurationToMinutes(duration: string): number { } const CRON_TOOL: Tool = { - name: 'cron', + name: CRON_TOOL_NAME, description: "Manage scheduled tasks. Use action 'add' to create a recurring or one-time job, 'list' to see all jobs, or 'remove' to delete a job. For one-time jobs, use the 'at' field with an RFC3339 timestamp.", inputSchema: { @@ -73,7 +91,7 @@ const CRON_TOOL: Tool = { type: 'array', items: { type: 'string' }, description: - 'Channel IDs to send task results to. Omit to auto-bind all agent channels. Use an empty array [] to skip channel delivery.' + 'Channel IDs to send task results to. Omit to use the current source channel when invoked from a channel; otherwise no channel delivery is configured. Use an empty array [] to skip channel delivery.' }, timeout_minutes: { type: 'number', @@ -90,7 +108,7 @@ const CRON_TOOL: Tool = { } const NOTIFY_TOOL: Tool = { - name: 'notify', + name: NOTIFY_TOOL_NAME, description: 'Send a notification to the user through connected channels (e.g. Telegram). Provide a message, a file to forward from your workspace, or both. Use this to proactively deliver task results, status updates, or produced files. File support by channel: Telegram/Feishu forward any file; WeChat images only; Discord/Slack/QQ do not support files yet (a file_path to those returns an error).', inputSchema: { @@ -124,10 +142,10 @@ const CHANNEL_CONFIG_SCHEMAS: Record ({ - tools: [CRON_TOOL, NOTIFY_TOOL, CONFIG_TOOL] - })) + tools(): Tool[] { + return [...AUTONOMY_TOOLS] + } - this.mcpServer.server.setRequestHandler(CallToolRequestSchema, async (request) => { - const toolName = request.params.name - const args = (request.params.arguments ?? {}) as Record + handles(toolName: string): boolean { + return AUTONOMY_TOOLS.some((tool) => tool.name === toolName) + } - try { - switch (toolName) { - case 'cron': { - const action = args.action - switch (action) { - case 'add': - return await this.addJob(args) - case 'list': - return this.listJobs() - case 'remove': - return await this.removeJob(args) - default: - throw new McpError(ErrorCode.InvalidParams, `Unknown action "${action}", expected add/list/remove`) - } - } - case 'notify': - return await this.sendNotification(args) - case 'config': { - const action = args.action - switch (action) { - case 'status': - return this.configStatus() - case 'rename': - return this.configRename(args) - case 'add_channel': - return await this.configAddChannel(args) - case 'update_channel': - return await this.configUpdateChannel(args) - case 'remove_channel': - return await this.configRemoveChannel(args) - case 'reconnect_channel': - return await this.configReconnectChannel(args) - case 'complete_bootstrap': - return this.configCompleteBootstrap() - case 'reset_bootstrap': - return this.configResetBootstrap() - default: - throw new McpError( - ErrorCode.InvalidParams, - `Unknown action "${action}", expected status/rename/add_channel/update_channel/remove_channel/reconnect_channel/complete_bootstrap/reset_bootstrap` - ) - } + async call(toolName: string, args: Record): Promise { + try { + switch (toolName) { + case CRON_TOOL_NAME: { + const action = args.action + switch (action) { + case 'add': + return await this.addJob(args) + case 'list': + return this.listJobs() + case 'remove': + return await this.removeJob(args) + default: + throw new McpError(ErrorCode.InvalidParams, `Unknown action "${action}", expected add/list/remove`) } - default: - throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${toolName}`) } - } catch (error) { - const message = error instanceof Error ? error.message : String(error) - logger.error(`Tool error: ${toolName}`, { agentId: this.agentId, error: message }) - return { - content: [{ type: 'text' as const, text: `Error: ${message}` }], - isError: true + case NOTIFY_TOOL_NAME: + return await this.sendNotification(args) + case CONFIG_TOOL_NAME: { + const action = args.action + switch (action) { + case 'status': + return this.configStatus() + case 'rename': + return this.configRename(args) + case 'add_channel': + return await this.configAddChannel(args) + case 'update_channel': + return await this.configUpdateChannel(args) + case 'remove_channel': + return await this.configRemoveChannel(args) + case 'reconnect_channel': + return await this.configReconnectChannel(args) + case 'complete_bootstrap': + return this.configCompleteBootstrap() + case 'reset_bootstrap': + return this.configResetBootstrap() + default: + throw new McpError( + ErrorCode.InvalidParams, + `Unknown action "${action}", expected status/rename/add_channel/update_channel/remove_channel/reconnect_channel/complete_bootstrap/reset_bootstrap` + ) + } } + default: + throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${toolName}`) } - }) + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + logger.error(`Tool error: ${toolName}`, { agentId: this.agentId, error: message }) + return { + content: [{ type: 'text' as const, text: `Error: ${message}` }], + isError: true + } + } } private async addJob(args: Record) { @@ -344,9 +346,17 @@ class ClawServer { trigger = { kind: 'once', at: date.getTime() } } - // Resolve channel_ids: explicit array, or default to the current channel + // Resolve channel_ids: explicit array, or default to the current channel. Validate that each + // explicit id belongs to this agent — cron is auto-approved and injected for every agent, so an + // unscoped id would let one agent deliver task output into another agent's channel. Foreign (and + // missing) ids get the same "not found" as the config-tool guards to avoid leaking existence. let channelIds: string[] | undefined if (Array.isArray(rawChannelIds)) { + for (const channelId of rawChannelIds) { + const channel = channelService.getChannel(channelId) + if (!channel || channel.agentId !== this.agentId) + throw new McpError(ErrorCode.InvalidParams, `Channel "${channelId}" not found`) + } channelIds = rawChannelIds } else if (this.sourceChannelId) { channelIds = [this.sourceChannelId] @@ -507,7 +517,6 @@ class ClawServer { optional_fields: schema.optional })), channels: channelSummary, - soul_enabled: config?.soul_enabled ?? false, heartbeat_enabled: config?.heartbeat_enabled ?? false } @@ -647,6 +656,8 @@ class ClawServer { const existing = channelService.getChannel(channelId) if (!existing) throw new McpError(ErrorCode.InvalidParams, `Channel "${channelId}" not found`) + if (existing.agentId !== this.agentId) + throw new McpError(ErrorCode.InvalidParams, `Channel "${channelId}" not found`) const updates: Record = {} if (args.name !== undefined) updates.name = args.name as string @@ -669,6 +680,8 @@ class ClawServer { const channel = channelService.getChannel(channelId) if (!channel) throw new McpError(ErrorCode.InvalidParams, `Channel "${channelId}" not found`) + if (channel.agentId !== this.agentId) + throw new McpError(ErrorCode.InvalidParams, `Channel "${channelId}" not found`) await agentChannelWorkflowService.deleteChannel(channelId) @@ -684,6 +697,8 @@ class ClawServer { const channel = channelService.getChannel(channelId) if (!channel) throw new McpError(ErrorCode.InvalidParams, `Channel "${channelId}" not found`) + if (channel.agentId !== this.agentId) + throw new McpError(ErrorCode.InvalidParams, `Channel "${channelId}" not found`) const needsQr = channel.type === 'wechat' || (channel.type === 'feishu' && !channel.config.app_id) @@ -814,5 +829,3 @@ class ClawServer { } } } - -export default ClawServer diff --git a/src/main/ai/mcp/servers/cherryBuiltinTools.ts b/src/main/ai/mcp/servers/cherryBuiltinTools.ts index 21f6b76a129..32ffb1803ee 100644 --- a/src/main/ai/mcp/servers/cherryBuiltinTools.ts +++ b/src/main/ai/mcp/servers/cherryBuiltinTools.ts @@ -12,6 +12,10 @@ * knowledge selection — the agent sees all of the user's knowledge bases. The * destructive `kb_manage` tool relies on Claude Code's own per-call permission * prompt for approval (the AI-SDK path uses the tool's `needsApproval` instead). + * + * The server also hosts the agent autonomy tools (`…__cron`, `…__notify`, + * `…__config` — see `cherryAutonomyTools.ts`), which act on behalf of the + * session's agent via the {@link CherryAgentContext} passed at construction. */ import { loggerService } from '@logger' @@ -63,6 +67,10 @@ import { } from '@shared/ai/builtinTools' import * as z from 'zod' +import { type CherryAgentContext, CherryAutonomyTools } from './cherryAutonomyTools' + +export type { CherryAgentContext } + const logger = loggerService.withContext('McpServer:CherryBuiltinTools') type ToolModelOutput = { type: 'text'; value: string } | { type: 'json'; value: unknown } @@ -182,12 +190,19 @@ export async function callCherryBuiltinTool(name: string, args: unknown, signal: export class CherryBuiltinToolsServer { public mcpServer: McpServer - constructor() { + constructor(agentContext: CherryAgentContext) { + const autonomy = new CherryAutonomyTools(agentContext) this.mcpServer = new McpServer({ name: 'cherry-tools', version: '1.0.0' }, { capabilities: { tools: {} } }) - this.mcpServer.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: listCherryBuiltinTools() })) - this.mcpServer.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => - callCherryBuiltinTool(request.params.name, request.params.arguments, extra.signal) - ) + this.mcpServer.server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [...listCherryBuiltinTools(), ...autonomy.tools()] + })) + this.mcpServer.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => { + const { name } = request.params + if (autonomy.handles(name)) { + return autonomy.call(name, (request.params.arguments ?? {}) as Record) + } + return callCherryBuiltinTool(name, request.params.arguments, extra.signal) + }) } } diff --git a/src/main/ai/mcp/servers/skills.ts b/src/main/ai/mcp/servers/skills.ts index 9d5b0d409bc..c63b06db3d2 100644 --- a/src/main/ai/mcp/servers/skills.ts +++ b/src/main/ai/mcp/servers/skills.ts @@ -76,7 +76,7 @@ const SKILLS_TOOL: Tool = { } /** - * MCP server exposing skill management to any agent (not gated on Soul Mode). + * MCP server exposing skill management to any agent. * * Skills are a generally useful capability — searching the marketplace, * installing, listing, and authoring skills via init/register applies to diff --git a/src/main/ai/mcp/servers/workspaceMemory.ts b/src/main/ai/mcp/servers/workspaceMemory.ts index 6856e676e74..e6834c8040e 100644 --- a/src/main/ai/mcp/servers/workspaceMemory.ts +++ b/src/main/ai/mcp/servers/workspaceMemory.ts @@ -88,7 +88,7 @@ const MEMORY_TOOL: Tool = { } /** - * MCP server exposing cross-session memory to any agent (not gated on Soul Mode). + * MCP server exposing cross-session memory to any agent. * * Memory lives in the agent's workspace under `memory/` — `FACT.md` for * durable knowledge and `JOURNAL.jsonl` for timestamped events. Any agent diff --git a/src/main/ai/runtime/claudeCode/ClaudeCodeRuntimeDriver.ts b/src/main/ai/runtime/claudeCode/ClaudeCodeRuntimeDriver.ts index c38f409ded3..96256f1bfb2 100644 --- a/src/main/ai/runtime/claudeCode/ClaudeCodeRuntimeDriver.ts +++ b/src/main/ai/runtime/claudeCode/ClaudeCodeRuntimeDriver.ts @@ -149,7 +149,9 @@ class ClaudeCodeRuntimeConnection implements AgentRuntimeConnection { } async start(): Promise { - const request = await buildClaudeCodeQueryRequestForAgentSession(this.input.sessionId, this.resumeToken) + const request = await buildClaudeCodeQueryRequestForAgentSession(this.input.sessionId, this.resumeToken, { + headless: this.input.headless === true + }) if (!request) { throw new Error(`Unable to build Claude Code query options for agent session ${this.input.sessionId}`) } diff --git a/src/main/ai/runtime/claudeCode/__tests__/ClaudeCodeRuntimeDriver.test.ts b/src/main/ai/runtime/claudeCode/__tests__/ClaudeCodeRuntimeDriver.test.ts index 14d9d56a155..ecc7ca83fc5 100644 --- a/src/main/ai/runtime/claudeCode/__tests__/ClaudeCodeRuntimeDriver.test.ts +++ b/src/main/ai/runtime/claudeCode/__tests__/ClaudeCodeRuntimeDriver.test.ts @@ -148,7 +148,7 @@ describe('ClaudeCodeRuntimeDriver', () => { resumeToken: 'resume-1' }) - expect(mocks.buildRequest).toHaveBeenCalledWith('session-1', 'resume-1') + expect(mocks.buildRequest).toHaveBeenCalledWith('session-1', 'resume-1', { headless: false }) const sdkInput = mocks.createClaudeQuery.mock.calls[0][0].prompt const nextInput = sdkInput[Symbol.asyncIterator]().next() diff --git a/src/main/ai/runtime/claudeCode/__tests__/ClaudeCodeWarmQueryManager.test.ts b/src/main/ai/runtime/claudeCode/__tests__/ClaudeCodeWarmQueryManager.test.ts index 79cd62efb3d..b9badecc7c0 100644 --- a/src/main/ai/runtime/claudeCode/__tests__/ClaudeCodeWarmQueryManager.test.ts +++ b/src/main/ai/runtime/claudeCode/__tests__/ClaudeCodeWarmQueryManager.test.ts @@ -161,11 +161,11 @@ describe('ClaudeCodeWarmQueryManager', () => { const withInstance = createClaudeCodeWarmQuerySignature({ model: 'sonnet', - mcpServers: { claw: { type: 'sdk', name: 'claw', instance: fakeInstance } } + mcpServers: { cherry: { type: 'sdk', name: 'cherry', instance: fakeInstance } } } as any) const withoutInstance = createClaudeCodeWarmQuerySignature({ model: 'sonnet', - mcpServers: { claw: { type: 'sdk', name: 'claw' } } + mcpServers: { cherry: { type: 'sdk', name: 'cherry' } } } as any) expect(withInstance).toBe(withoutInstance) @@ -179,6 +179,6 @@ describe('ClaudeCodeWarmQueryManager', () => { mcpServers: { srv: { type: 'sdk', name, instance: { connect: vi.fn() } } } } as any) - expect(withInstance('claw')).not.toBe(withInstance('assistant')) + expect(withInstance('cherry')).not.toBe(withInstance('assistant')) }) }) diff --git a/src/main/ai/runtime/claudeCode/__tests__/buildMcpServers.test.ts b/src/main/ai/runtime/claudeCode/__tests__/buildMcpServers.test.ts index 8372eb8ee0b..4cc27f03087 100644 --- a/src/main/ai/runtime/claudeCode/__tests__/buildMcpServers.test.ts +++ b/src/main/ai/runtime/claudeCode/__tests__/buildMcpServers.test.ts @@ -1,7 +1,7 @@ /** - * Regression for agents-jobs-3: the CherryClaw prompt/bootstrap drive memory via - * `mcp__agent-memory__memory`, so Soul Mode must actually inject the `agent-memory` - * server into the runtime MCP list AND allow its tools — not just reference the name. + * Regression for agents-jobs-3: the agent prompt/bootstrap drive memory via + * `mcp__agent-memory__memory`, so every agent must actually get the `agent-memory` + * server injected into the runtime MCP list AND allow its tools — not just reference the name. */ import type * as NodeFs from 'node:fs' @@ -99,13 +99,15 @@ function makeSession(path: string, type: 'user' | 'system' = 'user'): AgentSessi } describe('adjustAllowedToolsForMcp', () => { - it('lists read-only cherry-tools + claw + agent-memory in Soul Mode, excluding the mutating kb_manage', () => { - const allowed = adjustAllowedToolsForMcp(true, false) + it('lists auto-approved cherry-tools + agent-memory for every agent, excluding the mutating kb_manage', () => { + const allowed = adjustAllowedToolsForMcp(false) expect(allowed).toEqual( expect.arrayContaining([ 'mcp__cherry-tools__kb_search', 'mcp__cherry-tools__kb_list', - 'mcp__claw__*', + 'mcp__cherry-tools__cron', + 'mcp__cherry-tools__notify', + 'mcp__cherry-tools__config', 'mcp__agent-memory__*' ]) ) @@ -115,34 +117,26 @@ describe('adjustAllowedToolsForMcp', () => { expect(allowed).not.toContain('mcp__cherry-tools__*') }) - it('lists read-only cherry-tools + assistant for the Cherry Assistant, excluding kb_manage', () => { - const allowed = adjustAllowedToolsForMcp(false, true) + it('additionally lists assistant tools for the Cherry Assistant, excluding kb_manage', () => { + const allowed = adjustAllowedToolsForMcp(true) expect(allowed).toEqual( expect.arrayContaining(['mcp__cherry-tools__kb_search', 'mcp__cherry-tools__kb_list', 'mcp__assistant__*']) ) expect(allowed).not.toContain('mcp__cherry-tools__kb_manage') expect(allowed).not.toContain('mcp__cherry-tools__*') }) - - it('leaves the allowlist undefined for a plain agent (all tools permitted)', () => { - expect(adjustAllowedToolsForMcp(false, false)).toBeUndefined() - }) }) describe('buildMcpServers', () => { - it('injects the agent-memory server in Soul Mode (REGRESSION agents-jobs-3)', async () => { - const result = buildMcpServers(session, agent, true, false) - expect(Object.keys(result ?? {})).toEqual(expect.arrayContaining(['claw', 'agent-memory'])) - }) - - it('does not inject agent-memory when Soul Mode is off', async () => { - const result = buildMcpServers(session, agent, false, false) - expect(result?.['agent-memory']).toBeUndefined() + it('injects the agent-memory server for every agent (REGRESSION agents-jobs-3)', async () => { + const result = buildMcpServers(session, agent, false) + expect(Object.keys(result ?? {})).toEqual(expect.arrayContaining(['cherry-tools', 'agent-memory'])) }) - it('injects cherry-tools for every session and no longer injects exa', async () => { - const result = buildMcpServers(session, agent, false, false) + it('injects cherry-tools for every session; the standalone cherry server and exa are gone', async () => { + const result = buildMcpServers(session, agent, false) expect(result?.['cherry-tools']).toBeDefined() + expect(result?.cherry).toBeUndefined() expect(result?.exa).toBeUndefined() }) }) diff --git a/src/main/ai/runtime/claudeCode/__tests__/buildSystemPrompt.test.ts b/src/main/ai/runtime/claudeCode/__tests__/buildSystemPrompt.test.ts index 34f3b40ed5a..0585189c3cf 100644 --- a/src/main/ai/runtime/claudeCode/__tests__/buildSystemPrompt.test.ts +++ b/src/main/ai/runtime/claudeCode/__tests__/buildSystemPrompt.test.ts @@ -54,7 +54,7 @@ vi.mock('@main/ai/agents/builtin/BuiltinAgentProvisioner', () => ({ provisionBuiltinAgent: vi.fn() })) -vi.mock('@main/ai/agents/cherryclaw/prompt', () => ({ +vi.mock('@main/ai/agents/prompt', () => ({ PromptBuilder: vi.fn(() => ({ buildSystemPrompt: vi.fn().mockResolvedValue('SOUL_PROMPT') })) })) @@ -76,18 +76,20 @@ describe('buildSystemPrompt — report_artifacts prompt', () => { mockFindBySessionId.mockResolvedValue(null) }) - it('appends the report_artifacts prompt in standard mode with user instructions', async () => { + it('appends the report_artifacts prompt with user instructions (raw-string path)', async () => { const result = await buildSystemPrompt(makeSession(), makeAgent({ instructions: 'Do the task.' }), '/tmp/cwd') - expect(result).toMatchObject({ type: 'preset', preset: 'claude_code' }) - const append = (result as { append: string }).append - expect(append).toContain('Do the task.') - expect(append).toContain(ARTIFACTS_MARKER) + // Every agent returns a raw string (not a `{ type: 'preset', append }` object) that carries the + // soul prompt + user instructions + the artifacts block. + expect(typeof result).toBe('string') + expect(result as string).toContain('SOUL_PROMPT') + expect(result as string).toContain('Do the task.') + expect(result as string).toContain(ARTIFACTS_MARKER) }) - it('appends the report_artifacts prompt in standard mode without user instructions', async () => { + it('appends the report_artifacts prompt without user instructions', async () => { const result = await buildSystemPrompt(makeSession(), makeAgent(), '/tmp/cwd') - const append = (result as { append: string }).append - expect(append).toContain(ARTIFACTS_MARKER) + expect(typeof result).toBe('string') + expect(result as string).toContain(ARTIFACTS_MARKER) }) it('does not append it for the Cherry Assistant (parity with feat/chat-page)', async () => { @@ -98,17 +100,6 @@ describe('buildSystemPrompt — report_artifacts prompt', () => { const result = await buildSystemPrompt(makeSession(), agent, '/tmp/cwd') expect(JSON.stringify(result)).not.toContain(ARTIFACTS_MARKER) }) - - it('appends the report_artifacts prompt in soul mode (raw-string path)', async () => { - const agent = makeAgent({ instructions: 'Soul task.', configuration: { soul_enabled: true } as never }) - const result = await buildSystemPrompt(makeSession(), agent, '/tmp/cwd') - // Soul mode returns a raw string (not the standard `{ type: 'preset', append }` object), so it's a - // distinct path that must still carry the soul prompt + user instructions + the artifacts block. - expect(typeof result).toBe('string') - expect(result as string).toContain('SOUL_PROMPT') - expect(result as string).toContain('Soul task.') - expect(result as string).toContain(ARTIFACTS_MARKER) - }) }) describe('buildSystemPrompt — bundled-runtime guidance', () => { @@ -116,24 +107,16 @@ describe('buildSystemPrompt — bundled-runtime guidance', () => { mockFindBySessionId.mockResolvedValue(null) }) - it('steers the agent to bun/uv in standard mode with user instructions', async () => { + it('steers the agent to bun/uv with user instructions', async () => { const result = await buildSystemPrompt(makeSession(), makeAgent({ instructions: 'Do the task.' }), '/tmp/cwd') - const append = (result as { append: string }).append - expect(append).toContain(RUNTIME_MARKER) + expect(result as string).toContain(RUNTIME_MARKER) // The model is told to use bun / uv explicitly, not node/npm/pip. - expect(append).toContain('bun') - expect(append).toContain('uv run python') + expect(result as string).toContain('bun') + expect(result as string).toContain('uv run python') }) - it('steers the agent to bun/uv in standard mode without user instructions', async () => { + it('steers the agent to bun/uv without user instructions', async () => { const result = await buildSystemPrompt(makeSession(), makeAgent(), '/tmp/cwd') - const append = (result as { append: string }).append - expect(append).toContain(RUNTIME_MARKER) - }) - - it('steers the agent to bun/uv in soul mode', async () => { - const agent = makeAgent({ instructions: 'Soul task.', configuration: { soul_enabled: true } as never }) - const result = await buildSystemPrompt(makeSession(), agent, '/tmp/cwd') expect(result as string).toContain(RUNTIME_MARKER) }) diff --git a/src/main/ai/runtime/claudeCode/__tests__/settingsBuilder.test.ts b/src/main/ai/runtime/claudeCode/__tests__/settingsBuilder.test.ts index d3cf0e1486b..6dacb2b5672 100644 --- a/src/main/ai/runtime/claudeCode/__tests__/settingsBuilder.test.ts +++ b/src/main/ai/runtime/claudeCode/__tests__/settingsBuilder.test.ts @@ -15,7 +15,6 @@ const mocks = vi.hoisted(() => ({ modelGetByKey: vi.fn(), findBySessionId: vi.fn(), createToolPolicySnapshot: vi.fn(), - listChannels: vi.fn(), applicationGet: vi.fn(), applicationGetPath: vi.fn(), getShellEnv: vi.fn(), @@ -55,8 +54,7 @@ vi.mock('@data/services/AgentService', () => ({ vi.mock('@data/services/AgentChannelService', () => ({ agentChannelService: { - findBySessionId: mocks.findBySessionId, - listChannels: mocks.listChannels + findBySessionId: mocks.findBySessionId } })) @@ -84,7 +82,7 @@ vi.mock('@main/ai/agents/builtin/BuiltinAgentProvisioner', () => ({ provisionBuiltinAgent: vi.fn() })) -vi.mock('@main/ai/agents/cherryclaw/prompt', () => ({ +vi.mock('@main/ai/agents/prompt', () => ({ PromptBuilder: vi.fn(() => ({ buildSystemPrompt: vi.fn(async () => 'soul prompt') })) })) @@ -92,10 +90,6 @@ vi.mock('@main/ai/mcp/servers/assistant', () => ({ default: vi.fn(() => ({ mcpServer: {} })) })) -vi.mock('@main/ai/mcp/servers/claw', () => ({ - default: vi.fn(() => ({ mcpServer: {} })) -})) - vi.mock('@main/ai/runtime/claudeCode/createSdkMcpServerInstance', () => ({ createSdkMcpServerInstance: vi.fn() })) @@ -164,7 +158,19 @@ vi.mock('../ToolApprovalRegistry', () => ({ } })) -const { buildClaudeCodeSessionSettings, disposeToolPolicySnapshot } = await import('../settingsBuilder') +const { buildClaudeCodeSessionSettings, disposeToolPolicySnapshot, redactProxyUrlForAssistantContext } = await import( + '../settingsBuilder' +) + +describe('redactProxyUrlForAssistantContext', () => { + it('redacts proxy credentials while keeping the host and port visible', () => { + expect(redactProxyUrlForAssistantContext('http://user:pass@proxy.example:8080')).toBe('http://proxy.example:8080/') + }) + + it('leaves plain proxy URLs unchanged', () => { + expect(redactProxyUrlForAssistantContext('http://proxy.example:8080')).toBe('http://proxy.example:8080') + }) +}) describe('buildClaudeCodeSessionSettings', () => { beforeEach(() => { @@ -195,7 +201,6 @@ describe('buildClaudeCodeSessionSettings', () => { update: vi.fn(), setPermissionMode: vi.fn() }) - mocks.listChannels.mockReturnValue([]) mocks.applicationGet.mockImplementation((name: string) => { if (name === 'PreferenceService') { return { get: vi.fn(() => undefined) } @@ -364,10 +369,11 @@ describe('buildClaudeCodeSessionSettings', () => { const settings = await buildClaudeCodeSessionSettings(session as never, {} as never) expect(settings.disallowedTools).toEqual(expect.arrayContaining(['Bash', 'Read'])) - expect(settings.allowedTools).toBeUndefined() + // The injected cherry-tools/agent-memory servers are always pre-approved via the allowlist. + expect(settings.allowedTools).toEqual(expect.arrayContaining(['mcp__cherry-tools__cron', 'mcp__agent-memory__*'])) }) - it('composes disallowedTools: globals + EnterWorktree (no .git cwd) + dedup, no AskUserQuestion for a plain agent', async () => { + it('composes disallowedTools: globals + EnterWorktree (no .git cwd) + dedup', async () => { mocks.getAgent.mockReturnValue({ id: 'agent-1', type: 'claude-code', @@ -388,13 +394,11 @@ describe('buildClaudeCodeSessionSettings', () => { // GLOBALLY_DISALLOWED_TOOLS always blocked; EnterWorktree blocked because the cwd has no .git. expect(disallowed).toEqual(expect.arrayContaining(['WebSearch', 'WebFetch', 'EnterWorktree'])) - // A plain (non-assistant, non-soul) agent does not block AskUserQuestion. - expect(disallowed).not.toContain('AskUserQuestion') // The `new Set` dedup holds — no entry appears twice even when registry + globals overlap. expect(new Set(disallowed).size).toBe(disallowed.length) }) - it('soul mode adds SOUL_MODE_DISALLOWED_TOOLS to disallowedTools', async () => { + it('leaves interactive tools available for plain agents (only registry-disabled tools blocked)', async () => { mocks.getAgent.mockReturnValue({ id: 'agent-1', type: 'claude-code', @@ -402,7 +406,7 @@ describe('buildClaudeCodeSessionSettings', () => { mcps: [], allowedTools: [], disabledTools: [], - configuration: { soul_enabled: true } + configuration: {} }) const session = { id: 'session-1', @@ -413,12 +417,97 @@ describe('buildClaudeCodeSessionSettings', () => { const settings = await buildClaudeCodeSessionSettings(session as never, {} as never) const disallowed = settings.disallowedTools ?? [] - expect(disallowed).toEqual( - expect.arrayContaining(['CronCreate', 'EnterPlanMode', 'AskUserQuestion', 'NotebookEdit']) - ) + // Interactive tools are no longer blanket-disabled now that the soul-mode overlay is gone. + expect(disallowed).not.toEqual(expect.arrayContaining(['AskUserQuestion'])) + expect(disallowed).not.toEqual(expect.arrayContaining(['EnterPlanMode'])) + // Tools classified `disabled` in the declarative registry stay blocked. + expect(disallowed).toEqual(expect.arrayContaining(['CronCreate', 'NotebookEdit', 'TodoWrite'])) expect(new Set(disallowed).size).toBe(disallowed.length) }) + it('adds AskUserQuestion to disallowedTools for explicitly headless sessions', async () => { + const session = { + id: 'session-1', + agentId: 'agent-1', + workspace: { type: 'user', path: '/workspace/project' } + } + + const settings = await buildClaudeCodeSessionSettings(session as never, {} as never, { headless: true }) + + expect(settings.disallowedTools ?? []).toContain('AskUserQuestion') + }) + + it('denies AskUserQuestion at tool fire time for the current headless turn', async () => { + const isCurrentTurnHeadless = vi.fn(() => true) + mocks.applicationGet.mockImplementation((name: string) => { + if (name === 'PreferenceService') return { get: vi.fn(() => undefined) } + if (name === 'McpCatalogService') return { listTools: vi.fn(async () => []) } + if (name === 'AgentSessionRuntimeService') return { isCurrentTurnHeadless } + throw new Error(`Unexpected application.get(${name})`) + }) + const session = { + id: 'session-1', + agentId: 'agent-1', + workspace: { type: 'user', path: '/workspace/project' } + } + + const settings = await buildClaudeCodeSessionSettings(session as never, {} as never) + const result = await settings.canUseTool?.('AskUserQuestion', {}, { + signal: { aborted: false }, + toolUseID: 'tool-use-1' + } as never) + + expect(isCurrentTurnHeadless).toHaveBeenCalledWith('session-1') + expect(result).toEqual({ + behavior: 'deny', + message: + 'This channel or scheduled turn has no interactive responder, so proceed without asking the user and state your assumptions instead.' + }) + }) + + it('does not deny AskUserQuestion at tool fire time for the current interactive turn', async () => { + const isCurrentTurnHeadless = vi.fn(() => false) + mocks.applicationGet.mockImplementation((name: string) => { + if (name === 'PreferenceService') return { get: vi.fn(() => undefined) } + if (name === 'McpCatalogService') return { listTools: vi.fn(async () => []) } + if (name === 'AgentSessionRuntimeService') return { isCurrentTurnHeadless } + throw new Error(`Unexpected application.get(${name})`) + }) + mocks.createToolPolicySnapshot.mockResolvedValue({ + resolve: vi.fn(() => ({ approval: 'auto' })), + isDisabled: vi.fn(() => false), + update: vi.fn(), + setPermissionMode: vi.fn() + }) + const session = { + id: 'session-1', + agentId: 'agent-1', + workspace: { type: 'user', path: '/workspace/project' } + } + + const settings = await buildClaudeCodeSessionSettings(session as never, {} as never) + const result = await settings.canUseTool?.('AskUserQuestion', { prompt: 'Need input?' }, { + signal: { aborted: false }, + toolUseID: 'tool-use-1' + } as never) + + expect(isCurrentTurnHeadless).toHaveBeenCalledWith('session-1') + expect(result).toEqual({ behavior: 'allow', updatedInput: { prompt: 'Need input?' } }) + }) + + it('keeps AskUserQuestion available for channel-linked interactive sessions', async () => { + mocks.findBySessionId.mockReturnValue({ id: 'channel-1', sessionId: 'session-1' }) + const session = { + id: 'session-1', + agentId: 'agent-1', + workspace: { type: 'user', path: '/workspace/project' } + } + + const settings = await buildClaudeCodeSessionSettings(session as never, {} as never) + + expect(settings.disallowedTools ?? []).not.toContain('AskUserQuestion') + }) + it('assistant role adds AskUserQuestion to disallowedTools', async () => { mocks.getAgent.mockReturnValue({ id: 'agent-1', @@ -524,25 +613,6 @@ describe('buildClaudeCodeSessionSettings', () => { ) }) - it('warns and falls back to no channels when channel lookup fails during tool-policy build', async () => { - const session = { - id: 'session-1', - agentId: 'agent-1', - workspace: { type: 'user', path: '/workspace/project' } - } - mocks.listChannels.mockImplementationOnce(() => { - throw new Error('channel db down') - }) - - const settings = await buildClaudeCodeSessionSettings(session as never, {} as never) - - expect(settings.cwd).toBe('/workspace/project') - expect(mocks.loggerWarn).toHaveBeenCalledWith('Failed to list channels for tool policy context', { - agentId: 'agent-1', - error: 'channel db down' - }) - }) - // Warm-pool correctness: hooks baked at prewarm must resolve session state by id at fire-time, so // a warm-hit connection's live updates (snapshot refresh / re-bound emitter / new steer holder) // reach the running subprocess instead of a stale per-build instance. diff --git a/src/main/ai/runtime/claudeCode/agentSessionWarmup.ts b/src/main/ai/runtime/claudeCode/agentSessionWarmup.ts index a9abccc4852..fc99ffa0804 100644 --- a/src/main/ai/runtime/claudeCode/agentSessionWarmup.ts +++ b/src/main/ai/runtime/claudeCode/agentSessionWarmup.ts @@ -46,7 +46,8 @@ interface ClaudeCodeRuntimeRoute { export async function buildClaudeCodeQueryRequestForAgentSession( sessionId: string, - effectiveResume?: string + effectiveResume?: string, + sessionOptions: { headless?: boolean } = {} ): Promise { const session = agentSessionService.getById(sessionId) if (!session?.agentId) return undefined @@ -63,7 +64,10 @@ export async function buildClaudeCodeQueryRequestForAgentSession( const resumeSessionId = effectiveResume ?? agentSessionMessageService.getLastRuntimeResumeToken(session.id) ?? undefined const settings = mergeRuntimeSettings( - await buildClaudeCodeSessionSettings(session, provider, { lastAgentSessionId: resumeSessionId }), + await buildClaudeCodeSessionSettings(session, provider, { + lastAgentSessionId: resumeSessionId, + headless: sessionOptions.headless === true + }), route ) const sdkModelId = route.modelIds.primary diff --git a/src/main/ai/runtime/claudeCode/settingsBuilder.ts b/src/main/ai/runtime/claudeCode/settingsBuilder.ts index ee9eabf9d04..b339d64d634 100644 --- a/src/main/ai/runtime/claudeCode/settingsBuilder.ts +++ b/src/main/ai/runtime/claudeCode/settingsBuilder.ts @@ -30,10 +30,9 @@ import { modelService } from '@data/services/ModelService' import { providerService } from '@data/services/ProviderService' import { loggerService } from '@logger' import { isProvisioned, provisionBuiltinAgent } from '@main/ai/agents/builtin/BuiltinAgentProvisioner' -import { PromptBuilder } from '@main/ai/agents/cherryclaw/prompt' +import { PromptBuilder } from '@main/ai/agents/prompt' import AssistantServer from '@main/ai/mcp/servers/assistant' import CherryBuiltinToolsServer from '@main/ai/mcp/servers/cherryBuiltinTools' -import ClawServer from '@main/ai/mcp/servers/claw' import WorkspaceMemoryServer from '@main/ai/mcp/servers/workspaceMemory' import { createSdkMcpServerInstance } from '@main/ai/runtime/claudeCode/createSdkMcpServerInstance' import { skillService } from '@main/ai/skills/SkillService' @@ -54,11 +53,7 @@ import { autoDiscoverGitBash } from '@main/utils/commandResolver' import { getPathStatus, type PathStatus } from '@main/utils/file' import { rtkRewrite } from '@main/utils/rtk' import { getShellEnv } from '@main/utils/shellEnv' -import { - CHANNEL_SECURITY_PROMPT, - REPORT_ARTIFACTS_PROMPT, - SOUL_MODE_DISALLOWED_TOOLS -} from '@shared/ai/claudecode/constants' +import { CHANNEL_SECURITY_PROMPT, REPORT_ARTIFACTS_PROMPT } from '@shared/ai/claudecode/constants' import { toCamelCase } from '@shared/ai/tools/mcpToolName' import type { AgentEntity } from '@shared/data/api/schemas/agents' import type { AgentSessionEntity } from '@shared/data/api/schemas/agentSessions' @@ -172,6 +167,19 @@ function extractSteerText(input: AgentRuntimeUserInput): string { ) } +export function redactProxyUrlForAssistantContext(proxyUrl: string): string { + try { + const url = new URL(proxyUrl) + if (!url.username && !url.password) return proxyUrl + // Drop proxy userinfo before this value enters the assistant system prompt. + url.username = '' + url.password = '' + return url.toString() + } catch { + return proxyUrl.replace(/^([a-z][a-z\d+.-]*:\/\/)[^/@\s]+@/i, '$1') + } +} + /** * Build a lightweight environment snapshot (~200 tokens) for Cherry Assistant. * Injected into system prompt so the agent knows the user's setup immediately. @@ -202,7 +210,7 @@ async function buildAssistantContext(): Promise { `- App: Cherry Studio v${appVersion}`, `- OS: ${platform}`, `- Language: ${language}, Theme: ${theme}`, - proxy ? `- Proxy: ${proxy}` : '- Proxy: none', + proxy ? `- Proxy: ${redactProxyUrlForAssistantContext(proxy)}` : '- Proxy: none', `- Providers (${providers.length}): ${providers.map((p) => p.name ?? p.id).join(', ') || 'none configured'}`, `- MCP Servers: ${activeMcp.length} active / ${mcpServers.length} total`, '', @@ -237,6 +245,7 @@ async function probeHost(host: string): Promise<{ host: string; ok: boolean }> { export interface ClaudeCodeSessionOptions { lastAgentSessionId?: string + headless?: boolean thinkingOptions?: { effort?: 'low' | 'medium' | 'high' | 'max' thinking?: { type: 'adaptive' } | { type: 'enabled'; budgetTokens?: number } | { type: 'disabled' } @@ -283,20 +292,21 @@ export async function buildClaudeCodeSessionSettings( const steerHolder = getSteerHolder(session.id) // The hooks resolve the approval emitter / steer holder by session id at fire-time, so they are // not passed in; the holders above are created here only to expose them on `settings`. - const { canUseTool, hooks, disallowedTools, toolPolicySnapshot } = await buildToolPermissions(session, agent) + const { canUseTool, hooks, disallowedTools, toolPolicySnapshot } = await buildToolPermissions(session, agent, { + headless: options?.headless === true + }) // 5. System prompt const systemPrompt = await buildSystemPrompt(session, agent, cwd) // 6. MCP servers (session + built-in) const agentConfig = agent.configuration - const soulEnabled = agentConfig?.soul_enabled === true const isAssistant = agentConfig?.builtin_role === 'assistant' - const mcpServers = buildMcpServers(session, agent, soulEnabled, isAssistant) + const mcpServers = buildMcpServers(session, agent, isAssistant) const mcpToolMetadata = await buildMcpToolMetadata(agent) - // 8. Auto-approve allowlist for injected built-in MCP servers (soul/assistant only) - const finalAllowedTools = adjustAllowedToolsForMcp(soulEnabled, isAssistant) + // 8. Auto-approve allowlist for injected built-in MCP servers + const finalAllowedTools = adjustAllowedToolsForMcp(isAssistant) // 9. Skills — pass the SDK skill-name whitelist (managed skills enabled for this // agent + the workspace's own .claude/skills). The CLAUDE_CONFIG_DIR/skills mirror @@ -630,7 +640,8 @@ async function discoverPlugins(cwd: string, agentId: string): Promise> }> { const agentConfig = agent.configuration - const soulEnabled = agentConfig?.soul_enabled === true const isAssistant = agentConfig?.builtin_role === 'assistant' + const isHeadless = options.headless === true - // Raw session context for tool enable-predicates (worktree needs .git; claw notify/config need a - // connected channel). Channels are fetched once here so the predicates stay synchronous. + // Raw session context for tool enable-predicates (worktree tools need a .git dir). const cwd = session.workspace?.path const conditionContext: ClaudeToolContext | undefined = cwd ? { - cwd, - channels: (() => { - try { - return channelService.listChannels({ agentId: agent.id }) - } catch (error) { - logger.warn('Failed to list channels for tool policy context', { - agentId: agent.id, - error: error instanceof Error ? error.message : String(error) - }) - return [] - } - })() + cwd } : undefined @@ -669,10 +668,11 @@ async function buildToolPermissions( // user's knowledge bases, report_artifacts only records a declaration. The // untrusted-channel exposure this creates (approval-free reads + web_fetch URL egress for // channel-linked sessions) is bounded by the system-level channel security policy - // (CHANNEL_SECURITY_PROMPT). The MUTATING kb_manage tool is carved out below — it modifies the + // (CHANNEL_SECURITY_PROMPT). The autonomy tools (cron/notify/config) it also hosts stay + // auto-approved — they were blanket-allowed as the standalone `cherry` server before the + // merge. The MUTATING kb_manage tool is carved out below — it modifies the // user's knowledge bases, so it must go through per-call approval rather than this prefix. 'mcp__cherry-tools__', - ...(soulEnabled ? ['mcp__claw__'] : []), ...(isAssistant ? ['mcp__assistant__'] : []) ], // Mutating cherry-tools (kb_manage) match the prefix above but must still prompt for approval. @@ -685,6 +685,19 @@ async function buildToolPermissions( return { behavior: 'deny', message: 'Tool request was cancelled' } } + // Busy-session enqueue/steer cannot rebuild a connection's baked policy, so enforce per-turn + // headless AskUserQuestion denial at fire time. + if ( + toolName === 'AskUserQuestion' && + application.get('AgentSessionRuntimeService').isCurrentTurnHeadless(session.id) + ) { + return { + behavior: 'deny', + message: + 'This channel or scheduled turn has no interactive responder, so proceed without asking the user and state your assumptions instead.' + } + } + // Resolve the snapshot by id at fire-time — a warm-pooled query's baked `canUseTool` must read // the live session snapshot, not a per-build instance the running subprocess never sees. const snapshot = getToolPolicySnapshot(session.id) @@ -818,12 +831,11 @@ async function buildToolPermissions( canUseTool, hooks: { PreToolUse: [{ hooks: [disabledToolHook, dependencyIsolationHook, rtkRewriteHook, steerHook] }] }, // `disabled`-exposure tools (incl. WebSearch/WebFetch) come from the declarative - // registry; soul/assistant overlays stay until they migrate to per-tool exposure (PR-7). + // registry; agent/assistant overlays stay until they migrate to per-tool exposure (PR-7). disallowedTools: [ ...new Set([ ...resolveDisallowedTools({ disabledTools: agent.disabledTools }, conditionContext), - ...(soulEnabled ? SOUL_MODE_DISALLOWED_TOOLS : []), - ...(isAssistant ? ['AskUserQuestion'] : []) + ...(isAssistant || isHeadless ? ['AskUserQuestion'] : []) ]) ], toolPolicySnapshot @@ -859,7 +871,6 @@ export async function buildSystemPrompt( cwd: string ): Promise { const agentConfig = agent.configuration - const soulEnabled = agentConfig?.soul_enabled === true const builtinRole = agentConfig?.builtin_role as string | undefined const isAssistant = builtinRole === 'assistant' @@ -896,32 +907,14 @@ export async function buildSystemPrompt( // Not added to the assistant path above — it injects its own environment via buildAssistantContext. const runtimeBlock = `\n\n${await buildRuntimeContext()}` - // Soul mode - if (soulEnabled) { - const soulPrompt = await promptBuilder.buildSystemPrompt(cwd, agentConfig) - const userInstructions = instructions ? `\n\n${instructions}` : '' - return `${soulPrompt}${userInstructions}${channelSecurityBlock}${artifactsBlock}${runtimeBlock}\n\n${langInstruction}` - } - - // Standard mode - if (instructions) { - return { - type: 'preset', - preset: 'claude_code', - append: `${instructions}${channelSecurityBlock}${artifactsBlock}${runtimeBlock}\n\n${langInstruction}` - } - } - return { - type: 'preset', - preset: 'claude_code', - append: `${channelSecurityBlock}${artifactsBlock}${runtimeBlock}\n\n${langInstruction}` - } + const soulPrompt = await promptBuilder.buildSystemPrompt(cwd, agentConfig, Boolean(instructions?.trim())) + const userInstructions = instructions ? `\n\n${instructions}` : '' + return `${soulPrompt}${userInstructions}${channelSecurityBlock}${artifactsBlock}${runtimeBlock}\n\n${langInstruction}` } export function buildMcpServers( session: AgentSessionEntity, agent: AgentEntity, - soulEnabled: boolean, isAssistant: boolean ): Record | undefined { const mcpList: Record = {} @@ -939,45 +932,45 @@ export function buildMcpServers( } } - // 3. Cherry tools + // 3. Cherry tools — builtin lookups plus the agent autonomy tools (cron / notify / config), + // which register only because the agent context is passed. Use `agent.id` instead of + // `session.agentId` so TS can see the value is non-null after the upstream + // orphan check in buildClaudeCodeSessionSettings. + const sourceChannelId = resolveSourceChannel(agent.id, session.id) + let workspaceSource: AgentSessionWorkspaceSource + switch (session.workspace.type) { + case AGENT_WORKSPACE_TYPE.USER: + workspaceSource = { type: AGENT_WORKSPACE_TYPE.USER, workspaceId: session.workspaceId } + break + case AGENT_WORKSPACE_TYPE.SYSTEM: + workspaceSource = { type: AGENT_WORKSPACE_TYPE.SYSTEM } + break + default: { + const exhaustive: never = session.workspace.type + throw new Error(`Unsupported workspace type: ${String(exhaustive)}`) + } + } mcpList['cherry-tools'] = { type: 'sdk', name: 'cherry-tools', - instance: new CherryBuiltinToolsServer().mcpServer + instance: new CherryBuiltinToolsServer({ + agentId: agent.id, + workspaceSource, + workspacePath: session.workspace.path, + sourceChannelId + }).mcpServer } - // 4. Claw — agent autonomy tools (soul mode only). Use `agent.id` instead of - // `session.agentId` so TS can see the value is non-null after the upstream - // orphan check in buildClaudeCodeSessionSettings. - if (soulEnabled) { - const sourceChannelId = resolveSourceChannel(agent.id, session.id) - let workspaceSource: AgentSessionWorkspaceSource - switch (session.workspace.type) { - case AGENT_WORKSPACE_TYPE.USER: - workspaceSource = { type: AGENT_WORKSPACE_TYPE.USER, workspaceId: session.workspaceId } - break - case AGENT_WORKSPACE_TYPE.SYSTEM: - workspaceSource = { type: AGENT_WORKSPACE_TYPE.SYSTEM } - break - default: { - const exhaustive: never = session.workspace.type - throw new Error(`Unsupported workspace type: ${String(exhaustive)}`) - } - } - const clawServer = new ClawServer(agent.id, workspaceSource, session.workspace.path, sourceChannelId) - mcpList.claw = { type: 'sdk', name: 'claw', instance: clawServer.mcpServer } - - // agent-memory — the FACT.md / JOURNAL.jsonl memory tool the CherryClaw prompt and the - // workspace bootstrap drive via `mcp__agent-memory__memory`. Without it the documented - // "log completion" step (and all memory writes) have no backing server. - const memoryServer = new WorkspaceMemoryServer(agent.id, session.workspace.path) - mcpList['agent-memory'] = { type: 'sdk', name: 'agent-memory', instance: memoryServer.mcpServer } + // agent-memory — the FACT.md / JOURNAL.jsonl memory tool the agent prompt and the + // workspace bootstrap drive via `mcp__agent-memory__memory`. Without it the documented + // "log completion" step (and all memory writes) have no backing server. + const memoryServer = new WorkspaceMemoryServer(agent.id, session.workspace.path) + mcpList['agent-memory'] = { type: 'sdk', name: 'agent-memory', instance: memoryServer.mcpServer } - logger.debug('Soul Mode: injected claw + agent-memory MCP servers', { - agentId: agent.id, - totalMcpServers: Object.keys(mcpList).length - }) - } + logger.debug('Injected cherry-tools + agent-memory MCP servers', { + agentId: agent.id, + totalMcpServers: Object.keys(mcpList).length + }) // 5. Assistant — navigate + diagnose tools (Cherry Assistant only) if (isAssistant) { @@ -1055,17 +1048,14 @@ function resolveSourceChannel(agentId: string, sessionId: string): string | unde } /** - * Auto-approve allowlist for injected built-in MCP servers. Returns `undefined` for a plain agent - * (Claude Code then permits all tools; cherry-tools is auto-approved via the canUseTool prefix). - * Soul/assistant agents force an explicit allowlist so their claw/agent-memory/assistant tools pass. - * The read-only cherry-tools are listed explicitly (not a wildcard) so the mutating kb_manage tool is - * excluded from the SDK pre-approval and routed through per-call approval via canUseTool. + * Auto-approve allowlist for injected built-in MCP servers, so the + * cherry-tools/agent-memory/assistant tools pass without per-call approval. + * The auto-approved cherry-tools are listed explicitly (not a wildcard) so the mutating kb_manage + * tool is excluded from the SDK pre-approval and routed through per-call approval via canUseTool. */ -export function adjustAllowedToolsForMcp(soulEnabled: boolean, isAssistant: boolean): string[] | undefined { - if (!soulEnabled && !isAssistant) return undefined - +export function adjustAllowedToolsForMcp(isAssistant: boolean): string[] { const result = CHERRY_BUILTIN_AUTO_APPROVED_TOOL_NAMES.map(toCherryBuiltinRuntimeName) - if (soulEnabled) result.push('mcp__claw__*', 'mcp__agent-memory__*') + result.push('mcp__agent-memory__*') if (isAssistant) result.push('mcp__assistant__*') return result } diff --git a/src/main/ai/runtime/types.ts b/src/main/ai/runtime/types.ts index e808ee4e7d3..72dc930bbb3 100644 --- a/src/main/ai/runtime/types.ts +++ b/src/main/ai/runtime/types.ts @@ -28,6 +28,7 @@ export interface AgentRuntimeConnectInput { agentId: string modelId: UniqueModelId resumeToken?: string + headless?: boolean trace?: AgentRuntimeTraceContext } diff --git a/src/main/ai/streamManager/api/startAgentSessionRun.ts b/src/main/ai/streamManager/api/startAgentSessionRun.ts index 6683c99b681..6fc36bf872d 100644 --- a/src/main/ai/streamManager/api/startAgentSessionRun.ts +++ b/src/main/ai/streamManager/api/startAgentSessionRun.ts @@ -27,6 +27,7 @@ export async function startAgentSessionRun(input: { sessionId: string userParts: CherryMessagePart[] listeners: StreamListener[] + headless?: boolean }): Promise { if (input.listeners.length === 0) { throw new Error('startAgentSessionRun requires at least one listener') @@ -45,7 +46,8 @@ export async function startAgentSessionRun(input: { const prepared = await agentChatContextProvider.prepareDispatch(primary, { trigger: 'submit-message', topicId, - userMessageParts: input.userParts + userMessageParts: input.userParts, + headless: input.headless === true }) manager.send({ diff --git a/src/main/ai/streamManager/context/AgentChatContextProvider.ts b/src/main/ai/streamManager/context/AgentChatContextProvider.ts index 676a46a8665..7d1a6a1909a 100644 --- a/src/main/ai/streamManager/context/AgentChatContextProvider.ts +++ b/src/main/ai/streamManager/context/AgentChatContextProvider.ts @@ -111,7 +111,9 @@ export class AgentChatContextProvider implements ChatContextProvider { // Fire-and-forget is safe: the naming service isolates errors and rechecks state before writing. topicNamingService.maybeRenameAgentSessionFromFirstUserMessage(sessionId, savedUserMessage.data) - application.get('AgentSessionRuntimeService').enqueueUserMessage(sessionId, userMessage) + application + .get('AgentSessionRuntimeService') + .enqueueUserMessage(sessionId, userMessage, { headless: req.headless === true }) return { topicId: req.topicId, @@ -184,6 +186,7 @@ export class AgentChatContextProvider implements ChatContextProvider { modelId: uniqueModelId, assistantMessageId, userMessage, + headless: req.headless === true, traceId }) diff --git a/src/main/ai/streamManager/context/__tests__/AgentChatContextProvider.test.ts b/src/main/ai/streamManager/context/__tests__/AgentChatContextProvider.test.ts index 5df9d5c5601..e20a072151a 100644 --- a/src/main/ai/streamManager/context/__tests__/AgentChatContextProvider.test.ts +++ b/src/main/ai/streamManager/context/__tests__/AgentChatContextProvider.test.ts @@ -1,7 +1,7 @@ -import type { AiStreamOpenRequest } from '@shared/ai/transport' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { StreamListener } from '../../types' +import type { MainDispatchRequest } from '../dispatch' const mocks = vi.hoisted(() => ({ getSession: vi.fn(), @@ -58,13 +58,13 @@ function makeSubscriber(id = 'wc:1:agent-session:session-1'): StreamListener { } } -function openReq(overrides: Partial = {}): AiStreamOpenRequest { +function openReq(overrides: Partial = {}): MainDispatchRequest { return { topicId: 'agent-session:session-1', trigger: 'submit-message', userMessageParts: [{ type: 'text', text: 'hello' }], ...overrides - } as AiStreamOpenRequest + } as MainDispatchRequest } describe('AgentChatContextProvider', () => { @@ -185,6 +185,7 @@ describe('AgentChatContextProvider', () => { modelId: 'anthropic::claude-sonnet', assistantMessageId: prepared.models[0].request.messageId, userMessage: expect.objectContaining({ id: prepared.userMessageId, role: 'user', sessionId: 'session-1' }), + headless: false, traceId: 'a'.repeat(32) }) expect(prepared.listeners).toEqual([ @@ -205,7 +206,8 @@ describe('AgentChatContextProvider', () => { expect(mocks.runtimeBeginTurn).not.toHaveBeenCalled() expect(mocks.runtimeEnqueueUserMessage).toHaveBeenCalledWith( 'session-1', - expect.objectContaining({ role: 'user', sessionId: 'session-1' }) + expect.objectContaining({ role: 'user', sessionId: 'session-1' }), + { headless: false } ) expect(prepared.models).toEqual([]) expect(prepared.userMessageId).toEqual(expect.any(String)) @@ -219,6 +221,19 @@ describe('AgentChatContextProvider', () => { expect(prepared.listeners).toEqual([subscriber]) }) + it('forwards headless to the runtime when busy dispatch enqueues a follow-up', async () => { + const subscriber = makeSubscriber() + mocks.runtimeIsSessionBusy.mockReturnValue(true) + + await provider.prepareDispatch(subscriber, openReq({ headless: true })) + + expect(mocks.runtimeEnqueueUserMessage).toHaveBeenCalledWith( + 'session-1', + expect.objectContaining({ role: 'user', sessionId: 'session-1' }), + { headless: true } + ) + }) + it('triggers first-user-message session rename after submit-message persists the user row', async () => { const subscriber = makeSubscriber() mocks.runtimeIsSessionBusy.mockReturnValue(false) diff --git a/src/main/ai/streamManager/context/dispatch.ts b/src/main/ai/streamManager/context/dispatch.ts index fe1248b0ab2..27d27403510 100644 --- a/src/main/ai/streamManager/context/dispatch.ts +++ b/src/main/ai/streamManager/context/dispatch.ts @@ -40,7 +40,17 @@ export interface MainSteerContinuationRequest { userMessageId: string } -export type MainDispatchRequest = AiStreamOpenRequest | MainContinueConversationRequest | MainSteerContinuationRequest +export type MainDispatchRequest = ( + | AiStreamOpenRequest + | MainContinueConversationRequest + | MainSteerContinuationRequest +) & { + /** + * Main-only dispatch flag: the run has no interactive responder (channel message, scheduled + * task), so runtimes must not enable ask-the-user tools. Never set on renderer requests. + */ + headless?: boolean +} const logger = loggerService.withContext('chatContextDispatch') diff --git a/src/main/ai/tools/adapters/claudeCode/__tests__/agentTools.test.ts b/src/main/ai/tools/adapters/claudeCode/__tests__/agentTools.test.ts index 22ec1868847..c33ff1328fb 100644 --- a/src/main/ai/tools/adapters/claudeCode/__tests__/agentTools.test.ts +++ b/src/main/ai/tools/adapters/claudeCode/__tests__/agentTools.test.ts @@ -78,6 +78,15 @@ describe('createClaudeAgentToolPolicySnapshot — live disabledTools', () => { expect(snapshot.isDisabled('Bash')).toBe(true) }) + it('honors disabledTools for notify and config autonomy tools', async () => { + const snapshot = await createClaudeAgentToolPolicySnapshot( + makeAgent(['mcp__cherry-tools__notify', 'mcp__cherry-tools__config']) + ) + expect(snapshot.isDisabled('mcp__cherry-tools__notify')).toBe(true) + expect(snapshot.isDisabled('mcp__cherry-tools__config')).toBe(true) + expect(snapshot.isDisabled('mcp__cherry-tools__cron')).toBe(false) + }) + it('keeps prior MCP descriptors when a later server listing fails', async () => { mocks.listMcpTools.mockReturnValueOnce([{ name: 'search_docs', description: 'Search docs' }]) const snapshot = await createClaudeAgentToolPolicySnapshot(makeAgent([], ['mcp-1'])) @@ -153,6 +162,19 @@ describe('createClaudeAgentToolPolicySnapshot — auto-allow prefix + approval e // A sibling read tool under the same prefix is still auto-approved. expect(snapshot.resolve('mcp__cherry-tools__kb_read')).toMatchObject({ approval: 'auto' }) }) + + it('auto-approves the merged autonomy tools while kb_manage still prompts', async () => { + // The former standalone `cherry` server's cron/notify/config now live under cherry-tools and + // must stay auto-approved; the mutating kb_manage carve-out must survive the merge. + const snapshot = await createClaudeAgentToolPolicySnapshot(makeAgent(), { + autoAllowRuntimeNamePrefixes: ['mcp__cherry-tools__'], + autoAllowRuntimeNameExceptions: ['mcp__cherry-tools__kb_manage'] + }) + expect(snapshot.resolve('mcp__cherry-tools__cron')).toMatchObject({ approval: 'auto' }) + expect(snapshot.resolve('mcp__cherry-tools__notify')).toMatchObject({ approval: 'auto' }) + expect(snapshot.resolve('mcp__cherry-tools__config')).toMatchObject({ approval: 'auto' }) + expect(snapshot.resolve('mcp__cherry-tools__kb_manage')).toMatchObject({ approval: 'prompt' }) + }) }) describe('createClaudeAgentToolPolicySnapshot — production approval-gate wiring', () => { diff --git a/src/main/ai/tools/adapters/claudeCode/__tests__/toolConditions.test.ts b/src/main/ai/tools/adapters/claudeCode/__tests__/toolConditions.test.ts index 70d0d499cdb..6c017d89450 100644 --- a/src/main/ai/tools/adapters/claudeCode/__tests__/toolConditions.test.ts +++ b/src/main/ai/tools/adapters/claudeCode/__tests__/toolConditions.test.ts @@ -38,6 +38,14 @@ describe('resolveDisallowedTools', () => { expect(disallowed.has('BashOutput')).toBe(true) }) + it('honors user opt-outs for notify and config autonomy tools', () => { + const disallowed = new Set( + resolveDisallowedTools({ disabledTools: ['mcp__cherry-tools__notify', 'mcp__cherry-tools__config'] }) + ) + expect(disallowed.has('mcp__cherry-tools__notify')).toBe(true) + expect(disallowed.has('mcp__cherry-tools__config')).toBe(true) + }) + it('disabling the "Knowledge Search" toggle also revokes kb_list and kb_read (they dependsOn kb_search)', () => { // kb_read returns whole documents and kb_list browses every base — strictly more than kb_search's // chunks — so the visible kb_search toggle must honestly cover them, not leave read access reachable. @@ -72,24 +80,26 @@ describe('resolveDisallowedTools', () => { it('treats predicate-gated tools as enabled when no ctx is supplied', () => { const disallowed = new Set(resolveDisallowedTools({})) expect(disallowed.has('EnterWorktree')).toBe(false) - expect(disallowed.has('mcp__claw__notify')).toBe(false) + expect(disallowed.has('mcp__cherry-tools__notify')).toBe(false) }) - it('disables worktree without .git and claw notify/config without channels', () => { + it('disables worktree tools without .git but keeps notify available (self-degrades when no channels)', () => { existsSync.mockReturnValue(false) // no .git - const disallowed = new Set(resolveDisallowedTools({}, { cwd: '/ws', channels: [] })) + const disallowed = new Set(resolveDisallowedTools({}, { cwd: '/ws' })) expect(disallowed.has('EnterWorktree')).toBe(true) expect(disallowed.has('ExitWorktree')).toBe(true) - expect(disallowed.has('mcp__claw__notify')).toBe(true) - expect(disallowed.has('mcp__claw__config')).toBe(true) + // notify is no longer channel-gated: it reports "no connected channels" at call time instead of + // being hard-disabled, so an agent can add its first channel and notify in the same session. + expect(disallowed.has('mcp__cherry-tools__notify')).toBe(false) + expect(disallowed.has('mcp__cherry-tools__config')).toBe(false) }) - it('enables worktree with .git and claw notify/config with a channel', () => { + it('enables worktree tools with .git and keeps notify/config available', () => { existsSync.mockReturnValue(true) // .git present - const disallowed = new Set(resolveDisallowedTools({}, { cwd: '/ws', channels: [{ id: 'c1' }] })) + const disallowed = new Set(resolveDisallowedTools({}, { cwd: '/ws' })) expect(disallowed.has('EnterWorktree')).toBe(false) expect(disallowed.has('ExitWorktree')).toBe(false) - expect(disallowed.has('mcp__claw__notify')).toBe(false) - expect(disallowed.has('mcp__claw__config')).toBe(false) + expect(disallowed.has('mcp__cherry-tools__notify')).toBe(false) + expect(disallowed.has('mcp__cherry-tools__config')).toBe(false) }) }) diff --git a/src/main/ai/tools/adapters/claudeCode/cherryBuiltinApproval.ts b/src/main/ai/tools/adapters/claudeCode/cherryBuiltinApproval.ts index 67f9f835c62..bd2dd89274b 100644 --- a/src/main/ai/tools/adapters/claudeCode/cherryBuiltinApproval.ts +++ b/src/main/ai/tools/adapters/claudeCode/cherryBuiltinApproval.ts @@ -9,10 +9,13 @@ */ import { + CONFIG_TOOL_NAME, + CRON_TOOL_NAME, KB_LIST_TOOL_NAME, KB_MANAGE_TOOL_NAME, KB_READ_TOOL_NAME, KB_SEARCH_TOOL_NAME, + NOTIFY_TOOL_NAME, REPORT_ARTIFACTS_TOOL_NAME, WEB_FETCH_TOOL_NAME, WEB_SEARCH_TOOL_NAME @@ -26,13 +29,16 @@ export const toCherryBuiltinRuntimeName = (toolName: string): string => `mcp__${ /** * cherry-tools that mutate the user's knowledge bases (add / delete / refresh sources) and therefore - * MUST go through per-call user approval — never auto-approved, even for soul/assistant sessions. + * MUST go through per-call user approval — never auto-approved, even for agent/assistant sessions. */ export const CHERRY_BUILTIN_APPROVAL_REQUIRED_TOOL_NAMES: readonly string[] = [KB_MANAGE_TOOL_NAME] /** - * cherry-tools that only read (web/kb lookups) or record a declaration (report_artifacts), so they - * are safe to auto-approve for agent sessions. Excludes the mutating tools above. + * cherry-tools that only read (web/kb lookups), record a declaration (report_artifacts), or drive + * the agent's own in-app autonomy (cron/notify/config — auto-approved since they shipped as the + * blanket-allowed standalone `cherry` server; their side effects stay inside the app: scheduling + * the agent's tasks, notifying the user's channels, managing the agent's own config). Excludes the + * mutating tools above. */ export const CHERRY_BUILTIN_AUTO_APPROVED_TOOL_NAMES: readonly string[] = [ WEB_SEARCH_TOOL_NAME, @@ -40,5 +46,8 @@ export const CHERRY_BUILTIN_AUTO_APPROVED_TOOL_NAMES: readonly string[] = [ KB_SEARCH_TOOL_NAME, KB_READ_TOOL_NAME, KB_LIST_TOOL_NAME, - REPORT_ARTIFACTS_TOOL_NAME + REPORT_ARTIFACTS_TOOL_NAME, + CRON_TOOL_NAME, + NOTIFY_TOOL_NAME, + CONFIG_TOOL_NAME ] diff --git a/src/main/ai/tools/adapters/claudeCode/toolConditions.ts b/src/main/ai/tools/adapters/claudeCode/toolConditions.ts index 26a6a8c43c7..94672992c6e 100644 --- a/src/main/ai/tools/adapters/claudeCode/toolConditions.ts +++ b/src/main/ai/tools/adapters/claudeCode/toolConditions.ts @@ -16,8 +16,6 @@ import { CLAUDE_TOOL_DEFS } from '@shared/ai/claudecode/toolRegistry' export interface ClaudeToolContext { /** Session workspace directory. */ cwd: string - /** The agent's channels (pre-fetched — channel lookup is async; predicates read the list). */ - channels: readonly unknown[] } function dirHasGit(cwd: string): boolean { @@ -35,9 +33,7 @@ function dirHasGit(cwd: string): boolean { */ const TOOL_ENABLE_PREDICATES: Record boolean> = { EnterWorktree: (ctx) => dirHasGit(ctx.cwd), - ExitWorktree: (ctx) => dirHasGit(ctx.cwd), - mcp__claw__notify: (ctx) => ctx.channels.length > 0, - mcp__claw__config: (ctx) => ctx.channels.length > 0 + ExitWorktree: (ctx) => dirHasGit(ctx.cwd) } /** diff --git a/src/main/data/migration/v2/migrators/AgentsMigrator.ts b/src/main/data/migration/v2/migrators/AgentsMigrator.ts index e6e54040928..96a5f6f4c42 100644 --- a/src/main/data/migration/v2/migrators/AgentsMigrator.ts +++ b/src/main/data/migration/v2/migrators/AgentsMigrator.ts @@ -41,6 +41,8 @@ type V1ScheduledTaskRow = { type V1ChannelTaskSubscription = { channel_id: string task_id: string + channel_agent_id: string | null + task_agent_id: string } const HEARTBEAT_INTERVAL_FALLBACK_MS = 60 * 60_000 @@ -507,16 +509,23 @@ export class AgentsMigrator extends BaseMigrator { const v1Subs = db.all( sql.raw( - 'SELECT channel_id, task_id FROM agents_legacy.channel_task_subscriptions ' + - 'WHERE channel_id IN (SELECT id FROM agent_channel) ' + - 'AND task_id IN (SELECT id FROM agents_legacy.scheduled_tasks WHERE agent_id IN (SELECT id FROM agent))' + 'SELECT s.channel_id, s.task_id, c.agent_id AS channel_agent_id, t.agent_id AS task_agent_id ' + + 'FROM agents_legacy.channel_task_subscriptions s ' + + 'JOIN agent_channel c ON c.id = s.channel_id ' + + 'JOIN agents_legacy.scheduled_tasks t ON t.id = s.task_id ' + + 'JOIN agent a ON a.id = t.agent_id' ) ) let subCount = 0 + let skippedCrossAgentSubCount = 0 for (const sub of v1Subs) { const newScheduleId = idMap.get(sub.task_id) if (!newScheduleId) continue + if (sub.channel_agent_id !== sub.task_agent_id) { + skippedCrossAgentSubCount++ + continue + } await db .insert(agentChannelTaskTable) .values({ channelId: sub.channel_id, taskId: newScheduleId }) @@ -527,6 +536,7 @@ export class AgentsMigrator extends BaseMigrator { logger.info('Scheduled tasks migrated', { schedules: migratedCount, channelLinks: subCount, + skippedCrossAgentChannelLinks: skippedCrossAgentSubCount, sanitizedNames: droppedNameCount }) } diff --git a/src/main/data/migration/v2/migrators/__tests__/AgentsMigrator.task.test.ts b/src/main/data/migration/v2/migrators/__tests__/AgentsMigrator.task.test.ts index e10c75da0e3..fec376ce122 100644 --- a/src/main/data/migration/v2/migrators/__tests__/AgentsMigrator.task.test.ts +++ b/src/main/data/migration/v2/migrators/__tests__/AgentsMigrator.task.test.ts @@ -32,6 +32,8 @@ import { AgentsMigrator } from '../AgentsMigrator' const AGENT_ID = 'agent-v1-001' const CHANNEL_ID = 'channel-v1-001' +const FOREIGN_AGENT_ID = 'agent-v1-foreign' +const FOREIGN_CHANNEL_ID = 'channel-v1-foreign' async function seedLegacyDb(path: string): Promise { const db = new Database(path) @@ -98,9 +100,7 @@ async function seedLegacyDb(path: string): Promise { VALUES (?, ?, ?, ?, ?, ?, ?, ?)` ).run('log-2', 'task-interval', null, '2026-05-19T10:00:00.000Z', 567, 'error', null, 'boom') - // Two subscriptions — one pointing at a task that survives the migration, - // one pointing at a task whose agent does NOT exist in target → migrator - // must skip the dangling row instead of FK-failing. + // Three subscriptions: one valid, one dangling task, one cross-agent link. db.prepare(`INSERT INTO channel_task_subscriptions (channel_id, task_id) VALUES (?, ?)`).run( CHANNEL_ID, 'task-cron' @@ -109,6 +109,10 @@ async function seedLegacyDb(path: string): Promise { CHANNEL_ID, 'orphan-task' ) + db.prepare(`INSERT INTO channel_task_subscriptions (channel_id, task_id) VALUES (?, ?)`).run( + FOREIGN_CHANNEL_ID, + 'task-cron' + ) } finally { db.close() } @@ -140,15 +144,34 @@ describe('AgentsMigrator > migrateScheduledTasksTs', () => { model: null, orderKey: 'a0' }) - await dbh.db.insert(agentChannelTable).values({ - id: CHANNEL_ID, - type: 'telegram', - name: 'TG channel', - agentId: AGENT_ID, - workspace: { type: 'system' }, - config: { bot_token: 'x', allowed_chat_ids: [] }, - isActive: true + await dbh.db.insert(agentTable).values({ + id: FOREIGN_AGENT_ID, + type: 'claude-code', + name: 'Foreign Agent', + instructions: 'helper', + model: null, + orderKey: 'a1' }) + await dbh.db.insert(agentChannelTable).values([ + { + id: CHANNEL_ID, + type: 'telegram', + name: 'TG channel', + agentId: AGENT_ID, + workspace: { type: 'system' }, + config: { bot_token: 'x', allowed_chat_ids: [] }, + isActive: true + }, + { + id: FOREIGN_CHANNEL_ID, + type: 'telegram', + name: 'Foreign TG channel', + agentId: FOREIGN_AGENT_ID, + workspace: { type: 'system' }, + config: { bot_token: 'x', allowed_chat_ids: [] }, + isActive: true + } + ]) }) /** Helper: ATTACH the legacy DB to the target connection, run the TS-loop, @@ -239,9 +262,8 @@ describe('AgentsMigrator > migrateScheduledTasksTs', () => { const newScheduleId = cron[0]?.id const links = await dbh.db.select().from(agentChannelTaskTable) - // Only one subscription survives — the orphan-task row is dropped because - // its task isn't in legacy.scheduled_tasks (the filter pulls only rows - // whose task_id and channel_id both resolve). + // Only one subscription survives — the orphan-task row is dangling, and + // the foreign channel belongs to a different agent than the task. expect(links).toHaveLength(1) expect(links[0]?.channelId).toBe(CHANNEL_ID) expect(links[0]?.taskId).toBe(newScheduleId) diff --git a/src/main/data/services/AgentChannelService.ts b/src/main/data/services/AgentChannelService.ts index 7bf97e147ec..196b3f4bad0 100644 --- a/src/main/data/services/AgentChannelService.ts +++ b/src/main/data/services/AgentChannelService.ts @@ -184,6 +184,12 @@ export class AgentChannelService { } } + clearTaskSubscriptionsForChannel(channelId: string): void { + const database = application.get('DbService').getDb() + database.delete(channelTaskSubscriptionsTable).where(eq(channelTaskSubscriptionsTable.channelId, channelId)).run() + logger.info('Channel task subscriptions cleared', { channelId }) + } + getSubscribedChannels(taskId: string): AgentChannelEntity[] { const database = application.get('DbService').getDb() const subs = database diff --git a/src/main/data/services/AgentChannelWorkflowService.ts b/src/main/data/services/AgentChannelWorkflowService.ts index 9d2e5598884..c86d9134470 100644 --- a/src/main/data/services/AgentChannelWorkflowService.ts +++ b/src/main/data/services/AgentChannelWorkflowService.ts @@ -69,6 +69,11 @@ export class AgentChannelWorkflowService { logger.warn('updateChannel: row disappeared mid-update', { channelId }) return null } + const agentIdChanged = updates.agentId !== undefined && updates.agentId !== (existing.agentId ?? null) + if (agentIdChanged) { + // Deliberately not restored if syncChannel fails: conservative cleanup prevents wrong-agent delivery. + agentChannelService.clearTaskSubscriptionsForChannel(channelId) + } try { await application.get('ChannelManager').syncChannel(channelId, { awaitConnect: true, strictDisconnect: true }) @@ -90,6 +95,10 @@ export class AgentChannelWorkflowService { try { agentChannelService.updateChannel(channelId, restoreUpdates) + if (agentIdChanged) { + // Re-clear subscriptions created during the transient rebind window. + agentChannelService.clearTaskSubscriptionsForChannel(channelId) + } } catch (restoreError) { logger.warn('Failed to restore channel after sync failure', { channelId, diff --git a/src/main/data/services/AgentTaskService.ts b/src/main/data/services/AgentTaskService.ts index 7be2beb092d..69b9f10500b 100644 --- a/src/main/data/services/AgentTaskService.ts +++ b/src/main/data/services/AgentTaskService.ts @@ -1,7 +1,7 @@ /** * Thin facade — preserves existing DataApi/MCP IPC shape (ScheduledTaskEntity etc.). * Internally delegates to JobManager + jobScheduleService + jobService. - * TODO: migrate callers (data/api/handlers/agents.ts, ai/mcp/servers/claw.ts) to the + * TODO: migrate callers (data/api/handlers/agents.ts, ai/mcp/servers/cherryAutonomyTools.ts) to the * generic Job/Scheduler API directly, then delete this facade. */ @@ -60,15 +60,10 @@ function deriveStatus(snapshot: JobScheduleSnapshot): 'active' | 'paused' | 'com } export class AgentTaskService { - /** - * Scheduled tasks require an autonomous agent — either Soul Mode - * (soul_enabled) or bypassPermissions permission mode — otherwise - * tool calls during task execution will fail with permission errors. - */ - private assertAutonomous(agentId: string): void { + private assertAgentExists(agentId: string): void { const database = application.get('DbService').getDb() const [row] = database - .select({ configuration: agentsTable.configuration }) + .select({ id: agentsTable.id }) .from(agentsTable) .where(eq(agentsTable.id, agentId)) .limit(1) @@ -77,20 +72,22 @@ export class AgentTaskService { if (!row) { throw DataApiErrorFactory.notFound('Agent', agentId) } + } - const config: Record = row.configuration ?? {} - - if (config.soul_enabled === true || config.permission_mode === 'bypassPermissions') { - return + private assertChannelsBelongToAgent(agentId: string, channelIds: string[]): void { + for (const channelId of channelIds) { + const channel = agentChannelService.getChannel(channelId) + if (!channel || channel.agentId !== agentId) { + throw DataApiErrorFactory.notFound('Channel', channelId) + } } - - throw DataApiErrorFactory.invalidOperation( - 'Scheduled tasks require Soul Mode or Bypass Permissions mode. Update the agent settings first.' - ) } async createTask(agentId: string, dto: CreateTaskDto): Promise { - this.assertAutonomous(agentId) + this.assertAgentExists(agentId) + if (dto.channelIds?.length) { + this.assertChannelsBelongToAgent(agentId, dto.channelIds) + } const timeoutMinutes = dto.timeoutMinutes ?? 2 const jobInputTemplate: AgentTaskJobInputTemplate = { @@ -176,6 +173,9 @@ export class AgentTaskService { const existingSnapshot = jobScheduleService.getById(taskId) const existingTemplate = existingSnapshot ? normalizeAgentTaskTemplate(existingSnapshot.jobInputTemplate) : null if (!existingSnapshot || !existingTemplate) return null + if (patch.channelIds !== undefined) { + this.assertChannelsBelongToAgent(agentId, patch.channelIds) + } // Build the updated jobInputTemplate when prompt/timeoutMinutes changed. const nextPrompt = patch.prompt ?? existingTemplate.prompt diff --git a/src/main/data/services/__tests__/AgentChannelWorkflowService.integration.test.ts b/src/main/data/services/__tests__/AgentChannelWorkflowService.integration.test.ts index 97b8d8f7dd0..bfe5756de2e 100644 --- a/src/main/data/services/__tests__/AgentChannelWorkflowService.integration.test.ts +++ b/src/main/data/services/__tests__/AgentChannelWorkflowService.integration.test.ts @@ -9,7 +9,8 @@ */ import { agentTable } from '@data/db/schemas/agent' -import { agentChannelTable } from '@data/db/schemas/agentChannel' +import { agentChannelTable, agentChannelTaskTable } from '@data/db/schemas/agentChannel' +import { jobScheduleTable } from '@data/db/schemas/job' import { setupTestDatabase } from '@test-helpers/db' import { eq } from 'drizzle-orm' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -52,6 +53,17 @@ describe('AgentChannelWorkflowService.updateChannel — DB rollback integration' }) } + async function insertSchedule(id: string): Promise { + await dbh.db.insert(jobScheduleTable).values({ + id, + type: 'agent.task', + name: id, + trigger: { kind: 'interval', ms: 60_000 }, + jobInputTemplate: {}, + catchUpPolicy: { kind: 'skip-missed' } + }) + } + it('restores the SQLite row byte-for-byte when syncChannel throws', async () => { await insertAgent('agent-rollback-1') syncChannelMock.mockResolvedValue(undefined) @@ -104,6 +116,63 @@ describe('AgentChannelWorkflowService.updateChannel — DB rollback integration' expect(after.createdAt).toBe(snapshot.createdAt) }) + it('clears task subscriptions when agentId is rebound', async () => { + await insertAgent('agent-old') + await insertAgent('agent-new') + await insertSchedule('schedule-clear-1') + await insertSchedule('schedule-clear-2') + syncChannelMock.mockResolvedValue(undefined) + + const created = await agentChannelWorkflowService.createChannel({ + type: 'telegram', + name: 'Rebound Channel', + agentId: 'agent-old', + workspace: { type: 'system' }, + config: TELEGRAM_CONFIG, + isActive: true + }) + await dbh.db.insert(agentChannelTaskTable).values([ + { channelId: created.id, taskId: 'schedule-clear-1' }, + { channelId: created.id, taskId: 'schedule-clear-2' } + ]) + + await agentChannelWorkflowService.updateChannel(created.id, { agentId: 'agent-new' }) + + const remaining = await dbh.db + .select() + .from(agentChannelTaskTable) + .where(eq(agentChannelTaskTable.channelId, created.id)) + expect(remaining).toEqual([]) + }) + + it('keeps task subscriptions when agentId is not updated', async () => { + await insertAgent('agent-unchanged') + await insertSchedule('schedule-keep-1') + await insertSchedule('schedule-keep-2') + syncChannelMock.mockResolvedValue(undefined) + + const created = await agentChannelWorkflowService.createChannel({ + type: 'telegram', + name: 'Unchanged Channel', + agentId: 'agent-unchanged', + workspace: { type: 'system' }, + config: TELEGRAM_CONFIG, + isActive: true + }) + await dbh.db.insert(agentChannelTaskTable).values([ + { channelId: created.id, taskId: 'schedule-keep-1' }, + { channelId: created.id, taskId: 'schedule-keep-2' } + ]) + + await agentChannelWorkflowService.updateChannel(created.id, { name: 'Renamed Channel' }) + + const remaining = await dbh.db + .select({ taskId: agentChannelTaskTable.taskId }) + .from(agentChannelTaskTable) + .where(eq(agentChannelTaskTable.channelId, created.id)) + expect(remaining.map((row) => row.taskId).sort()).toEqual(['schedule-keep-1', 'schedule-keep-2']) + }) + it('restores nullable fields to NULL (not undefined-skip) when original was NULL', async () => { // Regression guard for the Drizzle "undefined = skip" pitfall in rollback: // the snapshot comes from rowToEntity (NULL → undefined). Without an explicit diff --git a/src/main/data/services/__tests__/AgentChannelWorkflowService.test.ts b/src/main/data/services/__tests__/AgentChannelWorkflowService.test.ts index dd3d0e931fe..01b53b3fd6d 100644 --- a/src/main/data/services/__tests__/AgentChannelWorkflowService.test.ts +++ b/src/main/data/services/__tests__/AgentChannelWorkflowService.test.ts @@ -15,11 +15,18 @@ vi.mock('@application', async () => { } as Parameters[0]) }) -const { createChannelMock, getChannelMock, updateChannelMock, deleteChannelMock } = vi.hoisted(() => ({ +const { + createChannelMock, + getChannelMock, + updateChannelMock, + deleteChannelMock, + clearTaskSubscriptionsForChannelMock +} = vi.hoisted(() => ({ createChannelMock: vi.fn(), getChannelMock: vi.fn(), updateChannelMock: vi.fn(), - deleteChannelMock: vi.fn() + deleteChannelMock: vi.fn(), + clearTaskSubscriptionsForChannelMock: vi.fn() })) vi.mock('@data/services/AgentChannelService', () => ({ @@ -27,7 +34,8 @@ vi.mock('@data/services/AgentChannelService', () => ({ createChannel: createChannelMock, getChannel: getChannelMock, updateChannel: updateChannelMock, - deleteChannel: deleteChannelMock + deleteChannel: deleteChannelMock, + clearTaskSubscriptionsForChannel: clearTaskSubscriptionsForChannelMock } })) @@ -156,6 +164,30 @@ describe('AgentChannelWorkflowService', () => { expect(result).toEqual(updated) }) + it('clears task subscriptions when agentId is rebound', async () => { + const existing = makeChannel({ agentId: 'agent-1' }) + const updated = makeChannel({ agentId: 'agent-2' }) + getChannelMock.mockReturnValue(existing) + updateChannelMock.mockReturnValue(updated) + syncChannelMock.mockResolvedValue(undefined) + + await agentChannelWorkflowService.updateChannel('ch-1', { agentId: 'agent-2' }) + + expect(clearTaskSubscriptionsForChannelMock).toHaveBeenCalledWith('ch-1') + }) + + it('keeps task subscriptions when agentId is not updated', async () => { + const existing = makeChannel({ agentId: 'agent-1' }) + const updated = makeChannel({ name: 'New Name', agentId: 'agent-1' }) + getChannelMock.mockReturnValue(existing) + updateChannelMock.mockReturnValue(updated) + syncChannelMock.mockResolvedValue(undefined) + + await agentChannelWorkflowService.updateChannel('ch-1', { name: 'New Name' }) + + expect(clearTaskSubscriptionsForChannelMock).not.toHaveBeenCalled() + }) + it('restores all fields (name/agentId/sessionId/config/isActive/activeChatIds/permissionMode) when syncChannel throws', async () => { const existing = makeChannel() const updated = makeChannel({ name: 'New Name' }) @@ -194,6 +226,22 @@ describe('AgentChannelWorkflowService', () => { ) }) + it('re-clears task subscriptions after rolling back a failed agentId rebind', async () => { + const existing = makeChannel({ agentId: 'agent-1' }) + const updated = makeChannel({ agentId: 'agent-2' }) + getChannelMock.mockReturnValue(existing) + updateChannelMock.mockReturnValueOnce(updated).mockReturnValueOnce(existing) + syncChannelMock.mockRejectedValue(new Error('sync failed')) + + await expect(agentChannelWorkflowService.updateChannel('ch-1', { agentId: 'agent-2' })).rejects.toThrow( + 'sync failed' + ) + + expect(clearTaskSubscriptionsForChannelMock).toHaveBeenCalledTimes(2) + expect(clearTaskSubscriptionsForChannelMock).toHaveBeenNthCalledWith(1, 'ch-1') + expect(clearTaskSubscriptionsForChannelMock).toHaveBeenNthCalledWith(2, 'ch-1') + }) + it('rejects discord-shaped config when existing channel is telegram (cross-type guard)', async () => { const existing = makeChannel({ type: 'telegram', config: { bot_token: 'tok' } }) getChannelMock.mockReturnValue(existing) diff --git a/src/main/data/services/__tests__/AgentService.test.ts b/src/main/data/services/__tests__/AgentService.test.ts index 0dc561532c0..7838ba7e2b7 100644 --- a/src/main/data/services/__tests__/AgentService.test.ts +++ b/src/main/data/services/__tests__/AgentService.test.ts @@ -49,11 +49,6 @@ vi.mock('@main/apiServer/services/models', () => ({ } })) -// Mock workspace seeding — filesystem ops not needed in unit tests -vi.mock('@main/ai/agents/cherryclaw/seedWorkspace', () => ({ - seedWorkspaceTemplates: vi.fn() -})) - describe('AgentService', () => { const dbh = setupTestDatabase() const uuidV4Pattern = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/ diff --git a/src/main/data/services/__tests__/AgentTaskService.test.ts b/src/main/data/services/__tests__/AgentTaskService.test.ts index 507328fe251..77f5058696d 100644 --- a/src/main/data/services/__tests__/AgentTaskService.test.ts +++ b/src/main/data/services/__tests__/AgentTaskService.test.ts @@ -18,12 +18,17 @@ vi.mock('@application', async () => { const dbDeleteMock = vi.fn() const dbInsertMock = vi.fn() const dbSelectMock = vi.fn() -const { replaceTaskSubscriptionsMock } = vi.hoisted(() => ({ +const { getChannelMock, replaceTaskSubscriptionsMock } = vi.hoisted(() => ({ + getChannelMock: vi.fn(), replaceTaskSubscriptionsMock: vi.fn() })) vi.mock('@data/services/AgentChannelService', () => ({ - agentChannelService: { getSubscribedChannels: vi.fn(), replaceTaskSubscriptions: replaceTaskSubscriptionsMock } + agentChannelService: { + getChannel: getChannelMock, + getSubscribedChannels: vi.fn(), + replaceTaskSubscriptions: replaceTaskSubscriptionsMock + } })) vi.mock('@data/services/JobScheduleService', () => ({ jobScheduleService: { getById: vi.fn(), listAll: vi.fn() } @@ -40,6 +45,7 @@ import { jobService } from '@data/services/JobService' import { agentTaskService } from '../AgentTaskService' const AGENT_ID = 'agent-a1' +const OTHER_AGENT_ID = 'agent-b1' const TASK_ID = 'sched-1' const validTrigger = { kind: 'interval' as const, ms: 60_000 } @@ -102,7 +108,7 @@ const updateJobScheduleMock = vi.fn() const unregisterJobScheduleByIdMock = vi.fn() function setupApplicationMocks(opts: { configuration?: Record | null } = {}) { - const { configuration = { soul_enabled: true } } = opts + const { configuration = {} } = opts const fakeQueryChain = { from: () => ({ where: () => ({ @@ -145,6 +151,8 @@ describe('AgentTaskService (thin facade)', () => { dbSelectMock.mockReset() dbInsertMock.mockReset() dbDeleteMock.mockReset() + vi.mocked(agentChannelService.getChannel).mockReset() + vi.mocked(agentChannelService.getChannel).mockImplementation((id: string) => ({ id, agentId: AGENT_ID }) as never) vi.mocked(agentChannelService.getSubscribedChannels).mockReset() vi.mocked(agentChannelService.getSubscribedChannels).mockReturnValue([]) replaceTaskSubscriptionsMock.mockReset() @@ -158,7 +166,7 @@ describe('AgentTaskService (thin facade)', () => { }) describe('createTask', () => { - it('registers a schedule with agent.task type when the agent is autonomous', async () => { + it('registers a schedule with agent.task type', async () => { setupApplicationMocks() registerJobScheduleMock.mockReturnValueOnce({ id: TASK_ID }) vi.mocked(jobScheduleService.getById).mockReturnValueOnce(makeSnapshot()) @@ -184,32 +192,49 @@ describe('AgentTaskService (thin facade)', () => { expect(registerJobScheduleMock).not.toHaveBeenCalled() }) - it('throws invalidOperation when the agent is not autonomous', async () => { - setupApplicationMocks({ configuration: { soul_enabled: false } }) + it('creates task subscriptions for channels owned by the agent', async () => { + setupApplicationMocks() + registerJobScheduleMock.mockReturnValueOnce({ id: TASK_ID }) + vi.mocked(jobScheduleService.getById).mockReturnValueOnce(makeSnapshot()) - await expect(agentTaskService.createTask(AGENT_ID, validDto)).rejects.toMatchObject({ - message: expect.stringContaining('Soul Mode') - }) - expect(registerJobScheduleMock).not.toHaveBeenCalled() + await agentTaskService.createTask(AGENT_ID, { ...validDto, channelIds: ['channel-1', 'channel-2'] }) + + expect(agentChannelService.getChannel).toHaveBeenCalledWith('channel-1') + expect(agentChannelService.getChannel).toHaveBeenCalledWith('channel-2') + expect(replaceTaskSubscriptionsMock).toHaveBeenCalledWith(TASK_ID, ['channel-1', 'channel-2']) + expect(dbInsertMock).not.toHaveBeenCalled() }) - it('accepts bypassPermissions as a valid autonomous configuration', async () => { - setupApplicationMocks({ configuration: { permission_mode: 'bypassPermissions' } }) - registerJobScheduleMock.mockReturnValueOnce({ id: TASK_ID }) - vi.mocked(jobScheduleService.getById).mockReturnValueOnce(makeSnapshot()) + it('rejects a channel owned by another agent before registering a schedule', async () => { + setupApplicationMocks() + vi.mocked(agentChannelService.getChannel).mockReturnValueOnce({ + id: 'foreign-channel', + agentId: OTHER_AGENT_ID + } as never) + + await expect( + agentTaskService.createTask(AGENT_ID, { ...validDto, channelIds: ['foreign-channel'] }) + ).rejects.toMatchObject({ + message: expect.stringContaining('Channel') + }) - await expect(agentTaskService.createTask(AGENT_ID, validDto)).resolves.toMatchObject({ id: TASK_ID }) + expect(registerJobScheduleMock).not.toHaveBeenCalled() + expect(replaceTaskSubscriptionsMock).not.toHaveBeenCalled() }) - it('delegates task channel subscriptions to AgentChannelService', async () => { + it('rejects a nonexistent channel id before registering a schedule', async () => { setupApplicationMocks() - registerJobScheduleMock.mockReturnValueOnce({ id: TASK_ID }) - vi.mocked(jobScheduleService.getById).mockReturnValueOnce(makeSnapshot()) + vi.mocked(agentChannelService.getChannel).mockReturnValueOnce(null) - await agentTaskService.createTask(AGENT_ID, { ...validDto, channelIds: ['channel-1', 'channel-2'] }) + await expect( + agentTaskService.createTask(AGENT_ID, { ...validDto, channelIds: ['missing-channel'] }) + ).rejects.toMatchObject({ + message: expect.stringContaining('Channel') + }) - expect(replaceTaskSubscriptionsMock).toHaveBeenCalledWith(TASK_ID, ['channel-1', 'channel-2']) - expect(dbInsertMock).not.toHaveBeenCalled() + expect(registerJobScheduleMock).not.toHaveBeenCalled() + expect(unregisterJobScheduleByIdMock).not.toHaveBeenCalled() + expect(replaceTaskSubscriptionsMock).not.toHaveBeenCalled() }) it('rolls back the registered schedule when channel subscription replacement fails', async () => { @@ -221,9 +246,9 @@ describe('AgentTaskService (thin facade)', () => { }) unregisterJobScheduleByIdMock.mockResolvedValueOnce(true) - await expect( - agentTaskService.createTask(AGENT_ID, { ...validDto, channelIds: ['missing-channel'] }) - ).rejects.toThrow(error) + await expect(agentTaskService.createTask(AGENT_ID, { ...validDto, channelIds: ['channel-1'] })).rejects.toThrow( + error + ) expect(unregisterJobScheduleByIdMock).toHaveBeenCalledWith(TASK_ID) expect(vi.mocked(jobScheduleService.getById)).not.toHaveBeenCalled() @@ -374,7 +399,7 @@ describe('AgentTaskService (thin facade)', () => { expect(patch).not.toHaveProperty('jobInputTemplate') }) - it('delegates channel replacement when channelIds are patched', async () => { + it('updates task subscriptions for channels owned by the agent', async () => { setupApplicationMocks() vi.mocked(jobScheduleService.getById) .mockReturnValueOnce(makeSnapshot()) @@ -384,10 +409,43 @@ describe('AgentTaskService (thin facade)', () => { await agentTaskService.updateTask(AGENT_ID, TASK_ID, { channelIds: ['channel-3'] }) + expect(agentChannelService.getChannel).toHaveBeenCalledWith('channel-3') expect(replaceTaskSubscriptionsMock).toHaveBeenCalledWith(TASK_ID, ['channel-3']) expect(dbDeleteMock).not.toHaveBeenCalled() }) + it('rejects a foreign channel before updating a task', async () => { + setupApplicationMocks() + vi.mocked(agentChannelService.getChannel).mockReturnValueOnce({ + id: 'foreign-channel', + agentId: OTHER_AGENT_ID + } as never) + vi.mocked(jobScheduleService.getById).mockReturnValueOnce(makeSnapshot()).mockReturnValueOnce(makeSnapshot()) + + await expect( + agentTaskService.updateTask(AGENT_ID, TASK_ID, { channelIds: ['foreign-channel'] }) + ).rejects.toMatchObject({ + message: expect.stringContaining('Channel') + }) + + expect(updateJobScheduleMock).not.toHaveBeenCalled() + expect(replaceTaskSubscriptionsMock).not.toHaveBeenCalled() + }) + + it('clears task subscriptions when channelIds is empty', async () => { + setupApplicationMocks() + vi.mocked(jobScheduleService.getById) + .mockReturnValueOnce(makeSnapshot()) + .mockReturnValueOnce(makeSnapshot()) + .mockReturnValueOnce(makeSnapshot({ id: TASK_ID })) + updateJobScheduleMock.mockReturnValueOnce(makeSnapshot()) + + await agentTaskService.updateTask(AGENT_ID, TASK_ID, { channelIds: [] }) + + expect(agentChannelService.getChannel).not.toHaveBeenCalled() + expect(replaceTaskSubscriptionsMock).toHaveBeenCalledWith(TASK_ID, []) + }) + it('returns null when the task does not exist', async () => { setupApplicationMocks() vi.mocked(jobScheduleService.getById).mockReturnValueOnce(null) diff --git a/src/renderer/components/composer/tools/definitions/permissionModeTool.tsx b/src/renderer/components/composer/tools/definitions/permissionModeTool.tsx index 07943f23f26..2d2e46c8261 100644 --- a/src/renderer/components/composer/tools/definitions/permissionModeTool.tsx +++ b/src/renderer/components/composer/tools/definitions/permissionModeTool.tsx @@ -43,11 +43,6 @@ const usePermissionModeToolController = (context: PermissionModeContext) => { const configuration = agent.configuration ?? defaultConfiguration const updatedConfiguration = { ...configuration, permission_mode: nextMode } - // Disable soul mode when switching away from bypassPermissions - if (nextMode !== 'bypassPermissions' && configuration.soul_enabled === true) { - updatedConfiguration.soul_enabled = false - } - void updateAgent({ id: agentId, configuration: updatedConfiguration }, { showSuccessToast: false }) }, [currentMode, agent, agentId, updateAgent] diff --git a/src/renderer/components/composer/variants/AgentComposer.tsx b/src/renderer/components/composer/variants/AgentComposer.tsx index aefc4c6e9c0..8374a4c2d14 100644 --- a/src/renderer/components/composer/variants/AgentComposer.tsx +++ b/src/renderer/components/composer/variants/AgentComposer.tsx @@ -37,7 +37,6 @@ import { useTopicStreamStatus } from '@renderer/hooks/useTopicStreamStatus' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import type { ThinkingOption } from '@renderer/types/reasoning' import { TopicType } from '@renderer/types/topic' -import { isSoulModeEnabled } from '@renderer/utils/agent/agentConfiguration' import { buildAgentSessionTopicId } from '@renderer/utils/agentSession' import { buildFilePartsForAttachments } from '@renderer/utils/file/buildFileParts' import { getSendMessageShortcutLabel } from '@renderer/utils/input' @@ -916,12 +915,10 @@ const AgentComposerInner = ({ [availableSkills, draftCacheKey, reconcileTokens] ) - const placeholderText = useMemo(() => { - if (isSoulModeEnabled(agentBase?.configuration)) return t('agent.input.soul_placeholder') - return t('agent.input.placeholder', { - key: getSendMessageShortcutLabel(sendMessageShortcut) - }) - }, [agentBase?.configuration, sendMessageShortcut, t]) + const placeholderText = useMemo( + () => t('agent.input.placeholder', { key: getSendMessageShortcutLabel(sendMessageShortcut) }), + [sendMessageShortcut, t] + ) const buildQueuedPayload = useCallback( (draft: ComposerSerializedDraft): ComposerQueuedMessagePayload | null => diff --git a/src/renderer/components/composer/variants/__tests__/AgentComposer.test.tsx b/src/renderer/components/composer/variants/__tests__/AgentComposer.test.tsx index 86e4e39e468..61c6b04ce01 100644 --- a/src/renderer/components/composer/variants/__tests__/AgentComposer.test.tsx +++ b/src/renderer/components/composer/variants/__tests__/AgentComposer.test.tsx @@ -389,8 +389,7 @@ vi.mock('@renderer/components/resourceCatalog/dialogs/edit', () => ({ })) vi.mock('@renderer/pages/agents/AgentSettings/shared', () => ({ - AgentLabel: ({ agent }: any) => {agent.name}, - isSoulModeEnabled: () => false + AgentLabel: ({ agent }: any) => {agent.name} })) vi.mock('@renderer/data/hooks/usePreference', () => ({ diff --git a/src/renderer/components/resourceCatalog/dialogs/edit/AgentEditDialog.tsx b/src/renderer/components/resourceCatalog/dialogs/edit/AgentEditDialog.tsx index db1ddced4d3..4ae8fe29901 100644 --- a/src/renderer/components/resourceCatalog/dialogs/edit/AgentEditDialog.tsx +++ b/src/renderer/components/resourceCatalog/dialogs/edit/AgentEditDialog.tsx @@ -68,7 +68,6 @@ type AgentEditFormValues = { disabledTools: string[] permissionMode: string envVarsText: string - soulEnabled: boolean heartbeatEnabled: boolean heartbeatInterval: number } @@ -124,7 +123,6 @@ function defaultValuesForAgent(resource: AgentDetail): AgentEditFormValues { disabledTools: [...form.disabledTools], permissionMode: form.permissionMode, envVarsText: form.envVarsText, - soulEnabled: form.soulEnabled, heartbeatEnabled: form.heartbeatEnabled, heartbeatInterval: form.heartbeatInterval } @@ -152,7 +150,6 @@ function buildAgentFormState(baseline: AgentFormState, values: AgentEditFormValu disabledTools: values.disabledTools, permissionMode: values.permissionMode, envVarsText: values.envVarsText, - soulEnabled: values.soulEnabled, heartbeatEnabled: values.heartbeatEnabled, heartbeatInterval: values.heartbeatInterval } @@ -165,7 +162,6 @@ function syncAgentFormState(form: UseFormReturn, next: Agen form.setValue('mcps', next.mcps, { shouldDirty: true }) form.setValue('disabledTools', next.disabledTools, { shouldDirty: true }) form.setValue('permissionMode', next.permissionMode, { shouldDirty: true }) - form.setValue('soulEnabled', next.soulEnabled, { shouldDirty: true }) form.setValue('heartbeatEnabled', next.heartbeatEnabled, { shouldDirty: true }) form.setValue('heartbeatInterval', next.heartbeatInterval, { shouldDirty: true }) } @@ -342,7 +338,6 @@ function AgentBasicFields({ }) { const { t } = useTranslation() const heartbeatEnabled = form.watch('heartbeatEnabled') - const soulEnabled = form.watch('soulEnabled') return (
@@ -402,10 +397,7 @@ function AgentBasicFields({ label={t('library.config.agent.field.description.label')} placeholder={t('library.config.agent.field.description.placeholder')} /> - - {!soulEnabled && ( - - )} + - patchAgentForm: (patch: Partial) => void -}) { - const { t } = useTranslation() - const label = t('library.config.agent.field.soul_enabled.label') - - return ( - ( - -
- - patchAgentForm({ soulEnabled: checked })} - /> -
- -
- )} - /> - ) -} - function PermissionModeField({ form, portalContainer, diff --git a/src/renderer/components/resourceCatalog/dialogs/edit/__tests__/EditDialogs.test.tsx b/src/renderer/components/resourceCatalog/dialogs/edit/__tests__/EditDialogs.test.tsx index 80b8f13823b..6255ce8a246 100644 --- a/src/renderer/components/resourceCatalog/dialogs/edit/__tests__/EditDialogs.test.tsx +++ b/src/renderer/components/resourceCatalog/dialogs/edit/__tests__/EditDialogs.test.tsx @@ -211,8 +211,6 @@ vi.mock('react-i18next', async (importOriginal) => { useTranslation: () => ({ t: (key: string, fallback?: string) => ({ - 'agent.cherryClaw.heartbeat.enabledHelper': 'Send heartbeat messages.', - 'agent.cherryClaw.heartbeat.intervalHelper': 'Heartbeat interval.', 'agent.settings.tooling.preapproved.autoBadge': 'Added by mode', 'agent.settings.tooling.preapproved.autoDisabledTooltip': 'Added by {{mode}}', 'common.avatar': 'Avatar', @@ -246,8 +244,6 @@ vi.mock('react-i18next', async (importOriginal) => { 'library.config.agent.field.plan_model.label': 'Plan model', 'library.config.agent.field.small_model.hint': 'Small model.', 'library.config.agent.field.small_model.label': 'Small model', - 'library.config.agent.field.soul_enabled.help': 'Use workspace soul files and autonomous tools.', - 'library.config.agent.field.soul_enabled.label': 'Autonomous mode', 'library.config.agent.field.instructions.label': 'Instructions', 'library.config.agent.field.instructions.placeholder': 'Tell this agent how to work', 'library.config.agent.field.env_vars.help': 'One KEY=VALUE per line', @@ -426,7 +422,6 @@ const AGENT: AgentDetail = { mcps: [], configuration: { avatar: '🤖', - soul_enabled: false, heartbeat_enabled: true, heartbeat_interval: 30 }, @@ -858,29 +853,6 @@ describe('edit dialogs', () => { ) }) - it('hides permission mode while autonomous mode is enabled', async () => { - render() - - expect(screen.getByRole('combobox', { name: 'Permission mode' })).toBeInTheDocument() - - fireEvent.click(screen.getByRole('switch', { name: 'Autonomous mode' })) - - expect(screen.queryByRole('combobox', { name: 'Permission mode' })).not.toBeInTheDocument() - - fireEvent.click(screen.getByRole('button', { name: 'Save' })) - - await waitFor(() => - expect(updateAgentMock).toHaveBeenCalledWith({ - body: expect.objectContaining({ - configuration: expect.objectContaining({ - permission_mode: 'bypassPermissions', - soul_enabled: true - }) - }) - }) - ) - }) - it('shows agent tool categories directly in the left tab list', async () => { render() diff --git a/src/renderer/components/resourceCatalog/selectors/AgentSelector.tsx b/src/renderer/components/resourceCatalog/selectors/AgentSelector.tsx index 547d09456da..e0f49ca765b 100644 --- a/src/renderer/components/resourceCatalog/selectors/AgentSelector.tsx +++ b/src/renderer/components/resourceCatalog/selectors/AgentSelector.tsx @@ -164,8 +164,7 @@ export function AgentSelector(props: AgentSelectorProps) { skillIds: values.skillIds, configuration: { avatar: values.avatar, - permission_mode: 'bypassPermissions', - soul_enabled: true + permission_mode: 'bypassPermissions' } } }) diff --git a/src/renderer/components/resourceCatalog/selectors/__tests__/AgentSelector.test.tsx b/src/renderer/components/resourceCatalog/selectors/__tests__/AgentSelector.test.tsx index 985f54f26ab..69e7f6ec30e 100644 --- a/src/renderer/components/resourceCatalog/selectors/__tests__/AgentSelector.test.tsx +++ b/src/renderer/components/resourceCatalog/selectors/__tests__/AgentSelector.test.tsx @@ -123,8 +123,6 @@ vi.mock('react-i18next', async (importOriginal) => { 'common.name': 'Name', 'common.required_field': 'Required', 'common.save': 'Save', - 'agent.cherryClaw.heartbeat.enabledHelper': 'Send heartbeat messages.', - 'agent.cherryClaw.heartbeat.intervalHelper': 'Heartbeat interval.', 'agent.edit.title': 'Edit agent', 'library.config.agent.field.description.hint': 'Short agent summary.', 'library.config.agent.field.description.label': 'Description', @@ -142,8 +140,6 @@ vi.mock('react-i18next', async (importOriginal) => { 'library.config.agent.field.plan_model.label': 'Plan model', 'library.config.agent.field.small_model.hint': 'Small model.', 'library.config.agent.field.small_model.label': 'Small model', - 'library.config.agent.field.soul_enabled.help': 'Use soul.md.', - 'library.config.agent.field.soul_enabled.label': 'Soul', 'library.config.basic.model_clear': 'Clear', 'library.config.basic.model_not_found': 'Model {{id}} is unavailable.', 'library.config.basic.model_pick': 'Pick model', @@ -199,7 +195,6 @@ const AGENTS_RESPONSE = { allowedTools: [], configuration: { avatar: '🤖', - soul_enabled: false, heartbeat_enabled: true, heartbeat_interval: 30 }, @@ -221,7 +216,6 @@ const AGENTS_RESPONSE = { allowedTools: [], configuration: { avatar: '🤖', - soul_enabled: false, heartbeat_enabled: true, heartbeat_interval: 30 }, @@ -498,8 +492,7 @@ describe('AgentSelector', () => { skillIds: [], configuration: { avatar: '🤖', - permission_mode: 'bypassPermissions', - soul_enabled: true + permission_mode: 'bypassPermissions' } } }) diff --git a/src/renderer/hooks/agent/useAgent.ts b/src/renderer/hooks/agent/useAgent.ts index b44e04c83b9..bc96794d389 100644 --- a/src/renderer/hooks/agent/useAgent.ts +++ b/src/renderer/hooks/agent/useAgent.ts @@ -33,7 +33,7 @@ export const useAgent = (id: string | null) => { params: { agentId: id! }, enabled: !!id, swrOptions: { - // Agent config may be modified externally (e.g. claw MCP tool in main process), + // Agent config may be modified externally (e.g. cherry MCP tool in main process), // so always revalidate on mount and reduce dedup window to get fresh data. revalidateOnMount: true, dedupingInterval: 2000, diff --git a/src/renderer/hooks/agent/useChannels.ts b/src/renderer/hooks/agent/useChannels.ts index 0263b8536e5..90fd26443ea 100644 --- a/src/renderer/hooks/agent/useChannels.ts +++ b/src/renderer/hooks/agent/useChannels.ts @@ -29,7 +29,7 @@ export const useChannels = (type?: AgentChannelType) => { return await createTrigger({ body: channelData }) } catch (err) { logger.error('Failed to create channel', err as Error) - window.toast.error(formatErrorMessageWithPrefix(err, t('agent.cherryClaw.channels.createError'))) + window.toast.error(formatErrorMessageWithPrefix(err, t('agent.channels.createError'))) return null } }, @@ -45,7 +45,7 @@ export const useChannels = (type?: AgentChannelType) => { return await updateTrigger({ params: { channelId: id }, body: updates as never }) } catch (err) { logger.error('Failed to update channel', err as Error) - window.toast.error(formatErrorMessageWithPrefix(err, t('agent.cherryClaw.channels.updateError'))) + window.toast.error(formatErrorMessageWithPrefix(err, t('agent.channels.updateError'))) return null } }, @@ -61,7 +61,7 @@ export const useChannels = (type?: AgentChannelType) => { await deleteTrigger({ params: { channelId: id } }) } catch (err) { logger.error('Failed to delete channel', err as Error) - window.toast.error(formatErrorMessageWithPrefix(err, t('agent.cherryClaw.channels.deleteError'))) + window.toast.error(formatErrorMessageWithPrefix(err, t('agent.channels.deleteError'))) } }, [deleteTrigger, t] diff --git a/src/renderer/hooks/agent/useTasks.ts b/src/renderer/hooks/agent/useTasks.ts index 66d8786d42f..e79ce63248e 100644 --- a/src/renderer/hooks/agent/useTasks.ts +++ b/src/renderer/hooks/agent/useTasks.ts @@ -33,7 +33,7 @@ export const useCreateTask = () => { return result as unknown as ScheduledTaskEntity } catch (error) { window.toast.error( - formatErrorMessageWithPrefix(error, t('agent.cherryClaw.tasks.error.createFailed', 'Failed to create task')) + formatErrorMessageWithPrefix(error, t('agent.tasks.error.createFailed', 'Failed to create task')) ) return undefined } @@ -56,7 +56,7 @@ export const useUpdateTask = () => { return result as unknown as ScheduledTaskEntity } catch (error) { window.toast.error( - formatErrorMessageWithPrefix(error, t('agent.cherryClaw.tasks.error.updateFailed', 'Failed to update task')) + formatErrorMessageWithPrefix(error, t('agent.tasks.error.updateFailed', 'Failed to update task')) ) return undefined } @@ -72,12 +72,10 @@ export const useRunTask = () => { async (taskId: string): Promise => { try { await ipcApi.request('ai.run_agent_task', taskId) - window.toast.success({ key: 'run-task', title: t('agent.cherryClaw.tasks.runTriggered') }) + window.toast.success({ key: 'run-task', title: t('agent.tasks.runTriggered') }) return true } catch (error) { - window.toast.error( - formatErrorMessageWithPrefix(error, t('agent.cherryClaw.tasks.error.runFailed', 'Failed to run task')) - ) + window.toast.error(formatErrorMessageWithPrefix(error, t('agent.tasks.error.runFailed', 'Failed to run task'))) return false } }, @@ -99,7 +97,7 @@ export const useDeleteTask = () => { return true } catch (error) { window.toast.error( - formatErrorMessageWithPrefix(error, t('agent.cherryClaw.tasks.error.deleteFailed', 'Failed to delete task')) + formatErrorMessageWithPrefix(error, t('agent.tasks.error.deleteFailed', 'Failed to delete task')) ) return false } diff --git a/src/renderer/hooks/resourceCatalog/__tests__/useResourceCatalogController.test.tsx b/src/renderer/hooks/resourceCatalog/__tests__/useResourceCatalogController.test.tsx index fdcca6e4459..59d7e0adad8 100644 --- a/src/renderer/hooks/resourceCatalog/__tests__/useResourceCatalogController.test.tsx +++ b/src/renderer/hooks/resourceCatalog/__tests__/useResourceCatalogController.test.tsx @@ -142,8 +142,7 @@ describe('useResourceCatalogController', () => { expect(controllerMocks.createAgent).toHaveBeenCalledWith({ configuration: { avatar: createValues.avatar, - permission_mode: 'bypassPermissions', - soul_enabled: true + permission_mode: 'bypassPermissions' }, description: createValues.description, instructions: createValues.prompt, diff --git a/src/renderer/hooks/resourceCatalog/useResourceCatalogController.ts b/src/renderer/hooks/resourceCatalog/useResourceCatalogController.ts index d835851e8ee..4847da8b502 100644 --- a/src/renderer/hooks/resourceCatalog/useResourceCatalogController.ts +++ b/src/renderer/hooks/resourceCatalog/useResourceCatalogController.ts @@ -218,8 +218,7 @@ export function useResourceCatalogController(resourceType: ResourceCatalogContro skillIds: values.skillIds, configuration: { avatar: values.avatar, - permission_mode: 'bypassPermissions', - soul_enabled: true + permission_mode: 'bypassPermissions' } }) } diff --git a/src/renderer/i18n/locales/en-us.json b/src/renderer/i18n/locales/en-us.json index 3140f4648ac..32d3d96b2e2 100644 --- a/src/renderer/i18n/locales/en-us.json +++ b/src/renderer/i18n/locales/en-us.json @@ -31,262 +31,113 @@ "submit": "Submit", "title": "Questions from Agent" }, - "cherryClaw": { - "channels": { - "add": "Add", - "bindAgent": "Bind Agent", - "chatIdsAutoTrackHint": "When left empty, the system will auto-track: you need to send a message to the Bot on the platform first, then the system will record the Chat ID for future notifications.", - "comingSoon": "Coming soon", - "connected": "Connected", - "connecting": "Connecting", - "createError": "Failed to create channel", - "deleteConfirm": "Delete channel \"{{name}}\"?", - "deleteError": "Failed to delete channel", - "description": "Connect your agent to messaging platforms.", - "disconnected": "Disconnected", - "discord": { - "botToken": "Bot Token", - "botTokenPlaceholder": "Enter your Discord bot token", - "channelIds": "Allowed Channel IDs", - "channelIdsHint": "Format: channel:id or dm:id. Leave empty to allow all.", - "channelIdsPlaceholder": "channel:123456789, dm:987654321", - "description": "Receive and respond to messages via a Discord bot using WebSocket gateway.", - "title": "Discord", - "whoamiTip": "💡 Tip: Send /whoami to the bot to get your channel ID in the correct format." - }, - "error": "Error", - "feishu": { - "appId": "App ID", - "appIdPlaceholder": "Enter your Feishu app ID", - "appSecret": "App Secret", - "appSecretPlaceholder": "Enter your Feishu app secret", - "chatIds": "Allowed Chat IDs", - "chatIdsHint": "Comma-separated chat IDs. Leave empty to allow all chats.", - "chatIdsPlaceholder": "oc_xxxxx, oc_yyyyy", - "connected": "Connected", - "description": "Receive and respond to messages via a Feishu/Lark bot using WebSocket.", - "domain": "Domain", - "domainFeishu": "Feishu (China)", - "domainLark": "Lark (International)", - "encryptKey": "Encrypt Key", - "encryptKeyPlaceholder": "Enter the Encrypt Key from your Feishu app", - "loginHint": "No credentials configured. Enable the channel to start QR code registration, or enter App ID and App Secret manually.", - "qrExpired": "QR code expired. Please retry by toggling the channel.", - "qrHint": "Waiting for QR code scan...", - "qrScanHint": "Open Feishu on your phone and scan the QR code to create a bot app.", - "qrTitle": "Feishu QR Registration", - "title": "Feishu", - "verificationToken": "Verification Token", - "verificationTokenPlaceholder": "Enter the Verification Token from your Feishu app" - }, - "logs": "Logs", - "noInstances": "No {{type}} channels configured. Click \"+ Add\" to create one.", - "noLogs": "No logs yet", - "notifyReceiver": "Receive task notifications", - "notifyReceiverHint": "Send scheduler task results to this channel.", - "qq": { - "appId": "App ID", - "appIdPlaceholder": "Enter your QQ Bot App ID", - "chatIds": "Allowed Chat IDs", - "chatIdsHint": "Format: c2c:openid, group:groupid, channel:channelid. Leave empty to allow all.", - "chatIdsPlaceholder": "c2c:abc123, group:xyz789", - "clientSecret": "Client Secret", - "clientSecretPlaceholder": "Enter your QQ Bot client secret", - "description": "Receive and respond to messages via QQ Bot official API.", - "title": "QQ", - "whoamiTip": "💡 Tip: Send /whoami to the bot to get your chat ID in the correct format." - }, - "security": { - "inheritFromAgent": "Inherit from agent", - "permissionMode": "Channel Permission Mode", - "permissionModeHint": "Override the agent's permission mode for messages from this channel. \"Inherit\" uses the agent's default." - }, - "selectAgent": "Select an agent to bind", - "slack": { - "appToken": "App-Level Token", - "appTokenPlaceholder": "xapp-...", - "botToken": "Bot Token", - "botTokenPlaceholder": "xoxb-...", - "channelIds": "Allowed Channel IDs", - "channelIdsHint": "Slack channel IDs. Leave empty to allow all.", - "channelIdsPlaceholder": "C01234567, D89012345", - "description": "Receive and respond to messages via a Slack bot using Socket Mode.", - "title": "Slack", - "whoamiTip": "💡 Tip: Send /whoami to the bot to get the channel ID." - }, - "soulModeRequired": "This agent does not have Autonomous Mode enabled. Channel features will not work. Please enable it in agent settings.", - "tab": "Channels", - "telegram": { - "botToken": "Bot Token", - "botTokenPlaceholder": "Enter your Telegram bot token", - "chatIds": "Allowed Chat IDs", - "chatIdsHint": "Comma-separated. Leave empty to allow all chats.", - "chatIdsPlaceholder": "123456789, 987654321", - "description": "Receive and respond to messages via a Telegram bot using long polling.", - "title": "Telegram" - }, - "title": "Channels", - "updateError": "Failed to update channel", - "wechat": { - "addAccount": "Add WeChat Account", - "chatIds": "Allowed User IDs", - "chatIdsHint": "Comma-separated. Leave empty to allow all users.", - "chatIdsPlaceholder": "wxid_abc123, wxid_def456", - "connected": "Logged in", - "description": "Receive and respond to messages via WeChat using the iLink Bot API.", - "disconnected": "Not logged in", - "loginHint": "First login requires scanning a QR code. A QR code will appear automatically when the channel connects.", - "qrHint": "Open WeChat on your phone, scan the QR code to log in.", - "qrTitle": "WeChat QR Login", - "title": "WeChat", - "whoamiTip": "Tip: Send /whoami in WeChat to get a user's ID." - } - }, - "heartbeat": { - "enabled": "Enable Heartbeat", - "enabledHelper": "When enabled and heartbeat.md exists in the workspace, the scheduler reads its content and sends it to the main session at the configured interval.", - "interval": "Interval (minutes)", - "intervalHelper": "How often the heartbeat runs, in minutes." - }, - "sandbox": { - "description": "Restricts the agent's filesystem and network access to the workspace directory using OS-level sandboxing.", - "helper": "Restrict filesystem and command access to the workspace path.", - "label": "Sandbox Mode" - }, - "scheduler": { - "cron": { - "helper": "Standard cron expression (e.g., '*/30 * * * *' for every 30 minutes).", - "label": "Cron Expression", - "placeholder": "*/30 * * * *" - }, - "description": "The scheduler triggers the agent autonomously on a schedule.", - "enabled": "Enable Scheduler", - "enabledHelper": "When enabled, the agent will run automatically based on the configured schedule.", - "errors": { - "invalidCron": "Invalid cron expression", - "invalidDelay": "Delay must be a positive number", - "invalidInterval": "Interval must be a positive number" - }, - "interval": { - "helper": "How often the agent should run, in seconds.", - "label": "Interval (seconds)" - }, - "lastRun": "Last Run", - "never": "Never", - "nextRun": "Next Run", - "oneTime": { - "helper": "One-time delay in seconds before the agent runs.", - "label": "Delay (seconds)" - }, - "status": { - "running": "Running", - "stopped": "Stopped", - "tickInProgress": "Tick in progress" - }, - "tab": "Scheduler", - "title": "Scheduler Configuration", - "type": { - "cron": "Cron Expression", - "interval": "Fixed Interval", - "label": "Schedule Type", - "one-time": "One-Time Delay" - } + "channels": { + "add": "Add", + "bindAgent": "Bind Agent", + "chatIdsAutoTrackHint": "When left empty, the system will auto-track: you need to send a message to the Bot on the platform first, then the system will record the Chat ID for future notifications.", + "comingSoon": "Coming soon", + "connected": "Connected", + "connecting": "Connecting", + "createError": "Failed to create channel", + "deleteConfirm": "Delete channel \"{{name}}\"?", + "deleteError": "Failed to delete channel", + "description": "Connect your agent to messaging platforms.", + "disconnected": "Disconnected", + "discord": { + "botToken": "Bot Token", + "botTokenPlaceholder": "Enter your Discord bot token", + "channelIds": "Allowed Channel IDs", + "channelIdsHint": "Format: channel:id or dm:id. Leave empty to allow all.", + "channelIdsPlaceholder": "channel:123456789, dm:987654321", + "description": "Receive and respond to messages via a Discord bot using WebSocket gateway.", + "title": "Discord", + "whoamiTip": "💡 Tip: Send /whoami to the bot to get your channel ID in the correct format." }, - "soul": { - "description": "The soul defines the agent's personality and base behavior. It is read from soul.md in the workspace root.", - "empty": "No soul.md found in the workspace root. Create one to define the agent's personality.", - "enabled": "Enable Soul", - "enabledHelper": "When enabled, the agent reads soul.md from the workspace root and prepends it to the system prompt.", - "fileLabel": "soul.md", - "preview": "Soul Preview", - "tab": "Soul", - "title": "Soul Configuration" - }, - "tasks": { - "add": "Add Task", - "cancel": "Cancel", - "channels": { - "label": "Send to Channels", - "noActiveChatIds": "The selected channels have no available recipients (Chat ID). Task results may not be delivered. Please send a message to the Bot on the platform first.", - "placeholder": "Select channels to receive results" - }, - "cronPlaceholder": "e.g. 0 9 * * * (every day at 9 AM)", - "delete": { - "confirm": "Are you sure you want to delete this task?", - "label": "Delete" - }, - "edit": "Edit", - "empty": "No scheduled tasks. Add one to get started.", - "error": { - "createFailed": "Failed to create task", - "deleteFailed": "Failed to delete task", - "loadFailed": "Failed to load tasks", - "runFailed": "Failed to run task", - "updateFailed": "Failed to update task" - }, - "frequency": { - "everyPrefix": "Every", - "everySuffix": "minutes", - "label": "Execution Frequency" - }, - "intervalPlaceholder": "At least 1", - "intervalUnit": "minutes", - "lastRun": "Last Run", - "logs": { - "cancelled": "Cancelled", - "completed": "Completed", - "duration": "Duration", - "empty": "No run history yet.", - "failed": "Failed", - "justNow": "just now", - "label": "Run History", - "loadError": "Failed to load run history", - "result": "Result", - "runAt": "Run At", - "running": "Running...", - "search": "Search logs...", - "status": "Status", - "viewSession": "View session" - }, - "name": { - "label": "Name", - "placeholder": "e.g. Daily code review" - }, - "nextRun": "Next Run", - "oncePlaceholder": "Select date and time", - "pause": "Pause", - "prompt": { - "expand": "Expand editor", - "label": "Prompt", - "placeholder": "What should the agent do when this task runs?" - }, - "resume": "Resume", - "run": "Run", - "runTriggered": "Task triggered", - "save": "Save", - "scheduleType": { - "cron": "Cron", - "interval": "Interval", - "once": "Once" - }, - "status": { - "active": "Active", - "completed": "Completed", - "paused": "Paused" - }, - "tab": "Tasks", - "time": { - "hoursAgo": "{{count}}h ago", - "minutesAgo": "{{count}}m ago" - }, - "timeout": { - "label": "Maximum Execution Time", - "placeholder": "No limit" - }, - "title": "Scheduled Tasks" + "error": "Error", + "feishu": { + "appId": "App ID", + "appIdPlaceholder": "Enter your Feishu app ID", + "appSecret": "App Secret", + "appSecretPlaceholder": "Enter your Feishu app secret", + "chatIds": "Allowed Chat IDs", + "chatIdsHint": "Comma-separated chat IDs. Leave empty to allow all chats.", + "chatIdsPlaceholder": "oc_xxxxx, oc_yyyyy", + "connected": "Connected", + "description": "Receive and respond to messages via a Feishu/Lark bot using WebSocket.", + "domain": "Domain", + "domainFeishu": "Feishu (China)", + "domainLark": "Lark (International)", + "encryptKey": "Encrypt Key", + "encryptKeyPlaceholder": "Enter the Encrypt Key from your Feishu app", + "loginHint": "No credentials configured. Enable the channel to start QR code registration, or enter App ID and App Secret manually.", + "qrExpired": "QR code expired. Please retry by toggling the channel.", + "qrHint": "Waiting for QR code scan...", + "qrScanHint": "Open Feishu on your phone and scan the QR code to create a bot app.", + "qrTitle": "Feishu QR Registration", + "title": "Feishu", + "verificationToken": "Verification Token", + "verificationTokenPlaceholder": "Enter the Verification Token from your Feishu app" }, - "warning": { - "bypassPermissions": "CherryClaw agents run with Full Auto Mode by default. All tools execute without asking for approval." + "logs": "Logs", + "noInstances": "No {{type}} channels configured. Click \"+ Add\" to create one.", + "noLogs": "No logs yet", + "notifyReceiver": "Receive task notifications", + "notifyReceiverHint": "Send scheduler task results to this channel.", + "qq": { + "appId": "App ID", + "appIdPlaceholder": "Enter your QQ Bot App ID", + "chatIds": "Allowed Chat IDs", + "chatIdsHint": "Format: c2c:openid, group:groupid, channel:channelid. Leave empty to allow all.", + "chatIdsPlaceholder": "c2c:abc123, group:xyz789", + "clientSecret": "Client Secret", + "clientSecretPlaceholder": "Enter your QQ Bot client secret", + "description": "Receive and respond to messages via QQ Bot official API.", + "title": "QQ", + "whoamiTip": "💡 Tip: Send /whoami to the bot to get your chat ID in the correct format." + }, + "security": { + "inheritFromAgent": "Inherit from agent", + "permissionMode": "Channel Permission Mode", + "permissionModeHint": "Override the agent's permission mode for messages from this channel. \"Inherit\" uses the agent's default." + }, + "selectAgent": "Select an agent to bind", + "slack": { + "appToken": "App-Level Token", + "appTokenPlaceholder": "xapp-...", + "botToken": "Bot Token", + "botTokenPlaceholder": "xoxb-...", + "channelIds": "Allowed Channel IDs", + "channelIdsHint": "Slack channel IDs. Leave empty to allow all.", + "channelIdsPlaceholder": "C01234567, D89012345", + "description": "Receive and respond to messages via a Slack bot using Socket Mode.", + "title": "Slack", + "whoamiTip": "💡 Tip: Send /whoami to the bot to get the channel ID." + }, + "tab": "Channels", + "telegram": { + "botToken": "Bot Token", + "botTokenPlaceholder": "Enter your Telegram bot token", + "chatIds": "Allowed Chat IDs", + "chatIdsHint": "Comma-separated. Leave empty to allow all chats.", + "chatIdsPlaceholder": "123456789, 987654321", + "description": "Receive and respond to messages via a Telegram bot using long polling.", + "title": "Telegram" + }, + "title": "Channels", + "updateError": "Failed to update channel", + "wechat": { + "addAccount": "Add WeChat Account", + "chatIds": "Allowed User IDs", + "chatIdsHint": "Comma-separated. Leave empty to allow all users.", + "chatIdsPlaceholder": "wxid_abc123, wxid_def456", + "connected": "Logged in", + "description": "Receive and respond to messages via WeChat using the iLink Bot API.", + "disconnected": "Not logged in", + "loginHint": "First login requires scanning a QR code. A QR code will appear automatically when the channel connects.", + "qrHint": "Open WeChat on your phone, scan the QR code to log in.", + "qrTitle": "WeChat QR Login", + "title": "WeChat", + "whoamiTip": "Tip: Send /whoami in WeChat to get a user's ID." } }, "delete": { @@ -343,8 +194,7 @@ "type": "Agent Icon" }, "input": { - "placeholder": "Type a message. Press {{key}} to send. Type / to search paths or commands.", - "soul_placeholder": "Help me connect to Telegram" + "placeholder": "Type a message. Press {{key}} to send. Type / to search paths or commands." }, "list": { "error": { @@ -718,12 +568,6 @@ "tab": "Skills", "title": "Installed Skills" }, - "soulMode": { - "description": "When enabled, the agent uses a custom system prompt from workspace soul files, injects autonomous task management tools, and disables interactive tools not suited for autonomous operation.", - "disabledBadge": "Disabled by Autonomous Mode", - "disabledTooltip": "This tool is automatically disabled when Autonomous Mode is active", - "title": "Autonomous Mode" - }, "tooling": { "mcp": { "description": "Connect MCP servers to unlock additional tools you can approve above.", @@ -795,6 +639,89 @@ "title": "Manage skills" } }, + "tasks": { + "add": "Add Task", + "cancel": "Cancel", + "channels": { + "label": "Send to Channels", + "noActiveChatIds": "The selected channels have no available recipients (Chat ID). Task results may not be delivered. Please send a message to the Bot on the platform first.", + "placeholder": "Select channels to receive results" + }, + "cronPlaceholder": "e.g. 0 9 * * * (every day at 9 AM)", + "delete": { + "confirm": "Are you sure you want to delete this task?", + "label": "Delete" + }, + "edit": "Edit", + "empty": "No scheduled tasks. Add one to get started.", + "error": { + "createFailed": "Failed to create task", + "deleteFailed": "Failed to delete task", + "loadFailed": "Failed to load tasks", + "runFailed": "Failed to run task", + "updateFailed": "Failed to update task" + }, + "frequency": { + "everyPrefix": "Every", + "everySuffix": "minutes", + "label": "Execution Frequency" + }, + "intervalPlaceholder": "At least 1", + "intervalUnit": "minutes", + "lastRun": "Last Run", + "logs": { + "cancelled": "Cancelled", + "completed": "Completed", + "duration": "Duration", + "empty": "No run history yet.", + "failed": "Failed", + "justNow": "just now", + "label": "Run History", + "loadError": "Failed to load run history", + "result": "Result", + "runAt": "Run At", + "running": "Running...", + "search": "Search logs...", + "status": "Status", + "viewSession": "View session" + }, + "name": { + "label": "Name", + "placeholder": "e.g. Daily code review" + }, + "nextRun": "Next Run", + "oncePlaceholder": "Select date and time", + "pause": "Pause", + "prompt": { + "expand": "Expand editor", + "label": "Prompt", + "placeholder": "What should the agent do when this task runs?" + }, + "resume": "Resume", + "run": "Run", + "runTriggered": "Task triggered", + "save": "Save", + "scheduleType": { + "cron": "Cron", + "interval": "Interval", + "once": "Once" + }, + "status": { + "active": "Active", + "completed": "Completed", + "paused": "Paused" + }, + "tab": "Tasks", + "time": { + "hoursAgo": "{{count}}h ago", + "minutesAgo": "{{count}}m ago" + }, + "timeout": { + "label": "Maximum Execution Time", + "placeholder": "No limit" + }, + "title": "Scheduled Tasks" + }, "todo": { "mock": { "actions": { @@ -946,6 +873,14 @@ "description": "Executes shell commands in your environment", "label": "Bash" }, + "CherryConfig": { + "description": "Inspects and manages this agent configuration and channels", + "label": "Agent Config" + }, + "CherryCron": { + "description": "Manages the in-app scheduler", + "label": "Scheduler" + }, "CherryKbManage": { "description": "Adds, deletes, or refreshes documents in your knowledge bases", "label": "Manage Knowledge" @@ -954,6 +889,10 @@ "description": "Searches your knowledge bases", "label": "Knowledge Search" }, + "CherryNotify": { + "description": "Sends a notification through a connected channel", + "label": "Notify" + }, "CherryWebFetch": { "description": "Fetches and reads a web page", "label": "Web Fetch" @@ -962,10 +901,6 @@ "description": "Searches the web via your configured provider", "label": "Web Search" }, - "ClawCron": { - "description": "Manages the in-app scheduler", - "label": "Scheduler" - }, "Edit": { "description": "Makes targeted edits to specific files", "label": "Edit" @@ -2998,11 +2933,10 @@ "placeholder": "Give the agent a name" }, "permission_mode": { - "help": "Switching to bypassPermissions is equivalent to enabling Soul mode", "label": "Permission mode", "option": { "acceptEdits": "Accept edits", - "bypassPermissions": "Bypass permissions (Soul)", + "bypassPermissions": "Bypass permissions", "default": "Default", "plan": "Plan mode" } @@ -3014,10 +2948,6 @@ "small_model": { "hint": "Lightweight checks and formatting", "label": "Small model" - }, - "soul_enabled": { - "help": "When enabled, automatically grants bypassPermissions", - "label": "Soul mode" } }, "model_config": "Model configuration", @@ -7257,7 +7187,7 @@ }, "scheduledTasks": { "description": "Manage scheduled tasks across all agents. Tasks run automatically on the configured schedule.", - "noAgents": "Scheduled tasks require agents with Soul Mode or Bypass Permissions enabled. Update an agent's settings to get started.", + "noAgents": "No agents found. Create an agent first to add scheduled tasks.", "noAgentsTip": "Tip: You can also ask your agent to create scheduled tasks via chat.", "noTasks": "No scheduled tasks. Click \"+ Add\" to create one for an agent.", "selectTask": "Select a task to view details", diff --git a/src/renderer/i18n/locales/zh-cn.json b/src/renderer/i18n/locales/zh-cn.json index bca2f705c30..3cfe8075aa2 100644 --- a/src/renderer/i18n/locales/zh-cn.json +++ b/src/renderer/i18n/locales/zh-cn.json @@ -31,262 +31,113 @@ "submit": "提交", "title": "来自智能体的问题" }, - "cherryClaw": { - "channels": { - "add": "添加", - "bindAgent": "绑定智能体", - "chatIdsAutoTrackHint": "留空时系统将自动追踪:需要先在对应平台上主动给 Bot 发送一条消息,系统才会记录 Chat ID 用于后续通知。", - "comingSoon": "即将推出", - "connected": "已连接", - "connecting": "连接中", - "createError": "", - "deleteConfirm": "确定删除频道「{{name}}」?", - "deleteError": "", - "description": "将您的智能体连接到消息平台。", - "disconnected": "已断开", - "discord": { - "botToken": "Bot Token", - "botTokenPlaceholder": "输入您的 Discord 机器人 Token", - "channelIds": "允许的频道 ID", - "channelIdsHint": "格式:channel:id 或 dm:id。留空表示允许所有。", - "channelIdsPlaceholder": "channel:123456789, dm:987654321", - "description": "通过 Discord 机器人使用 WebSocket 网关接收和回复消息。", - "title": "Discord", - "whoamiTip": "💡 提示:发送 /whoami 给机器人即可获取正确格式的频道 ID。" - }, - "error": "错误", - "feishu": { - "appId": "应用ID", - "appIdPlaceholder": "输入你的飞书应用 ID", - "appSecret": "应用密钥", - "appSecretPlaceholder": "输入你的飞书应用密钥", - "chatIds": "允许的聊天 ID", - "chatIdsHint": "逗号分隔的聊天 ID。留空则允许所有聊天。", - "chatIdsPlaceholder": "oc_xxxxx,oc_yyyyy", - "connected": "已连接", - "description": "通过 WebSocket 使用飞书/ Lark 机器人接收并回复消息。", - "domain": "域名", - "domainFeishu": "飞书(中国)", - "domainLark": "飞书(国际版)", - "encryptKey": "加密密钥", - "encryptKeyPlaceholder": "请输入来自你飞书应用的加密密钥", - "loginHint": "未配置凭证。启用频道后将自动开始扫码注册,或手动输入 App ID 和 App Secret。", - "qrExpired": "二维码已过期,请重新切换频道开关重试。", - "qrHint": "等待扫描二维码...", - "qrScanHint": "打开手机飞书,扫描二维码以创建机器人应用。", - "qrTitle": "飞书扫码注册", - "title": "飞书", - "verificationToken": "验证令牌", - "verificationTokenPlaceholder": "输入您飞书应用中的验证令牌" - }, - "logs": "日志", - "noInstances": "暂无 {{type}} 频道,点击「+ 添加」创建。", - "noLogs": "暂无日志", - "notifyReceiver": "接收任务通知", - "notifyReceiverHint": "将定时任务结果发送到此频道。", - "qq": { - "appId": "App ID", - "appIdPlaceholder": "输入您的 QQ 机器人 App ID", - "chatIds": "允许的会话 ID", - "chatIdsHint": "格式:c2c:openid, group:groupid, channel:channelid。留空表示允许所有。", - "chatIdsPlaceholder": "c2c:abc123, group:xyz789", - "clientSecret": "Client Secret", - "clientSecretPlaceholder": "输入您的 QQ 机器人 Client Secret", - "description": "通过 QQ 机器人官方 API 接收和回复消息。", - "title": "QQ", - "whoamiTip": "💡 提示:发送 /whoami 给机器人即可获取正确格式的会话 ID。" - }, - "security": { - "inheritFromAgent": "继承智能体设置", - "permissionMode": "频道权限模式", - "permissionModeHint": "覆盖此频道消息的智能体权限模式。选择「继承」则使用智能体默认设置。" - }, - "selectAgent": "选择要绑定的智能体", - "slack": { - "appToken": "应用级别 Token", - "appTokenPlaceholder": "xapp-...", - "botToken": "Bot Token", - "botTokenPlaceholder": "xoxb-...", - "channelIds": "允许的频道 ID", - "channelIdsHint": "Slack 频道 ID。留空表示允许所有。", - "channelIdsPlaceholder": "C01234567, D89012345", - "description": "通过 Slack 机器人使用 Socket Mode 接收和回复消息。", - "title": "Slack", - "whoamiTip": "💡 提示:发送 /whoami 给机器人即可获取频道 ID。" - }, - "soulModeRequired": "该智能体尚未开启自主模式,频道功能将无法使用。请在智能体设置中开启自主模式。", - "tab": "频道", - "telegram": { - "botToken": "Bot Token", - "botTokenPlaceholder": "输入您的 Telegram 机器人 Token", - "chatIds": "允许的会话 ID", - "chatIdsHint": "用逗号分隔。留空表示允许所有会话。", - "chatIdsPlaceholder": "123456789, 987654321", - "description": "通过 Telegram 机器人使用长轮询方式接收和回复消息。", - "title": "Telegram" - }, - "title": "频道", - "updateError": "", - "wechat": { - "addAccount": "添加微信账号", - "chatIds": "允许的用户 ID", - "chatIdsHint": "用逗号分隔。留空表示允许所有用户。", - "chatIdsPlaceholder": "wxid_abc123, wxid_def456", - "connected": "已登录", - "description": "通过微信 iLink Bot API 接收和回复消息。", - "disconnected": "未登录", - "loginHint": "首次登录需要扫描二维码,启用频道后将自动弹出二维码。", - "qrHint": "请使用手机微信扫描二维码完成登录。", - "qrTitle": "微信扫码登录", - "title": "微信", - "whoamiTip": "提示:在微信中发送 /whoami 可获取用户 ID。" - } - }, - "heartbeat": { - "enabled": "启用心跳", - "enabledHelper": "启用后,如果工作区存在 heartbeat.md 文件,调度器会按配置的间隔读取其内容并发送到主会话。", - "interval": "间隔(分钟)", - "intervalHelper": "心跳运行的频率,单位为分钟。" - }, - "sandbox": { - "description": "使用操作系统级沙箱限制智能体的文件系统和网络访问,仅限于工作区目录。", - "helper": "将文件系统和命令访问限制在工作区路径内。", - "label": "沙箱模式" - }, - "scheduler": { - "cron": { - "helper": "标准 cron 表达式(例如 '*/30 * * * *' 表示每 30 分钟)。", - "label": "Cron 表达式", - "placeholder": "*/30 * * * *" - }, - "description": "调度器按计划自动触发智能体运行。", - "enabled": "启用调度器", - "enabledHelper": "启用后,智能体将根据配置的计划自动运行。", - "errors": { - "invalidCron": "无效的 cron 表达式", - "invalidDelay": "延迟必须是正数", - "invalidInterval": "间隔必须是正数" - }, - "interval": { - "helper": "智能体运行的频率,单位为秒。", - "label": "间隔(秒)" - }, - "lastRun": "上次运行", - "never": "从未", - "nextRun": "下次运行", - "oneTime": { - "helper": "智能体运行前的一次性延迟,单位为秒。", - "label": "延迟(秒)" - }, - "status": { - "running": "运行中", - "stopped": "已停止", - "tickInProgress": "执行中" - }, - "tab": "调度器", - "title": "调度器配置", - "type": { - "cron": "Cron 表达式", - "interval": "固定间隔", - "label": "调度类型", - "one-time": "一次性延迟" - } + "channels": { + "add": "添加", + "bindAgent": "绑定智能体", + "chatIdsAutoTrackHint": "留空时系统将自动追踪:需要先在对应平台上主动给 Bot 发送一条消息,系统才会记录 Chat ID 用于后续通知。", + "comingSoon": "即将推出", + "connected": "已连接", + "connecting": "连接中", + "createError": "", + "deleteConfirm": "确定删除频道「{{name}}」?", + "deleteError": "", + "description": "将您的智能体连接到消息平台。", + "disconnected": "已断开", + "discord": { + "botToken": "Bot Token", + "botTokenPlaceholder": "输入您的 Discord 机器人 Token", + "channelIds": "允许的频道 ID", + "channelIdsHint": "格式:channel:id 或 dm:id。留空表示允许所有。", + "channelIdsPlaceholder": "channel:123456789, dm:987654321", + "description": "通过 Discord 机器人使用 WebSocket 网关接收和回复消息。", + "title": "Discord", + "whoamiTip": "💡 提示:发送 /whoami 给机器人即可获取正确格式的频道 ID。" }, - "soul": { - "description": "灵魂定义了智能体的个性和基础行为。它从工作区根目录的 soul.md 文件中读取。", - "empty": "在工作区根目录未找到 soul.md 文件。创建一个来定义智能体的个性。", - "enabled": "启用灵魂", - "enabledHelper": "启用后,智能体会从工作区根目录读取 soul.md 并将其添加到系统提示词前面。", - "fileLabel": "soul.md", - "preview": "灵魂预览", - "tab": "灵魂", - "title": "灵魂配置" - }, - "tasks": { - "add": "添加任务", - "cancel": "取消", - "channels": { - "label": "发送到频道", - "noActiveChatIds": "所选频道当前无可用的接收目标(Chat ID),任务结果可能无法送达。请先在对应平台上给 Bot 发送一条消息。", - "placeholder": "选择接收结果的频道" - }, - "cronPlaceholder": "例如 0 9 * * *(每天上午 9 点)", - "delete": { - "confirm": "确定要删除此任务吗?", - "label": "删除" - }, - "edit": "编辑", - "empty": "暂无定时任务。添加一个开始吧。", - "error": { - "createFailed": "创建任务失败", - "deleteFailed": "删除任务失败", - "loadFailed": "", - "runFailed": "运行任务失败", - "updateFailed": "更新任务失败" - }, - "frequency": { - "everyPrefix": "间隔", - "everySuffix": "分钟执行", - "label": "执行频率" - }, - "intervalPlaceholder": "至少1", - "intervalUnit": "分钟", - "lastRun": "上次运行", - "logs": { - "cancelled": "已取消", - "completed": "已完成", - "duration": "耗时", - "empty": "暂无运行历史。", - "failed": "失败", - "justNow": "刚刚", - "label": "运行历史", - "loadError": "", - "result": "结果", - "runAt": "运行时间", - "running": "运行中...", - "search": "搜索运行记录...", - "status": "状态", - "viewSession": "查看会话" - }, - "name": { - "label": "名称", - "placeholder": "例如:每日代码审查" - }, - "nextRun": "下次运行", - "oncePlaceholder": "选择日期和时间", - "pause": "暂停", - "prompt": { - "expand": "展开编辑器", - "label": "提示词", - "placeholder": "此任务运行时智能体应该做什么?" - }, - "resume": "恢复", - "run": "运行", - "runTriggered": "任务已触发", - "save": "保存", - "scheduleType": { - "cron": "Cron", - "interval": "间隔", - "once": "一次性" - }, - "status": { - "active": "活跃", - "completed": "已完成", - "paused": "已暂停" - }, - "tab": "任务", - "time": { - "hoursAgo": "{{count}}小时前", - "minutesAgo": "{{count}}分钟前" - }, - "timeout": { - "label": "最长执行时间", - "placeholder": "无限制" - }, - "title": "定时任务" + "error": "错误", + "feishu": { + "appId": "应用ID", + "appIdPlaceholder": "输入你的飞书应用 ID", + "appSecret": "应用密钥", + "appSecretPlaceholder": "输入你的飞书应用密钥", + "chatIds": "允许的聊天 ID", + "chatIdsHint": "逗号分隔的聊天 ID。留空则允许所有聊天。", + "chatIdsPlaceholder": "oc_xxxxx,oc_yyyyy", + "connected": "已连接", + "description": "通过 WebSocket 使用飞书/ Lark 机器人接收并回复消息。", + "domain": "域名", + "domainFeishu": "飞书(中国)", + "domainLark": "飞书(国际版)", + "encryptKey": "加密密钥", + "encryptKeyPlaceholder": "请输入来自你飞书应用的加密密钥", + "loginHint": "未配置凭证。启用频道后将自动开始扫码注册,或手动输入 App ID 和 App Secret。", + "qrExpired": "二维码已过期,请重新切换频道开关重试。", + "qrHint": "等待扫描二维码...", + "qrScanHint": "打开手机飞书,扫描二维码以创建机器人应用。", + "qrTitle": "飞书扫码注册", + "title": "飞书", + "verificationToken": "验证令牌", + "verificationTokenPlaceholder": "输入您飞书应用中的验证令牌" }, - "warning": { - "bypassPermissions": "CherryClaw 智能体默认以全自动模式运行。所有工具执行时不会请求批准。" + "logs": "日志", + "noInstances": "暂无 {{type}} 频道,点击「+ 添加」创建。", + "noLogs": "暂无日志", + "notifyReceiver": "接收任务通知", + "notifyReceiverHint": "将定时任务结果发送到此频道。", + "qq": { + "appId": "App ID", + "appIdPlaceholder": "输入您的 QQ 机器人 App ID", + "chatIds": "允许的会话 ID", + "chatIdsHint": "格式:c2c:openid, group:groupid, channel:channelid。留空表示允许所有。", + "chatIdsPlaceholder": "c2c:abc123, group:xyz789", + "clientSecret": "Client Secret", + "clientSecretPlaceholder": "输入您的 QQ 机器人 Client Secret", + "description": "通过 QQ 机器人官方 API 接收和回复消息。", + "title": "QQ", + "whoamiTip": "💡 提示:发送 /whoami 给机器人即可获取正确格式的会话 ID。" + }, + "security": { + "inheritFromAgent": "继承智能体设置", + "permissionMode": "频道权限模式", + "permissionModeHint": "覆盖此频道消息的智能体权限模式。选择「继承」则使用智能体默认设置。" + }, + "selectAgent": "选择要绑定的智能体", + "slack": { + "appToken": "应用级别 Token", + "appTokenPlaceholder": "xapp-...", + "botToken": "Bot Token", + "botTokenPlaceholder": "xoxb-...", + "channelIds": "允许的频道 ID", + "channelIdsHint": "Slack 频道 ID。留空表示允许所有。", + "channelIdsPlaceholder": "C01234567, D89012345", + "description": "通过 Slack 机器人使用 Socket Mode 接收和回复消息。", + "title": "Slack", + "whoamiTip": "💡 提示:发送 /whoami 给机器人即可获取频道 ID。" + }, + "tab": "频道", + "telegram": { + "botToken": "Bot Token", + "botTokenPlaceholder": "输入您的 Telegram 机器人 Token", + "chatIds": "允许的会话 ID", + "chatIdsHint": "用逗号分隔。留空表示允许所有会话。", + "chatIdsPlaceholder": "123456789, 987654321", + "description": "通过 Telegram 机器人使用长轮询方式接收和回复消息。", + "title": "Telegram" + }, + "title": "频道", + "updateError": "", + "wechat": { + "addAccount": "添加微信账号", + "chatIds": "允许的用户 ID", + "chatIdsHint": "用逗号分隔。留空表示允许所有用户。", + "chatIdsPlaceholder": "wxid_abc123, wxid_def456", + "connected": "已登录", + "description": "通过微信 iLink Bot API 接收和回复消息。", + "disconnected": "未登录", + "loginHint": "首次登录需要扫描二维码,启用频道后将自动弹出二维码。", + "qrHint": "请使用手机微信扫描二维码完成登录。", + "qrTitle": "微信扫码登录", + "title": "微信", + "whoamiTip": "提示:在微信中发送 /whoami 可获取用户 ID。" } }, "delete": { @@ -343,8 +194,7 @@ "type": "智能体图标" }, "input": { - "placeholder": "输入消息,按 {{key}} 发送,输入 / 搜索路径或命令", - "soul_placeholder": "帮我连接微信" + "placeholder": "输入消息,按 {{key}} 发送,输入 / 搜索路径或命令" }, "list": { "error": { @@ -718,12 +568,6 @@ "tab": "技能", "title": "已安装技能" }, - "soulMode": { - "description": "启用后,智能体将使用工作区灵魂文件构建自定义系统提示词,注入自主任务管理工具,并禁用不适合自主运行的交互式工具。", - "disabledBadge": "自主模式已禁用", - "disabledTooltip": "该工具在自主模式开启时被自动禁用", - "title": "自主模式" - }, "tooling": { "mcp": { "description": "连接 MCP 服务器即可解锁更多可在上方预先授权的工具。", @@ -795,6 +639,89 @@ "title": "管理技能" } }, + "tasks": { + "add": "添加任务", + "cancel": "取消", + "channels": { + "label": "发送到频道", + "noActiveChatIds": "所选频道当前无可用的接收目标(Chat ID),任务结果可能无法送达。请先在对应平台上给 Bot 发送一条消息。", + "placeholder": "选择接收结果的频道" + }, + "cronPlaceholder": "例如 0 9 * * *(每天上午 9 点)", + "delete": { + "confirm": "确定要删除此任务吗?", + "label": "删除" + }, + "edit": "编辑", + "empty": "暂无定时任务。添加一个开始吧。", + "error": { + "createFailed": "创建任务失败", + "deleteFailed": "删除任务失败", + "loadFailed": "", + "runFailed": "运行任务失败", + "updateFailed": "更新任务失败" + }, + "frequency": { + "everyPrefix": "间隔", + "everySuffix": "分钟执行", + "label": "执行频率" + }, + "intervalPlaceholder": "至少1", + "intervalUnit": "分钟", + "lastRun": "上次运行", + "logs": { + "cancelled": "已取消", + "completed": "已完成", + "duration": "耗时", + "empty": "暂无运行历史。", + "failed": "失败", + "justNow": "刚刚", + "label": "运行历史", + "loadError": "", + "result": "结果", + "runAt": "运行时间", + "running": "运行中...", + "search": "搜索运行记录...", + "status": "状态", + "viewSession": "查看会话" + }, + "name": { + "label": "名称", + "placeholder": "例如:每日代码审查" + }, + "nextRun": "下次运行", + "oncePlaceholder": "选择日期和时间", + "pause": "暂停", + "prompt": { + "expand": "展开编辑器", + "label": "提示词", + "placeholder": "此任务运行时智能体应该做什么?" + }, + "resume": "恢复", + "run": "运行", + "runTriggered": "任务已触发", + "save": "保存", + "scheduleType": { + "cron": "Cron", + "interval": "间隔", + "once": "一次性" + }, + "status": { + "active": "活跃", + "completed": "已完成", + "paused": "已暂停" + }, + "tab": "任务", + "time": { + "hoursAgo": "{{count}}小时前", + "minutesAgo": "{{count}}分钟前" + }, + "timeout": { + "label": "最长执行时间", + "placeholder": "无限制" + }, + "title": "定时任务" + }, "todo": { "mock": { "actions": { @@ -946,6 +873,14 @@ "description": "在你的环境中执行 Shell 命令", "label": "Bash" }, + "CherryConfig": { + "description": "检查并管理此智能体配置和频道", + "label": "智能体配置" + }, + "CherryCron": { + "description": "管理应用内的定时调度", + "label": "定时任务" + }, "CherryKbManage": { "description": "添加、删除或刷新知识库中的文档", "label": "知识库管理" @@ -954,6 +889,10 @@ "description": "搜索你的知识库", "label": "知识库搜索" }, + "CherryNotify": { + "description": "通过已连接的频道发送通知", + "label": "通知" + }, "CherryWebFetch": { "description": "抓取并读取网页", "label": "网页抓取" @@ -962,10 +901,6 @@ "description": "通过你配置的提供方搜索网页", "label": "网页搜索" }, - "ClawCron": { - "description": "管理应用内的定时调度", - "label": "定时任务" - }, "Edit": { "description": "对特定文件进行精确编辑", "label": "编辑文件" @@ -2998,11 +2933,10 @@ "placeholder": "给 Agent 起个名字" }, "permission_mode": { - "help": "切换到 bypassPermissions 等同于启用 Soul 模式", "label": "权限模式", "option": { "acceptEdits": "接受编辑", - "bypassPermissions": "绕过权限 (Soul)", + "bypassPermissions": "绕过权限", "default": "默认", "plan": "Plan 模式" } @@ -3014,10 +2948,6 @@ "small_model": { "hint": "负责简单判断和格式化", "label": "Small 模型" - }, - "soul_enabled": { - "help": "启用后自动授予 bypassPermissions 权限", - "label": "Soul 模式" } }, "model_config": "模型配置", @@ -7257,7 +7187,7 @@ }, "scheduledTasks": { "description": "管理所有 Agent 的定时任务。任务将按照配置的时间计划自动运行。", - "noAgents": "定时任务需要开启灵魂模式或无权限模式的 Agent 才能运行。请先在 Agent 设置中启用。", + "noAgents": "暂无可用的 Agent。请先创建一个 Agent,再添加定时任务。", "noAgentsTip": "提示:你也可以直接在对话中让 Agent 创建定时任务。", "noTasks": "暂无定时任务。点击「+ 添加」为某个 Agent 创建任务。", "selectTask": "选择一个任务以查看详情", diff --git a/src/renderer/i18n/locales/zh-tw.json b/src/renderer/i18n/locales/zh-tw.json index 9bae151c628..8c5ef1dde77 100644 --- a/src/renderer/i18n/locales/zh-tw.json +++ b/src/renderer/i18n/locales/zh-tw.json @@ -31,262 +31,113 @@ "submit": "提交", "title": "來自Agent的提問" }, - "cherryClaw": { - "channels": { - "add": "新增", - "bindAgent": "綁定代理", - "chatIdsAutoTrackHint": "若留空,系統將自動追蹤:您需先在平台上向機器人發送一則訊息,系統才會記錄聊天 ID,以供後續通知使用。", - "comingSoon": "即將推出", - "connected": "已連線", - "connecting": "連接", - "createError": "", - "deleteConfirm": "確定刪除頻道「{{name}}」?", - "deleteError": "", - "description": "將您的 Agent 連接到訊息平台。", - "disconnected": "已中斷連線", - "discord": { - "botToken": "Bot Token", - "botTokenPlaceholder": "輸入您的 Discord 機器人 Token", - "channelIds": "允許的頻道 ID", - "channelIdsHint": "格式:channel:id 或 dm:id。留空表示允許所有。", - "channelIdsPlaceholder": "channel:123456789, dm:987654321", - "description": "透過 Discord 機器人使用 WebSocket 閘道接收和回覆訊息。", - "title": "Discord", - "whoamiTip": "💡 提示:發送 /whoami 給機器人即可取得正確格式的頻道 ID。" - }, - "error": "錯誤", - "feishu": { - "appId": "應用程式 ID", - "appIdPlaceholder": "輸入您的飛書應用程式 ID", - "appSecret": "應用程式密鑰", - "appSecretPlaceholder": "輸入你的飛書應用程式密鑰", - "chatIds": "允許的聊天 ID", - "chatIdsHint": "逗號分隔的聊天 ID。留空以允許所有聊天。", - "chatIdsPlaceholder": "oc_xxxxx, oc_yyyyy", - "connected": "已連線", - "description": "透過 WebSocket 使用飛書/Lark 機器人接收並回覆訊息。", - "domain": "網域", - "domainFeishu": "飛書(中國)", - "domainLark": "Lark(國際版)", - "encryptKey": "加密金鑰", - "encryptKeyPlaceholder": "從您的飛書應用程式輸入加密金鑰", - "loginHint": "未設定憑證。啟用頻道以開始 QR 碼註冊,或手動輸入應用程式 ID 與應用程式密鑰。", - "qrExpired": "QR碼已過期,請切換頻道後重試。", - "qrHint": "等待掃描 QR 碼...", - "qrScanHint": "在手機上開啟飛書,掃描 QR Code 以建立機器人應用程式。", - "qrTitle": "飛書 QR 報名", - "title": "飛書", - "verificationToken": "驗證權杖", - "verificationTokenPlaceholder": "從您的 Feishu 應用程式輸入驗證碼" - }, - "logs": "紀錄", - "noInstances": "尚無 {{type}} 頻道,點擊「+ 新增」建立。", - "noLogs": "尚無日誌", - "notifyReceiver": "接收任務通知", - "notifyReceiverHint": "將排程任務結果傳送到此頻道。", - "qq": { - "appId": "App ID", - "appIdPlaceholder": "輸入您的 QQ 機器人 App ID", - "chatIds": "允許的對話 ID", - "chatIdsHint": "格式:c2c:openid, group:groupid, channel:channelid。留空表示允許所有。", - "chatIdsPlaceholder": "c2c:abc123, group:xyz789", - "clientSecret": "Client Secret", - "clientSecretPlaceholder": "輸入您的 QQ 機器人 Client Secret", - "description": "透過 QQ 機器人官方 API 接收和回覆訊息。", - "title": "QQ", - "whoamiTip": "💡 提示:發送 /whoami 給機器人即可獲取正確格式的對話 ID。" - }, - "security": { - "inheritFromAgent": "繼承智能體設定", - "permissionMode": "頻道權限模式", - "permissionModeHint": "覆寫此頻道訊息的智能體權限模式。選擇「繼承」則使用智能體預設設定。" - }, - "selectAgent": "選擇要綁定的代理", - "slack": { - "appToken": "應用層級 Token", - "appTokenPlaceholder": "xapp-...", - "botToken": "Bot Token", - "botTokenPlaceholder": "xoxb-...", - "channelIds": "允許的頻道 ID", - "channelIdsHint": "Slack 頻道 ID。留空表示允許所有。", - "channelIdsPlaceholder": "C01234567, D89012345", - "description": "透過 Slack 機器人使用 Socket Mode 接收和回覆訊息。", - "title": "Slack", - "whoamiTip": "💡 提示:發送 /whoami 給機器人即可取得頻道 ID。" - }, - "soulModeRequired": "此代理未啟用自主模式,頻道功能將無法運作。請在代理設定中啟用。", - "tab": "頻道", - "telegram": { - "botToken": "Bot Token", - "botTokenPlaceholder": "輸入您的 Telegram 機器人 Token", - "chatIds": "允許的對話 ID", - "chatIdsHint": "用逗號分隔。留空表示允許所有對話。", - "chatIdsPlaceholder": "123456789, 987654321", - "description": "透過 Telegram 機器人使用長輪詢方式接收和回覆訊息。", - "title": "Telegram" - }, - "title": "頻道", - "updateError": "", - "wechat": { - "addAccount": "新增微信帳號", - "chatIds": "允許的使用者 ID", - "chatIdsHint": "以逗號分隔。留空以允許所有使用者。", - "chatIdsPlaceholder": "wxid_abc123, wxid_def456", - "connected": "已連線", - "description": "透過 iLink Bot API 在微信上接收並回覆訊息。", - "disconnected": "已斷線", - "loginHint": "首次登入需要掃描 QR 碼。請查看應用程式記錄以取得登入網址。", - "qrHint": "打開手機上的微信,掃描 QR 碼登入。", - "qrTitle": "微信 QR 碼登入", - "title": "微信", - "whoamiTip": "提示:在微信中發送 /whoami 可取得使用者的 ID。" - } - }, - "heartbeat": { - "enabled": "啟用心跳", - "enabledHelper": "啟用後,如果工作區存在 heartbeat.md 檔案,排程器會按設定的間隔讀取其內容並傳送到主對話。", - "interval": "間隔(分鐘)", - "intervalHelper": "心跳執行的頻率,單位為分鐘。" - }, - "sandbox": { - "description": "使用作業系統級沙箱限制 Agent 的檔案系統和網路存取,僅限於工作區目錄。", - "helper": "將檔案系統和命令存取限制在工作區路徑內。", - "label": "沙箱模式" - }, - "scheduler": { - "cron": { - "helper": "標準 cron 表達式(例如 '*/30 * * * *' 表示每 30 分鐘)。", - "label": "Cron 表達式", - "placeholder": "*/30 * * * *" - }, - "description": "排程器按計劃自動觸發 Agent 執行。", - "enabled": "啟用排程器", - "enabledHelper": "啟用後,Agent 將根據設定的計劃自動執行。", - "errors": { - "invalidCron": "無效的 cron 表達式", - "invalidDelay": "延遲必須是正數", - "invalidInterval": "間隔必須是正數" - }, - "interval": { - "helper": "Agent 執行的頻率,單位為秒。", - "label": "間隔(秒)" - }, - "lastRun": "上次執行", - "never": "從未", - "nextRun": "下次執行", - "oneTime": { - "helper": "Agent 執行前的一次性延遲,單位為秒。", - "label": "延遲(秒)" - }, - "status": { - "running": "執行中", - "stopped": "已停止", - "tickInProgress": "執行中" - }, - "tab": "排程器", - "title": "排程器設定", - "type": { - "cron": "Cron 表達式", - "interval": "固定間隔", - "label": "排程類型", - "one-time": "一次性延遲" - } - }, - "soul": { - "description": "靈魂定義了 Agent 的個性和基礎行為。它從工作區根目錄的 soul.md 檔案中讀取。", - "empty": "在工作區根目錄未找到 soul.md 檔案。建立一個來定義 Agent 的個性。", - "enabled": "啟用靈魂", - "enabledHelper": "啟用後,Agent 會從工作區根目錄讀取 soul.md 並將其添加到系統提示詞前面。", - "fileLabel": "soul.md", - "preview": "靈魂預覽", - "tab": "靈魂", - "title": "靈魂設定" - }, - "tasks": { - "add": "新增任務", - "cancel": "取消", - "channels": { - "label": "傳送至頻道", - "noActiveChatIds": "所選頻道沒有可用的收件人(聊天 ID)。任務結果可能無法送達。請先在平台上向機器人發送一則訊息。", - "placeholder": "選擇接收結果的頻道" - }, - "cronPlaceholder": "例如 0 9 * * *(每天上午 9 點)", - "delete": { - "confirm": "確定要刪除此任務嗎?", - "label": "刪除" - }, - "edit": "編輯", - "empty": "暫無排程任務。新增一個開始吧。", - "error": { - "createFailed": "建立任務失敗", - "deleteFailed": "無法刪除任務", - "loadFailed": "", - "runFailed": "無法執行任務", - "updateFailed": "更新任務失敗" - }, - "frequency": { - "everyPrefix": "間隔", - "everySuffix": "分鐘執行", - "label": "執行頻率" - }, - "intervalPlaceholder": "至少1", - "intervalUnit": "分鐘", - "lastRun": "上次執行", - "logs": { - "cancelled": "已取消", - "completed": "已完成", - "duration": "耗時", - "empty": "暫無執行歷史。", - "failed": "失敗", - "justNow": "剛剛", - "label": "執行歷史", - "loadError": "", - "result": "結果", - "runAt": "執行時間", - "running": "執行中...", - "search": "搜尋執行記錄...", - "status": "狀態", - "viewSession": "查看會話" - }, - "name": { - "label": "名稱", - "placeholder": "例如:每日程式碼審查" - }, - "nextRun": "下次執行", - "oncePlaceholder": "選擇日期和時間", - "pause": "暫停", - "prompt": { - "expand": "展開編輯器", - "label": "提示詞", - "placeholder": "此任務執行時 Agent 應該做什麼?" - }, - "resume": "恢復", - "run": "執行", - "runTriggered": "任務已觸發", - "save": "儲存", - "scheduleType": { - "cron": "Cron", - "interval": "間隔", - "once": "一次性" - }, - "status": { - "active": "活躍", - "completed": "已完成", - "paused": "已暫停" - }, - "tab": "任務", - "time": { - "hoursAgo": "{{count}}小時前", - "minutesAgo": "{{count}}分鐘前" - }, - "timeout": { - "label": "最長執行時間", - "placeholder": "無限制" - }, - "title": "排程任務" + "channels": { + "add": "新增", + "bindAgent": "綁定代理", + "chatIdsAutoTrackHint": "若留空,系統將自動追蹤:您需先在平台上向機器人發送一則訊息,系統才會記錄聊天 ID,以供後續通知使用。", + "comingSoon": "即將推出", + "connected": "已連線", + "connecting": "連接", + "createError": "", + "deleteConfirm": "確定刪除頻道「{{name}}」?", + "deleteError": "", + "description": "將您的 Agent 連接到訊息平台。", + "disconnected": "已中斷連線", + "discord": { + "botToken": "Bot Token", + "botTokenPlaceholder": "輸入您的 Discord 機器人 Token", + "channelIds": "允許的頻道 ID", + "channelIdsHint": "格式:channel:id 或 dm:id。留空表示允許所有。", + "channelIdsPlaceholder": "channel:123456789, dm:987654321", + "description": "透過 Discord 機器人使用 WebSocket 閘道接收和回覆訊息。", + "title": "Discord", + "whoamiTip": "💡 提示:發送 /whoami 給機器人即可取得正確格式的頻道 ID。" }, - "warning": { - "bypassPermissions": "CherryClaw Agent 預設以全自動模式執行。所有工具執行時不會請求批准。" + "error": "錯誤", + "feishu": { + "appId": "應用程式 ID", + "appIdPlaceholder": "輸入您的飛書應用程式 ID", + "appSecret": "應用程式密鑰", + "appSecretPlaceholder": "輸入你的飛書應用程式密鑰", + "chatIds": "允許的聊天 ID", + "chatIdsHint": "逗號分隔的聊天 ID。留空以允許所有聊天。", + "chatIdsPlaceholder": "oc_xxxxx, oc_yyyyy", + "connected": "已連線", + "description": "透過 WebSocket 使用飛書/Lark 機器人接收並回覆訊息。", + "domain": "網域", + "domainFeishu": "飛書(中國)", + "domainLark": "Lark(國際版)", + "encryptKey": "加密金鑰", + "encryptKeyPlaceholder": "從您的飛書應用程式輸入加密金鑰", + "loginHint": "未設定憑證。啟用頻道以開始 QR 碼註冊,或手動輸入應用程式 ID 與應用程式密鑰。", + "qrExpired": "QR碼已過期,請切換頻道後重試。", + "qrHint": "等待掃描 QR 碼...", + "qrScanHint": "在手機上開啟飛書,掃描 QR Code 以建立機器人應用程式。", + "qrTitle": "飛書 QR 報名", + "title": "飛書", + "verificationToken": "驗證權杖", + "verificationTokenPlaceholder": "從您的 Feishu 應用程式輸入驗證碼" + }, + "logs": "紀錄", + "noInstances": "尚無 {{type}} 頻道,點擊「+ 新增」建立。", + "noLogs": "尚無日誌", + "notifyReceiver": "接收任務通知", + "notifyReceiverHint": "將排程任務結果傳送到此頻道。", + "qq": { + "appId": "App ID", + "appIdPlaceholder": "輸入您的 QQ 機器人 App ID", + "chatIds": "允許的對話 ID", + "chatIdsHint": "格式:c2c:openid, group:groupid, channel:channelid。留空表示允許所有。", + "chatIdsPlaceholder": "c2c:abc123, group:xyz789", + "clientSecret": "Client Secret", + "clientSecretPlaceholder": "輸入您的 QQ 機器人 Client Secret", + "description": "透過 QQ 機器人官方 API 接收和回覆訊息。", + "title": "QQ", + "whoamiTip": "💡 提示:發送 /whoami 給機器人即可獲取正確格式的對話 ID。" + }, + "security": { + "inheritFromAgent": "繼承智能體設定", + "permissionMode": "頻道權限模式", + "permissionModeHint": "覆寫此頻道訊息的智能體權限模式。選擇「繼承」則使用智能體預設設定。" + }, + "selectAgent": "選擇要綁定的代理", + "slack": { + "appToken": "應用層級 Token", + "appTokenPlaceholder": "xapp-...", + "botToken": "Bot Token", + "botTokenPlaceholder": "xoxb-...", + "channelIds": "允許的頻道 ID", + "channelIdsHint": "Slack 頻道 ID。留空表示允許所有。", + "channelIdsPlaceholder": "C01234567, D89012345", + "description": "透過 Slack 機器人使用 Socket Mode 接收和回覆訊息。", + "title": "Slack", + "whoamiTip": "💡 提示:發送 /whoami 給機器人即可取得頻道 ID。" + }, + "tab": "頻道", + "telegram": { + "botToken": "Bot Token", + "botTokenPlaceholder": "輸入您的 Telegram 機器人 Token", + "chatIds": "允許的對話 ID", + "chatIdsHint": "用逗號分隔。留空表示允許所有對話。", + "chatIdsPlaceholder": "123456789, 987654321", + "description": "透過 Telegram 機器人使用長輪詢方式接收和回覆訊息。", + "title": "Telegram" + }, + "title": "頻道", + "updateError": "", + "wechat": { + "addAccount": "新增微信帳號", + "chatIds": "允許的使用者 ID", + "chatIdsHint": "以逗號分隔。留空以允許所有使用者。", + "chatIdsPlaceholder": "wxid_abc123, wxid_def456", + "connected": "已連線", + "description": "透過 iLink Bot API 在微信上接收並回覆訊息。", + "disconnected": "已斷線", + "loginHint": "首次登入需要掃描 QR 碼。請查看應用程式記錄以取得登入網址。", + "qrHint": "打開手機上的微信,掃描 QR 碼登入。", + "qrTitle": "微信 QR 碼登入", + "title": "微信", + "whoamiTip": "提示:在微信中發送 /whoami 可取得使用者的 ID。" } }, "delete": { @@ -343,8 +194,7 @@ "type": "智能體圖示" }, "input": { - "placeholder": "輸入訊息,按 {{key}} 傳送,輸入 / 搜尋路徑或指令", - "soul_placeholder": "幫我連接微信" + "placeholder": "輸入訊息,按 {{key}} 傳送,輸入 / 搜尋路徑或指令" }, "list": { "error": { @@ -718,12 +568,6 @@ "tab": "技能", "title": "已安裝技能" }, - "soulMode": { - "description": "啟用後,代理程式會使用來自工作區靈魂檔案的自訂系統提示,注入自主任務管理工具,並停用不適合自主操作的互動式工具。", - "disabledBadge": "被自主模式停用", - "disabledTooltip": "當自主模式啟用時,此工具會自動停用", - "title": "自主模式" - }, "tooling": { "mcp": { "description": "連線 MCP 伺服器即可解鎖更多可在上方預先授權的工具。", @@ -795,6 +639,89 @@ "title": "管理技能" } }, + "tasks": { + "add": "新增任務", + "cancel": "取消", + "channels": { + "label": "傳送至頻道", + "noActiveChatIds": "所選頻道沒有可用的收件人(聊天 ID)。任務結果可能無法送達。請先在平台上向機器人發送一則訊息。", + "placeholder": "選擇接收結果的頻道" + }, + "cronPlaceholder": "例如 0 9 * * *(每天上午 9 點)", + "delete": { + "confirm": "確定要刪除此任務嗎?", + "label": "刪除" + }, + "edit": "編輯", + "empty": "暫無排程任務。新增一個開始吧。", + "error": { + "createFailed": "建立任務失敗", + "deleteFailed": "無法刪除任務", + "loadFailed": "", + "runFailed": "無法執行任務", + "updateFailed": "更新任務失敗" + }, + "frequency": { + "everyPrefix": "間隔", + "everySuffix": "分鐘執行", + "label": "執行頻率" + }, + "intervalPlaceholder": "至少1", + "intervalUnit": "分鐘", + "lastRun": "上次執行", + "logs": { + "cancelled": "已取消", + "completed": "已完成", + "duration": "耗時", + "empty": "暫無執行歷史。", + "failed": "失敗", + "justNow": "剛剛", + "label": "執行歷史", + "loadError": "", + "result": "結果", + "runAt": "執行時間", + "running": "執行中...", + "search": "搜尋執行記錄...", + "status": "狀態", + "viewSession": "查看會話" + }, + "name": { + "label": "名稱", + "placeholder": "例如:每日程式碼審查" + }, + "nextRun": "下次執行", + "oncePlaceholder": "選擇日期和時間", + "pause": "暫停", + "prompt": { + "expand": "展開編輯器", + "label": "提示詞", + "placeholder": "此任務執行時 Agent 應該做什麼?" + }, + "resume": "恢復", + "run": "執行", + "runTriggered": "任務已觸發", + "save": "儲存", + "scheduleType": { + "cron": "Cron", + "interval": "間隔", + "once": "一次性" + }, + "status": { + "active": "活躍", + "completed": "已完成", + "paused": "已暫停" + }, + "tab": "任務", + "time": { + "hoursAgo": "{{count}}小時前", + "minutesAgo": "{{count}}分鐘前" + }, + "timeout": { + "label": "最長執行時間", + "placeholder": "無限制" + }, + "title": "排程任務" + }, "todo": { "mock": { "actions": { @@ -946,6 +873,14 @@ "description": "在你的環境中執行 Shell 命令", "label": "Bash" }, + "CherryConfig": { + "description": "檢查並管理此智能體設定和頻道", + "label": "智能體設定" + }, + "CherryCron": { + "description": "管理應用內的定時排程", + "label": "定時任務" + }, "CherryKbManage": { "description": "新增、刪除或重新整理知識庫中的文件", "label": "知識庫管理" @@ -954,6 +889,10 @@ "description": "搜尋你的知識庫", "label": "知識庫搜尋" }, + "CherryNotify": { + "description": "透過已連接的頻道傳送通知", + "label": "通知" + }, "CherryWebFetch": { "description": "擷取並讀取網頁", "label": "網頁擷取" @@ -962,10 +901,6 @@ "description": "透過你設定的提供方搜尋網頁", "label": "網頁搜尋" }, - "ClawCron": { - "description": "管理應用內的定時排程", - "label": "定時任務" - }, "Edit": { "description": "對特定檔案進行精確編輯", "label": "編輯檔案" @@ -2998,11 +2933,10 @@ "placeholder": "給代理人取個名字" }, "permission_mode": { - "help": "切換到 bypassPermissions 等同於啟用靈魂模式", "label": "權限模式", "option": { "acceptEdits": "接受編輯", - "bypassPermissions": "繞過權限(靈魂)", + "bypassPermissions": "繞過權限", "default": "預設", "plan": "計畫模式" } @@ -3014,10 +2948,6 @@ "small_model": { "hint": "輕量級檢查與格式化", "label": "小型模型" - }, - "soul_enabled": { - "help": "啟用後,將自動授予 bypassPermissions", - "label": "靈魂模式" } }, "model_config": "模型配置", @@ -7257,7 +7187,7 @@ }, "scheduledTasks": { "description": "管理所有代理的排程任務。任務將依設定好的排程自動執行。", - "noAgents": "排程任務需要開啟靈魂模式或無權限模式的代理才能執行。請先在代理設定中啟用。", + "noAgents": "沒有可用的代理。請先建立一個代理,再新增排程任務。", "noAgentsTip": "提示:你也可以直接在對話中讓代理建立排程任務。", "noTasks": "沒有排程任務。點擊「+ 新增」為代理程式建立一個。", "selectTask": "選擇一個任務來查看詳細資訊", diff --git a/src/renderer/i18n/translate/de-de.json b/src/renderer/i18n/translate/de-de.json index 8dbf07ddd7c..75bca029fb9 100644 --- a/src/renderer/i18n/translate/de-de.json +++ b/src/renderer/i18n/translate/de-de.json @@ -31,262 +31,113 @@ "submit": "Einreichen", "title": "Fragen vom Agenten" }, - "cherryClaw": { - "channels": { - "add": "Hinzufügen", - "bindAgent": "Bind-Agent", - "chatIdsAutoTrackHint": "Wenn es leer bleibt, verfolgt das System automatisch: Sie müssen dem Bot auf der Plattform zuerst eine Nachricht senden, dann zeichnet das System die Chat-ID für zukünftige Benachrichtigungen auf.", - "comingSoon": "Demnächst verfügbar", - "connected": "Verbunden", - "connecting": "Verbinden", - "createError": "Fehler beim Erstellen des Kanals", - "deleteConfirm": "Kanal „{{name}}“ löschen?", - "deleteError": "Fehler beim Löschen des Kanals", - "description": "Verbinden Sie Ihren Agenten mit Messaging-Plattformen.", - "disconnected": "Getrennt", - "discord": { - "botToken": "Bot-Token", - "botTokenPlaceholder": "Gib dein Discord-Bot-Token ein", - "channelIds": "Erlaubte Kanal-IDs", - "channelIdsHint": "Format: channel:id oder dm:id. Leer lassen, um alle zuzulassen.", - "channelIdsPlaceholder": "channel:123456789, dm:987654321", - "description": "Nachrichten über einen Discord-Bot über WebSocket-Gateway empfangen und darauf reagieren.", - "title": "Discord", - "whoamiTip": "💡 Tipp: Sende /whoami an den Bot, um deine Kanal-ID im richtigen Format zu erhalten." - }, - "error": "Fehler", - "feishu": { - "appId": "App-ID", - "appIdPlaceholder": "Geben Sie Ihre Feishu-App-ID ein", - "appSecret": "App-Geheimnis", - "appSecretPlaceholder": "Geben Sie Ihr Feishu-App-Geheimnis ein", - "chatIds": "Erlaubte Chat-IDs", - "chatIdsHint": "Kommagetrennte Chat-IDs. Leer lassen, um alle Chats zuzulassen.", - "chatIdsPlaceholder": "oc_xxxxx, oc_yyyyy", - "connected": "Verbunden", - "description": "Nachrichten über einen Feishu/Lark-Bot per WebSocket empfangen und beantworten.", - "domain": "Domain", - "domainFeishu": "Feishu (China)", - "domainLark": "Lark (International)", - "encryptKey": "Verschlüsselungsschlüssel", - "encryptKeyPlaceholder": "Geben Sie den Verschlüsselungsschlüssel Ihrer Feishu-App ein", - "loginHint": "Keine Anmeldeinformationen konfiguriert. Aktivieren Sie den Kanal, um die QR-Code-Registrierung zu starten, oder geben Sie App-ID und App-Secret manuell ein.", - "qrExpired": "QR-Code abgelaufen. Bitte versuchen Sie es erneut, indem Sie den Kanal wechseln.", - "qrHint": "Warten auf QR-Code-Scan...", - "qrScanHint": "Öffne Feishu auf deinem Handy und scanne den QR-Code, um eine Bot-App zu erstellen.", - "qrTitle": "Feishu-QR-Registrierung", - "title": "Feishu", - "verificationToken": "Verifizierungstoken", - "verificationTokenPlaceholder": "Geben Sie das Verifizierungstoken Ihrer Feishu-App ein" - }, - "logs": "Protokolle", - "noInstances": "Keine {{type}}-Kanäle konfiguriert. Klicken Sie auf „+ Hinzufügen“, um einen zu erstellen.", - "noLogs": "Noch keine Protokolle", - "notifyReceiver": "Aufgabenbenachrichtigungen empfangen", - "notifyReceiverHint": "Scheduler-Aufgabenergebnisse an diesen Kanal senden.", - "qq": { - "appId": "App-ID", - "appIdPlaceholder": "Geben Sie Ihre QQ Bot App-ID ein", - "chatIds": "Erlaubte Chat-IDs", - "chatIdsHint": "Format: c2c:openid, group:groupid, channel:channelid. Leer lassen für alle.", - "chatIdsPlaceholder": "c2c:abc123, group:xyz789", - "clientSecret": "Client Secret", - "clientSecretPlaceholder": "Geben Sie Ihr QQ Bot Client Secret ein", - "description": "Nachrichten über die offizielle QQ Bot API empfangen und beantworten.", - "title": "QQ", - "whoamiTip": "💡 Tipp: Senden Sie /whoami an den Bot, um Ihre Chat-ID im richtigen Format zu erhalten." - }, - "security": { - "inheritFromAgent": "Von Agent erben", - "permissionMode": "Kanalberechtigungsmodus", - "permissionModeHint": "Überschreibe den Berechtigungsmodus des Agenten für Nachrichten aus diesem Kanal. „Vererben“ verwendet die Standardeinstellung des Agenten." - }, - "selectAgent": "Wählen Sie einen Agenten zum Binden aus", - "slack": { - "appToken": "App-Level-Token", - "appTokenPlaceholder": "xapp-...", - "botToken": "Bot-Token", - "botTokenPlaceholder": "xoxb-...", - "channelIds": "Erlaubte Kanal-IDs", - "channelIdsHint": "Slack-Kanal-IDs. Leer lassen, um alle zu erlauben.", - "channelIdsPlaceholder": "C01234567, D89012345", - "description": "Nachrichten über einen Slack-Bot im Socket-Mode empfangen und darauf reagieren.", - "title": "Slack", - "whoamiTip": "💡 Tipp: Sende /whoami an den Bot, um die Kanal-ID zu erhalten." - }, - "soulModeRequired": "Dieser Agent hat den autonomen Modus nicht aktiviert. Kanal-Funktionen werden nicht funktionieren. Bitte aktivieren Sie ihn in den Agenten-Einstellungen.", - "tab": "Kanäle", - "telegram": { - "botToken": "Bot-Token", - "botTokenPlaceholder": "Geben Sie Ihr Telegram-Bot-Token ein", - "chatIds": "Erlaubte Chat-IDs", - "chatIdsHint": "Kommagetrennt. Leer lassen für alle Chats.", - "chatIdsPlaceholder": "123456789, 987654321", - "description": "Nachrichten über einen Telegram-Bot mit Long Polling empfangen und beantworten.", - "title": "Telegram" - }, - "title": "Kanäle", - "updateError": "Fehler beim Aktualisieren des Kanals", - "wechat": { - "addAccount": "WeChat-Konto hinzufügen", - "chatIds": "Erlaubte Benutzer-IDs", - "chatIdsHint": "Durch Komma getrennt. Leer lassen, um alle Benutzer zuzulassen.", - "chatIdsPlaceholder": "wxid_abc123, wxid_def456", - "connected": "Verbunden", - "description": "Nachrichten über WeChat mit der iLink Bot API empfangen und beantworten.", - "disconnected": "Getrennt", - "loginHint": "Die erste Anmeldung erfordert das Scannen eines QR-Codes. Überprüfe die App-Logs für die Anmelde-URL.", - "qrHint": "Öffnen Sie WeChat auf Ihrem Telefon, scannen Sie den QR-Code, um sich anzumelden.", - "qrTitle": "WeChat-QR-Login", - "title": "WeChat", - "whoamiTip": "Tipp: Sende /whoami in WeChat, um die ID eines Benutzers zu erhalten." - } - }, - "heartbeat": { - "enabled": "Heartbeat aktivieren", - "enabledHelper": "Wenn aktiviert und heartbeat.md im Workspace existiert, liest der Scheduler dessen Inhalt und sendet ihn im konfigurierten Intervall an die Hauptsitzung.", - "interval": "Intervall (Minuten)", - "intervalHelper": "Wie oft der Heartbeat ausgeführt wird, in Minuten." - }, - "sandbox": { - "description": "Beschränkt den Dateisystem- und Netzwerkzugriff des Agenten auf das Workspace-Verzeichnis mittels OS-Level-Sandboxing.", - "helper": "Dateisystem- und Befehlszugriff auf den Workspace-Pfad beschränken.", - "label": "Sandbox-Modus" - }, - "scheduler": { - "cron": { - "helper": "Standard-Cron-Ausdruck (z.B. '*/30 * * * *' für alle 30 Minuten).", - "label": "Cron-Ausdruck", - "placeholder": "*/30 * * * *" - }, - "description": "Der Scheduler löst den Agenten automatisch nach Zeitplan aus.", - "enabled": "Scheduler aktivieren", - "enabledHelper": "Wenn aktiviert, wird der Agent automatisch basierend auf dem konfigurierten Zeitplan ausgeführt.", - "errors": { - "invalidCron": "Ungültiger Cron-Ausdruck", - "invalidDelay": "Verzögerung muss eine positive Zahl sein", - "invalidInterval": "Intervall muss eine positive Zahl sein" - }, - "interval": { - "helper": "Wie oft der Agent ausgeführt werden soll, in Sekunden.", - "label": "Intervall (Sekunden)" - }, - "lastRun": "Letzte Ausführung", - "never": "Nie", - "nextRun": "Nächste Ausführung", - "oneTime": { - "helper": "Einmalige Verzögerung in Sekunden vor der Agentenausführung.", - "label": "Verzögerung (Sekunden)" - }, - "status": { - "running": "Läuft", - "stopped": "Gestoppt", - "tickInProgress": "Tick läuft" - }, - "tab": "Scheduler", - "title": "Scheduler-Konfiguration", - "type": { - "cron": "Cron-Ausdruck", - "interval": "Festes Intervall", - "label": "Zeitplantyp", - "one-time": "Einmalige Verzögerung" - } + "channels": { + "add": "Hinzufügen", + "bindAgent": "Bind-Agent", + "chatIdsAutoTrackHint": "Wenn es leer bleibt, verfolgt das System automatisch: Sie müssen dem Bot auf der Plattform zuerst eine Nachricht senden, dann zeichnet das System die Chat-ID für zukünftige Benachrichtigungen auf.", + "comingSoon": "Demnächst verfügbar", + "connected": "Verbunden", + "connecting": "Verbinden", + "createError": "Fehler beim Erstellen des Kanals", + "deleteConfirm": "Kanal „{{name}}“ löschen?", + "deleteError": "Fehler beim Löschen des Kanals", + "description": "Verbinden Sie Ihren Agenten mit Messaging-Plattformen.", + "disconnected": "Getrennt", + "discord": { + "botToken": "Bot-Token", + "botTokenPlaceholder": "Gib dein Discord-Bot-Token ein", + "channelIds": "Erlaubte Kanal-IDs", + "channelIdsHint": "Format: channel:id oder dm:id. Leer lassen, um alle zuzulassen.", + "channelIdsPlaceholder": "channel:123456789, dm:987654321", + "description": "Nachrichten über einen Discord-Bot über WebSocket-Gateway empfangen und darauf reagieren.", + "title": "Discord", + "whoamiTip": "💡 Tipp: Sende /whoami an den Bot, um deine Kanal-ID im richtigen Format zu erhalten." }, - "soul": { - "description": "Die Soul definiert die Persönlichkeit und das Grundverhalten des Agenten. Sie wird aus soul.md im Workspace-Stammverzeichnis gelesen.", - "empty": "Keine soul.md im Workspace-Stammverzeichnis gefunden. Erstellen Sie eine, um die Persönlichkeit des Agenten zu definieren.", - "enabled": "Soul aktivieren", - "enabledHelper": "Wenn aktiviert, liest der Agent soul.md aus dem Workspace-Stammverzeichnis und stellt es dem System-Prompt voran.", - "fileLabel": "soul.md", - "preview": "Soul-Vorschau", - "tab": "Soul", - "title": "Soul-Konfiguration" - }, - "tasks": { - "add": "Aufgabe hinzufügen", - "cancel": "Abbrechen", - "channels": { - "label": "An Kanäle senden", - "noActiveChatIds": "Die ausgewählten Kanäle verfügen über keine verfügbaren Empfänger (Chat-ID). Aufgabenergebnisse werden möglicherweise nicht zugestellt. Bitte senden Sie dem Bot zunächst eine Nachricht auf der Plattform.", - "placeholder": "Wähle Kanäle, um Ergebnisse zu erhalten" - }, - "cronPlaceholder": "z.B. 0 9 * * * (täglich um 9 Uhr)", - "delete": { - "confirm": "Sind Sie sicher, dass Sie diese Aufgabe löschen möchten?", - "label": "Löschen" - }, - "edit": "Bearbeiten", - "empty": "Keine geplanten Aufgaben. Fügen Sie eine hinzu, um zu beginnen.", - "error": { - "createFailed": "Fehler beim Erstellen der Aufgabe", - "deleteFailed": "Fehler beim Löschen der Aufgabe", - "loadFailed": "Aufgaben konnten nicht geladen werden", - "runFailed": "Aufgabe konnte nicht ausgeführt werden", - "updateFailed": "Aktualisierung der Aufgabe fehlgeschlagen" - }, - "frequency": { - "everyPrefix": "Alle", - "everySuffix": "Minuten", - "label": "Ausführungshäufigkeit" - }, - "intervalPlaceholder": "Mindestens 1", - "intervalUnit": "Minuten", - "lastRun": "Letzte Ausführung", - "logs": { - "cancelled": "Storniert", - "completed": "Abgeschlossen", - "duration": "Dauer", - "empty": "Noch keine Ausführungshistorie.", - "failed": "Fehlgeschlagen", - "justNow": "soeben", - "label": "Ausführungshistorie", - "loadError": "Fehler beim Laden des Laufverlaufs", - "result": "Ergebnis", - "runAt": "Ausgeführt am", - "running": "Läuft...", - "search": "Suchprotokolle...", - "status": "Status", - "viewSession": "Sitzung anzeigen" - }, - "name": { - "label": "Name", - "placeholder": "z.B. Tägliche Code-Überprüfung" - }, - "nextRun": "Nächste Ausführung", - "oncePlaceholder": "Datum und Uhrzeit auswählen", - "pause": "Pausieren", - "prompt": { - "expand": "Editor erweitern", - "label": "Prompt", - "placeholder": "Was soll der Agent tun, wenn diese Aufgabe ausgeführt wird?" - }, - "resume": "Fortsetzen", - "run": "Ausführen", - "runTriggered": "Aufgabe ausgelöst", - "save": "Speichern", - "scheduleType": { - "cron": "Cron", - "interval": "Intervall", - "once": "Einmalig" - }, - "status": { - "active": "Aktiv", - "completed": "Abgeschlossen", - "paused": "Pausiert" - }, - "tab": "Aufgaben", - "time": { - "hoursAgo": "vor {{count}} Stunden", - "minutesAgo": "vor {{count}} m" - }, - "timeout": { - "label": "Maximale Ausführungszeit", - "placeholder": "Kein Limit" - }, - "title": "Geplante Aufgaben" + "error": "Fehler", + "feishu": { + "appId": "App-ID", + "appIdPlaceholder": "Geben Sie Ihre Feishu-App-ID ein", + "appSecret": "App-Geheimnis", + "appSecretPlaceholder": "Geben Sie Ihr Feishu-App-Geheimnis ein", + "chatIds": "Erlaubte Chat-IDs", + "chatIdsHint": "Kommagetrennte Chat-IDs. Leer lassen, um alle Chats zuzulassen.", + "chatIdsPlaceholder": "oc_xxxxx, oc_yyyyy", + "connected": "Verbunden", + "description": "Nachrichten über einen Feishu/Lark-Bot per WebSocket empfangen und beantworten.", + "domain": "Domain", + "domainFeishu": "Feishu (China)", + "domainLark": "Lark (International)", + "encryptKey": "Verschlüsselungsschlüssel", + "encryptKeyPlaceholder": "Geben Sie den Verschlüsselungsschlüssel Ihrer Feishu-App ein", + "loginHint": "Keine Anmeldeinformationen konfiguriert. Aktivieren Sie den Kanal, um die QR-Code-Registrierung zu starten, oder geben Sie App-ID und App-Secret manuell ein.", + "qrExpired": "QR-Code abgelaufen. Bitte versuchen Sie es erneut, indem Sie den Kanal wechseln.", + "qrHint": "Warten auf QR-Code-Scan...", + "qrScanHint": "Öffne Feishu auf deinem Handy und scanne den QR-Code, um eine Bot-App zu erstellen.", + "qrTitle": "Feishu-QR-Registrierung", + "title": "Feishu", + "verificationToken": "Verifizierungstoken", + "verificationTokenPlaceholder": "Geben Sie das Verifizierungstoken Ihrer Feishu-App ein" }, - "warning": { - "bypassPermissions": "CherryClaw-Agenten laufen standardmäßig im Vollautomatikmodus. Alle Tools werden ohne Genehmigungsanfrage ausgeführt." + "logs": "Protokolle", + "noInstances": "Keine {{type}}-Kanäle konfiguriert. Klicken Sie auf „+ Hinzufügen“, um einen zu erstellen.", + "noLogs": "Noch keine Protokolle", + "notifyReceiver": "Aufgabenbenachrichtigungen empfangen", + "notifyReceiverHint": "Scheduler-Aufgabenergebnisse an diesen Kanal senden.", + "qq": { + "appId": "App-ID", + "appIdPlaceholder": "Geben Sie Ihre QQ Bot App-ID ein", + "chatIds": "Erlaubte Chat-IDs", + "chatIdsHint": "Format: c2c:openid, group:groupid, channel:channelid. Leer lassen für alle.", + "chatIdsPlaceholder": "c2c:abc123, group:xyz789", + "clientSecret": "Client Secret", + "clientSecretPlaceholder": "Geben Sie Ihr QQ Bot Client Secret ein", + "description": "Nachrichten über die offizielle QQ Bot API empfangen und beantworten.", + "title": "QQ", + "whoamiTip": "💡 Tipp: Senden Sie /whoami an den Bot, um Ihre Chat-ID im richtigen Format zu erhalten." + }, + "security": { + "inheritFromAgent": "Von Agent erben", + "permissionMode": "Kanalberechtigungsmodus", + "permissionModeHint": "Überschreibe den Berechtigungsmodus des Agenten für Nachrichten aus diesem Kanal. „Vererben“ verwendet die Standardeinstellung des Agenten." + }, + "selectAgent": "Wählen Sie einen Agenten zum Binden aus", + "slack": { + "appToken": "App-Level-Token", + "appTokenPlaceholder": "xapp-...", + "botToken": "Bot-Token", + "botTokenPlaceholder": "xoxb-...", + "channelIds": "Erlaubte Kanal-IDs", + "channelIdsHint": "Slack-Kanal-IDs. Leer lassen, um alle zu erlauben.", + "channelIdsPlaceholder": "C01234567, D89012345", + "description": "Nachrichten über einen Slack-Bot im Socket-Mode empfangen und darauf reagieren.", + "title": "Slack", + "whoamiTip": "💡 Tipp: Sende /whoami an den Bot, um die Kanal-ID zu erhalten." + }, + "tab": "Kanäle", + "telegram": { + "botToken": "Bot-Token", + "botTokenPlaceholder": "Geben Sie Ihr Telegram-Bot-Token ein", + "chatIds": "Erlaubte Chat-IDs", + "chatIdsHint": "Kommagetrennt. Leer lassen für alle Chats.", + "chatIdsPlaceholder": "123456789, 987654321", + "description": "Nachrichten über einen Telegram-Bot mit Long Polling empfangen und beantworten.", + "title": "Telegram" + }, + "title": "Kanäle", + "updateError": "Fehler beim Aktualisieren des Kanals", + "wechat": { + "addAccount": "WeChat-Konto hinzufügen", + "chatIds": "Erlaubte Benutzer-IDs", + "chatIdsHint": "Durch Komma getrennt. Leer lassen, um alle Benutzer zuzulassen.", + "chatIdsPlaceholder": "wxid_abc123, wxid_def456", + "connected": "Verbunden", + "description": "Nachrichten über WeChat mit der iLink Bot API empfangen und beantworten.", + "disconnected": "Getrennt", + "loginHint": "Die erste Anmeldung erfordert das Scannen eines QR-Codes. Überprüfe die App-Logs für die Anmelde-URL.", + "qrHint": "Öffnen Sie WeChat auf Ihrem Telefon, scannen Sie den QR-Code, um sich anzumelden.", + "qrTitle": "WeChat-QR-Login", + "title": "WeChat", + "whoamiTip": "Tipp: Sende /whoami in WeChat, um die ID eines Benutzers zu erhalten." } }, "delete": { @@ -339,15 +190,20 @@ "home": { "welcome_title": "Wovon sollen wir heute sprechen?" }, + "icon": { + "type": "[to be translated]:Agent Icon" + }, "input": { - "placeholder": "Gib hier deine Nachricht ein, senden mit {{key}} – @ Pfad auswählen, / Befehl auswählen", - "soul_placeholder": "Hilf mir, mich mit Telegram zu verbinden" + "placeholder": "Gib hier deine Nachricht ein, senden mit {{key}} – @ Pfad auswählen, / Befehl auswählen" }, "list": { "error": { "failed": "Agent-Liste abrufen fehlgeschlagen" } }, + "manage": { + "title": "[to be translated]:Manage Agents" + }, "pin": { "title": "Pin-Agent" }, @@ -712,12 +568,6 @@ "tab": "Fähigkeiten", "title": "Installierte Fertigkeiten" }, - "soulMode": { - "description": "Wenn aktiviert, verwendet der Agent eine benutzerdefinierte Systemaufforderung aus Workspace-Soul-Dateien, fügt autonome Aufgabenverwaltungstools ein und deaktiviert interaktive Tools, die sich nicht für den autonomen Betrieb eignen.", - "disabledBadge": "Deaktiviert durch den autonomen Modus", - "disabledTooltip": "Dieses Tool wird automatisch deaktiviert, wenn der Autonome Modus aktiv ist.", - "title": "Autonomer Modus" - }, "tooling": { "mcp": { "description": "Verbinden Sie MCP-Server, um weitere Tools freizuschalten, die oben vorab autorisiert werden können.", @@ -784,6 +634,94 @@ } }, "sidebar_title": "Agenten", + "skill": { + "manage": { + "title": "[to be translated]:Manage skills" + } + }, + "tasks": { + "add": "Aufgabe hinzufügen", + "cancel": "Abbrechen", + "channels": { + "label": "An Kanäle senden", + "noActiveChatIds": "Die ausgewählten Kanäle verfügen über keine verfügbaren Empfänger (Chat-ID). Aufgabenergebnisse werden möglicherweise nicht zugestellt. Bitte senden Sie dem Bot zunächst eine Nachricht auf der Plattform.", + "placeholder": "Wähle Kanäle, um Ergebnisse zu erhalten" + }, + "cronPlaceholder": "z.B. 0 9 * * * (täglich um 9 Uhr)", + "delete": { + "confirm": "Sind Sie sicher, dass Sie diese Aufgabe löschen möchten?", + "label": "Löschen" + }, + "edit": "Bearbeiten", + "empty": "Keine geplanten Aufgaben. Fügen Sie eine hinzu, um zu beginnen.", + "error": { + "createFailed": "Fehler beim Erstellen der Aufgabe", + "deleteFailed": "Fehler beim Löschen der Aufgabe", + "loadFailed": "Aufgaben konnten nicht geladen werden", + "runFailed": "Aufgabe konnte nicht ausgeführt werden", + "updateFailed": "Aktualisierung der Aufgabe fehlgeschlagen" + }, + "frequency": { + "everyPrefix": "Alle", + "everySuffix": "Minuten", + "label": "Ausführungshäufigkeit" + }, + "intervalPlaceholder": "Mindestens 1", + "intervalUnit": "Minuten", + "lastRun": "Letzte Ausführung", + "logs": { + "cancelled": "Storniert", + "completed": "Abgeschlossen", + "duration": "Dauer", + "empty": "Noch keine Ausführungshistorie.", + "failed": "Fehlgeschlagen", + "justNow": "soeben", + "label": "Ausführungshistorie", + "loadError": "Fehler beim Laden des Laufverlaufs", + "result": "Ergebnis", + "runAt": "Ausgeführt am", + "running": "Läuft...", + "search": "Suchprotokolle...", + "status": "Status", + "viewSession": "Sitzung anzeigen" + }, + "name": { + "label": "Name", + "placeholder": "z.B. Tägliche Code-Überprüfung" + }, + "nextRun": "Nächste Ausführung", + "oncePlaceholder": "Datum und Uhrzeit auswählen", + "pause": "Pausieren", + "prompt": { + "expand": "Editor erweitern", + "label": "Prompt", + "placeholder": "Was soll der Agent tun, wenn diese Aufgabe ausgeführt wird?" + }, + "resume": "Fortsetzen", + "run": "Ausführen", + "runTriggered": "Aufgabe ausgelöst", + "save": "Speichern", + "scheduleType": { + "cron": "Cron", + "interval": "Intervall", + "once": "Einmalig" + }, + "status": { + "active": "Aktiv", + "completed": "Abgeschlossen", + "paused": "Pausiert" + }, + "tab": "Aufgaben", + "time": { + "hoursAgo": "vor {{count}} Stunden", + "minutesAgo": "vor {{count}} m" + }, + "timeout": { + "label": "Maximale Ausführungszeit", + "placeholder": "Kein Limit" + }, + "title": "Geplante Aufgaben" + }, "todo": { "mock": { "actions": { @@ -935,6 +873,14 @@ "description": "Führt Shell-Befehle in deiner Umgebung aus", "label": "Bash" }, + "CherryConfig": { + "description": "[to be translated]:Inspects and manages this agent configuration and channels", + "label": "[to be translated]:Agent Config" + }, + "CherryCron": { + "description": "Verwaltet den In-App-Planer", + "label": "Scheduler" + }, "CherryKbManage": { "description": "Fügt Dokumente zu Ihren Wissensdatenbanken hinzu, löscht sie oder aktualisiert sie", "label": "Wissen verwalten" @@ -943,6 +889,10 @@ "description": "Durchsucht Ihre Wissensdatenbanken", "label": "Wissenskontrolle" }, + "CherryNotify": { + "description": "[to be translated]:Sends a notification through a connected channel", + "label": "[to be translated]:Notify" + }, "CherryWebFetch": { "description": "Ruft eine Webseite ab und liest sie", "label": "Web abrufen" @@ -951,10 +901,6 @@ "description": "Durchsucht das Web über Ihren konfigurierten Anbieter", "label": "Websuche" }, - "ClawCron": { - "description": "Verwaltet den In-App-Planer", - "label": "Scheduler" - }, "Edit": { "description": "Führt gezielte Bearbeitungen an bestimmten Dateien durch", "label": "Bearbeiten" @@ -1087,6 +1033,7 @@ "clear": { "content": "Das Leeren von Themen löscht alle Themen und Dateien unter dem Assistenten. Möchten Sie fortfahren?", "menu_title": "Lösche alle Assistenten-Konversationen", + "success_title": "[to be translated]:Cleared {{count}} topics", "title": "Themen leeren" }, "copy": { @@ -2986,11 +2933,10 @@ "placeholder": "Gib dem Agenten einen Namen" }, "permission_mode": { - "help": "Das Wechseln zu bypassPermissions entspricht dem Aktivieren des Soul-Modus", "label": "Berechtigungsmodus", "option": { "acceptEdits": "Änderungen übernehmen", - "bypassPermissions": "Berechtigungen umgehen (Seele)", + "bypassPermissions": "Berechtigungen umgehen", "default": "Standard", "plan": "Plan-Modus" } @@ -3002,10 +2948,6 @@ "small_model": { "hint": "Leichtgewichtige Überprüfungen und Formatierung", "label": "Kleines Modell (optional)" - }, - "soul_enabled": { - "help": "Wenn aktiviert, wird bypassPermissions automatisch gewährt", - "label": "Seelenmodus" } }, "model_config": "Modellkonfiguration", @@ -5331,6 +5273,13 @@ "auto_switch_to_topics": "Automatisch zu Themenansicht wechseln", "title": "Erweiterte Einstellungen" }, + "agent": { + "position": { + "label": "[to be translated]:Session position", + "left": "[to be translated]:Left", + "right": "[to be translated]:Right" + } + }, "appearance": { "title": "Erscheinung" }, @@ -7596,6 +7545,7 @@ }, "title": { "apps": "Mini-Apps", + "chat": "[to be translated]:Chat", "code": "Code", "files": "Datei", "home": "Startseite", diff --git a/src/renderer/i18n/translate/el-gr.json b/src/renderer/i18n/translate/el-gr.json index 8f1cb677a79..8f869874b5e 100644 --- a/src/renderer/i18n/translate/el-gr.json +++ b/src/renderer/i18n/translate/el-gr.json @@ -31,262 +31,113 @@ "submit": "Υποβολή", "title": "Ερωτήσεις από τον πράκτορα" }, - "cherryClaw": { - "channels": { - "add": "Προσθήκη", - "bindAgent": "Δέσμευση Πράκτορα", - "chatIdsAutoTrackHint": "Όταν μείνει κενό, το σύστημα θα παρακολουθεί αυτόματα: πρέπει πρώτα να στείλετε ένα μήνυμα στο Bot στην πλατφόρμα, τότε το σύστημα θα καταγράψει το Chat ID για μελλοντικές ειδοποιήσεις.", - "comingSoon": "Σύντομα διαθέσιμο", + "channels": { + "add": "Προσθήκη", + "bindAgent": "Δέσμευση Πράκτορα", + "chatIdsAutoTrackHint": "Όταν μείνει κενό, το σύστημα θα παρακολουθεί αυτόματα: πρέπει πρώτα να στείλετε ένα μήνυμα στο Bot στην πλατφόρμα, τότε το σύστημα θα καταγράψει το Chat ID για μελλοντικές ειδοποιήσεις.", + "comingSoon": "Σύντομα διαθέσιμο", + "connected": "Συνδεδεμένος", + "connecting": "Σύνδεση", + "createError": "Αποτυχία δημιουργίας καναλιού", + "deleteConfirm": "Διαγραφή καναλιού \"{{name}}\";", + "deleteError": "Αποτυχία διαγραφής καναλιού", + "description": "Συνδέστε τον πράκτορά σας σε πλατφόρμες μηνυμάτων.", + "disconnected": "Αποσυνδέθηκε", + "discord": { + "botToken": "Διακριτικό Bot", + "botTokenPlaceholder": "Εισάγετε το διακριτικό του bot σας στο Discord", + "channelIds": "Επιτρεπόμενα Αναγνωριστικά Καναλιών", + "channelIdsHint": "Μορφή: channel:id ή dm:id. Αφήστε κενό για να επιτρέψετε όλα.", + "channelIdsPlaceholder": "κανάλι:123456789, dm:987654321", + "description": "Λάβετε και απαντήστε σε μηνύματα μέσω ενός bot Discord χρησιμοποιώντας την πύλη WebSocket.", + "title": "Discord", + "whoamiTip": "💡 Συμβουλή: Στείλτε /whoami στο bot για να λάβετε το ID του καναλιού σας στη σωστή μορφή." + }, + "error": "Σφάλμα", + "feishu": { + "appId": "App ID", + "appIdPlaceholder": "Εισαγάγετε το App ID της εφαρμογής Feishu", + "appSecret": "App Secret", + "appSecretPlaceholder": "Εισαγάγετε το App Secret της εφαρμογής Feishu", + "chatIds": "Επιτρεπόμενα Chat IDs", + "chatIdsHint": "Chat IDs διαχωρισμένα με κόμμα. Αφήστε κενό για να επιτρέψετε όλες τις συνομιλίες.", + "chatIdsPlaceholder": "oc_xxxxx, oc_yyyyy", "connected": "Συνδεδεμένος", - "connecting": "Σύνδεση", - "createError": "Αποτυχία δημιουργίας καναλιού", - "deleteConfirm": "Διαγραφή καναλιού \"{{name}}\";", - "deleteError": "Αποτυχία διαγραφής καναλιού", - "description": "Συνδέστε τον πράκτορά σας σε πλατφόρμες μηνυμάτων.", + "description": "Λήψη και απάντηση μηνυμάτων μέσω bot Feishu/Lark χρησιμοποιώντας WebSocket.", + "domain": "Τομέας", + "domainFeishu": "Feishu (Κίνα)", + "domainLark": "Lark (Διεθνές)", + "encryptKey": "Κλειδί κρυπτογράφησης", + "encryptKeyPlaceholder": "Εισαγάγετε το κλειδί κρυπτογράφησης της εφαρμογής Feishu", + "loginHint": "Δεν έχουν διαμορφωθεί διαπιστευτήρια. Ενεργοποιήστε το κανάλι για να ξεκινήσει η εγγραφή μέσω κωδικού QR ή εισάγετε χειροκίνητα το App ID και το App Secret.", + "qrExpired": "Ο κωδικός QR έληξε. Προσπαθήστε ξανά εναλλάσσοντας το κανάλι.", + "qrHint": "Αναμονή για σάρωση κωδικού QR...", + "qrScanHint": "Άνοιξε το Feishu στο κινητό σου και σάρωσε τον κωδικό QR για να δημιουργήσεις μια εφαρμογή bot.", + "qrTitle": "Εγγραφή μέσω QR Feishu", + "title": "Feishu", + "verificationToken": "Token επαλήθευσης", + "verificationTokenPlaceholder": "Εισαγάγετε το token επαλήθευσης της εφαρμογής Feishu" + }, + "logs": "Κούτσουρα", + "noInstances": "Δεν έχουν διαμορφωθεί κανάλια {{type}}. Κάντε κλικ στο \"+ Προσθήκη\" για να δημιουργήσετε ένα.", + "noLogs": "Δεν υπάρχουν αρχεία καταγραφής ακόμα", + "notifyReceiver": "Λήψη ειδοποιήσεων εργασιών", + "notifyReceiverHint": "Αποστολή αποτελεσμάτων προγραμματισμένων εργασιών σε αυτό το κανάλι.", + "qq": { + "appId": "App ID", + "appIdPlaceholder": "Εισάγετε το App ID του QQ Bot σας", + "chatIds": "Επιτρεπόμενα ID συνομιλίας", + "chatIdsHint": "Μορφή: c2c:openid, group:groupid, channel:channelid. Αφήστε κενό για να επιτρέψετε όλα.", + "chatIdsPlaceholder": "c2c:abc123, group:xyz789", + "clientSecret": "Client Secret", + "clientSecretPlaceholder": "Εισάγετε το Client Secret του QQ Bot σας", + "description": "Λήψη και απάντηση μηνυμάτων μέσω του επίσημου API του QQ Bot.", + "title": "QQ", + "whoamiTip": "💡 Συμβουλή: Στείλτε /whoami στο bot για να λάβετε το ID συνομιλίας σας στη σωστή μορφή." + }, + "security": { + "inheritFromAgent": "Κληρονομήστε από τον πράκτορα", + "permissionMode": "Λειτουργία Αδειών Καναλιού", + "permissionModeHint": "Παράκαμψη της λειτουργίας αδειών του πράκτορα για μηνύματα από αυτό το κανάλι. Η επιλογή «Κληρονόμηση» χρησιμοποιεί την προεπιλογή του πράκτορα." + }, + "selectAgent": "Επιλέξτε έναν πράκτορα για σύνδεση", + "slack": { + "appToken": "Διακριτικό Επιπέδου Εφαρμογής", + "appTokenPlaceholder": "xapp-...", + "botToken": "Διακριτικό Bot", + "botTokenPlaceholder": "xoxb-...", + "channelIds": "Επιτρεπόμενα Αναγνωριστικά Καναλιών", + "channelIdsHint": "Αναγνωριστικά καναλιών του Slack. Αφήστε κενό για να επιτρέπονται όλα.", + "channelIdsPlaceholder": "C01234567, D89012345", + "description": "Λάβετε και απαντήστε σε μηνύματα μέσω ενός bot του Slack χρησιμοποιώντας τη λειτουργία Socket.", + "title": "Slack", + "whoamiTip": "💡 Συμβουλή: Στείλε /whoami στο bot για να πάρεις το ID του καναλιού." + }, + "tab": "Κανάλια", + "telegram": { + "botToken": "Token Bot", + "botTokenPlaceholder": "Εισάγετε το token του Telegram bot σας", + "chatIds": "Επιτρεπόμενα ID συνομιλίας", + "chatIdsHint": "Διαχωρισμένα με κόμμα. Αφήστε κενό για να επιτρέψετε όλες τις συνομιλίες.", + "chatIdsPlaceholder": "123456789, 987654321", + "description": "Λήψη και απάντηση μηνυμάτων μέσω Telegram bot χρησιμοποιώντας long polling.", + "title": "Telegram" + }, + "title": "Κανάλια", + "updateError": "Αποτυχία ενημέρωσης καναλιού", + "wechat": { + "addAccount": "Προσθέστε λογαριασμό WeChat", + "chatIds": "Επιτρεπόμενα Αναγνωριστικά Χρηστών", + "chatIdsHint": "Διαχωρισμένα με κόμμα. Αφήστε κενό για να επιτρέπονται όλοι οι χρήστες.", + "chatIdsPlaceholder": "wxid_abc123, wxid_def456", + "connected": "Συνδεδεμένος", + "description": "Λάβετε και απαντήστε σε μηνύματα μέσω WeChat χρησιμοποιώντας το iLink Bot API.", "disconnected": "Αποσυνδέθηκε", - "discord": { - "botToken": "Διακριτικό Bot", - "botTokenPlaceholder": "Εισάγετε το διακριτικό του bot σας στο Discord", - "channelIds": "Επιτρεπόμενα Αναγνωριστικά Καναλιών", - "channelIdsHint": "Μορφή: channel:id ή dm:id. Αφήστε κενό για να επιτρέψετε όλα.", - "channelIdsPlaceholder": "κανάλι:123456789, dm:987654321", - "description": "Λάβετε και απαντήστε σε μηνύματα μέσω ενός bot Discord χρησιμοποιώντας την πύλη WebSocket.", - "title": "Discord", - "whoamiTip": "💡 Συμβουλή: Στείλτε /whoami στο bot για να λάβετε το ID του καναλιού σας στη σωστή μορφή." - }, - "error": "Σφάλμα", - "feishu": { - "appId": "App ID", - "appIdPlaceholder": "Εισαγάγετε το App ID της εφαρμογής Feishu", - "appSecret": "App Secret", - "appSecretPlaceholder": "Εισαγάγετε το App Secret της εφαρμογής Feishu", - "chatIds": "Επιτρεπόμενα Chat IDs", - "chatIdsHint": "Chat IDs διαχωρισμένα με κόμμα. Αφήστε κενό για να επιτρέψετε όλες τις συνομιλίες.", - "chatIdsPlaceholder": "oc_xxxxx, oc_yyyyy", - "connected": "Συνδεδεμένος", - "description": "Λήψη και απάντηση μηνυμάτων μέσω bot Feishu/Lark χρησιμοποιώντας WebSocket.", - "domain": "Τομέας", - "domainFeishu": "Feishu (Κίνα)", - "domainLark": "Lark (Διεθνές)", - "encryptKey": "Κλειδί κρυπτογράφησης", - "encryptKeyPlaceholder": "Εισαγάγετε το κλειδί κρυπτογράφησης της εφαρμογής Feishu", - "loginHint": "Δεν έχουν διαμορφωθεί διαπιστευτήρια. Ενεργοποιήστε το κανάλι για να ξεκινήσει η εγγραφή μέσω κωδικού QR ή εισάγετε χειροκίνητα το App ID και το App Secret.", - "qrExpired": "Ο κωδικός QR έληξε. Προσπαθήστε ξανά εναλλάσσοντας το κανάλι.", - "qrHint": "Αναμονή για σάρωση κωδικού QR...", - "qrScanHint": "Άνοιξε το Feishu στο κινητό σου και σάρωσε τον κωδικό QR για να δημιουργήσεις μια εφαρμογή bot.", - "qrTitle": "Εγγραφή μέσω QR Feishu", - "title": "Feishu", - "verificationToken": "Token επαλήθευσης", - "verificationTokenPlaceholder": "Εισαγάγετε το token επαλήθευσης της εφαρμογής Feishu" - }, - "logs": "Κούτσουρα", - "noInstances": "Δεν έχουν διαμορφωθεί κανάλια {{type}}. Κάντε κλικ στο \"+ Προσθήκη\" για να δημιουργήσετε ένα.", - "noLogs": "Δεν υπάρχουν αρχεία καταγραφής ακόμα", - "notifyReceiver": "Λήψη ειδοποιήσεων εργασιών", - "notifyReceiverHint": "Αποστολή αποτελεσμάτων προγραμματισμένων εργασιών σε αυτό το κανάλι.", - "qq": { - "appId": "App ID", - "appIdPlaceholder": "Εισάγετε το App ID του QQ Bot σας", - "chatIds": "Επιτρεπόμενα ID συνομιλίας", - "chatIdsHint": "Μορφή: c2c:openid, group:groupid, channel:channelid. Αφήστε κενό για να επιτρέψετε όλα.", - "chatIdsPlaceholder": "c2c:abc123, group:xyz789", - "clientSecret": "Client Secret", - "clientSecretPlaceholder": "Εισάγετε το Client Secret του QQ Bot σας", - "description": "Λήψη και απάντηση μηνυμάτων μέσω του επίσημου API του QQ Bot.", - "title": "QQ", - "whoamiTip": "💡 Συμβουλή: Στείλτε /whoami στο bot για να λάβετε το ID συνομιλίας σας στη σωστή μορφή." - }, - "security": { - "inheritFromAgent": "Κληρονομήστε από τον πράκτορα", - "permissionMode": "Λειτουργία Αδειών Καναλιού", - "permissionModeHint": "Παράκαμψη της λειτουργίας αδειών του πράκτορα για μηνύματα από αυτό το κανάλι. Η επιλογή «Κληρονόμηση» χρησιμοποιεί την προεπιλογή του πράκτορα." - }, - "selectAgent": "Επιλέξτε έναν πράκτορα για σύνδεση", - "slack": { - "appToken": "Διακριτικό Επιπέδου Εφαρμογής", - "appTokenPlaceholder": "xapp-...", - "botToken": "Διακριτικό Bot", - "botTokenPlaceholder": "xoxb-...", - "channelIds": "Επιτρεπόμενα Αναγνωριστικά Καναλιών", - "channelIdsHint": "Αναγνωριστικά καναλιών του Slack. Αφήστε κενό για να επιτρέπονται όλα.", - "channelIdsPlaceholder": "C01234567, D89012345", - "description": "Λάβετε και απαντήστε σε μηνύματα μέσω ενός bot του Slack χρησιμοποιώντας τη λειτουργία Socket.", - "title": "Slack", - "whoamiTip": "💡 Συμβουλή: Στείλε /whoami στο bot για να πάρεις το ID του καναλιού." - }, - "soulModeRequired": "Αυτός ο πράκτορας δεν έχει ενεργοποιημένη την Αυτόνομη Λειτουργία. Οι λειτουργίες καναλιού δεν θα λειτουργούν. Παρακαλώ ενεργοποιήστε την στις ρυθμίσεις του πράκτορα.", - "tab": "Κανάλια", - "telegram": { - "botToken": "Token Bot", - "botTokenPlaceholder": "Εισάγετε το token του Telegram bot σας", - "chatIds": "Επιτρεπόμενα ID συνομιλίας", - "chatIdsHint": "Διαχωρισμένα με κόμμα. Αφήστε κενό για να επιτρέψετε όλες τις συνομιλίες.", - "chatIdsPlaceholder": "123456789, 987654321", - "description": "Λήψη και απάντηση μηνυμάτων μέσω Telegram bot χρησιμοποιώντας long polling.", - "title": "Telegram" - }, - "title": "Κανάλια", - "updateError": "Αποτυχία ενημέρωσης καναλιού", - "wechat": { - "addAccount": "Προσθέστε λογαριασμό WeChat", - "chatIds": "Επιτρεπόμενα Αναγνωριστικά Χρηστών", - "chatIdsHint": "Διαχωρισμένα με κόμμα. Αφήστε κενό για να επιτρέπονται όλοι οι χρήστες.", - "chatIdsPlaceholder": "wxid_abc123, wxid_def456", - "connected": "Συνδεδεμένος", - "description": "Λάβετε και απαντήστε σε μηνύματα μέσω WeChat χρησιμοποιώντας το iLink Bot API.", - "disconnected": "Αποσυνδέθηκε", - "loginHint": "Η πρώτη σύνδεση απαιτεί σάρωση κωδικού QR. Ελέγξτε τα αρχεία καταγραφής της εφαρμογής για τη διεύθυνση URL σύνδεσης.", - "qrHint": "Άνοιξε το WeChat στο κινητό σου και σάρωσε τον κωδικό QR για να συνδεθείς.", - "qrTitle": "Σύνδεση με QR code στο WeChat", - "title": "WeChat", - "whoamiTip": "Συμβουλή: Στείλτε /whoami στο WeChat για να λάβετε το αναγνωριστικό ενός χρήστη." - } - }, - "heartbeat": { - "enabled": "Ενεργοποίηση Heartbeat", - "enabledHelper": "Όταν είναι ενεργοποιημένο και υπάρχει heartbeat.md στον χώρο εργασίας, ο προγραμματιστής διαβάζει το περιεχόμενό του και το στέλνει στην κύρια συνεδρία στο διαμορφωμένο διάστημα.", - "interval": "Διάστημα (λεπτά)", - "intervalHelper": "Πόσο συχνά εκτελείται το heartbeat, σε λεπτά." - }, - "sandbox": { - "description": "Περιορίζει την πρόσβαση του πράκτορα στο σύστημα αρχείων και το δίκτυο στον κατάλογο του χώρου εργασίας χρησιμοποιώντας sandboxing σε επίπεδο λειτουργικού συστήματος.", - "helper": "Περιορισμός πρόσβασης στο σύστημα αρχείων και εντολών στη διαδρομή του χώρου εργασίας.", - "label": "Λειτουργία Sandbox" - }, - "scheduler": { - "cron": { - "helper": "Τυπική έκφραση cron (π.χ. '*/30 * * * *' για κάθε 30 λεπτά).", - "label": "Έκφραση Cron", - "placeholder": "*/30 * * * *" - }, - "description": "Ο προγραμματιστής ενεργοποιεί τον πράκτορα αυτόματα σύμφωνα με ένα χρονοδιάγραμμα.", - "enabled": "Ενεργοποίηση προγραμματιστή", - "enabledHelper": "Όταν είναι ενεργοποιημένο, ο πράκτορας θα εκτελείται αυτόματα με βάση το διαμορφωμένο χρονοδιάγραμμα.", - "errors": { - "invalidCron": "Μη έγκυρη έκφραση cron", - "invalidDelay": "Η καθυστέρηση πρέπει να είναι θετικός αριθμός", - "invalidInterval": "Το διάστημα πρέπει να είναι θετικός αριθμός" - }, - "interval": { - "helper": "Πόσο συχνά πρέπει να εκτελείται ο πράκτορας, σε δευτερόλεπτα.", - "label": "Διάστημα (δευτερόλεπτα)" - }, - "lastRun": "Τελευταία εκτέλεση", - "never": "Ποτέ", - "nextRun": "Επόμενη εκτέλεση", - "oneTime": { - "helper": "Μοναδική καθυστέρηση σε δευτερόλεπτα πριν την εκτέλεση του πράκτορα.", - "label": "Καθυστέρηση (δευτερόλεπτα)" - }, - "status": { - "running": "Εκτελείται", - "stopped": "Σταματημένο", - "tickInProgress": "Tick σε εξέλιξη" - }, - "tab": "Προγραμματιστής", - "title": "Ρύθμιση προγραμματιστή", - "type": { - "cron": "Έκφραση Cron", - "interval": "Σταθερό διάστημα", - "label": "Τύπος χρονοδιαγράμματος", - "one-time": "Μοναδική καθυστέρηση" - } - }, - "soul": { - "description": "Η ψυχή καθορίζει την προσωπικότητα και τη βασική συμπεριφορά του πράκτορα. Διαβάζεται από το soul.md στη ρίζα του χώρου εργασίας.", - "empty": "Δεν βρέθηκε soul.md στη ρίζα του χώρου εργασίας. Δημιουργήστε ένα για να ορίσετε την προσωπικότητα του πράκτορα.", - "enabled": "Ενεργοποίηση ψυχής", - "enabledHelper": "Όταν είναι ενεργοποιημένο, ο πράκτορας διαβάζει το soul.md από τη ρίζα του χώρου εργασίας και το προσθέτει στην αρχή του system prompt.", - "fileLabel": "soul.md", - "preview": "Προεπισκόπηση ψυχής", - "tab": "Ψυχή", - "title": "Ρύθμιση ψυχής" - }, - "tasks": { - "add": "Προσθήκη εργασίας", - "cancel": "Ακύρωση", - "channels": { - "label": "Αποστολή σε Κανάλια", - "noActiveChatIds": "Τα επιλεγμένα κανάλια δεν διαθέτουν διαθέσιμους παραλήπτες (Chat ID). Τα αποτελέσματα της εργασίας ενδέχεται να μην παραδοθούν. Παρακαλώ στείλτε πρώτα ένα μήνυμα στο Bot στην πλατφόρμα.", - "placeholder": "Επιλέξτε κανάλια για να λαμβάνετε αποτελέσματα" - }, - "cronPlaceholder": "π.χ. 0 9 * * * (κάθε μέρα στις 9 π.μ.)", - "delete": { - "confirm": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτή την εργασία;", - "label": "Διαγραφή" - }, - "edit": "Επεξεργασία", - "empty": "Δεν υπάρχουν προγραμματισμένες εργασίες. Προσθέστε μία για να ξεκινήσετε.", - "error": { - "createFailed": "Αποτυχία δημιουργίας εργασίας", - "deleteFailed": "Αποτυχία διαγραφής εργασίας", - "loadFailed": "Αποτυχία φόρτωσης εργασιών", - "runFailed": "Αποτυχία εκτέλεσης εργασίας", - "updateFailed": "Αποτυχία ενημέρωσης εργασίας" - }, - "frequency": { - "everyPrefix": "Κάθε", - "everySuffix": "λεπτά", - "label": "Συχνότητα εκτέλεσης" - }, - "intervalPlaceholder": "Τουλάχιστον 1", - "intervalUnit": "λεπτά", - "lastRun": "Τελευταία εκτέλεση", - "logs": { - "cancelled": "Ακυρώθηκε", - "completed": "Ολοκληρώθηκε", - "duration": "Διάρκεια", - "empty": "Δεν υπάρχει ακόμα ιστορικό εκτελέσεων.", - "failed": "Απέτυχε", - "justNow": "μόλις τώρα", - "label": "Ιστορικό εκτελέσεων", - "loadError": "Αποτυχία φόρτωσης ιστορικού εκτέλεσης", - "result": "Αποτέλεσμα", - "runAt": "Εκτελέστηκε στις", - "running": "Τρέχει...", - "search": "Αναζήτηση αρχείων καταγραφής...", - "status": "Κατάσταση", - "viewSession": "Δείτε τη συνεδρία" - }, - "name": { - "label": "Όνομα", - "placeholder": "π.χ. Καθημερινή αναθεώρηση κώδικα" - }, - "nextRun": "Επόμενη εκτέλεση", - "oncePlaceholder": "Επιλέξτε ημερομηνία και ώρα", - "pause": "Παύση", - "prompt": { - "expand": "Αναπτύξτε τον επεξεργαστή", - "label": "Prompt", - "placeholder": "Τι πρέπει να κάνει ο πράκτορας όταν εκτελείται αυτή η εργασία;" - }, - "resume": "Συνέχιση", - "run": "Εκτέλεση", - "runTriggered": "Η εργασία ενεργοποιήθηκε", - "save": "Αποθήκευση", - "scheduleType": { - "cron": "Cron", - "interval": "Διάστημα", - "once": "Μία φορά" - }, - "status": { - "active": "Ενεργή", - "completed": "Ολοκληρώθηκε", - "paused": "Σε παύση" - }, - "tab": "Εργασίες", - "time": { - "hoursAgo": "{{count}}h πριν", - "minutesAgo": "{{count}}μ πριν" - }, - "timeout": { - "label": "Μέγιστος χρόνος εκτέλεσης", - "placeholder": "Χωρίς όριο" - }, - "title": "Προγραμματισμένες εργασίες" - }, - "warning": { - "bypassPermissions": "Οι πράκτορες CherryClaw εκτελούνται σε πλήρη αυτόματη λειτουργία από προεπιλογή. Όλα τα εργαλεία εκτελούνται χωρίς να ζητούν έγκριση." + "loginHint": "Η πρώτη σύνδεση απαιτεί σάρωση κωδικού QR. Ελέγξτε τα αρχεία καταγραφής της εφαρμογής για τη διεύθυνση URL σύνδεσης.", + "qrHint": "Άνοιξε το WeChat στο κινητό σου και σάρωσε τον κωδικό QR για να συνδεθείς.", + "qrTitle": "Σύνδεση με QR code στο WeChat", + "title": "WeChat", + "whoamiTip": "Συμβουλή: Στείλτε /whoami στο WeChat για να λάβετε το αναγνωριστικό ενός χρήστη." } }, "delete": { @@ -339,15 +190,20 @@ "home": { "welcome_title": "Τι θα συζητήσουμε σήμερα;" }, + "icon": { + "type": "[to be translated]:Agent Icon" + }, "input": { - "placeholder": "Εισάγετε το μήνυμά σας εδώ, στείλτε με {{key}} - @ επιλέξτε διαδρομή, / επιλέξτε εντολή", - "soul_placeholder": "Βοηθήστε με να συνδεθώ στο Telegram" + "placeholder": "Εισάγετε το μήνυμά σας εδώ, στείλτε με {{key}} - @ επιλέξτε διαδρομή, / επιλέξτε εντολή" }, "list": { "error": { "failed": "Αποτυχία καταχώρησης πρακτόρων." } }, + "manage": { + "title": "[to be translated]:Manage Agents" + }, "pin": { "title": "Προσάρτησε Πράκτορα" }, @@ -712,12 +568,6 @@ "tab": "Δεξιότητες", "title": "Εγκατεστημένες Δεξιότητες" }, - "soulMode": { - "description": "Όταν είναι ενεργοποιημένο, ο πράκτορας χρησιμοποιεί ένα προσαρμοσμένο σύστημα προτροπής από αρχεία soul του χώρου εργασίας, εισάγει αυτόνομα εργαλεία διαχείρισης εργασιών και απενεργοποιεί διαδραστικά εργαλεία που δεν είναι κατάλληλα για αυτόνομη λειτουργία.", - "disabledBadge": "Απενεργοποιημένο από Αυτόνομη Λειτουργία", - "disabledTooltip": "Αυτό το εργαλείο απενεργοποιείται αυτόματα όταν είναι ενεργή η Αυτόνομη Λειτουργία", - "title": "Αυτόνομη Λειτουργία" - }, "tooling": { "mcp": { "description": "Συνδέστε διακομιστές MCP για να ξεκλειδώσετε πρόσθετα εργαλεία που μπορείτε να εγκρίνετε παραπάνω.", @@ -784,6 +634,94 @@ } }, "sidebar_title": "Πράκτορες", + "skill": { + "manage": { + "title": "[to be translated]:Manage skills" + } + }, + "tasks": { + "add": "Προσθήκη εργασίας", + "cancel": "Ακύρωση", + "channels": { + "label": "Αποστολή σε Κανάλια", + "noActiveChatIds": "Τα επιλεγμένα κανάλια δεν διαθέτουν διαθέσιμους παραλήπτες (Chat ID). Τα αποτελέσματα της εργασίας ενδέχεται να μην παραδοθούν. Παρακαλώ στείλτε πρώτα ένα μήνυμα στο Bot στην πλατφόρμα.", + "placeholder": "Επιλέξτε κανάλια για να λαμβάνετε αποτελέσματα" + }, + "cronPlaceholder": "π.χ. 0 9 * * * (κάθε μέρα στις 9 π.μ.)", + "delete": { + "confirm": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτή την εργασία;", + "label": "Διαγραφή" + }, + "edit": "Επεξεργασία", + "empty": "Δεν υπάρχουν προγραμματισμένες εργασίες. Προσθέστε μία για να ξεκινήσετε.", + "error": { + "createFailed": "Αποτυχία δημιουργίας εργασίας", + "deleteFailed": "Αποτυχία διαγραφής εργασίας", + "loadFailed": "Αποτυχία φόρτωσης εργασιών", + "runFailed": "Αποτυχία εκτέλεσης εργασίας", + "updateFailed": "Αποτυχία ενημέρωσης εργασίας" + }, + "frequency": { + "everyPrefix": "Κάθε", + "everySuffix": "λεπτά", + "label": "Συχνότητα εκτέλεσης" + }, + "intervalPlaceholder": "Τουλάχιστον 1", + "intervalUnit": "λεπτά", + "lastRun": "Τελευταία εκτέλεση", + "logs": { + "cancelled": "Ακυρώθηκε", + "completed": "Ολοκληρώθηκε", + "duration": "Διάρκεια", + "empty": "Δεν υπάρχει ακόμα ιστορικό εκτελέσεων.", + "failed": "Απέτυχε", + "justNow": "μόλις τώρα", + "label": "Ιστορικό εκτελέσεων", + "loadError": "Αποτυχία φόρτωσης ιστορικού εκτέλεσης", + "result": "Αποτέλεσμα", + "runAt": "Εκτελέστηκε στις", + "running": "Τρέχει...", + "search": "Αναζήτηση αρχείων καταγραφής...", + "status": "Κατάσταση", + "viewSession": "Δείτε τη συνεδρία" + }, + "name": { + "label": "Όνομα", + "placeholder": "π.χ. Καθημερινή αναθεώρηση κώδικα" + }, + "nextRun": "Επόμενη εκτέλεση", + "oncePlaceholder": "Επιλέξτε ημερομηνία και ώρα", + "pause": "Παύση", + "prompt": { + "expand": "Αναπτύξτε τον επεξεργαστή", + "label": "Prompt", + "placeholder": "Τι πρέπει να κάνει ο πράκτορας όταν εκτελείται αυτή η εργασία;" + }, + "resume": "Συνέχιση", + "run": "Εκτέλεση", + "runTriggered": "Η εργασία ενεργοποιήθηκε", + "save": "Αποθήκευση", + "scheduleType": { + "cron": "Cron", + "interval": "Διάστημα", + "once": "Μία φορά" + }, + "status": { + "active": "Ενεργή", + "completed": "Ολοκληρώθηκε", + "paused": "Σε παύση" + }, + "tab": "Εργασίες", + "time": { + "hoursAgo": "{{count}}h πριν", + "minutesAgo": "{{count}}μ πριν" + }, + "timeout": { + "label": "Μέγιστος χρόνος εκτέλεσης", + "placeholder": "Χωρίς όριο" + }, + "title": "Προγραμματισμένες εργασίες" + }, "todo": { "mock": { "actions": { @@ -935,6 +873,14 @@ "description": "Εκτελεί εντολές κελύφους στο περιβάλλον σας", "label": "Bash" }, + "CherryConfig": { + "description": "[to be translated]:Inspects and manages this agent configuration and channels", + "label": "[to be translated]:Agent Config" + }, + "CherryCron": { + "description": "Διαχειρίζεται τον ενσωματωμένο προγραμματιστή εντός της εφαρμογής", + "label": "Προγραμματιστής" + }, "CherryKbManage": { "description": "Προσθέτει, διαγράφει ή ανανεώνει έγγραφα στις βάσεις γνώσης σας", "label": "Διαχείριση Γνώσης" @@ -943,6 +889,10 @@ "description": "Αναζητά τις βάσεις γνώσης σας", "label": "Αναζήτηση Γνώσης" }, + "CherryNotify": { + "description": "[to be translated]:Sends a notification through a connected channel", + "label": "[to be translated]:Notify" + }, "CherryWebFetch": { "description": "Ανάκτηση και ανάγνωση μιας ιστοσελίδας", "label": "Λήψη από τον Ιστό" @@ -951,10 +901,6 @@ "description": "Αναζητά στο διαδίκτυο μέσω του ρυθμισμένου παρόχου σας", "label": "Αναζήτηση στον Ιστό" }, - "ClawCron": { - "description": "Διαχειρίζεται τον ενσωματωμένο προγραμματιστή εντός της εφαρμογής", - "label": "Προγραμματιστής" - }, "Edit": { "description": "Πραγματοποιεί στοχευμένες επεξεργασίες σε συγκεκριμένα αρχεία", "label": "Επεξεργασία" @@ -1087,6 +1033,7 @@ "clear": { "content": "Η διαγραφή του θέματος θα διαγράψει όλα τα θέματα και τα αρχεία του βοηθού. Είστε σίγουροι πως θέλετε να συνεχίσετε;", "menu_title": "Διαγραφή όλων των συνομιλιών με τον βοηθό", + "success_title": "[to be translated]:Cleared {{count}} topics", "title": "Διαγραφή θέματος" }, "copy": { @@ -2986,11 +2933,10 @@ "placeholder": "Δώσε στον πράκτορα ένα όνομα" }, "permission_mode": { - "help": "Η εναλλαγή στο bypassPermissions ισοδυναμεί με την ενεργοποίηση της λειτουργίας Soul", "label": "Λειτουργία δικαιωμάτων", "option": { "acceptEdits": "Αποδοχή επεξεργασιών", - "bypassPermissions": "Παράκαμψη δικαιωμάτων (Ψυχή)", + "bypassPermissions": "Παράκαμψη δικαιωμάτων", "default": "Προεπιλογή", "plan": "Λειτουργία σχεδίου" } @@ -3002,10 +2948,6 @@ "small_model": { "hint": "Ελαφριοί έλεγχοι και μορφοποίηση", "label": "Μικρό μοντέλο (προαιρετικό)" - }, - "soul_enabled": { - "help": "Όταν είναι ενεργοποιημένο, παραχωρεί αυτόματα bypassPermissions", - "label": "Λειτουργία ψυχής" } }, "model_config": "Διαμόρφωση μοντέλου", @@ -5331,6 +5273,13 @@ "auto_switch_to_topics": "Αυτόματη μετάβαση σε θέματα", "title": "Ρυθμίσεις Ανώτερου Νiveau" }, + "agent": { + "position": { + "label": "[to be translated]:Session position", + "left": "[to be translated]:Left", + "right": "[to be translated]:Right" + } + }, "appearance": { "title": "Εμφάνιση" }, @@ -7596,6 +7545,7 @@ }, "title": { "apps": "Εφαρμογές", + "chat": "[to be translated]:Chat", "code": "Κώδικας", "files": "Αρχεία", "home": "Αρχική Σελίδα", diff --git a/src/renderer/i18n/translate/es-es.json b/src/renderer/i18n/translate/es-es.json index 370d6b55ea1..7e76067a559 100644 --- a/src/renderer/i18n/translate/es-es.json +++ b/src/renderer/i18n/translate/es-es.json @@ -31,262 +31,113 @@ "submit": "Enviar", "title": "Preguntas del Agente" }, - "cherryClaw": { - "channels": { - "add": "Agregar", - "bindAgent": "Agente vinculante", - "chatIdsAutoTrackHint": "Cuando se deja vacío, el sistema lo rastreará automáticamente: primero debes enviar un mensaje al Bot en la plataforma, luego el sistema registrará el ID del chat para futuras notificaciones.", - "comingSoon": "Próximamente", - "connected": "Conectado", - "connecting": "Conectando", - "createError": "Error al crear el canal", - "deleteConfirm": "¿Eliminar canal \"{{name}}\"?", - "deleteError": "Error al eliminar el canal", - "description": "Conecte su agente a plataformas de mensajería.", - "disconnected": "Desconectado", - "discord": { - "botToken": "Token del Bot", - "botTokenPlaceholder": "Introduce tu token de bot de Discord", - "channelIds": "IDs de canales permitidos", - "channelIdsHint": "Formato: canal:id o mp:id. Dejar vacío para permitir todos.", - "channelIdsPlaceholder": "canal:123456789, md:987654321", - "description": "Recibir y responder mensajes a través de un bot de Discord usando la pasarela WebSocket.", - "title": "Discord", - "whoamiTip": "💡 Consejo: Envía /whoami al bot para obtener tu ID de canal en el formato correcto." - }, - "error": "Error", - "feishu": { - "appId": "App ID", - "appIdPlaceholder": "Introduzca el App ID de su aplicación Feishu", - "appSecret": "App Secret", - "appSecretPlaceholder": "Introduzca el App Secret de su aplicación Feishu", - "chatIds": "IDs de chat permitidos", - "chatIdsHint": "IDs de chat separados por comas. Deje vacío para permitir todos los chats.", - "chatIdsPlaceholder": "oc_xxxxx, oc_yyyyy", - "connected": "Conectado", - "description": "Recibir y responder mensajes a través de un bot de Feishu/Lark usando WebSocket.", - "domain": "Dominio", - "domainFeishu": "Feishu (China)", - "domainLark": "Lark (Internacional)", - "encryptKey": "Clave de cifrado", - "encryptKeyPlaceholder": "Introduzca la clave de cifrado de su aplicación Feishu", - "loginHint": "No se han configurado credenciales. Activa el canal para iniciar el registro con código QR o introduce manualmente el ID de la aplicación y el secreto de la aplicación.", - "qrExpired": "Código QR expirado. Por favor, inténtalo de nuevo cambiando el canal.", - "qrHint": "Esperando el escaneo del código QR...", - "qrScanHint": "Abre Feishu en tu teléfono y escanea el código QR para crear una aplicación de bot.", - "qrTitle": "Registro con código QR de Feishu", - "title": "Feishu", - "verificationToken": "Token de verificación", - "verificationTokenPlaceholder": "Introduzca el token de verificación de su aplicación Feishu" - }, - "logs": "Registros", - "noInstances": "No hay canales de {{type}} configurados. Haz clic en \"+ Añadir\" para crear uno.", - "noLogs": "Aún no hay registros", - "notifyReceiver": "Recibir notificaciones de tareas", - "notifyReceiverHint": "Enviar resultados de tareas programadas a este canal.", - "qq": { - "appId": "App ID", - "appIdPlaceholder": "Ingrese su App ID de QQ Bot", - "chatIds": "IDs de chat permitidos", - "chatIdsHint": "Formato: c2c:openid, group:groupid, channel:channelid. Dejar vacío para permitir todos.", - "chatIdsPlaceholder": "c2c:abc123, group:xyz789", - "clientSecret": "Client Secret", - "clientSecretPlaceholder": "Ingrese su Client Secret de QQ Bot", - "description": "Recibir y responder mensajes a través de la API oficial de QQ Bot.", - "title": "QQ", - "whoamiTip": "💡 Consejo: Envíe /whoami al bot para obtener su ID de chat en el formato correcto." - }, - "security": { - "inheritFromAgent": "Heredar del agente", - "permissionMode": "Modo de Permisos del Canal", - "permissionModeHint": "Anular el modo de permisos del agente para los mensajes de este canal. \"Heredar\" utiliza la configuración predeterminada del agente." - }, - "selectAgent": "Selecciona un agente para vincular", - "slack": { - "appToken": "Token a nivel de aplicación", - "appTokenPlaceholder": "xapp-...", - "botToken": "Token del Bot", - "botTokenPlaceholder": "xoxb-...", - "channelIds": "IDs de Canales Permitidos", - "channelIdsHint": "IDs de canales de Slack. Dejar vacío para permitir todos.", - "channelIdsPlaceholder": "C01234567, D89012345", - "description": "Recibe y responde a mensajes mediante un bot de Slack usando el modo Socket.", - "title": "Slack", - "whoamiTip": "💡 Consejo: Envía /whoami al bot para obtener el ID del canal." - }, - "soulModeRequired": "Este agente no tiene el Modo Autónomo habilitado. Las funciones del canal no funcionarán. Por favor, habilítalo en la configuración del agente.", - "tab": "Canales", - "telegram": { - "botToken": "Token del bot", - "botTokenPlaceholder": "Ingrese su token de bot de Telegram", - "chatIds": "IDs de chat permitidos", - "chatIdsHint": "Separados por comas. Dejar vacío para permitir todos los chats.", - "chatIdsPlaceholder": "123456789, 987654321", - "description": "Recibir y responder mensajes a través de un bot de Telegram usando long polling.", - "title": "Telegram" - }, - "title": "Canales", - "updateError": "Error al actualizar el canal", - "wechat": { - "addAccount": "Agregar cuenta de WeChat", - "chatIds": "IDs de Usuario Permitidos", - "chatIdsHint": "Separados por comas. Dejar vacío para permitir todos los usuarios.", - "chatIdsPlaceholder": "wxid_abc123, wxid_def456", - "connected": "Conectado", - "description": "Recibe y responde a mensajes a través de WeChat utilizando la API de iLink Bot.", - "disconnected": "Desconectado", - "loginHint": "El primer inicio de sesión requiere escanear un código QR. Verifica los registros de la aplicación para obtener la URL de inicio de sesión.", - "qrHint": "Abre WeChat en tu teléfono, escanea el código QR para iniciar sesión.", - "qrTitle": "Inicio de sesión con código QR de WeChat", - "title": "WeChat", - "whoamiTip": "Consejo: Envía /whoami en WeChat para obtener el ID de un usuario." - } - }, - "heartbeat": { - "enabled": "Habilitar Heartbeat", - "enabledHelper": "Cuando está habilitado y heartbeat.md existe en el espacio de trabajo, el programador lee su contenido y lo envía a la sesión principal en el intervalo configurado.", - "interval": "Intervalo (minutos)", - "intervalHelper": "Con qué frecuencia se ejecuta el heartbeat, en minutos." - }, - "sandbox": { - "description": "Restringe el acceso al sistema de archivos y red del agente al directorio del espacio de trabajo usando sandboxing a nivel de sistema operativo.", - "helper": "Restringir el acceso al sistema de archivos y comandos a la ruta del espacio de trabajo.", - "label": "Modo Sandbox" - }, - "scheduler": { - "cron": { - "helper": "Expresión cron estándar (ej: '*/30 * * * *' para cada 30 minutos).", - "label": "Expresión Cron", - "placeholder": "*/30 * * * *" - }, - "description": "El programador activa el agente automáticamente según un horario.", - "enabled": "Habilitar programador", - "enabledHelper": "Cuando está habilitado, el agente se ejecutará automáticamente según el horario configurado.", - "errors": { - "invalidCron": "Expresión cron inválida", - "invalidDelay": "El retraso debe ser un número positivo", - "invalidInterval": "El intervalo debe ser un número positivo" - }, - "interval": { - "helper": "Con qué frecuencia debe ejecutarse el agente, en segundos.", - "label": "Intervalo (segundos)" - }, - "lastRun": "Última ejecución", - "never": "Nunca", - "nextRun": "Próxima ejecución", - "oneTime": { - "helper": "Retraso único en segundos antes de que el agente se ejecute.", - "label": "Retraso (segundos)" - }, - "status": { - "running": "Ejecutando", - "stopped": "Detenido", - "tickInProgress": "Tick en progreso" - }, - "tab": "Programador", - "title": "Configuración del programador", - "type": { - "cron": "Expresión Cron", - "interval": "Intervalo fijo", - "label": "Tipo de programación", - "one-time": "Retraso único" - } + "channels": { + "add": "Agregar", + "bindAgent": "Agente vinculante", + "chatIdsAutoTrackHint": "Cuando se deja vacío, el sistema lo rastreará automáticamente: primero debes enviar un mensaje al Bot en la plataforma, luego el sistema registrará el ID del chat para futuras notificaciones.", + "comingSoon": "Próximamente", + "connected": "Conectado", + "connecting": "Conectando", + "createError": "Error al crear el canal", + "deleteConfirm": "¿Eliminar canal \"{{name}}\"?", + "deleteError": "Error al eliminar el canal", + "description": "Conecte su agente a plataformas de mensajería.", + "disconnected": "Desconectado", + "discord": { + "botToken": "Token del Bot", + "botTokenPlaceholder": "Introduce tu token de bot de Discord", + "channelIds": "IDs de canales permitidos", + "channelIdsHint": "Formato: canal:id o mp:id. Dejar vacío para permitir todos.", + "channelIdsPlaceholder": "canal:123456789, md:987654321", + "description": "Recibir y responder mensajes a través de un bot de Discord usando la pasarela WebSocket.", + "title": "Discord", + "whoamiTip": "💡 Consejo: Envía /whoami al bot para obtener tu ID de canal en el formato correcto." }, - "soul": { - "description": "El alma define la personalidad y el comportamiento base del agente. Se lee desde soul.md en la raíz del espacio de trabajo.", - "empty": "No se encontró soul.md en la raíz del espacio de trabajo. Cree uno para definir la personalidad del agente.", - "enabled": "Habilitar alma", - "enabledHelper": "Cuando está habilitado, el agente lee soul.md desde la raíz del espacio de trabajo y lo antepone al prompt del sistema.", - "fileLabel": "soul.md", - "preview": "Vista previa del alma", - "tab": "Alma", - "title": "Configuración del alma" - }, - "tasks": { - "add": "Agregar tarea", - "cancel": "Cancelar", - "channels": { - "label": "Enviar a Canales", - "noActiveChatIds": "Los canales seleccionados no tienen destinatarios disponibles (ID de chat). Es posible que los resultados de la tarea no se entreguen. Primero, envíe un mensaje al Bot en la plataforma.", - "placeholder": "Seleccionar canales para recibir resultados" - }, - "cronPlaceholder": "ej: 0 9 * * * (todos los días a las 9 AM)", - "delete": { - "confirm": "¿Está seguro de que desea eliminar esta tarea?", - "label": "Eliminar" - }, - "edit": "Editar", - "empty": "No hay tareas programadas. Agregue una para comenzar.", - "error": { - "createFailed": "Error al crear la tarea", - "deleteFailed": "Error al eliminar la tarea", - "loadFailed": "Error al cargar las tareas", - "runFailed": "Error al ejecutar la tarea", - "updateFailed": "Error al actualizar la tarea" - }, - "frequency": { - "everyPrefix": "Cada", - "everySuffix": "minutos", - "label": "Frecuencia de ejecución" - }, - "intervalPlaceholder": "Al menos 1", - "intervalUnit": "minutos", - "lastRun": "Última ejecución", - "logs": { - "cancelled": "Cancelado", - "completed": "Completado", - "duration": "Duración", - "empty": "Aún no hay historial de ejecución.", - "failed": "Fallido", - "justNow": "ahora mismo", - "label": "Historial de ejecución", - "loadError": "Error al cargar el historial de ejecución", - "result": "Resultado", - "runAt": "Ejecutado en", - "running": "Corriendo...", - "search": "Buscar registros...", - "status": "Estado", - "viewSession": "Ver sesión" - }, - "name": { - "label": "Nombre", - "placeholder": "ej: Revisión de código diaria" - }, - "nextRun": "Próxima ejecución", - "oncePlaceholder": "Seleccionar fecha y hora", - "pause": "Pausar", - "prompt": { - "expand": "Expandir editor", - "label": "Prompt", - "placeholder": "¿Qué debe hacer el agente cuando se ejecute esta tarea?" - }, - "resume": "Reanudar", - "run": "Ejecutar", - "runTriggered": "Tarea activada", - "save": "Guardar", - "scheduleType": { - "cron": "Cron", - "interval": "Intervalo", - "once": "Una vez" - }, - "status": { - "active": "Activo", - "completed": "Completado", - "paused": "Pausado" - }, - "tab": "Tareas", - "time": { - "hoursAgo": "hace {{count}}h", - "minutesAgo": "{{count}}m hace" - }, - "timeout": { - "label": "Tiempo máximo de ejecución", - "placeholder": "Sin límite" - }, - "title": "Tareas programadas" + "error": "Error", + "feishu": { + "appId": "App ID", + "appIdPlaceholder": "Introduzca el App ID de su aplicación Feishu", + "appSecret": "App Secret", + "appSecretPlaceholder": "Introduzca el App Secret de su aplicación Feishu", + "chatIds": "IDs de chat permitidos", + "chatIdsHint": "IDs de chat separados por comas. Deje vacío para permitir todos los chats.", + "chatIdsPlaceholder": "oc_xxxxx, oc_yyyyy", + "connected": "Conectado", + "description": "Recibir y responder mensajes a través de un bot de Feishu/Lark usando WebSocket.", + "domain": "Dominio", + "domainFeishu": "Feishu (China)", + "domainLark": "Lark (Internacional)", + "encryptKey": "Clave de cifrado", + "encryptKeyPlaceholder": "Introduzca la clave de cifrado de su aplicación Feishu", + "loginHint": "No se han configurado credenciales. Activa el canal para iniciar el registro con código QR o introduce manualmente el ID de la aplicación y el secreto de la aplicación.", + "qrExpired": "Código QR expirado. Por favor, inténtalo de nuevo cambiando el canal.", + "qrHint": "Esperando el escaneo del código QR...", + "qrScanHint": "Abre Feishu en tu teléfono y escanea el código QR para crear una aplicación de bot.", + "qrTitle": "Registro con código QR de Feishu", + "title": "Feishu", + "verificationToken": "Token de verificación", + "verificationTokenPlaceholder": "Introduzca el token de verificación de su aplicación Feishu" }, - "warning": { - "bypassPermissions": "Los agentes CherryClaw se ejecutan en modo automático completo por defecto. Todas las herramientas se ejecutan sin pedir aprobación." + "logs": "Registros", + "noInstances": "No hay canales de {{type}} configurados. Haz clic en \"+ Añadir\" para crear uno.", + "noLogs": "Aún no hay registros", + "notifyReceiver": "Recibir notificaciones de tareas", + "notifyReceiverHint": "Enviar resultados de tareas programadas a este canal.", + "qq": { + "appId": "App ID", + "appIdPlaceholder": "Ingrese su App ID de QQ Bot", + "chatIds": "IDs de chat permitidos", + "chatIdsHint": "Formato: c2c:openid, group:groupid, channel:channelid. Dejar vacío para permitir todos.", + "chatIdsPlaceholder": "c2c:abc123, group:xyz789", + "clientSecret": "Client Secret", + "clientSecretPlaceholder": "Ingrese su Client Secret de QQ Bot", + "description": "Recibir y responder mensajes a través de la API oficial de QQ Bot.", + "title": "QQ", + "whoamiTip": "💡 Consejo: Envíe /whoami al bot para obtener su ID de chat en el formato correcto." + }, + "security": { + "inheritFromAgent": "Heredar del agente", + "permissionMode": "Modo de Permisos del Canal", + "permissionModeHint": "Anular el modo de permisos del agente para los mensajes de este canal. \"Heredar\" utiliza la configuración predeterminada del agente." + }, + "selectAgent": "Selecciona un agente para vincular", + "slack": { + "appToken": "Token a nivel de aplicación", + "appTokenPlaceholder": "xapp-...", + "botToken": "Token del Bot", + "botTokenPlaceholder": "xoxb-...", + "channelIds": "IDs de Canales Permitidos", + "channelIdsHint": "IDs de canales de Slack. Dejar vacío para permitir todos.", + "channelIdsPlaceholder": "C01234567, D89012345", + "description": "Recibe y responde a mensajes mediante un bot de Slack usando el modo Socket.", + "title": "Slack", + "whoamiTip": "💡 Consejo: Envía /whoami al bot para obtener el ID del canal." + }, + "tab": "Canales", + "telegram": { + "botToken": "Token del bot", + "botTokenPlaceholder": "Ingrese su token de bot de Telegram", + "chatIds": "IDs de chat permitidos", + "chatIdsHint": "Separados por comas. Dejar vacío para permitir todos los chats.", + "chatIdsPlaceholder": "123456789, 987654321", + "description": "Recibir y responder mensajes a través de un bot de Telegram usando long polling.", + "title": "Telegram" + }, + "title": "Canales", + "updateError": "Error al actualizar el canal", + "wechat": { + "addAccount": "Agregar cuenta de WeChat", + "chatIds": "IDs de Usuario Permitidos", + "chatIdsHint": "Separados por comas. Dejar vacío para permitir todos los usuarios.", + "chatIdsPlaceholder": "wxid_abc123, wxid_def456", + "connected": "Conectado", + "description": "Recibe y responde a mensajes a través de WeChat utilizando la API de iLink Bot.", + "disconnected": "Desconectado", + "loginHint": "El primer inicio de sesión requiere escanear un código QR. Verifica los registros de la aplicación para obtener la URL de inicio de sesión.", + "qrHint": "Abre WeChat en tu teléfono, escanea el código QR para iniciar sesión.", + "qrTitle": "Inicio de sesión con código QR de WeChat", + "title": "WeChat", + "whoamiTip": "Consejo: Envía /whoami en WeChat para obtener el ID de un usuario." } }, "delete": { @@ -339,15 +190,20 @@ "home": { "welcome_title": "¿De qué hablaremos hoy?" }, + "icon": { + "type": "[to be translated]:Agent Icon" + }, "input": { - "placeholder": "Introduce tu mensaje aquí, envía con {{key}} - @ seleccionar ruta, / seleccionar comando", - "soul_placeholder": "Ayúdame a conectarme a Telegram" + "placeholder": "Introduce tu mensaje aquí, envía con {{key}} - @ seleccionar ruta, / seleccionar comando" }, "list": { "error": { "failed": "Error al listar agentes." } }, + "manage": { + "title": "[to be translated]:Manage Agents" + }, "pin": { "title": "Agente Pin" }, @@ -712,12 +568,6 @@ "tab": "Habilidades", "title": "Habilidades Instaladas" }, - "soulMode": { - "description": "Cuando está habilitado, el agente utiliza un mensaje del sistema personalizado desde archivos soul del espacio de trabajo, inyecta herramientas de gestión de tareas autónomas y desactiva las herramientas interactivas no adecuadas para el funcionamiento autónomo.", - "disabledBadge": "Desactivado por el Modo Autónomo", - "disabledTooltip": "Esta herramienta se desactiva automáticamente cuando el Modo Autónomo está activo.", - "title": "Modo Autónomo" - }, "tooling": { "mcp": { "description": "Conecta servidores MCP para desbloquear herramientas adicionales que puedes aprobar arriba.", @@ -784,6 +634,94 @@ } }, "sidebar_title": "Agentes", + "skill": { + "manage": { + "title": "[to be translated]:Manage skills" + } + }, + "tasks": { + "add": "Agregar tarea", + "cancel": "Cancelar", + "channels": { + "label": "Enviar a Canales", + "noActiveChatIds": "Los canales seleccionados no tienen destinatarios disponibles (ID de chat). Es posible que los resultados de la tarea no se entreguen. Primero, envíe un mensaje al Bot en la plataforma.", + "placeholder": "Seleccionar canales para recibir resultados" + }, + "cronPlaceholder": "ej: 0 9 * * * (todos los días a las 9 AM)", + "delete": { + "confirm": "¿Está seguro de que desea eliminar esta tarea?", + "label": "Eliminar" + }, + "edit": "Editar", + "empty": "No hay tareas programadas. Agregue una para comenzar.", + "error": { + "createFailed": "Error al crear la tarea", + "deleteFailed": "Error al eliminar la tarea", + "loadFailed": "Error al cargar las tareas", + "runFailed": "Error al ejecutar la tarea", + "updateFailed": "Error al actualizar la tarea" + }, + "frequency": { + "everyPrefix": "Cada", + "everySuffix": "minutos", + "label": "Frecuencia de ejecución" + }, + "intervalPlaceholder": "Al menos 1", + "intervalUnit": "minutos", + "lastRun": "Última ejecución", + "logs": { + "cancelled": "Cancelado", + "completed": "Completado", + "duration": "Duración", + "empty": "Aún no hay historial de ejecución.", + "failed": "Fallido", + "justNow": "ahora mismo", + "label": "Historial de ejecución", + "loadError": "Error al cargar el historial de ejecución", + "result": "Resultado", + "runAt": "Ejecutado en", + "running": "Corriendo...", + "search": "Buscar registros...", + "status": "Estado", + "viewSession": "Ver sesión" + }, + "name": { + "label": "Nombre", + "placeholder": "ej: Revisión de código diaria" + }, + "nextRun": "Próxima ejecución", + "oncePlaceholder": "Seleccionar fecha y hora", + "pause": "Pausar", + "prompt": { + "expand": "Expandir editor", + "label": "Prompt", + "placeholder": "¿Qué debe hacer el agente cuando se ejecute esta tarea?" + }, + "resume": "Reanudar", + "run": "Ejecutar", + "runTriggered": "Tarea activada", + "save": "Guardar", + "scheduleType": { + "cron": "Cron", + "interval": "Intervalo", + "once": "Una vez" + }, + "status": { + "active": "Activo", + "completed": "Completado", + "paused": "Pausado" + }, + "tab": "Tareas", + "time": { + "hoursAgo": "hace {{count}}h", + "minutesAgo": "{{count}}m hace" + }, + "timeout": { + "label": "Tiempo máximo de ejecución", + "placeholder": "Sin límite" + }, + "title": "Tareas programadas" + }, "todo": { "mock": { "actions": { @@ -935,6 +873,14 @@ "description": "Ejecuta comandos de shell en tu entorno", "label": "Bash" }, + "CherryConfig": { + "description": "[to be translated]:Inspects and manages this agent configuration and channels", + "label": "[to be translated]:Agent Config" + }, + "CherryCron": { + "description": "Gestiona el programador dentro de la aplicación", + "label": "Planificador" + }, "CherryKbManage": { "description": "Agrega, elimina o actualiza documentos en tus bases de conocimiento", "label": "Gestionar conocimiento" @@ -943,6 +889,10 @@ "description": "Busca en tus bases de conocimiento", "label": "Búsqueda de conocimiento" }, + "CherryNotify": { + "description": "[to be translated]:Sends a notification through a connected channel", + "label": "[to be translated]:Notify" + }, "CherryWebFetch": { "description": "Obtiene y lee una página web", "label": "Obtención web" @@ -951,10 +901,6 @@ "description": "Busca en la web a través de tu proveedor configurado", "label": "Búsqueda en la web" }, - "ClawCron": { - "description": "Gestiona el programador dentro de la aplicación", - "label": "Planificador" - }, "Edit": { "description": "Realiza ediciones dirigidas a archivos específicos", "label": "Editar" @@ -1087,6 +1033,7 @@ "clear": { "content": "Vaciar el tema eliminará todos los temas y archivos del asistente. ¿Está seguro de que desea continuar?", "menu_title": "Eliminar todas las conversaciones con el asistente", + "success_title": "[to be translated]:Cleared {{count}} topics", "title": "Vaciar Tema" }, "copy": { @@ -2986,11 +2933,10 @@ "placeholder": "Dale un nombre al agente" }, "permission_mode": { - "help": "Cambiar a bypassPermissions es equivalente a activar el modo Soul", "label": "Modo de permiso", "option": { "acceptEdits": "Aceptar ediciones", - "bypassPermissions": "Omitir permisos (Alma)", + "bypassPermissions": "Omitir permisos", "default": "Predeterminado", "plan": "Modo de planificación" } @@ -3002,10 +2948,6 @@ "small_model": { "hint": "Verificaciones ligeras y formato", "label": "Modelo pequeño (opcional)" - }, - "soul_enabled": { - "help": "Cuando está habilitado, otorga automáticamente bypassPermissions", - "label": "Modo alma" } }, "model_config": "Configuración del modelo", @@ -5331,6 +5273,13 @@ "auto_switch_to_topics": "Cambiar automáticamente a temas", "title": "Configuración avanzada" }, + "agent": { + "position": { + "label": "[to be translated]:Session position", + "left": "[to be translated]:Left", + "right": "[to be translated]:Right" + } + }, "appearance": { "title": "Apariencia" }, @@ -7596,6 +7545,7 @@ }, "title": { "apps": "Aplicaciones", + "chat": "[to be translated]:Chat", "code": "Código", "files": "Archivos", "home": "Inicio", diff --git a/src/renderer/i18n/translate/fr-fr.json b/src/renderer/i18n/translate/fr-fr.json index e7287fef673..28d559adec5 100644 --- a/src/renderer/i18n/translate/fr-fr.json +++ b/src/renderer/i18n/translate/fr-fr.json @@ -31,262 +31,113 @@ "submit": "Soumettre", "title": "Questions de l'agent" }, - "cherryClaw": { - "channels": { - "add": "Ajouter", - "bindAgent": "Agent Lier", - "chatIdsAutoTrackHint": "Lorsqu'il est laissé vide, le système effectuera un suivi automatique : vous devez d'abord envoyer un message au Bot sur la plateforme, puis le système enregistrera l'ID du chat pour les notifications futures.", - "comingSoon": "Bientôt disponible", - "connected": "Connecté", - "connecting": "Connexion", - "createError": "Échec de la création du canal", - "deleteConfirm": "Supprimer le canal \"{{name}}\" ?", - "deleteError": "Échec de la suppression du canal", - "description": "Connectez votre agent aux plateformes de messagerie.", - "disconnected": "Déconnecté", - "discord": { - "botToken": "Jeton de bot", - "botTokenPlaceholder": "Entrez votre jeton de bot Discord", - "channelIds": "IDs de canaux autorisés", - "channelIdsHint": "Format : channel:id ou dm:id. Laissez vide pour tout autoriser.", - "channelIdsPlaceholder": "canal:123456789, mp:987654321", - "description": "Recevoir et répondre aux messages via un bot Discord en utilisant la passerelle WebSocket.", - "title": "Discord", - "whoamiTip": "💡 Astuce : Envoyez /whoami au bot pour obtenir l’ID de votre chaîne au bon format." - }, - "error": "Erreur", - "feishu": { - "appId": "App ID", - "appIdPlaceholder": "Saisissez l'App ID de votre application Feishu", - "appSecret": "App Secret", - "appSecretPlaceholder": "Saisissez l'App Secret de votre application Feishu", - "chatIds": "IDs de chat autorisés", - "chatIdsHint": "IDs de chat séparés par des virgules. Laissez vide pour autoriser tous les chats.", - "chatIdsPlaceholder": "oc_xxxxx, oc_yyyyy", - "connected": "Connecté", - "description": "Recevoir et répondre aux messages via un bot Feishu/Lark en utilisant WebSocket.", - "domain": "Domaine", - "domainFeishu": "Feishu (Chine)", - "domainLark": "Lark (International)", - "encryptKey": "Clé de chiffrement", - "encryptKeyPlaceholder": "Saisissez la clé de chiffrement de votre application Feishu", - "loginHint": "Aucune information d’identification configurée. Activez le canal pour démarrer l’enregistrement par code QR, ou saisissez manuellement l’ID de l’application et le secret de l’application.", - "qrExpired": "Code QR expiré. Veuillez réessayer en basculant le canal.", - "qrHint": "En attente de la numérisation du code QR...", - "qrScanHint": "Ouvre Feishu sur ton téléphone et scanne le code QR pour créer une application bot.", - "qrTitle": "Inscription par QR code Feishu", - "title": "Feishu", - "verificationToken": "Jeton de vérification", - "verificationTokenPlaceholder": "Saisissez le jeton de vérification de votre application Feishu" - }, - "logs": "Journaux", - "noInstances": "Aucun canal {{type}} configuré. Cliquez sur « + Ajouter » pour en créer un.", - "noLogs": "Pas encore de journaux", - "notifyReceiver": "Recevoir les notifications de tâches", - "notifyReceiverHint": "Envoyer les résultats des tâches planifiées à ce canal.", - "qq": { - "appId": "App ID", - "appIdPlaceholder": "Entrez votre App ID QQ Bot", - "chatIds": "IDs de chat autorisés", - "chatIdsHint": "Format : c2c:openid, group:groupid, channel:channelid. Laissez vide pour tout autoriser.", - "chatIdsPlaceholder": "c2c:abc123, group:xyz789", - "clientSecret": "Client Secret", - "clientSecretPlaceholder": "Entrez votre Client Secret QQ Bot", - "description": "Recevoir et répondre aux messages via l'API officielle QQ Bot.", - "title": "QQ", - "whoamiTip": "💡 Astuce : Envoyez /whoami au bot pour obtenir votre ID de chat au bon format." - }, - "security": { - "inheritFromAgent": "Hériter de l'agent", - "permissionMode": "Mode de permission du canal", - "permissionModeHint": "Remplace le mode d'autorisation de l'agent pour les messages provenant de ce canal. « Hériter » utilise la valeur par défaut de l'agent." - }, - "selectAgent": "Sélectionnez un agent à lier", - "slack": { - "appToken": "Jeton au niveau de l'application", - "appTokenPlaceholder": "xapp-...", - "botToken": "Jeton du bot", - "botTokenPlaceholder": "xoxb-...", - "channelIds": "ID de canaux autorisés", - "channelIdsHint": "IDs de canaux Slack. Laissez vide pour autoriser tous.", - "channelIdsPlaceholder": "C01234567, D89012345", - "description": "Recevoir et répondre aux messages via un bot Slack en mode Socket.", - "title": "Slack", - "whoamiTip": "💡 Astuce : Envoie /whoami au bot pour obtenir l’ID du canal." - }, - "soulModeRequired": "Cet agent n’a pas le Mode Autonome activé. Les fonctionnalités du canal ne fonctionneront pas. Veuillez l’activer dans les paramètres de l’agent.", - "tab": "Canaux", - "telegram": { - "botToken": "Token du bot", - "botTokenPlaceholder": "Entrez votre token de bot Telegram", - "chatIds": "IDs de chat autorisés", - "chatIdsHint": "Séparés par des virgules. Laissez vide pour autoriser tous les chats.", - "chatIdsPlaceholder": "123456789, 987654321", - "description": "Recevoir et répondre aux messages via un bot Telegram en utilisant le long polling.", - "title": "Telegram" - }, - "title": "Canaux", - "updateError": "Échec de la mise à jour du canal", - "wechat": { - "addAccount": "Ajouter un compte WeChat", - "chatIds": "Identifiants d'utilisateurs autorisés", - "chatIdsHint": "Séparés par des virgules. Laisser vide pour autoriser tous les utilisateurs.", - "chatIdsPlaceholder": "wxid_abc123, wxid_def456", - "connected": "Connecté", - "description": "Recevoir et répondre aux messages via WeChat en utilisant l'API iLink Bot.", - "disconnected": "Déconnecté", - "loginHint": "La première connexion nécessite de scanner un code QR. Vérifiez les journaux de l'application pour obtenir l'URL de connexion.", - "qrHint": "Ouvrez WeChat sur votre téléphone, scannez le code QR pour vous connecter.", - "qrTitle": "Connexion par QR WeChat", - "title": "WeChat", - "whoamiTip": "Astuce : envoyez /whoami dans WeChat pour obtenir l'ID d'un utilisateur." - } - }, - "heartbeat": { - "enabled": "Activer le Heartbeat", - "enabledHelper": "Lorsqu'activé et si heartbeat.md existe dans l'espace de travail, le planificateur lit son contenu et l'envoie à la session principale à l'intervalle configuré.", - "interval": "Intervalle (minutes)", - "intervalHelper": "Fréquence d'exécution du heartbeat, en minutes." - }, - "sandbox": { - "description": "Restreint l'accès au système de fichiers et au réseau de l'agent au répertoire de l'espace de travail en utilisant le sandboxing au niveau du système d'exploitation.", - "helper": "Restreindre l'accès au système de fichiers et aux commandes au chemin de l'espace de travail.", - "label": "Mode Sandbox" - }, - "scheduler": { - "cron": { - "helper": "Expression cron standard (ex: '*/30 * * * *' pour toutes les 30 minutes).", - "label": "Expression Cron", - "placeholder": "*/30 * * * *" - }, - "description": "Le planificateur déclenche l'agent automatiquement selon un calendrier.", - "enabled": "Activer le planificateur", - "enabledHelper": "Lorsqu'activé, l'agent s'exécutera automatiquement selon le calendrier configuré.", - "errors": { - "invalidCron": "Expression cron invalide", - "invalidDelay": "Le délai doit être un nombre positif", - "invalidInterval": "L'intervalle doit être un nombre positif" - }, - "interval": { - "helper": "Fréquence d'exécution de l'agent, en secondes.", - "label": "Intervalle (secondes)" - }, - "lastRun": "Dernière exécution", - "never": "Jamais", - "nextRun": "Prochaine exécution", - "oneTime": { - "helper": "Délai unique en secondes avant l'exécution de l'agent.", - "label": "Délai (secondes)" - }, - "status": { - "running": "En cours", - "stopped": "Arrêté", - "tickInProgress": "Tick en cours" - }, - "tab": "Planificateur", - "title": "Configuration du planificateur", - "type": { - "cron": "Expression Cron", - "interval": "Intervalle fixe", - "label": "Type de planification", - "one-time": "Délai unique" - } + "channels": { + "add": "Ajouter", + "bindAgent": "Agent Lier", + "chatIdsAutoTrackHint": "Lorsqu'il est laissé vide, le système effectuera un suivi automatique : vous devez d'abord envoyer un message au Bot sur la plateforme, puis le système enregistrera l'ID du chat pour les notifications futures.", + "comingSoon": "Bientôt disponible", + "connected": "Connecté", + "connecting": "Connexion", + "createError": "Échec de la création du canal", + "deleteConfirm": "Supprimer le canal \"{{name}}\" ?", + "deleteError": "Échec de la suppression du canal", + "description": "Connectez votre agent aux plateformes de messagerie.", + "disconnected": "Déconnecté", + "discord": { + "botToken": "Jeton de bot", + "botTokenPlaceholder": "Entrez votre jeton de bot Discord", + "channelIds": "IDs de canaux autorisés", + "channelIdsHint": "Format : channel:id ou dm:id. Laissez vide pour tout autoriser.", + "channelIdsPlaceholder": "canal:123456789, mp:987654321", + "description": "Recevoir et répondre aux messages via un bot Discord en utilisant la passerelle WebSocket.", + "title": "Discord", + "whoamiTip": "💡 Astuce : Envoyez /whoami au bot pour obtenir l’ID de votre chaîne au bon format." }, - "soul": { - "description": "L'âme définit la personnalité et le comportement de base de l'agent. Elle est lue depuis soul.md à la racine de l'espace de travail.", - "empty": "Aucun soul.md trouvé à la racine de l'espace de travail. Créez-en un pour définir la personnalité de l'agent.", - "enabled": "Activer l'âme", - "enabledHelper": "Lorsqu'activé, l'agent lit soul.md depuis la racine de l'espace de travail et l'ajoute au début du prompt système.", - "fileLabel": "soul.md", - "preview": "Aperçu de l'âme", - "tab": "Âme", - "title": "Configuration de l'âme" - }, - "tasks": { - "add": "Ajouter une tâche", - "cancel": "Annuler", - "channels": { - "label": "Envoyer aux chaînes", - "noActiveChatIds": "Les canaux sélectionnés ne possèdent aucun destinataire disponible (ID de chat). Les résultats de la tâche peuvent ne pas être livrés. Veuillez d'abord envoyer un message au Bot sur la plateforme.", - "placeholder": "Sélectionnez les chaînes pour recevoir les résultats" - }, - "cronPlaceholder": "ex: 0 9 * * * (tous les jours à 9h)", - "delete": { - "confirm": "Êtes-vous sûr de vouloir supprimer cette tâche ?", - "label": "Supprimer" - }, - "edit": "Modifier", - "empty": "Aucune tâche planifiée. Ajoutez-en une pour commencer.", - "error": { - "createFailed": "Échec de la création de la tâche", - "deleteFailed": "Échec de la suppression de la tâche", - "loadFailed": "Échec du chargement des tâches", - "runFailed": "Échec de l'exécution de la tâche", - "updateFailed": "Échec de la mise à jour de la tâche" - }, - "frequency": { - "everyPrefix": "Toutes les", - "everySuffix": "minutes", - "label": "Fréquence d’exécution" - }, - "intervalPlaceholder": "Au moins 1", - "intervalUnit": "minutes", - "lastRun": "Dernière exécution", - "logs": { - "cancelled": "Annulé", - "completed": "Terminé", - "duration": "Durée", - "empty": "Aucun historique d'exécution.", - "failed": "Échoué", - "justNow": "à l'instant", - "label": "Historique d'exécution", - "loadError": "Échec du chargement de l'historique des exécutions", - "result": "Résultat", - "runAt": "Exécuté le", - "running": "En cours d'exécution...", - "search": "Rechercher les journaux...", - "status": "Statut", - "viewSession": "Voir la session" - }, - "name": { - "label": "Nom", - "placeholder": "ex: Revue de code quotidienne" - }, - "nextRun": "Prochaine exécution", - "oncePlaceholder": "Sélectionner la date et l'heure", - "pause": "Pause", - "prompt": { - "expand": "Développer l'éditeur", - "label": "Prompt", - "placeholder": "Que doit faire l'agent lors de l'exécution de cette tâche ?" - }, - "resume": "Reprendre", - "run": "Exécuter", - "runTriggered": "Tâche déclenchée", - "save": "Enregistrer", - "scheduleType": { - "cron": "Cron", - "interval": "Intervalle", - "once": "Une fois" - }, - "status": { - "active": "Actif", - "completed": "Terminé", - "paused": "En pause" - }, - "tab": "Tâches", - "time": { - "hoursAgo": "{{count}}h il y a", - "minutesAgo": "{{count}}m il y a" - }, - "timeout": { - "label": "Temps d’exécution maximal", - "placeholder": "Aucune limite" - }, - "title": "Tâches planifiées" + "error": "Erreur", + "feishu": { + "appId": "App ID", + "appIdPlaceholder": "Saisissez l'App ID de votre application Feishu", + "appSecret": "App Secret", + "appSecretPlaceholder": "Saisissez l'App Secret de votre application Feishu", + "chatIds": "IDs de chat autorisés", + "chatIdsHint": "IDs de chat séparés par des virgules. Laissez vide pour autoriser tous les chats.", + "chatIdsPlaceholder": "oc_xxxxx, oc_yyyyy", + "connected": "Connecté", + "description": "Recevoir et répondre aux messages via un bot Feishu/Lark en utilisant WebSocket.", + "domain": "Domaine", + "domainFeishu": "Feishu (Chine)", + "domainLark": "Lark (International)", + "encryptKey": "Clé de chiffrement", + "encryptKeyPlaceholder": "Saisissez la clé de chiffrement de votre application Feishu", + "loginHint": "Aucune information d’identification configurée. Activez le canal pour démarrer l’enregistrement par code QR, ou saisissez manuellement l’ID de l’application et le secret de l’application.", + "qrExpired": "Code QR expiré. Veuillez réessayer en basculant le canal.", + "qrHint": "En attente de la numérisation du code QR...", + "qrScanHint": "Ouvre Feishu sur ton téléphone et scanne le code QR pour créer une application bot.", + "qrTitle": "Inscription par QR code Feishu", + "title": "Feishu", + "verificationToken": "Jeton de vérification", + "verificationTokenPlaceholder": "Saisissez le jeton de vérification de votre application Feishu" }, - "warning": { - "bypassPermissions": "Les agents CherryClaw fonctionnent en mode automatique complet par défaut. Tous les outils s'exécutent sans demander d'approbation." + "logs": "Journaux", + "noInstances": "Aucun canal {{type}} configuré. Cliquez sur « + Ajouter » pour en créer un.", + "noLogs": "Pas encore de journaux", + "notifyReceiver": "Recevoir les notifications de tâches", + "notifyReceiverHint": "Envoyer les résultats des tâches planifiées à ce canal.", + "qq": { + "appId": "App ID", + "appIdPlaceholder": "Entrez votre App ID QQ Bot", + "chatIds": "IDs de chat autorisés", + "chatIdsHint": "Format : c2c:openid, group:groupid, channel:channelid. Laissez vide pour tout autoriser.", + "chatIdsPlaceholder": "c2c:abc123, group:xyz789", + "clientSecret": "Client Secret", + "clientSecretPlaceholder": "Entrez votre Client Secret QQ Bot", + "description": "Recevoir et répondre aux messages via l'API officielle QQ Bot.", + "title": "QQ", + "whoamiTip": "💡 Astuce : Envoyez /whoami au bot pour obtenir votre ID de chat au bon format." + }, + "security": { + "inheritFromAgent": "Hériter de l'agent", + "permissionMode": "Mode de permission du canal", + "permissionModeHint": "Remplace le mode d'autorisation de l'agent pour les messages provenant de ce canal. « Hériter » utilise la valeur par défaut de l'agent." + }, + "selectAgent": "Sélectionnez un agent à lier", + "slack": { + "appToken": "Jeton au niveau de l'application", + "appTokenPlaceholder": "xapp-...", + "botToken": "Jeton du bot", + "botTokenPlaceholder": "xoxb-...", + "channelIds": "ID de canaux autorisés", + "channelIdsHint": "IDs de canaux Slack. Laissez vide pour autoriser tous.", + "channelIdsPlaceholder": "C01234567, D89012345", + "description": "Recevoir et répondre aux messages via un bot Slack en mode Socket.", + "title": "Slack", + "whoamiTip": "💡 Astuce : Envoie /whoami au bot pour obtenir l’ID du canal." + }, + "tab": "Canaux", + "telegram": { + "botToken": "Token du bot", + "botTokenPlaceholder": "Entrez votre token de bot Telegram", + "chatIds": "IDs de chat autorisés", + "chatIdsHint": "Séparés par des virgules. Laissez vide pour autoriser tous les chats.", + "chatIdsPlaceholder": "123456789, 987654321", + "description": "Recevoir et répondre aux messages via un bot Telegram en utilisant le long polling.", + "title": "Telegram" + }, + "title": "Canaux", + "updateError": "Échec de la mise à jour du canal", + "wechat": { + "addAccount": "Ajouter un compte WeChat", + "chatIds": "Identifiants d'utilisateurs autorisés", + "chatIdsHint": "Séparés par des virgules. Laisser vide pour autoriser tous les utilisateurs.", + "chatIdsPlaceholder": "wxid_abc123, wxid_def456", + "connected": "Connecté", + "description": "Recevoir et répondre aux messages via WeChat en utilisant l'API iLink Bot.", + "disconnected": "Déconnecté", + "loginHint": "La première connexion nécessite de scanner un code QR. Vérifiez les journaux de l'application pour obtenir l'URL de connexion.", + "qrHint": "Ouvrez WeChat sur votre téléphone, scannez le code QR pour vous connecter.", + "qrTitle": "Connexion par QR WeChat", + "title": "WeChat", + "whoamiTip": "Astuce : envoyez /whoami dans WeChat pour obtenir l'ID d'un utilisateur." } }, "delete": { @@ -339,15 +190,20 @@ "home": { "welcome_title": "De quoi allons-nous parler aujourd'hui ?" }, + "icon": { + "type": "[to be translated]:Agent Icon" + }, "input": { - "placeholder": "Entrez votre message ici, envoyez avec {{key}} - @ sélectionner le chemin, / sélectionner la commande", - "soul_placeholder": "Aidez-moi à me connecter à Telegram" + "placeholder": "Entrez votre message ici, envoyez avec {{key}} - @ sélectionner le chemin, / sélectionner la commande" }, "list": { "error": { "failed": "Échec de la liste des agents." } }, + "manage": { + "title": "[to be translated]:Manage Agents" + }, "pin": { "title": "Agent Pin" }, @@ -712,12 +568,6 @@ "tab": "Compétences", "title": "Compétences installées" }, - "soulMode": { - "description": "Lorsqu'il est activé, l'agent utilise une invite système personnée provenant des fichiers soul de l'espace de travail, injecte des outils de gestion autonome des tâches et désactive les outils interactifs non adaptés au fonctionnement autonome.", - "disabledBadge": "Désactivé par le mode autonome", - "disabledTooltip": "Cet outil est automatiquement désactivé lorsque le Mode Autonome est actif", - "title": "Mode autonome" - }, "tooling": { "mcp": { "description": "Connectez des serveurs MCP pour débloquer des outils supplémentaires que vous pouvez approuver ci-dessus.", @@ -784,6 +634,94 @@ } }, "sidebar_title": "Agents", + "skill": { + "manage": { + "title": "[to be translated]:Manage skills" + } + }, + "tasks": { + "add": "Ajouter une tâche", + "cancel": "Annuler", + "channels": { + "label": "Envoyer aux chaînes", + "noActiveChatIds": "Les canaux sélectionnés ne possèdent aucun destinataire disponible (ID de chat). Les résultats de la tâche peuvent ne pas être livrés. Veuillez d'abord envoyer un message au Bot sur la plateforme.", + "placeholder": "Sélectionnez les chaînes pour recevoir les résultats" + }, + "cronPlaceholder": "ex: 0 9 * * * (tous les jours à 9h)", + "delete": { + "confirm": "Êtes-vous sûr de vouloir supprimer cette tâche ?", + "label": "Supprimer" + }, + "edit": "Modifier", + "empty": "Aucune tâche planifiée. Ajoutez-en une pour commencer.", + "error": { + "createFailed": "Échec de la création de la tâche", + "deleteFailed": "Échec de la suppression de la tâche", + "loadFailed": "Échec du chargement des tâches", + "runFailed": "Échec de l'exécution de la tâche", + "updateFailed": "Échec de la mise à jour de la tâche" + }, + "frequency": { + "everyPrefix": "Toutes les", + "everySuffix": "minutes", + "label": "Fréquence d’exécution" + }, + "intervalPlaceholder": "Au moins 1", + "intervalUnit": "minutes", + "lastRun": "Dernière exécution", + "logs": { + "cancelled": "Annulé", + "completed": "Terminé", + "duration": "Durée", + "empty": "Aucun historique d'exécution.", + "failed": "Échoué", + "justNow": "à l'instant", + "label": "Historique d'exécution", + "loadError": "Échec du chargement de l'historique des exécutions", + "result": "Résultat", + "runAt": "Exécuté le", + "running": "En cours d'exécution...", + "search": "Rechercher les journaux...", + "status": "Statut", + "viewSession": "Voir la session" + }, + "name": { + "label": "Nom", + "placeholder": "ex: Revue de code quotidienne" + }, + "nextRun": "Prochaine exécution", + "oncePlaceholder": "Sélectionner la date et l'heure", + "pause": "Pause", + "prompt": { + "expand": "Développer l'éditeur", + "label": "Prompt", + "placeholder": "Que doit faire l'agent lors de l'exécution de cette tâche ?" + }, + "resume": "Reprendre", + "run": "Exécuter", + "runTriggered": "Tâche déclenchée", + "save": "Enregistrer", + "scheduleType": { + "cron": "Cron", + "interval": "Intervalle", + "once": "Une fois" + }, + "status": { + "active": "Actif", + "completed": "Terminé", + "paused": "En pause" + }, + "tab": "Tâches", + "time": { + "hoursAgo": "{{count}}h il y a", + "minutesAgo": "{{count}}m il y a" + }, + "timeout": { + "label": "Temps d’exécution maximal", + "placeholder": "Aucune limite" + }, + "title": "Tâches planifiées" + }, "todo": { "mock": { "actions": { @@ -935,6 +873,14 @@ "description": "Exécute des commandes shell dans votre environnement", "label": "Bash" }, + "CherryConfig": { + "description": "[to be translated]:Inspects and manages this agent configuration and channels", + "label": "[to be translated]:Agent Config" + }, + "CherryCron": { + "description": "Gère le planificateur intégré à l'application", + "label": "Planificateur" + }, "CherryKbManage": { "description": "Ajoute, supprime ou actualise des documents dans vos bases de connaissances", "label": "Gérer les connaissances" @@ -943,6 +889,10 @@ "description": "Recherche dans vos bases de connaissances", "label": "Recherche de connaissances" }, + "CherryNotify": { + "description": "[to be translated]:Sends a notification through a connected channel", + "label": "[to be translated]:Notify" + }, "CherryWebFetch": { "description": "Récupère et lit une page web", "label": "Récupération Web" @@ -951,10 +901,6 @@ "description": "Effectue des recherches sur le web via votre fournisseur configuré", "label": "Recherche sur le Web" }, - "ClawCron": { - "description": "Gère le planificateur intégré à l'application", - "label": "Planificateur" - }, "Edit": { "description": "Effectue des modifications ciblées sur des fichiers spécifiques", "label": "Modifier" @@ -1087,6 +1033,7 @@ "clear": { "content": "Supprimer le sujet supprimera tous les sujets et fichiers de l'aide. Êtes-vous sûr de vouloir continuer ?", "menu_title": "Supprimer toutes les conversations avec l'assistant", + "success_title": "[to be translated]:Cleared {{count}} topics", "title": "Supprimer les sujets" }, "copy": { @@ -2986,11 +2933,10 @@ "placeholder": "Donnez un nom à l'agent" }, "permission_mode": { - "help": "Passer à bypassPermissions équivaut à activer le mode Soul", "label": "Mode d'autorisation", "option": { "acceptEdits": "Accepter les modifications", - "bypassPermissions": "Contourner les permissions (Âme)", + "bypassPermissions": "Contourner les permissions", "default": "Défaut", "plan": "Mode plan" } @@ -3002,10 +2948,6 @@ "small_model": { "hint": "Vérifications et formatage légers", "label": "Petit modèle (optionnel)" - }, - "soul_enabled": { - "help": "Lorsque activé, accorde automatiquement bypassPermissions", - "label": "Mode âme" } }, "model_config": "Configuration du modèle", @@ -5331,6 +5273,13 @@ "auto_switch_to_topics": "Basculer automatiquement vers les sujets", "title": "Paramètres avancés" }, + "agent": { + "position": { + "label": "[to be translated]:Session position", + "left": "[to be translated]:Left", + "right": "[to be translated]:Right" + } + }, "appearance": { "title": "Apparence" }, @@ -7596,6 +7545,7 @@ }, "title": { "apps": "Mini-programmes", + "chat": "[to be translated]:Chat", "code": "Code", "files": "Fichiers", "home": "Page d'accueil", diff --git a/src/renderer/i18n/translate/ja-jp.json b/src/renderer/i18n/translate/ja-jp.json index 35d7e7dc60f..634e1febc82 100644 --- a/src/renderer/i18n/translate/ja-jp.json +++ b/src/renderer/i18n/translate/ja-jp.json @@ -31,262 +31,113 @@ "submit": "送信", "title": "エージェントからの質問" }, - "cherryClaw": { - "channels": { - "add": "追加", - "bindAgent": "バインドエージェント", - "chatIdsAutoTrackHint": "空欄のままにしておくと、システムが自動的に追跡します:まずプラットフォーム上でボットにメッセージを送信する必要があり、その後システムがチャットIDを記録して今後の通知に使用します。", - "comingSoon": "近日公開", - "connected": "接続済み", - "connecting": "接続", - "createError": "チャンネルの作成に失敗しました", - "deleteConfirm": "チャンネル「{{name}}」を削除しますか?", - "deleteError": "チャンネルの削除に失敗しました", - "description": "エージェントをメッセージングプラットフォームに接続します。", - "disconnected": "切断済み", - "discord": { - "botToken": "ボットトークン", - "botTokenPlaceholder": "Discordボットのトークンを入力してください", - "channelIds": "許可されたチャンネルID", - "channelIdsHint": "フォーマット: channel:id または dm:id。すべてを許可するには空のままにしてください。", - "channelIdsPlaceholder": "channel:123456789, dm:987654321", - "description": "WebSocketゲートウェイを介してDiscordボットでメッセージを受信し、応答する。", - "title": "ディスコード", - "whoamiTip": "💡 ヒント:正しい形式でチャンネルIDを取得するには、ボットに /whoami を送信してください。" - }, - "error": "エラー", - "feishu": { - "appId": "App ID", - "appIdPlaceholder": "FeishuアプリのApp IDを入力してください", - "appSecret": "App Secret", - "appSecretPlaceholder": "FeishuアプリのApp Secretを入力してください", - "chatIds": "許可するチャットID", - "chatIdsHint": "カンマ区切りのチャットID。空欄にするとすべてのチャットを許可します。", - "chatIdsPlaceholder": "oc_xxxxx, oc_yyyyy", - "connected": "接続済み", - "description": "Feishu/Larkボットを使用してWebSocket経由でメッセージを受信・応答します。", - "domain": "ドメイン", - "domainFeishu": "Feishu(中国)", - "domainLark": "Lark(国際版)", - "encryptKey": "暗号化キー", - "encryptKeyPlaceholder": "Feishuアプリの暗号化キーを入力してください", - "loginHint": "資格情報が設定されていません。QRコード登録を開始するにはチャネルを有効にするか、アプリIDとアプリシークレットを手動で入力してください。", - "qrExpired": "QRコードの有効期限が切れました。チャンネルを切り替えて再試行してください。", - "qrHint": "QRコードのスキャンを待っています...", - "qrScanHint": "スマートフォンでFeishuを開き、QRコードをスキャンしてボットアプリを作成してください。", - "qrTitle": "Feishu QR登録", - "title": "Feishu", - "verificationToken": "検証トークン", - "verificationTokenPlaceholder": "Feishuアプリの検証トークンを入力してください" - }, - "logs": "ログ", - "noInstances": "{{type}}チャンネルが設定されていません。「+追加」をクリックして作成してください。", - "noLogs": "まだログはありません", - "notifyReceiver": "タスク通知を受信", - "notifyReceiverHint": "スケジュールタスクの結果をこのチャンネルに送信します。", - "qq": { - "appId": "App ID", - "appIdPlaceholder": "QQ Bot の App ID を入力", - "chatIds": "許可するチャット ID", - "chatIdsHint": "形式: c2c:openid, group:groupid, channel:channelid。空欄ですべて許可。", - "chatIdsPlaceholder": "c2c:abc123, group:xyz789", - "clientSecret": "Client Secret", - "clientSecretPlaceholder": "QQ Bot の Client Secret を入力", - "description": "QQ Bot 公式 API を通じてメッセージを受信・返信します。", - "title": "QQ", - "whoamiTip": "💡 ヒント: ボットに /whoami を送信すると、正しい形式のチャット ID を取得できます。" - }, - "security": { - "inheritFromAgent": "エージェントから継承", - "permissionMode": "チャンネル権限モード", - "permissionModeHint": "このチャンネルからのメッセージについて、エージェントの権限モードを上書きします。「継承」を選択すると、エージェントのデフォルト設定が使用されます。" - }, - "selectAgent": "エージェントを選択してバインドしてください", - "slack": { - "appToken": "アプリレベルトークン", - "appTokenPlaceholder": "xapp-...", - "botToken": "ボットトークン", - "botTokenPlaceholder": "xoxb-...", - "channelIds": "許可されたチャンネルID", - "channelIdsHint": "SlackチャンネルID。すべてを許可する場合は空のままにしてください。", - "channelIdsPlaceholder": "C01234567、D89012345", - "description": "Socket Mode を使用して Slack ボット経由でメッセージを受信し、返信する。", - "title": "スラック", - "whoamiTip": "💡ヒント:チャンネルIDを取得するには、ボットに /whoami を送信してください。" - }, - "soulModeRequired": "このエージェントは自律モードが有効になっていません。チャンネル機能は動作しません。エージェント設定で有効にしてください。", - "tab": "チャンネル", - "telegram": { - "botToken": "Bot Token", - "botTokenPlaceholder": "Telegram ボットトークンを入力", - "chatIds": "許可するチャット ID", - "chatIdsHint": "カンマ区切り。空欄ですべてのチャットを許可。", - "chatIdsPlaceholder": "123456789, 987654321", - "description": "Telegram ボットでロングポーリングを使用してメッセージを受信・返信します。", - "title": "Telegram" - }, - "title": "チャンネル", - "updateError": "チャンネルの更新に失敗しました", - "wechat": { - "addAccount": "WeChatアカウントを追加", - "chatIds": "許可されたユーザーID", - "chatIdsHint": "カンマ区切り。空欄にするとすべてのユーザーが許可されます。", - "chatIdsPlaceholder": "wxid_abc123、wxid_def456", - "connected": "接続済み", - "description": "iLink Bot APIを使用してWeChat経由でメッセージを受信・応答する。", - "disconnected": "切断されました", - "loginHint": "初回ログインにはQRコードのスキャンが必要です。ログインURLはアプリのログを確認してください。", - "qrHint": "スマートフォンでWeChatを開き、QRコードをスキャンしてログインしてください。", - "qrTitle": "WeChat QRコードログイン", - "title": "WeChat", - "whoamiTip": "ヒント:WeChatで/whoamiを送信すると、ユーザーのIDが取得できます。" - } - }, - "heartbeat": { - "enabled": "ハートビートを有効化", - "enabledHelper": "有効にすると、ワークスペースに heartbeat.md が存在する場合、スケジューラが設定された間隔でその内容を読み取り、メインセッションに送信します。", - "interval": "間隔(分)", - "intervalHelper": "ハートビートの実行頻度(分単位)。" - }, - "sandbox": { - "description": "OS レベルのサンドボックスを使用して、エージェントのファイルシステムとネットワークアクセスをワークスペースディレクトリに制限します。", - "helper": "ファイルシステムとコマンドアクセスをワークスペースパスに制限します。", - "label": "サンドボックスモード" - }, - "scheduler": { - "cron": { - "helper": "標準の cron 式(例: '*/30 * * * *' で 30 分ごと)。", - "label": "Cron 式", - "placeholder": "*/30 * * * *" - }, - "description": "スケジューラはスケジュールに従ってエージェントを自動的にトリガーします。", - "enabled": "スケジューラを有効化", - "enabledHelper": "有効にすると、エージェントは設定されたスケジュールに基づいて自動的に実行されます。", - "errors": { - "invalidCron": "無効な cron 式", - "invalidDelay": "遅延は正の数である必要があります", - "invalidInterval": "間隔は正の数である必要があります" - }, - "interval": { - "helper": "エージェントの実行頻度(秒単位)。", - "label": "間隔(秒)" - }, - "lastRun": "前回の実行", - "never": "なし", - "nextRun": "次回の実行", - "oneTime": { - "helper": "エージェント実行前の一回限りの遅延(秒単位)。", - "label": "遅延(秒)" - }, - "status": { - "running": "実行中", - "stopped": "停止中", - "tickInProgress": "実行中" - }, - "tab": "スケジューラ", - "title": "スケジューラ設定", - "type": { - "cron": "Cron 式", - "interval": "固定間隔", - "label": "スケジュールタイプ", - "one-time": "一回限りの遅延" - } + "channels": { + "add": "追加", + "bindAgent": "バインドエージェント", + "chatIdsAutoTrackHint": "空欄のままにしておくと、システムが自動的に追跡します:まずプラットフォーム上でボットにメッセージを送信する必要があり、その後システムがチャットIDを記録して今後の通知に使用します。", + "comingSoon": "近日公開", + "connected": "接続済み", + "connecting": "接続", + "createError": "チャンネルの作成に失敗しました", + "deleteConfirm": "チャンネル「{{name}}」を削除しますか?", + "deleteError": "チャンネルの削除に失敗しました", + "description": "エージェントをメッセージングプラットフォームに接続します。", + "disconnected": "切断済み", + "discord": { + "botToken": "ボットトークン", + "botTokenPlaceholder": "Discordボットのトークンを入力してください", + "channelIds": "許可されたチャンネルID", + "channelIdsHint": "フォーマット: channel:id または dm:id。すべてを許可するには空のままにしてください。", + "channelIdsPlaceholder": "channel:123456789, dm:987654321", + "description": "WebSocketゲートウェイを介してDiscordボットでメッセージを受信し、応答する。", + "title": "ディスコード", + "whoamiTip": "💡 ヒント:正しい形式でチャンネルIDを取得するには、ボットに /whoami を送信してください。" }, - "soul": { - "description": "ソウルはエージェントの個性と基本動作を定義します。ワークスペースルートの soul.md から読み込まれます。", - "empty": "ワークスペースルートに soul.md が見つかりません。エージェントの個性を定義するために作成してください。", - "enabled": "ソウルを有効化", - "enabledHelper": "有効にすると、エージェントはワークスペースルートから soul.md を読み取り、システムプロンプトの先頭に追加します。", - "fileLabel": "soul.md", - "preview": "ソウルプレビュー", - "tab": "ソウル", - "title": "ソウル設定" - }, - "tasks": { - "add": "タスクを追加", - "cancel": "キャンセル", - "channels": { - "label": "チャンネルに送信", - "noActiveChatIds": "選択されたチャンネルには利用可能な受信者(チャットID)が存在しません。タスクの結果が配信されない可能性があります。まず、プラットフォーム上でボットにメッセージを送信してください。", - "placeholder": "結果を受け取るチャンネルを選択してください" - }, - "cronPlaceholder": "例: 0 9 * * *(毎日午前 9 時)", - "delete": { - "confirm": "このタスクを削除してもよろしいですか?", - "label": "削除" - }, - "edit": "編集", - "empty": "スケジュールタスクがありません。追加して開始しましょう。", - "error": { - "createFailed": "タスクの作成に失敗しました", - "deleteFailed": "タスクの削除に失敗しました", - "loadFailed": "タスクの読み込みに失敗しました", - "runFailed": "タスクの実行に失敗しました", - "updateFailed": "タスクの更新に失敗しました" - }, - "frequency": { - "everyPrefix": "間隔", - "everySuffix": "分ごとに実行", - "label": "実行頻度" - }, - "intervalPlaceholder": "1以上", - "intervalUnit": "議事録", - "lastRun": "前回の実行", - "logs": { - "cancelled": "キャンセル", - "completed": "完了", - "duration": "所要時間", - "empty": "実行履歴がありません。", - "failed": "失敗", - "justNow": "さっき", - "label": "実行履歴", - "loadError": "実行履歴の読み込みに失敗しました", - "result": "結果", - "runAt": "実行時刻", - "running": "走っている…", - "search": "検索ログ...", - "status": "ステータス", - "viewSession": "セッションを表示" - }, - "name": { - "label": "名前", - "placeholder": "例: 毎日のコードレビュー" - }, - "nextRun": "次回の実行", - "oncePlaceholder": "日時を選択", - "pause": "一時停止", - "prompt": { - "expand": "エディタを展開", - "label": "プロンプト", - "placeholder": "このタスク実行時にエージェントが行うべきことは?" - }, - "resume": "再開", - "run": "実行", - "runTriggered": "タスクがトリガーされました", - "save": "保存", - "scheduleType": { - "cron": "Cron", - "interval": "間隔", - "once": "一回限り" - }, - "status": { - "active": "アクティブ", - "completed": "完了", - "paused": "一時停止中" - }, - "tab": "タスク", - "time": { - "hoursAgo": "{{count}}時間前", - "minutesAgo": "{{count}}分前" - }, - "timeout": { - "label": "最長実行時間", - "placeholder": "制限なし" - }, - "title": "スケジュールタスク" + "error": "エラー", + "feishu": { + "appId": "App ID", + "appIdPlaceholder": "FeishuアプリのApp IDを入力してください", + "appSecret": "App Secret", + "appSecretPlaceholder": "FeishuアプリのApp Secretを入力してください", + "chatIds": "許可するチャットID", + "chatIdsHint": "カンマ区切りのチャットID。空欄にするとすべてのチャットを許可します。", + "chatIdsPlaceholder": "oc_xxxxx, oc_yyyyy", + "connected": "接続済み", + "description": "Feishu/Larkボットを使用してWebSocket経由でメッセージを受信・応答します。", + "domain": "ドメイン", + "domainFeishu": "Feishu(中国)", + "domainLark": "Lark(国際版)", + "encryptKey": "暗号化キー", + "encryptKeyPlaceholder": "Feishuアプリの暗号化キーを入力してください", + "loginHint": "資格情報が設定されていません。QRコード登録を開始するにはチャネルを有効にするか、アプリIDとアプリシークレットを手動で入力してください。", + "qrExpired": "QRコードの有効期限が切れました。チャンネルを切り替えて再試行してください。", + "qrHint": "QRコードのスキャンを待っています...", + "qrScanHint": "スマートフォンでFeishuを開き、QRコードをスキャンしてボットアプリを作成してください。", + "qrTitle": "Feishu QR登録", + "title": "Feishu", + "verificationToken": "検証トークン", + "verificationTokenPlaceholder": "Feishuアプリの検証トークンを入力してください" }, - "warning": { - "bypassPermissions": "CherryClaw エージェントはデフォルトでフルオートモードで実行されます。すべてのツールは承認なしで実行されます。" + "logs": "ログ", + "noInstances": "{{type}}チャンネルが設定されていません。「+追加」をクリックして作成してください。", + "noLogs": "まだログはありません", + "notifyReceiver": "タスク通知を受信", + "notifyReceiverHint": "スケジュールタスクの結果をこのチャンネルに送信します。", + "qq": { + "appId": "App ID", + "appIdPlaceholder": "QQ Bot の App ID を入力", + "chatIds": "許可するチャット ID", + "chatIdsHint": "形式: c2c:openid, group:groupid, channel:channelid。空欄ですべて許可。", + "chatIdsPlaceholder": "c2c:abc123, group:xyz789", + "clientSecret": "Client Secret", + "clientSecretPlaceholder": "QQ Bot の Client Secret を入力", + "description": "QQ Bot 公式 API を通じてメッセージを受信・返信します。", + "title": "QQ", + "whoamiTip": "💡 ヒント: ボットに /whoami を送信すると、正しい形式のチャット ID を取得できます。" + }, + "security": { + "inheritFromAgent": "エージェントから継承", + "permissionMode": "チャンネル権限モード", + "permissionModeHint": "このチャンネルからのメッセージについて、エージェントの権限モードを上書きします。「継承」を選択すると、エージェントのデフォルト設定が使用されます。" + }, + "selectAgent": "エージェントを選択してバインドしてください", + "slack": { + "appToken": "アプリレベルトークン", + "appTokenPlaceholder": "xapp-...", + "botToken": "ボットトークン", + "botTokenPlaceholder": "xoxb-...", + "channelIds": "許可されたチャンネルID", + "channelIdsHint": "SlackチャンネルID。すべてを許可する場合は空のままにしてください。", + "channelIdsPlaceholder": "C01234567、D89012345", + "description": "Socket Mode を使用して Slack ボット経由でメッセージを受信し、返信する。", + "title": "スラック", + "whoamiTip": "💡ヒント:チャンネルIDを取得するには、ボットに /whoami を送信してください。" + }, + "tab": "チャンネル", + "telegram": { + "botToken": "Bot Token", + "botTokenPlaceholder": "Telegram ボットトークンを入力", + "chatIds": "許可するチャット ID", + "chatIdsHint": "カンマ区切り。空欄ですべてのチャットを許可。", + "chatIdsPlaceholder": "123456789, 987654321", + "description": "Telegram ボットでロングポーリングを使用してメッセージを受信・返信します。", + "title": "Telegram" + }, + "title": "チャンネル", + "updateError": "チャンネルの更新に失敗しました", + "wechat": { + "addAccount": "WeChatアカウントを追加", + "chatIds": "許可されたユーザーID", + "chatIdsHint": "カンマ区切り。空欄にするとすべてのユーザーが許可されます。", + "chatIdsPlaceholder": "wxid_abc123、wxid_def456", + "connected": "接続済み", + "description": "iLink Bot APIを使用してWeChat経由でメッセージを受信・応答する。", + "disconnected": "切断されました", + "loginHint": "初回ログインにはQRコードのスキャンが必要です。ログインURLはアプリのログを確認してください。", + "qrHint": "スマートフォンでWeChatを開き、QRコードをスキャンしてログインしてください。", + "qrTitle": "WeChat QRコードログイン", + "title": "WeChat", + "whoamiTip": "ヒント:WeChatで/whoamiを送信すると、ユーザーのIDが取得できます。" } }, "delete": { @@ -339,15 +190,20 @@ "home": { "welcome_title": "今日は何について話しましょうか?" }, + "icon": { + "type": "[to be translated]:Agent Icon" + }, "input": { - "placeholder": "メッセージをここに入力し、{{key}}で送信 - @でパスを選択、/でコマンドを選択", - "soul_placeholder": "Telegramに接続するのを手伝ってください" + "placeholder": "メッセージをここに入力し、{{key}}で送信 - @でパスを選択、/でコマンドを選択" }, "list": { "error": { "failed": "エージェントの一覧取得に失敗しました。" } }, + "manage": { + "title": "[to be translated]:Manage Agents" + }, "pin": { "title": "ピンエージェント" }, @@ -712,12 +568,6 @@ "tab": "スキル", "title": "インストール済みスキル" }, - "soulMode": { - "description": "有効にすると、エージェントはワークスペースのsoulファイルからカスタムシステムプロンプトを使用し、自律的タスク管理ツールを注入し、自律動作には不適切なインタラクティブツールを無効化します。", - "disabledBadge": "自律モードにより無効化されました", - "disabledTooltip": "このツールは、自律モードがアクティブな場合に自動的に無効になります", - "title": "自律モード" - }, "tooling": { "mcp": { "description": "MCPサーバーを接続して、上で承認できる追加ツールを解放します。", @@ -784,6 +634,94 @@ } }, "sidebar_title": "エージェント", + "skill": { + "manage": { + "title": "[to be translated]:Manage skills" + } + }, + "tasks": { + "add": "タスクを追加", + "cancel": "キャンセル", + "channels": { + "label": "チャンネルに送信", + "noActiveChatIds": "選択されたチャンネルには利用可能な受信者(チャットID)が存在しません。タスクの結果が配信されない可能性があります。まず、プラットフォーム上でボットにメッセージを送信してください。", + "placeholder": "結果を受け取るチャンネルを選択してください" + }, + "cronPlaceholder": "例: 0 9 * * *(毎日午前 9 時)", + "delete": { + "confirm": "このタスクを削除してもよろしいですか?", + "label": "削除" + }, + "edit": "編集", + "empty": "スケジュールタスクがありません。追加して開始しましょう。", + "error": { + "createFailed": "タスクの作成に失敗しました", + "deleteFailed": "タスクの削除に失敗しました", + "loadFailed": "タスクの読み込みに失敗しました", + "runFailed": "タスクの実行に失敗しました", + "updateFailed": "タスクの更新に失敗しました" + }, + "frequency": { + "everyPrefix": "間隔", + "everySuffix": "分ごとに実行", + "label": "実行頻度" + }, + "intervalPlaceholder": "1以上", + "intervalUnit": "議事録", + "lastRun": "前回の実行", + "logs": { + "cancelled": "キャンセル", + "completed": "完了", + "duration": "所要時間", + "empty": "実行履歴がありません。", + "failed": "失敗", + "justNow": "さっき", + "label": "実行履歴", + "loadError": "実行履歴の読み込みに失敗しました", + "result": "結果", + "runAt": "実行時刻", + "running": "走っている…", + "search": "検索ログ...", + "status": "ステータス", + "viewSession": "セッションを表示" + }, + "name": { + "label": "名前", + "placeholder": "例: 毎日のコードレビュー" + }, + "nextRun": "次回の実行", + "oncePlaceholder": "日時を選択", + "pause": "一時停止", + "prompt": { + "expand": "エディタを展開", + "label": "プロンプト", + "placeholder": "このタスク実行時にエージェントが行うべきことは?" + }, + "resume": "再開", + "run": "実行", + "runTriggered": "タスクがトリガーされました", + "save": "保存", + "scheduleType": { + "cron": "Cron", + "interval": "間隔", + "once": "一回限り" + }, + "status": { + "active": "アクティブ", + "completed": "完了", + "paused": "一時停止中" + }, + "tab": "タスク", + "time": { + "hoursAgo": "{{count}}時間前", + "minutesAgo": "{{count}}分前" + }, + "timeout": { + "label": "最長実行時間", + "placeholder": "制限なし" + }, + "title": "スケジュールタスク" + }, "todo": { "mock": { "actions": { @@ -935,6 +873,14 @@ "description": "環境でシェルコマンドを実行します", "label": "Bash" }, + "CherryConfig": { + "description": "[to be translated]:Inspects and manages this agent configuration and channels", + "label": "[to be translated]:Agent Config" + }, + "CherryCron": { + "description": "アプリ内スケジューラを管理します", + "label": "スケジューラ" + }, "CherryKbManage": { "description": "ナレッジベース内のドキュメントを追加、削除、または更新します", "label": "ナレッジ管理" @@ -943,6 +889,10 @@ "description": "ナレッジベースを検索します", "label": "ナレッジ検索" }, + "CherryNotify": { + "description": "[to be translated]:Sends a notification through a connected channel", + "label": "[to be translated]:Notify" + }, "CherryWebFetch": { "description": "ウェブページを取得して読み取る", "label": "Web Fetch" @@ -951,10 +901,6 @@ "description": "設定されたプロバイダーを通じてウェブを検索します", "label": "ウェブ検索" }, - "ClawCron": { - "description": "アプリ内スケジューラを管理します", - "label": "スケジューラ" - }, "Edit": { "description": "特定のファイルに対してターゲットを絞った編集を行う", "label": "編集" @@ -1087,6 +1033,7 @@ "clear": { "content": "トピックをクリアすると、アシスタント内のすべてのトピックとファイルが削除されます。続行しますか?", "menu_title": "すべてのアシスタントとの会話を削除してください", + "success_title": "[to be translated]:Cleared {{count}} topics", "title": "トピックをクリア" }, "copy": { @@ -2986,11 +2933,10 @@ "placeholder": "エージェントに名前を付けてください" }, "permission_mode": { - "help": "bypassPermissionsに切り替えることは、Soulモードを有効にすることと同等です。", "label": "パーミッションモード", "option": { "acceptEdits": "編集を承認する", - "bypassPermissions": "権限をバイパス(ソウル)", + "bypassPermissions": "権限をバイパス", "default": "デフォルト", "plan": "プラン モード" } @@ -3002,10 +2948,6 @@ "small_model": { "hint": "軽量なチェックとフォーマット", "label": "小型モデル(オプション)" - }, - "soul_enabled": { - "help": "有効にすると、bypassPermissions を自動的に付与します", - "label": "ソウルモード" } }, "model_config": "モデル構成", @@ -5331,6 +5273,13 @@ "auto_switch_to_topics": "トピックに自動的に切り替える", "title": "詳細設定" }, + "agent": { + "position": { + "label": "[to be translated]:Session position", + "left": "[to be translated]:Left", + "right": "[to be translated]:Right" + } + }, "appearance": { "title": "外観" }, @@ -7596,6 +7545,7 @@ }, "title": { "apps": "アプリ", + "chat": "[to be translated]:Chat", "code": "Code", "files": "ファイル", "home": "ホーム", diff --git a/src/renderer/i18n/translate/pt-pt.json b/src/renderer/i18n/translate/pt-pt.json index 8f1dc7d137e..21b5047a4ac 100644 --- a/src/renderer/i18n/translate/pt-pt.json +++ b/src/renderer/i18n/translate/pt-pt.json @@ -31,262 +31,113 @@ "submit": "Enviar", "title": "Perguntas do Agente" }, - "cherryClaw": { - "channels": { - "add": "Adicionar", - "bindAgent": "Bind Agente", - "chatIdsAutoTrackHint": "Quando deixado vazio, o sistema rastreará automaticamente: você precisa enviar uma mensagem ao Bot na plataforma primeiro, então o sistema registrará o ID do Chat para notificações futuras.", - "comingSoon": "Em breve", - "connected": "Conectado", - "connecting": "Conectando", - "createError": "Falha ao criar canal", - "deleteConfirm": "Excluir canal \"{{name}}\"?", - "deleteError": "Falha ao excluir canal", - "description": "Conecte o seu agente a plataformas de mensagens.", - "disconnected": "Desconectado", - "discord": { - "botToken": "Token do Bot", - "botTokenPlaceholder": "Insira o token do seu bot do Discord", - "channelIds": "IDs de Canais Permitidos", - "channelIdsHint": "Formato: canal:id ou dm:id. Deixe vazio para permitir todos.", - "channelIdsPlaceholder": "canal:123456789, mp:987654321", - "description": "Receba e responda a mensagens por meio de um bot do Discord usando o gateway WebSocket.", - "title": "Discord", - "whoamiTip": "💡 Dica: Envie /whoami para o bot para obter o ID do seu canal no formato correto." - }, - "error": "Erro", - "feishu": { - "appId": "App ID", - "appIdPlaceholder": "Introduza o App ID da sua aplicação Feishu", - "appSecret": "App Secret", - "appSecretPlaceholder": "Introduza o App Secret da sua aplicação Feishu", - "chatIds": "IDs de chat permitidos", - "chatIdsHint": "IDs de chat separados por vírgulas. Deixe vazio para permitir todos os chats.", - "chatIdsPlaceholder": "oc_xxxxx, oc_yyyyy", - "connected": "Conectado", - "description": "Receber e responder a mensagens através de um bot Feishu/Lark usando WebSocket.", - "domain": "Domínio", - "domainFeishu": "Feishu (China)", - "domainLark": "Lark (Internacional)", - "encryptKey": "Chave de encriptação", - "encryptKeyPlaceholder": "Introduza a chave de encriptação da sua aplicação Feishu", - "loginHint": "Nenhuma credencial configurada. Ative o canal para iniciar o registro do código QR ou insira manualmente o ID do aplicativo e o segredo do aplicativo.", - "qrExpired": "Código QR expirado. Por favor, tente novamente alternando o canal.", - "qrHint": "Aguardando leitura do código QR...", - "qrScanHint": "Abra o Feishu no seu celular e escaneie o código QR para criar um aplicativo de bot.", - "qrTitle": "Registro por QR Code do Feishu", - "title": "Feishu", - "verificationToken": "Token de verificação", - "verificationTokenPlaceholder": "Introduza o token de verificação da sua aplicação Feishu" - }, - "logs": "Registros", - "noInstances": "Nenhum canal {{type}} configurado. Clique em \"+ Adicionar\" para criar um.", - "noLogs": "Ainda não há registros", - "notifyReceiver": "Receber notificações de tarefas", - "notifyReceiverHint": "Enviar resultados de tarefas agendadas para este canal.", - "qq": { - "appId": "App ID", - "appIdPlaceholder": "Introduza o seu App ID do QQ Bot", - "chatIds": "IDs de chat permitidos", - "chatIdsHint": "Formato: c2c:openid, group:groupid, channel:channelid. Deixe vazio para permitir todos.", - "chatIdsPlaceholder": "c2c:abc123, group:xyz789", - "clientSecret": "Client Secret", - "clientSecretPlaceholder": "Introduza o seu Client Secret do QQ Bot", - "description": "Receber e responder a mensagens através da API oficial do QQ Bot.", - "title": "QQ", - "whoamiTip": "💡 Dica: Envie /whoami ao bot para obter o seu ID de chat no formato correto." - }, - "security": { - "inheritFromAgent": "Herdar do agente", - "permissionMode": "Modo de Permissão do Canal", - "permissionModeHint": "Substitua o modo de permissão do agente para mensagens deste canal. \"Herdar\" utiliza o padrão do agente." - }, - "selectAgent": "Selecione um agente para vincular", - "slack": { - "appToken": "Token de Nível de Aplicação", - "appTokenPlaceholder": "xapp-...", - "botToken": "Token do Bot", - "botTokenPlaceholder": "xoxb-...", - "channelIds": "IDs de Canais Permitidos", - "channelIdsHint": "IDs de canais do Slack. Deixe vazio para permitir todos.", - "channelIdsPlaceholder": "C01234567, D89012345", - "description": "Receba e responda a mensagens por meio de um bot do Slack usando o modo Socket.", - "title": "Slack", - "whoamiTip": "💡 Dica: Envie /whoami ao bot para obter o ID do canal." - }, - "soulModeRequired": "Este agente não tem o Modo Autônomo ativado. Os recursos do canal não funcionarão. Ative-o nas configurações do agente.", - "tab": "Canais", - "telegram": { - "botToken": "Token do bot", - "botTokenPlaceholder": "Introduza o token do seu bot Telegram", - "chatIds": "IDs de chat permitidos", - "chatIdsHint": "Separados por vírgulas. Deixe vazio para permitir todos os chats.", - "chatIdsPlaceholder": "123456789, 987654321", - "description": "Receber e responder a mensagens através de um bot Telegram usando long polling.", - "title": "Telegram" - }, - "title": "Canais", - "updateError": "Falha ao atualizar o canal", - "wechat": { - "addAccount": "Adicionar Conta WeChat", - "chatIds": "IDs de Usuários Permitidos", - "chatIdsHint": "Separados por vírgula. Deixe vazio para permitir todos os utilizadores.", - "chatIdsPlaceholder": "wxid_abc123, wxid_def456", - "connected": "Conectado", - "description": "Receba e responda a mensagens via WeChat usando a API iLink Bot.", - "disconnected": "Desconectado", - "loginHint": "O primeiro login exige a leitura de um código QR. Verifique os logs da aplicação para obter o URL de login.", - "qrHint": "Abra o WeChat no seu celular, escaneie o código QR para fazer login.", - "qrTitle": "Login por QR Code do WeChat", - "title": "WeChat", - "whoamiTip": "Dica: Envie /whoami no WeChat para obter o ID de um usuário." - } - }, - "heartbeat": { - "enabled": "Ativar Heartbeat", - "enabledHelper": "Quando ativado e heartbeat.md existe no espaço de trabalho, o agendador lê o seu conteúdo e envia-o para a sessão principal no intervalo configurado.", - "interval": "Intervalo (minutos)", - "intervalHelper": "Com que frequência o heartbeat é executado, em minutos." - }, - "sandbox": { - "description": "Restringe o acesso do agente ao sistema de ficheiros e rede apenas ao diretório do espaço de trabalho usando sandboxing ao nível do sistema operativo.", - "helper": "Restringir o acesso ao sistema de ficheiros e comandos ao caminho do espaço de trabalho.", - "label": "Modo Sandbox" - }, - "scheduler": { - "cron": { - "helper": "Expressão cron padrão (ex: '*/30 * * * *' para cada 30 minutos).", - "label": "Expressão Cron", - "placeholder": "*/30 * * * *" - }, - "description": "O agendador aciona o agente automaticamente conforme um cronograma.", - "enabled": "Ativar agendador", - "enabledHelper": "Quando ativado, o agente será executado automaticamente com base no cronograma configurado.", - "errors": { - "invalidCron": "Expressão cron inválida", - "invalidDelay": "O atraso deve ser um número positivo", - "invalidInterval": "O intervalo deve ser um número positivo" - }, - "interval": { - "helper": "Com que frequência o agente deve ser executado, em segundos.", - "label": "Intervalo (segundos)" - }, - "lastRun": "Última execução", - "never": "Nunca", - "nextRun": "Próxima execução", - "oneTime": { - "helper": "Atraso único em segundos antes do agente ser executado.", - "label": "Atraso (segundos)" - }, - "status": { - "running": "A executar", - "stopped": "Parado", - "tickInProgress": "Tick em progresso" - }, - "tab": "Agendador", - "title": "Configuração do agendador", - "type": { - "cron": "Expressão Cron", - "interval": "Intervalo fixo", - "label": "Tipo de agendamento", - "one-time": "Atraso único" - } + "channels": { + "add": "Adicionar", + "bindAgent": "Bind Agente", + "chatIdsAutoTrackHint": "Quando deixado vazio, o sistema rastreará automaticamente: você precisa enviar uma mensagem ao Bot na plataforma primeiro, então o sistema registrará o ID do Chat para notificações futuras.", + "comingSoon": "Em breve", + "connected": "Conectado", + "connecting": "Conectando", + "createError": "Falha ao criar canal", + "deleteConfirm": "Excluir canal \"{{name}}\"?", + "deleteError": "Falha ao excluir canal", + "description": "Conecte o seu agente a plataformas de mensagens.", + "disconnected": "Desconectado", + "discord": { + "botToken": "Token do Bot", + "botTokenPlaceholder": "Insira o token do seu bot do Discord", + "channelIds": "IDs de Canais Permitidos", + "channelIdsHint": "Formato: canal:id ou dm:id. Deixe vazio para permitir todos.", + "channelIdsPlaceholder": "canal:123456789, mp:987654321", + "description": "Receba e responda a mensagens por meio de um bot do Discord usando o gateway WebSocket.", + "title": "Discord", + "whoamiTip": "💡 Dica: Envie /whoami para o bot para obter o ID do seu canal no formato correto." }, - "soul": { - "description": "A alma define a personalidade e o comportamento base do agente. É lida a partir de soul.md na raiz do espaço de trabalho.", - "empty": "Nenhum soul.md encontrado na raiz do espaço de trabalho. Crie um para definir a personalidade do agente.", - "enabled": "Ativar alma", - "enabledHelper": "Quando ativado, o agente lê soul.md da raiz do espaço de trabalho e adiciona-o ao início do prompt do sistema.", - "fileLabel": "soul.md", - "preview": "Pré-visualização da alma", - "tab": "Alma", - "title": "Configuração da alma" - }, - "tasks": { - "add": "Adicionar tarefa", - "cancel": "Cancelar", - "channels": { - "label": "Enviar para os Canais", - "noActiveChatIds": "Os canais selecionados não têm destinatários disponíveis (ID do Chat). Os resultados da tarefa podem não ser entregues. Por favor, envie uma mensagem ao Bot na plataforma primeiro.", - "placeholder": "Selecione os canais para receber os resultados" - }, - "cronPlaceholder": "ex: 0 9 * * * (todos os dias às 9h)", - "delete": { - "confirm": "Tem certeza de que deseja eliminar esta tarefa?", - "label": "Eliminar" - }, - "edit": "Editar", - "empty": "Sem tarefas agendadas. Adicione uma para começar.", - "error": { - "createFailed": "Falha ao criar tarefa", - "deleteFailed": "Falha ao excluir tarefa", - "loadFailed": "Falha ao carregar tarefas", - "runFailed": "Falha ao executar a tarefa", - "updateFailed": "Falha ao atualizar a tarefa" - }, - "frequency": { - "everyPrefix": "A cada", - "everySuffix": "minutos", - "label": "Frequência de execução" - }, - "intervalPlaceholder": "Pelo menos 1", - "intervalUnit": "minutos", - "lastRun": "Última execução", - "logs": { - "cancelled": "Cancelado", - "completed": "Concluído", - "duration": "Duração", - "empty": "Ainda sem histórico de execução.", - "failed": "Falhou", - "justNow": "agora mesmo", - "label": "Histórico de execução", - "loadError": "Falha ao carregar o histórico de execução", - "result": "Resultado", - "runAt": "Executado em", - "running": "Correndo...", - "search": "Pesquisar registros...", - "status": "Estado", - "viewSession": "Ver sessão" - }, - "name": { - "label": "Nome", - "placeholder": "ex: Revisão de código diária" - }, - "nextRun": "Próxima execução", - "oncePlaceholder": "Selecionar data e hora", - "pause": "Pausar", - "prompt": { - "expand": "Expandir editor", - "label": "Prompt", - "placeholder": "O que o agente deve fazer quando esta tarefa for executada?" - }, - "resume": "Retomar", - "run": "Executar", - "runTriggered": "Tarefa acionada", - "save": "Guardar", - "scheduleType": { - "cron": "Cron", - "interval": "Intervalo", - "once": "Uma vez" - }, - "status": { - "active": "Ativo", - "completed": "Concluído", - "paused": "Em pausa" - }, - "tab": "Tarefas", - "time": { - "hoursAgo": "{{count}}h atrás", - "minutesAgo": "{{count}}m atrás" - }, - "timeout": { - "label": "Tempo máximo de execução", - "placeholder": "Sem limite" - }, - "title": "Tarefas agendadas" + "error": "Erro", + "feishu": { + "appId": "App ID", + "appIdPlaceholder": "Introduza o App ID da sua aplicação Feishu", + "appSecret": "App Secret", + "appSecretPlaceholder": "Introduza o App Secret da sua aplicação Feishu", + "chatIds": "IDs de chat permitidos", + "chatIdsHint": "IDs de chat separados por vírgulas. Deixe vazio para permitir todos os chats.", + "chatIdsPlaceholder": "oc_xxxxx, oc_yyyyy", + "connected": "Conectado", + "description": "Receber e responder a mensagens através de um bot Feishu/Lark usando WebSocket.", + "domain": "Domínio", + "domainFeishu": "Feishu (China)", + "domainLark": "Lark (Internacional)", + "encryptKey": "Chave de encriptação", + "encryptKeyPlaceholder": "Introduza a chave de encriptação da sua aplicação Feishu", + "loginHint": "Nenhuma credencial configurada. Ative o canal para iniciar o registro do código QR ou insira manualmente o ID do aplicativo e o segredo do aplicativo.", + "qrExpired": "Código QR expirado. Por favor, tente novamente alternando o canal.", + "qrHint": "Aguardando leitura do código QR...", + "qrScanHint": "Abra o Feishu no seu celular e escaneie o código QR para criar um aplicativo de bot.", + "qrTitle": "Registro por QR Code do Feishu", + "title": "Feishu", + "verificationToken": "Token de verificação", + "verificationTokenPlaceholder": "Introduza o token de verificação da sua aplicação Feishu" }, - "warning": { - "bypassPermissions": "Os agentes CherryClaw funcionam em modo automático completo por predefinição. Todas as ferramentas são executadas sem pedir aprovação." + "logs": "Registros", + "noInstances": "Nenhum canal {{type}} configurado. Clique em \"+ Adicionar\" para criar um.", + "noLogs": "Ainda não há registros", + "notifyReceiver": "Receber notificações de tarefas", + "notifyReceiverHint": "Enviar resultados de tarefas agendadas para este canal.", + "qq": { + "appId": "App ID", + "appIdPlaceholder": "Introduza o seu App ID do QQ Bot", + "chatIds": "IDs de chat permitidos", + "chatIdsHint": "Formato: c2c:openid, group:groupid, channel:channelid. Deixe vazio para permitir todos.", + "chatIdsPlaceholder": "c2c:abc123, group:xyz789", + "clientSecret": "Client Secret", + "clientSecretPlaceholder": "Introduza o seu Client Secret do QQ Bot", + "description": "Receber e responder a mensagens através da API oficial do QQ Bot.", + "title": "QQ", + "whoamiTip": "💡 Dica: Envie /whoami ao bot para obter o seu ID de chat no formato correto." + }, + "security": { + "inheritFromAgent": "Herdar do agente", + "permissionMode": "Modo de Permissão do Canal", + "permissionModeHint": "Substitua o modo de permissão do agente para mensagens deste canal. \"Herdar\" utiliza o padrão do agente." + }, + "selectAgent": "Selecione um agente para vincular", + "slack": { + "appToken": "Token de Nível de Aplicação", + "appTokenPlaceholder": "xapp-...", + "botToken": "Token do Bot", + "botTokenPlaceholder": "xoxb-...", + "channelIds": "IDs de Canais Permitidos", + "channelIdsHint": "IDs de canais do Slack. Deixe vazio para permitir todos.", + "channelIdsPlaceholder": "C01234567, D89012345", + "description": "Receba e responda a mensagens por meio de um bot do Slack usando o modo Socket.", + "title": "Slack", + "whoamiTip": "💡 Dica: Envie /whoami ao bot para obter o ID do canal." + }, + "tab": "Canais", + "telegram": { + "botToken": "Token do bot", + "botTokenPlaceholder": "Introduza o token do seu bot Telegram", + "chatIds": "IDs de chat permitidos", + "chatIdsHint": "Separados por vírgulas. Deixe vazio para permitir todos os chats.", + "chatIdsPlaceholder": "123456789, 987654321", + "description": "Receber e responder a mensagens através de um bot Telegram usando long polling.", + "title": "Telegram" + }, + "title": "Canais", + "updateError": "Falha ao atualizar o canal", + "wechat": { + "addAccount": "Adicionar Conta WeChat", + "chatIds": "IDs de Usuários Permitidos", + "chatIdsHint": "Separados por vírgula. Deixe vazio para permitir todos os utilizadores.", + "chatIdsPlaceholder": "wxid_abc123, wxid_def456", + "connected": "Conectado", + "description": "Receba e responda a mensagens via WeChat usando a API iLink Bot.", + "disconnected": "Desconectado", + "loginHint": "O primeiro login exige a leitura de um código QR. Verifique os logs da aplicação para obter o URL de login.", + "qrHint": "Abra o WeChat no seu celular, escaneie o código QR para fazer login.", + "qrTitle": "Login por QR Code do WeChat", + "title": "WeChat", + "whoamiTip": "Dica: Envie /whoami no WeChat para obter o ID de um usuário." } }, "delete": { @@ -339,15 +190,20 @@ "home": { "welcome_title": "Sobre o que vamos falar hoje?" }, + "icon": { + "type": "[to be translated]:Agent Icon" + }, "input": { - "placeholder": "Digite sua mensagem aqui, envie com {{key}} - @ selecionar caminho, / selecionar comando", - "soul_placeholder": "Ajude-me a conectar ao Telegram" + "placeholder": "Digite sua mensagem aqui, envie com {{key}} - @ selecionar caminho, / selecionar comando" }, "list": { "error": { "failed": "Falha ao listar agentes." } }, + "manage": { + "title": "[to be translated]:Manage Agents" + }, "pin": { "title": "Agente Pin" }, @@ -712,12 +568,6 @@ "tab": "Habilidades", "title": "Habilidades Instaladas" }, - "soulMode": { - "description": "Quando ativado, o agente utiliza um prompt de sistema personalizado dos arquivos soul do workspace, injeta ferramentas de gerenciamento de tarefas autônomas e desabilita ferramentas interativas inadequadas para operação autônoma.", - "disabledBadge": "Desativado pelo Modo Autónomo", - "disabledTooltip": "Esta ferramenta é automaticamente desativada quando o Modo Autónomo está ativo", - "title": "Modo Autônomo" - }, "tooling": { "mcp": { "description": "Conecte servidores MCP para desbloquear ferramentas adicionais que você pode aprovar acima.", @@ -784,6 +634,94 @@ } }, "sidebar_title": "Agentes", + "skill": { + "manage": { + "title": "[to be translated]:Manage skills" + } + }, + "tasks": { + "add": "Adicionar tarefa", + "cancel": "Cancelar", + "channels": { + "label": "Enviar para os Canais", + "noActiveChatIds": "Os canais selecionados não têm destinatários disponíveis (ID do Chat). Os resultados da tarefa podem não ser entregues. Por favor, envie uma mensagem ao Bot na plataforma primeiro.", + "placeholder": "Selecione os canais para receber os resultados" + }, + "cronPlaceholder": "ex: 0 9 * * * (todos os dias às 9h)", + "delete": { + "confirm": "Tem certeza de que deseja eliminar esta tarefa?", + "label": "Eliminar" + }, + "edit": "Editar", + "empty": "Sem tarefas agendadas. Adicione uma para começar.", + "error": { + "createFailed": "Falha ao criar tarefa", + "deleteFailed": "Falha ao excluir tarefa", + "loadFailed": "Falha ao carregar tarefas", + "runFailed": "Falha ao executar a tarefa", + "updateFailed": "Falha ao atualizar a tarefa" + }, + "frequency": { + "everyPrefix": "A cada", + "everySuffix": "minutos", + "label": "Frequência de execução" + }, + "intervalPlaceholder": "Pelo menos 1", + "intervalUnit": "minutos", + "lastRun": "Última execução", + "logs": { + "cancelled": "Cancelado", + "completed": "Concluído", + "duration": "Duração", + "empty": "Ainda sem histórico de execução.", + "failed": "Falhou", + "justNow": "agora mesmo", + "label": "Histórico de execução", + "loadError": "Falha ao carregar o histórico de execução", + "result": "Resultado", + "runAt": "Executado em", + "running": "Correndo...", + "search": "Pesquisar registros...", + "status": "Estado", + "viewSession": "Ver sessão" + }, + "name": { + "label": "Nome", + "placeholder": "ex: Revisão de código diária" + }, + "nextRun": "Próxima execução", + "oncePlaceholder": "Selecionar data e hora", + "pause": "Pausar", + "prompt": { + "expand": "Expandir editor", + "label": "Prompt", + "placeholder": "O que o agente deve fazer quando esta tarefa for executada?" + }, + "resume": "Retomar", + "run": "Executar", + "runTriggered": "Tarefa acionada", + "save": "Guardar", + "scheduleType": { + "cron": "Cron", + "interval": "Intervalo", + "once": "Uma vez" + }, + "status": { + "active": "Ativo", + "completed": "Concluído", + "paused": "Em pausa" + }, + "tab": "Tarefas", + "time": { + "hoursAgo": "{{count}}h atrás", + "minutesAgo": "{{count}}m atrás" + }, + "timeout": { + "label": "Tempo máximo de execução", + "placeholder": "Sem limite" + }, + "title": "Tarefas agendadas" + }, "todo": { "mock": { "actions": { @@ -935,6 +873,14 @@ "description": "Executa comandos shell no seu ambiente", "label": "Bash" }, + "CherryConfig": { + "description": "[to be translated]:Inspects and manages this agent configuration and channels", + "label": "[to be translated]:Agent Config" + }, + "CherryCron": { + "description": "Gerencia o agendador dentro do aplicativo", + "label": "Agendador" + }, "CherryKbManage": { "description": "Adiciona, exclui ou atualiza documentos em suas bases de conhecimento", "label": "Gerenciar Conhecimento" @@ -943,6 +889,10 @@ "description": "Pesquisa suas bases de conhecimento", "label": "Pesquisa de Conhecimento" }, + "CherryNotify": { + "description": "[to be translated]:Sends a notification through a connected channel", + "label": "[to be translated]:Notify" + }, "CherryWebFetch": { "description": "Busca e lê uma página da web", "label": "Busca na Web" @@ -951,10 +901,6 @@ "description": "Pesquisa na web através do seu provedor configurado", "label": "Pesquisa na Web" }, - "ClawCron": { - "description": "Gerencia o agendador dentro do aplicativo", - "label": "Agendador" - }, "Edit": { "description": "Faz edições direcionadas em arquivos específicos", "label": "Editar" @@ -1087,6 +1033,7 @@ "clear": { "content": "Limpar o tópico removerá todos os tópicos e arquivos do assistente. Tem certeza de que deseja continuar?", "menu_title": "Apagar todas as conversas com o assistente", + "success_title": "[to be translated]:Cleared {{count}} topics", "title": "Limpar Tópico" }, "copy": { @@ -2986,11 +2933,10 @@ "placeholder": "Dê um nome ao agente" }, "permission_mode": { - "help": "Mudar para bypassPermissions é equivalente a ativar o modo Soul", "label": "Modo de permissão", "option": { "acceptEdits": "Aceitar edições", - "bypassPermissions": "Ignorar permissões (Alma)", + "bypassPermissions": "Ignorar permissões", "default": "Padrão", "plan": "Modo de plano" } @@ -3002,10 +2948,6 @@ "small_model": { "hint": "Verificações leves e formatação", "label": "Modelo pequeno (opcional)" - }, - "soul_enabled": { - "help": "Quando ativado, concede automaticamente bypassPermissions", - "label": "Modo alma" } }, "model_config": "Configuração do modelo", @@ -5331,6 +5273,13 @@ "auto_switch_to_topics": "Alternar automaticamente para tópicos", "title": "Configurações avançadas" }, + "agent": { + "position": { + "label": "[to be translated]:Session position", + "left": "[to be translated]:Left", + "right": "[to be translated]:Right" + } + }, "appearance": { "title": "Aparência" }, @@ -7596,6 +7545,7 @@ }, "title": { "apps": "Miniaplicativos", + "chat": "[to be translated]:Chat", "code": "Código", "files": "Arquivos", "home": "Página Inicial", diff --git a/src/renderer/i18n/translate/ro-ro.json b/src/renderer/i18n/translate/ro-ro.json index 39ce784a170..2552a8eec45 100644 --- a/src/renderer/i18n/translate/ro-ro.json +++ b/src/renderer/i18n/translate/ro-ro.json @@ -31,262 +31,113 @@ "submit": "Trimite", "title": "Întrebări de la Agent" }, - "cherryClaw": { - "channels": { - "add": "Adaugă", - "bindAgent": "Agent de legătură", - "chatIdsAutoTrackHint": "Când este lăsat gol, sistemul va urmări automat: trebuie să trimiți mai întâi un mesaj Bot-ului pe platformă, apoi sistemul va înregistra ID-ul conversației pentru notificările viitoare.", - "comingSoon": "În curând", - "connected": "Conectat", - "connecting": "Conectare", - "createError": "Nu s-a reușit crearea canalului", - "deleteConfirm": "Șterge canalul „{{name}}”?", - "deleteError": "Nu s-a putut șterge canalul", - "description": "Conectați agentul la platforme de mesagerie.", - "disconnected": "Deconectat", - "discord": { - "botToken": "Token Bot", - "botTokenPlaceholder": "Introdu token-ul botului tău Discord", - "channelIds": "ID-uri de canale permise", - "channelIdsHint": "Format: canal:id sau dm:id. Lasă gol pentru a permite tot.", - "channelIdsPlaceholder": "canal:123456789, dm:987654321", - "description": "Primește și răspunde la mesaje printr-un bot Discord folosind gateway WebSocket.", - "title": "Discord", - "whoamiTip": "💡 Sfaturi: Trimite /whoami către bot pentru a obține ID-ul canalului tău în formatul corect." - }, - "error": "Eroare", - "feishu": { - "appId": "App ID", - "appIdPlaceholder": "Introduceți App ID-ul aplicației Feishu", - "appSecret": "App Secret", - "appSecretPlaceholder": "Introduceți App Secret-ul aplicației Feishu", - "chatIds": "ID-uri de chat permise", - "chatIdsHint": "ID-uri de chat separate prin virgulă. Lăsați gol pentru a permite toate chat-urile.", - "chatIdsPlaceholder": "oc_xxxxx, oc_yyyyy", - "connected": "Conectat", - "description": "Primiți și răspundeți la mesaje printr-un bot Feishu/Lark folosind WebSocket.", - "domain": "Domeniu", - "domainFeishu": "Feishu (China)", - "domainLark": "Lark (Internațional)", - "encryptKey": "Cheie de criptare", - "encryptKeyPlaceholder": "Introduceți cheia de criptare a aplicației Feishu", - "loginHint": "Nu sunt configurate credențiale. Activează canalul pentru a începe înregistrarea prin cod QR sau introdu manual ID-ul aplicației și secretul aplicației.", - "qrExpired": "Codul QR a expirat. Vă rugăm să reîncercați comutând canalul.", - "qrHint": "Se așteaptă scanarea codului QR...", - "qrScanHint": "Deschide Feishu pe telefonul tău și scanează codul QR pentru a crea o aplicație bot.", - "qrTitle": "Înregistrare prin cod QR Feishu", - "title": "Feishu", - "verificationToken": "Token de verificare", - "verificationTokenPlaceholder": "Introduceți tokenul de verificare al aplicației Feishu" - }, - "logs": "Jurnale", - "noInstances": "Niciun canal {{type}} configurat. Faceți clic pe „+ Adăugare” pentru a crea unul.", - "noLogs": "Niciun jurnal încă", - "notifyReceiver": "Primește notificări de sarcini", - "notifyReceiverHint": "Trimite rezultatele sarcinilor programate pe acest canal.", - "qq": { - "appId": "App ID", - "appIdPlaceholder": "Introduceți App ID-ul QQ Bot", - "chatIds": "ID-uri de chat permise", - "chatIdsHint": "Format: c2c:openid, group:groupid, channel:channelid. Lăsați gol pentru a permite toate.", - "chatIdsPlaceholder": "c2c:abc123, group:xyz789", - "clientSecret": "Client Secret", - "clientSecretPlaceholder": "Introduceți Client Secret-ul QQ Bot", - "description": "Primește și răspunde la mesaje prin API-ul oficial QQ Bot.", - "title": "QQ", - "whoamiTip": "💡 Sfat: Trimiteți /whoami la bot pentru a obține ID-ul de chat în formatul corect." - }, - "security": { - "inheritFromAgent": "Moștenește de la agent", - "permissionMode": "Modul de permisiune al canalului", - "permissionModeHint": "Suprascrie modul de permisiune al agentului pentru mesajele de pe acest canal. „Moștenire” utilizează setarea implicită a agentului." - }, - "selectAgent": "Selectați un agent pentru a-l lega", - "slack": { - "appToken": "Token la nivel de aplicație", - "appTokenPlaceholder": "xapp-...", - "botToken": "Token bot", - "botTokenPlaceholder": "xoxb-...", - "channelIds": "ID-uri canale permise", - "channelIdsHint": "ID-urile canalelor Slack. Lăsați gol pentru a permite toate.", - "channelIdsPlaceholder": "C01234567, D89012345", - "description": "Primește și răspunde la mesaje printr-un bot Slack folosind Socket Mode.", - "title": "Slack", - "whoamiTip": "💡 Sfat: Trimite /whoami către bot pentru a obține ID-ul canalului." - }, - "soulModeRequired": "Acest agent nu are Activat Modul Autonom. Funcționalitățile canalului nu vor funcționa. Vă rugăm să-l activați în setările agentului.", - "tab": "Canale", - "telegram": { - "botToken": "Token Bot", - "botTokenPlaceholder": "Introduceți token-ul bot-ului Telegram", - "chatIds": "ID-uri de chat permise", - "chatIdsHint": "Separate prin virgulă. Lăsați gol pentru a permite toate chat-urile.", - "chatIdsPlaceholder": "123456789, 987654321", - "description": "Primește și răspunde la mesaje prin bot Telegram folosind long polling.", - "title": "Telegram" - }, - "title": "Canale", - "updateError": "Nu s-a putut actualiza canalul", - "wechat": { - "addAccount": "Adaugă cont WeChat", - "chatIds": "ID-uri de utilizator permise", - "chatIdsHint": "Separat prin virgulă. Lăsați gol pentru a permite tuturor utilizatorilor.", - "chatIdsPlaceholder": "wxid_abc123, wxid_def456", - "connected": "Conectat", - "description": "Primește și răspunde la mesaje prin WeChat folosind API-ul iLink Bot.", - "disconnected": "Deconectat", - "loginHint": "Prima autentificare necesită scanarea unui cod QR. Verifică jurnalele aplicației pentru adresa URL de autentificare.", - "qrHint": "Deschideți WeChat pe telefon, scanați codul QR pentru a vă conecta.", - "qrTitle": "Autentificare WeChat cu cod QR", - "title": "WeChat", - "whoamiTip": "Sfat: Trimiteți /whoami în WeChat pentru a obține ID-ul unui utilizator." - } - }, - "heartbeat": { - "enabled": "Activează Heartbeat", - "enabledHelper": "Când este activat și heartbeat.md există în spațiul de lucru, planificatorul citește conținutul și îl trimite la sesiunea principală la intervalul configurat.", - "interval": "Interval (minute)", - "intervalHelper": "Cât de des rulează heartbeat-ul, în minute." - }, - "sandbox": { - "description": "Restricționează accesul agentului la sistemul de fișiere și rețea doar la directorul spațiului de lucru folosind sandboxing la nivel de sistem de operare.", - "helper": "Restricționează accesul la sistemul de fișiere și comenzi la calea spațiului de lucru.", - "label": "Mod Sandbox" - }, - "scheduler": { - "cron": { - "helper": "Expresie cron standard (ex: '*/30 * * * *' pentru fiecare 30 de minute).", - "label": "Expresie Cron", - "placeholder": "*/30 * * * *" - }, - "description": "Planificatorul declanșează agentul automat conform unui program.", - "enabled": "Activează planificatorul", - "enabledHelper": "Când este activat, agentul va rula automat pe baza programului configurat.", - "errors": { - "invalidCron": "Expresie cron invalidă", - "invalidDelay": "Întârzierea trebuie să fie un număr pozitiv", - "invalidInterval": "Intervalul trebuie să fie un număr pozitiv" - }, - "interval": { - "helper": "Cât de des ar trebui să ruleze agentul, în secunde.", - "label": "Interval (secunde)" - }, - "lastRun": "Ultima rulare", - "never": "Niciodată", - "nextRun": "Următoarea rulare", - "oneTime": { - "helper": "Întârziere unică în secunde înainte de rularea agentului.", - "label": "Întârziere (secunde)" - }, - "status": { - "running": "Rulează", - "stopped": "Oprit", - "tickInProgress": "Tick în progres" - }, - "tab": "Planificator", - "title": "Configurare planificator", - "type": { - "cron": "Expresie Cron", - "interval": "Interval fix", - "label": "Tip program", - "one-time": "Întârziere unică" - } + "channels": { + "add": "Adaugă", + "bindAgent": "Agent de legătură", + "chatIdsAutoTrackHint": "Când este lăsat gol, sistemul va urmări automat: trebuie să trimiți mai întâi un mesaj Bot-ului pe platformă, apoi sistemul va înregistra ID-ul conversației pentru notificările viitoare.", + "comingSoon": "În curând", + "connected": "Conectat", + "connecting": "Conectare", + "createError": "Nu s-a reușit crearea canalului", + "deleteConfirm": "Șterge canalul „{{name}}”?", + "deleteError": "Nu s-a putut șterge canalul", + "description": "Conectați agentul la platforme de mesagerie.", + "disconnected": "Deconectat", + "discord": { + "botToken": "Token Bot", + "botTokenPlaceholder": "Introdu token-ul botului tău Discord", + "channelIds": "ID-uri de canale permise", + "channelIdsHint": "Format: canal:id sau dm:id. Lasă gol pentru a permite tot.", + "channelIdsPlaceholder": "canal:123456789, dm:987654321", + "description": "Primește și răspunde la mesaje printr-un bot Discord folosind gateway WebSocket.", + "title": "Discord", + "whoamiTip": "💡 Sfaturi: Trimite /whoami către bot pentru a obține ID-ul canalului tău în formatul corect." }, - "soul": { - "description": "Sufletul definește personalitatea și comportamentul de bază al agentului. Este citit din soul.md în rădăcina spațiului de lucru.", - "empty": "Nu s-a găsit soul.md în rădăcina spațiului de lucru. Creați unul pentru a defini personalitatea agentului.", - "enabled": "Activează sufletul", - "enabledHelper": "Când este activat, agentul citește soul.md din rădăcina spațiului de lucru și îl adaugă la începutul promptului de sistem.", - "fileLabel": "soul.md", - "preview": "Previzualizare suflet", - "tab": "Suflet", - "title": "Configurare suflet" - }, - "tasks": { - "add": "Adaugă sarcină", - "cancel": "Anulează", - "channels": { - "label": "Trimite către canale", - "noActiveChatIds": "Canalele selectate nu au destinatari disponibili (ID de chat). Este posibil ca rezultatele sarcinii să nu fie livrate. Vă rugăm să trimiteți mai întâi un mesaj botului pe platformă.", - "placeholder": "Selectați canalele pentru a primi rezultatele" - }, - "cronPlaceholder": "ex: 0 9 * * * (în fiecare zi la ora 9)", - "delete": { - "confirm": "Sunteți sigur că doriți să ștergeți această sarcină?", - "label": "Șterge" - }, - "edit": "Editează", - "empty": "Nu există sarcini programate. Adăugați una pentru a începe.", - "error": { - "createFailed": "Nu s-a reușit crearea sarcinii", - "deleteFailed": "Nu s-a reușit ștergerea sarcinii", - "loadFailed": "Nu s-au putut încărca sarcinile", - "runFailed": "Nu s-a reușit executarea sarcinii", - "updateFailed": "Nu s-a reușit actualizarea sarcinii" - }, - "frequency": { - "everyPrefix": "La fiecare", - "everySuffix": "minute", - "label": "Frecvență de execuție" - }, - "intervalPlaceholder": "Cel puțin 1", - "intervalUnit": "minute", - "lastRun": "Ultima rulare", - "logs": { - "cancelled": "Anulat", - "completed": "Finalizat", - "duration": "Durată", - "empty": "Nu există încă istoric de rulare.", - "failed": "Eșuat", - "justNow": "chiar acum", - "label": "Istoric rulări", - "loadError": "Nu s-a reușit încărcarea istoricului de execuție", - "result": "Rezultat", - "runAt": "Rulat la", - "running": "Alergând...", - "search": "Caută jurnalele...", - "status": "Stare", - "viewSession": "Vizualizare sesiune" - }, - "name": { - "label": "Nume", - "placeholder": "ex: Revizuire cod zilnică" - }, - "nextRun": "Următoarea rulare", - "oncePlaceholder": "Selectați data și ora", - "pause": "Pauză", - "prompt": { - "expand": "Extinde editorul", - "label": "Prompt", - "placeholder": "Ce ar trebui să facă agentul când rulează această sarcină?" - }, - "resume": "Reia", - "run": "Rulează", - "runTriggered": "Sarcină declanșată", - "save": "Salvează", - "scheduleType": { - "cron": "Cron", - "interval": "Interval", - "once": "O singură dată" - }, - "status": { - "active": "Activă", - "completed": "Finalizată", - "paused": "În pauză" - }, - "tab": "Sarcini", - "time": { - "hoursAgo": "acum {{count}}h", - "minutesAgo": "acum {{count}}m" - }, - "timeout": { - "label": "Timp maxim de execuție", - "placeholder": "Fără limită" - }, - "title": "Sarcini programate" + "error": "Eroare", + "feishu": { + "appId": "App ID", + "appIdPlaceholder": "Introduceți App ID-ul aplicației Feishu", + "appSecret": "App Secret", + "appSecretPlaceholder": "Introduceți App Secret-ul aplicației Feishu", + "chatIds": "ID-uri de chat permise", + "chatIdsHint": "ID-uri de chat separate prin virgulă. Lăsați gol pentru a permite toate chat-urile.", + "chatIdsPlaceholder": "oc_xxxxx, oc_yyyyy", + "connected": "Conectat", + "description": "Primiți și răspundeți la mesaje printr-un bot Feishu/Lark folosind WebSocket.", + "domain": "Domeniu", + "domainFeishu": "Feishu (China)", + "domainLark": "Lark (Internațional)", + "encryptKey": "Cheie de criptare", + "encryptKeyPlaceholder": "Introduceți cheia de criptare a aplicației Feishu", + "loginHint": "Nu sunt configurate credențiale. Activează canalul pentru a începe înregistrarea prin cod QR sau introdu manual ID-ul aplicației și secretul aplicației.", + "qrExpired": "Codul QR a expirat. Vă rugăm să reîncercați comutând canalul.", + "qrHint": "Se așteaptă scanarea codului QR...", + "qrScanHint": "Deschide Feishu pe telefonul tău și scanează codul QR pentru a crea o aplicație bot.", + "qrTitle": "Înregistrare prin cod QR Feishu", + "title": "Feishu", + "verificationToken": "Token de verificare", + "verificationTokenPlaceholder": "Introduceți tokenul de verificare al aplicației Feishu" }, - "warning": { - "bypassPermissions": "Agenții CherryClaw rulează în mod automat complet în mod implicit. Toate instrumentele se execută fără a solicita aprobare." + "logs": "Jurnale", + "noInstances": "Niciun canal {{type}} configurat. Faceți clic pe „+ Adăugare” pentru a crea unul.", + "noLogs": "Niciun jurnal încă", + "notifyReceiver": "Primește notificări de sarcini", + "notifyReceiverHint": "Trimite rezultatele sarcinilor programate pe acest canal.", + "qq": { + "appId": "App ID", + "appIdPlaceholder": "Introduceți App ID-ul QQ Bot", + "chatIds": "ID-uri de chat permise", + "chatIdsHint": "Format: c2c:openid, group:groupid, channel:channelid. Lăsați gol pentru a permite toate.", + "chatIdsPlaceholder": "c2c:abc123, group:xyz789", + "clientSecret": "Client Secret", + "clientSecretPlaceholder": "Introduceți Client Secret-ul QQ Bot", + "description": "Primește și răspunde la mesaje prin API-ul oficial QQ Bot.", + "title": "QQ", + "whoamiTip": "💡 Sfat: Trimiteți /whoami la bot pentru a obține ID-ul de chat în formatul corect." + }, + "security": { + "inheritFromAgent": "Moștenește de la agent", + "permissionMode": "Modul de permisiune al canalului", + "permissionModeHint": "Suprascrie modul de permisiune al agentului pentru mesajele de pe acest canal. „Moștenire” utilizează setarea implicită a agentului." + }, + "selectAgent": "Selectați un agent pentru a-l lega", + "slack": { + "appToken": "Token la nivel de aplicație", + "appTokenPlaceholder": "xapp-...", + "botToken": "Token bot", + "botTokenPlaceholder": "xoxb-...", + "channelIds": "ID-uri canale permise", + "channelIdsHint": "ID-urile canalelor Slack. Lăsați gol pentru a permite toate.", + "channelIdsPlaceholder": "C01234567, D89012345", + "description": "Primește și răspunde la mesaje printr-un bot Slack folosind Socket Mode.", + "title": "Slack", + "whoamiTip": "💡 Sfat: Trimite /whoami către bot pentru a obține ID-ul canalului." + }, + "tab": "Canale", + "telegram": { + "botToken": "Token Bot", + "botTokenPlaceholder": "Introduceți token-ul bot-ului Telegram", + "chatIds": "ID-uri de chat permise", + "chatIdsHint": "Separate prin virgulă. Lăsați gol pentru a permite toate chat-urile.", + "chatIdsPlaceholder": "123456789, 987654321", + "description": "Primește și răspunde la mesaje prin bot Telegram folosind long polling.", + "title": "Telegram" + }, + "title": "Canale", + "updateError": "Nu s-a putut actualiza canalul", + "wechat": { + "addAccount": "Adaugă cont WeChat", + "chatIds": "ID-uri de utilizator permise", + "chatIdsHint": "Separat prin virgulă. Lăsați gol pentru a permite tuturor utilizatorilor.", + "chatIdsPlaceholder": "wxid_abc123, wxid_def456", + "connected": "Conectat", + "description": "Primește și răspunde la mesaje prin WeChat folosind API-ul iLink Bot.", + "disconnected": "Deconectat", + "loginHint": "Prima autentificare necesită scanarea unui cod QR. Verifică jurnalele aplicației pentru adresa URL de autentificare.", + "qrHint": "Deschideți WeChat pe telefon, scanați codul QR pentru a vă conecta.", + "qrTitle": "Autentificare WeChat cu cod QR", + "title": "WeChat", + "whoamiTip": "Sfat: Trimiteți /whoami în WeChat pentru a obține ID-ul unui utilizator." } }, "delete": { @@ -339,15 +190,20 @@ "home": { "welcome_title": "Despre ce să vorbim astăzi?" }, + "icon": { + "type": "[to be translated]:Agent Icon" + }, "input": { - "placeholder": "Introdu mesajul aici, trimite cu {{key}} - @ selectează calea, / selectează comanda", - "soul_placeholder": "Ajută-mă să mă conectez la Telegram" + "placeholder": "Introdu mesajul aici, trimite cu {{key}} - @ selectează calea, / selectează comanda" }, "list": { "error": { "failed": "Nu s-a putut afișa lista de agenți." } }, + "manage": { + "title": "[to be translated]:Manage Agents" + }, "pin": { "title": "Agent Pin" }, @@ -712,12 +568,6 @@ "tab": "Abilități", "title": "Abilități instalate" }, - "soulMode": { - "description": "Când este activată, agentul folosește o comandă de sistem personalizată din fișierele soul ale spațiului de lucru, injectează instrumente autonome de gestionare a sarcinilor și dezactivează instrumentele interactive care nu sunt potrivite pentru funcționarea autonomă.", - "disabledBadge": "Dezactivat de Modul Autonom", - "disabledTooltip": "Acest instrument este dezactivat automat când Modul Autonom este activ", - "title": "Mod Autonom" - }, "tooling": { "mcp": { "description": "Conectează servere MCP pentru a debloca instrumente suplimentare pe care le poți aproba mai sus.", @@ -784,6 +634,94 @@ } }, "sidebar_title": "Agenți", + "skill": { + "manage": { + "title": "[to be translated]:Manage skills" + } + }, + "tasks": { + "add": "Adaugă sarcină", + "cancel": "Anulează", + "channels": { + "label": "Trimite către canale", + "noActiveChatIds": "Canalele selectate nu au destinatari disponibili (ID de chat). Este posibil ca rezultatele sarcinii să nu fie livrate. Vă rugăm să trimiteți mai întâi un mesaj botului pe platformă.", + "placeholder": "Selectați canalele pentru a primi rezultatele" + }, + "cronPlaceholder": "ex: 0 9 * * * (în fiecare zi la ora 9)", + "delete": { + "confirm": "Sunteți sigur că doriți să ștergeți această sarcină?", + "label": "Șterge" + }, + "edit": "Editează", + "empty": "Nu există sarcini programate. Adăugați una pentru a începe.", + "error": { + "createFailed": "Nu s-a reușit crearea sarcinii", + "deleteFailed": "Nu s-a reușit ștergerea sarcinii", + "loadFailed": "Nu s-au putut încărca sarcinile", + "runFailed": "Nu s-a reușit executarea sarcinii", + "updateFailed": "Nu s-a reușit actualizarea sarcinii" + }, + "frequency": { + "everyPrefix": "La fiecare", + "everySuffix": "minute", + "label": "Frecvență de execuție" + }, + "intervalPlaceholder": "Cel puțin 1", + "intervalUnit": "minute", + "lastRun": "Ultima rulare", + "logs": { + "cancelled": "Anulat", + "completed": "Finalizat", + "duration": "Durată", + "empty": "Nu există încă istoric de rulare.", + "failed": "Eșuat", + "justNow": "chiar acum", + "label": "Istoric rulări", + "loadError": "Nu s-a reușit încărcarea istoricului de execuție", + "result": "Rezultat", + "runAt": "Rulat la", + "running": "Alergând...", + "search": "Caută jurnalele...", + "status": "Stare", + "viewSession": "Vizualizare sesiune" + }, + "name": { + "label": "Nume", + "placeholder": "ex: Revizuire cod zilnică" + }, + "nextRun": "Următoarea rulare", + "oncePlaceholder": "Selectați data și ora", + "pause": "Pauză", + "prompt": { + "expand": "Extinde editorul", + "label": "Prompt", + "placeholder": "Ce ar trebui să facă agentul când rulează această sarcină?" + }, + "resume": "Reia", + "run": "Rulează", + "runTriggered": "Sarcină declanșată", + "save": "Salvează", + "scheduleType": { + "cron": "Cron", + "interval": "Interval", + "once": "O singură dată" + }, + "status": { + "active": "Activă", + "completed": "Finalizată", + "paused": "În pauză" + }, + "tab": "Sarcini", + "time": { + "hoursAgo": "acum {{count}}h", + "minutesAgo": "acum {{count}}m" + }, + "timeout": { + "label": "Timp maxim de execuție", + "placeholder": "Fără limită" + }, + "title": "Sarcini programate" + }, "todo": { "mock": { "actions": { @@ -935,6 +873,14 @@ "description": "Execută comenzi shell în mediul tău", "label": "Bash" }, + "CherryConfig": { + "description": "[to be translated]:Inspects and manages this agent configuration and channels", + "label": "[to be translated]:Agent Config" + }, + "CherryCron": { + "description": "Gestionează programatorul din aplicație", + "label": "Planificator" + }, "CherryKbManage": { "description": "Adaugă, șterge sau reîmprospătează documente în bazele tale de cunoștințe", "label": "Gestionează cunoștințele" @@ -943,6 +889,10 @@ "description": "Caută în bazele tale de cunoștințe", "label": "Căutare de cunoștințe" }, + "CherryNotify": { + "description": "[to be translated]:Sends a notification through a connected channel", + "label": "[to be translated]:Notify" + }, "CherryWebFetch": { "description": "Preia și citește o pagină web", "label": "Preluare Web" @@ -951,10 +901,6 @@ "description": "Caută pe web prin furnizorul configurat", "label": "Căutare pe web" }, - "ClawCron": { - "description": "Gestionează programatorul din aplicație", - "label": "Planificator" - }, "Edit": { "description": "Efectuează editări țintite asupra fișierelor specifice", "label": "Editează" @@ -1087,6 +1033,7 @@ "clear": { "content": "Golirea subiectului va șterge toate subiectele și fișierele din asistent. Ești sigur că vrei să continui?", "menu_title": "Șterge toate conversațiile cu asistentul", + "success_title": "[to be translated]:Cleared {{count}} topics", "title": "Șterge subiectele" }, "copy": { @@ -2986,11 +2933,10 @@ "placeholder": "Dă agentului un nume" }, "permission_mode": { - "help": "Comutarea la bypassPermissions echivalează cu activarea modului Soul", "label": "Mod de permisiune", "option": { "acceptEdits": "Acceptă modificările", - "bypassPermissions": "Ocolire permisiuni (Suflet)", + "bypassPermissions": "Ocolire permisiuni", "default": "Implicit", "plan": "Mod plan" } @@ -3002,10 +2948,6 @@ "small_model": { "hint": "Verificări ușoare și formatare", "label": "Model mic (opțional)" - }, - "soul_enabled": { - "help": "Când este activată, acordă automat bypassPermissions", - "label": "Mod suflet" } }, "model_config": "Configurație model", @@ -5331,6 +5273,13 @@ "auto_switch_to_topics": "Comutare automată la subiect", "title": "Setări avansate" }, + "agent": { + "position": { + "label": "[to be translated]:Session position", + "left": "[to be translated]:Left", + "right": "[to be translated]:Right" + } + }, "appearance": { "title": "Aspect" }, @@ -7596,6 +7545,7 @@ }, "title": { "apps": "Aplicații", + "chat": "[to be translated]:Chat", "code": "Cod", "files": "Fișiere", "home": "Acasă", diff --git a/src/renderer/i18n/translate/ru-ru.json b/src/renderer/i18n/translate/ru-ru.json index a00e2ce1aa4..99404436896 100644 --- a/src/renderer/i18n/translate/ru-ru.json +++ b/src/renderer/i18n/translate/ru-ru.json @@ -31,262 +31,113 @@ "submit": "Отправить", "title": "Вопросы от агента" }, - "cherryClaw": { - "channels": { - "add": "Добавить", - "bindAgent": "Связать агента", - "chatIdsAutoTrackHint": "Если оставить пустым, система автоматически отследит: сначала нужно отправить сообщение боту на платформе, затем система запишет Chat ID для будущих уведомлений.", - "comingSoon": "Скоро", - "connected": "Подключено", - "connecting": "Подключение", - "createError": "Не удалось создать канал", - "deleteConfirm": "Удалить канал \"{{name}}\"?", - "deleteError": "Не удалось удалить канал", - "description": "Подключите вашего агента к платформам обмена сообщениями.", - "disconnected": "Отключено", - "discord": { - "botToken": "Токен бота", - "botTokenPlaceholder": "Введите токен вашего бота Discord", - "channelIds": "Разрешённые идентификаторы каналов", - "channelIdsHint": "Формат: channel:id или dm:id. Оставьте пустым, чтобы разрешить все.", - "channelIdsPlaceholder": "канал:123456789, лс:987654321", - "description": "Получайте и отвечайте на сообщения через Discord-бота с использованием WebSocket-шлюза.", - "title": "Дискорд", - "whoamiTip": "💡 Совет: Отправьте боту команду /whoami, чтобы получить ваш ID канала в правильном формате." - }, - "error": "Ошибка", - "feishu": { - "appId": "App ID", - "appIdPlaceholder": "Введите App ID вашего приложения Feishu", - "appSecret": "App Secret", - "appSecretPlaceholder": "Введите App Secret вашего приложения Feishu", - "chatIds": "Разрешённые ID чатов", - "chatIdsHint": "ID чатов через запятую. Оставьте пустым, чтобы разрешить все чаты.", - "chatIdsPlaceholder": "oc_xxxxx, oc_yyyyy", - "connected": "Подключено", - "description": "Получение и ответ на сообщения через бота Feishu/Lark с использованием WebSocket.", - "domain": "Домен", - "domainFeishu": "Feishu (Китай)", - "domainLark": "Lark (Международный)", - "encryptKey": "Ключ шифрования", - "encryptKeyPlaceholder": "Введите ключ шифрования вашего приложения Feishu", - "loginHint": "Учетные данные не настроены. Включите канал, чтобы начать регистрацию по QR-коду, или введите App ID и App Secret вручную.", - "qrExpired": "QR-код истёк. Повторите попытку, переключив канал.", - "qrHint": "Ожидание сканирования QR-кода...", - "qrScanHint": "Откройте Feishu на телефоне и отсканируйте QR-код, чтобы создать бота-приложение.", - "qrTitle": "Регистрация через QR-код Feishu", - "title": "Feishu", - "verificationToken": "Токен верификации", - "verificationTokenPlaceholder": "Введите токен верификации вашего приложения Feishu" - }, - "logs": "Журналы", - "noInstances": "Каналы {{type}} не настроены. Нажмите «+ Добавить», чтобы создать канал.", - "noLogs": "Журналы пока отсутствуют", - "notifyReceiver": "Получать уведомления о задачах", - "notifyReceiverHint": "Отправлять результаты запланированных задач в этот канал.", - "qq": { - "appId": "App ID", - "appIdPlaceholder": "Введите App ID вашего QQ бота", - "chatIds": "Разрешённые ID чатов", - "chatIdsHint": "Формат: c2c:openid, group:groupid, channel:channelid. Оставьте пустым для разрешения всех.", - "chatIdsPlaceholder": "c2c:abc123, group:xyz789", - "clientSecret": "Client Secret", - "clientSecretPlaceholder": "Введите Client Secret вашего QQ бота", - "description": "Получать и отвечать на сообщения через официальный API QQ Bot.", - "title": "QQ", - "whoamiTip": "💡 Совет: Отправьте /whoami боту, чтобы получить ваш ID чата в правильном формате." - }, - "security": { - "inheritFromAgent": "Унаследовать от агента", - "permissionMode": "Режим прав доступа канала", - "permissionModeHint": "Переопределить режим разрешений агента для сообщений из этого канала. «Наследовать» использует настройки агента по умолчанию." - }, - "selectAgent": "Выберите агента для привязки", - "slack": { - "appToken": "Токен уровня приложения", - "appTokenPlaceholder": "xapp-...", - "botToken": "Токен бота", - "botTokenPlaceholder": "xoxb-...", - "channelIds": "Разрешённые идентификаторы каналов", - "channelIdsHint": "ID каналов Slack. Оставьте пустым, чтобы разрешить все.", - "channelIdsPlaceholder": "C01234567, D89012345", - "description": "Получайте и отвечайте на сообщения через бота в Slack с использованием Socket Mode.", - "title": "Слак", - "whoamiTip": "💡 Совет: Отправьте боту команду /whoami, чтобы получить ID канала." - }, - "soulModeRequired": "У этого агента не включён автономный режим. Функции канала не будут работать. Пожалуйста, включите его в настройках агента.", - "tab": "Каналы", - "telegram": { - "botToken": "Токен бота", - "botTokenPlaceholder": "Введите токен вашего Telegram бота", - "chatIds": "Разрешённые ID чатов", - "chatIdsHint": "Через запятую. Оставьте пустым для разрешения всех чатов.", - "chatIdsPlaceholder": "123456789, 987654321", - "description": "Получать и отвечать на сообщения через Telegram бота с использованием long polling.", - "title": "Telegram" - }, - "title": "Каналы", - "updateError": "Не удалось обновить канал", - "wechat": { - "addAccount": "Добавить аккаунт WeChat", - "chatIds": "Разрешённые идентификаторы пользователей", - "chatIdsHint": "Разделённые запятыми. Оставьте пустым, чтобы разрешить всех пользователей.", - "chatIdsPlaceholder": "wxid_abc123, wxid_def456", - "connected": "Подключено", - "description": "Получайте и отвечайте на сообщения через WeChat с помощью iLink Bot API.", - "disconnected": "Отключено", - "loginHint": "Первый вход требует сканирования QR-кода. Проверьте журналы приложения для получения URL-адреса входа.", - "qrHint": "Откройте WeChat на телефоне, отсканируйте QR-код для входа.", - "qrTitle": "Вход по QR-коду WeChat", - "title": "WeChat", - "whoamiTip": "Совет: Отправьте /whoami в WeChat, чтобы получить ID пользователя." - } - }, - "heartbeat": { - "enabled": "Включить Heartbeat", - "enabledHelper": "При включении, если heartbeat.md существует в рабочей области, планировщик читает его содержимое и отправляет в основную сессию с настроенным интервалом.", - "interval": "Интервал (минуты)", - "intervalHelper": "Как часто выполняется heartbeat, в минутах." - }, - "sandbox": { - "description": "Ограничивает доступ агента к файловой системе и сети только директорией рабочей области с использованием песочницы на уровне ОС.", - "helper": "Ограничить доступ к файловой системе и командам путём рабочей области.", - "label": "Режим песочницы" - }, - "scheduler": { - "cron": { - "helper": "Стандартное cron-выражение (например, '*/30 * * * *' для каждых 30 минут).", - "label": "Cron-выражение", - "placeholder": "*/30 * * * *" - }, - "description": "Планировщик автоматически запускает агента по расписанию.", - "enabled": "Включить планировщик", - "enabledHelper": "При включении агент будет автоматически запускаться согласно настроенному расписанию.", - "errors": { - "invalidCron": "Неверное cron-выражение", - "invalidDelay": "Задержка должна быть положительным числом", - "invalidInterval": "Интервал должен быть положительным числом" - }, - "interval": { - "helper": "Как часто агент должен запускаться, в секундах.", - "label": "Интервал (секунды)" - }, - "lastRun": "Последний запуск", - "never": "Никогда", - "nextRun": "Следующий запуск", - "oneTime": { - "helper": "Однократная задержка в секундах перед запуском агента.", - "label": "Задержка (секунды)" - }, - "status": { - "running": "Выполняется", - "stopped": "Остановлен", - "tickInProgress": "Tick выполняется" - }, - "tab": "Планировщик", - "title": "Настройка планировщика", - "type": { - "cron": "Cron-выражение", - "interval": "Фиксированный интервал", - "label": "Тип расписания", - "one-time": "Однократная задержка" - } + "channels": { + "add": "Добавить", + "bindAgent": "Связать агента", + "chatIdsAutoTrackHint": "Если оставить пустым, система автоматически отследит: сначала нужно отправить сообщение боту на платформе, затем система запишет Chat ID для будущих уведомлений.", + "comingSoon": "Скоро", + "connected": "Подключено", + "connecting": "Подключение", + "createError": "Не удалось создать канал", + "deleteConfirm": "Удалить канал \"{{name}}\"?", + "deleteError": "Не удалось удалить канал", + "description": "Подключите вашего агента к платформам обмена сообщениями.", + "disconnected": "Отключено", + "discord": { + "botToken": "Токен бота", + "botTokenPlaceholder": "Введите токен вашего бота Discord", + "channelIds": "Разрешённые идентификаторы каналов", + "channelIdsHint": "Формат: channel:id или dm:id. Оставьте пустым, чтобы разрешить все.", + "channelIdsPlaceholder": "канал:123456789, лс:987654321", + "description": "Получайте и отвечайте на сообщения через Discord-бота с использованием WebSocket-шлюза.", + "title": "Дискорд", + "whoamiTip": "💡 Совет: Отправьте боту команду /whoami, чтобы получить ваш ID канала в правильном формате." }, - "soul": { - "description": "Душа определяет личность и базовое поведение агента. Она читается из soul.md в корне рабочей области.", - "empty": "Файл soul.md не найден в корне рабочей области. Создайте его, чтобы определить личность агента.", - "enabled": "Включить душу", - "enabledHelper": "При включении агент читает soul.md из корня рабочей области и добавляет его в начало системного промпта.", - "fileLabel": "soul.md", - "preview": "Предпросмотр души", - "tab": "Душа", - "title": "Настройка души" - }, - "tasks": { - "add": "Добавить задачу", - "cancel": "Отмена", - "channels": { - "label": "Отправить в каналы", - "noActiveChatIds": "Выбранные каналы не имеют доступных получателей (Chat ID). Результаты задачи могут быть не доставлены. Пожалуйста, сначала отправьте сообщение боту на платформе.", - "placeholder": "Выберите каналы для получения результатов" - }, - "cronPlaceholder": "напр. 0 9 * * * (каждый день в 9 утра)", - "delete": { - "confirm": "Вы уверены, что хотите удалить эту задачу?", - "label": "Удалить" - }, - "edit": "Редактировать", - "empty": "Нет запланированных задач. Добавьте одну, чтобы начать.", - "error": { - "createFailed": "Не удалось создать задачу", - "deleteFailed": "Не удалось удалить задачу", - "loadFailed": "Не удалось загрузить задачи", - "runFailed": "Не удалось выполнить задачу", - "updateFailed": "Не удалось обновить задачу" - }, - "frequency": { - "everyPrefix": "Каждые", - "everySuffix": "минут", - "label": "Частота выполнения" - }, - "intervalPlaceholder": "Минимум 1", - "intervalUnit": "минуты", - "lastRun": "Последний запуск", - "logs": { - "cancelled": "Отменено", - "completed": "Завершено", - "duration": "Длительность", - "empty": "История запусков пуста.", - "failed": "Не удалось", - "justNow": "только что", - "label": "История запусков", - "loadError": "Не удалось загрузить историю запусков", - "result": "Результат", - "runAt": "Запущено в", - "running": "Бежит...", - "search": "Искать журналы...", - "status": "Статус", - "viewSession": "Просмотр сеанса" - }, - "name": { - "label": "Название", - "placeholder": "напр. Ежедневный обзор кода" - }, - "nextRun": "Следующий запуск", - "oncePlaceholder": "Выберите дату и время", - "pause": "Пауза", - "prompt": { - "expand": "Развернуть редактор", - "label": "Промпт", - "placeholder": "Что должен делать агент при выполнении этой задачи?" - }, - "resume": "Возобновить", - "run": "Запустить", - "runTriggered": "Задача запущена", - "save": "Сохранить", - "scheduleType": { - "cron": "Cron", - "interval": "Интервал", - "once": "Однократно" - }, - "status": { - "active": "Активна", - "completed": "Завершена", - "paused": "Приостановлена" - }, - "tab": "Задачи", - "time": { - "hoursAgo": "{{count}} ч назад", - "minutesAgo": "{{count}} мин. назад" - }, - "timeout": { - "label": "Максимальное время выполнения", - "placeholder": "Без ограничений" - }, - "title": "Запланированные задачи" + "error": "Ошибка", + "feishu": { + "appId": "App ID", + "appIdPlaceholder": "Введите App ID вашего приложения Feishu", + "appSecret": "App Secret", + "appSecretPlaceholder": "Введите App Secret вашего приложения Feishu", + "chatIds": "Разрешённые ID чатов", + "chatIdsHint": "ID чатов через запятую. Оставьте пустым, чтобы разрешить все чаты.", + "chatIdsPlaceholder": "oc_xxxxx, oc_yyyyy", + "connected": "Подключено", + "description": "Получение и ответ на сообщения через бота Feishu/Lark с использованием WebSocket.", + "domain": "Домен", + "domainFeishu": "Feishu (Китай)", + "domainLark": "Lark (Международный)", + "encryptKey": "Ключ шифрования", + "encryptKeyPlaceholder": "Введите ключ шифрования вашего приложения Feishu", + "loginHint": "Учетные данные не настроены. Включите канал, чтобы начать регистрацию по QR-коду, или введите App ID и App Secret вручную.", + "qrExpired": "QR-код истёк. Повторите попытку, переключив канал.", + "qrHint": "Ожидание сканирования QR-кода...", + "qrScanHint": "Откройте Feishu на телефоне и отсканируйте QR-код, чтобы создать бота-приложение.", + "qrTitle": "Регистрация через QR-код Feishu", + "title": "Feishu", + "verificationToken": "Токен верификации", + "verificationTokenPlaceholder": "Введите токен верификации вашего приложения Feishu" }, - "warning": { - "bypassPermissions": "Агенты CherryClaw по умолчанию работают в полностью автоматическом режиме. Все инструменты выполняются без запроса одобрения." + "logs": "Журналы", + "noInstances": "Каналы {{type}} не настроены. Нажмите «+ Добавить», чтобы создать канал.", + "noLogs": "Журналы пока отсутствуют", + "notifyReceiver": "Получать уведомления о задачах", + "notifyReceiverHint": "Отправлять результаты запланированных задач в этот канал.", + "qq": { + "appId": "App ID", + "appIdPlaceholder": "Введите App ID вашего QQ бота", + "chatIds": "Разрешённые ID чатов", + "chatIdsHint": "Формат: c2c:openid, group:groupid, channel:channelid. Оставьте пустым для разрешения всех.", + "chatIdsPlaceholder": "c2c:abc123, group:xyz789", + "clientSecret": "Client Secret", + "clientSecretPlaceholder": "Введите Client Secret вашего QQ бота", + "description": "Получать и отвечать на сообщения через официальный API QQ Bot.", + "title": "QQ", + "whoamiTip": "💡 Совет: Отправьте /whoami боту, чтобы получить ваш ID чата в правильном формате." + }, + "security": { + "inheritFromAgent": "Унаследовать от агента", + "permissionMode": "Режим прав доступа канала", + "permissionModeHint": "Переопределить режим разрешений агента для сообщений из этого канала. «Наследовать» использует настройки агента по умолчанию." + }, + "selectAgent": "Выберите агента для привязки", + "slack": { + "appToken": "Токен уровня приложения", + "appTokenPlaceholder": "xapp-...", + "botToken": "Токен бота", + "botTokenPlaceholder": "xoxb-...", + "channelIds": "Разрешённые идентификаторы каналов", + "channelIdsHint": "ID каналов Slack. Оставьте пустым, чтобы разрешить все.", + "channelIdsPlaceholder": "C01234567, D89012345", + "description": "Получайте и отвечайте на сообщения через бота в Slack с использованием Socket Mode.", + "title": "Слак", + "whoamiTip": "💡 Совет: Отправьте боту команду /whoami, чтобы получить ID канала." + }, + "tab": "Каналы", + "telegram": { + "botToken": "Токен бота", + "botTokenPlaceholder": "Введите токен вашего Telegram бота", + "chatIds": "Разрешённые ID чатов", + "chatIdsHint": "Через запятую. Оставьте пустым для разрешения всех чатов.", + "chatIdsPlaceholder": "123456789, 987654321", + "description": "Получать и отвечать на сообщения через Telegram бота с использованием long polling.", + "title": "Telegram" + }, + "title": "Каналы", + "updateError": "Не удалось обновить канал", + "wechat": { + "addAccount": "Добавить аккаунт WeChat", + "chatIds": "Разрешённые идентификаторы пользователей", + "chatIdsHint": "Разделённые запятыми. Оставьте пустым, чтобы разрешить всех пользователей.", + "chatIdsPlaceholder": "wxid_abc123, wxid_def456", + "connected": "Подключено", + "description": "Получайте и отвечайте на сообщения через WeChat с помощью iLink Bot API.", + "disconnected": "Отключено", + "loginHint": "Первый вход требует сканирования QR-кода. Проверьте журналы приложения для получения URL-адреса входа.", + "qrHint": "Откройте WeChat на телефоне, отсканируйте QR-код для входа.", + "qrTitle": "Вход по QR-коду WeChat", + "title": "WeChat", + "whoamiTip": "Совет: Отправьте /whoami в WeChat, чтобы получить ID пользователя." } }, "delete": { @@ -339,15 +190,20 @@ "home": { "welcome_title": "О чём мы поговорим сегодня?" }, + "icon": { + "type": "[to be translated]:Agent Icon" + }, "input": { - "placeholder": "Введите ваше сообщение здесь, отправьте с помощью {{key}} — @ выбрать путь, / выбрать команду", - "soul_placeholder": "Помогите мне подключиться к Telegram" + "placeholder": "Введите ваше сообщение здесь, отправьте с помощью {{key}} — @ выбрать путь, / выбрать команду" }, "list": { "error": { "failed": "Не удалось получить список агентов." } }, + "manage": { + "title": "[to be translated]:Manage Agents" + }, "pin": { "title": "Пин-агент" }, @@ -712,12 +568,6 @@ "tab": "Навыки", "title": "Установленные навыки" }, - "soulMode": { - "description": "При включении агент использует пользовательский системный промпт из файлов soul рабочей области, внедряет инструменты автономного управления задачами и отключает интерактивные инструменты, не подходящие для автономной работы.", - "disabledBadge": "Отключено автономным режимом", - "disabledTooltip": "Этот инструмент автоматически отключается при активном автономном режиме", - "title": "Автономный режим" - }, "tooling": { "mcp": { "description": "Подключите серверы MCP, чтобы разблокировать дополнительные инструменты, которые вы можете одобрить выше.", @@ -784,6 +634,94 @@ } }, "sidebar_title": "Агенты", + "skill": { + "manage": { + "title": "[to be translated]:Manage skills" + } + }, + "tasks": { + "add": "Добавить задачу", + "cancel": "Отмена", + "channels": { + "label": "Отправить в каналы", + "noActiveChatIds": "Выбранные каналы не имеют доступных получателей (Chat ID). Результаты задачи могут быть не доставлены. Пожалуйста, сначала отправьте сообщение боту на платформе.", + "placeholder": "Выберите каналы для получения результатов" + }, + "cronPlaceholder": "напр. 0 9 * * * (каждый день в 9 утра)", + "delete": { + "confirm": "Вы уверены, что хотите удалить эту задачу?", + "label": "Удалить" + }, + "edit": "Редактировать", + "empty": "Нет запланированных задач. Добавьте одну, чтобы начать.", + "error": { + "createFailed": "Не удалось создать задачу", + "deleteFailed": "Не удалось удалить задачу", + "loadFailed": "Не удалось загрузить задачи", + "runFailed": "Не удалось выполнить задачу", + "updateFailed": "Не удалось обновить задачу" + }, + "frequency": { + "everyPrefix": "Каждые", + "everySuffix": "минут", + "label": "Частота выполнения" + }, + "intervalPlaceholder": "Минимум 1", + "intervalUnit": "минуты", + "lastRun": "Последний запуск", + "logs": { + "cancelled": "Отменено", + "completed": "Завершено", + "duration": "Длительность", + "empty": "История запусков пуста.", + "failed": "Не удалось", + "justNow": "только что", + "label": "История запусков", + "loadError": "Не удалось загрузить историю запусков", + "result": "Результат", + "runAt": "Запущено в", + "running": "Бежит...", + "search": "Искать журналы...", + "status": "Статус", + "viewSession": "Просмотр сеанса" + }, + "name": { + "label": "Название", + "placeholder": "напр. Ежедневный обзор кода" + }, + "nextRun": "Следующий запуск", + "oncePlaceholder": "Выберите дату и время", + "pause": "Пауза", + "prompt": { + "expand": "Развернуть редактор", + "label": "Промпт", + "placeholder": "Что должен делать агент при выполнении этой задачи?" + }, + "resume": "Возобновить", + "run": "Запустить", + "runTriggered": "Задача запущена", + "save": "Сохранить", + "scheduleType": { + "cron": "Cron", + "interval": "Интервал", + "once": "Однократно" + }, + "status": { + "active": "Активна", + "completed": "Завершена", + "paused": "Приостановлена" + }, + "tab": "Задачи", + "time": { + "hoursAgo": "{{count}} ч назад", + "minutesAgo": "{{count}} мин. назад" + }, + "timeout": { + "label": "Максимальное время выполнения", + "placeholder": "Без ограничений" + }, + "title": "Запланированные задачи" + }, "todo": { "mock": { "actions": { @@ -935,6 +873,14 @@ "description": "Выполняет shell-команды в вашей среде", "label": "Баш" }, + "CherryConfig": { + "description": "[to be translated]:Inspects and manages this agent configuration and channels", + "label": "[to be translated]:Agent Config" + }, + "CherryCron": { + "description": "Управляет встроенным планировщиком", + "label": "Планировщик" + }, "CherryKbManage": { "description": "Добавляет, удаляет или обновляет документы в ваших базах знаний", "label": "Управление знаниями" @@ -943,6 +889,10 @@ "description": "Ищет в ваших базах знаний", "label": "Поиск знаний" }, + "CherryNotify": { + "description": "[to be translated]:Sends a notification through a connected channel", + "label": "[to be translated]:Notify" + }, "CherryWebFetch": { "description": "Получает и читает веб-страницу", "label": "Веб-запрос" @@ -951,10 +901,6 @@ "description": "Выполняет поиск в Интернете через настроенного вами поставщика", "label": "Веб-поиск" }, - "ClawCron": { - "description": "Управляет встроенным планировщиком", - "label": "Планировщик" - }, "Edit": { "description": "Вносит целенаправленные правки в конкретные файлы", "label": "Редактировать" @@ -1087,6 +1033,7 @@ "clear": { "content": "Очистка топика удалит все топики и файлы в ассистенте. Вы уверены, что хотите продолжить?", "menu_title": "Удалить все разговоры с ассистентом", + "success_title": "[to be translated]:Cleared {{count}} topics", "title": "Очистить топики" }, "copy": { @@ -2986,11 +2933,10 @@ "placeholder": "Дайте агенту имя" }, "permission_mode": { - "help": "Переключение на bypassPermissions эквивалентно включению режима Soul.", "label": "Режим разрешений", "option": { "acceptEdits": "Принять правки", - "bypassPermissions": "Обход разрешений (Душа)", + "bypassPermissions": "Обход разрешений", "default": "По умолчанию", "plan": "Режим плана" } @@ -3002,10 +2948,6 @@ "small_model": { "hint": "Лёгкие проверки и форматирование", "label": "Маленькая модель (необязательно)" - }, - "soul_enabled": { - "help": "При включении автоматически предоставляет bypassPermissions", - "label": "Режим души" } }, "model_config": "Конфигурация модели", @@ -5331,6 +5273,13 @@ "auto_switch_to_topics": "Автоматически переключаться на топик", "title": "Расширенные настройки" }, + "agent": { + "position": { + "label": "[to be translated]:Session position", + "left": "[to be translated]:Left", + "right": "[to be translated]:Right" + } + }, "appearance": { "title": "Внешний вид" }, @@ -7596,6 +7545,7 @@ }, "title": { "apps": "Приложения", + "chat": "[to be translated]:Chat", "code": "Code", "files": "Файлы", "home": "Главная", diff --git a/src/renderer/i18n/translate/vi-vn.json b/src/renderer/i18n/translate/vi-vn.json index aecfa4e1bdb..f9e5bfeac82 100644 --- a/src/renderer/i18n/translate/vi-vn.json +++ b/src/renderer/i18n/translate/vi-vn.json @@ -31,262 +31,113 @@ "submit": "Gửi", "title": "Câu hỏi từ Đại lý" }, - "cherryClaw": { - "channels": { - "add": "Thêm", - "bindAgent": "Ràng buộc Đại lý", - "chatIdsAutoTrackHint": "Khi để trống, hệ thống sẽ tự động theo dõi: bạn cần gửi tin nhắn đến Bot trên nền tảng trước, sau đó hệ thống sẽ ghi lại Chat ID để gửi thông báo trong tương lai.", - "comingSoon": "Sắp ra mắt", - "connected": "Đã kết nối", - "connecting": "Kết nối", - "createError": "Không tạo được kênh", - "deleteConfirm": "Xóa kênh \"{{name}}\"?", - "deleteError": "Không xóa được kênh", - "description": "Kết nối tác nhân của bạn với các nền tảng nhắn tin.", - "disconnected": "Ngắt kết nối", - "discord": { - "botToken": "Mã thông báo Bot", - "botTokenPlaceholder": "Nhập mã thông báo bot Discord của bạn", - "channelIds": "ID Kênh Được Phép", - "channelIdsHint": "Định dạng: channel:id hoặc dm:id. Để trống để cho phép tất cả.", - "channelIdsPlaceholder": "kênh:123456789, dm:987654321", - "description": "Nhận và phản hồi tin nhắn thông qua bot Discord bằng cổng WebSocket.", - "title": "Discord", - "whoamiTip": "💡 Mẹo: Gửi /whoami cho bot để lấy ID kênh của bạn ở định dạng chính xác." - }, - "error": "Lỗi", - "feishu": { - "appId": "ID ứng dụng", - "appIdPlaceholder": "Nhập ID ứng dụng Feishu của bạn", - "appSecret": "Bí mật ứng dụng", - "appSecretPlaceholder": "Nhập bí mật ứng dụng Feishu của bạn", - "chatIds": "ID Trò chuyện Được Phép", - "chatIdsHint": "Các ID trò chuyện được phân tách bằng dấu phẩy. Để trống để cho phép tất cả các trò chuyện.", - "chatIdsPlaceholder": "oc_xxxxx, oc_yyyyy", - "connected": "Đã kết nối", - "description": "Nhận và phản hồi tin nhắn qua bot Feishu/Lark bằng WebSocket.", - "domain": "Tên miền", - "domainFeishu": "Feishu (Trung Quốc)", - "domainLark": "Lark (Quốc tế)", - "encryptKey": "Khóa Mã Hóa", - "encryptKeyPlaceholder": "Nhập Khóa Mã hóa từ ứng dụng Feishu của bạn", - "loginHint": "Chưa cấu hình thông tin xác thực. Kích hoạt kênh để bắt đầu đăng ký mã QR hoặc nhập thủ công App ID và App Secret.", - "qrExpired": "Mã QR đã hết hạn. Vui lòng thử lại bằng cách chuyển đổi kênh.", - "qrHint": "Đang chờ quét mã QR...", - "qrScanHint": "Mở Feishu trên điện thoại của bạn và quét mã QR để tạo một ứng dụng bot.", - "qrTitle": "Đăng ký QR Feishu", - "title": "Feishu", - "verificationToken": "Mã Xác Minh", - "verificationTokenPlaceholder": "Nhập Mã Xác Minh từ ứng dụng Feishu của bạn" - }, - "logs": "Nhật ký", - "noInstances": "Không có kênh {{type}} nào được cấu hình. Nhấp vào \"+ Thêm\" để tạo một kênh.", - "noLogs": "Chưa có nhật ký nào", - "notifyReceiver": "Nhận thông báo nhiệm vụ", - "notifyReceiverHint": "Gửi kết quả nhiệm vụ lập lịch đến kênh này.", - "qq": { - "appId": "ID ứng dụng", - "appIdPlaceholder": "Nhập ID ứng dụng QQ Bot của bạn", - "chatIds": "ID Trò chuyện Được Phép", - "chatIdsHint": "Định dạng: c2c:openid, group:groupid, channel:channelid. Để trống để cho phép tất cả.", - "chatIdsPlaceholder": "c2c:abc123, nhóm:xyz789", - "clientSecret": "Bí mật khách hàng", - "clientSecretPlaceholder": "Nhập client secret của QQ Bot của bạn", - "description": "Nhận và phản hồi tin nhắn qua API chính thức của QQ Bot.", - "title": "QQ", - "whoamiTip": "💡 Mẹo: Gửi /whoami cho bot để lấy ID cuộc trò chuyện của bạn ở định dạng chính xác." - }, - "security": { - "inheritFromAgent": "Kế thừa từ agent", - "permissionMode": "Chế độ Quyền của Kênh", - "permissionModeHint": "Ghi đè chế độ quyền của agent cho tin nhắn từ kênh này. \"Kế thừa\" sử dụng mặc định của agent." - }, - "selectAgent": "Chọn một đại lý để liên kết", - "slack": { - "appToken": "Mã thông báo cấp ứng dụng", - "appTokenPlaceholder": "xapp-...", - "botToken": "Mã thông báo Bot", - "botTokenPlaceholder": "xoxb-...", - "channelIds": "ID Kênh Được Phép", - "channelIdsHint": "ID kênh Slack. Để trống để cho phép tất cả.", - "channelIdsPlaceholder": "C01234567, D89012345", - "description": "Nhận và phản hồi tin nhắn qua bot Slack bằng Socket Mode.", - "title": "Slack", - "whoamiTip": "💡 Mẹo: Gửi /whoami cho bot để lấy ID kênh." - }, - "soulModeRequired": "Tác nhân này chưa được bật Chế độ Tự chủ. Các tính năng kênh sẽ không hoạt động. Vui lòng bật trong cài đặt tác nhân.", - "tab": "Kênh", - "telegram": { - "botToken": "Mã thông báo Bot", - "botTokenPlaceholder": "Nhập mã thông báo bot Telegram của bạn", - "chatIds": "ID Trò chuyện Được Phép", - "chatIdsHint": "Cách nhau bằng dấu phẩy. Để trống để cho phép tất cả các cuộc trò chuyện.", - "chatIdsPlaceholder": "123456789, 987654321", - "description": "Nhận và phản hồi tin nhắn qua bot Telegram bằng cách sử dụng long polling.", - "title": "Telegram" - }, - "title": "Kênh", - "updateError": "Không thể cập nhật kênh", - "wechat": { - "addAccount": "Thêm Tài khoản WeChat", - "chatIds": "ID Người Dùng Được Phép", - "chatIdsHint": "Cách nhau bằng dấu phẩy. Để trống để cho phép tất cả người dùng.", - "chatIdsPlaceholder": "wxid_abc123, wxid_def456", - "connected": "Đã đăng nhập", - "description": "Nhận và phản hồi tin nhắn qua WeChat bằng API iLink Bot.", - "disconnected": "Chưa đăng nhập", - "loginHint": "Lần đăng nhập đầu tiên yêu cầu quét mã QR. Một mã QR sẽ tự động xuất hiện khi kênh kết nối.", - "qrHint": "Mở WeChat trên điện thoại của bạn, quét mã QR để đăng nhập.", - "qrTitle": "Đăng nhập bằng mã QR WeChat", - "title": "WeChat", - "whoamiTip": "Mẹo: Gửi /whoami trong WeChat để lấy ID của người dùng." - } - }, - "heartbeat": { - "enabled": "Bật nhịp tim", - "enabledHelper": "Khi được bật và heartbeat.md tồn tại trong không gian làm việc, trình lập lịch đọc nội dung của nó và gửi đến phiên chính theo khoảng thời gian đã cấu hình.", - "interval": "Khoảng thời gian (phút)", - "intervalHelper": "Tần suất nhịp tim chạy, tính bằng phút." - }, - "sandbox": { - "description": "Giới hạn quyền truy cập hệ thống tập tin và mạng của tác nhân trong thư mục không gian làm việc bằng cách sử dụng cơ chế cách ly ở cấp độ hệ điều hành.", - "helper": "Giới hạn quyền truy cập hệ thống tập tin và lệnh chỉ trong đường dẫn không gian làm việc.", - "label": "Chế độ Sandbox" - }, - "scheduler": { - "cron": { - "helper": "Biểu thức cron chuẩn (ví dụ, '*/30 * * * *' cho mỗi 30 phút).", - "label": "Biểu thức Cron", - "placeholder": "*/30 * * * *" - }, - "description": "Bộ lập lịch kích hoạt tác nhân một cách tự động theo lịch trình.", - "enabled": "Bật Trình lập lịch", - "enabledHelper": "Khi được bật, tác nhân sẽ tự động chạy theo lịch trình đã cấu hình.", - "errors": { - "invalidCron": "Biểu thức cron không hợp lệ", - "invalidDelay": "Độ trễ phải là một số dương", - "invalidInterval": "Khoảng thời gian phải là một số dương" - }, - "interval": { - "helper": "Thường xuyên agent chạy bao lâu một lần, tính bằng giây.", - "label": "Khoảng thời gian (giây)" - }, - "lastRun": "Lần Chạy Cuối", - "never": "Không bao giờ", - "nextRun": "Lần chạy tiếp theo", - "oneTime": { - "helper": "Độ trễ một lần tính bằng giây trước khi tác nhân chạy.", - "label": "Độ trễ (giây)" - }, - "status": { - "running": "Chạy", - "stopped": "Dừng lại", - "tickInProgress": "Đang chạy" - }, - "tab": "Trình lập lịch", - "title": "Cấu hình Trình lập lịch", - "type": { - "cron": "Biểu thức Cron", - "interval": "Khoảng thời gian cố định", - "label": "Loại lịch trình", - "one-time": "Trì hoãn một lần" - } + "channels": { + "add": "Thêm", + "bindAgent": "Ràng buộc Đại lý", + "chatIdsAutoTrackHint": "Khi để trống, hệ thống sẽ tự động theo dõi: bạn cần gửi tin nhắn đến Bot trên nền tảng trước, sau đó hệ thống sẽ ghi lại Chat ID để gửi thông báo trong tương lai.", + "comingSoon": "Sắp ra mắt", + "connected": "Đã kết nối", + "connecting": "Kết nối", + "createError": "Không tạo được kênh", + "deleteConfirm": "Xóa kênh \"{{name}}\"?", + "deleteError": "Không xóa được kênh", + "description": "Kết nối tác nhân của bạn với các nền tảng nhắn tin.", + "disconnected": "Ngắt kết nối", + "discord": { + "botToken": "Mã thông báo Bot", + "botTokenPlaceholder": "Nhập mã thông báo bot Discord của bạn", + "channelIds": "ID Kênh Được Phép", + "channelIdsHint": "Định dạng: channel:id hoặc dm:id. Để trống để cho phép tất cả.", + "channelIdsPlaceholder": "kênh:123456789, dm:987654321", + "description": "Nhận và phản hồi tin nhắn thông qua bot Discord bằng cổng WebSocket.", + "title": "Discord", + "whoamiTip": "💡 Mẹo: Gửi /whoami cho bot để lấy ID kênh của bạn ở định dạng chính xác." }, - "soul": { - "description": "Tâm hồn xác định tính cách và hành vi cơ bản của tác nhân. Nó được đọc từ soul.md trong thư mục gốc của workspace.", - "empty": "Không tìm thấy soul.md trong thư mục gốc của workspace. Hãy tạo một file để định nghĩa tính cách của agent.", - "enabled": "Kích hoạt Soul", - "enabledHelper": "Khi được bật, tác nhân sẽ đọc soul.md từ thư mục gốc của không gian làm việc và đặt nó vào đầu lời nhắc hệ thống.", - "fileLabel": "tâm hồn.md", - "preview": "Xem trước Linh hồn", - "tab": "Tâm hồn", - "title": "Cấu hình Linh hồn" - }, - "tasks": { - "add": "Thêm Nhiệm vụ", - "cancel": "Hủy", - "channels": { - "label": "Gửi đến các kênh", - "noActiveChatIds": "Các kênh được chọn không có người nhận khả dụng (Chat ID). Kết quả tác vụ có thể không được gửi đến. Vui lòng gửi tin nhắn đến Bot trên nền tảng trước.", - "placeholder": "Chọn các kênh để nhận kết quả" - }, - "cronPlaceholder": "ví dụ: 0 9 * * * (mỗi ngày lúc 9 giờ sáng)", - "delete": { - "confirm": "Bạn có chắc chắn muốn xóa nhiệm vụ này không?", - "label": "Xóa" - }, - "edit": "Chỉnh sửa", - "empty": "Không có tác vụ nào được lên lịch. Thêm một tác vụ để bắt đầu.", - "error": { - "createFailed": "Không thể tạo tác vụ", - "deleteFailed": "Không xóa được tác vụ", - "loadFailed": "Không tải được nhiệm vụ", - "runFailed": "Không thể chạy tác vụ", - "updateFailed": "Không thể cập nhật tác vụ" - }, - "frequency": { - "everyPrefix": "Mỗi", - "everySuffix": "phút", - "label": "Tần suất thực thi" - }, - "intervalPlaceholder": "Tối thiểu 1", - "intervalUnit": "phút", - "lastRun": "Lần chạy cuối", - "logs": { - "cancelled": "Đã hủy", - "completed": "Hoàn thành", - "duration": "Thời lượng", - "empty": "Chưa có lịch sử chạy nào.", - "failed": "Thất bại", - "justNow": "vừa mới đây", - "label": "Lịch sử chạy", - "loadError": "Không tải được lịch sử chạy", - "result": "Kết quả", - "runAt": "Chạy tại", - "running": "Đang chạy...", - "search": "Tìm nhật ký...", - "status": "Trạng thái", - "viewSession": "Xem phiên" - }, - "name": { - "label": "Name", - "placeholder": "ví dụ: Xem xét mã hàng ngày" - }, - "nextRun": "Lần chạy tiếp theo", - "oncePlaceholder": "Chọn ngày và giờ", - "pause": "Tạm dừng", - "prompt": { - "expand": "Mở rộng trình chỉnh sửa", - "label": "Lời nhắc", - "placeholder": "Đại lý nên làm gì khi nhiệm vụ này chạy?" - }, - "resume": "Sơ yếu lý lịch", - "run": "Chạy", - "runTriggered": "Nhiệm vụ được kích hoạt", - "save": "Lưu", - "scheduleType": { - "cron": "Cron", - "interval": "Interval", - "once": "Một khi" - }, - "status": { - "active": "Hoạt động", - "completed": "Hoàn thành", - "paused": "Tạm dừng" - }, - "tab": "Nhiệm vụ", - "time": { - "hoursAgo": "{{count}}h ago", - "minutesAgo": "{{count}} m pli frue" - }, - "timeout": { - "label": "Thời gian thực thi tối đa", - "placeholder": "Keine Grenzen" - }, - "title": "Các Tác Vụ Đã Lên Lịch" + "error": "Lỗi", + "feishu": { + "appId": "ID ứng dụng", + "appIdPlaceholder": "Nhập ID ứng dụng Feishu của bạn", + "appSecret": "Bí mật ứng dụng", + "appSecretPlaceholder": "Nhập bí mật ứng dụng Feishu của bạn", + "chatIds": "ID Trò chuyện Được Phép", + "chatIdsHint": "Các ID trò chuyện được phân tách bằng dấu phẩy. Để trống để cho phép tất cả các trò chuyện.", + "chatIdsPlaceholder": "oc_xxxxx, oc_yyyyy", + "connected": "Đã kết nối", + "description": "Nhận và phản hồi tin nhắn qua bot Feishu/Lark bằng WebSocket.", + "domain": "Tên miền", + "domainFeishu": "Feishu (Trung Quốc)", + "domainLark": "Lark (Quốc tế)", + "encryptKey": "Khóa Mã Hóa", + "encryptKeyPlaceholder": "Nhập Khóa Mã hóa từ ứng dụng Feishu của bạn", + "loginHint": "Chưa cấu hình thông tin xác thực. Kích hoạt kênh để bắt đầu đăng ký mã QR hoặc nhập thủ công App ID và App Secret.", + "qrExpired": "Mã QR đã hết hạn. Vui lòng thử lại bằng cách chuyển đổi kênh.", + "qrHint": "Đang chờ quét mã QR...", + "qrScanHint": "Mở Feishu trên điện thoại của bạn và quét mã QR để tạo một ứng dụng bot.", + "qrTitle": "Đăng ký QR Feishu", + "title": "Feishu", + "verificationToken": "Mã Xác Minh", + "verificationTokenPlaceholder": "Nhập Mã Xác Minh từ ứng dụng Feishu của bạn" }, - "warning": { - "bypassPermissions": "Các tác nhân CherryClaw hoạt động ở chế độ Tự động hoàn toàn theo mặc định. Mọi công cụ đều thực thi mà không yêu cầu phê duyệt." + "logs": "Nhật ký", + "noInstances": "Không có kênh {{type}} nào được cấu hình. Nhấp vào \"+ Thêm\" để tạo một kênh.", + "noLogs": "Chưa có nhật ký nào", + "notifyReceiver": "Nhận thông báo nhiệm vụ", + "notifyReceiverHint": "Gửi kết quả nhiệm vụ lập lịch đến kênh này.", + "qq": { + "appId": "ID ứng dụng", + "appIdPlaceholder": "Nhập ID ứng dụng QQ Bot của bạn", + "chatIds": "ID Trò chuyện Được Phép", + "chatIdsHint": "Định dạng: c2c:openid, group:groupid, channel:channelid. Để trống để cho phép tất cả.", + "chatIdsPlaceholder": "c2c:abc123, nhóm:xyz789", + "clientSecret": "Bí mật khách hàng", + "clientSecretPlaceholder": "Nhập client secret của QQ Bot của bạn", + "description": "Nhận và phản hồi tin nhắn qua API chính thức của QQ Bot.", + "title": "QQ", + "whoamiTip": "💡 Mẹo: Gửi /whoami cho bot để lấy ID cuộc trò chuyện của bạn ở định dạng chính xác." + }, + "security": { + "inheritFromAgent": "Kế thừa từ agent", + "permissionMode": "Chế độ Quyền của Kênh", + "permissionModeHint": "Ghi đè chế độ quyền của agent cho tin nhắn từ kênh này. \"Kế thừa\" sử dụng mặc định của agent." + }, + "selectAgent": "Chọn một đại lý để liên kết", + "slack": { + "appToken": "Mã thông báo cấp ứng dụng", + "appTokenPlaceholder": "xapp-...", + "botToken": "Mã thông báo Bot", + "botTokenPlaceholder": "xoxb-...", + "channelIds": "ID Kênh Được Phép", + "channelIdsHint": "ID kênh Slack. Để trống để cho phép tất cả.", + "channelIdsPlaceholder": "C01234567, D89012345", + "description": "Nhận và phản hồi tin nhắn qua bot Slack bằng Socket Mode.", + "title": "Slack", + "whoamiTip": "💡 Mẹo: Gửi /whoami cho bot để lấy ID kênh." + }, + "tab": "Kênh", + "telegram": { + "botToken": "Mã thông báo Bot", + "botTokenPlaceholder": "Nhập mã thông báo bot Telegram của bạn", + "chatIds": "ID Trò chuyện Được Phép", + "chatIdsHint": "Cách nhau bằng dấu phẩy. Để trống để cho phép tất cả các cuộc trò chuyện.", + "chatIdsPlaceholder": "123456789, 987654321", + "description": "Nhận và phản hồi tin nhắn qua bot Telegram bằng cách sử dụng long polling.", + "title": "Telegram" + }, + "title": "Kênh", + "updateError": "Không thể cập nhật kênh", + "wechat": { + "addAccount": "Thêm Tài khoản WeChat", + "chatIds": "ID Người Dùng Được Phép", + "chatIdsHint": "Cách nhau bằng dấu phẩy. Để trống để cho phép tất cả người dùng.", + "chatIdsPlaceholder": "wxid_abc123, wxid_def456", + "connected": "Đã đăng nhập", + "description": "Nhận và phản hồi tin nhắn qua WeChat bằng API iLink Bot.", + "disconnected": "Chưa đăng nhập", + "loginHint": "Lần đăng nhập đầu tiên yêu cầu quét mã QR. Một mã QR sẽ tự động xuất hiện khi kênh kết nối.", + "qrHint": "Mở WeChat trên điện thoại của bạn, quét mã QR để đăng nhập.", + "qrTitle": "Đăng nhập bằng mã QR WeChat", + "title": "WeChat", + "whoamiTip": "Mẹo: Gửi /whoami trong WeChat để lấy ID của người dùng." } }, "delete": { @@ -339,15 +190,20 @@ "home": { "welcome_title": "Hôm nay chúng ta sẽ nói về điều gì?" }, + "icon": { + "type": "[to be translated]:Agent Icon" + }, "input": { - "placeholder": "Nhập tin nhắn của bạn ở đây, gửi bằng {{key}} - @ chọn đường dẫn, / chọn lệnh", - "soul_placeholder": "Giúp tôi kết nối với Telegram" + "placeholder": "Nhập tin nhắn của bạn ở đây, gửi bằng {{key}} - @ chọn đường dẫn, / chọn lệnh" }, "list": { "error": { "failed": "Không thể liệt kê các tác nhân." } }, + "manage": { + "title": "[to be translated]:Manage Agents" + }, "pin": { "title": "Đại lý Ghim" }, @@ -712,12 +568,6 @@ "tab": "Kỹ năng", "title": "Kỹ năng đã cài đặt" }, - "soulMode": { - "description": "Khi được bật, tác nhân sử dụng lời nhắc hệ thống tùy chỉnh từ các tệp soul trong không gian làm việc, chèn các công cụ quản lý tác vụ tự động và vô hiệu hóa các công cụ tương tác không phù hợp với hoạt động tự động.", - "disabledBadge": "Bị vô hiệu hóa bởi Chế độ Tự động", - "disabledTooltip": "Công cụ này sẽ tự động bị vô hiệu hóa khi Chế độ Tự động đang hoạt động", - "title": "Chế độ tự hành" - }, "tooling": { "mcp": { "description": "Kết nối các máy chủ MCP để mở khóa các công cụ bổ sung mà bạn có thể phê duyệt ở trên.", @@ -784,6 +634,94 @@ } }, "sidebar_title": "Đại lý", + "skill": { + "manage": { + "title": "[to be translated]:Manage skills" + } + }, + "tasks": { + "add": "Thêm Nhiệm vụ", + "cancel": "Hủy", + "channels": { + "label": "Gửi đến các kênh", + "noActiveChatIds": "Các kênh được chọn không có người nhận khả dụng (Chat ID). Kết quả tác vụ có thể không được gửi đến. Vui lòng gửi tin nhắn đến Bot trên nền tảng trước.", + "placeholder": "Chọn các kênh để nhận kết quả" + }, + "cronPlaceholder": "ví dụ: 0 9 * * * (mỗi ngày lúc 9 giờ sáng)", + "delete": { + "confirm": "Bạn có chắc chắn muốn xóa nhiệm vụ này không?", + "label": "Xóa" + }, + "edit": "Chỉnh sửa", + "empty": "Không có tác vụ nào được lên lịch. Thêm một tác vụ để bắt đầu.", + "error": { + "createFailed": "Không thể tạo tác vụ", + "deleteFailed": "Không xóa được tác vụ", + "loadFailed": "Không tải được nhiệm vụ", + "runFailed": "Không thể chạy tác vụ", + "updateFailed": "Không thể cập nhật tác vụ" + }, + "frequency": { + "everyPrefix": "Mỗi", + "everySuffix": "phút", + "label": "Tần suất thực thi" + }, + "intervalPlaceholder": "Tối thiểu 1", + "intervalUnit": "phút", + "lastRun": "Lần chạy cuối", + "logs": { + "cancelled": "Đã hủy", + "completed": "Hoàn thành", + "duration": "Thời lượng", + "empty": "Chưa có lịch sử chạy nào.", + "failed": "Thất bại", + "justNow": "vừa mới đây", + "label": "Lịch sử chạy", + "loadError": "Không tải được lịch sử chạy", + "result": "Kết quả", + "runAt": "Chạy tại", + "running": "Đang chạy...", + "search": "Tìm nhật ký...", + "status": "Trạng thái", + "viewSession": "Xem phiên" + }, + "name": { + "label": "Name", + "placeholder": "ví dụ: Xem xét mã hàng ngày" + }, + "nextRun": "Lần chạy tiếp theo", + "oncePlaceholder": "Chọn ngày và giờ", + "pause": "Tạm dừng", + "prompt": { + "expand": "Mở rộng trình chỉnh sửa", + "label": "Lời nhắc", + "placeholder": "Đại lý nên làm gì khi nhiệm vụ này chạy?" + }, + "resume": "Sơ yếu lý lịch", + "run": "Chạy", + "runTriggered": "Nhiệm vụ được kích hoạt", + "save": "Lưu", + "scheduleType": { + "cron": "Cron", + "interval": "Interval", + "once": "Một khi" + }, + "status": { + "active": "Hoạt động", + "completed": "Hoàn thành", + "paused": "Tạm dừng" + }, + "tab": "Nhiệm vụ", + "time": { + "hoursAgo": "{{count}}h ago", + "minutesAgo": "{{count}} m pli frue" + }, + "timeout": { + "label": "Thời gian thực thi tối đa", + "placeholder": "Keine Grenzen" + }, + "title": "Các Tác Vụ Đã Lên Lịch" + }, "todo": { "mock": { "actions": { @@ -935,6 +873,14 @@ "description": "Thực thi các lệnh shell trong môi trường của bạn", "label": "Bash" }, + "CherryConfig": { + "description": "[to be translated]:Inspects and manages this agent configuration and channels", + "label": "[to be translated]:Agent Config" + }, + "CherryCron": { + "description": "Quản lý trình lên lịch trong ứng dụng", + "label": "Trình lập lịch" + }, "CherryKbManage": { "description": "Thêm, xóa hoặc làm mới tài liệu trong cơ sở kiến thức của bạn", "label": "Quản lý kiến thức" @@ -943,6 +889,10 @@ "description": "Tìm kiếm trong cơ sở kiến thức của bạn", "label": "Tìm kiếm kiến thức" }, + "CherryNotify": { + "description": "[to be translated]:Sends a notification through a connected channel", + "label": "[to be translated]:Notify" + }, "CherryWebFetch": { "description": "Tìm nạp và đọc một trang web", "label": "Tìm nạp Web" @@ -951,10 +901,6 @@ "description": "Tìm kiếm trên web thông qua nhà cung cấp đã cấu hình của bạn", "label": "Tìm kiếm web" }, - "ClawCron": { - "description": "Quản lý trình lên lịch trong ứng dụng", - "label": "Trình lập lịch" - }, "Edit": { "description": "Thực hiện chỉnh sửa có mục tiêu trên các tập tin cụ thể", "label": "Chỉnh sửa" @@ -1087,6 +1033,7 @@ "clear": { "content": "Xóa chủ đề sẽ xóa tất cả các chủ đề và tệp trong trợ lý. Bạn có chắc chắn muốn tiếp tục?", "menu_title": "Xóa tất cả các cuộc trò chuyện với trợ lý", + "success_title": "[to be translated]:Cleared {{count}} topics", "title": "Temas claros" }, "copy": { @@ -2986,11 +2933,10 @@ "placeholder": "Đặt tên cho đại lý" }, "permission_mode": { - "help": "Chuyển sang bypassPermissions tương đương với việc bật chế độ Soul", "label": "Chế độ cấp phép", "option": { "acceptEdits": "Chấp nhận chỉnh sửa", - "bypassPermissions": "Bỏ qua quyền (Soul)", + "bypassPermissions": "Bỏ qua quyền", "default": "Mặc định", "plan": "Chế độ lập kế hoạch" } @@ -3002,10 +2948,6 @@ "small_model": { "hint": "Kiểm tra nhẹ và định dạng", "label": "Mô hình nhỏ (tùy chọn)" - }, - "soul_enabled": { - "help": "Khi được bật, tự động cấp quyền bypassPermissions", - "label": "Chế độ linh hồn" } }, "model_config": "Cấu hình mô hình", @@ -5331,6 +5273,13 @@ "auto_switch_to_topics": "Tự động chuyển sang chủ đề", "title": "Cài đặt nâng cao" }, + "agent": { + "position": { + "label": "[to be translated]:Session position", + "left": "[to be translated]:Left", + "right": "[to be translated]:Right" + } + }, "appearance": { "title": "Vẻ ngoài" }, @@ -7229,7 +7178,7 @@ }, "scheduledTasks": { "description": "Quản lý các tác vụ đã lên lịch trên tất cả các tác nhân. Các tác vụ sẽ tự động chạy theo lịch đã cấu hình.", - "noAgents": "Các tác vụ theo lịch trình yêu cầu các tác nhân có chế độ Soul hoặc Quyền Bypass được bật. Cập nhật cài đặt của một tác nhân để bắt đầu.", + "noAgents": "Không tìm thấy tác nhân nào. Hãy tạo một tác nhân trước để thêm các tác vụ theo lịch trình.", "noAgentsTip": "Mẹo: Bạn cũng có thể yêu cầu đại lý của mình tạo các tác vụ theo lịch trình qua trò chuyện.", "noTasks": "Không có tác vụ nào được lên lịch. Nhấp vào \"+ Thêm\" để tạo một tác vụ cho tác nhân.", "selectTask": "Chọn một nhiệm vụ để xem chi tiết", @@ -7596,6 +7545,7 @@ }, "title": { "apps": "Ứng dụng", + "chat": "[to be translated]:Chat", "code": "Mã", "files": "Tập tin", "home": "Trang chủ", diff --git a/src/renderer/pages/agents/components/AgentConversationPickerDialog.tsx b/src/renderer/pages/agents/components/AgentConversationPickerDialog.tsx index e2c2654ddf5..5604ca9eb34 100644 --- a/src/renderer/pages/agents/components/AgentConversationPickerDialog.tsx +++ b/src/renderer/pages/agents/components/AgentConversationPickerDialog.tsx @@ -83,8 +83,7 @@ export function AgentConversationPickerDialog({ description: values.description, configuration: { avatar: values.avatar, - permission_mode: 'bypassPermissions', - soul_enabled: true + permission_mode: 'bypassPermissions' } } }) diff --git a/src/renderer/pages/agents/components/__tests__/AgentConversationPickerDialog.test.tsx b/src/renderer/pages/agents/components/__tests__/AgentConversationPickerDialog.test.tsx index f45ab9a7f32..829d514ebc8 100644 --- a/src/renderer/pages/agents/components/__tests__/AgentConversationPickerDialog.test.tsx +++ b/src/renderer/pages/agents/components/__tests__/AgentConversationPickerDialog.test.tsx @@ -95,7 +95,7 @@ describe('AgentConversationPickerDialog', () => { planModel: 'p::m', smallModel: 'p::m', description: 'desc', - configuration: { avatar: '🤖', permission_mode: 'bypassPermissions', soul_enabled: true } + configuration: { avatar: '🤖', permission_mode: 'bypassPermissions' } } }) ) diff --git a/src/renderer/pages/settings/ChannelsSettings/ChannelDetail.tsx b/src/renderer/pages/settings/ChannelsSettings/ChannelDetail.tsx index bafa56c8549..ff56cd218db 100644 --- a/src/renderer/pages/settings/ChannelsSettings/ChannelDetail.tsx +++ b/src/renderer/pages/settings/ChannelsSettings/ChannelDetail.tsx @@ -1,5 +1,4 @@ import { - Alert, Badge, Button, ConfirmDialog, @@ -26,10 +25,8 @@ import { SettingDivider, SettingsContentBody, SettingTitle } from '@renderer/com import { useQuery } from '@renderer/data/hooks/useDataApi' import { useAgents } from '@renderer/hooks/agent/useAgent' import { useChannels } from '@renderer/hooks/agent/useChannels' -import { isSoulModeEnabled } from '@renderer/utils/agent/agentConfiguration' import { getChannelTypeIcon } from '@renderer/utils/agentSession' import { AGENT_WORKSPACE_TYPE } from '@shared/data/api/schemas/agentWorkspaces' -import type { AgentConfiguration } from '@shared/data/types/agent' import { ChevronDown, CircleSlash, FileText, Folder, Pencil, Plus, Trash2 } from 'lucide-react' import type { FC } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -153,15 +150,13 @@ const ChannelLogModal: FC<{ - {`${channelName} — ${t('agent.cherryClaw.channels.logs')}`} + {`${channelName} — ${t('agent.channels.logs')}`} {logs.length > 0 && }
{logs.length === 0 && ( -
- {t('agent.cherryClaw.channels.noLogs')} -
+
{t('agent.channels.noLogs')}
)} {logs.map((entry, i) => (
@@ -190,9 +185,7 @@ type EditModalProps = { onDelete: (id: string) => void } -const ChannelEditModal: FC< - EditModalProps & { agentEntities?: Array<{ id: string; configuration?: AgentConfiguration }> } -> = ({ open, channel, agents, onClose, onSave, onDelete, agentEntities }) => { +const ChannelEditModal: FC = ({ open, channel, agents, onClose, onSave, onDelete }) => { const { t } = useTranslation() const [name, setName] = useState('') const [agentId, setAgentId] = useState(null) @@ -208,9 +201,6 @@ const ChannelEditModal: FC< } }, [channel]) - const selectedAgent = agentEntities?.find((a) => a.id === agentId) - const showSoulModeWarning = agentId && selectedAgent && !isSoulModeEnabled(selectedAgent.configuration) - const handleNameBlur = useCallback(() => { if (channel && name.trim() && name.trim() !== channel.name) { onSave(channel.id, { name: name.trim() }) @@ -276,10 +266,10 @@ const ChannelEditModal: FC< />
- + - {showSoulModeWarning && ( - - )} {/* Workspace is a secondary detail — channel sessions default to "No work directory". */}
{t('agent.session.display.workdir')} @@ -352,7 +334,7 @@ const ChannelInstanceRow: FC<{ statusColor = 'bg-green-500' statusTag = ( - {t('agent.cherryClaw.channels.connected')} + {t('agent.channels.connected')} ) } else if (hasError) { @@ -360,7 +342,7 @@ const ChannelInstanceRow: FC<{ statusTag = ( - {t('agent.cherryClaw.channels.error')} + {t('agent.channels.error')} ) @@ -380,7 +362,7 @@ const ChannelInstanceRow: FC<{ {summary}
- + @@ -402,7 +384,7 @@ const ChannelInstanceRow: FC<{ = ({ channelDef }) => { // SWR-managed remote data const { channels, isLoading, mutate, createChannel, updateChannel, deleteChannel } = useChannels(channelDef.type) const { agents: agentList } = useAgents() - const { agents, agentEntities } = useMemo(() => { - const list = agentList ?? [] - return { - agents: list.map((a) => ({ id: a.id, name: a.name ?? a.id })), - agentEntities: list.map((a) => ({ id: a.id, configuration: a.configuration })) - } - }, [agentList]) + const agents = useMemo(() => (agentList ?? []).map((a) => ({ id: a.id, name: a.name ?? a.id })), [agentList]) const channelList: ChannelData[] = useMemo( () => @@ -554,12 +530,12 @@ const ChannelDetail: FC = ({ channelDef }) => { {channelDef.name}

- {channelDef.available ? t(channelDef.description) : t('agent.cherryClaw.channels.comingSoon')} + {channelDef.available ? t(channelDef.description) : t('agent.channels.comingSoon')}

@@ -568,7 +544,7 @@ const ChannelDetail: FC = ({ channelDef }) => { )} @@ -591,7 +567,6 @@ const ChannelDetail: FC = ({ channelDef }) => { open={!!editingChannel} channel={editingChannel} agents={agents} - agentEntities={agentEntities} onClose={() => setEditingChannelId(null)} onSave={handleSave} onDelete={handleDelete} diff --git a/src/renderer/pages/settings/ChannelsSettings/ChannelForms.tsx b/src/renderer/pages/settings/ChannelsSettings/ChannelForms.tsx index f03fbad4d2d..e21292b79cd 100644 --- a/src/renderer/pages/settings/ChannelsSettings/ChannelForms.tsx +++ b/src/renderer/pages/settings/ChannelsSettings/ChannelForms.tsx @@ -21,7 +21,7 @@ import type { ChannelData } from './channelTypes' // --------------- Permission mode --------------- const PERMISSION_MODE_OPTIONS: Array<{ value: PermissionMode | ''; labelKey: string }> = [ - { value: '', labelKey: 'agent.cherryClaw.channels.security.inheritFromAgent' }, + { value: '', labelKey: 'agent.channels.security.inheritFromAgent' }, { value: 'default', labelKey: 'agent.settings.tooling.permissionMode.default.title' }, { value: 'acceptEdits', labelKey: 'agent.settings.tooling.permissionMode.acceptEdits.title' }, { value: 'bypassPermissions', labelKey: 'agent.settings.tooling.permissionMode.bypassPermissions.title' }, @@ -66,12 +66,12 @@ const ChannelPermissionMode: FC = ({ channel, onConfigChange } const { t } = useTranslation() return (
- + onConfigChange({ config: { ...cfg, domain: value as FeishuDomain } })}> @@ -221,8 +219,8 @@ const FeishuDomainSelector: FC = ({ channel, onConfigChange }) - {t('agent.cherryClaw.channels.feishu.domainFeishu')} - {t('agent.cherryClaw.channels.feishu.domainLark')} + {t('agent.channels.feishu.domainFeishu')} + {t('agent.channels.feishu.domainLark')}
@@ -261,24 +259,20 @@ export const FeishuForm: FC = ({ channel, onConfigChange }) =>
{!hasCredentials && (
- {status === 'pending' && ( - {t('agent.cherryClaw.channels.feishu.qrHint')} - )} + {status === 'pending' && {t('agent.channels.feishu.qrHint')}} {status === 'expired' && ( <> - {t('agent.cherryClaw.channels.feishu.qrExpired')} + {t('agent.channels.feishu.qrExpired')} )} - {status === 'idle' && ( - {t('agent.cherryClaw.channels.feishu.loginHint')} - )} + {status === 'idle' && {t('agent.channels.feishu.loginHint')}}
)} {hasCredentials && (
- {t('agent.cherryClaw.channels.feishu.connected')} + {t('agent.channels.feishu.connected')}
)} = ({ channel, onConfigChange }) => fields={[ { key: 'app_id', - label: t('agent.cherryClaw.channels.feishu.appId'), - placeholder: t('agent.cherryClaw.channels.feishu.appIdPlaceholder') + label: t('agent.channels.feishu.appId'), + placeholder: t('agent.channels.feishu.appIdPlaceholder') }, { key: 'app_secret', - label: t('agent.cherryClaw.channels.feishu.appSecret'), - placeholder: t('agent.cherryClaw.channels.feishu.appSecretPlaceholder'), + label: t('agent.channels.feishu.appSecret'), + placeholder: t('agent.channels.feishu.appSecretPlaceholder'), secret: true }, { key: 'encrypt_key', - label: t('agent.cherryClaw.channels.feishu.encryptKey'), - placeholder: t('agent.cherryClaw.channels.feishu.encryptKeyPlaceholder'), + label: t('agent.channels.feishu.encryptKey'), + placeholder: t('agent.channels.feishu.encryptKeyPlaceholder'), secret: true }, { key: 'verification_token', - label: t('agent.cherryClaw.channels.feishu.verificationToken'), - placeholder: t('agent.cherryClaw.channels.feishu.verificationTokenPlaceholder'), + label: t('agent.channels.feishu.verificationToken'), + placeholder: t('agent.channels.feishu.verificationTokenPlaceholder'), secret: true } ]} extraContent={} chatIds={{ - label: t('agent.cherryClaw.channels.feishu.chatIds'), - placeholder: t('agent.cherryClaw.channels.feishu.chatIdsPlaceholder'), - hint: t('agent.cherryClaw.channels.feishu.chatIdsHint') + label: t('agent.channels.feishu.chatIds'), + placeholder: t('agent.channels.feishu.chatIdsPlaceholder'), + hint: t('agent.channels.feishu.chatIdsHint') }} /> @@ -326,13 +320,11 @@ export const FeishuForm: FC = ({ channel, onConfigChange }) => }}> - {t('agent.cherryClaw.channels.feishu.qrTitle')} + {t('agent.channels.feishu.qrTitle')}
{qrUrl && } - - {t('agent.cherryClaw.channels.feishu.qrScanHint')} - + {t('agent.channels.feishu.qrScanHint')}
@@ -349,17 +341,17 @@ export const DiscordForm: FC = ({ channel, onConfigChange }) = fields={[ { key: 'bot_token', - label: t('agent.cherryClaw.channels.discord.botToken'), - placeholder: t('agent.cherryClaw.channels.discord.botTokenPlaceholder'), + label: t('agent.channels.discord.botToken'), + placeholder: t('agent.channels.discord.botTokenPlaceholder'), secret: true, span: 2 } ]} chatIds={{ - label: t('agent.cherryClaw.channels.discord.channelIds'), - placeholder: t('agent.cherryClaw.channels.discord.channelIdsPlaceholder'), - hint: t('agent.cherryClaw.channels.discord.channelIdsHint'), - extraHint: t('agent.cherryClaw.channels.discord.whoamiTip'), + label: t('agent.channels.discord.channelIds'), + placeholder: t('agent.channels.discord.channelIdsPlaceholder'), + hint: t('agent.channels.discord.channelIdsHint'), + extraHint: t('agent.channels.discord.whoamiTip'), fullWidth: true, configKey: 'allowed_channel_ids' }} @@ -376,21 +368,21 @@ export const QQForm: FC = ({ channel, onConfigChange }) => { fields={[ { key: 'app_id', - label: t('agent.cherryClaw.channels.qq.appId'), - placeholder: t('agent.cherryClaw.channels.qq.appIdPlaceholder') + label: t('agent.channels.qq.appId'), + placeholder: t('agent.channels.qq.appIdPlaceholder') }, { key: 'client_secret', - label: t('agent.cherryClaw.channels.qq.clientSecret'), - placeholder: t('agent.cherryClaw.channels.qq.clientSecretPlaceholder'), + label: t('agent.channels.qq.clientSecret'), + placeholder: t('agent.channels.qq.clientSecretPlaceholder'), secret: true } ]} chatIds={{ - label: t('agent.cherryClaw.channels.qq.chatIds'), - placeholder: t('agent.cherryClaw.channels.qq.chatIdsPlaceholder'), - hint: t('agent.cherryClaw.channels.qq.chatIdsHint'), - extraHint: t('agent.cherryClaw.channels.qq.whoamiTip'), + label: t('agent.channels.qq.chatIds'), + placeholder: t('agent.channels.qq.chatIdsPlaceholder'), + hint: t('agent.channels.qq.chatIdsHint'), + extraHint: t('agent.channels.qq.whoamiTip'), fullWidth: true }} /> @@ -441,17 +433,17 @@ export const WeChatForm: FC void }> = ({ c {status === 'confirmed' && ( <> - {t('agent.cherryClaw.channels.wechat.connected')} + {t('agent.channels.wechat.connected')} )} {status === 'disconnected' && ( <> - {t('agent.cherryClaw.channels.wechat.disconnected')} + {t('agent.channels.wechat.disconnected')} )} {(status === 'idle' || status === 'pending') && ( - {t('agent.cherryClaw.channels.wechat.loginHint')} + {t('agent.channels.wechat.loginHint')} )}
{loginUserId && status === 'confirmed' && ( @@ -472,13 +464,11 @@ export const WeChatForm: FC void }> = ({ c }}> - {t('agent.cherryClaw.channels.wechat.qrTitle')} + {t('agent.channels.wechat.qrTitle')}
{qrUrl && } - - {t('agent.cherryClaw.channels.wechat.qrHint')} - + {t('agent.channels.wechat.qrHint')}
@@ -495,24 +485,24 @@ export const SlackForm: FC = ({ channel, onConfigChange }) => fields={[ { key: 'bot_token', - label: t('agent.cherryClaw.channels.slack.botToken'), - placeholder: t('agent.cherryClaw.channels.slack.botTokenPlaceholder'), + label: t('agent.channels.slack.botToken'), + placeholder: t('agent.channels.slack.botTokenPlaceholder'), secret: true, span: 2 }, { key: 'app_token', - label: t('agent.cherryClaw.channels.slack.appToken'), - placeholder: t('agent.cherryClaw.channels.slack.appTokenPlaceholder'), + label: t('agent.channels.slack.appToken'), + placeholder: t('agent.channels.slack.appTokenPlaceholder'), secret: true, span: 2 } ]} chatIds={{ - label: t('agent.cherryClaw.channels.slack.channelIds'), - placeholder: t('agent.cherryClaw.channels.slack.channelIdsPlaceholder'), - hint: t('agent.cherryClaw.channels.slack.channelIdsHint'), - extraHint: t('agent.cherryClaw.channels.slack.whoamiTip'), + label: t('agent.channels.slack.channelIds'), + placeholder: t('agent.channels.slack.channelIdsPlaceholder'), + hint: t('agent.channels.slack.channelIdsHint'), + extraHint: t('agent.channels.slack.whoamiTip'), fullWidth: true, configKey: 'allowed_channel_ids' }} diff --git a/src/renderer/pages/settings/ChannelsSettings/__tests__/ChannelDetail.test.tsx b/src/renderer/pages/settings/ChannelsSettings/__tests__/ChannelDetail.test.tsx new file mode 100644 index 00000000000..d8f4af38864 --- /dev/null +++ b/src/renderer/pages/settings/ChannelsSettings/__tests__/ChannelDetail.test.tsx @@ -0,0 +1,198 @@ +import { fireEvent, render, screen, waitFor, within } from '@testing-library/react' +import React from 'react' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import ChannelDetail from '../ChannelDetail' +import type { AvailableChannel } from '../channelTypes' + +const channelMocks = vi.hoisted(() => ({ + channels: [] as Array>, + createChannel: vi.fn(), + updateChannel: vi.fn(), + deleteChannel: vi.fn(), + mutate: vi.fn() +})) + +const agentMocks = vi.hoisted(() => ({ + agents: [{ id: 'agent-1', name: 'Agent One' }] +})) + +vi.mock('@logger', () => ({ + loggerService: { + withContext: () => ({ + warn: vi.fn(), + error: vi.fn(), + info: vi.fn() + }) + } +})) + +vi.mock('@renderer/components/CopyButton', () => ({ + default: () => +})) + +vi.mock('@renderer/components/resourceCatalog/selectors', () => ({ + WorkspaceSelector: ({ trigger }: { trigger: React.ReactNode }) => <>{trigger} +})) + +vi.mock('@renderer/components/Scrollbar', () => ({ + default: ({ children, ...props }: React.HTMLAttributes) =>
{children}
+})) + +vi.mock('@renderer/components/SettingsPrimitives', () => ({ + SettingDivider: (props: React.HTMLAttributes) =>
, + SettingsContentBody: ({ children, ...props }: React.HTMLAttributes) => ( +
{children}
+ ), + SettingTitle: ({ children, ...props }: React.HTMLAttributes) =>
{children}
+})) + +vi.mock('@renderer/data/hooks/useDataApi', () => ({ + useQuery: () => ({ data: [] }) +})) + +vi.mock('@renderer/hooks/agent/useAgent', () => ({ + useAgents: () => ({ agents: agentMocks.agents }) +})) + +vi.mock('@renderer/hooks/agent/useChannels', () => ({ + useChannels: () => ({ + channels: channelMocks.channels, + isLoading: false, + mutate: channelMocks.mutate, + createChannel: channelMocks.createChannel, + updateChannel: channelMocks.updateChannel, + deleteChannel: channelMocks.deleteChannel + }) +})) + +vi.mock('@renderer/utils/agentSession', () => ({ + getChannelTypeIcon: () => null +})) + +vi.mock('react-i18next', () => ({ + initReactI18next: { type: '3rdParty', init: vi.fn() }, + useTranslation: () => ({ + i18n: { language: 'en-US' }, + t: (key: string) => key + }) +})) + +vi.mock('@cherrystudio/ui', () => { + const SelectContext = React.createContext<{ onValueChange?: (value: string) => void } | null>(null) + + const passthrough = + (tag: keyof React.JSX.IntrinsicElements) => + ({ children, closeOnOverlayClick, ...props }: { children?: React.ReactNode; closeOnOverlayClick?: boolean }) => { + void closeOnOverlayClick + return React.createElement(tag, props, children) + } + + return { + Badge: passthrough('span'), + Button: ({ children, onClick, ...props }: React.ButtonHTMLAttributes) => ( + + ), + ConfirmDialog: ({ open }: { open?: boolean }) => (open ?
: null), + Dialog: ({ children, open }: { children?: React.ReactNode; open?: boolean }) => + open ?
{children}
: null, + DialogContent: passthrough('div'), + DialogHeader: passthrough('div'), + DialogTitle: passthrough('h2'), + EmptyState: ({ description }: { description?: React.ReactNode }) =>
{description}
, + Input: (props: React.InputHTMLAttributes) => , + Select: ({ children, onValueChange }: { children?: React.ReactNode; onValueChange?: (value: string) => void }) => ( + {children} + ), + SelectContent: passthrough('div'), + SelectItem: ({ children, value }: { children?: React.ReactNode; value: string }) => { + const context = React.use(SelectContext) + + return ( + + ) + }, + SelectTrigger: passthrough('div'), + SelectValue: passthrough('div'), + Spinner: ({ text }: { text?: React.ReactNode }) =>
{text}
, + Switch: ({ + checked, + onCheckedChange, + ...props + }: { + checked?: boolean + onCheckedChange?: (checked: boolean) => void + }) => ( +
@@ -257,7 +257,7 @@ const TaskChannelSelector: FC<{ return ( <> - {t('agent.cherryClaw.tasks.channels.label')} + {t('agent.tasks.channels.label')} ({ value: ch.id, @@ -290,7 +290,7 @@ const TaskChannelSelector: FC<{ {hasNoChatIds && (
- {t('agent.cherryClaw.tasks.channels.noActiveChatIds')} + {t('agent.tasks.channels.noActiveChatIds')}
)}
@@ -314,16 +314,20 @@ const TaskDetail: FC<{ const isCompleted = task.status === 'completed' const statusLabels: Record = { - active: t('agent.cherryClaw.tasks.status.active'), - paused: t('agent.cherryClaw.tasks.status.paused'), - completed: t('agent.cherryClaw.tasks.status.completed') + active: t('agent.tasks.status.active'), + paused: t('agent.tasks.status.paused'), + completed: t('agent.tasks.status.completed') } const scheduleTypeLabels: Record = { - cron: t('agent.cherryClaw.tasks.scheduleType.cron'), - interval: t('agent.cherryClaw.tasks.scheduleType.interval'), - once: t('agent.cherryClaw.tasks.scheduleType.once') + cron: t('agent.tasks.scheduleType.cron'), + interval: t('agent.tasks.scheduleType.interval'), + once: t('agent.tasks.scheduleType.once') } const agentName = agents.find((a) => a.id === task.agentId)?.name ?? task.agentId + const taskChannels = useMemo( + () => channels.filter((channel) => channel.agentId === task.agentId), + [channels, task.agentId] + ) const initialSchedule = triggerToFormState(task.trigger) const [name, setName] = useState(task.name) @@ -346,11 +350,10 @@ const TaskDetail: FC<{ ? t('agent.session.workspace_selector.no_project') : (workspaces?.find((w) => w.id === workspaceId)?.name ?? workspaceId) - const toggleStatusLabel = - task.status === 'active' ? t('agent.cherryClaw.tasks.pause') : t('agent.cherryClaw.tasks.resume') + const toggleStatusLabel = task.status === 'active' ? t('agent.tasks.pause') : t('agent.tasks.resume') const moreLabel = t('common.more') - const runLabel = t('agent.cherryClaw.tasks.run') - const deleteLabel = t('agent.cherryClaw.tasks.delete.label') + const runLabel = t('agent.tasks.run') + const deleteLabel = t('agent.tasks.delete.label') useEffect(() => { setName(task.name) @@ -408,7 +411,7 @@ const TaskDetail: FC<{ if (task.trigger.kind === 'cron') return task.trigger.expr if (task.trigger.kind === 'interval') { const minutes = Math.max(1, Math.round(task.trigger.ms / 60_000)) - return `${minutes} ${t('agent.cherryClaw.tasks.intervalUnit')}` + return `${minutes} ${t('agent.tasks.intervalUnit')}` } return formatDateTime(new Date(task.trigger.at).toISOString()) } @@ -429,7 +432,7 @@ const TaskDetail: FC<{ checked={task.status === 'active'} onCheckedChange={(checked) => onToggleStatus(task.id, checked ? 'active' : 'paused')} // Name the state the switch controls; aria-checked carries on/off. title keeps the action hint for sighted hover. - aria-label={t('agent.cherryClaw.tasks.status.active')} + aria-label={t('agent.tasks.status.active')} title={toggleStatusLabel} /> )} @@ -480,13 +483,13 @@ const TaskDetail: FC<{ {task.lastRun && ( - {t('agent.cherryClaw.tasks.lastRun')}: {formatDateTime(task.lastRun)} + {t('agent.tasks.lastRun')}: {formatDateTime(task.lastRun)} )} {task.nextRun && ( - {t('agent.cherryClaw.tasks.nextRun')}: {formatDateTime(task.nextRun)} + {t('agent.tasks.nextRun')}: {formatDateTime(task.nextRun)} )} @@ -498,7 +501,7 @@ const TaskDetail: FC<{
- {t('agent.cherryClaw.tasks.name.label')} + {t('agent.tasks.name.label')} setName(e.target.value)} @@ -511,9 +514,9 @@ const TaskDetail: FC<{ header card. */}
- {t('agent.cherryClaw.tasks.prompt.label')} + {t('agent.tasks.prompt.label')} {!isCompleted && ( - +
@@ -840,6 +838,17 @@ const CreateForm: FC<{ const { data: workspaces } = useQuery('/agent-workspaces') const [saving, setSaving] = useState(false) + const availableChannels = useMemo( + () => (agentId ? channels.filter((channel) => channel.agentId === agentId) : []), + [agentId, channels] + ) + + useEffect(() => { + setChannelIds((current) => + current.filter((channelId) => availableChannels.some((channel) => channel.id === channelId)) + ) + }, [availableChannels]) + const isSystemWorkspace = workspaceId === null const workspaceLabel = isSystemWorkspace ? t('agent.session.workspace_selector.no_project') @@ -873,16 +882,16 @@ const CreateForm: FC<{ return ( - {t('agent.cherryClaw.tasks.add')} + {t('agent.tasks.add')}
{agents.length > 1 && ( <> - {t('agent.cherryClaw.channels.bindAgent')} + {t('agent.channels.bindAgent')}