Compare commits

...

9 Commits

Author SHA1 Message Date
lobehubbot 8ec55f5941 🔖 chore(release): release version v2.2.6 [skip ci] 2026-06-17 03:16:17 +00:00
Arvin Xu 63f13c2a31 🚀 release: 20260617 (#15947)
# 🚀 LobeHub Release (20260617)

**Release Date:** June 17, 2026  
**Since v2.2.5:** 42 commits · 42 merged PRs · 8 contributors

> This weekly release deepens server-side agent orchestration, brings
desktop file and worktree capabilities to the web through device RPC,
and smooths out the everyday rough edges — cold-start boot, connector
credential safety, and chat refresh feedback.

---

##  Highlights

- **Server-side Group Orchestration** — Agents can now call other agent
members server-side, enabling multi-agent collaboration without a
desktop in the loop. (#15870)
- **Desktop File Ops on the Web** — Project file operations and `git
worktree` listing now replicate from desktop to web via device RPC, so
cloud sessions can read and act on local working directories. (#15885,
#15889)
- **Fleet Running-Tasks Dashboard** — A lab-gated dashboard for
in-flight Fleet tasks, with running topics re-synced each time the
Observation tab opens. (#15817, #15922)
- **callAgent as a Deferred Tool** — The agent loop now runs `callAgent`
as a deferred tool, giving cleaner sub-agent invocation and tool-chain
handling. (#15765)
- **Connector Credential Safety** — Editing a connector no longer risks
silently wiping saved credentials; they are restored in edit mode and
preserved on save. (#15909)
- **Smoother Cold Start** — Boot now shows one continuous loading screen
instead of a brand-logo flash on cold start. (#15926)

---

## 🏗️ Core Agent & Architecture

- Improved connector, document, and Fleet agent workflows. (#15936)
- Scoped the agent conversation subtree to an explicit `agentId` for
clearer multi-agent boundaries. (#15866)
- Added a role-aware dual-form message-chain reader to the conversation
flow. (#15908)
- Anchored the server-side main chain to a run's real last tool in
heterogeneous agents. (#15883)
- Drove resume completion off the authoritative Durable Object status in
the gateway client. (#15919)
- Corrected target `agentId` and refreshed the sidebar in gateway mode
for the agent builder. (#15888)
- Forwarded model extend params on the server-side agent runtime.
(#15891)
- Preserved preference-memory receipt routing in agent signals. (#15892)
- Filtered the `.tool-results` archive out of document lists by default.
(#15935)
- Optimized the agent document list query. (#15904)

---

## 📱 Devices & Platforms

- Locked a run to the explicitly selected device, never offering
device-switching mid-run. (#15914)
- Exposed `deviceRouter` on the mobile router. (#15925)
- Opened a new Home tab from the desktop tab bar "+" button. (#15825)
- Added support for approved external local file previews on desktop.
(#15895)
- Removed web onboarding aliases from the desktop build. (#15902)
- Consolidated auth SPA loading. (#15903)

---

## 🖥️ User Experience

- Fixed assorted workspace problems and clarified workspace copy/move
actions. (#15928, #15897)
- Showed a cached message-refresh hint with breathing room around it.
(#15901, #15906)
- Added an unread-reply indicator on collapsed project groups. (#15915)
- Anchored the sidebar spacer immediately after the accordion block.
(#15871)
- Capped nested thread-list height with scroll overflow. (#15861)
- Removed the ParamsPanelToggle control icon from the chat header.
(#15860)
- Defaulted the React Scan scanner UI to off. (#15934)
- Refined top-up best-value and referral reward-rules copy. (#15924,
#15923)
- Only enforced the chat upload file-type whitelist in chat mode.
(#15884)

---

## 🔒 Reliability

- Deduped unread-count polling to reduce redundant requests. (#15881)
- Aligned dayjs locale imports. (#15896)

---

## 👥 Contributors

Huge thanks to the **8 contributors** who shipped **42 merged PRs**
across **42 commits** this cycle.

@arvinxx · @Innei · @tjx666 · @LiJian · @AmAzing129 · @sudongyuer ·
@rivertwilight · @Rdmclin2

---

**Full Changelog**:
https://github.com/lobehub/lobehub/compare/v2.2.5...release/weekly-20260617
2026-06-17 11:10:44 +08:00
Arvin Xu 3f82033939 feat(agent): improve connector, document, and fleet workflows (#15936)
*  feat(agent): allow removing unauthorized connectors from the auth alert

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 💄 style(chat): add Beta tag and info popover to Gateway Mode toggle

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 💄 style(fleet): render OpStatusTray seamlessly when no reply panel is attached

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 fix(fleet): show skeleton rows while the running-task sidebar loads

The sidebar fell straight through to the "no running tasks" empty state
during the initial fetch, so a brief flash of "empty" hid tasks that were
actually loading. Thread the SWR loading flag in and render placeholder
rows until the first result lands.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 fix(fleet): open existing agents from the board picker & fix reply-tray chrome

- AddColumnButton: selecting an agent now opens its main conversation
  (topicId null) instead of minting a throwaway empty topic via an async
  createTopic that could silently fail; dedupes + scrolls to an already-open
  column. Matches "open this agent" elsewhere in the app.
- AgentColumn: stop double-rendering OpStatusTray while the reply panel is open
  (ChatInput owns its own overlay tray); lift the collapse button above that
  floating tray so it no longer cuts the tray's top border; give ChatList its
  own flex region so the seamless tray isn't squeezed/clipped.
- OpStatusTray: keep a hairline top divider in seamless mode so the running tray
  still reads as separated from the conversation above.
- RunningTaskSidebar / AddColumnButton: harden scroll-into-view with double rAF
  so the (re-)added column reliably scrolls into view after it commits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(fleet): add one-click close-all-idle-columns action

Adds a `removeColumns` batch store action and wires the running-board
header button (committed earlier in e47228c6f7) to it, so users can clear
every non-running column in one click. Idle is derived from the board's
own columns against the live running set; running columns are untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  add agent document share URLs

*  add standalone agent document page

*  open agent documents as pages

* 💄 style(chat): polish Agent Gateway mode popover

* 🐛 fix(fleet): stabilize reply area and panel collapse state

* 🐛 fix: restore agent document portal opening

* 💄 style: adjust agent document header actions

* 🐛 fix: handle workspace document links and fleet idle state

* 🐛 fix(portal): import WideScreenContainer in document Body

The full-page document view rendered <WideScreenContainer> without
importing it, breaking type-check (TS2304: Cannot find name).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 10:45:27 +08:00
Arvin Xu 95db11309e 🐛 fix(agent-documents): filter .tool-results archive from document lists by default (#15935)
* 🐛 fix(agent-documents): filter `.tool-results` archive from document lists by default

The auto-created `.tool-results` archive folder leaked into the user-facing
documents panel because `listDocuments` / `listDocumentsForTopic` did not apply
the `excludeArchivedToolResults` filter that other read paths already use.

Make the service filter the archive by default, and let the agent
document-listing tool (server + client runtimes) explicitly opt back in via
`includeArchivedToolResults`, preserving agent archive discovery.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(agent-documents): hide archived tool result in current-topic lists

The `.tool-results` archive folder is created by mkdir but only the archived
file is associated with the topic (see archiveToolResultIfNeeded), so the
folder row never appears in the current-topic list. `excludeArchivedToolResults`
derived the archive folder id from the list alone, leaving the set empty and
leaking the archived `.txt` into `scope: 'currentTopic'` results.

Look the `.tool-results` root folder up directly in listDocumentsForTopic and
pass its id into the filter via the new explicit `archiveFolderIds` argument.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  test(agent-documents): assert runtime opts into archived tool results

The server runtime now calls listDocuments with
`{ includeArchivedToolResults: true }`; update the expectation accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 03:09:25 +08:00
Innei 6e0cd5f299 🐛 fix(react-scan): default scanner UI to off (#15934) 2026-06-17 01:28:23 +08:00
Arvin Xu 91d684878f feat(project-file): replicate desktop file operations to web via device RPC (#15885)
*  feat(project-file): replicate desktop file operations to web via device RPC

Project file tree operations only ran over Electron IPC, so remote/web
devices could browse files but not move, rename, or edit them. This wires
move/rename/write through the device RPC the same way getProjectFileIndex
already does, reusing the host-agnostic @lobechat/local-file-shell impls.

- device-control: whitelist moveLocalFiles/renameLocalFile/writeLocalFile + dispatch cases
- deviceGateway: moveProjectFiles/renameProjectFile/writeProjectFile (mutations throw on failure, no silent degrade)
- device router: matching device.* procedures
- projectFileService: deviceId-aware chokepoint methods (IPC locally vs RPC remotely)
- saveLocalFile now routes through projectFileService; remote LocalFile editor is no longer read-only

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(provider): rename ClientMode to CustomProviderDetail

ClientMode is a leftover from the old client-mode (IndexedDB) vs
server-mode (Postgres) DB split; there is no ServerMode counterpart and
the name no longer reflects what the component does — it renders the
detail view for a custom (user-created) provider, fetched by id. Rename
it to CustomProviderDetail and update the debugId + import.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🔒 fix(project-file): contain remote file mutations to the workspace root

Guard the move/rename/write device RPCs against paths escaping the project
root: these routes accept absolute paths from an untrusted browser session,
so the gateway now confirms every path stays within the working directory
(Windows-aware) before forwarding to a device. Thread `workingDirectory`
through the service and tRPC layers.

Also scope edit buffers by tab identity (device + working directory + path)
instead of bare file path, so the same path opened on two devices/workspaces
keeps independent unsaved content, and surface write failures so a failed
save keeps the buffer dirty.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 01:09:05 +08:00
Rdmclin2 2d897cea73 🐛 fix: workspace related problems (#15928)
* fix: page editor right panel loading

* feat: add page agent aquire lock

* feat: add hook self locker

* feat: add page copilot lock

* fix: session cope lock

* feat: add page draft and editor state fresh

* fix: multiple same person edit lock and recover

* fix: edit same tab

* chore: update i18n files

* fix: lint error

* fix: edit lock owner check

* chore: reuse owner id

* chore: viewer cannot see right panel
2026-06-17 00:01:42 +08:00
Innei 99785d3cc7 ♻️ refactor(auth): consolidate auth SPA loading (#15903)
* ♻️ refactor(auth): consolidate auth SPA loading

* ♻️ refactor(auth): restore auth loading visuals

* 💄 style(auth): use skeleton for OAuth consent loading
2026-06-16 23:37:55 +08:00
lobehubbot 1fa6f47fc9 🔖 chore(release): release version v2.2.5 [skip ci] 2026-06-15 11:39:57 +00:00
242 changed files with 6731 additions and 1000 deletions
+25
View File
@@ -2,6 +2,31 @@
# Changelog
## [Version 2.2.6](https://github.com/lobehub/lobe-chat/compare/v2.2.6-canary.8...v2.2.6)
<sup>Released on **2026-06-17**</sup>
#### ✨ Features
- **agent**: improve connector, document, and fleet workflows.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's improved
- **agent**: improve connector, document, and fleet workflows, closes [#15936](https://github.com/lobehub/lobe-chat/issues/15936) ([3f82033](https://github.com/lobehub/lobe-chat/commit/3f82033))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
## [Version 2.2.1](https://github.com/lobehub/lobe-chat/compare/v0.0.0-nightly.pr15228.13999...v2.2.1)
<sup>Released on **2026-05-29**</sup>
+6
View File
@@ -17,3 +17,9 @@ packages:
- './stubs/business-const'
- './stubs/types'
- '.'
allowBuilds:
electron: set this to true or false
electron-winstaller: set this to true or false
esbuild: set this to true or false
get-windows: set this to true or false
node-mac-permissions: set this to true or false
@@ -393,6 +393,7 @@ describe('agentRouter', () => {
expiresAt: new Date(),
holderId: userId,
lockedByOther: false,
ownerId: null,
});
const caller = agentRouter.createCaller(wsCtx());
@@ -410,6 +411,7 @@ describe('agentRouter', () => {
expiresAt: new Date(),
holderId: userId,
lockedByOther: false,
ownerId: null,
});
const caller = agentRouter.createCaller(wsCtx());
@@ -500,6 +500,7 @@ describe('agentGroupRouter', () => {
expiresAt: new Date(),
holderId: userId,
lockedByOther: false,
ownerId: null,
});
const caller = agentGroupRouter.createCaller(wsCtx());
@@ -517,6 +518,7 @@ describe('agentGroupRouter', () => {
expiresAt: new Date(),
holderId: userId,
lockedByOther: false,
ownerId: null,
});
const caller = agentGroupRouter.createCaller(wsCtx());
@@ -41,6 +41,7 @@ export const updateDocumentInputSchema = z.object({
editorData: z.string().optional(),
fileType: z.string().optional(),
id: z.string(),
lockOwnerId: z.string().optional(),
metadata: z.record(z.any()).optional(),
parentId: z.string().nullable().optional(),
restoreFromHistoryId: z.string().optional(),
@@ -51,6 +52,7 @@ export const updateDocumentInputSchema = z.object({
export const saveDocumentHistoryInputSchema = z.object({
documentId: z.string(),
editorData: z.string(),
lockOwnerId: z.string().optional(),
saveSource: documentHistorySaveSourceSchema,
});
@@ -98,6 +100,8 @@ export interface UpdateDocumentOutput {
export interface SaveDocumentHistoryInput {
documentId: string;
editorData: string;
/** Edit-session id proving the client still holds the workspace page lease. */
lockOwnerId?: string;
saveSource: DocumentHistorySaveSource;
}
@@ -130,6 +134,7 @@ export interface UpdateDocumentInput {
editorData?: string;
fileType?: string;
id: string;
lockOwnerId?: string;
metadata?: Record<string, any>;
parentId?: string | null;
restoreFromHistoryId?: string;
@@ -372,12 +372,16 @@ export const agentDocumentRouter = router({
.input(
z.object({
agentId: z.string(),
// Reveal the auto-created `.tool-results` archive. Off by default so
// user-facing lists stay clean; the agent document-listing tool opts in.
includeArchivedToolResults: z.boolean().optional().default(false),
scope: z.enum(['agent', 'currentTopic']).optional().default('agent'),
sourceType: z.enum(['all', 'file', 'web']).optional().default('all'),
topicId: z.string().optional(),
}),
)
.query(async ({ ctx, input }) => {
const { includeArchivedToolResults } = input;
if (input.scope === 'currentTopic') {
if (!input.topicId) throw new Error('topicId is required to list current topic documents');
@@ -385,10 +389,13 @@ export const agentDocumentRouter = router({
input.agentId,
input.topicId,
input.sourceType,
{ includeArchivedToolResults },
);
}
return ctx.agentDocumentService.listDocuments(input.agentId, input.sourceType);
return ctx.agentDocumentService.listDocuments(input.agentId, input.sourceType, {
includeArchivedToolResults,
});
}),
/**
+67
View File
@@ -384,6 +384,73 @@ export const deviceRouter = router({
}),
),
/**
* Move files/folders within a directory on a remote device, via the device's
* `moveLocalFiles` RPC. Powers the Files tree's drag-to-move in device mode.
*/
moveProjectFiles: deviceProcedure
.input(
z.object({
deviceId: z.string(),
items: z.array(z.object({ newPath: z.string(), oldPath: z.string() })),
workingDirectory: z.string(),
}),
)
.mutation(async ({ ctx, input }) =>
deviceGateway.moveProjectFiles({
deviceId: input.deviceId,
items: input.items,
userId: ctx.userId,
workingDirectory: input.workingDirectory,
}),
),
/**
* Rename a single file/folder in a directory on a remote device, via the
* device's `renameLocalFile` RPC.
*/
renameProjectFile: deviceProcedure
.input(
z.object({
deviceId: z.string(),
newName: z.string(),
path: z.string(),
workingDirectory: z.string(),
}),
)
.mutation(async ({ ctx, input }) =>
deviceGateway.renameProjectFile({
deviceId: input.deviceId,
newName: input.newName,
path: input.path,
userId: ctx.userId,
workingDirectory: input.workingDirectory,
}),
),
/**
* Save edited content back to a file on a remote device, via the device's
* `writeLocalFile` RPC. Powers remote save in the LocalFile editor.
*/
writeProjectFile: deviceProcedure
.input(
z.object({
content: z.string(),
deviceId: z.string(),
path: z.string(),
workingDirectory: z.string(),
}),
)
.mutation(async ({ ctx, input }) =>
deviceGateway.writeProjectFile({
content: input.content,
deviceId: input.deviceId,
path: input.path,
userId: ctx.userId,
workingDirectory: input.workingDirectory,
}),
),
/**
* Check whether a path exists on a remote device and is a directory, via the
* device's `statPath` RPC. Lets a web client validate a manually-entered
+11 -6
View File
@@ -183,6 +183,7 @@ export const documentRouter = router({
input.documentId,
editorData,
input.saveSource,
input.lockOwnerId,
);
}),
@@ -255,23 +256,27 @@ export const documentRouter = router({
acquireDocumentLock: documentProcedure
.use(withScopedPermission('document:update'))
.input(z.object({ id: z.string() }))
.input(z.object({ id: z.string(), ownerId: z.string().optional() }))
.mutation(async ({ ctx, input }) => {
return ctx.documentService.acquireDocumentLock(input.id);
return input.ownerId
? ctx.documentService.acquireDocumentLockWithOwner(input.id, input.ownerId)
: ctx.documentService.acquireDocumentLock(input.id);
}),
getDocumentLock: documentProcedure
.use(withScopedPermission('document:update'))
.input(z.object({ id: z.string() }))
.input(z.object({ id: z.string(), ownerId: z.string().optional() }))
.query(async ({ ctx, input }) => {
return ctx.documentService.getDocumentLock(input.id);
return ctx.documentService.getDocumentLock(input.id, input.ownerId);
}),
releaseDocumentLock: documentProcedure
.use(withScopedPermission('document:update'))
.input(z.object({ id: z.string() }))
.input(z.object({ id: z.string(), ownerId: z.string().optional() }))
.mutation(async ({ ctx, input }) => {
await ctx.documentService.releaseDocumentLock(input.id);
if (input.ownerId)
await ctx.documentService.releaseDocumentLockWithOwner(input.id, input.ownerId);
else await ctx.documentService.releaseDocumentLock(input.id);
}),
updateDocument: documentProcedure
@@ -96,6 +96,7 @@ describe('AgentDocumentsService', () => {
findContextByAgent: vi.fn(),
findByDocumentIds: vi.fn(),
findByFilename: vi.fn(),
findByParentAndFilename: vi.fn(),
findSkillDocsByAgent: vi.fn(),
hasByAgent: vi.fn(),
listByAgent: vi.fn(),
@@ -333,6 +334,65 @@ describe('AgentDocumentsService', () => {
expect(mockModel.listByAgent).toHaveBeenCalledWith('agent-1', { sourceType: 'web' });
expect(mockModel.findByAgent).not.toHaveBeenCalled();
});
it('should hide the .tool-results archive folder and its children by default', async () => {
mockModel.listByAgent.mockResolvedValue([
{
documentId: 'archive-root',
fileType: 'custom/folder',
filename: '.tool-results',
id: 'doc-archive',
parentId: null,
title: '.tool-results',
},
{
documentId: 'archive-child',
filename: 'dump.md',
id: 'doc-child',
parentId: 'archive-root',
title: 'dump',
},
{
documentId: 'documents-1',
filename: 'a.md',
id: 'doc-1',
parentId: null,
title: 'A',
},
]);
const service = new AgentDocumentsService(db, userId);
const result = await service.listDocuments('agent-1');
expect(result.map((d) => d.documentId)).toEqual(['documents-1']);
});
it('should include the .tool-results archive when includeArchivedToolResults is set', async () => {
mockModel.listByAgent.mockResolvedValue([
{
documentId: 'archive-root',
fileType: 'custom/folder',
filename: '.tool-results',
id: 'doc-archive',
parentId: null,
title: '.tool-results',
},
{
documentId: 'documents-1',
filename: 'a.md',
id: 'doc-1',
parentId: null,
title: 'A',
},
]);
const service = new AgentDocumentsService(db, userId);
const result = await service.listDocuments('agent-1', undefined, {
includeArchivedToolResults: true,
});
expect(result.map((d) => d.documentId)).toEqual(['archive-root', 'documents-1']);
});
});
describe('listDocumentsForTopic', () => {
@@ -397,6 +457,64 @@ describe('AgentDocumentsService', () => {
});
expect(mockModel.findByDocumentIds).not.toHaveBeenCalled();
});
it('should hide an archived tool result whose `.tool-results` folder is not topic-associated', async () => {
// The archive folder is created by mkdir but only the archived file gets
// associated with the topic, so the folder never appears in the list.
mockTopicDocumentModel.findByTopicId.mockResolvedValue([
{ id: 'archive-child', title: 'dump' },
]);
mockModel.listByDocumentIds.mockResolvedValue([
{
documentId: 'archive-child',
filename: 'topic_call.txt',
id: 'agent-doc-archive-child',
parentId: 'archive-root',
title: 'dump',
},
]);
mockModel.findByParentAndFilename.mockResolvedValue({
documentId: 'archive-root',
fileType: 'custom/folder',
filename: '.tool-results',
id: 'agent-doc-archive-root',
parentId: null,
});
const service = new AgentDocumentsService(db, userId);
const result = await service.listDocumentsForTopic('agent-1', 'topic-1');
expect(mockModel.findByParentAndFilename).toHaveBeenCalledWith(
'agent-1',
null,
'.tool-results',
);
expect(result).toEqual([]);
});
it('should keep the archived tool result when includeArchivedToolResults is set', async () => {
mockTopicDocumentModel.findByTopicId.mockResolvedValue([
{ id: 'archive-child', title: 'dump' },
]);
mockModel.listByDocumentIds.mockResolvedValue([
{
documentId: 'archive-child',
filename: 'topic_call.txt',
id: 'agent-doc-archive-child',
parentId: 'archive-root',
title: 'dump',
},
]);
const service = new AgentDocumentsService(db, userId);
const result = await service.listDocumentsForTopic('agent-1', 'topic-1', undefined, {
includeArchivedToolResults: true,
});
expect(result.map((d) => d.documentId)).toEqual(['archive-child']);
// No folder lookup needed when archives are included.
expect(mockModel.findByParentAndFilename).not.toHaveBeenCalled();
});
});
describe('getDocumentByFilename', () => {
@@ -71,18 +71,13 @@ type ProjectableAgentDocument = Pick<
'content' | 'editorData' | 'fileType' | 'templateId'
>;
/**
* Hide the auto-created `.tool-results/` archive (root folder + its children)
* from user-facing document lists. Agents still discover archived entries via
* the tool-oriented `listDocuments` / `listDocumentsForTopic` paths, which hit
* the model directly.
*/
const excludeArchivedToolResults = <
/** Collect ids of root `.tool-results` archive folders present in a doc list. */
const collectArchiveFolderIds = <
T extends Pick<AgentDocument, 'documentId' | 'parentId' | 'filename' | 'fileType'>,
>(
docs: T[],
): T[] => {
const archiveFolderIds = new Set(
): Set<string> =>
new Set(
docs
.filter(
(d) =>
@@ -92,6 +87,24 @@ const excludeArchivedToolResults = <
)
.map((d) => d.documentId),
);
/**
* Hide the auto-created `.tool-results/` archive (root folder + its children)
* from user-facing document lists. Applied by default everywhere, including
* `listDocuments` / `listDocumentsForTopic`. The tool runtime that lets agents
* discover archived entries opts back in via `includeArchivedToolResults`.
*
* `archiveFolderIds` lets callers whose list may not contain the folder row
* supply the ids explicitly — the topic path only sees the archived file
* (which is topic-associated), never the folder, so it can't be derived from
* the list alone.
*/
const excludeArchivedToolResults = <
T extends Pick<AgentDocument, 'documentId' | 'parentId' | 'filename' | 'fileType'>,
>(
docs: T[],
archiveFolderIds: Set<string> = collectArchiveFolderIds(docs),
): T[] => {
if (archiveFolderIds.size === 0) return docs;
return docs.filter(
(d) =>
@@ -613,16 +626,23 @@ export class AgentDocumentsService {
}
}
async listDocuments(agentId: string, sourceType?: AgentDocumentListSourceType) {
if (!sourceType) return this.agentDocumentModel.listByAgent(agentId);
async listDocuments(
agentId: string,
sourceType?: AgentDocumentListSourceType,
options?: { includeArchivedToolResults?: boolean },
) {
const docs = sourceType
? await this.agentDocumentModel.listByAgent(agentId, { sourceType })
: await this.agentDocumentModel.listByAgent(agentId);
return this.agentDocumentModel.listByAgent(agentId, { sourceType });
return options?.includeArchivedToolResults ? docs : excludeArchivedToolResults(docs);
}
async listDocumentsForTopic(
agentId: string,
topicId: string,
sourceType?: AgentDocumentListSourceType,
options?: { includeArchivedToolResults?: boolean },
) {
const topicDocs = await this.topicDocumentModel.findByTopicId(topicId);
const documentIds = topicDocs.map((doc) => doc.id);
@@ -631,9 +651,26 @@ export class AgentDocumentsService {
: await this.agentDocumentModel.listByDocumentIds(agentId, documentIds);
const docsByDocumentId = new Map(docs.map((doc) => [doc.documentId, doc]));
return topicDocs
const ordered = topicDocs
.map((topicDoc) => docsByDocumentId.get(topicDoc.id))
.filter((doc): doc is AgentDocumentListItem => Boolean(doc));
if (options?.includeArchivedToolResults) return ordered;
// The `.tool-results` folder is never topic-associated (only the archived
// file is), so it isn't in `ordered`. Look it up directly so the archived
// file can be filtered out by its parent id.
const archiveFolder = await this.agentDocumentModel.findByParentAndFilename(
agentId,
null,
TOOL_RESULTS_DIR_NAME,
);
const archiveFolderIds =
archiveFolder?.fileType === DOCUMENT_FOLDER_TYPE
? new Set([archiveFolder.documentId])
: new Set<string>();
return excludeArchivedToolResults(ordered, archiveFolderIds);
}
async getDocumentByFilename(agentId: string, filename: string) {
@@ -769,6 +769,148 @@ describe('DeviceGateway', () => {
});
});
describe('file mutation containment', () => {
const configure = () => {
mockEnv.DEVICE_GATEWAY_URL = 'https://gateway.example.com';
mockEnv.DEVICE_GATEWAY_SERVICE_TOKEN = 'token';
};
describe('writeProjectFile', () => {
it('invokes the rpc when the path is inside the workspace', async () => {
configure();
mockClient.invokeRpc.mockResolvedValue({ data: { success: true }, success: true });
const proxy = new DeviceGateway();
const result = await proxy.writeProjectFile({
content: 'next',
deviceId: 'dev-1',
path: '/proj/src/App.tsx',
userId: 'user-1',
workingDirectory: '/proj',
});
expect(result).toEqual({ success: true });
expect(mockClient.invokeRpc).toHaveBeenCalledWith(
{ deviceId: 'dev-1', timeout: 30_000, userId: 'user-1' },
{ method: 'writeLocalFile', params: { content: 'next', path: '/proj/src/App.tsx' } },
);
});
it('throws without invoking the rpc when the path escapes the workspace', async () => {
configure();
const proxy = new DeviceGateway();
await expect(
proxy.writeProjectFile({
content: 'pwned',
deviceId: 'dev-1',
path: '/etc/passwd',
userId: 'user-1',
workingDirectory: '/proj',
}),
).rejects.toThrow(/outside the approved workspace/);
expect(mockClient.invokeRpc).not.toHaveBeenCalled();
});
it('rejects a `..` traversal that resolves outside the workspace', async () => {
configure();
const proxy = new DeviceGateway();
await expect(
proxy.writeProjectFile({
content: 'pwned',
deviceId: 'dev-1',
path: '/proj/../secrets.env',
userId: 'user-1',
workingDirectory: '/proj',
}),
).rejects.toThrow(/outside the approved workspace/);
expect(mockClient.invokeRpc).not.toHaveBeenCalled();
});
it('contains Windows device paths using Windows path semantics', async () => {
configure();
const proxy = new DeviceGateway();
await expect(
proxy.writeProjectFile({
content: 'pwned',
deviceId: 'dev-1',
path: 'C:\\Windows\\System32\\drivers\\etc\\hosts',
userId: 'user-1',
workingDirectory: 'C:\\proj',
}),
).rejects.toThrow(/outside the approved workspace/);
expect(mockClient.invokeRpc).not.toHaveBeenCalled();
});
});
describe('renameProjectFile', () => {
it('throws without invoking the rpc when the path escapes the workspace', async () => {
configure();
const proxy = new DeviceGateway();
await expect(
proxy.renameProjectFile({
deviceId: 'dev-1',
newName: 'evil.ts',
path: '/etc/hosts',
userId: 'user-1',
workingDirectory: '/proj',
}),
).rejects.toThrow(/outside the approved workspace/);
expect(mockClient.invokeRpc).not.toHaveBeenCalled();
});
});
describe('moveProjectFiles', () => {
it('throws when any item moves out of the workspace', async () => {
configure();
const proxy = new DeviceGateway();
await expect(
proxy.moveProjectFiles({
deviceId: 'dev-1',
items: [
{ newPath: '/proj/b.ts', oldPath: '/proj/a.ts' },
{ newPath: '/tmp/exfil.ts', oldPath: '/proj/c.ts' },
],
userId: 'user-1',
workingDirectory: '/proj',
}),
).rejects.toThrow(/outside the approved workspace/);
expect(mockClient.invokeRpc).not.toHaveBeenCalled();
});
it('invokes the rpc when every item stays inside the workspace', async () => {
configure();
mockClient.invokeRpc.mockResolvedValue({
data: [{ newPath: '/proj/b.ts', sourcePath: '/proj/a.ts', success: true }],
success: true,
});
const proxy = new DeviceGateway();
const result = await proxy.moveProjectFiles({
deviceId: 'dev-1',
items: [{ newPath: '/proj/b.ts', oldPath: '/proj/a.ts' }],
userId: 'user-1',
workingDirectory: '/proj',
});
expect(result).toEqual([
{ newPath: '/proj/b.ts', sourcePath: '/proj/a.ts', success: true },
]);
expect(mockClient.invokeRpc).toHaveBeenCalledWith(
{ deviceId: 'dev-1', timeout: 30_000, userId: 'user-1' },
{
method: 'moveLocalFiles',
params: { items: [{ newPath: '/proj/b.ts', oldPath: '/proj/a.ts' }] },
},
);
});
});
});
describe('getClient (lazy initialization)', () => {
it('should return null when URL is missing', async () => {
mockEnv.DEVICE_GATEWAY_SERVICE_TOKEN = 'token';
@@ -1,3 +1,5 @@
import path from 'node:path';
import { type DeviceAttachment } from '@lobechat/builtin-tool-remote-device';
import {
type DeviceMessageApiResult,
@@ -26,7 +28,11 @@ import type {
DeviceGitWorktreeListItem,
DeviceListProjectSkillsResult,
DeviceLocalFilePreviewResult,
DeviceMoveProjectFileItem,
DeviceMoveProjectFileResultItem,
DeviceProjectFileIndexResult,
DeviceRenameProjectFileResult,
DeviceWriteProjectFileResult,
ProjectSkillMeta,
WorkspaceInitResult,
} from '@lobechat/types';
@@ -36,6 +42,42 @@ import { gatewayEnv } from '@/envs/gateway';
const log = debug('lobe-server:device-gateway');
/**
* Is `target` the same as, or nested inside, `root`?
*
* The device's working directory may be a POSIX path (`/Users/…`) or a Windows
* path (`C:\…`) while this check runs on the cloud server (POSIX). We pick the
* path flavour from the root's shape so a Windows device path is still resolved
* with Windows semantics rather than being mangled by `path.posix`.
*/
const isPathWithinRoot = (root: string, target: string): boolean => {
const p = /^[A-Z]:[/\\]/i.test(root) ? path.win32 : path.posix;
if (!p.isAbsolute(root) || !p.isAbsolute(target)) return false;
const relative = p.relative(p.resolve(root), p.resolve(target));
return relative === '' || (!relative.startsWith('..') && !p.isAbsolute(relative));
};
/**
* Guard the web/remote file mutations (move / rename / write) against escaping
* the project root. These routes accept absolute paths straight from an
* untrusted browser session, so before forwarding them to a device we confirm
* every path stays inside the workspace the UI is operating in — otherwise a
* caller could bypass the Files tree and mutate arbitrary locations on the
* device. Mirrors the read path's `workspaceRoot` containment check.
*/
const assertPathsWithinWorkspace = (
workspaceRoot: string,
candidates: Array<string | undefined>,
): void => {
if (!workspaceRoot) throw new Error('A workspace root is required for file mutations');
for (const candidate of candidates) {
if (!candidate || !isPathWithinRoot(workspaceRoot, candidate)) {
throw new Error(`Path is outside the approved workspace: ${candidate ?? '(empty)'}`);
}
}
};
export type { DeviceAttachment, DeviceStatusResult, DeviceSystemInfo };
export class DeviceGateway {
@@ -683,6 +725,108 @@ export class DeviceGateway {
}
}
/**
* Move one or more files/folders within a directory on a remote device, via
* the device's `moveLocalFiles` RPC. Powers the Files tree's move in device
* mode. Unlike the read RPCs this is a user-initiated mutation, so a missing
* gateway / offline device / failed call throws rather than degrading to
* `undefined` — the UI surfaces the error instead of silently no-op'ing.
*/
async moveProjectFiles(params: {
deviceId: string;
items: DeviceMoveProjectFileItem[];
timeout?: number;
userId: string;
workingDirectory: string;
}): Promise<DeviceMoveProjectFileResultItem[]> {
const { userId, deviceId, items, workingDirectory, timeout = 30_000 } = params;
const client = this.getClient();
if (!client) throw new Error('Device gateway not configured');
assertPathsWithinWorkspace(
workingDirectory,
items.flatMap((item) => [item.oldPath, item.newPath]),
);
const result = await client.invokeRpc<DeviceMoveProjectFileResultItem[]>(
{ deviceId, timeout, userId },
{ method: 'moveLocalFiles', params: { items } },
);
if (!result.success || !result.data) {
log('moveProjectFiles: failed for deviceId=%s — %s', deviceId, result.error);
throw new Error(result.error || 'Move failed');
}
return result.data;
}
/**
* Rename a single file/folder in a directory on a remote device, via the
* device's `renameLocalFile` RPC. Like `moveProjectFiles`, a transport failure
* throws rather than degrading silently.
*/
async renameProjectFile(params: {
deviceId: string;
newName: string;
path: string;
timeout?: number;
userId: string;
workingDirectory: string;
}): Promise<DeviceRenameProjectFileResult> {
const { userId, deviceId, path, newName, workingDirectory, timeout = 30_000 } = params;
const client = this.getClient();
if (!client) throw new Error('Device gateway not configured');
// The rename stays in the same directory (the device rejects separators in
// `newName`), so containing the source path also contains the target.
assertPathsWithinWorkspace(workingDirectory, [path]);
const result = await client.invokeRpc<DeviceRenameProjectFileResult>(
{ deviceId, timeout, userId },
{ method: 'renameLocalFile', params: { newName, path } },
);
if (!result.success || !result.data) {
log('renameProjectFile: failed for deviceId=%s — %s', deviceId, result.error);
throw new Error(result.error || 'Rename failed');
}
return result.data;
}
/**
* Save edited content back to a file on a remote device, via the device's
* `writeLocalFile` RPC. Powers remote save in the LocalFile editor. Like the
* other file mutations, a transport failure throws rather than degrading.
*/
async writeProjectFile(params: {
content: string;
deviceId: string;
path: string;
timeout?: number;
userId: string;
workingDirectory: string;
}): Promise<DeviceWriteProjectFileResult> {
const { userId, deviceId, path, content, workingDirectory, timeout = 30_000 } = params;
const client = this.getClient();
if (!client) throw new Error('Device gateway not configured');
assertPathsWithinWorkspace(workingDirectory, [path]);
const result = await client.invokeRpc<DeviceWriteProjectFileResult>(
{ deviceId, timeout, userId },
{ method: 'writeLocalFile', params: { content, path } },
);
if (!result.success || !result.data) {
log('writeProjectFile: failed for deviceId=%s — %s', deviceId, result.error);
throw new Error(result.error || 'Write failed');
}
return result.data;
}
/**
* Check whether a path exists on the device and is a directory, via the same
* generic `invokeRpc` channel as `gitInfo`. Lets a web / remote client
@@ -806,7 +806,7 @@ describe('DocumentService', () => {
it('should reject a workspace save when another member holds the edit lock', async () => {
const wsService = new DocumentService(mockDb, userId, 'ws-1');
mockDocumentModel.findById.mockResolvedValue(createCurrentDocument({ workspaceId: 'ws-1' }));
vi.spyOn(EditLockService.prototype, 'getBlockingHolder').mockResolvedValue('other-user');
vi.spyOn(EditLockService.prototype, 'canWrite').mockResolvedValue(false);
await expect(wsService.updateDocument('doc-1', { content: 'x' })).rejects.toMatchObject({
code: 'CONFLICT',
@@ -818,13 +818,25 @@ describe('DocumentService', () => {
const wsService = new DocumentService(mockDb, userId, 'ws-1');
mockDocumentModel.update.mockResolvedValue({ id: 'doc-1' });
mockDocumentModel.findById.mockResolvedValue(createCurrentDocument({ workspaceId: 'ws-1' }));
vi.spyOn(EditLockService.prototype, 'getBlockingHolder').mockResolvedValue(null);
vi.spyOn(EditLockService.prototype, 'canWrite').mockResolvedValue(true);
await wsService.updateDocument('doc-1', { content: 'x' });
expect(mockDocumentModel.update).toHaveBeenCalled();
});
it('checks workspace body saves against the provided lock owner id', async () => {
const wsService = new DocumentService(mockDb, userId, 'ws-1');
mockDocumentModel.update.mockResolvedValue({ id: 'doc-1' });
mockDocumentModel.findById.mockResolvedValue(createCurrentDocument({ workspaceId: 'ws-1' }));
const guardSpy = vi.spyOn(EditLockService.prototype, 'canWrite').mockResolvedValue(true);
await wsService.updateDocument('doc-1', { content: 'x', lockOwnerId: 'owner-1' });
expect(guardSpy).toHaveBeenCalledWith('document', 'doc-1', 'owner-1');
expect(mockDocumentModel.update).toHaveBeenCalled();
});
it('allows a metadata-only save while another member holds the lock (only the body is locked)', async () => {
const wsService = new DocumentService(mockDb, userId, 'ws-1');
mockDocumentModel.update.mockResolvedValue({ id: 'doc-1' });
@@ -832,7 +844,7 @@ describe('DocumentService', () => {
mockDocumentModel.findById.mockResolvedValue(
createCurrentDocument({ content: 'body', editorData: { blocks: [] }, workspaceId: 'ws-1' }),
);
const guardSpy = vi.spyOn(EditLockService.prototype, 'getBlockingHolder');
const guardSpy = vi.spyOn(EditLockService.prototype, 'canWrite');
await wsService.updateDocument('doc-1', {
content: 'body',
@@ -853,7 +865,7 @@ describe('DocumentService', () => {
mockDocumentModel.findById.mockResolvedValue(
createCurrentDocument({ editorData: { blocks: [] }, workspaceId: 'ws-1' }),
);
vi.spyOn(EditLockService.prototype, 'getBlockingHolder').mockResolvedValue('other-user');
vi.spyOn(EditLockService.prototype, 'canWrite').mockResolvedValue(false);
// editorData changed (historyAppended) → guard runs even with no `content`.
await expect(
@@ -863,13 +875,151 @@ describe('DocumentService', () => {
});
});
describe('runWithDocumentLock', () => {
it('runs the callback without touching the lock for personal documents', async () => {
const acquireSpy = vi.spyOn(EditLockService.prototype, 'acquire');
const releaseSpy = vi.spyOn(EditLockService.prototype, 'release');
const fn = vi.fn().mockResolvedValue('ok');
const result = await service.runWithDocumentLock('doc-1', fn);
expect(result).toBe('ok');
expect(fn).toHaveBeenCalledTimes(1);
expect(acquireSpy).not.toHaveBeenCalled();
expect(releaseSpy).not.toHaveBeenCalled();
});
it('acquires a free lock, runs the callback, then releases it', async () => {
const wsService = new DocumentService(mockDb, userId, 'ws-1');
vi.spyOn(EditLockService.prototype, 'getActiveLock').mockResolvedValue(undefined);
vi.spyOn(EditLockService.prototype, 'acquire').mockResolvedValue({
expiresAt: new Date(),
holderId: userId,
lockedByOther: false,
ownerId: 'server-owner',
});
const releaseSpy = vi.spyOn(EditLockService.prototype, 'release').mockResolvedValue(true);
const fn = vi.fn().mockResolvedValue('written');
const result = await wsService.runWithDocumentLock('doc-1', fn);
expect(result).toBe('written');
expect(fn).toHaveBeenCalledTimes(1);
expect(releaseSpy).toHaveBeenCalledWith(
'document',
'doc-1',
expect.stringMatching(/^server:/),
);
});
it('rejects when the same user already holds the lease in another edit session', async () => {
const wsService = new DocumentService(mockDb, userId, 'ws-1');
vi.spyOn(EditLockService.prototype, 'getActiveLock').mockResolvedValue({
expiresAt: new Date(),
ownerId: 'page-owner',
userId,
});
vi.spyOn(EditLockService.prototype, 'acquire').mockResolvedValue({
expiresAt: new Date(),
holderId: userId,
lockedByOther: true,
ownerId: 'page-owner',
});
const releaseSpy = vi.spyOn(EditLockService.prototype, 'release');
const fn = vi.fn();
await expect(wsService.runWithDocumentLock('doc-1', fn)).rejects.toMatchObject({
code: 'CONFLICT',
});
expect(fn).not.toHaveBeenCalled();
expect(releaseSpy).not.toHaveBeenCalled();
});
it('rejects with CONFLICT and skips the callback when another member holds the lock', async () => {
const wsService = new DocumentService(mockDb, userId, 'ws-1');
vi.spyOn(EditLockService.prototype, 'getActiveLock').mockResolvedValue({
expiresAt: new Date(),
ownerId: 'other-owner',
userId: 'other-user',
});
vi.spyOn(EditLockService.prototype, 'acquire').mockResolvedValue({
expiresAt: new Date(),
holderId: 'other-user',
lockedByOther: true,
ownerId: 'other-owner',
});
const releaseSpy = vi.spyOn(EditLockService.prototype, 'release');
const fn = vi.fn();
await expect(wsService.runWithDocumentLock('doc-1', fn)).rejects.toMatchObject({
code: 'CONFLICT',
});
expect(fn).not.toHaveBeenCalled();
expect(releaseSpy).not.toHaveBeenCalled();
});
it('still releases a freshly-claimed lock when the callback throws', async () => {
const wsService = new DocumentService(mockDb, userId, 'ws-1');
vi.spyOn(EditLockService.prototype, 'getActiveLock').mockResolvedValue(undefined);
vi.spyOn(EditLockService.prototype, 'acquire').mockResolvedValue({
expiresAt: new Date(),
holderId: userId,
lockedByOther: false,
ownerId: 'server-owner',
});
const releaseSpy = vi.spyOn(EditLockService.prototype, 'release').mockResolvedValue(true);
const fn = vi.fn().mockRejectedValue(new Error('boom'));
await expect(wsService.runWithDocumentLock('doc-1', fn)).rejects.toThrow('boom');
expect(releaseSpy).toHaveBeenCalledWith(
'document',
'doc-1',
expect.stringMatching(/^server:/),
);
});
it('rides along on the user existing lease and skips release', async () => {
// The user's live editor already holds the lock. The server run must
// refresh under the user's ownerId — not mint a fresh one and release
// afterwards — or the editor's next save would be rejected by the
// owner-scoped guard, and another collaborator could grab the gap.
const wsService = new DocumentService(mockDb, userId, 'ws-1');
vi.spyOn(EditLockService.prototype, 'getActiveLock').mockResolvedValue({
expiresAt: new Date(),
ownerId: 'user-tab-A',
userId,
});
const acquireSpy = vi.spyOn(EditLockService.prototype, 'acquire').mockResolvedValue({
expiresAt: new Date(),
holderId: userId,
lockedByOther: false,
ownerId: 'user-tab-A',
});
const releaseSpy = vi.spyOn(EditLockService.prototype, 'release');
const fn = vi.fn().mockResolvedValue('written');
const result = await wsService.runWithDocumentLock('doc-1', fn);
expect(result).toBe('written');
expect(fn).toHaveBeenCalledTimes(1);
expect(acquireSpy).toHaveBeenCalledWith('document', 'doc-1', 'user-tab-A');
expect(releaseSpy).not.toHaveBeenCalled();
});
});
describe('document edit lock', () => {
it('reports unlocked for personal documents without touching the lock service', async () => {
const acquireSpy = vi.spyOn(EditLockService.prototype, 'acquire');
const result = await service.acquireDocumentLock('doc-1');
expect(result).toEqual({ expiresAt: null, holderId: null, lockedByOther: false });
expect(result).toEqual({
expiresAt: null,
holderId: null,
lockedByOther: false,
ownerId: null,
});
expect(acquireSpy).not.toHaveBeenCalled();
});
@@ -878,12 +1028,17 @@ describe('DocumentService', () => {
const expiresAt = new Date(Date.now() + 60_000);
const acquireSpy = vi
.spyOn(EditLockService.prototype, 'acquire')
.mockResolvedValue({ expiresAt, holderId: userId, lockedByOther: false });
.mockResolvedValue({ expiresAt, holderId: userId, lockedByOther: false, ownerId: userId });
const result = await wsService.acquireDocumentLock('doc-1');
expect(acquireSpy).toHaveBeenCalledWith('document', 'doc-1');
expect(result).toEqual({ expiresAt, holderId: userId, lockedByOther: false });
expect(acquireSpy).toHaveBeenCalledWith('document', 'doc-1', userId);
expect(result).toEqual({
expiresAt,
holderId: userId,
lockedByOther: false,
ownerId: userId,
});
});
it('reports another member as holder when the lock is taken', async () => {
@@ -893,11 +1048,17 @@ describe('DocumentService', () => {
expiresAt,
holderId: 'other-user',
lockedByOther: true,
ownerId: 'other-owner',
});
const result = await wsService.acquireDocumentLock('doc-1');
expect(result).toEqual({ expiresAt, holderId: 'other-user', lockedByOther: true });
expect(result).toEqual({
expiresAt,
holderId: 'other-user',
lockedByOther: true,
ownerId: 'other-owner',
});
});
it('releaseDocumentLock is a no-op for personal documents', async () => {
@@ -910,33 +1071,42 @@ describe('DocumentService', () => {
const wsService = new DocumentService(mockDb, userId, 'ws-1');
const releaseSpy = vi.spyOn(EditLockService.prototype, 'release').mockResolvedValue(true);
await wsService.releaseDocumentLock('doc-1');
expect(releaseSpy).toHaveBeenCalledWith('document', 'doc-1');
expect(releaseSpy).toHaveBeenCalledWith('document', 'doc-1', userId);
});
it('acquireDocumentLock broadcasts lock.changed on a holder edge (first claim)', async () => {
const wsService = new DocumentService(mockDb, userId, 'ws-1');
vi.spyOn(EditLockService.prototype, 'getActiveHolder').mockResolvedValue(undefined);
vi.spyOn(EditLockService.prototype, 'getActiveLock').mockResolvedValue(undefined);
vi.spyOn(EditLockService.prototype, 'acquire').mockResolvedValue({
expiresAt: new Date(),
holderId: userId,
lockedByOther: false,
ownerId: userId,
});
await wsService.acquireDocumentLock('doc-1');
expect(publishResourceEventMock).toHaveBeenCalledWith(
{ id: 'doc-1', type: 'document' },
expect.objectContaining({ data: { holderId: userId }, type: 'lock.changed' }),
expect.objectContaining({
data: expect.objectContaining({ holderId: userId, ownerId: userId }),
type: 'lock.changed',
}),
);
});
it('acquireDocumentLock does NOT broadcast on a steady-state heartbeat (same holder)', async () => {
const wsService = new DocumentService(mockDb, userId, 'ws-1');
vi.spyOn(EditLockService.prototype, 'getActiveHolder').mockResolvedValue(userId);
vi.spyOn(EditLockService.prototype, 'getActiveLock').mockResolvedValue({
expiresAt: new Date(),
ownerId: userId,
userId,
});
vi.spyOn(EditLockService.prototype, 'acquire').mockResolvedValue({
expiresAt: new Date(),
holderId: userId,
lockedByOther: false,
ownerId: userId,
});
await wsService.acquireDocumentLock('doc-1');
@@ -952,7 +1122,10 @@ describe('DocumentService', () => {
expect(publishResourceEventMock).toHaveBeenCalledWith(
{ id: 'doc-1', type: 'document' },
expect.objectContaining({ data: { holderId: null }, type: 'lock.changed' }),
expect.objectContaining({
data: expect.objectContaining({ holderId: null, ownerId: null }),
type: 'lock.changed',
}),
);
});
@@ -1010,7 +1183,7 @@ describe('DocumentService', () => {
it('does not check the lock for personal documents', async () => {
mockDocumentModel.findById.mockResolvedValue({ id: 'doc-1', editorData: { blocks: [] } });
const guardSpy = vi.spyOn(EditLockService.prototype, 'getBlockingHolder');
const guardSpy = vi.spyOn(EditLockService.prototype, 'canWrite');
await service.saveDocumentHistory('doc-1', { blocks: [] }, 'llm_call');
@@ -1021,7 +1194,7 @@ describe('DocumentService', () => {
it('rejects a workspace history snapshot when another member holds the lock', async () => {
const wsService = new DocumentService(mockDb, userId, 'ws-1');
mockDocumentModel.findById.mockResolvedValue({ id: 'doc-1', editorData: { blocks: [] } });
vi.spyOn(EditLockService.prototype, 'getBlockingHolder').mockResolvedValue('other-user');
vi.spyOn(EditLockService.prototype, 'canWrite').mockResolvedValue(false);
await expect(
wsService.saveDocumentHistory('doc-1', { blocks: [] }, 'llm_call'),
@@ -1032,12 +1205,22 @@ describe('DocumentService', () => {
it('allows a workspace history snapshot when no other member holds the lock', async () => {
const wsService = new DocumentService(mockDb, userId, 'ws-1');
mockDocumentModel.findById.mockResolvedValue({ id: 'doc-1', editorData: { blocks: [] } });
vi.spyOn(EditLockService.prototype, 'getBlockingHolder').mockResolvedValue(null);
vi.spyOn(EditLockService.prototype, 'canWrite').mockResolvedValue(true);
await wsService.saveDocumentHistory('doc-1', { blocks: [] }, 'llm_call');
expect(mockDocumentHistoryService.createHistory).toHaveBeenCalled();
});
it('forwards the lock owner so the holder can snapshot its own page', async () => {
const wsService = new DocumentService(mockDb, userId, 'ws-1');
mockDocumentModel.findById.mockResolvedValue({ id: 'doc-1', editorData: { blocks: [] } });
const guardSpy = vi.spyOn(EditLockService.prototype, 'canWrite').mockResolvedValue(true);
await wsService.saveDocumentHistory('doc-1', { blocks: [] }, 'llm_call', 'page-owner-1');
expect(guardSpy).toHaveBeenCalledWith('document', 'doc-1', 'page-owner-1');
});
});
describe('trySaveCurrentDocumentHistory', () => {
+115 -18
View File
@@ -1,3 +1,5 @@
import { randomUUID } from 'node:crypto';
import { CUSTOM_DOCUMENT_FILE_TYPE, CUSTOM_FOLDER_FILE_TYPE } from '@lobechat/const';
import { type LobeChatDatabase } from '@lobechat/database';
import { type DocumentItem } from '@lobechat/database/schemas';
@@ -216,18 +218,34 @@ export class DocumentService {
* always report as unlocked.
*/
async acquireDocumentLock(id: string): Promise<DocumentLockResult> {
if (!this.workspaceId) return { expiresAt: null, holderId: null, lockedByOther: false };
return this.acquireDocumentLockWithOwner(id, this.userId);
}
const prevHolder = await this.editLockService.getActiveHolder('document', id);
const result = await this.editLockService.acquire('document', id);
async acquireDocumentLockWithOwner(id: string, ownerId: string): Promise<DocumentLockResult> {
if (!this.workspaceId)
return { expiresAt: null, holderId: null, lockedByOther: false, ownerId: null };
const prevHolder = await this.editLockService.getActiveLock('document', id);
const result = await this.editLockService.acquire('document', id, ownerId);
// Broadcast only on a holder edge (first claim / takeover). This method also
// serves the periodic heartbeat, so a steady-state refresh (same holder)
// must not emit an event.
if ((result.holderId ?? null) !== (prevHolder ?? null)) {
if (
(result.holderId ?? null) !== (prevHolder?.userId ?? null) ||
(result.ownerId ?? null) !== (prevHolder?.ownerId ?? null)
) {
void publishResourceEvent(
{ id, type: 'document' },
{ actorId: this.userId, data: { holderId: result.holderId }, type: 'lock.changed' },
{
actorId: this.userId,
data: {
expiresAt: result.expiresAt?.toISOString() ?? null,
holderId: result.holderId,
ownerId: result.ownerId,
},
type: 'lock.changed',
},
);
}
@@ -238,13 +256,20 @@ export class DocumentService {
* Read-only peek of the current edit lock (does not acquire). Lets a client
* render a workspace page read-only on open when another member holds it.
*/
async getDocumentLock(id: string): Promise<DocumentLockResult> {
if (!this.workspaceId) return { expiresAt: null, holderId: null, lockedByOther: false };
const holder = await this.editLockService.getActiveHolder('document', id);
async getDocumentLock(id: string, ownerId?: string): Promise<DocumentLockResult> {
if (!this.workspaceId)
return { expiresAt: null, holderId: null, lockedByOther: false, ownerId: null };
const holder = await this.editLockService.getActiveLock('document', id);
const lockedByOther = holder
? holder.ownerId
? holder.ownerId !== ownerId
: holder.userId !== this.userId
: false;
return {
expiresAt: null,
holderId: holder ?? null,
lockedByOther: Boolean(holder) && holder !== this.userId,
expiresAt: holder?.expiresAt ?? null,
holderId: holder?.userId ?? null,
lockedByOther,
ownerId: holder?.ownerId ?? null,
};
}
@@ -252,18 +277,87 @@ export class DocumentService {
* Release the edit lock if the current user holds it. No-op in personal mode.
*/
async releaseDocumentLock(id: string): Promise<void> {
return this.releaseDocumentLockWithOwner(id, this.userId);
}
async releaseDocumentLockWithOwner(id: string, ownerId: string): Promise<void> {
if (!this.workspaceId) return;
// Only broadcast "unlocked" when we actually released our own lock — if the
// lease had expired and another member took over, the lock is still held and
// a bogus holderId:null would wrongly flip their viewers to editable.
const released = await this.editLockService.release('document', id);
const released = await this.editLockService.release('document', id, ownerId);
if (!released) return;
void publishResourceEvent(
{ id, type: 'document' },
{ actorId: this.userId, data: { holderId: null }, type: 'lock.changed' },
{
actorId: this.userId,
data: { expiresAt: null, holderId: null, ownerId: null },
type: 'lock.changed',
},
);
}
/**
* Run a server-initiated read-modify-write (e.g. a Page Agent tool) under the
* collaborative edit lock. Acquiring the lock up front — rather than only
* checking it at persist time like {@link updateDocument} — serializes agent
* writes against other workspace members and rejects when someone else is
* actively editing, so an agent can no longer silently clobber a human's
* in-progress edits or another concurrent agent write.
*
* No-op in personal mode (no workspace → no collaboration → no lock). When
* Redis is down the underlying lock degrades to "unlocked" (fail-open), so
* this never blocks a write.
*/
async runWithDocumentLock<T>(id: string, fn: () => Promise<T>): Promise<T> {
if (!this.workspaceId) {
// TEMP DIAGNOSTIC (LOBE-10470): distinguishes "no-op because workspaceId is
// missing at runtime" from "lock actually evaluated". Remove once verified.
log('runWithDocumentLock skip: no workspaceId (id=%s userId=%s)', id, this.userId);
return fn();
}
// If this user's live editor already holds the lease, ride along on the
// same ownerId so the acquire below is a pure heartbeat. Stealing the lock
// with a fresh `server:UUID` would silently rewrite the lease's ownerId,
// demote the user's saves through the owner-scoped write guard, and on the
// finally release leave a window where another member could grab the free
// lock. When we're truly claiming a lock, mint a server-scoped owner id
// we can identify in release.
const holderBefore = await this.editLockService.getActiveLock('document', id);
const heldBeforeByUser = holderBefore?.userId === this.userId;
const ownerId =
heldBeforeByUser && holderBefore?.ownerId ? holderBefore.ownerId : `server:${randomUUID()}`;
const lock = await this.acquireDocumentLockWithOwner(id, ownerId);
// TEMP DIAGNOSTIC (LOBE-10470): one reproduction reveals workspaceId/holder/acquire.
log(
'runWithDocumentLock: id=%s userId=%s ws=%s holderBefore=%s acquired=%o',
id,
this.userId,
this.workspaceId,
holderBefore?.userId,
lock,
);
if (lock.lockedByOther) {
throw new TRPCError({
cause: { data: { code: 'DocumentLocked' } },
code: 'CONFLICT',
message: 'Document is being edited by another user',
});
}
try {
return await fn();
} finally {
// Only release a lease we freshly claimed. When the same user already
// held it, leave their session alive — releasing would briefly flip
// their editor to read-only and let another member grab the lock in
// the gap before the next client heartbeat.
if (!heldBeforeByUser) await this.releaseDocumentLockWithOwner(id, ownerId);
}
}
async listDocumentHistory(
params: ListDocumentHistoryParams,
options?: DocumentHistoryAccessOptions,
@@ -292,6 +386,7 @@ export class DocumentService {
documentId: string,
editorData: Record<string, any>,
saveSource: DocumentHistorySaveSource,
lockOwnerId?: string,
): Promise<SaveDocumentHistoryResult> {
const currentDocument = await this.documentModel.findById(documentId);
if (!currentDocument) {
@@ -301,10 +396,12 @@ export class DocumentService {
// Same collaborative edit-lock guard as updateDocument: don't record a
// history snapshot for a workspace document another member is editing, so a
// locked-out actor (e.g. a Copilot mutation that will itself be rejected)
// can't pollute the version timeline.
// can't pollute the version timeline. The lock holder forwards its
// `lockOwnerId` so it can still snapshot its own page (e.g. the pre-mutation
// snapshot a Copilot edit takes) without being blocked by its own lease.
if (this.workspaceId) {
const blockedBy = await this.editLockService.getBlockingHolder('document', documentId);
if (blockedBy) {
const canWrite = await this.editLockService.canWrite('document', documentId, lockOwnerId);
if (!canWrite) {
throw new TRPCError({
cause: { data: { code: 'DocumentLocked' } },
code: 'CONFLICT',
@@ -449,8 +546,8 @@ export class DocumentService {
historyAppended ||
(params.content !== undefined && params.content !== currentDocument.content);
if (this.workspaceId && contentChanged) {
const blockedBy = await this.editLockService.getBlockingHolder('document', id);
if (blockedBy) {
const canWrite = await this.editLockService.canWrite('document', id, params.lockOwnerId);
if (!canWrite) {
throw new TRPCError({
cause: { data: { code: 'DocumentLocked' } },
code: 'CONFLICT',
@@ -59,6 +59,7 @@ export interface UpdateDocumentParams {
content?: string;
editorData?: Record<string, any>;
fileType?: string;
lockOwnerId?: string;
metadata?: Record<string, any>;
parentId?: string | null;
restoreFromHistoryId?: string;
@@ -83,4 +84,6 @@ export interface DocumentLockResult {
holderId: string | null;
/** True when another active user holds the lock (caller is locked out). */
lockedByOther: boolean;
/** The edit-session id currently holding the lock, or null when unlocked / legacy. */
ownerId: string | null;
}
@@ -5,17 +5,28 @@ import { EditLockService } from '../index';
/**
* Minimal in-memory fake of the ioredis calls EditLockService uses:
* `set(k, v, 'EX', ttl[, 'NX'])`, `get(k)`, and the compare-and-delete `eval`.
* The eval mirrors RELEASE_SCRIPT: legacy raw payloads delete when ARGV[2]
* (userId) matches; JSON payloads require both userId and ownerId to match.
*/
const makeFakeRedis = () => {
const store = new Map<string, string>();
return {
eval: vi.fn(async (_script: string, _numKeys: number, key: string, arg: string) => {
if (store.get(key) === arg) {
store.delete(key);
return 1;
}
return 0;
}),
eval: vi.fn(
async (_script: string, _numKeys: number, key: string, ownerArg: string, userArg: string) => {
const raw = store.get(key);
if (!raw) return 0;
let matches = raw === userArg;
try {
const parsed = JSON.parse(raw);
matches = matches || (parsed.userId === userArg && parsed.ownerId === ownerArg);
} catch {}
if (matches) {
store.delete(key);
return 1;
}
return 0;
},
),
get: vi.fn(async (key: string) => store.get(key) ?? null),
set: vi.fn(async (key: string, value: string, ...args: unknown[]) => {
if (args.includes('NX') && store.has(key)) return null;
@@ -31,41 +42,106 @@ describe('EditLockService', () => {
const redis = makeFakeRedis();
const svc = new EditLockService('user-1', redis as any);
const result = await svc.acquire('document', 'doc-1');
const result = await svc.acquire('document', 'doc-1', 'owner-1');
expect(result.holderId).toBe('user-1');
expect(result.ownerId).toBe('owner-1');
expect(result.lockedByOther).toBe(false);
expect(result.expiresAt).toBeInstanceOf(Date);
expect(redis.store.get('editlock:document:doc-1')).toBe('user-1');
expect(JSON.parse(redis.store.get('editlock:document:doc-1')!)).toEqual(
expect.objectContaining({ ownerId: 'owner-1', userId: 'user-1' }),
);
});
it('reports another member as holder when the lock is already taken', async () => {
const redis = makeFakeRedis();
await new EditLockService('user-1', redis as any).acquire('document', 'doc-1');
await new EditLockService('user-1', redis as any).acquire('document', 'doc-1', 'owner-1');
const result = await new EditLockService('user-2', redis as any).acquire('document', 'doc-1');
expect(result).toEqual({ expiresAt: null, holderId: 'user-1', lockedByOther: true });
expect(result).toEqual(
expect.objectContaining({ holderId: 'user-1', lockedByOther: true, ownerId: 'owner-1' }),
);
});
it('lets the holder refresh their own lease', async () => {
const redis = makeFakeRedis();
const svc = new EditLockService('user-1', redis as any);
await svc.acquire('document', 'doc-1');
await svc.acquire('document', 'doc-1', 'owner-1');
const result = await svc.acquire('document', 'doc-1');
const result = await svc.acquire('document', 'doc-1', 'owner-1');
expect(result.holderId).toBe('user-1');
expect(result.ownerId).toBe('owner-1');
expect(result.lockedByOther).toBe(false);
});
it('lets the same user take over their own ghost lock from another session', async () => {
// A refresh / navigate-away whose release never reached the server leaves a
// stale ownerId in Redis. The new session should silently take over rather
// than report "you're editing this in another tab" — the old session is
// almost certainly gone.
const redis = makeFakeRedis();
await new EditLockService('user-1', redis as any).acquire('document', 'doc-1', 'owner-1');
const result = await new EditLockService('user-1', redis as any).acquire(
'document',
'doc-1',
'owner-2',
);
expect(result).toEqual(
expect.objectContaining({ holderId: 'user-1', lockedByOther: false, ownerId: 'owner-2' }),
);
expect(JSON.parse(redis.store.get('editlock:document:doc-1')!)).toEqual(
expect.objectContaining({ ownerId: 'owner-2', userId: 'user-1' }),
);
});
it('still treats a different user with a different owner as blocked (takeover is user-scoped)', async () => {
const redis = makeFakeRedis();
await new EditLockService('user-1', redis as any).acquire('document', 'doc-1', 'owner-1');
const result = await new EditLockService('user-2', redis as any).acquire(
'document',
'doc-1',
'owner-2',
);
expect(result).toEqual(
expect.objectContaining({ holderId: 'user-1', lockedByOther: true, ownerId: 'owner-1' }),
);
});
it('refuses to refresh when a stranger replays the broadcast ownerId', async () => {
// The ownerId is broadcast on `lock.changed`, so another workspace member can
// learn it from a subscription. They must not be able to echo it back to
// refresh or take over the lock — only the original holder's userId may.
const redis = makeFakeRedis();
await new EditLockService('user-1', redis as any).acquire('document', 'doc-1', 'owner-1');
const result = await new EditLockService('user-2', redis as any).acquire(
'document',
'doc-1',
'owner-1',
);
expect(result).toEqual(
expect.objectContaining({ holderId: 'user-1', lockedByOther: true, ownerId: 'owner-1' }),
);
// The persisted lock must still belong to user-1.
expect(JSON.parse(redis.store.get('editlock:document:doc-1')!)).toEqual(
expect.objectContaining({ ownerId: 'owner-1', userId: 'user-1' }),
);
});
it('getActiveHolder reports the current holder, or undefined when free', async () => {
const redis = makeFakeRedis();
expect(
await new EditLockService('user-1', redis as any).getActiveHolder('document', 'doc-1'),
).toBeUndefined();
await new EditLockService('user-1', redis as any).acquire('document', 'doc-1');
await new EditLockService('user-1', redis as any).acquire('document', 'doc-1', 'owner-1');
expect(
await new EditLockService('user-2', redis as any).getActiveHolder('document', 'doc-1'),
).toBe('user-1');
@@ -73,46 +149,104 @@ describe('EditLockService', () => {
it('keys locks per resource type, so the same id does not collide across types', async () => {
const redis = makeFakeRedis();
await new EditLockService('user-1', redis as any).acquire('document', 'shared-id');
await new EditLockService('user-1', redis as any).acquire('document', 'shared-id', 'owner-1');
// A different resource family with the same id is independently lockable.
const result = await new EditLockService('user-2', redis as any).acquire('agent', 'shared-id');
expect(result.holderId).toBe('user-2');
expect(result.lockedByOther).toBe(false);
expect(redis.store.get('editlock:document:shared-id')).toBe('user-1');
expect(redis.store.get('editlock:agent:shared-id')).toBe('user-2');
expect(JSON.parse(redis.store.get('editlock:document:shared-id')!)).toEqual(
expect.objectContaining({ userId: 'user-1' }),
);
expect(JSON.parse(redis.store.get('editlock:agent:shared-id')!)).toEqual(
expect.objectContaining({ userId: 'user-2' }),
);
});
it('getBlockingHolder returns the holder only when it is someone else', async () => {
const redis = makeFakeRedis();
await new EditLockService('user-1', redis as any).acquire('document', 'doc-1');
await new EditLockService('user-1', redis as any).acquire('document', 'doc-1', 'owner-1');
expect(
await new EditLockService('user-2', redis as any).getBlockingHolder('document', 'doc-1'),
).toBe('user-1');
expect(
await new EditLockService('user-1', redis as any).getBlockingHolder('document', 'doc-1'),
await new EditLockService('user-1', redis as any).getBlockingHolder(
'document',
'doc-1',
'owner-1',
),
).toBeNull();
expect(
await new EditLockService('user-1', redis as any).getBlockingHolder(
'document',
'doc-1',
'owner-2',
),
).toBe('user-1');
// Stranger replaying the broadcast ownerId must still be blocked.
expect(
await new EditLockService('user-2', redis as any).getBlockingHolder(
'document',
'doc-1',
'owner-1',
),
).toBe('user-1');
});
it('only releases the lock for the current holder', async () => {
it('only releases the lock for the current owner', async () => {
const redis = makeFakeRedis();
await new EditLockService('user-1', redis as any).acquire('document', 'doc-1');
await new EditLockService('user-1', redis as any).acquire('document', 'doc-1', 'owner-1');
// A non-holder release is a no-op and reports it did not release.
expect(await new EditLockService('user-2', redis as any).release('document', 'doc-1')).toBe(
false,
// A non-owner release is a no-op and reports it did not release.
expect(
await new EditLockService('user-1', redis as any).release('document', 'doc-1', 'owner-2'),
).toBe(false);
expect(JSON.parse(redis.store.get('editlock:document:doc-1')!)).toEqual(
expect.objectContaining({ ownerId: 'owner-1' }),
);
expect(redis.store.get('editlock:document:doc-1')).toBe('user-1');
// The holder can release, and reports the lock was actually freed.
expect(await new EditLockService('user-1', redis as any).release('document', 'doc-1')).toBe(
true,
);
// The owner can release, and reports the lock was actually freed.
expect(
await new EditLockService('user-1', redis as any).release('document', 'doc-1', 'owner-1'),
).toBe(true);
expect(redis.store.has('editlock:document:doc-1')).toBe(false);
});
it('refuses to release when a stranger replays the broadcast ownerId', async () => {
const redis = makeFakeRedis();
await new EditLockService('user-1', redis as any).acquire('document', 'doc-1', 'owner-1');
expect(
await new EditLockService('user-2', redis as any).release('document', 'doc-1', 'owner-1'),
).toBe(false);
expect(JSON.parse(redis.store.get('editlock:document:doc-1')!)).toEqual(
expect.objectContaining({ ownerId: 'owner-1', userId: 'user-1' }),
);
});
it('requires a matching owner id for owner-scoped writes', async () => {
const redis = makeFakeRedis();
const svc = new EditLockService('user-1', redis as any);
await svc.acquire('document', 'doc-1', 'owner-1');
await expect(svc.canWrite('document', 'doc-1', 'owner-1')).resolves.toBe(true);
await expect(svc.canWrite('document', 'doc-1', 'owner-2')).resolves.toBe(false);
await expect(svc.canWrite('document', 'doc-1')).resolves.toBe(false);
redis.store.delete('editlock:document:doc-1');
await expect(svc.canWrite('document', 'doc-1', 'owner-1')).resolves.toBe(false);
await expect(svc.canWrite('document', 'doc-1')).resolves.toBe(true);
});
it('refuses canWrite when a stranger replays the broadcast ownerId', async () => {
const redis = makeFakeRedis();
await new EditLockService('user-1', redis as any).acquire('document', 'doc-1', 'owner-1');
const stranger = new EditLockService('user-2', redis as any);
await expect(stranger.canWrite('document', 'doc-1', 'owner-1')).resolves.toBe(false);
});
it('degrades to unlocked / no-op when Redis is unavailable', async () => {
const svc = new EditLockService('user-1', null);
@@ -120,6 +254,7 @@ describe('EditLockService', () => {
expiresAt: null,
holderId: null,
lockedByOther: false,
ownerId: null,
});
expect(await svc.getBlockingHolder('document', 'doc-1')).toBeNull();
await expect(svc.release('document', 'doc-1')).resolves.toBe(false);
@@ -140,9 +275,11 @@ describe('EditLockService', () => {
expiresAt: null,
holderId: null,
lockedByOther: false,
ownerId: null,
});
expect(await svc.getActiveHolder('document', 'doc-1')).toBeUndefined();
expect(await svc.getBlockingHolder('document', 'doc-1')).toBeNull();
await expect(svc.canWrite('document', 'doc-1', 'owner-1')).resolves.toBe(true);
await expect(svc.release('document', 'doc-1')).resolves.toBe(false);
});
});
+150 -21
View File
@@ -18,21 +18,69 @@ export interface EditLockResult {
holderId: string | null;
/** True when another user holds the lock (caller is locked out). */
lockedByOther: boolean;
/** The edit-session id currently holding the lock, or null for legacy/unlocked. */
ownerId: string | null;
}
const UNLOCKED: EditLockResult = { expiresAt: null, holderId: null, lockedByOther: false };
export interface ActiveEditLock {
expiresAt: Date | null;
ownerId: string | null;
userId: string;
}
const UNLOCKED: EditLockResult = {
expiresAt: null,
holderId: null,
lockedByOther: false,
ownerId: null,
};
const lockKey = (type: EditLockResourceType, id: string) => `editlock:${type}:${id}`;
// Release only if the caller still holds the lock (compare-and-delete), so a
// stale releaser can't drop a lease another member has since taken over.
// stale releaser can't drop a lease another member has since taken over. The
// ownerId is broadcast on lock.changed, so it can't be used as a capability on
// its own — we also bind to the caller's userId (ARGV[2]) so a stranger who
// learned the ownerId from a broadcast cannot release another member's lock.
const RELEASE_SCRIPT = `
if redis.call('get', KEYS[1]) == ARGV[1] then
local raw = redis.call('get', KEYS[1])
if not raw then
return 0
end
if raw == ARGV[2] then
return redis.call('del', KEYS[1])
end
local ok, decoded = pcall(cjson.decode, raw)
if ok and decoded["userId"] == ARGV[2] and decoded["ownerId"] == ARGV[1] then
return redis.call('del', KEYS[1])
end
return 0
`;
const parseStoredLock = (raw: string): ActiveEditLock => {
try {
const parsed = JSON.parse(raw) as {
expiresAt?: unknown;
ownerId?: unknown;
userId?: unknown;
};
if (typeof parsed.userId === 'string') {
const expiresAt = typeof parsed.expiresAt === 'string' ? new Date(parsed.expiresAt) : null;
return {
expiresAt: expiresAt && !Number.isNaN(expiresAt.getTime()) ? expiresAt : null,
ownerId: typeof parsed.ownerId === 'string' ? parsed.ownerId : null,
userId: parsed.userId,
};
}
} catch {
// Existing deployments may still have raw user-id values in Redis. Treat
// them as legacy locks so rolling deploys do not temporarily unlock pages.
}
return { expiresAt: null, ownerId: null, userId: raw };
};
/**
* Redis-backed collaborative edit lock, keyed by (resourceType, resourceId).
*
@@ -73,28 +121,49 @@ export class EditLockService {
* Acquire the lock when it is free (or already mine), refreshing the lease;
* otherwise report whoever currently holds it. Doubles as the heartbeat.
*/
async acquire(type: EditLockResourceType, id: string): Promise<EditLockResult> {
async acquire(
type: EditLockResourceType,
id: string,
ownerId = this.userId,
): Promise<EditLockResult> {
const redis = this.redis;
if (!redis) return UNLOCKED;
const key = lockKey(type, id);
try {
const nextLock = this.serialize(ownerId);
// Claim only when the key is absent (NX). The TTL gives automatic expiry, so
// a hard-closed tab frees the lock without any cleanup job.
const claimed = await redis.set(key, this.userId, 'EX', EDIT_LOCK_TTL_SECONDS, 'NX');
if (claimed) return this.held();
const claimed = await redis.set(key, nextLock, 'EX', EDIT_LOCK_TTL_SECONDS, 'NX');
if (claimed) return this.held(ownerId);
const holder = await redis.get(key);
if (holder === this.userId) {
// Already mine — refresh the lease (heartbeat).
await redis.set(key, this.userId, 'EX', EDIT_LOCK_TTL_SECONDS);
return this.held();
const raw = await redis.get(key);
if (raw) {
const holder = parseStoredLock(raw);
// Owner-only matches are unsafe: ownerId is fanned out on lock.changed,
// so a workspace member could echo a stranger's ownerId back to steal
// the lock. Bind ownership to the calling userId. When the same user
// shows up with a different ownerId (refresh, crashed tab, HMR), the
// old session is almost certainly a ghost — silently take over with
// the new owner rather than telling the user they're editing in
// another tab. Two truly concurrent tabs will keep flipping the owner
// on their own heartbeats — that's CRDT territory, not ours to police.
if (holder.userId === this.userId) {
await redis.set(key, nextLock, 'EX', EDIT_LOCK_TTL_SECONDS);
return this.held(ownerId);
}
return {
expiresAt: holder.expiresAt,
holderId: holder.userId,
lockedByOther: true,
ownerId: holder.ownerId,
};
}
if (holder) return { expiresAt: null, holderId: holder, lockedByOther: true };
// Freed between the NX and the GET — try once more.
const reclaimed = await redis.set(key, this.userId, 'EX', EDIT_LOCK_TTL_SECONDS, 'NX');
return reclaimed ? this.held() : UNLOCKED;
const reclaimed = await redis.set(key, nextLock, 'EX', EDIT_LOCK_TTL_SECONDS, 'NX');
return reclaimed ? this.held(ownerId) : UNLOCKED;
} catch (error) {
// Fail-open: a Redis outage (configured but unreachable) must never block
// editing — report unlocked rather than surfacing the command rejection.
@@ -105,11 +174,16 @@ export class EditLockService {
/** Current holder of the lock, or undefined when unlocked / Redis is down. */
async getActiveHolder(type: EditLockResourceType, id: string): Promise<string | undefined> {
return (await this.getActiveLock(type, id))?.userId;
}
/** Current lock payload, or undefined when unlocked / Redis is down. */
async getActiveLock(type: EditLockResourceType, id: string): Promise<ActiveEditLock | undefined> {
const redis = this.redis;
if (!redis) return undefined;
try {
const holder = await redis.get(lockKey(type, id));
return holder ?? undefined;
return holder ? parseStoredLock(holder) : undefined;
} catch (error) {
// Fail-open: a Redis outage must not turn the write guards into 500s.
log('getActiveHolder failed for %s:%s %O', type, id, error);
@@ -121,9 +195,49 @@ export class EditLockService {
* The holder when someone *other* than the caller holds the lock, else null.
* Used by write guards; returns null when Redis is down (fail-open).
*/
async getBlockingHolder(type: EditLockResourceType, id: string): Promise<string | null> {
const holder = await this.getActiveHolder(type, id);
return holder && holder !== this.userId ? holder : null;
async getBlockingHolder(
type: EditLockResourceType,
id: string,
ownerId?: string,
): Promise<string | null> {
const holder = await this.getActiveLock(type, id);
if (!holder) return null;
// ownerId is broadcast on lock.changed; it can't authorize on its own.
// Bind to userId first, then keep the stale-tab guard (same user, different
// active ownerId still blocks so a ghost tab can't save over a newer one).
if (holder.userId !== this.userId) return holder.userId;
if (holder.ownerId && holder.ownerId !== ownerId) return holder.userId;
return null;
}
/**
* Validate a content write against the current lease. When a caller provides
* an owner id, the active Redis lock must still belong to that owner; otherwise
* a stale tab whose lease expired could save over a newer editor. Without an
* owner id, this preserves the advisory-lock behavior: writes are allowed only
* when no modern owner-scoped lock is active (legacy same-user locks remain
* compatible during rolling deploys).
*/
async canWrite(type: EditLockResourceType, id: string, ownerId?: string): Promise<boolean> {
const redis = this.redis;
if (!redis) return true;
try {
const raw = await redis.get(lockKey(type, id));
if (!raw) return !ownerId;
const holder = parseStoredLock(raw);
// ownerId is broadcast on lock.changed; matching it alone isn't proof of
// ownership. Bind the write to the calling userId before honoring the
// owner-scoped match.
if (holder.userId !== this.userId) return false;
if (holder.ownerId) return holder.ownerId === ownerId;
return true;
} catch (error) {
log('canWrite failed for %s:%s %O', type, id, error);
return true;
}
}
/**
@@ -132,10 +246,16 @@ export class EditLockService {
* the lease had already expired or another member has since taken it over, so
* callers can avoid broadcasting a bogus "unlocked" event.
*/
async release(type: EditLockResourceType, id: string): Promise<boolean> {
async release(type: EditLockResourceType, id: string, ownerId = this.userId): Promise<boolean> {
if (!this.redis) return false;
try {
const deleted = await this.redis.eval(RELEASE_SCRIPT, 1, lockKey(type, id), this.userId);
const deleted = await this.redis.eval(
RELEASE_SCRIPT,
1,
lockKey(type, id),
ownerId,
this.userId,
);
return deleted === 1;
} catch (error) {
log('release failed for %s:%s %O', type, id, error);
@@ -143,11 +263,20 @@ export class EditLockService {
}
}
private held(): EditLockResult {
private held(ownerId: string): EditLockResult {
return {
expiresAt: new Date(Date.now() + EDIT_LOCK_TTL_SECONDS * 1000),
holderId: this.userId,
lockedByOther: false,
ownerId,
};
}
private serialize(ownerId: string): string {
return JSON.stringify({
expiresAt: new Date(Date.now() + EDIT_LOCK_TTL_SECONDS * 1000).toISOString(),
ownerId,
userId: this.userId,
});
}
}
@@ -2,6 +2,7 @@ import { AgentDocumentsExecutionRuntime } from '@lobechat/builtin-tool-agent-doc
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { TaskModel } from '@/database/models/task';
import { WorkspaceModel } from '@/database/models/workspace';
import { AgentDocumentsService } from '@/server/services/agentDocuments';
import { agentDocumentsRuntime } from '../agentDocuments';
@@ -12,7 +13,11 @@ const agentDocumentToolOutcomeMocks = vi.hoisted(() => ({
vi.mock('@/server/services/agentDocuments');
vi.mock('@/database/models/task');
vi.mock('@/database/models/workspace');
vi.mock('@/server/services/agentDocuments/toolOutcome', () => agentDocumentToolOutcomeMocks);
vi.mock('@/envs/app', () => ({
appEnv: { APP_URL: 'https://app.example.com' },
}));
describe('agentDocumentsRuntime', () => {
it('should have correct identifier', () => {
@@ -48,6 +53,7 @@ describe('agentDocumentsRuntime auto-pin to task', () => {
renameDocumentById: ReturnType<typeof vi.fn>;
};
let pinDocument: ReturnType<typeof vi.fn>;
let findWorkspaceById: ReturnType<typeof vi.fn>;
beforeEach(() => {
agentDocumentToolOutcomeMocks.emitAgentDocumentToolOutcomeSafely.mockClear();
@@ -59,12 +65,14 @@ describe('agentDocumentsRuntime auto-pin to task', () => {
renameDocumentById: vi.fn().mockResolvedValue(newDoc),
};
pinDocument = vi.fn().mockResolvedValue(undefined);
findWorkspaceById = vi.fn().mockResolvedValue({ slug: 'lobe-team' });
vi.mocked(AgentDocumentsService).mockImplementation(() => serviceImpl as any);
vi.mocked(TaskModel).mockImplementation(() => ({ pinDocument }) as any);
vi.mocked(WorkspaceModel).mockImplementation(() => ({ findById: findWorkspaceById }) as any);
});
const buildContext = (taskId?: string) => {
const buildContext = (taskId?: string, workspaceId?: string) => {
// Mock the workspace lookup chain that `pinToTask` runs against the task
// row. Returning `workspaceId: null` reproduces personal-mode behavior.
const limit = vi.fn().mockResolvedValue([{ workspaceId: null }]);
@@ -76,6 +84,7 @@ describe('agentDocumentsRuntime auto-pin to task', () => {
taskId,
toolManifestMap: {},
userId: 'user-1',
workspaceId,
};
};
@@ -187,6 +196,32 @@ describe('agentDocumentsRuntime auto-pin to task', () => {
expect(pinDocument).not.toHaveBeenCalled();
});
it('includes the workspace slug in generated document URLs', async () => {
const runtime = agentDocumentsRuntime.factory(buildContext(undefined, 'workspace-1'));
const result = await runtime.createDocument(
{ content: 'body', title: 'Daily Brief' },
{ agentId: 'agent-1' },
);
expect(findWorkspaceById).toHaveBeenCalledWith('workspace-1');
expect(result.content).toContain(
'https://app.example.com/lobe-team/agent/agent-1/docs/documents-row-id',
);
});
it('omits document URLs for workspace-scoped runs when the workspace slug cannot be resolved', async () => {
findWorkspaceById.mockResolvedValueOnce(undefined);
const runtime = agentDocumentsRuntime.factory(buildContext(undefined, 'workspace-1'));
const result = await runtime.createDocument(
{ content: 'body', title: 'Daily Brief' },
{ agentId: 'agent-1' },
);
expect(result.content).toBe('Created document "Daily Brief" (agent-doc-assoc-id).');
});
});
describe('AgentDocumentsExecutionRuntime.createDocument', () => {
@@ -226,6 +261,31 @@ describe('AgentDocumentsExecutionRuntime.createDocument', () => {
});
});
it('includes a document URL when a URL builder is configured', async () => {
const stub = makeStub();
stub.createDocument.mockResolvedValue({
documentId: 'docs_document-row-id',
filename: 'daily-brief',
id: 'agent-doc-assoc-id',
title: 'Daily Brief',
});
const runtime = new AgentDocumentsExecutionRuntime(stub, {
getDocumentUrl: ({ agentId, documentId }) =>
`https://app.example.com/agent/${agentId}/docs/${documentId}`,
});
const result = await runtime.createDocument(
{ content: 'body', title: 'Daily Brief' },
{ agentId: 'agent-1' },
);
expect(result.success).toBe(true);
expect(result.content).toContain(
'https://app.example.com/agent/agent-1/docs/docs_document-row-id',
);
expect(result.content).toContain('Use id agent-doc-assoc-id for further edits');
});
it('refuses to run without agentId', async () => {
const stub = makeStub();
const runtime = new AgentDocumentsExecutionRuntime(stub);
@@ -26,7 +26,10 @@ describe('agentDocumentsRuntime', () => {
});
const result = await runtime.listDocuments({}, { agentId: 'agent-1' });
expect(listDocuments).toHaveBeenCalledWith('agent-1', 'all');
// The agent runtime opts into seeing the archived `.tool-results`.
expect(listDocuments).toHaveBeenCalledWith('agent-1', 'all', {
includeArchivedToolResults: true,
});
expect(result).toEqual({
content: JSON.stringify([
{ filename: 'rules.md', id: 'doc-1', title: 'Rules' },
@@ -1,15 +1,28 @@
import type { DocumentLoadRule } from '@lobechat/agent-templates';
import { AgentDocumentsIdentifier } from '@lobechat/builtin-tool-agent-documents';
import {
AgentDocumentsIdentifier,
buildAgentDocumentUrl,
} from '@lobechat/builtin-tool-agent-documents';
import { AgentDocumentsExecutionRuntime } from '@lobechat/builtin-tool-agent-documents/executionRuntime';
import { eq } from 'drizzle-orm';
import { TaskModel } from '@/database/models/task';
import { WorkspaceModel } from '@/database/models/workspace';
import { tasks } from '@/database/schemas';
import { appEnv } from '@/envs/app';
import { AgentDocumentsService } from '@/server/services/agentDocuments';
import { emitAgentDocumentToolOutcomeSafely } from '@/server/services/agentDocuments/toolOutcome';
import { type ServerRuntimeRegistration } from './types';
const getAgentDocumentAppUrl = (): string | undefined => {
try {
return appEnv.APP_URL;
} catch {
return process.env.APP_URL;
}
};
export const agentDocumentsRuntime: ServerRuntimeRegistration = {
factory: (context) => {
if (!context.userId || !context.serverDB) {
@@ -20,6 +33,7 @@ export const agentDocumentsRuntime: ServerRuntimeRegistration = {
const userId = context.userId;
const service = new AgentDocumentsService(db, userId, context.workspaceId);
const { taskId } = context;
let workspaceSlugPromise: Promise<string | undefined> | undefined;
const emitDocumentOutcome = async (input: {
agentId?: string;
agentDocumentId?: string;
@@ -109,136 +123,168 @@ export const agentDocumentsRuntime: ServerRuntimeRegistration = {
return doc;
};
return new AgentDocumentsExecutionRuntime({
copyDocument: async ({ agentId, id, newTitle }) =>
pinToTask(
await withDocumentOutcome(
{
agentId,
apiName: 'copyDocument',
getAgentDocumentId: (result) => result?.id,
relation: 'created',
summary: 'Agent documents copied a document.',
toolAction: 'copy',
},
() => service.copyDocumentById(id, newTitle, agentId),
),
),
createDocument: async ({ agentId, content, hintIsSkill, title }) =>
pinToTask(
await withDocumentOutcome(
{
agentId,
apiName: 'createDocument',
getAgentDocumentId: (result) => result?.id,
hintIsSkill,
relation: 'created',
summary: 'Agent documents created a document.',
toolAction: 'create',
},
() => service.createDocument(agentId, title, content, { hintIsSkill }),
),
),
createTopicDocument: async ({ agentId, content, hintIsSkill, title, topicId }) =>
pinToTask(
await withDocumentOutcome(
{
agentId,
apiName: 'createTopicDocument',
getAgentDocumentId: (result) => result?.id,
hintIsSkill,
relation: 'created',
summary: 'Agent documents created a topic document.',
toolAction: 'create',
},
() => service.createForTopic(agentId, title, content, topicId, { hintIsSkill }),
),
),
listDocuments: async ({ agentId, sourceType }) => {
const docs = await service.listDocuments(agentId, sourceType);
return docs.map((d) => ({
documentId: d.documentId,
filename: d.filename,
id: d.id,
title: d.title,
}));
},
listTopicDocuments: async ({ agentId, sourceType, topicId }) => {
const docs = await service.listDocumentsForTopic(agentId, topicId, sourceType);
return docs.map((d) => ({
documentId: d.documentId,
filename: d.filename,
id: d.id,
title: d.title,
}));
},
modifyNodes: ({ agentId, id, operations }) =>
withDocumentOutcome(
{
agentId,
apiName: 'modifyNodes',
getAgentDocumentId: () => id,
relation: 'updated',
summary: 'Agent documents modified document nodes.',
toolAction: 'edit',
},
() => service.modifyDocumentNodesById(id, operations, agentId),
),
readDocument: ({ agentId, id }) => service.getDocumentSnapshotById(id, agentId),
removeDocument: ({ agentId, id }) =>
withDocumentOutcome(
{
agentId,
apiName: 'removeDocument',
getAgentDocumentId: () => id,
relation: 'removed',
summary: 'Agent documents removed a document.',
toolAction: 'remove',
},
() => service.removeDocumentById(id, agentId),
),
renameDocument: ({ agentId, id, newTitle }) =>
withDocumentOutcome(
{
agentId,
apiName: 'renameDocument',
getAgentDocumentId: () => id,
relation: 'updated',
summary: 'Agent documents renamed a document.',
toolAction: 'rename',
},
() => service.renameDocumentById(id, newTitle, agentId),
),
replaceDocumentContent: ({ agentId, content, id }) =>
withDocumentOutcome(
{
agentId,
apiName: 'replaceDocumentContent',
getAgentDocumentId: () => id,
relation: 'updated',
summary: 'Agent documents replaced document content.',
toolAction: 'replace',
},
() => service.replaceDocumentContentById(id, content, agentId),
),
updateLoadRule: ({ agentId, id, rule }) =>
withDocumentOutcome(
{
agentId,
apiName: 'updateLoadRule',
getAgentDocumentId: () => id,
relation: 'updated',
summary: 'Agent documents updated a load rule.',
toolAction: 'update',
},
() =>
service.updateLoadRuleById(
id,
{ ...rule, rule: rule.rule as DocumentLoadRule | undefined },
agentId,
const resolveWorkspaceSlugForUrl = async (): Promise<string | undefined> => {
if (!context.workspaceId) return undefined;
workspaceSlugPromise ??= new WorkspaceModel(db, userId)
.findById(context.workspaceId)
.then((workspace) => workspace?.slug)
.catch((error) => {
console.error('[agentDocumentsRuntime] Failed to resolve workspace slug:', error);
return undefined;
});
return workspaceSlugPromise;
};
return new AgentDocumentsExecutionRuntime(
{
copyDocument: async ({ agentId, id, newTitle }) =>
pinToTask(
await withDocumentOutcome(
{
agentId,
apiName: 'copyDocument',
getAgentDocumentId: (result) => result?.id,
relation: 'created',
summary: 'Agent documents copied a document.',
toolAction: 'copy',
},
() => service.copyDocumentById(id, newTitle, agentId),
),
),
});
),
createDocument: async ({ agentId, content, hintIsSkill, title }) =>
pinToTask(
await withDocumentOutcome(
{
agentId,
apiName: 'createDocument',
getAgentDocumentId: (result) => result?.id,
hintIsSkill,
relation: 'created',
summary: 'Agent documents created a document.',
toolAction: 'create',
},
() => service.createDocument(agentId, title, content, { hintIsSkill }),
),
),
createTopicDocument: async ({ agentId, content, hintIsSkill, title, topicId }) =>
pinToTask(
await withDocumentOutcome(
{
agentId,
apiName: 'createTopicDocument',
getAgentDocumentId: (result) => result?.id,
hintIsSkill,
relation: 'created',
summary: 'Agent documents created a topic document.',
toolAction: 'create',
},
() => service.createForTopic(agentId, title, content, topicId, { hintIsSkill }),
),
),
listDocuments: async ({ agentId, sourceType }) => {
// Agents discover archived tool results via this path (see
// `excludeArchivedToolResults`), so keep the `.tool-results` archive visible.
const docs = await service.listDocuments(agentId, sourceType, {
includeArchivedToolResults: true,
});
return docs.map((d) => ({
documentId: d.documentId,
filename: d.filename,
id: d.id,
title: d.title,
}));
},
listTopicDocuments: async ({ agentId, sourceType, topicId }) => {
const docs = await service.listDocumentsForTopic(agentId, topicId, sourceType, {
includeArchivedToolResults: true,
});
return docs.map((d) => ({
documentId: d.documentId,
filename: d.filename,
id: d.id,
title: d.title,
}));
},
modifyNodes: ({ agentId, id, operations }) =>
withDocumentOutcome(
{
agentId,
apiName: 'modifyNodes',
getAgentDocumentId: () => id,
relation: 'updated',
summary: 'Agent documents modified document nodes.',
toolAction: 'edit',
},
() => service.modifyDocumentNodesById(id, operations, agentId),
),
readDocument: ({ agentId, id }) => service.getDocumentSnapshotById(id, agentId),
removeDocument: ({ agentId, id }) =>
withDocumentOutcome(
{
agentId,
apiName: 'removeDocument',
getAgentDocumentId: () => id,
relation: 'removed',
summary: 'Agent documents removed a document.',
toolAction: 'remove',
},
() => service.removeDocumentById(id, agentId),
),
renameDocument: ({ agentId, id, newTitle }) =>
withDocumentOutcome(
{
agentId,
apiName: 'renameDocument',
getAgentDocumentId: () => id,
relation: 'updated',
summary: 'Agent documents renamed a document.',
toolAction: 'rename',
},
() => service.renameDocumentById(id, newTitle, agentId),
),
replaceDocumentContent: ({ agentId, content, id }) =>
withDocumentOutcome(
{
agentId,
apiName: 'replaceDocumentContent',
getAgentDocumentId: () => id,
relation: 'updated',
summary: 'Agent documents replaced document content.',
toolAction: 'replace',
},
() => service.replaceDocumentContentById(id, content, agentId),
),
updateLoadRule: ({ agentId, id, rule }) =>
withDocumentOutcome(
{
agentId,
apiName: 'updateLoadRule',
getAgentDocumentId: () => id,
relation: 'updated',
summary: 'Agent documents updated a load rule.',
toolAction: 'update',
},
() =>
service.updateLoadRuleById(
id,
{ ...rule, rule: rule.rule as DocumentLoadRule | undefined },
agentId,
),
),
},
{
getDocumentUrl: async ({ agentId, documentId }) => {
const workspaceSlug = await resolveWorkspaceSlugForUrl();
if (context.workspaceId && !workspaceSlug) return undefined;
return buildAgentDocumentUrl(getAgentDocumentAppUrl(), agentId, documentId, {
workspaceSlug,
});
},
},
);
},
identifier: AgentDocumentsIdentifier,
};
@@ -195,80 +195,95 @@ const withEditor = async (
throw new Error('documentId is required');
}
const snapshot = await loadSnapshot(documentModel, documentId);
const env = buildEnv(snapshot, documentId);
const exportEditorData = options.exportEditorData !== false;
const persist = options.persist !== false;
const invariantCheck = options.invariantCheck !== false;
try {
const beforeHash = exportEditorData
? hashEditorData(env.headless.export().editorData)
: undefined;
// Acquire the collaborative edit lock around the entire read-modify-write so
// the agent reads, mutates and persists atomically: serialized against other
// workspace members and rejected (CONFLICT) when someone else is actively
// editing, instead of silently clobbering their work. Read-only invocations
// (persist: false) never write, so they skip the lock.
const run = async (): Promise<HandlerOutput> => {
const snapshot = await loadSnapshot(documentModel, documentId);
const env = buildEnv(snapshot, documentId);
const handlerResult = await handler(env);
try {
const beforeHash = exportEditorData
? hashEditorData(env.headless.export().editorData)
: undefined;
const exported = exportEditorData ? env.headless.export() : undefined;
const afterHash = exported ? hashEditorData(exported.editorData) : undefined;
const titleChanged = env.getTitle() !== snapshot.title;
const editorChanged = exportEditorData && beforeHash !== undefined && beforeHash !== afterHash;
const handlerResult = await handler(env);
const invariantViolation = invariantCheck
? detectInvariantViolation(apiName, {
editorChanged,
handlerReportedChange: detectHandlerReportedChange(apiName, handlerResult.state),
titleChanged,
})
: undefined;
const exported = exportEditorData ? env.headless.export() : undefined;
const afterHash = exported ? hashEditorData(exported.editorData) : undefined;
const titleChanged = env.getTitle() !== snapshot.title;
const editorChanged =
exportEditorData && beforeHash !== undefined && beforeHash !== afterHash;
if (invariantViolation) {
console.warn(
`[PageAgentServerRuntime] invariant violation in ${apiName}:`,
invariantViolation,
{ documentId, operationId: ctx.operationId, toolCallId: ctx.toolCallId },
);
const invariantViolation = invariantCheck
? detectInvariantViolation(apiName, {
editorChanged,
handlerReportedChange: detectHandlerReportedChange(apiName, handlerResult.state),
titleChanged,
})
: undefined;
if (invariantViolation) {
console.warn(
`[PageAgentServerRuntime] invariant violation in ${apiName}:`,
invariantViolation,
{ documentId, operationId: ctx.operationId, toolCallId: ctx.toolCallId },
);
}
const patch: {
content?: string;
editorData?: Record<string, unknown>;
title?: string;
} = {};
if (exported) {
patch.content = exported.markdown;
patch.editorData = exported.editorData as unknown as Record<string, unknown>;
}
if (titleChanged) {
patch.title = env.getTitle();
}
if (persist && Object.keys(patch).length > 0) {
await documentService.updateDocument(documentId, {
content: patch.content,
editorData: patch.editorData,
saveSource: 'llm_call',
title: patch.title,
});
}
return {
content: handlerResult.content,
state: {
...handlerResult.state,
documentContent: patch.content,
documentEditorData: patch.editorData,
documentTitle: env.getTitle(),
...(invariantViolation ? { invariantViolation } : {}),
},
};
} finally {
env.headless.destroy();
}
};
const patch: {
content?: string;
editorData?: Record<string, unknown>;
title?: string;
} = {};
if (exported) {
patch.content = exported.markdown;
patch.editorData = exported.editorData as unknown as Record<string, unknown>;
}
if (titleChanged) {
patch.title = env.getTitle();
}
if (persist && Object.keys(patch).length > 0) {
await documentService.updateDocument(documentId, {
content: patch.content,
editorData: patch.editorData,
saveSource: 'llm_call',
title: patch.title,
});
}
return {
content: handlerResult.content,
state: {
...handlerResult.state,
documentContent: patch.content,
documentEditorData: patch.editorData,
documentTitle: env.getTitle(),
...(invariantViolation ? { invariantViolation } : {}),
},
};
} finally {
env.headless.destroy();
}
return persist ? documentService.runWithDocumentLock(documentId, run) : run();
};
const buildService = (db: LobeChatDatabase, userId: string): PageAgentRuntimeService => {
const documentModel = new DocumentModel(db, userId);
const documentService = new DocumentService(db, userId);
const buildService = (
db: LobeChatDatabase,
userId: string,
workspaceId?: string,
): PageAgentRuntimeService => {
const documentModel = new DocumentModel(db, userId, workspaceId);
const documentService = new DocumentService(db, userId, workspaceId);
const serviceCtx: PageAgentServiceContext = { documentModel, documentService };
return {
@@ -393,7 +408,9 @@ export const pageAgentRuntime: ServerRuntimeRegistration = {
if (!context.userId || !context.serverDB) {
throw new Error('userId and serverDB are required for Page Agent execution');
}
return new PageAgentExecutionRuntime(buildService(context.serverDB, context.userId));
return new PageAgentExecutionRuntime(
buildService(context.serverDB, context.userId, context.workspaceId),
);
},
identifier: PageAgentIdentifier,
};
+5
View File
@@ -1,4 +1,9 @@
[
{
"children": {},
"date": "2026-06-17",
"version": "2.2.6"
},
{
"children": {},
"date": "2026-05-29",
+18
View File
@@ -44,6 +44,7 @@
"builtinCopilot": "المساعد المدمج",
"chatList.expandMessage": "توسيع الرسالة",
"chatList.longMessageDetail": "عرض التفاصيل",
"chatList.refreshing": "جلب أحدث الرسائل...",
"chatMode.agent": "وكيل",
"chatMode.agentCap.env": "بيئة التشغيل",
"chatMode.agentCap.files": "الوصول إلى الملفات",
@@ -164,6 +165,7 @@
"extendParams.urlContext.title": "استخراج محتوى رابط الويب",
"followUpPlaceholder": "متابعة. @ لإسناد مهام لوكلاء آخرين.",
"followUpPlaceholderHeterogeneous": "تابع.",
"gatewayMode.title": "وضع البوابة",
"group.desc": "ادفع المهمة للأمام مع عدة وكلاء في مساحة مشتركة واحدة.",
"group.memberTooltip": "يوجد {{count}} عضو في المجموعة",
"group.orchestratorThinking": "المنسق يفكر...",
@@ -245,6 +247,7 @@
"inbox.title": "Lobe AI",
"input.addAi": "إضافة رسالة من الذكاء الاصطناعي",
"input.addUser": "إضافة رسالة من المستخدم",
"input.agentModeUnsupportedModel": "النموذج الحالي لا يدعم استدعاء الأدوات الوكيلية. قم بالتبديل إلى نموذج يدعم الوكيل للحصول على أفضل تجربة.",
"input.costEstimate.creditsPerMillionTokens": "{{credits}} ائتمان/مليون رموز",
"input.costEstimate.hint": "التكلفة المقدرة: ~{{credits}} ائتمان",
"input.costEstimate.inputLabel": "الإدخال",
@@ -332,6 +335,7 @@
"messages.modelCard.pricing.outputTokens": "المخرجات {{amount}} أرصدة · ${{amount}}/مليون",
"messages.modelCard.pricing.writeCacheInputTokens": "كتابة إلى التخزين المؤقت {{amount}} أرصدة · ${{amount}}/مليون",
"messages.tokenDetails.average": "متوسط السعر للوحدة",
"messages.tokenDetails.cacheRate": "معدل التخزين المؤقت",
"messages.tokenDetails.input": "المدخلات",
"messages.tokenDetails.inputAudio": "مدخل صوتي",
"messages.tokenDetails.inputCached": "مدخل مخزن مؤقتًا",
@@ -1093,7 +1097,21 @@
"workingPanel.review.viewMode.unified": "التبديل إلى العرض الموحد",
"workingPanel.review.wordWrap.disable": "تعطيل التفاف النص",
"workingPanel.review.wordWrap.enable": "تمكين التفاف النص",
"workingPanel.skills.actions.comingSoon": "قريبًا",
"workingPanel.skills.actions.delete": "حذف",
"workingPanel.skills.actions.rename": "إعادة تسمية",
"workingPanel.skills.actions.view": "عرض",
"workingPanel.skills.delete.agentConfirm": "إزالة المهارة “{{name}}” من هذا الوكيل؟ لا يمكن التراجع عن ذلك.",
"workingPanel.skills.delete.error": "فشل في حذف المهارة",
"workingPanel.skills.delete.success": "تم حذف المهارة",
"workingPanel.skills.delete.title": "حذف المهارة؟",
"workingPanel.skills.delete.userConfirm": "إلغاء تثبيت المهارة “{{name}}”؟ لا يمكن التراجع عن ذلك.",
"workingPanel.skills.detail.title": "تفاصيل المهارة",
"workingPanel.skills.empty": "لم يتم العثور على مهارات في هذا المشروع",
"workingPanel.skills.rename.action": "إعادة تسمية",
"workingPanel.skills.rename.error": "فشل في إعادة تسمية المهارة",
"workingPanel.skills.rename.placeholder": "اسم المهارة",
"workingPanel.skills.rename.title": "إعادة تسمية المهارة",
"workingPanel.skills.section.agent": "مهارات الوكيل",
"workingPanel.skills.section.project": "مهارات المشروع",
"workingPanel.skills.section.user": "مهارات المستخدم",
+17
View File
@@ -1,4 +1,20 @@
{
"fleet.addColumn": "إضافة عمود",
"fleet.allShown": "تم عرض جميع المهام الجارية",
"fleet.backToHome": "العودة إلى الصفحة الرئيسية",
"fleet.closeColumn": "إغلاق العمود",
"fleet.createTask": "إنشاء مهمة",
"fleet.empty": "لا توجد مهام مفتوحة",
"fleet.emptyDesc": "اختر مهمة جارية على اليسار، أو استخدم + لإضافة عمود.",
"fleet.noRunningTasks": "لا توجد مهام جارية",
"fleet.openInChat": "فتح في الدردشة",
"fleet.reply": "رد",
"fleet.runningTasks": "المهام الجارية",
"fleet.status.idle": "خامل",
"fleet.status.paused": "متوقف مؤقتًا",
"fleet.status.running": "قيد التشغيل",
"fleet.status.scheduled": "مجدول",
"fleet.tooltip": "عرض جميع الوكلاء جنبًا إلى جنب",
"gateway.description": "الوصف",
"gateway.descriptionPlaceholder": "اختياري",
"gateway.deviceName": "اسم الجهاز",
@@ -26,6 +42,7 @@
"navigation.memoryIdentities": "الذاكرة - الهويات",
"navigation.memoryPreferences": "الذاكرة - التفضيلات",
"navigation.noPages": "لا توجد صفحات بعد",
"navigation.observation": "وضع المراقبة",
"navigation.onboarding": "البدء",
"navigation.page": "صفحة",
"navigation.pages": "الصفحات",
+10
View File
@@ -95,8 +95,18 @@
"pageEditor.duplicateError": "فشل في تكرار الصفحة",
"pageEditor.duplicateSuccess": "تم تكرار الصفحة بنجاح",
"pageEditor.editMode.checking": "جارٍ التحقق من توفر التعديل…",
"pageEditor.editMode.draftRestoreCancel": "تجاهل",
"pageEditor.editMode.draftRestoreContent": "تم العثور على تغييرات محلية غير محفوظة من جلستك الأخيرة. هل تريد استعادتها؟",
"pageEditor.editMode.draftRestoreOk": "استعادة",
"pageEditor.editMode.draftRestoreTitle": "استعادة المسودة غير المحفوظة",
"pageEditor.editMode.lockLostDescription": "لم تتم مزامنة التعديلات الأخيرة بعد. ستستأنف الحفظ بمجرد استعادة الاتصال.",
"pageEditor.editMode.lockLostTitle": "تم فقدان قفل التعديل مؤقتًا",
"pageEditor.editMode.lockUnstable": "إعادة الاتصال بقفل التعديل...",
"pageEditor.editMode.lockedByOther": "{{name}} يقوم بتعديل هذا المستند",
"pageEditor.editMode.lockedBySelf": "أنت تقوم بتعديل هذا المستند في علامة تبويب أخرى",
"pageEditor.editMode.lockedBySelfDescription": "سيستأنف الحفظ بعد إغلاق الجلسة الأخرى أو انتهاء صلاحية قفلها (~30 ثانية).",
"pageEditor.editMode.lockedBySomeone": "شخص آخر يقوم بتعديل هذا المستند",
"pageEditor.editMode.lockedDescription": "الصفحة للقراءة فقط أثناء تعديلهم. لن يتم حفظ تغييراتك حتى ينتهوا.",
"pageEditor.editedAt": "آخر تعديل في {{time}}",
"pageEditor.editedBy": "آخر تعديل بواسطة {{name}}",
"pageEditor.editorPlaceholder": "اضغط \"/\" للوصول إلى الذكاء الاصطناعي والأوامر",
+2 -2
View File
@@ -5,8 +5,8 @@
"features.agentSelfIteration.title": "التكرار الذاتي للوكيل",
"features.assistantMessageGroup.desc": "تجميع رسائل الوكيل ونتائج استدعاء الأدوات معًا للعرض",
"features.assistantMessageGroup.title": "تجميع رسائل الوكيل",
"features.gatewayMode.desc": "تنفيذ مهام الوكيل على الخادم عبر بوابة WebSocket بدلًا من التشغيل محليًا، مما يتيح تنفيذًا أسرع ويقلل من استهلاك موارد العميل.",
"features.gatewayMode.title": "تنفيذ الوكيل من جانب الخادم (البوابة)",
"features.fleet.desc": "عرض إدخال الأسطول في شريط العنوان — لوحة معلومات جنبًا إلى جنب لجميع المهام الجارية عبر وكلائك.",
"features.fleet.title": "عرض الأسطول",
"features.groupChat.desc": "تمكين تنسيق الدردشة الجماعية متعددة الوكلاء.",
"features.groupChat.title": "دردشة جماعية (متعددة الوكلاء)",
"features.imessage.desc": "ربط الوكلاء بـ iMessage من خلال جسر BlueBubbles المحلي لتطبيق LobeHub Desktop.",
+8 -2
View File
@@ -890,7 +890,7 @@
"skillStore.wantMore.feedback.title": "[طلب مهارة] لخّص المهارة التي تحتاجها في جملة واحدة",
"skillStore.wantMore.reachedEnd": "لقد وصلت إلى النهاية. لم تجد ما تبحث عنه؟",
"startConversation": "ابدأ المحادثة",
"storage.actions.copyAgentGroups.button": "نسخ إلى",
"storage.actions.copyAgentGroups.button": "نسخ إلى...",
"storage.actions.copyAgentGroups.desc": "انسخ مجموعات الوكلاء وأعضائها إلى مساحة عمل أخرى أو حساب شخصي.",
"storage.actions.copyAgentGroups.title": "نسخ مجموعات الوكلاء",
"storage.actions.copyLobeAI.button": "نسخ إلى",
@@ -1032,6 +1032,8 @@
"tab.addCustomSkill": "إضافة مهارة مخصصة",
"tab.advanced": "متقدم",
"tab.advanced.appUpdates.title": "تحديثات التطبيق",
"tab.advanced.gatewayMode.desc": "تشغيل مهام الوكيل المدعومة عبر بوابة السحابة افتراضيًا. يمكن للوكلاء الفرديين تجاوز هذا من قائمة الدردشة.",
"tab.advanced.gatewayMode.title": "وضع البوابة",
"tab.advanced.toolsAndDiagnostics.title": "الأدوات والتشخيصات",
"tab.advanced.updateChannel.canary": "كناري",
"tab.advanced.updateChannel.canaryDesc": "يتم تشغيله عند كل دمج PR، مع عدة إصدارات يومياً. الأكثر عدم استقراراً.",
@@ -1169,7 +1171,6 @@
"tools.builtins.uninstallConfirm.desc": "هل أنت متأكد أنك تريد إلغاء تثبيت {{name}}؟ سيتم إزالة هذه المهارة من الوكيل الحالي.",
"tools.builtins.uninstallConfirm.title": "إلغاء تثبيت {{name}}",
"tools.builtins.uninstalled": "تم إلغاء التثبيت",
"tools.disabled": "النموذج الحالي لا يدعم استدعاء الوظائف ولا يمكنه استخدام المهارة",
"tools.composio.addServer": "إضافة خادم",
"tools.composio.authCompleted": "تم التحقق من الهوية",
"tools.composio.authFailed": "فشل التحقق من الهوية",
@@ -1186,6 +1187,10 @@
"tools.composio.notEnabled": "خدمة Composio غير مفعلة",
"tools.composio.oauthRequired": "يرجى إكمال التحقق من OAuth في النافذة الجديدة",
"tools.composio.pendingAuth": "في انتظار التحقق",
"tools.composio.reauthorize": "إعادة التفويض",
"tools.composio.remove": "إزالة",
"tools.composio.removeConfirm.desc": "{{name}} سيتم إزالته نهائيًا من الخدمات المتصلة بك. لا يمكن التراجع عن هذا الإجراء.",
"tools.composio.removeConfirm.title": "إزالة {{name}}؟",
"tools.composio.serverCreated": "تم إنشاء الخادم بنجاح",
"tools.composio.serverCreatedFailed": "فشل في إنشاء الخادم",
"tools.composio.serverRemoved": "تمت إزالة الخادم",
@@ -1238,6 +1243,7 @@
"tools.composio.servers.zendesk.readme": "تكامل مع Zendesk لإدارة تذاكر الدعم وتفاعلات العملاء. أنشئ الطلبات، وحدثها، وتتبعها، وادخل إلى بيانات العملاء، وسهّل عمليات الدعم.",
"tools.composio.tools": "الأدوات",
"tools.composio.verifyAuth": "لقد أكملت التحقق",
"tools.disabled": "النموذج الحالي لا يدعم استدعاء الوظائف ولا يمكنه استخدام المهارة",
"tools.lobehubSkill.authorize": "تفويض",
"tools.lobehubSkill.connect": "اتصال",
"tools.lobehubSkill.connected": "متصل",
+15
View File
@@ -16,6 +16,7 @@
"actions.favorite": "المفضلة",
"actions.import": "استيراد المحادثة",
"actions.markCompleted": "وضع علامة كمكتمل",
"actions.moveToAgent": "نقل إلى مساعد آخر",
"actions.openInNewTab": "افتح في علامة تبويب جديدة",
"actions.openInNewWindow": "فتح في نافذة جديدة",
"actions.removeAll": "حذف جميع المواضيع",
@@ -80,6 +81,9 @@
"management.bulk.deleteConfirm": "أنت على وشك حذف {{count}} موضوعًا. لا يمكن التراجع عن هذا الإجراء.",
"management.bulk.deleteTitle": "حذف المواضيع؟",
"management.bulk.favorite": "مفضلة",
"management.bulk.move": "نقل إلى مساعد",
"management.bulk.moveEmpty": "لا يوجد مساعدون آخرون",
"management.bulk.moveSearchPlaceholder": "ابحث عن المساعدين…",
"management.bulk.selectedCount_one": "{{count}} محدد",
"management.bulk.selectedCount_other": "{{count}} محددة",
"management.card.noPreview": "لا توجد معاينة متاحة",
@@ -118,6 +122,17 @@
"management.group.noProject": "لا يوجد مشروع",
"management.group.none": "لا شيء",
"management.loadingMore": "جارٍ تحميل المزيد من المواضيع…",
"management.moveModal.back": "رجوع",
"management.moveModal.confirmContent_one": "هل تريد نقل {{count}} موضوع إلى “{{title}}”؟",
"management.moveModal.confirmContent_other": "هل تريد نقل {{count}} مواضيع إلى “{{title}}”؟",
"management.moveModal.confirmOk": "نقل",
"management.moveModal.doneOk": "تم",
"management.moveModal.done_one": "تم نقل {{count}} موضوع",
"management.moveModal.done_other": "تم نقل {{count}} مواضيع",
"management.moveModal.error": "فشل النقل، يرجى المحاولة مرة أخرى",
"management.moveModal.goToTarget": "انتقل إلى “{{title}}”",
"management.moveModal.moving": "جارٍ النقل…",
"management.moveModal.title": "نقل المواضيع",
"management.searchPlaceholder": "ابحث في مواضيع هذا الوكيل…",
"management.sidebarEntry": "المواضيع",
"management.sort.createdAt": "وقت الإنشاء",
+18
View File
@@ -44,6 +44,7 @@
"builtinCopilot": "Вграден Копилот",
"chatList.expandMessage": "Разгъни съобщението",
"chatList.longMessageDetail": "Прегледай подробности",
"chatList.refreshing": "Извличане на най-новите съобщения...",
"chatMode.agent": "Агент",
"chatMode.agentCap.env": "Работна среда",
"chatMode.agentCap.files": "Достъп до файлове",
@@ -164,6 +165,7 @@
"extendParams.urlContext.title": "Извличане на съдържание от уеб връзки",
"followUpPlaceholder": "Последващо действие. Използвайте @, за да възлагате задачи на други агенти.",
"followUpPlaceholderHeterogeneous": "Последващ въпрос.",
"gatewayMode.title": "Режим на шлюз",
"group.desc": "Придвижете задача напред с няколко Агента в едно споделено пространство.",
"group.memberTooltip": "Групата има {{count}} член(а)",
"group.orchestratorThinking": "Оркестраторът мисли...",
@@ -245,6 +247,7 @@
"inbox.title": "Lobe AI",
"input.addAi": "Добави AI съобщение",
"input.addUser": "Добави потребителско съобщение",
"input.agentModeUnsupportedModel": "Текущият модел не поддържа агентски инструменти. Превключете към модел с агентски възможности за най-добро изживяване.",
"input.costEstimate.creditsPerMillionTokens": "{{credits}} кредита/М токена",
"input.costEstimate.hint": "Оценена цена: ~{{credits}} кредита",
"input.costEstimate.inputLabel": "Вход",
@@ -332,6 +335,7 @@
"messages.modelCard.pricing.outputTokens": "Изход {{amount}} кредита · ${{amount}}/M",
"messages.modelCard.pricing.writeCacheInputTokens": "Кеш запис {{amount}} кредита · ${{amount}}/M",
"messages.tokenDetails.average": "Средна единична цена",
"messages.tokenDetails.cacheRate": "Скорост на кеширане",
"messages.tokenDetails.input": "Вход",
"messages.tokenDetails.inputAudio": "Аудио вход",
"messages.tokenDetails.inputCached": "Кеширан вход",
@@ -1093,7 +1097,21 @@
"workingPanel.review.viewMode.unified": "Превключете към обединен изглед",
"workingPanel.review.wordWrap.disable": "Деактивирай пренасяне на думи",
"workingPanel.review.wordWrap.enable": "Активирай пренасяне на думи",
"workingPanel.skills.actions.comingSoon": "Очаквайте скоро",
"workingPanel.skills.actions.delete": "Изтриване",
"workingPanel.skills.actions.rename": "Преименуване",
"workingPanel.skills.actions.view": "Преглед",
"workingPanel.skills.delete.agentConfirm": "Да премахна ли умението „{{name}}“ от този агент? Това действие не може да бъде отменено.",
"workingPanel.skills.delete.error": "Неуспешно изтриване на умение",
"workingPanel.skills.delete.success": "Умението е изтрито",
"workingPanel.skills.delete.title": "Изтриване на умение?",
"workingPanel.skills.delete.userConfirm": "Да деинсталирам ли умението „{{name}}“? Това действие не може да бъде отменено.",
"workingPanel.skills.detail.title": "Детайли за умението",
"workingPanel.skills.empty": "Няма намерени умения в този проект",
"workingPanel.skills.rename.action": "Преименуване",
"workingPanel.skills.rename.error": "Неуспешно преименуване на умение",
"workingPanel.skills.rename.placeholder": "Име на умението",
"workingPanel.skills.rename.title": "Преименуване на умение",
"workingPanel.skills.section.agent": "Умения на агента",
"workingPanel.skills.section.project": "Умения на проекта",
"workingPanel.skills.section.user": "Умения на потребителя",
+17
View File
@@ -1,4 +1,20 @@
{
"fleet.addColumn": "Добавяне на колона",
"fleet.allShown": "Всички текущи задачи са показани",
"fleet.backToHome": "Обратно към началната страница",
"fleet.closeColumn": "Затваряне на колона",
"fleet.createTask": "Създаване на задача",
"fleet.empty": "Няма отворени задачи",
"fleet.emptyDesc": "Изберете текуща задача отляво или използвайте +, за да добавите колона.",
"fleet.noRunningTasks": "Няма текущи задачи",
"fleet.openInChat": "Отваряне в чата",
"fleet.reply": "Отговор",
"fleet.runningTasks": "Текущи задачи",
"fleet.status.idle": "Неактивен",
"fleet.status.paused": "Пауза",
"fleet.status.running": "В процес на изпълнение",
"fleet.status.scheduled": "Планирано",
"fleet.tooltip": "Преглед на всички агенти един до друг",
"gateway.description": "Описание",
"gateway.descriptionPlaceholder": "По избор",
"gateway.deviceName": "Име на устройството",
@@ -26,6 +42,7 @@
"navigation.memoryIdentities": "Памят - Идентичности",
"navigation.memoryPreferences": "Памят - Предпочитания",
"navigation.noPages": "Все още няма страници",
"navigation.observation": "Режим на наблюдение",
"navigation.onboarding": "Въведение",
"navigation.page": "Страница",
"navigation.pages": "Страници",
+10
View File
@@ -95,8 +95,18 @@
"pageEditor.duplicateError": "Неуспешно дублиране на страницата",
"pageEditor.duplicateSuccess": "Страницата е дублирана успешно",
"pageEditor.editMode.checking": "Проверка на наличността за редактиране…",
"pageEditor.editMode.draftRestoreCancel": "Отхвърли",
"pageEditor.editMode.draftRestoreContent": "Намерени са незапазени локални промени от последната ви сесия. Да ги възстановя ли?",
"pageEditor.editMode.draftRestoreOk": "Възстанови",
"pageEditor.editMode.draftRestoreTitle": "Възстановяване на незапазен чернова",
"pageEditor.editMode.lockLostDescription": "Последните редакции все още не са синхронизирани. Те ще продължат да се запазват, когато връзката се възстанови.",
"pageEditor.editMode.lockLostTitle": "Временно загубено заключване за редакция",
"pageEditor.editMode.lockUnstable": "Възстановяване на заключването за редакция...",
"pageEditor.editMode.lockedByOther": "{{name}} редактира този документ",
"pageEditor.editMode.lockedBySelf": "Редактирате този документ в друг раздел",
"pageEditor.editMode.lockedBySelfDescription": "Запазването ще продължи, след като другата сесия се затвори или нейното заключване изтече (~30 секунди).",
"pageEditor.editMode.lockedBySomeone": "Някой друг редактира този документ",
"pageEditor.editMode.lockedDescription": "Страницата е само за четене, докато те редактират. Вашите промени няма да бъдат запазени, докато не приключат.",
"pageEditor.editedAt": "Последна редакция на {{time}}",
"pageEditor.editedBy": "Последна редакция от {{name}}",
"pageEditor.editorPlaceholder": "Натиснете \"/\" за ИИ и команди",
+2 -2
View File
@@ -5,8 +5,8 @@
"features.agentSelfIteration.title": "Само-итерация на агента",
"features.assistantMessageGroup.desc": "Групиране на съобщенията от агента и резултатите от извикванията на инструменти заедно за показване",
"features.assistantMessageGroup.title": "Групиране на съобщения от агент",
"features.gatewayMode.desc": "Изпълнявайте задачите на агента на сървъра чрез Gateway WebSocket вместо локално. Осигурява по-бързо изпълнение и намалява използването на ресурси от клиента.",
"features.gatewayMode.title": "Изпълнение на агента от страна на сървъра (Gateway)",
"features.fleet.desc": "Показване на записа Fleet в заглавната лента — табло за управление, което показва всички текущи задачи на вашите агенти едновременно.",
"features.fleet.title": "Изглед Fleet",
"features.groupChat.desc": "Активиране на координация в групов чат с множество агенти.",
"features.groupChat.title": "Групов чат (многоагентен)",
"features.imessage.desc": "Свързване на агентите с iMessage чрез локалния LobeHub Desktop BlueBubbles мост.",
+8 -2
View File
@@ -890,7 +890,7 @@
"skillStore.wantMore.feedback.title": "[Заявка за умение] Обобщете умението, от което се нуждаете, в едно изречение",
"skillStore.wantMore.reachedEnd": "Стигнахте до края. Не намирате това, което търсите?",
"startConversation": "Започни разговор",
"storage.actions.copyAgentGroups.button": "Копиране в",
"storage.actions.copyAgentGroups.button": "Копирай в...",
"storage.actions.copyAgentGroups.desc": "Копирайте групи агенти и техните членове в друго работно пространство или личен акаунт.",
"storage.actions.copyAgentGroups.title": "Копиране на групи агенти",
"storage.actions.copyLobeAI.button": "Копиране в",
@@ -1032,6 +1032,8 @@
"tab.addCustomSkill": "Добавяне на персонализирано умение",
"tab.advanced": "Разширени",
"tab.advanced.appUpdates.title": "Актуализации на приложението",
"tab.advanced.gatewayMode.desc": "Изпълнявайте поддържаните задачи на агентите през облачния Gateway по подразбиране. Индивидуалните агенти могат да променят това от менюто за чат.",
"tab.advanced.gatewayMode.title": "Режим Gateway",
"tab.advanced.toolsAndDiagnostics.title": "Инструменти и диагностика",
"tab.advanced.updateChannel.canary": "Канарче",
"tab.advanced.updateChannel.canaryDesc": "Задейства се при всяко сливане на PR, множество компилации на ден. Най-нестабилната версия.",
@@ -1169,7 +1171,6 @@
"tools.builtins.uninstallConfirm.desc": "Сигурни ли сте, че искате да деинсталирате {{name}}? Това умение ще бъде премахнато от текущия агент.",
"tools.builtins.uninstallConfirm.title": "Деинсталиране на {{name}}",
"tools.builtins.uninstalled": "Деинсталирано",
"tools.disabled": "Текущият модел не поддържа извикване на функции и не може да използва умението",
"tools.composio.addServer": "Добави сървър",
"tools.composio.authCompleted": "Удостоверяването е завършено",
"tools.composio.authFailed": "Удостоверяването не бе успешно",
@@ -1186,6 +1187,10 @@
"tools.composio.notEnabled": "Услугата Composio не е активирана",
"tools.composio.oauthRequired": "Моля, завършете OAuth удостоверяването в нов прозорец",
"tools.composio.pendingAuth": "Изчаква удостоверяване",
"tools.composio.reauthorize": "Повторно упълномощаване",
"tools.composio.remove": "Премахване",
"tools.composio.removeConfirm.desc": "{{name}} ще бъде окончателно премахнат от свързаните ви услуги. Това действие не може да бъде отменено.",
"tools.composio.removeConfirm.title": "Премахване на {{name}}?",
"tools.composio.serverCreated": "Сървърът е създаден успешно",
"tools.composio.serverCreatedFailed": "Неуспешно създаване на сървър",
"tools.composio.serverRemoved": "Сървърът е премахнат",
@@ -1238,6 +1243,7 @@
"tools.composio.servers.zendesk.readme": "Интегрирайте се със Zendesk за управление на клиентски запитвания и поддръжка. Създавайте, актуализирайте и проследявайте тикети, достъпвайте клиентски данни и оптимизирайте обслужването си.",
"tools.composio.tools": "инструменти",
"tools.composio.verifyAuth": "Завърших удостоверяването",
"tools.disabled": "Текущият модел не поддържа извикване на функции и не може да използва умението",
"tools.lobehubSkill.authorize": "Упълномощи",
"tools.lobehubSkill.connect": "Свържи",
"tools.lobehubSkill.connected": "Свързано",
+15
View File
@@ -16,6 +16,7 @@
"actions.favorite": "Любимо",
"actions.import": "Импортирай разговор",
"actions.markCompleted": "Отбележи като завършена",
"actions.moveToAgent": "Премести към друг асистент",
"actions.openInNewTab": "Отвори в нов раздел",
"actions.openInNewWindow": "Отвори в нов прозорец",
"actions.removeAll": "Изтрий всички теми",
@@ -80,6 +81,9 @@
"management.bulk.deleteConfirm": "Ще изтриете {{count}} теми. Това действие не може да бъде отменено.",
"management.bulk.deleteTitle": "Изтриване на теми?",
"management.bulk.favorite": "Любими",
"management.bulk.move": "Премести към асистент",
"management.bulk.moveEmpty": "Няма други асистенти",
"management.bulk.moveSearchPlaceholder": "Търсене на асистенти…",
"management.bulk.selectedCount_one": "{{count}} избрана",
"management.bulk.selectedCount_other": "{{count}} избрани",
"management.card.noPreview": "Няма наличен преглед",
@@ -118,6 +122,17 @@
"management.group.noProject": "Без проект",
"management.group.none": "Няма",
"management.loadingMore": "Зареждане на още теми...",
"management.moveModal.back": "Назад",
"management.moveModal.confirmContent_one": "Да преместя {{count}} тема в „{{title}}“?",
"management.moveModal.confirmContent_other": "Да преместя {{count}} теми в „{{title}}“?",
"management.moveModal.confirmOk": "Премести",
"management.moveModal.doneOk": "Готово",
"management.moveModal.done_one": "{{count}} тема преместена",
"management.moveModal.done_other": "{{count}} теми преместени",
"management.moveModal.error": "Преместването не бе успешно, моля опитайте отново",
"management.moveModal.goToTarget": "Отиди на „{{title}}“",
"management.moveModal.moving": "Преместване…",
"management.moveModal.title": "Преместване на теми",
"management.searchPlaceholder": "Търсене на теми на този агент...",
"management.sidebarEntry": "Теми",
"management.sort.createdAt": "Време на създаване",
+18
View File
@@ -44,6 +44,7 @@
"builtinCopilot": "Integrierter Copilot",
"chatList.expandMessage": "Nachricht erweitern",
"chatList.longMessageDetail": "Details anzeigen",
"chatList.refreshing": "Neueste Nachrichten werden abgerufen...",
"chatMode.agent": "Agent",
"chatMode.agentCap.env": "Laufzeitumgebung",
"chatMode.agentCap.files": "Dateizugriff",
@@ -164,6 +165,7 @@
"extendParams.urlContext.title": "Webseiteninhalte extrahieren",
"followUpPlaceholder": "Folgen Sie nach. @, um Aufgaben anderen Agenten zuzuweisen.",
"followUpPlaceholderHeterogeneous": "Weiter ausführen.",
"gatewayMode.title": "Gateway-Modus",
"group.desc": "Bringen Sie eine Aufgabe mit mehreren Agenten in einem gemeinsamen Raum voran.",
"group.memberTooltip": "Es gibt {{count}} Mitglieder in der Gruppe",
"group.orchestratorThinking": "Orchestrator denkt nach...",
@@ -245,6 +247,7 @@
"inbox.title": "Lobe AI",
"input.addAi": "KI-Nachricht hinzufügen",
"input.addUser": "Benutzernachricht hinzufügen",
"input.agentModeUnsupportedModel": "Das aktuelle Modell unterstützt keine agentischen Werkzeugaufrufe. Wechseln Sie zu einem Modell mit Agentenfähigkeit für die beste Erfahrung.",
"input.costEstimate.creditsPerMillionTokens": "{{credits}} Credits/M Tokens",
"input.costEstimate.hint": "Geschätzte Kosten: ~{{credits}} Credits",
"input.costEstimate.inputLabel": "Eingabe",
@@ -332,6 +335,7 @@
"messages.modelCard.pricing.outputTokens": "Output {{amount}} Credits · ${{amount}}/M",
"messages.modelCard.pricing.writeCacheInputTokens": "Cache-Schreiben {{amount}} Credits · ${{amount}}/M",
"messages.tokenDetails.average": "Durchschnittspreis pro Einheit",
"messages.tokenDetails.cacheRate": "Cache-Rate",
"messages.tokenDetails.input": "Input",
"messages.tokenDetails.inputAudio": "Audio-Input",
"messages.tokenDetails.inputCached": "Gecachter Input",
@@ -1093,7 +1097,21 @@
"workingPanel.review.viewMode.unified": "Zur einheitlichen Ansicht wechseln",
"workingPanel.review.wordWrap.disable": "Zeilenumbruch deaktivieren",
"workingPanel.review.wordWrap.enable": "Zeilenumbruch aktivieren",
"workingPanel.skills.actions.comingSoon": "Demnächst verfügbar",
"workingPanel.skills.actions.delete": "Löschen",
"workingPanel.skills.actions.rename": "Umbenennen",
"workingPanel.skills.actions.view": "Ansehen",
"workingPanel.skills.delete.agentConfirm": "Die Fähigkeit „{{name}}“ von diesem Agenten entfernen? Dies kann nicht rückgängig gemacht werden.",
"workingPanel.skills.delete.error": "Fähigkeit konnte nicht gelöscht werden",
"workingPanel.skills.delete.success": "Fähigkeit gelöscht",
"workingPanel.skills.delete.title": "Fähigkeit löschen?",
"workingPanel.skills.delete.userConfirm": "Die Fähigkeit „{{name}}“ deinstallieren? Dies kann nicht rückgängig gemacht werden.",
"workingPanel.skills.detail.title": "Fähigkeitsdetails",
"workingPanel.skills.empty": "Keine Fähigkeiten in diesem Projekt gefunden",
"workingPanel.skills.rename.action": "Umbenennen",
"workingPanel.skills.rename.error": "Fähigkeit konnte nicht umbenannt werden",
"workingPanel.skills.rename.placeholder": "Fähigkeitsname",
"workingPanel.skills.rename.title": "Fähigkeit umbenennen",
"workingPanel.skills.section.agent": "Agentenfähigkeiten",
"workingPanel.skills.section.project": "Projektfähigkeiten",
"workingPanel.skills.section.user": "Benutzerfähigkeiten",
+17
View File
@@ -1,4 +1,20 @@
{
"fleet.addColumn": "Spalte hinzufügen",
"fleet.allShown": "Alle laufenden Aufgaben werden angezeigt",
"fleet.backToHome": "Zurück zur Startseite",
"fleet.closeColumn": "Spalte schließen",
"fleet.createTask": "Aufgabe erstellen",
"fleet.empty": "Keine offenen Aufgaben",
"fleet.emptyDesc": "Wählen Sie eine laufende Aufgabe links aus oder verwenden Sie +, um eine Spalte hinzuzufügen.",
"fleet.noRunningTasks": "Keine laufenden Aufgaben",
"fleet.openInChat": "Im Chat öffnen",
"fleet.reply": "Antworten",
"fleet.runningTasks": "Laufende Aufgaben",
"fleet.status.idle": "Leerlauf",
"fleet.status.paused": "Pausiert",
"fleet.status.running": "Läuft",
"fleet.status.scheduled": "Geplant",
"fleet.tooltip": "Alle Agenten nebeneinander anzeigen",
"gateway.description": "Beschreibung",
"gateway.descriptionPlaceholder": "Optional",
"gateway.deviceName": "Gerätename",
@@ -26,6 +42,7 @@
"navigation.memoryIdentities": "Speicher - Identitäten",
"navigation.memoryPreferences": "Speicher - Präferenzen",
"navigation.noPages": "Noch keine Seiten",
"navigation.observation": "Beobachtungsmodus",
"navigation.onboarding": "Einführung",
"navigation.page": "Seite",
"navigation.pages": "Seiten",
+10
View File
@@ -95,8 +95,18 @@
"pageEditor.duplicateError": "Fehler beim Duplizieren der Seite",
"pageEditor.duplicateSuccess": "Seite erfolgreich dupliziert",
"pageEditor.editMode.checking": "Bearbeitsverfügbarkeit wird überprüft…",
"pageEditor.editMode.draftRestoreCancel": "Verwerfen",
"pageEditor.editMode.draftRestoreContent": "Es wurden nicht gespeicherte lokale Änderungen aus Ihrer letzten Sitzung gefunden. Möchten Sie diese wiederherstellen?",
"pageEditor.editMode.draftRestoreOk": "Wiederherstellen",
"pageEditor.editMode.draftRestoreTitle": "Nicht gespeicherten Entwurf wiederherstellen",
"pageEditor.editMode.lockLostDescription": "Die letzten Änderungen wurden noch nicht synchronisiert. Die Speicherung wird fortgesetzt, sobald die Verbindung wiederhergestellt ist.",
"pageEditor.editMode.lockLostTitle": "Bearbeitungssperre vorübergehend verloren",
"pageEditor.editMode.lockUnstable": "Bearbeitungssperre wird wiederhergestellt…",
"pageEditor.editMode.lockedByOther": "{{name}} bearbeitet dieses Dokument",
"pageEditor.editMode.lockedBySelf": "Sie bearbeiten dieses Dokument in einem anderen Tab",
"pageEditor.editMode.lockedBySelfDescription": "Die Speicherung wird fortgesetzt, nachdem die andere Sitzung geschlossen wurde oder deren Sperre abläuft (~30 Sekunden).",
"pageEditor.editMode.lockedBySomeone": "Jemand anderes bearbeitet dieses Dokument",
"pageEditor.editMode.lockedDescription": "Die Seite ist schreibgeschützt, während sie bearbeitet wird. Ihre Änderungen werden erst gespeichert, wenn die Bearbeitung abgeschlossen ist.",
"pageEditor.editedAt": "Zuletzt bearbeitet am {{time}}",
"pageEditor.editedBy": "Zuletzt bearbeitet von {{name}}",
"pageEditor.editorPlaceholder": "Drücken Sie \"/\" für KI und Befehle",
+2 -2
View File
@@ -5,8 +5,8 @@
"features.agentSelfIteration.title": "Agenten-Selbstiteration",
"features.assistantMessageGroup.desc": "Agenten-Nachrichten und deren Tool-Ergebnisse gemeinsam anzeigen",
"features.assistantMessageGroup.title": "Agenten-Nachrichten gruppieren",
"features.gatewayMode.desc": "Führt Agentenaufgaben über die Gateway-WebSocket-Verbindung auf dem Server aus, statt sie lokal auszuführen. Ermöglicht eine schnellere Ausführung und verringert die Client-Ressourcennutzung.",
"features.gatewayMode.title": "Serverseitige Agentenausführung (Gateway)",
"features.fleet.desc": "Zeigt den Fleet-Eintrag in der Titelleiste an ein nebeneinander angeordnetes Dashboard aller laufenden Aufgaben über Ihre Agenten hinweg.",
"features.fleet.title": "Flottenansicht",
"features.groupChat.desc": "Koordination von Gruppenchats mit mehreren Agenten aktivieren.",
"features.groupChat.title": "Gruppenchat (Multi-Agenten)",
"features.imessage.desc": "Verbinden Sie Agenten mit iMessage über die lokale LobeHub Desktop BlueBubbles-Bridge.",
+8 -2
View File
@@ -890,7 +890,7 @@
"skillStore.wantMore.feedback.title": "[Skill-Anfrage] Fassen Sie den benötigten Skill in einem Satz zusammen",
"skillStore.wantMore.reachedEnd": "Sie haben das Ende erreicht. Nicht gefunden, was Sie suchen?",
"startConversation": "Konversation starten",
"storage.actions.copyAgentGroups.button": "Kopieren nach",
"storage.actions.copyAgentGroups.button": "Kopieren nach...",
"storage.actions.copyAgentGroups.desc": "Agentengruppen und ihre Mitglieder in einen anderen Arbeitsbereich oder persönlichen Account kopieren.",
"storage.actions.copyAgentGroups.title": "Agentengruppen kopieren",
"storage.actions.copyLobeAI.button": "Kopieren nach",
@@ -1032,6 +1032,8 @@
"tab.addCustomSkill": "Benutzerdefinierten Skill hinzufügen",
"tab.advanced": "Erweitert",
"tab.advanced.appUpdates.title": "App-Updates",
"tab.advanced.gatewayMode.desc": "Führen Sie unterstützte Agentenaufgaben standardmäßig über das Cloud-Gateway aus. Einzelne Agenten können dies im Chat-Menü überschreiben.",
"tab.advanced.gatewayMode.title": "Gateway-Modus",
"tab.advanced.toolsAndDiagnostics.title": "Tools und Diagnosen",
"tab.advanced.updateChannel.canary": "Canary",
"tab.advanced.updateChannel.canaryDesc": "Ausgelöst bei jedem PR-Merge, mehrere Builds pro Tag. Am instabilsten.",
@@ -1169,7 +1171,6 @@
"tools.builtins.uninstallConfirm.desc": "Möchten Sie {{name}} wirklich deinstallieren? Diese Fähigkeit wird vom aktuellen Agenten entfernt.",
"tools.builtins.uninstallConfirm.title": "{{name}} deinstallieren",
"tools.builtins.uninstalled": "Deinstalliert",
"tools.disabled": "Das aktuelle Modell unterstützt keine Funktionsaufrufe und kann die Fähigkeit nicht nutzen",
"tools.composio.addServer": "Server hinzufügen",
"tools.composio.authCompleted": "Authentifizierung abgeschlossen",
"tools.composio.authFailed": "Authentifizierung fehlgeschlagen",
@@ -1186,6 +1187,10 @@
"tools.composio.notEnabled": "Composio-Dienst nicht aktiviert",
"tools.composio.oauthRequired": "Bitte schließen Sie die OAuth-Authentifizierung im neuen Fenster ab",
"tools.composio.pendingAuth": "Authentifizierung ausstehend",
"tools.composio.reauthorize": "Erneut autorisieren",
"tools.composio.remove": "Entfernen",
"tools.composio.removeConfirm.desc": "{{name}} wird dauerhaft aus Ihren verbundenen Diensten entfernt. Diese Aktion kann nicht rückgängig gemacht werden.",
"tools.composio.removeConfirm.title": "{{name}} entfernen?",
"tools.composio.serverCreated": "Server erfolgreich erstellt",
"tools.composio.serverCreatedFailed": "Servererstellung fehlgeschlagen",
"tools.composio.serverRemoved": "Server entfernt",
@@ -1238,6 +1243,7 @@
"tools.composio.servers.zendesk.readme": "Integrieren Sie Zendesk, um Support-Tickets und Kundeninteraktionen zu verwalten. Anfragen erstellen, aktualisieren und verfolgen, Kundendaten abrufen und Ihre Supportprozesse optimieren.",
"tools.composio.tools": "Werkzeuge",
"tools.composio.verifyAuth": "Ich habe die Authentifizierung abgeschlossen",
"tools.disabled": "Das aktuelle Modell unterstützt keine Funktionsaufrufe und kann die Fähigkeit nicht nutzen",
"tools.lobehubSkill.authorize": "Autorisieren",
"tools.lobehubSkill.connect": "Verbinden",
"tools.lobehubSkill.connected": "Verbunden",
+15
View File
@@ -16,6 +16,7 @@
"actions.favorite": "Favorit",
"actions.import": "Konversation importieren",
"actions.markCompleted": "Als abgeschlossen markieren",
"actions.moveToAgent": "Zu einem anderen Assistenten wechseln",
"actions.openInNewTab": "In neuem Tab öffnen",
"actions.openInNewWindow": "In neuem Fenster öffnen",
"actions.removeAll": "Alle Themen löschen",
@@ -80,6 +81,9 @@
"management.bulk.deleteConfirm": "Sie sind dabei, {{count}} Themen zu löschen. Diese Aktion kann nicht rückgängig gemacht werden.",
"management.bulk.deleteTitle": "Themen löschen?",
"management.bulk.favorite": "Favorisieren",
"management.bulk.move": "Zum Assistenten wechseln",
"management.bulk.moveEmpty": "Keine anderen Assistenten",
"management.bulk.moveSearchPlaceholder": "Assistenten suchen…",
"management.bulk.selectedCount_one": "{{count}} ausgewählt",
"management.bulk.selectedCount_other": "{{count}} ausgewählt",
"management.card.noPreview": "Keine Vorschau verfügbar",
@@ -118,6 +122,17 @@
"management.group.noProject": "Kein Projekt",
"management.group.none": "Keine",
"management.loadingMore": "Weitere Themen werden geladen…",
"management.moveModal.back": "Zurück",
"management.moveModal.confirmContent_one": "{{count}} Thema zu „{{title}}“ verschieben?",
"management.moveModal.confirmContent_other": "{{count}} Themen zu „{{title}}“ verschieben?",
"management.moveModal.confirmOk": "Verschieben",
"management.moveModal.doneOk": "Fertig",
"management.moveModal.done_one": "{{count}} Thema verschoben",
"management.moveModal.done_other": "{{count}} Themen verschoben",
"management.moveModal.error": "Verschieben fehlgeschlagen, bitte versuchen Sie es erneut",
"management.moveModal.goToTarget": "Zu „{{title}}“ wechseln",
"management.moveModal.moving": "Verschiebe…",
"management.moveModal.title": "Themen verschieben",
"management.searchPlaceholder": "Themen dieses Agenten durchsuchen…",
"management.sidebarEntry": "Themen",
"management.sort.createdAt": "Erstellungszeit",
+7 -1
View File
@@ -20,6 +20,9 @@
"agentDefaultMessage": "Hi, Im **{{name}}**. One sentence is enough.\n\nWant me to match your workflow better? Go to [Agent Settings]({{url}}) and fill in the Agent Profile (you can edit it anytime).",
"agentDefaultMessageWithSystemRole": "Hi, Im **{{name}}**. One sentence is enough—you're in control.",
"agentDefaultMessageWithoutEdit": "Hi, Im **{{name}}**. One sentence is enough—you're in control.",
"agentDocument.backToChat": "Back to chat",
"agentDocument.linkCopied": "Link copied",
"agentDocument.openAsPage": "Open as full page",
"agentProfile.files_one": "{{count}} file",
"agentProfile.files_other": "{{count}} files",
"agentProfile.knowledgeBases_one": "{{count}} knowledge base",
@@ -165,7 +168,9 @@
"extendParams.urlContext.title": "Extract Webpage Link Content",
"followUpPlaceholder": "Follow up.",
"followUpPlaceholderHeterogeneous": "Follow up.",
"gatewayMode.title": "Gateway Mode",
"gatewayMode.beta": "Beta",
"gatewayMode.desc": "Run agents in the cloud through LobeHub's Agent Gateway. Tasks keep running even after you close the page.",
"gatewayMode.title": "Agent Gateway Mode",
"group.desc": "Move a task forward with multiple Agents in one shared space.",
"group.memberTooltip": "There are {{count}} members in the group",
"group.orchestratorThinking": "Orchestrator is thinking...",
@@ -877,6 +882,7 @@
"toolAuth.authorize": "Authorize",
"toolAuth.authorizing": "Authorizing...",
"toolAuth.hint": "When Skills aren't authorized or configured, the related Skills won't work and the Agent's capabilities may be limited or run into errors.",
"toolAuth.remove": "Remove",
"toolAuth.signIn": "Sign In",
"toolAuth.title": "Authorize Skills for this Agent",
"topic.checkOpenNewTopic": "Start a new topic?",
+3
View File
@@ -3,6 +3,8 @@
"fleet.allShown": "All running tasks are shown",
"fleet.backToHome": "Back to home",
"fleet.closeColumn": "Close column",
"fleet.closeIdleColumns": "Close idle columns",
"fleet.closeIdleColumnsCount": "Close {{count}} idle columns",
"fleet.collapseReply": "Collapse",
"fleet.createTask": "Create task",
"fleet.dragHint": "Drag to reorder",
@@ -36,6 +38,7 @@
"navigation.discoverMcp": "Discover MCP",
"navigation.discoverModels": "Discover Models",
"navigation.discoverProviders": "Discover Providers",
"navigation.document": "Document",
"navigation.group": "Group",
"navigation.groupChat": "Group Chat",
"navigation.home": "Home",
+10
View File
@@ -95,8 +95,18 @@
"pageEditor.duplicateError": "Failed to duplicate the page",
"pageEditor.duplicateSuccess": "Page duplicated successfully",
"pageEditor.editMode.checking": "Checking edit availability…",
"pageEditor.editMode.draftRestoreCancel": "Discard",
"pageEditor.editMode.draftRestoreContent": "Found unsaved local changes from your last session. Restore them?",
"pageEditor.editMode.draftRestoreOk": "Restore",
"pageEditor.editMode.draftRestoreTitle": "Restore Unsaved Draft",
"pageEditor.editMode.lockLostDescription": "Recent edits havent synced yet. Theyll resume saving once the connection recovers.",
"pageEditor.editMode.lockLostTitle": "Edit lock temporarily lost",
"pageEditor.editMode.lockUnstable": "Reconnecting edit lock…",
"pageEditor.editMode.lockedByOther": "{{name}} is editing this document",
"pageEditor.editMode.lockedBySelf": "Youre editing this document in another tab",
"pageEditor.editMode.lockedBySelfDescription": "Saves will resume after the other session closes or its lock expires (~30s).",
"pageEditor.editMode.lockedBySomeone": "Someone else is editing this document",
"pageEditor.editMode.lockedDescription": "The page is read-only while they edit. Your changes wont be saved until theyre done.",
"pageEditor.editedAt": "Last edited on {{time}}",
"pageEditor.editedBy": "Last edited by {{name}}",
"pageEditor.editorPlaceholder": "Press \"/\" for AI and commands.",
+1 -1
View File
@@ -890,7 +890,7 @@
"skillStore.wantMore.feedback.title": "[Skill Request] Summarize the skill you need in one sentence",
"skillStore.wantMore.reachedEnd": "You've reached the end. Can't find what you need?",
"startConversation": "Start Conversation",
"storage.actions.copyAgentGroups.button": "Copy To",
"storage.actions.copyAgentGroups.button": "Copy to...",
"storage.actions.copyAgentGroups.desc": "Copy agent groups and their member agents into another workspace or personal account.",
"storage.actions.copyAgentGroups.title": "Agent Groups Copy",
"storage.actions.copyLobeAI.button": "Copy to...",
-4
View File
@@ -202,10 +202,6 @@
"securityBlacklist.sshPrivateKeys": "Reading SSH private keys can compromise system security",
"securityBlacklist.sudoers": "Modifying sudoers file without proper validation is dangerous",
"securityBlacklist.suidShells": "Setting SUID on shells or interpreters is a security risk",
"connector.edit": "Edit",
"connector.edit.success": "Connector updated",
"connector.uninstall": "Uninstall",
"connector.uninstallConfirm": "Are you sure you want to uninstall this connector?",
"updateArgs.duplicateKeyError": "Field key must be unique",
"updateArgs.form.add": "Add an Item",
"updateArgs.form.key": "Field Key",
+18
View File
@@ -44,6 +44,7 @@
"builtinCopilot": "Copiloto integrado",
"chatList.expandMessage": "Expandir mensaje",
"chatList.longMessageDetail": "Ver detalles",
"chatList.refreshing": "Obteniendo los mensajes más recientes...",
"chatMode.agent": "Agente",
"chatMode.agentCap.env": "Entorno de ejecución",
"chatMode.agentCap.files": "Acceso a archivos",
@@ -164,6 +165,7 @@
"extendParams.urlContext.title": "Extraer contenido de enlaces web",
"followUpPlaceholder": "Seguimiento. Usa @ para asignar tareas a otros agentes.",
"followUpPlaceholderHeterogeneous": "Continuar.",
"gatewayMode.title": "Modo Gateway",
"group.desc": "Avanza una tarea con múltiples Agentes en un espacio compartido.",
"group.memberTooltip": "Hay {{count}} miembros en el grupo",
"group.orchestratorThinking": "El Orquestador está pensando...",
@@ -245,6 +247,7 @@
"inbox.title": "Lobe AI",
"input.addAi": "Agregar mensaje de IA",
"input.addUser": "Agregar mensaje de usuario",
"input.agentModeUnsupportedModel": "El modelo actual no admite llamadas de herramientas agenticas. Cambia a un modelo con capacidad de agente para obtener la mejor experiencia.",
"input.costEstimate.creditsPerMillionTokens": "{{credits}} créditos/M tokens",
"input.costEstimate.hint": "Costo estimado: ~{{credits}} créditos",
"input.costEstimate.inputLabel": "Entrada",
@@ -332,6 +335,7 @@
"messages.modelCard.pricing.outputTokens": "Salida {{amount}} créditos · ${{amount}}/M",
"messages.modelCard.pricing.writeCacheInputTokens": "Escritura en caché {{amount}} créditos · ${{amount}}/M",
"messages.tokenDetails.average": "Precio unitario promedio",
"messages.tokenDetails.cacheRate": "Tasa de caché",
"messages.tokenDetails.input": "Entrada",
"messages.tokenDetails.inputAudio": "Entrada de audio",
"messages.tokenDetails.inputCached": "Entrada en caché",
@@ -1093,7 +1097,21 @@
"workingPanel.review.viewMode.unified": "Cambiar a vista unificada",
"workingPanel.review.wordWrap.disable": "Desactivar ajuste de línea",
"workingPanel.review.wordWrap.enable": "Activar ajuste de línea",
"workingPanel.skills.actions.comingSoon": "Próximamente",
"workingPanel.skills.actions.delete": "Eliminar",
"workingPanel.skills.actions.rename": "Renombrar",
"workingPanel.skills.actions.view": "Ver",
"workingPanel.skills.delete.agentConfirm": "¿Eliminar la habilidad “{{name}}” de este agente? Esto no se puede deshacer.",
"workingPanel.skills.delete.error": "Error al eliminar la habilidad",
"workingPanel.skills.delete.success": "Habilidad eliminada",
"workingPanel.skills.delete.title": "¿Eliminar habilidad?",
"workingPanel.skills.delete.userConfirm": "¿Desinstalar la habilidad “{{name}}”? Esto no se puede deshacer.",
"workingPanel.skills.detail.title": "Detalles de la habilidad",
"workingPanel.skills.empty": "No se encontraron habilidades en este proyecto",
"workingPanel.skills.rename.action": "Renombrar",
"workingPanel.skills.rename.error": "Error al renombrar la habilidad",
"workingPanel.skills.rename.placeholder": "Nombre de la habilidad",
"workingPanel.skills.rename.title": "Renombrar habilidad",
"workingPanel.skills.section.agent": "Habilidades del agente",
"workingPanel.skills.section.project": "Habilidades del proyecto",
"workingPanel.skills.section.user": "Habilidades del usuario",
+17
View File
@@ -1,4 +1,20 @@
{
"fleet.addColumn": "Agregar columna",
"fleet.allShown": "Se muestran todas las tareas en ejecución",
"fleet.backToHome": "Volver a inicio",
"fleet.closeColumn": "Cerrar columna",
"fleet.createTask": "Crear tarea",
"fleet.empty": "No hay tareas abiertas",
"fleet.emptyDesc": "Selecciona una tarea en ejecución a la izquierda o usa + para agregar una columna.",
"fleet.noRunningTasks": "No hay tareas en ejecución",
"fleet.openInChat": "Abrir en el chat",
"fleet.reply": "Responder",
"fleet.runningTasks": "Tareas en ejecución",
"fleet.status.idle": "Inactivo",
"fleet.status.paused": "Pausado",
"fleet.status.running": "En ejecución",
"fleet.status.scheduled": "Programado",
"fleet.tooltip": "Ver todos los agentes uno al lado del otro",
"gateway.description": "Descripción",
"gateway.descriptionPlaceholder": "Opcional",
"gateway.deviceName": "Nombre del Dispositivo",
@@ -26,6 +42,7 @@
"navigation.memoryIdentities": "Memoria - Identidades",
"navigation.memoryPreferences": "Memoria - Preferencias",
"navigation.noPages": "Aún no hay páginas",
"navigation.observation": "Modo de observación",
"navigation.onboarding": "Incorporación",
"navigation.page": "Página",
"navigation.pages": "Páginas",
+10
View File
@@ -95,8 +95,18 @@
"pageEditor.duplicateError": "Error al duplicar la página",
"pageEditor.duplicateSuccess": "Página duplicada correctamente",
"pageEditor.editMode.checking": "Comprobando la disponibilidad de edición…",
"pageEditor.editMode.draftRestoreCancel": "Descartar",
"pageEditor.editMode.draftRestoreContent": "Se encontraron cambios locales no guardados de tu última sesión. ¿Restaurarlos?",
"pageEditor.editMode.draftRestoreOk": "Restaurar",
"pageEditor.editMode.draftRestoreTitle": "Restaurar borrador no guardado",
"pageEditor.editMode.lockLostDescription": "Los cambios recientes aún no se han sincronizado. La guardado se reanudará una vez que se recupere la conexión.",
"pageEditor.editMode.lockLostTitle": "Bloqueo de edición perdido temporalmente",
"pageEditor.editMode.lockUnstable": "Reconectando el bloqueo de edición…",
"pageEditor.editMode.lockedByOther": "{{name}} está editando este documento",
"pageEditor.editMode.lockedBySelf": "Estás editando este documento en otra pestaña",
"pageEditor.editMode.lockedBySelfDescription": "El guardado se reanudará después de que la otra sesión se cierre o su bloqueo expire (~30s).",
"pageEditor.editMode.lockedBySomeone": "Alguien más está editando este documento",
"pageEditor.editMode.lockedDescription": "La página está en modo de solo lectura mientras ellos editan. Tus cambios no se guardarán hasta que terminen.",
"pageEditor.editedAt": "Última edición el {{time}}",
"pageEditor.editedBy": "Última edición por {{name}}",
"pageEditor.editorPlaceholder": "Presiona \"/\" para IA y comandos",
+2 -2
View File
@@ -5,8 +5,8 @@
"features.agentSelfIteration.title": "Autoiteración del agente",
"features.assistantMessageGroup.desc": "Agrupa los mensajes del agente y los resultados de sus herramientas para mostrarlos juntos",
"features.assistantMessageGroup.title": "Agrupación de Mensajes del Agente",
"features.gatewayMode.desc": "Ejecuta las tareas del agente en el servidor a través del WebSocket de Gateway en lugar de hacerlo localmente. Permite una ejecución más rápida y reduce el uso de recursos del cliente.",
"features.gatewayMode.title": "Ejecución del agente del lado del servidor (Gateway)",
"features.fleet.desc": "Mostrar la entrada de Fleet en la barra de título: un panel de control lado a lado de todas las tareas en ejecución en tus agentes.",
"features.fleet.title": "Vista de Fleet",
"features.groupChat.desc": "Activa la coordinación de chat grupal con múltiples agentes.",
"features.groupChat.title": "Chat Grupal (Multiagente)",
"features.imessage.desc": "Conectar agentes a iMessage a través del puente local LobeHub Desktop BlueBubbles.",
+8 -2
View File
@@ -890,7 +890,7 @@
"skillStore.wantMore.feedback.title": "[Solicitud de habilidad] Resume en una frase la habilidad que necesitas",
"skillStore.wantMore.reachedEnd": "Has llegado al final. ¿No encuentras lo que necesitas?",
"startConversation": "Iniciar Conversación",
"storage.actions.copyAgentGroups.button": "Copiar a",
"storage.actions.copyAgentGroups.button": "Copiar a...",
"storage.actions.copyAgentGroups.desc": "Copia grupos de agentes y sus miembros en otro espacio de trabajo o cuenta personal.",
"storage.actions.copyAgentGroups.title": "Copiar grupos de agentes",
"storage.actions.copyLobeAI.button": "Copiar a",
@@ -1032,6 +1032,8 @@
"tab.addCustomSkill": "Agregar habilidad personalizada",
"tab.advanced": "Avanzado",
"tab.advanced.appUpdates.title": "Actualizaciones de la aplicación",
"tab.advanced.gatewayMode.desc": "Ejecuta tareas de agentes compatibles a través del Gateway en la nube por defecto. Los agentes individuales pueden anular esto desde el menú de chat.",
"tab.advanced.gatewayMode.title": "Modo Gateway",
"tab.advanced.toolsAndDiagnostics.title": "Herramientas y diagnósticos",
"tab.advanced.updateChannel.canary": "Canary",
"tab.advanced.updateChannel.canaryDesc": "Activado con cada fusión de PR, múltiples compilaciones por día. El más inestable.",
@@ -1169,7 +1171,6 @@
"tools.builtins.uninstallConfirm.desc": "¿Estás seguro de que deseas desinstalar {{name}}? Esta habilidad se eliminará del agente actual.",
"tools.builtins.uninstallConfirm.title": "Desinstalar {{name}}",
"tools.builtins.uninstalled": "Desinstalado",
"tools.disabled": "El modelo actual no admite llamadas a funciones y no puede usar esta habilidad",
"tools.composio.addServer": "Agregar Servidor",
"tools.composio.authCompleted": "Autenticación Completada",
"tools.composio.authFailed": "Autenticación Fallida",
@@ -1186,6 +1187,10 @@
"tools.composio.notEnabled": "Servicio Composio no habilitado",
"tools.composio.oauthRequired": "Por favor, completa la autenticación OAuth en la nueva ventana",
"tools.composio.pendingAuth": "Autenticación Pendiente",
"tools.composio.reauthorize": "Reautorizar",
"tools.composio.remove": "Eliminar",
"tools.composio.removeConfirm.desc": "{{name}} se eliminará permanentemente de tus servicios conectados. Esta acción no se puede deshacer.",
"tools.composio.removeConfirm.title": "¿Eliminar {{name}}?",
"tools.composio.serverCreated": "Servidor creado con éxito",
"tools.composio.serverCreatedFailed": "Error al crear el servidor",
"tools.composio.serverRemoved": "Servidor eliminado",
@@ -1238,6 +1243,7 @@
"tools.composio.servers.zendesk.readme": "Integra con Zendesk para gestionar tickets de soporte e interacciones con clientes. Crea, actualiza y sigue solicitudes de soporte, accede a datos de clientes y optimiza tus operaciones de atención al cliente.",
"tools.composio.tools": "herramientas",
"tools.composio.verifyAuth": "He completado la autenticación",
"tools.disabled": "El modelo actual no admite llamadas a funciones y no puede usar esta habilidad",
"tools.lobehubSkill.authorize": "Autorizar",
"tools.lobehubSkill.connect": "Conectar",
"tools.lobehubSkill.connected": "Conectado",
+15
View File
@@ -16,6 +16,7 @@
"actions.favorite": "Favorito",
"actions.import": "Importar conversación",
"actions.markCompleted": "Marcar como completada",
"actions.moveToAgent": "Mover a otro asistente",
"actions.openInNewTab": "Abrir en una nueva pestaña",
"actions.openInNewWindow": "Abrir en una nueva ventana",
"actions.removeAll": "Eliminar todos los temas",
@@ -80,6 +81,9 @@
"management.bulk.deleteConfirm": "Estás a punto de eliminar {{count}} temas. Esta acción no se puede deshacer.",
"management.bulk.deleteTitle": "¿Eliminar temas?",
"management.bulk.favorite": "Favorito",
"management.bulk.move": "Mover al asistente",
"management.bulk.moveEmpty": "No hay otros asistentes",
"management.bulk.moveSearchPlaceholder": "Buscar asistentes…",
"management.bulk.selectedCount_one": "{{count}} seleccionado",
"management.bulk.selectedCount_other": "{{count}} seleccionados",
"management.card.noPreview": "Vista previa no disponible",
@@ -118,6 +122,17 @@
"management.group.noProject": "Sin proyecto",
"management.group.none": "Ninguno",
"management.loadingMore": "Cargando más temas…",
"management.moveModal.back": "Atrás",
"management.moveModal.confirmContent_one": "¿Mover {{count}} tema a “{{title}}”?",
"management.moveModal.confirmContent_other": "¿Mover {{count}} temas a “{{title}}”?",
"management.moveModal.confirmOk": "Mover",
"management.moveModal.doneOk": "Hecho",
"management.moveModal.done_one": "{{count}} tema movido",
"management.moveModal.done_other": "{{count}} temas movidos",
"management.moveModal.error": "Error al mover, por favor inténtalo de nuevo",
"management.moveModal.goToTarget": "Ir a “{{title}}”",
"management.moveModal.moving": "Moviendo…",
"management.moveModal.title": "Mover temas",
"management.searchPlaceholder": "Buscar temas de este agente…",
"management.sidebarEntry": "Temas",
"management.sort.createdAt": "Hora de creación",
+18
View File
@@ -44,6 +44,7 @@
"builtinCopilot": "همیار داخلی",
"chatList.expandMessage": "گسترش پیام",
"chatList.longMessageDetail": "مشاهده جزئیات",
"chatList.refreshing": "در حال دریافت پیام‌های جدید...",
"chatMode.agent": "نماینده",
"chatMode.agentCap.env": "محیط اجرایی",
"chatMode.agentCap.files": "دسترسی به فایل",
@@ -164,6 +165,7 @@
"extendParams.urlContext.title": "استخراج محتوای پیوند وب",
"followUpPlaceholder": "پیگیری. برای واگذاری وظیفه به عامل‌های دیگر از @ استفاده کنید.",
"followUpPlaceholderHeterogeneous": "پیگیری.",
"gatewayMode.title": "حالت دروازه",
"group.desc": "با چند عامل در یک فضای مشترک، یک وظیفه را پیش ببرید.",
"group.memberTooltip": "{{count}} عضو در گروه وجود دارد",
"group.orchestratorThinking": "هماهنگ‌کننده در حال تفکر است...",
@@ -245,6 +247,7 @@
"inbox.title": "Lobe AI",
"input.addAi": "افزودن پیام هوش مصنوعی",
"input.addUser": "افزودن پیام کاربر",
"input.agentModeUnsupportedModel": "مدل فعلی از فراخوانی ابزارهای عامل پشتیبانی نمی‌کند. برای بهترین تجربه به مدلی با قابلیت عامل تغییر دهید.",
"input.costEstimate.creditsPerMillionTokens": "{{credits}} اعتبار/میلیون توکن",
"input.costEstimate.hint": "هزینه تخمینی: ~{{credits}} اعتبار",
"input.costEstimate.inputLabel": "ورودی",
@@ -332,6 +335,7 @@
"messages.modelCard.pricing.outputTokens": "خروجی {{amount}} اعتبار · ${{amount}}/میلیون",
"messages.modelCard.pricing.writeCacheInputTokens": "نوشتن در کش {{amount}} اعتبار · ${{amount}}/میلیون",
"messages.tokenDetails.average": "میانگین قیمت واحد",
"messages.tokenDetails.cacheRate": "نرخ کش",
"messages.tokenDetails.input": "ورودی",
"messages.tokenDetails.inputAudio": "ورودی صوتی",
"messages.tokenDetails.inputCached": "ورودی کش‌شده",
@@ -1093,7 +1097,21 @@
"workingPanel.review.viewMode.unified": "تغییر به نمای یکپارچه",
"workingPanel.review.wordWrap.disable": "غیرفعال کردن پیچش کلمات",
"workingPanel.review.wordWrap.enable": "فعال کردن پیچش کلمات",
"workingPanel.skills.actions.comingSoon": "به زودی",
"workingPanel.skills.actions.delete": "حذف",
"workingPanel.skills.actions.rename": "تغییر نام",
"workingPanel.skills.actions.view": "مشاهده",
"workingPanel.skills.delete.agentConfirm": "مهارت «{{name}}» را از این عامل حذف کنید؟ این عمل قابل بازگشت نیست.",
"workingPanel.skills.delete.error": "حذف مهارت ناموفق بود",
"workingPanel.skills.delete.success": "مهارت حذف شد",
"workingPanel.skills.delete.title": "حذف مهارت؟",
"workingPanel.skills.delete.userConfirm": "آیا مهارت «{{name}}» را حذف می‌کنید؟ این عمل قابل بازگشت نیست.",
"workingPanel.skills.detail.title": "جزئیات مهارت",
"workingPanel.skills.empty": "هیچ مهارتی در این پروژه یافت نشد",
"workingPanel.skills.rename.action": "تغییر نام",
"workingPanel.skills.rename.error": "تغییر نام مهارت ناموفق بود",
"workingPanel.skills.rename.placeholder": "نام مهارت",
"workingPanel.skills.rename.title": "تغییر نام مهارت",
"workingPanel.skills.section.agent": "مهارت‌های عامل",
"workingPanel.skills.section.project": "مهارت‌های پروژه",
"workingPanel.skills.section.user": "مهارت‌های کاربر",
+17
View File
@@ -1,4 +1,20 @@
{
"fleet.addColumn": "افزودن ستون",
"fleet.allShown": "تمام وظایف در حال اجرا نمایش داده شده‌اند",
"fleet.backToHome": "بازگشت به خانه",
"fleet.closeColumn": "بستن ستون",
"fleet.createTask": "ایجاد وظیفه",
"fleet.empty": "هیچ وظیفه‌ای باز نیست",
"fleet.emptyDesc": "یک وظیفه در حال اجرا را از سمت چپ انتخاب کنید یا از + برای افزودن ستون استفاده کنید.",
"fleet.noRunningTasks": "هیچ وظیفه‌ای در حال اجرا نیست",
"fleet.openInChat": "باز کردن در چت",
"fleet.reply": "پاسخ",
"fleet.runningTasks": "وظایف در حال اجرا",
"fleet.status.idle": "بیکار",
"fleet.status.paused": "متوقف شده",
"fleet.status.running": "در حال اجرا",
"fleet.status.scheduled": "برنامه‌ریزی شده",
"fleet.tooltip": "مشاهده تمام عوامل در کنار یکدیگر",
"gateway.description": "توضیحات",
"gateway.descriptionPlaceholder": "اختیاری",
"gateway.deviceName": "نام دستگاه",
@@ -26,6 +42,7 @@
"navigation.memoryIdentities": "حافظه - هویت‌ها",
"navigation.memoryPreferences": "حافظه - ترجیحات",
"navigation.noPages": "هنوز صفحه‌ای وجود ندارد",
"navigation.observation": "حالت مشاهده",
"navigation.onboarding": "راه‌اندازی",
"navigation.page": "صفحه",
"navigation.pages": "صفحات",
+10
View File
@@ -95,8 +95,18 @@
"pageEditor.duplicateError": "تکثیر صفحه ناموفق بود",
"pageEditor.duplicateSuccess": "صفحه با موفقیت تکثیر شد",
"pageEditor.editMode.checking": "در حال بررسی امکان ویرایش...",
"pageEditor.editMode.draftRestoreCancel": "لغو",
"pageEditor.editMode.draftRestoreContent": "تغییرات ذخیره‌نشده محلی از جلسه قبلی شما یافت شد. آیا می‌خواهید آنها را بازیابی کنید؟",
"pageEditor.editMode.draftRestoreOk": "بازیابی",
"pageEditor.editMode.draftRestoreTitle": "بازیابی پیش‌نویس ذخیره‌نشده",
"pageEditor.editMode.lockLostDescription": "ویرایش‌های اخیر هنوز همگام‌سازی نشده‌اند. ذخیره‌سازی پس از بازیابی اتصال ادامه خواهد یافت.",
"pageEditor.editMode.lockLostTitle": "قفل ویرایش به طور موقت از دست رفت",
"pageEditor.editMode.lockUnstable": "در حال اتصال مجدد به قفل ویرایش...",
"pageEditor.editMode.lockedByOther": "{{name}} در حال ویرایش این سند است",
"pageEditor.editMode.lockedBySelf": "شما در حال ویرایش این سند در یک تب دیگر هستید",
"pageEditor.editMode.lockedBySelfDescription": "ذخیره‌سازی پس از بسته شدن جلسه دیگر یا انقضای قفل آن (~30 ثانیه) ادامه خواهد یافت.",
"pageEditor.editMode.lockedBySomeone": "شخص دیگری در حال ویرایش این سند است",
"pageEditor.editMode.lockedDescription": "صفحه در حالت فقط خواندنی است در حالی که آنها در حال ویرایش هستند. تغییرات شما ذخیره نخواهد شد تا زمانی که آنها کارشان را تمام کنند.",
"pageEditor.editedAt": "آخرین ویرایش در {{time}}",
"pageEditor.editedBy": "آخرین ویرایش توسط {{name}}",
"pageEditor.editorPlaceholder": "برای هوش مصنوعی و دستورات \"/\" را فشار دهید",
+2 -2
View File
@@ -5,8 +5,8 @@
"features.agentSelfIteration.title": "خود‌تکراری عامل",
"features.assistantMessageGroup.desc": "نمایش گروهی پیام‌های عامل و نتایج ابزارهای فراخوانی‌شده به‌صورت یکجا",
"features.assistantMessageGroup.title": "گروه‌بندی پیام‌های عامل",
"features.gatewayMode.desc": "اجرای وظایف ایجنت روی سرور از طریق وب‌سوکت Gateway به‌جای اجرای محلی. این کار سرعت اجرا را افزایش داده و مصرف منابع در دستگاه کاربر را کاهش می‌دهد.",
"features.gatewayMode.title": "اجرای ایجنت در سمت سرور (Gateway)",
"features.fleet.desc": "نمایش گزینه Fleet در نوار عنوان — یک داشبورد کنار هم از تمام وظایف در حال اجرا در میان عوامل شما.",
"features.fleet.title": "نمای Fleet",
"features.groupChat.desc": "فعال‌سازی هماهنگی گفت‌وگوی گروهی چندعاملی.",
"features.groupChat.title": "گفت‌وگوی گروهی (چندعاملی)",
"features.imessage.desc": "اتصال نمایندگان به iMessage از طریق پل BlueBubbles دسکتاپ LobeHub محلی.",
+8 -2
View File
@@ -890,7 +890,7 @@
"skillStore.wantMore.feedback.title": "[درخواست مهارت] مهارتی که نیاز دارید را در یک جمله خلاصه کنید",
"skillStore.wantMore.reachedEnd": "به انتها رسیدید. چیزی که نیاز دارید را پیدا نکردید؟",
"startConversation": "شروع گفتگو",
"storage.actions.copyAgentGroups.button": "کپی به",
"storage.actions.copyAgentGroups.button": "کپی به...",
"storage.actions.copyAgentGroups.desc": "گروه‌های عامل و اعضای آنها را به فضای کاری دیگر یا حساب شخصی کپی کنید.",
"storage.actions.copyAgentGroups.title": "کپی گروه‌های عامل",
"storage.actions.copyLobeAI.button": "کپی به",
@@ -1032,6 +1032,8 @@
"tab.addCustomSkill": "افزودن مهارت سفارشی",
"tab.advanced": "پیشرفته",
"tab.advanced.appUpdates.title": "به‌روزرسانی‌های برنامه",
"tab.advanced.gatewayMode.desc": "اجرای وظایف پشتیبانی شده‌ی عامل‌ها به صورت پیش‌فرض از طریق دروازه ابری. عامل‌های جداگانه می‌توانند این تنظیم را از منوی چت تغییر دهند.",
"tab.advanced.gatewayMode.title": "حالت دروازه",
"tab.advanced.toolsAndDiagnostics.title": "ابزارها و تشخیص‌ها",
"tab.advanced.updateChannel.canary": "کناری",
"tab.advanced.updateChannel.canaryDesc": "فعال‌شده با هر ادغام PR، چندین ساخت در روز. ناپایدارترین.",
@@ -1169,7 +1171,6 @@
"tools.builtins.uninstallConfirm.desc": "آیا مطمئن هستید که می‌خواهید {{name}} را حذف نصب کنید؟ این مهارت از عامل فعلی حذف خواهد شد.",
"tools.builtins.uninstallConfirm.title": "حذف نصب {{name}}",
"tools.builtins.uninstalled": "حذف نصب شد",
"tools.disabled": "مدل فعلی از فراخوانی توابع پشتیبانی نمی‌کند و نمی‌تواند از این مهارت استفاده کند",
"tools.composio.addServer": "افزودن سرور",
"tools.composio.authCompleted": "احراز هویت کامل شد",
"tools.composio.authFailed": "احراز هویت ناموفق بود",
@@ -1186,6 +1187,10 @@
"tools.composio.notEnabled": "سرویس Composio فعال نیست",
"tools.composio.oauthRequired": "لطفاً احراز هویت OAuth را در پنجره جدید کامل کنید",
"tools.composio.pendingAuth": "در انتظار احراز هویت",
"tools.composio.reauthorize": "مجدد مجاز کنید",
"tools.composio.remove": "حذف",
"tools.composio.removeConfirm.desc": "{{name}} به طور دائمی از خدمات متصل شما حذف خواهد شد. این عمل قابل بازگشت نیست.",
"tools.composio.removeConfirm.title": "حذف {{name}}؟",
"tools.composio.serverCreated": "سرور با موفقیت ایجاد شد",
"tools.composio.serverCreatedFailed": "ایجاد سرور ناموفق بود",
"tools.composio.serverRemoved": "سرور حذف شد",
@@ -1238,6 +1243,7 @@
"tools.composio.servers.zendesk.readme": "با Zendesk یکپارچه شوید تا تیکت‌های پشتیبانی و تعاملات مشتری را مدیریت کنید. درخواست‌های پشتیبانی را ایجاد، به‌روزرسانی و پیگیری کرده، به داده‌های مشتری دسترسی داشته و عملیات پشتیبانی خود را بهینه نمایید.",
"tools.composio.tools": "ابزارها",
"tools.composio.verifyAuth": "احراز هویت را کامل کرده‌ام",
"tools.disabled": "مدل فعلی از فراخوانی توابع پشتیبانی نمی‌کند و نمی‌تواند از این مهارت استفاده کند",
"tools.lobehubSkill.authorize": "اعطا مجوز",
"tools.lobehubSkill.connect": "اتصال",
"tools.lobehubSkill.connected": "متصل شد",
+15
View File
@@ -16,6 +16,7 @@
"actions.favorite": "مورد علاقه",
"actions.import": "وارد کردن گفت‌وگو",
"actions.markCompleted": "علامت‌گذاری به‌عنوان انجام‌شده",
"actions.moveToAgent": "انتقال به دستیار دیگر",
"actions.openInNewTab": "باز کردن در تب جدید",
"actions.openInNewWindow": "باز کردن در پنجره جدید",
"actions.removeAll": "حذف تمام گفت‌وگوها",
@@ -80,6 +81,9 @@
"management.bulk.deleteConfirm": "شما در حال حذف {{count}} موضوع هستید. این عمل قابل بازگشت نیست.",
"management.bulk.deleteTitle": "حذف موضوعات؟",
"management.bulk.favorite": "مورد علاقه",
"management.bulk.move": "انتقال به دستیار",
"management.bulk.moveEmpty": "هیچ دستیار دیگری وجود ندارد",
"management.bulk.moveSearchPlaceholder": "جستجوی دستیارها...",
"management.bulk.selectedCount_one": "{{count}} انتخاب شده",
"management.bulk.selectedCount_other": "{{count}} انتخاب شده",
"management.card.noPreview": "پیش‌نمایشی در دسترس نیست",
@@ -118,6 +122,17 @@
"management.group.noProject": "بدون پروژه",
"management.group.none": "هیچ‌کدام",
"management.loadingMore": "در حال بارگذاری موضوعات بیشتر...",
"management.moveModal.back": "بازگشت",
"management.moveModal.confirmContent_one": "آیا {{count}} موضوع به «{{title}}» منتقل شود؟",
"management.moveModal.confirmContent_other": "آیا {{count}} موضوع به «{{title}}» منتقل شوند؟",
"management.moveModal.confirmOk": "انتقال",
"management.moveModal.doneOk": "انجام شد",
"management.moveModal.done_one": "{{count}} موضوع منتقل شد",
"management.moveModal.done_other": "{{count}} موضوع منتقل شدند",
"management.moveModal.error": "انتقال ناموفق بود، لطفاً دوباره تلاش کنید",
"management.moveModal.goToTarget": "رفتن به «{{title}}»",
"management.moveModal.moving": "در حال انتقال...",
"management.moveModal.title": "انتقال موضوعات",
"management.searchPlaceholder": "جستجوی موضوعات این عامل...",
"management.sidebarEntry": "موضوعات",
"management.sort.createdAt": "زمان ایجاد",
+18
View File
@@ -44,6 +44,7 @@
"builtinCopilot": "Copilote intégré",
"chatList.expandMessage": "Développer le message",
"chatList.longMessageDetail": "Voir les détails",
"chatList.refreshing": "Récupération des derniers messages...",
"chatMode.agent": "Agent",
"chatMode.agentCap.env": "Environnement d'exécution",
"chatMode.agentCap.files": "Accès aux fichiers",
@@ -164,6 +165,7 @@
"extendParams.urlContext.title": "Extraire le contenu des liens web",
"followUpPlaceholder": "Donner suite. @ pour attribuer des tâches à dautres agents.",
"followUpPlaceholderHeterogeneous": "Poursuivre.",
"gatewayMode.title": "Mode Passerelle",
"group.desc": "Faites avancer une tâche avec plusieurs agents dans un espace partagé.",
"group.memberTooltip": "Il y a {{count}} membres dans le groupe",
"group.orchestratorThinking": "Lorchestrateur réfléchit...",
@@ -245,6 +247,7 @@
"inbox.title": "Lobe IA",
"input.addAi": "Ajouter un message IA",
"input.addUser": "Ajouter un message utilisateur",
"input.agentModeUnsupportedModel": "Le modèle actuel ne prend pas en charge l'appel d'outils agentiques. Passez à un modèle avec des capacités d'agent pour une meilleure expérience.",
"input.costEstimate.creditsPerMillionTokens": "{{credits}} crédits/M tokens",
"input.costEstimate.hint": "Coût estimé : ~{{credits}} crédits",
"input.costEstimate.inputLabel": "Entrée",
@@ -332,6 +335,7 @@
"messages.modelCard.pricing.outputTokens": "Sortie {{amount}} crédits · {{amount}} $/M",
"messages.modelCard.pricing.writeCacheInputTokens": "Écriture en cache {{amount}} crédits · {{amount}} $/M",
"messages.tokenDetails.average": "Prix unitaire moyen",
"messages.tokenDetails.cacheRate": "Taux de mise en cache",
"messages.tokenDetails.input": "Entrée",
"messages.tokenDetails.inputAudio": "Entrée audio",
"messages.tokenDetails.inputCached": "Entrée en cache",
@@ -1093,7 +1097,21 @@
"workingPanel.review.viewMode.unified": "Passer à la vue unifiée",
"workingPanel.review.wordWrap.disable": "Désactiver le retour à la ligne",
"workingPanel.review.wordWrap.enable": "Activer le retour à la ligne",
"workingPanel.skills.actions.comingSoon": "Bientôt disponible",
"workingPanel.skills.actions.delete": "Supprimer",
"workingPanel.skills.actions.rename": "Renommer",
"workingPanel.skills.actions.view": "Voir",
"workingPanel.skills.delete.agentConfirm": "Supprimer la compétence « {{name}} » de cet agent ? Cette action est irréversible.",
"workingPanel.skills.delete.error": "Échec de la suppression de la compétence",
"workingPanel.skills.delete.success": "Compétence supprimée",
"workingPanel.skills.delete.title": "Supprimer la compétence ?",
"workingPanel.skills.delete.userConfirm": "Désinstaller la compétence « {{name}} » ? Cette action est irréversible.",
"workingPanel.skills.detail.title": "Détails de la compétence",
"workingPanel.skills.empty": "Aucune compétence trouvée dans ce projet",
"workingPanel.skills.rename.action": "Renommer",
"workingPanel.skills.rename.error": "Échec du renommage de la compétence",
"workingPanel.skills.rename.placeholder": "Nom de la compétence",
"workingPanel.skills.rename.title": "Renommer la compétence",
"workingPanel.skills.section.agent": "Compétences de l'agent",
"workingPanel.skills.section.project": "Compétences du projet",
"workingPanel.skills.section.user": "Compétences de l'utilisateur",
+17
View File
@@ -1,4 +1,20 @@
{
"fleet.addColumn": "Ajouter une colonne",
"fleet.allShown": "Toutes les tâches en cours sont affichées",
"fleet.backToHome": "Retour à l'accueil",
"fleet.closeColumn": "Fermer la colonne",
"fleet.createTask": "Créer une tâche",
"fleet.empty": "Aucune tâche ouverte",
"fleet.emptyDesc": "Choisissez une tâche en cours à gauche, ou utilisez + pour ajouter une colonne.",
"fleet.noRunningTasks": "Aucune tâche en cours",
"fleet.openInChat": "Ouvrir dans le chat",
"fleet.reply": "Répondre",
"fleet.runningTasks": "Tâches en cours",
"fleet.status.idle": "Inactif",
"fleet.status.paused": "En pause",
"fleet.status.running": "En cours",
"fleet.status.scheduled": "Planifié",
"fleet.tooltip": "Voir tous les agents côte à côte",
"gateway.description": "Description",
"gateway.descriptionPlaceholder": "Optionnel",
"gateway.deviceName": "Nom de l'appareil",
@@ -26,6 +42,7 @@
"navigation.memoryIdentities": "Mémoire - Identités",
"navigation.memoryPreferences": "Mémoire - Préférences",
"navigation.noPages": "Aucune page pour le moment",
"navigation.observation": "Mode Observation",
"navigation.onboarding": "Intégration",
"navigation.page": "Page",
"navigation.pages": "Pages",
+10
View File
@@ -95,8 +95,18 @@
"pageEditor.duplicateError": "Échec de la duplication de la page",
"pageEditor.duplicateSuccess": "Page dupliquée avec succès",
"pageEditor.editMode.checking": "Vérification de la disponibilité de l'édition…",
"pageEditor.editMode.draftRestoreCancel": "Annuler",
"pageEditor.editMode.draftRestoreContent": "Des modifications locales non enregistrées de votre dernière session ont été trouvées. Les restaurer ?",
"pageEditor.editMode.draftRestoreOk": "Restaurer",
"pageEditor.editMode.draftRestoreTitle": "Restaurer le brouillon non enregistré",
"pageEditor.editMode.lockLostDescription": "Les modifications récentes ne se sont pas encore synchronisées. Elles reprendront l'enregistrement une fois la connexion rétablie.",
"pageEditor.editMode.lockLostTitle": "Verrouillage d'édition temporairement perdu",
"pageEditor.editMode.lockUnstable": "Reconnexion du verrouillage d'édition…",
"pageEditor.editMode.lockedByOther": "{{name}} est en train de modifier ce document",
"pageEditor.editMode.lockedBySelf": "Vous modifiez ce document dans un autre onglet",
"pageEditor.editMode.lockedBySelfDescription": "L'enregistrement reprendra après la fermeture de l'autre session ou l'expiration de son verrouillage (~30s).",
"pageEditor.editMode.lockedBySomeone": "Quelqu'un d'autre est en train de modifier ce document",
"pageEditor.editMode.lockedDescription": "La page est en lecture seule pendant qu'ils modifient. Vos modifications ne seront pas enregistrées tant qu'ils n'auront pas terminé.",
"pageEditor.editedAt": "Dernière modification le {{time}}",
"pageEditor.editedBy": "Dernière modification par {{name}}",
"pageEditor.editorPlaceholder": "Appuyez sur \"/\" pour l'IA et les commandes",
+2 -2
View File
@@ -5,8 +5,8 @@
"features.agentSelfIteration.title": "Autoitération de lagent",
"features.assistantMessageGroup.desc": "Regroupez les messages de l'agent et les résultats de leurs appels d'outils pour les afficher ensemble",
"features.assistantMessageGroup.title": "Regroupement des messages de l'agent",
"features.gatewayMode.desc": "Exécute les tâches de lagent sur le serveur via un WebSocket Gateway au lieu de les exécuter localement. Permet une exécution plus rapide et réduit lutilisation des ressources du client.",
"features.gatewayMode.title": "Exécution de lagent côté serveur (Gateway)",
"features.fleet.desc": "Afficher l'entrée Fleet dans la barre de titre — un tableau de bord côte à côte de toutes les tâches en cours sur vos agents.",
"features.fleet.title": "Vue Fleet",
"features.groupChat.desc": "Activez la coordination de discussions de groupe multi-agents.",
"features.groupChat.title": "Discussion de groupe (multi-agents)",
"features.imessage.desc": "Connecter les agents à iMessage via le pont local LobeHub Desktop BlueBubbles.",
+8 -2
View File
@@ -890,7 +890,7 @@
"skillStore.wantMore.feedback.title": "[Demande de compétence] Résumez la compétence dont vous avez besoin en une phrase",
"skillStore.wantMore.reachedEnd": "Vous êtes arrivé au bout. Vous ne trouvez pas ce que vous cherchez ?",
"startConversation": "Démarrer la conversation",
"storage.actions.copyAgentGroups.button": "Copier vers",
"storage.actions.copyAgentGroups.button": "Copier vers...",
"storage.actions.copyAgentGroups.desc": "Copiez des groupes d'agents et leurs membres dans un autre espace de travail ou compte personnel.",
"storage.actions.copyAgentGroups.title": "Copie des groupes d'agents",
"storage.actions.copyLobeAI.button": "Copier vers",
@@ -1032,6 +1032,8 @@
"tab.addCustomSkill": "Ajouter une compétence personnalisée",
"tab.advanced": "Avancé",
"tab.advanced.appUpdates.title": "Mises à jour de l'application",
"tab.advanced.gatewayMode.desc": "Exécutez par défaut les tâches d'agent prises en charge via le cloud Gateway. Les agents individuels peuvent remplacer cela depuis le menu de chat.",
"tab.advanced.gatewayMode.title": "Mode Gateway",
"tab.advanced.toolsAndDiagnostics.title": "Outils et diagnostics",
"tab.advanced.updateChannel.canary": "Canary",
"tab.advanced.updateChannel.canaryDesc": "Déclenché à chaque fusion de PR, plusieurs builds par jour. Le moins stable.",
@@ -1169,7 +1171,6 @@
"tools.builtins.uninstallConfirm.desc": "Êtes-vous sûr de vouloir désinstaller {{name}} ? Cette compétence sera supprimée de l'agent actuel.",
"tools.builtins.uninstallConfirm.title": "Désinstaller {{name}}",
"tools.builtins.uninstalled": "Désinstallé",
"tools.disabled": "Le modèle actuel ne prend pas en charge les appels de fonction et ne peut pas utiliser cette compétence",
"tools.composio.addServer": "Ajouter un serveur",
"tools.composio.authCompleted": "Authentification terminée",
"tools.composio.authFailed": "Échec de lauthentification",
@@ -1186,6 +1187,10 @@
"tools.composio.notEnabled": "Service Composio non activé",
"tools.composio.oauthRequired": "Veuillez compléter lauthentification OAuth dans la nouvelle fenêtre",
"tools.composio.pendingAuth": "Authentification en attente",
"tools.composio.reauthorize": "Réautoriser",
"tools.composio.remove": "Supprimer",
"tools.composio.removeConfirm.desc": "{{name}} sera définitivement supprimé de vos services connectés. Cette action est irréversible.",
"tools.composio.removeConfirm.title": "Supprimer {{name}} ?",
"tools.composio.serverCreated": "Serveur créé avec succès",
"tools.composio.serverCreatedFailed": "Échec de la création du serveur",
"tools.composio.serverRemoved": "Serveur supprimé",
@@ -1238,6 +1243,7 @@
"tools.composio.servers.zendesk.readme": "Intégrez Zendesk pour gérer les tickets de support et les interactions clients. Créez, mettez à jour et suivez les demandes d'assistance, accédez aux données clients et optimisez vos opérations de support.",
"tools.composio.tools": "outils",
"tools.composio.verifyAuth": "Jai terminé lauthentification",
"tools.disabled": "Le modèle actuel ne prend pas en charge les appels de fonction et ne peut pas utiliser cette compétence",
"tools.lobehubSkill.authorize": "Autoriser",
"tools.lobehubSkill.connect": "Connecter",
"tools.lobehubSkill.connected": "Connecté",
+15
View File
@@ -16,6 +16,7 @@
"actions.favorite": "Favori",
"actions.import": "Importer une conversation",
"actions.markCompleted": "Marquer comme terminé",
"actions.moveToAgent": "Déplacer vers un autre assistant",
"actions.openInNewTab": "Ouvrir dans un nouvel onglet",
"actions.openInNewWindow": "Ouvrir dans une nouvelle fenêtre",
"actions.removeAll": "Supprimer tous les sujets",
@@ -80,6 +81,9 @@
"management.bulk.deleteConfirm": "Vous êtes sur le point de supprimer {{count}} sujets. Cette action est irréversible.",
"management.bulk.deleteTitle": "Supprimer les sujets ?",
"management.bulk.favorite": "Favori",
"management.bulk.move": "Déplacer vers l'assistant",
"management.bulk.moveEmpty": "Aucun autre assistant",
"management.bulk.moveSearchPlaceholder": "Rechercher des assistants…",
"management.bulk.selectedCount_one": "{{count}} sélectionné",
"management.bulk.selectedCount_other": "{{count}} sélectionnés",
"management.card.noPreview": "Aucun aperçu disponible",
@@ -118,6 +122,17 @@
"management.group.noProject": "Aucun projet",
"management.group.none": "Aucun",
"management.loadingMore": "Chargement de plus de sujets…",
"management.moveModal.back": "Retour",
"management.moveModal.confirmContent_one": "Déplacer {{count}} sujet vers « {{title}} » ?",
"management.moveModal.confirmContent_other": "Déplacer {{count}} sujets vers « {{title}} » ?",
"management.moveModal.confirmOk": "Déplacer",
"management.moveModal.doneOk": "Terminé",
"management.moveModal.done_one": "{{count}} sujet déplacé",
"management.moveModal.done_other": "{{count}} sujets déplacés",
"management.moveModal.error": "Échec du déplacement, veuillez réessayer",
"management.moveModal.goToTarget": "Aller à « {{title}} »",
"management.moveModal.moving": "Déplacement…",
"management.moveModal.title": "Déplacer les sujets",
"management.searchPlaceholder": "Rechercher les sujets de cet agent…",
"management.sidebarEntry": "Sujets",
"management.sort.createdAt": "Date de création",
+18
View File
@@ -44,6 +44,7 @@
"builtinCopilot": "Copilota integrato",
"chatList.expandMessage": "Espandi messaggio",
"chatList.longMessageDetail": "Visualizza dettagli",
"chatList.refreshing": "Recupero dei messaggi più recenti...",
"chatMode.agent": "Agente",
"chatMode.agentCap.env": "Ambiente di esecuzione",
"chatMode.agentCap.files": "Accesso ai file",
@@ -164,6 +165,7 @@
"extendParams.urlContext.title": "Estrai Contenuto da Link Web",
"followUpPlaceholder": "Follow-up. Usa @ per assegnare attività ad altri agenti.",
"followUpPlaceholderHeterogeneous": "Continua.",
"gatewayMode.title": "Modalità Gateway",
"group.desc": "Fai avanzare un'attività con più Agenti in uno spazio condiviso.",
"group.memberTooltip": "Ci sono {{count}} membri nel gruppo",
"group.orchestratorThinking": "L'Orchestratore sta pensando...",
@@ -245,6 +247,7 @@
"inbox.title": "Lobe AI",
"input.addAi": "Aggiungi un messaggio AI",
"input.addUser": "Aggiungi un messaggio utente",
"input.agentModeUnsupportedModel": "Il modello attuale non supporta la chiamata di strumenti agentici. Passa a un modello con capacità agentiche per la migliore esperienza.",
"input.costEstimate.creditsPerMillionTokens": "{{credits}} crediti/M token",
"input.costEstimate.hint": "Costo stimato: ~{{credits}} crediti",
"input.costEstimate.inputLabel": "Input",
@@ -332,6 +335,7 @@
"messages.modelCard.pricing.outputTokens": "Output {{amount}} crediti · ${{amount}}/M",
"messages.modelCard.pricing.writeCacheInputTokens": "Scrittura cache {{amount}} crediti · ${{amount}}/M",
"messages.tokenDetails.average": "Prezzo unitario medio",
"messages.tokenDetails.cacheRate": "Tasso di cache",
"messages.tokenDetails.input": "Input",
"messages.tokenDetails.inputAudio": "Input audio",
"messages.tokenDetails.inputCached": "Input memorizzato",
@@ -1093,7 +1097,21 @@
"workingPanel.review.viewMode.unified": "Passa alla vista unificata",
"workingPanel.review.wordWrap.disable": "Disabilita ritorno a capo automatico",
"workingPanel.review.wordWrap.enable": "Abilita ritorno a capo automatico",
"workingPanel.skills.actions.comingSoon": "Prossimamente",
"workingPanel.skills.actions.delete": "Elimina",
"workingPanel.skills.actions.rename": "Rinomina",
"workingPanel.skills.actions.view": "Visualizza",
"workingPanel.skills.delete.agentConfirm": "Rimuovere l'abilità “{{name}}” da questo agente? Questa operazione non può essere annullata.",
"workingPanel.skills.delete.error": "Impossibile eliminare l'abilità",
"workingPanel.skills.delete.success": "Abilità eliminata",
"workingPanel.skills.delete.title": "Eliminare l'abilità?",
"workingPanel.skills.delete.userConfirm": "Disinstallare l'abilità “{{name}}”? Questa operazione non può essere annullata.",
"workingPanel.skills.detail.title": "Dettagli dell'abilità",
"workingPanel.skills.empty": "Nessuna competenza trovata in questo progetto",
"workingPanel.skills.rename.action": "Rinomina",
"workingPanel.skills.rename.error": "Impossibile rinominare l'abilità",
"workingPanel.skills.rename.placeholder": "Nome dell'abilità",
"workingPanel.skills.rename.title": "Rinomina abilità",
"workingPanel.skills.section.agent": "Competenze dell'agente",
"workingPanel.skills.section.project": "Competenze del progetto",
"workingPanel.skills.section.user": "Competenze dell'utente",
+17
View File
@@ -1,4 +1,20 @@
{
"fleet.addColumn": "Aggiungi colonna",
"fleet.allShown": "Tutti i compiti in esecuzione sono mostrati",
"fleet.backToHome": "Torna alla home",
"fleet.closeColumn": "Chiudi colonna",
"fleet.createTask": "Crea compito",
"fleet.empty": "Nessun compito aperto",
"fleet.emptyDesc": "Seleziona un compito in esecuzione a sinistra o usa + per aggiungere una colonna.",
"fleet.noRunningTasks": "Nessun compito in esecuzione",
"fleet.openInChat": "Apri nella chat",
"fleet.reply": "Rispondi",
"fleet.runningTasks": "Compiti in esecuzione",
"fleet.status.idle": "Inattivo",
"fleet.status.paused": "In pausa",
"fleet.status.running": "In esecuzione",
"fleet.status.scheduled": "Programmato",
"fleet.tooltip": "Visualizza tutti gli agenti fianco a fianco",
"gateway.description": "Descrizione",
"gateway.descriptionPlaceholder": "Facoltativo",
"gateway.deviceName": "Nome Dispositivo",
@@ -26,6 +42,7 @@
"navigation.memoryIdentities": "Memoria - Identità",
"navigation.memoryPreferences": "Memoria - Preferenze",
"navigation.noPages": "Nessuna pagina ancora",
"navigation.observation": "Modalità Osservazione",
"navigation.onboarding": "Onboarding",
"navigation.page": "Pagina",
"navigation.pages": "Pagine",
+10
View File
@@ -95,8 +95,18 @@
"pageEditor.duplicateError": "Duplicazione della pagina non riuscita",
"pageEditor.duplicateSuccess": "Pagina duplicata con successo",
"pageEditor.editMode.checking": "Verifica della disponibilità di modifica…",
"pageEditor.editMode.draftRestoreCancel": "Annulla",
"pageEditor.editMode.draftRestoreContent": "Sono state trovate modifiche locali non salvate dalla tua ultima sessione. Ripristinarle?",
"pageEditor.editMode.draftRestoreOk": "Ripristina",
"pageEditor.editMode.draftRestoreTitle": "Ripristina Bozza Non Salvata",
"pageEditor.editMode.lockLostDescription": "Le modifiche recenti non sono ancora state sincronizzate. Il salvataggio riprenderà una volta che la connessione sarà ristabilita.",
"pageEditor.editMode.lockLostTitle": "Blocco di modifica temporaneamente perso",
"pageEditor.editMode.lockUnstable": "Riconnessione al blocco di modifica in corso…",
"pageEditor.editMode.lockedByOther": "{{name}} sta modificando questo documento",
"pageEditor.editMode.lockedBySelf": "Stai modificando questo documento in un'altra scheda",
"pageEditor.editMode.lockedBySelfDescription": "Il salvataggio riprenderà dopo che l'altra sessione sarà chiusa o il suo blocco scadrà (~30s).",
"pageEditor.editMode.lockedBySomeone": "Qualcun altro sta modificando questo documento",
"pageEditor.editMode.lockedDescription": "La pagina è in sola lettura mentre viene modificata da un altro utente. Le tue modifiche non saranno salvate finché non avrà terminato.",
"pageEditor.editedAt": "Ultima modifica il {{time}}",
"pageEditor.editedBy": "Ultima modifica di {{name}}",
"pageEditor.editorPlaceholder": "Premi \"/\" per AI e comandi",
+2 -2
View File
@@ -5,8 +5,8 @@
"features.agentSelfIteration.title": "Auto-iterazione dell'agente",
"features.assistantMessageGroup.desc": "Raggruppa i messaggi dell'agente e i risultati delle chiamate agli strumenti per una visualizzazione unificata",
"features.assistantMessageGroup.title": "Raggruppamento Messaggi Agente",
"features.gatewayMode.desc": "Esegui le attività dellagente sul server tramite Gateway WebSocket invece di eseguirle in locale. Consente unesecuzione più rapida e riduce lutilizzo delle risorse del client.",
"features.gatewayMode.title": "Esecuzione dellAgente Lato Server (Gateway)",
"features.fleet.desc": "Mostra la voce Fleet nella barra del titolo — un pannello affiancato di tutte le attività in corso tra i tuoi agenti.",
"features.fleet.title": "Vista Fleet",
"features.groupChat.desc": "Abilita il coordinamento della chat di gruppo con più agenti.",
"features.groupChat.title": "Chat di Gruppo (Multi-Agente)",
"features.imessage.desc": "Collega gli agenti a iMessage tramite il bridge locale LobeHub Desktop BlueBubbles.",
+8 -2
View File
@@ -890,7 +890,7 @@
"skillStore.wantMore.feedback.title": "[Richiesta Skill] Riassumi in una frase la skill di cui hai bisogno",
"skillStore.wantMore.reachedEnd": "Hai raggiunto la fine. Non riesci a trovare ciò che cerchi?",
"startConversation": "Inizia Conversazione",
"storage.actions.copyAgentGroups.button": "Copia in",
"storage.actions.copyAgentGroups.button": "Copia in...",
"storage.actions.copyAgentGroups.desc": "Copia gruppi di agenti e i loro membri in un altro spazio di lavoro o account personale.",
"storage.actions.copyAgentGroups.title": "Copia gruppi di agenti",
"storage.actions.copyLobeAI.button": "Copia in",
@@ -1032,6 +1032,8 @@
"tab.addCustomSkill": "Aggiungi skill personalizzata",
"tab.advanced": "Avanzato",
"tab.advanced.appUpdates.title": "Aggiornamenti dell'app",
"tab.advanced.gatewayMode.desc": "Esegui le attività degli agenti supportati tramite il Gateway cloud per impostazione predefinita. Gli agenti individuali possono sovrascrivere questa impostazione dal menu chat.",
"tab.advanced.gatewayMode.title": "Modalità Gateway",
"tab.advanced.toolsAndDiagnostics.title": "Strumenti e diagnostica",
"tab.advanced.updateChannel.canary": "Canary",
"tab.advanced.updateChannel.canaryDesc": "Attivato ad ogni merge di PR, con più build al giorno. La versione meno stabile.",
@@ -1169,7 +1171,6 @@
"tools.builtins.uninstallConfirm.desc": "Sei sicuro di voler disinstallare {{name}}? Questa funzione verrà rimossa dall'agente attuale.",
"tools.builtins.uninstallConfirm.title": "Disinstalla {{name}}",
"tools.builtins.uninstalled": "Disinstallato",
"tools.disabled": "Il modello attuale non supporta le chiamate di funzione e non può utilizzare la competenza",
"tools.composio.addServer": "Aggiungi Server",
"tools.composio.authCompleted": "Autenticazione Completata",
"tools.composio.authFailed": "Autenticazione Fallita",
@@ -1186,6 +1187,10 @@
"tools.composio.notEnabled": "Servizio Composio non abilitato",
"tools.composio.oauthRequired": "Completa l'autenticazione OAuth nella nuova finestra",
"tools.composio.pendingAuth": "Autenticazione in Attesa",
"tools.composio.reauthorize": "Ri-autorizza",
"tools.composio.remove": "Rimuovi",
"tools.composio.removeConfirm.desc": "{{name}} sarà rimosso definitivamente dai tuoi servizi connessi. Questa azione non può essere annullata.",
"tools.composio.removeConfirm.title": "Rimuovere {{name}}?",
"tools.composio.serverCreated": "Server creato con successo",
"tools.composio.serverCreatedFailed": "Creazione server fallita",
"tools.composio.serverRemoved": "Server rimosso",
@@ -1238,6 +1243,7 @@
"tools.composio.servers.zendesk.readme": "Integra con Zendesk per gestire ticket di supporto e interazioni con i clienti. Crea, aggiorna e monitora richieste di supporto, accedi ai dati dei clienti e ottimizza le operazioni di assistenza.",
"tools.composio.tools": "strumenti",
"tools.composio.verifyAuth": "Ho completato l'autenticazione",
"tools.disabled": "Il modello attuale non supporta le chiamate di funzione e non può utilizzare la competenza",
"tools.lobehubSkill.authorize": "Autorizza",
"tools.lobehubSkill.connect": "Connetti",
"tools.lobehubSkill.connected": "Connesso",
+15
View File
@@ -16,6 +16,7 @@
"actions.favorite": "Preferito",
"actions.import": "Importa Conversazione",
"actions.markCompleted": "Segna come completato",
"actions.moveToAgent": "Sposta a un altro assistente",
"actions.openInNewTab": "Apri in una nuova scheda",
"actions.openInNewWindow": "Apri in una nuova finestra",
"actions.removeAll": "Elimina Tutti gli Argomenti",
@@ -80,6 +81,9 @@
"management.bulk.deleteConfirm": "Stai per eliminare {{count}} argomenti. Questa azione non può essere annullata.",
"management.bulk.deleteTitle": "Eliminare gli argomenti?",
"management.bulk.favorite": "Preferiti",
"management.bulk.move": "Sposta all'assistente",
"management.bulk.moveEmpty": "Nessun altro assistente",
"management.bulk.moveSearchPlaceholder": "Cerca assistenti…",
"management.bulk.selectedCount_one": "{{count}} selezionato",
"management.bulk.selectedCount_other": "{{count}} selezionati",
"management.card.noPreview": "Anteprima non disponibile",
@@ -118,6 +122,17 @@
"management.group.noProject": "Nessun progetto",
"management.group.none": "Nessuno",
"management.loadingMore": "Caricamento di altri argomenti…",
"management.moveModal.back": "Indietro",
"management.moveModal.confirmContent_one": "Spostare {{count}} argomento in “{{title}}”?",
"management.moveModal.confirmContent_other": "Spostare {{count}} argomenti in “{{title}}”?",
"management.moveModal.confirmOk": "Sposta",
"management.moveModal.doneOk": "Fatto",
"management.moveModal.done_one": "{{count}} argomento spostato",
"management.moveModal.done_other": "{{count}} argomenti spostati",
"management.moveModal.error": "Spostamento fallito, riprova",
"management.moveModal.goToTarget": "Vai a “{{title}}”",
"management.moveModal.moving": "Spostamento in corso…",
"management.moveModal.title": "Sposta argomenti",
"management.searchPlaceholder": "Cerca gli argomenti di questo agente…",
"management.sidebarEntry": "Argomenti",
"management.sort.createdAt": "Data di creazione",
+18
View File
@@ -44,6 +44,7 @@
"builtinCopilot": "内蔵 Copilot",
"chatList.expandMessage": "メッセージを展開",
"chatList.longMessageDetail": "詳細を見る",
"chatList.refreshing": "最新のメッセージを取得しています...",
"chatMode.agent": "エージェント",
"chatMode.agentCap.env": "実行環境",
"chatMode.agentCap.files": "ファイルアクセス",
@@ -164,6 +165,7 @@
"extendParams.urlContext.title": "ウェブリンクコンテンツの抽出",
"followUpPlaceholder": "フォローアップ。@で他のエージェントにタスクを割り当てできます。",
"followUpPlaceholderHeterogeneous": "フォローアップ。",
"gatewayMode.title": "ゲートウェイモード",
"group.desc": "同一の対話空間で、複数のアシスタントが一緒にタスクを推進します",
"group.memberTooltip": "グループに {{count}} 名のメンバーがいます",
"group.orchestratorThinking": "ホストが思考中…",
@@ -245,6 +247,7 @@
"inbox.title": "Lobe AI",
"input.addAi": "アシスタントメッセージを追加",
"input.addUser": "ユーザーメッセージを追加",
"input.agentModeUnsupportedModel": "現在のモデルはエージェントツールの呼び出しをサポートしていません。エージェント機能を備えたモデルに切り替えると、最適な体験が得られます。",
"input.costEstimate.creditsPerMillionTokens": "{{credits}} クレジット/Mトークン",
"input.costEstimate.hint": "推定コスト: 約{{credits}} クレジット",
"input.costEstimate.inputLabel": "入力",
@@ -332,6 +335,7 @@
"messages.modelCard.pricing.outputTokens": "出力 {{amount}} クレジット · ${{amount}}/M",
"messages.modelCard.pricing.writeCacheInputTokens": "キャッシュ書き込み {{amount}} クレジット · ${{amount}}/M",
"messages.tokenDetails.average": "平均単価",
"messages.tokenDetails.cacheRate": "キャッシュ率",
"messages.tokenDetails.input": "入力",
"messages.tokenDetails.inputAudio": "音声入力",
"messages.tokenDetails.inputCached": "キャッシュ入力",
@@ -1093,7 +1097,21 @@
"workingPanel.review.viewMode.unified": "統合ビューに切り替え",
"workingPanel.review.wordWrap.disable": "ワードラップを無効化",
"workingPanel.review.wordWrap.enable": "ワードラップを有効化",
"workingPanel.skills.actions.comingSoon": "近日公開",
"workingPanel.skills.actions.delete": "削除",
"workingPanel.skills.actions.rename": "名前を変更",
"workingPanel.skills.actions.view": "表示",
"workingPanel.skills.delete.agentConfirm": "このエージェントからスキル「{{name}}」を削除しますか?この操作は元に戻せません。",
"workingPanel.skills.delete.error": "スキルの削除に失敗しました",
"workingPanel.skills.delete.success": "スキルが削除されました",
"workingPanel.skills.delete.title": "スキルを削除しますか?",
"workingPanel.skills.delete.userConfirm": "スキル「{{name}}」をアンインストールしますか?この操作は元に戻せません。",
"workingPanel.skills.detail.title": "スキルの詳細",
"workingPanel.skills.empty": "このプロジェクトにはスキルが見つかりませんでした",
"workingPanel.skills.rename.action": "名前を変更",
"workingPanel.skills.rename.error": "スキルの名前変更に失敗しました",
"workingPanel.skills.rename.placeholder": "スキル名",
"workingPanel.skills.rename.title": "スキルの名前を変更",
"workingPanel.skills.section.agent": "エージェントスキル",
"workingPanel.skills.section.project": "プロジェクトスキル",
"workingPanel.skills.section.user": "ユーザースキル",
+17
View File
@@ -1,4 +1,20 @@
{
"fleet.addColumn": "列を追加",
"fleet.allShown": "すべての実行中のタスクが表示されています",
"fleet.backToHome": "ホームに戻る",
"fleet.closeColumn": "列を閉じる",
"fleet.createTask": "タスクを作成",
"fleet.empty": "開いているタスクはありません",
"fleet.emptyDesc": "左側の実行中のタスクを選択するか、+を使用して列を追加してください。",
"fleet.noRunningTasks": "実行中のタスクはありません",
"fleet.openInChat": "チャットで開く",
"fleet.reply": "返信",
"fleet.runningTasks": "実行中のタスク",
"fleet.status.idle": "待機中",
"fleet.status.paused": "一時停止中",
"fleet.status.running": "実行中",
"fleet.status.scheduled": "スケジュール済み",
"fleet.tooltip": "すべてのエージェントを並べて表示",
"gateway.description": "説明",
"gateway.descriptionPlaceholder": "任意",
"gateway.deviceName": "デバイス名",
@@ -26,6 +42,7 @@
"navigation.memoryIdentities": "メモリ - アイデンティティ",
"navigation.memoryPreferences": "メモリ - 設定",
"navigation.noPages": "ページがまだありません",
"navigation.observation": "観察モード",
"navigation.onboarding": "オンボーディング",
"navigation.page": "ページ",
"navigation.pages": "ページ",
+10
View File
@@ -95,8 +95,18 @@
"pageEditor.duplicateError": "ページの複製に失敗しました",
"pageEditor.duplicateSuccess": "ページを正常に複製しました",
"pageEditor.editMode.checking": "編集可能か確認しています…",
"pageEditor.editMode.draftRestoreCancel": "破棄",
"pageEditor.editMode.draftRestoreContent": "前回のセッションから保存されていないローカル変更が見つかりました。復元しますか?",
"pageEditor.editMode.draftRestoreOk": "復元",
"pageEditor.editMode.draftRestoreTitle": "保存されていない下書きの復元",
"pageEditor.editMode.lockLostDescription": "最近の編集内容がまだ同期されていません。接続が回復すると保存が再開されます。",
"pageEditor.editMode.lockLostTitle": "編集ロックが一時的に失われました",
"pageEditor.editMode.lockUnstable": "編集ロックを再接続中…",
"pageEditor.editMode.lockedByOther": "{{name}}がこのドキュメントを編集しています",
"pageEditor.editMode.lockedBySelf": "このドキュメントを別のタブで編集しています",
"pageEditor.editMode.lockedBySelfDescription": "保存は他のセッションが終了するか、そのロックが期限切れになる(約30秒)と再開されます。",
"pageEditor.editMode.lockedBySomeone": "他の誰かがこのドキュメントを編集しています",
"pageEditor.editMode.lockedDescription": "他のユーザーが編集中のため、このページは読み取り専用です。編集が完了するまで変更は保存されません。",
"pageEditor.editedAt": "最終編集:{{time}}",
"pageEditor.editedBy": "最終編集者:{{name}}",
"pageEditor.editorPlaceholder": "「/」で AI とコマンドを呼び出し",
+2 -2
View File
@@ -5,8 +5,8 @@
"features.agentSelfIteration.title": "エージェントの自己反復",
"features.assistantMessageGroup.desc": "アシスタントのメッセージとそのツール呼び出し結果をグループ化して表示します",
"features.assistantMessageGroup.title": "アシスタントメッセージのグループ化表示",
"features.gatewayMode.desc": "エージェントのタスクをローカルではなく Gateway WebSocket を介してサーバー上で実行します。これにより、より高速な処理が可能になり、クライアント側のリソース消費を削減できます。",
"features.gatewayMode.title": "サーバーサイドエージェント実行(Gateway)",
"features.fleet.desc": "タイトルバーにフリートエントリを表示します。エージェント全体で実行中のタスクを並べて表示するダッシュボードです。",
"features.fleet.title": "フリートビュー",
"features.groupChat.desc": "複数のAIアシスタントによるグループチャット機能を有効にします。",
"features.groupChat.title": "グループチャット(マルチアシスタント)",
"features.imessage.desc": "LobeHub Desktop BlueBubblesブリッジを介してエージェントをiMessageに接続します。",
+8 -2
View File
@@ -890,7 +890,7 @@
"skillStore.wantMore.feedback.title": "【スキルリクエスト】必要なスキルを一文で要約してください",
"skillStore.wantMore.reachedEnd": "これ以上表示できません。お探しのものが見つかりませんか?",
"startConversation": "会話を開始する",
"storage.actions.copyAgentGroups.button": "コピー先",
"storage.actions.copyAgentGroups.button": "コピー先...",
"storage.actions.copyAgentGroups.desc": "エージェントグループとそのメンバーエージェントを別のワークスペースまたは個人アカウントにコピーします。",
"storage.actions.copyAgentGroups.title": "エージェントグループのコピー",
"storage.actions.copyLobeAI.button": "コピー先",
@@ -1032,6 +1032,8 @@
"tab.addCustomSkill": "カスタムスキルを追加",
"tab.advanced": "高度な設定",
"tab.advanced.appUpdates.title": "アプリの更新",
"tab.advanced.gatewayMode.desc": "サポートされているエージェントタスクをデフォルトでクラウドゲートウェイを通じて実行します。個々のエージェントはチャットメニューからこれを上書きすることができます。",
"tab.advanced.gatewayMode.title": "ゲートウェイモード",
"tab.advanced.toolsAndDiagnostics.title": "ツールと診断",
"tab.advanced.updateChannel.canary": "カナリア",
"tab.advanced.updateChannel.canaryDesc": "すべてのPRマージでトリガーされ、1日に複数回ビルドされます。最も不安定です。",
@@ -1169,7 +1171,6 @@
"tools.builtins.uninstallConfirm.desc": "{{name}} をアンインストールしてもよろしいですか?このスキルは現在のエージェントから削除されます。",
"tools.builtins.uninstallConfirm.title": "{{name}} のアンインストール",
"tools.builtins.uninstalled": "アンインストール済み",
"tools.disabled": "現在のモデルは関数呼び出しをサポートしていません。スキルを使用できません",
"tools.composio.addServer": "サーバーを追加",
"tools.composio.authCompleted": "認証完了",
"tools.composio.authFailed": "認証に失敗しました",
@@ -1186,6 +1187,10 @@
"tools.composio.notEnabled": "Composio サービスは有効化されていません",
"tools.composio.oauthRequired": "新しいウィンドウで OAuth 認証を完了してください",
"tools.composio.pendingAuth": "認証待ち",
"tools.composio.reauthorize": "再認証",
"tools.composio.remove": "削除",
"tools.composio.removeConfirm.desc": "{{name}} は接続されたサービスから永久に削除されます。この操作は元に戻すことができません。",
"tools.composio.removeConfirm.title": "{{name}} を削除しますか?",
"tools.composio.serverCreated": "サーバーが正常に作成されました",
"tools.composio.serverCreatedFailed": "サーバーの作成に失敗しました",
"tools.composio.serverRemoved": "サーバーが削除されました",
@@ -1238,6 +1243,7 @@
"tools.composio.servers.zendesk.readme": "Zendeskと連携してサポートチケットや顧客対応を管理します。サポートリクエストの作成、更新、追跡、顧客データへのアクセス、サポート業務の効率化が可能です。",
"tools.composio.tools": "個のツール",
"tools.composio.verifyAuth": "認証を完了しました",
"tools.disabled": "現在のモデルは関数呼び出しをサポートしていません。スキルを使用できません",
"tools.lobehubSkill.authorize": "認証する",
"tools.lobehubSkill.connect": "接続する",
"tools.lobehubSkill.connected": "接続済み",
+15
View File
@@ -16,6 +16,7 @@
"actions.favorite": "お気に入り",
"actions.import": "会話をインポート",
"actions.markCompleted": "完了としてマーク",
"actions.moveToAgent": "別のアシスタントに移動",
"actions.openInNewTab": "新しいタブで開く",
"actions.openInNewWindow": "新しいウィンドウで開く",
"actions.removeAll": "すべてのトピックを削除",
@@ -80,6 +81,9 @@
"management.bulk.deleteConfirm": "{{count}}件のトピックを削除しようとしています。この操作は元に戻せません。",
"management.bulk.deleteTitle": "トピックを削除しますか?",
"management.bulk.favorite": "お気に入り",
"management.bulk.move": "アシスタントに移動",
"management.bulk.moveEmpty": "他のアシスタントがありません",
"management.bulk.moveSearchPlaceholder": "アシスタントを検索…",
"management.bulk.selectedCount_one": "{{count}}件選択済み",
"management.bulk.selectedCount_other": "{{count}}件選択済み",
"management.card.noPreview": "プレビューは利用できません",
@@ -118,6 +122,17 @@
"management.group.noProject": "プロジェクトなし",
"management.group.none": "なし",
"management.loadingMore": "さらにトピックを読み込み中…",
"management.moveModal.back": "戻る",
"management.moveModal.confirmContent_one": "{{count}} 件のトピックを「{{title}}」に移動しますか?",
"management.moveModal.confirmContent_other": "{{count}} 件のトピックを「{{title}}」に移動しますか?",
"management.moveModal.confirmOk": "移動",
"management.moveModal.doneOk": "完了",
"management.moveModal.done_one": "{{count}} 件のトピックを移動しました",
"management.moveModal.done_other": "{{count}} 件のトピックを移動しました",
"management.moveModal.error": "移動に失敗しました。もう一度お試しください",
"management.moveModal.goToTarget": "「{{title}}」に移動",
"management.moveModal.moving": "移動中…",
"management.moveModal.title": "トピックを移動",
"management.searchPlaceholder": "このエージェントのトピックを検索…",
"management.sidebarEntry": "トピック",
"management.sort.createdAt": "作成日時",
+18
View File
@@ -44,6 +44,7 @@
"builtinCopilot": "내장 Copilot",
"chatList.expandMessage": "메시지 펼치기",
"chatList.longMessageDetail": "자세히 보기",
"chatList.refreshing": "최신 메시지를 가져오는 중...",
"chatMode.agent": "에이전트",
"chatMode.agentCap.env": "실행 환경",
"chatMode.agentCap.files": "파일 접근",
@@ -164,6 +165,7 @@
"extendParams.urlContext.title": "웹 링크 컨텐츠 추출",
"followUpPlaceholder": "후속 작업. 다른 에이전트에게 작업을 할당하려면 @를 사용하세요.",
"followUpPlaceholderHeterogeneous": "후속 메시지를 입력하세요.",
"gatewayMode.title": "게이트웨이 모드",
"group.desc": "동일한 대화 공간에서 여러 도우미가 함께 작업을 추진합니다",
"group.memberTooltip": "그룹에 {{count}}명의 구성원이 있습니다",
"group.orchestratorThinking": "호스트가 생각 중…",
@@ -245,6 +247,7 @@
"inbox.title": "Lobe AI",
"input.addAi": "도우미 메시지 추가",
"input.addUser": "사용자 메시지 추가",
"input.agentModeUnsupportedModel": "현재 모델은 에이전트 도구 호출을 지원하지 않습니다. 최상의 경험을 위해 에이전트 기능이 있는 모델로 전환하세요.",
"input.costEstimate.creditsPerMillionTokens": "{{credits}} 크레딧/M 토큰",
"input.costEstimate.hint": "예상 비용: 약 {{credits}} 크레딧",
"input.costEstimate.inputLabel": "입력",
@@ -332,6 +335,7 @@
"messages.modelCard.pricing.outputTokens": "출력 {{amount}}크레딧 · ${{amount}}/M",
"messages.modelCard.pricing.writeCacheInputTokens": "캐시 쓰기 {{amount}}크레딧 · ${{amount}}/M",
"messages.tokenDetails.average": "평균 단가",
"messages.tokenDetails.cacheRate": "캐시 비율",
"messages.tokenDetails.input": "입력",
"messages.tokenDetails.inputAudio": "오디오 입력",
"messages.tokenDetails.inputCached": "입력 캐시",
@@ -1093,7 +1097,21 @@
"workingPanel.review.viewMode.unified": "통합 보기로 전환",
"workingPanel.review.wordWrap.disable": "자동 줄 바꿈 비활성화",
"workingPanel.review.wordWrap.enable": "자동 줄 바꿈 활성화",
"workingPanel.skills.actions.comingSoon": "곧 제공 예정",
"workingPanel.skills.actions.delete": "삭제",
"workingPanel.skills.actions.rename": "이름 변경",
"workingPanel.skills.actions.view": "보기",
"workingPanel.skills.delete.agentConfirm": "이 에이전트에서 스킬 “{{name}}”을(를) 제거하시겠습니까? 이 작업은 되돌릴 수 없습니다.",
"workingPanel.skills.delete.error": "스킬 삭제 실패",
"workingPanel.skills.delete.success": "스킬이 삭제되었습니다",
"workingPanel.skills.delete.title": "스킬을 삭제하시겠습니까?",
"workingPanel.skills.delete.userConfirm": "스킬 “{{name}}”을(를) 제거하시겠습니까? 이 작업은 되돌릴 수 없습니다.",
"workingPanel.skills.detail.title": "스킬 세부정보",
"workingPanel.skills.empty": "이 프로젝트에서 발견된 스킬이 없습니다",
"workingPanel.skills.rename.action": "이름 변경",
"workingPanel.skills.rename.error": "스킬 이름 변경 실패",
"workingPanel.skills.rename.placeholder": "스킬 이름",
"workingPanel.skills.rename.title": "스킬 이름 변경",
"workingPanel.skills.section.agent": "에이전트 스킬",
"workingPanel.skills.section.project": "프로젝트 스킬",
"workingPanel.skills.section.user": "사용자 스킬",
+17
View File
@@ -1,4 +1,20 @@
{
"fleet.addColumn": "열 추가",
"fleet.allShown": "모든 실행 중인 작업이 표시됩니다",
"fleet.backToHome": "홈으로 돌아가기",
"fleet.closeColumn": "열 닫기",
"fleet.createTask": "작업 생성",
"fleet.empty": "열린 작업 없음",
"fleet.emptyDesc": "왼쪽에서 실행 중인 작업을 선택하거나 +를 사용하여 열을 추가하세요.",
"fleet.noRunningTasks": "실행 중인 작업 없음",
"fleet.openInChat": "채팅에서 열기",
"fleet.reply": "답장",
"fleet.runningTasks": "실행 중인 작업",
"fleet.status.idle": "대기 중",
"fleet.status.paused": "일시 중지됨",
"fleet.status.running": "실행 중",
"fleet.status.scheduled": "예약됨",
"fleet.tooltip": "모든 에이전트를 나란히 보기",
"gateway.description": "설명",
"gateway.descriptionPlaceholder": "선택 사항",
"gateway.deviceName": "장치 이름",
@@ -26,6 +42,7 @@
"navigation.memoryIdentities": "메모리 - 신원",
"navigation.memoryPreferences": "메모리 - 선호도",
"navigation.noPages": "아직 페이지가 없습니다",
"navigation.observation": "관찰 모드",
"navigation.onboarding": "온보딩",
"navigation.page": "페이지",
"navigation.pages": "페이지",
+10
View File
@@ -95,8 +95,18 @@
"pageEditor.duplicateError": "페이지 복제에 실패했습니다",
"pageEditor.duplicateSuccess": "페이지가 성공적으로 복제되었습니다",
"pageEditor.editMode.checking": "편집 가능 여부 확인 중…",
"pageEditor.editMode.draftRestoreCancel": "취소",
"pageEditor.editMode.draftRestoreContent": "이전 세션에서 저장되지 않은 로컬 변경 사항이 발견되었습니다. 복원하시겠습니까?",
"pageEditor.editMode.draftRestoreOk": "복원",
"pageEditor.editMode.draftRestoreTitle": "저장되지 않은 초안 복원",
"pageEditor.editMode.lockLostDescription": "최근 편집 내용이 아직 동기화되지 않았습니다. 연결이 복구되면 저장이 재개됩니다.",
"pageEditor.editMode.lockLostTitle": "편집 잠금이 일시적으로 해제되었습니다",
"pageEditor.editMode.lockUnstable": "편집 잠금 재연결 중…",
"pageEditor.editMode.lockedByOther": "{{name}}님이 이 문서를 편집 중입니다",
"pageEditor.editMode.lockedBySelf": "이 문서를 다른 탭에서 편집 중입니다",
"pageEditor.editMode.lockedBySelfDescription": "다른 세션이 닫히거나 잠금이 만료된 후(~30초) 저장이 재개됩니다.",
"pageEditor.editMode.lockedBySomeone": "다른 사람이 이 문서를 편집 중입니다",
"pageEditor.editMode.lockedDescription": "다른 사용자가 편집 중인 동안 페이지는 읽기 전용 상태입니다. 그들이 완료할 때까지 변경 사항이 저장되지 않습니다.",
"pageEditor.editedAt": "마지막 편집: {{time}}",
"pageEditor.editedBy": "마지막 편집자: {{name}}",
"pageEditor.editorPlaceholder": "/를 눌러 AI 및 명령어 사용",
+2 -2
View File
@@ -5,8 +5,8 @@
"features.agentSelfIteration.title": "에이전트 자기 반복",
"features.assistantMessageGroup.desc": "도우미 메시지와 해당 도구 호출 결과를 그룹으로 묶어 표시합니다",
"features.assistantMessageGroup.title": "도우미 메시지 그룹화",
"features.gatewayMode.desc": "로컬에서 실행하는 대신 Gateway WebSocket을 통해 서버에서 에이전트 작업을 실행합니다. 더 빠른 처리 속도를 제공하고 클라이언트 리소스 사용을 줄여줍니다.",
"features.gatewayMode.title": "서버 사이드 에이전트 실행(Gateway)",
"features.fleet.desc": "제목 표시줄에 Fleet 항목을 표시합니다 — 에이전트 간 실행 중인 모든 작업의 대시보드를 나란히 볼 수 있습니다.",
"features.fleet.title": "Fleet 보기",
"features.groupChat.desc": "다중 도우미 그룹 채팅 조정 기능을 활성화합니다.",
"features.groupChat.title": "그룹 채팅 (다중 도우미)",
"features.imessage.desc": "로컬 LobeHub Desktop BlueBubbles 브리지를 통해 에이전트를 iMessage에 연결합니다.",
+8 -2
View File
@@ -890,7 +890,7 @@
"skillStore.wantMore.feedback.title": "[스킬 요청] 필요한 스킬을 한 문장으로 요약해 주세요",
"skillStore.wantMore.reachedEnd": "마지막까지 확인하셨습니다. 원하는 항목이 없으신가요?",
"startConversation": "대화를 시작하기",
"storage.actions.copyAgentGroups.button": "복사",
"storage.actions.copyAgentGroups.button": "복사 대상...",
"storage.actions.copyAgentGroups.desc": "에이전트 그룹과 해당 멤버 에이전트를 다른 워크스페이스 또는 개인 계정으로 복사합니다.",
"storage.actions.copyAgentGroups.title": "에이전트 그룹 복사",
"storage.actions.copyLobeAI.button": "복사",
@@ -1032,6 +1032,8 @@
"tab.addCustomSkill": "사용자 정의 스킬 추가",
"tab.advanced": "고급",
"tab.advanced.appUpdates.title": "앱 업데이트",
"tab.advanced.gatewayMode.desc": "기본적으로 클라우드 게이트웨이를 통해 지원되는 에이전트 작업을 실행합니다. 개별 에이전트는 채팅 메뉴에서 이를 재정의할 수 있습니다.",
"tab.advanced.gatewayMode.title": "게이트웨이 모드",
"tab.advanced.toolsAndDiagnostics.title": "도구 및 진단",
"tab.advanced.updateChannel.canary": "카나리",
"tab.advanced.updateChannel.canaryDesc": "모든 PR 병합 시 트리거되며, 하루에 여러 빌드가 생성됩니다. 가장 불안정합니다.",
@@ -1169,7 +1171,6 @@
"tools.builtins.uninstallConfirm.desc": "{{name}}을(를) 제거하시겠습니까? 이 기능은 현재 에이전트에서 삭제됩니다.",
"tools.builtins.uninstallConfirm.title": "{{name}} 제거",
"tools.builtins.uninstalled": "제거됨",
"tools.disabled": "현재 모델은 함수 호출을 지원하지 않으며 기능을 사용할 수 없습니다",
"tools.composio.addServer": "서버 추가",
"tools.composio.authCompleted": "인증 완료",
"tools.composio.authFailed": "인증 실패",
@@ -1186,6 +1187,10 @@
"tools.composio.notEnabled": "Composio 서비스가 활성화되지 않음",
"tools.composio.oauthRequired": "새 창에서 OAuth 인증을 완료해 주세요",
"tools.composio.pendingAuth": "인증 대기 중",
"tools.composio.reauthorize": "재인증",
"tools.composio.remove": "제거",
"tools.composio.removeConfirm.desc": "{{name}}이(가) 연결된 서비스에서 영구적으로 제거됩니다. 이 작업은 되돌릴 수 없습니다.",
"tools.composio.removeConfirm.title": "{{name}}을(를) 제거하시겠습니까?",
"tools.composio.serverCreated": "서버 생성 성공",
"tools.composio.serverCreatedFailed": "서버 생성 실패",
"tools.composio.serverRemoved": "서버 삭제됨",
@@ -1238,6 +1243,7 @@
"tools.composio.servers.zendesk.readme": "Zendesk와 통합하여 지원 티켓과 고객 상호작용을 관리하세요. 지원 요청 생성, 업데이트, 추적, 고객 데이터 접근, 지원 운영을 간소화할 수 있습니다.",
"tools.composio.tools": "개의 도구",
"tools.composio.verifyAuth": "인증을 완료했습니다",
"tools.disabled": "현재 모델은 함수 호출을 지원하지 않으며 기능을 사용할 수 없습니다",
"tools.lobehubSkill.authorize": "권한 부여",
"tools.lobehubSkill.connect": "연결",
"tools.lobehubSkill.connected": "연결됨",
+15
View File
@@ -16,6 +16,7 @@
"actions.favorite": "즐겨찾기",
"actions.import": "대화 가져오기",
"actions.markCompleted": "완료로 표시",
"actions.moveToAgent": "다른 어시스턴트로 이동",
"actions.openInNewTab": "새 탭에서 열기",
"actions.openInNewWindow": "새 창에서 열기",
"actions.removeAll": "모든 주제 삭제",
@@ -80,6 +81,9 @@
"management.bulk.deleteConfirm": "{{count}}개의 주제를 삭제하려고 합니다. 이 작업은 되돌릴 수 없습니다.",
"management.bulk.deleteTitle": "주제를 삭제하시겠습니까?",
"management.bulk.favorite": "즐겨찾기",
"management.bulk.move": "어시스턴트로 이동",
"management.bulk.moveEmpty": "다른 어시스턴트 없음",
"management.bulk.moveSearchPlaceholder": "어시스턴트 검색…",
"management.bulk.selectedCount_one": "{{count}}개 선택됨",
"management.bulk.selectedCount_other": "{{count}}개 선택됨",
"management.card.noPreview": "미리보기를 사용할 수 없습니다",
@@ -118,6 +122,17 @@
"management.group.noProject": "프로젝트 없음",
"management.group.none": "없음",
"management.loadingMore": "더 많은 주제를 불러오는 중…",
"management.moveModal.back": "뒤로",
"management.moveModal.confirmContent_one": "{{count}}개의 주제를 “{{title}}”로 이동하시겠습니까?",
"management.moveModal.confirmContent_other": "{{count}}개의 주제를 “{{title}}”로 이동하시겠습니까?",
"management.moveModal.confirmOk": "이동",
"management.moveModal.doneOk": "완료",
"management.moveModal.done_one": "{{count}}개의 주제가 이동되었습니다",
"management.moveModal.done_other": "{{count}}개의 주제가 이동되었습니다",
"management.moveModal.error": "이동 실패, 다시 시도해주세요",
"management.moveModal.goToTarget": "“{{title}}”로 이동",
"management.moveModal.moving": "이동 중…",
"management.moveModal.title": "주제 이동",
"management.searchPlaceholder": "이 에이전트의 주제를 검색하세요…",
"management.sidebarEntry": "주제",
"management.sort.createdAt": "생성 시간",
+18
View File
@@ -44,6 +44,7 @@
"builtinCopilot": "Ingebouwde Copilot",
"chatList.expandMessage": "Bericht uitvouwen",
"chatList.longMessageDetail": "Details bekijken",
"chatList.refreshing": "Laatste berichten ophalen...",
"chatMode.agent": "Agent",
"chatMode.agentCap.env": "Runtime-omgeving",
"chatMode.agentCap.files": "Bestandstoegang",
@@ -164,6 +165,7 @@
"extendParams.urlContext.title": "Webpagina-inhoud ophalen",
"followUpPlaceholder": "Vervolgen. @ om taken toe te wijzen aan andere agenten.",
"followUpPlaceholderHeterogeneous": "Vervolgbericht.",
"gatewayMode.title": "Gateway-modus",
"group.desc": "Werk samen met meerdere Agents in één gedeelde ruimte.",
"group.memberTooltip": "Er zijn {{count}} leden in de groep",
"group.orchestratorThinking": "Orchestrator is aan het denken...",
@@ -245,6 +247,7 @@
"inbox.title": "Lobe AI",
"input.addAi": "AI-bericht toevoegen",
"input.addUser": "Gebruikersbericht toevoegen",
"input.agentModeUnsupportedModel": "Het huidige model ondersteunt geen agentische tooloproepen. Schakel over naar een model met agentmogelijkheden voor de beste ervaring.",
"input.costEstimate.creditsPerMillionTokens": "{{credits}} credits/M tokens",
"input.costEstimate.hint": "Geschatte kosten: ~{{credits}} credits",
"input.costEstimate.inputLabel": "Invoer",
@@ -332,6 +335,7 @@
"messages.modelCard.pricing.outputTokens": "Uitvoer {{amount}} credits · ${{amount}}/M",
"messages.modelCard.pricing.writeCacheInputTokens": "Cache schrijven {{amount}} credits · ${{amount}}/M",
"messages.tokenDetails.average": "Gemiddelde eenheidsprijs",
"messages.tokenDetails.cacheRate": "Cachepercentage",
"messages.tokenDetails.input": "Invoer",
"messages.tokenDetails.inputAudio": "Audio-invoer",
"messages.tokenDetails.inputCached": "Ingevoerde cache",
@@ -1093,7 +1097,21 @@
"workingPanel.review.viewMode.unified": "Schakel over naar uniforme weergave",
"workingPanel.review.wordWrap.disable": "Tekstterugloop uitschakelen",
"workingPanel.review.wordWrap.enable": "Tekstterugloop inschakelen",
"workingPanel.skills.actions.comingSoon": "Binnenkort beschikbaar",
"workingPanel.skills.actions.delete": "Verwijderen",
"workingPanel.skills.actions.rename": "Hernoemen",
"workingPanel.skills.actions.view": "Bekijken",
"workingPanel.skills.delete.agentConfirm": "De vaardigheid “{{name}}” van deze agent verwijderen? Dit kan niet ongedaan worden gemaakt.",
"workingPanel.skills.delete.error": "Vaardigheid verwijderen mislukt",
"workingPanel.skills.delete.success": "Vaardigheid verwijderd",
"workingPanel.skills.delete.title": "Vaardigheid verwijderen?",
"workingPanel.skills.delete.userConfirm": "De vaardigheid “{{name}}” deïnstalleren? Dit kan niet ongedaan worden gemaakt.",
"workingPanel.skills.detail.title": "Vaardigheidsdetails",
"workingPanel.skills.empty": "Geen vaardigheden gevonden in dit project",
"workingPanel.skills.rename.action": "Hernoemen",
"workingPanel.skills.rename.error": "Hernoemen van vaardigheid mislukt",
"workingPanel.skills.rename.placeholder": "Naam van de vaardigheid",
"workingPanel.skills.rename.title": "Vaardigheid hernoemen",
"workingPanel.skills.section.agent": "Agentvaardigheden",
"workingPanel.skills.section.project": "Projectvaardigheden",
"workingPanel.skills.section.user": "Gebruikersvaardigheden",
+17
View File
@@ -1,4 +1,20 @@
{
"fleet.addColumn": "Kolom toevoegen",
"fleet.allShown": "Alle lopende taken worden weergegeven",
"fleet.backToHome": "Terug naar startpagina",
"fleet.closeColumn": "Kolom sluiten",
"fleet.createTask": "Taak aanmaken",
"fleet.empty": "Geen open taken",
"fleet.emptyDesc": "Selecteer een lopende taak aan de linkerkant, of gebruik + om een kolom toe te voegen.",
"fleet.noRunningTasks": "Geen lopende taken",
"fleet.openInChat": "Openen in chat",
"fleet.reply": "Antwoorden",
"fleet.runningTasks": "Lopende taken",
"fleet.status.idle": "Inactief",
"fleet.status.paused": "Gepauzeerd",
"fleet.status.running": "Bezig",
"fleet.status.scheduled": "Gepland",
"fleet.tooltip": "Bekijk alle agenten naast elkaar",
"gateway.description": "Beschrijving",
"gateway.descriptionPlaceholder": "Optioneel",
"gateway.deviceName": "Apparaatnaam",
@@ -26,6 +42,7 @@
"navigation.memoryIdentities": "Geheugen - Identiteiten",
"navigation.memoryPreferences": "Geheugen - Voorkeuren",
"navigation.noPages": "Nog geen pagina's",
"navigation.observation": "Observatiemodus",
"navigation.onboarding": "Onboarding",
"navigation.page": "Pagina",
"navigation.pages": "Pagina's",
+10
View File
@@ -95,8 +95,18 @@
"pageEditor.duplicateError": "Kopiëren van pagina mislukt",
"pageEditor.duplicateSuccess": "Pagina succesvol gekopieerd",
"pageEditor.editMode.checking": "Beschikbaarheid van bewerken controleren…",
"pageEditor.editMode.draftRestoreCancel": "Verwijderen",
"pageEditor.editMode.draftRestoreContent": "Onopgeslagen lokale wijzigingen van je laatste sessie gevonden. Wil je ze herstellen?",
"pageEditor.editMode.draftRestoreOk": "Herstellen",
"pageEditor.editMode.draftRestoreTitle": "Onopgeslagen concept herstellen",
"pageEditor.editMode.lockLostDescription": "Recente wijzigingen zijn nog niet gesynchroniseerd. Ze worden opgeslagen zodra de verbinding is hersteld.",
"pageEditor.editMode.lockLostTitle": "Bewerkingsslot tijdelijk verloren",
"pageEditor.editMode.lockUnstable": "Bewerkingsslot opnieuw verbinden...",
"pageEditor.editMode.lockedByOther": "{{name}} is dit document aan het bewerken",
"pageEditor.editMode.lockedBySelf": "Je bewerkt dit document in een andere tabblad",
"pageEditor.editMode.lockedBySelfDescription": "Opslaan wordt hervat nadat de andere sessie is gesloten of het slot verloopt (~30s).",
"pageEditor.editMode.lockedBySomeone": "Iemand anders is dit document aan het bewerken",
"pageEditor.editMode.lockedDescription": "De pagina is alleen-lezen terwijl zij bewerken. Je wijzigingen worden pas opgeslagen als zij klaar zijn.",
"pageEditor.editedAt": "Laatst bewerkt op {{time}}",
"pageEditor.editedBy": "Laatst bewerkt door {{name}}",
"pageEditor.editorPlaceholder": "Druk op \"/\" voor AI en opdrachten",
+2 -2
View File
@@ -5,8 +5,8 @@
"features.agentSelfIteration.title": "Agent-zelfiteratie",
"features.assistantMessageGroup.desc": "Groepeer berichten van de agent en de resultaten van hun tool-aanroepen samen voor weergave",
"features.assistantMessageGroup.title": "Agentberichtengroepering",
"features.gatewayMode.desc": "Voer agenttaken op de server uit via de GatewayWebSocket in plaats van ze lokaal uit te voeren. Zorgt voor snellere uitvoering en vermindert het gebruik van clientbronnen.",
"features.gatewayMode.title": "Server-side uitvoering van agenten (Gateway)",
"features.fleet.desc": "Toon de Fleet-invoer in de titelbalk — een zij-aan-zij dashboard van alle lopende taken over uw agents.",
"features.fleet.title": "Fleet-weergave",
"features.groupChat.desc": "Schakel coördinatie van groepschats met meerdere agenten in.",
"features.groupChat.title": "Groepschat (Meerdere Agenten)",
"features.imessage.desc": "Verbind agents met iMessage via de lokale LobeHub Desktop BlueBubbles-bridge.",
+8 -2
View File
@@ -890,7 +890,7 @@
"skillStore.wantMore.feedback.title": "[Vaardigheidsverzoek] Vat in één zin samen welke vaardigheid je nodig hebt",
"skillStore.wantMore.reachedEnd": "Je bent aan het einde gekomen. Kun je niet vinden wat je zoekt?",
"startConversation": "Gesprek starten",
"storage.actions.copyAgentGroups.button": "Kopiëren naar",
"storage.actions.copyAgentGroups.button": "Kopiëren naar...",
"storage.actions.copyAgentGroups.desc": "Kopieer agentgroepen en hun leden naar een andere werkruimte of persoonlijk account.",
"storage.actions.copyAgentGroups.title": "Agentgroepen kopiëren",
"storage.actions.copyLobeAI.button": "Kopiëren naar",
@@ -1032,6 +1032,8 @@
"tab.addCustomSkill": "Aangepaste skill toevoegen",
"tab.advanced": "Geavanceerd",
"tab.advanced.appUpdates.title": "App-updates",
"tab.advanced.gatewayMode.desc": "Voer standaard ondersteunde agenttaken uit via de cloud Gateway. Individuele agents kunnen dit overschrijven via het chatmenu.",
"tab.advanced.gatewayMode.title": "Gateway-modus",
"tab.advanced.toolsAndDiagnostics.title": "Hulpmiddelen en diagnostiek",
"tab.advanced.updateChannel.canary": "Canary",
"tab.advanced.updateChannel.canaryDesc": "Geactiveerd bij elke PR-merge, meerdere builds per dag. Meest onstabiel.",
@@ -1169,7 +1171,6 @@
"tools.builtins.uninstallConfirm.desc": "Weet je zeker dat je {{name}} wilt verwijderen? Deze vaardigheid wordt verwijderd van de huidige agent.",
"tools.builtins.uninstallConfirm.title": "{{name}} verwijderen",
"tools.builtins.uninstalled": "Verwijderd",
"tools.disabled": "Het huidige model ondersteunt geen functieaanroepen en kan de vaardigheid niet gebruiken",
"tools.composio.addServer": "Server Toevoegen",
"tools.composio.authCompleted": "Authenticatie Voltooid",
"tools.composio.authFailed": "Authenticatie Mislukt",
@@ -1186,6 +1187,10 @@
"tools.composio.notEnabled": "Composio-service niet ingeschakeld",
"tools.composio.oauthRequired": "Voltooi OAuth-authenticatie in het nieuwe venster",
"tools.composio.pendingAuth": "Authenticatie In Afwachting",
"tools.composio.reauthorize": "Opnieuw autoriseren",
"tools.composio.remove": "Verwijderen",
"tools.composio.removeConfirm.desc": "{{name}} wordt permanent verwijderd uit je verbonden diensten. Deze actie kan niet ongedaan worden gemaakt.",
"tools.composio.removeConfirm.title": "{{name}} verwijderen?",
"tools.composio.serverCreated": "Server succesvol aangemaakt",
"tools.composio.serverCreatedFailed": "Server aanmaken mislukt",
"tools.composio.serverRemoved": "Server verwijderd",
@@ -1238,6 +1243,7 @@
"tools.composio.servers.zendesk.readme": "Integreer met Zendesk om supporttickets en klantinteracties te beheren. Maak supportverzoeken aan, werk ze bij, volg ze op, krijg toegang tot klantgegevens en stroomlijn je supportprocessen.",
"tools.composio.tools": "tools",
"tools.composio.verifyAuth": "Ik heb de authenticatie voltooid",
"tools.disabled": "Het huidige model ondersteunt geen functieaanroepen en kan de vaardigheid niet gebruiken",
"tools.lobehubSkill.authorize": "Autoriseren",
"tools.lobehubSkill.connect": "Verbinden",
"tools.lobehubSkill.connected": "Verbonden",
+15
View File
@@ -16,6 +16,7 @@
"actions.favorite": "Favoriet",
"actions.import": "Gesprek importeren",
"actions.markCompleted": "Markeren als voltooid",
"actions.moveToAgent": "Verplaatsen naar een andere assistent",
"actions.openInNewTab": "Openen in nieuw tabblad",
"actions.openInNewWindow": "Openen in een nieuw venster",
"actions.removeAll": "Alle onderwerpen verwijderen",
@@ -80,6 +81,9 @@
"management.bulk.deleteConfirm": "Je staat op het punt om {{count}} onderwerpen te verwijderen. Deze actie kan niet ongedaan worden gemaakt.",
"management.bulk.deleteTitle": "Onderwerpen verwijderen?",
"management.bulk.favorite": "Favoriet",
"management.bulk.move": "Verplaatsen naar assistent",
"management.bulk.moveEmpty": "Geen andere assistenten",
"management.bulk.moveSearchPlaceholder": "Zoek assistenten…",
"management.bulk.selectedCount_one": "{{count}} geselecteerd",
"management.bulk.selectedCount_other": "{{count}} geselecteerd",
"management.card.noPreview": "Geen voorbeeld beschikbaar",
@@ -118,6 +122,17 @@
"management.group.noProject": "Geen project",
"management.group.none": "Geen",
"management.loadingMore": "Meer onderwerpen laden…",
"management.moveModal.back": "Terug",
"management.moveModal.confirmContent_one": "{{count}} onderwerp verplaatsen naar “{{title}}”?",
"management.moveModal.confirmContent_other": "{{count}} onderwerpen verplaatsen naar “{{title}}”?",
"management.moveModal.confirmOk": "Verplaatsen",
"management.moveModal.doneOk": "Klaar",
"management.moveModal.done_one": "{{count}} onderwerp verplaatst",
"management.moveModal.done_other": "{{count}} onderwerpen verplaatst",
"management.moveModal.error": "Verplaatsen mislukt, probeer het opnieuw",
"management.moveModal.goToTarget": "Ga naar “{{title}}”",
"management.moveModal.moving": "Bezig met verplaatsen…",
"management.moveModal.title": "Onderwerpen verplaatsen",
"management.searchPlaceholder": "Zoek in de onderwerpen van deze agent…",
"management.sidebarEntry": "Onderwerpen",
"management.sort.createdAt": "Aangemaakt op",
+18
View File
@@ -44,6 +44,7 @@
"builtinCopilot": "Wbudowany Copilot",
"chatList.expandMessage": "Rozwiń wiadomość",
"chatList.longMessageDetail": "Zobacz szczegóły",
"chatList.refreshing": "Pobieranie najnowszych wiadomości...",
"chatMode.agent": "Agent",
"chatMode.agentCap.env": "Środowisko uruchomieniowe",
"chatMode.agentCap.files": "Dostęp do plików",
@@ -164,6 +165,7 @@
"extendParams.urlContext.title": "Wyodrębnij treść linku",
"followUpPlaceholder": "Kontynuuj. Użyj @, aby przypisać zadania innym agentom.",
"followUpPlaceholderHeterogeneous": "Kontynuuj.",
"gatewayMode.title": "Tryb bramy",
"group.desc": "Pracuj nad zadaniem z wieloma Agentami w jednej wspólnej przestrzeni.",
"group.memberTooltip": "Grupa zawiera {{count}} członków",
"group.orchestratorThinking": "Orkiestrator myśli...",
@@ -245,6 +247,7 @@
"inbox.title": "Lobe AI",
"input.addAi": "Dodaj wiadomość AI",
"input.addUser": "Dodaj wiadomość użytkownika",
"input.agentModeUnsupportedModel": "Obecny model nie obsługuje agentowego wywoływania narzędzi. Przełącz się na model z funkcją agenta, aby uzyskać najlepsze doświadczenie.",
"input.costEstimate.creditsPerMillionTokens": "{{credits}} kredytów/M tokenów",
"input.costEstimate.hint": "Szacowany koszt: ~{{credits}} kredytów",
"input.costEstimate.inputLabel": "Wejście",
@@ -332,6 +335,7 @@
"messages.modelCard.pricing.outputTokens": "Wyjście {{amount}} kredytów · ${{amount}}/M",
"messages.modelCard.pricing.writeCacheInputTokens": "Zapis do bufora {{amount}} kredytów · ${{amount}}/M",
"messages.tokenDetails.average": "Średnia cena jednostkowa",
"messages.tokenDetails.cacheRate": "Wskaźnik pamięci podręcznej",
"messages.tokenDetails.input": "Wejście",
"messages.tokenDetails.inputAudio": "Wejście audio",
"messages.tokenDetails.inputCached": "Buforowane wejście",
@@ -1093,7 +1097,21 @@
"workingPanel.review.viewMode.unified": "Przełącz na widok zjednoczony",
"workingPanel.review.wordWrap.disable": "Wyłącz zawijanie wierszy",
"workingPanel.review.wordWrap.enable": "Włącz zawijanie wierszy",
"workingPanel.skills.actions.comingSoon": "Już wkrótce",
"workingPanel.skills.actions.delete": "Usuń",
"workingPanel.skills.actions.rename": "Zmień nazwę",
"workingPanel.skills.actions.view": "Zobacz",
"workingPanel.skills.delete.agentConfirm": "Usunąć umiejętność „{{name}}” z tego agenta? Tego działania nie można cofnąć.",
"workingPanel.skills.delete.error": "Nie udało się usunąć umiejętności",
"workingPanel.skills.delete.success": "Umiejętność została usunięta",
"workingPanel.skills.delete.title": "Usunąć umiejętność?",
"workingPanel.skills.delete.userConfirm": "Odinstalować umiejętność „{{name}}”? Tego działania nie można cofnąć.",
"workingPanel.skills.detail.title": "Szczegóły umiejętności",
"workingPanel.skills.empty": "Nie znaleziono umiejętności w tym projekcie",
"workingPanel.skills.rename.action": "Zmień nazwę",
"workingPanel.skills.rename.error": "Nie udało się zmienić nazwy umiejętności",
"workingPanel.skills.rename.placeholder": "Nazwa umiejętności",
"workingPanel.skills.rename.title": "Zmień nazwę umiejętności",
"workingPanel.skills.section.agent": "Umiejętności agenta",
"workingPanel.skills.section.project": "Umiejętności projektu",
"workingPanel.skills.section.user": "Umiejętności użytkownika",
+17
View File
@@ -1,4 +1,20 @@
{
"fleet.addColumn": "Dodaj kolumnę",
"fleet.allShown": "Wszystkie uruchomione zadania są wyświetlane",
"fleet.backToHome": "Powrót do strony głównej",
"fleet.closeColumn": "Zamknij kolumnę",
"fleet.createTask": "Utwórz zadanie",
"fleet.empty": "Brak otwartych zadań",
"fleet.emptyDesc": "Wybierz uruchomione zadanie po lewej stronie lub użyj +, aby dodać kolumnę.",
"fleet.noRunningTasks": "Brak uruchomionych zadań",
"fleet.openInChat": "Otwórz w czacie",
"fleet.reply": "Odpowiedz",
"fleet.runningTasks": "Uruchomione zadania",
"fleet.status.idle": "Bezczynny",
"fleet.status.paused": "Wstrzymany",
"fleet.status.running": "Uruchomiony",
"fleet.status.scheduled": "Zaplanowany",
"fleet.tooltip": "Wyświetl wszystkich agentów obok siebie",
"gateway.description": "Opis",
"gateway.descriptionPlaceholder": "Opcjonalne",
"gateway.deviceName": "Nazwa urządzenia",
@@ -26,6 +42,7 @@
"navigation.memoryIdentities": "Pamięć - Tożsamości",
"navigation.memoryPreferences": "Pamięć - Preferencje",
"navigation.noPages": "Brak stron",
"navigation.observation": "Tryb obserwacji",
"navigation.onboarding": "Wprowadzenie",
"navigation.page": "Strona",
"navigation.pages": "Strony",
+10
View File
@@ -95,8 +95,18 @@
"pageEditor.duplicateError": "Nie udało się zduplikować strony",
"pageEditor.duplicateSuccess": "Strona została pomyślnie zduplikowana",
"pageEditor.editMode.checking": "Sprawdzanie dostępności edycji…",
"pageEditor.editMode.draftRestoreCancel": "Odrzuć",
"pageEditor.editMode.draftRestoreContent": "Znaleziono niezapisane lokalne zmiany z ostatniej sesji. Przywrócić je?",
"pageEditor.editMode.draftRestoreOk": "Przywróć",
"pageEditor.editMode.draftRestoreTitle": "Przywróć niezapisany szkic",
"pageEditor.editMode.lockLostDescription": "Ostatnie zmiany nie zostały jeszcze zsynchronizowane. Zapis zostanie wznowiony po odzyskaniu połączenia.",
"pageEditor.editMode.lockLostTitle": "Tymczasowo utracono blokadę edycji",
"pageEditor.editMode.lockUnstable": "Ponowne łączenie blokady edycji…",
"pageEditor.editMode.lockedByOther": "{{name}} edytuje ten dokument",
"pageEditor.editMode.lockedBySelf": "Edytujesz ten dokument w innej karcie",
"pageEditor.editMode.lockedBySelfDescription": "Zapis zostanie wznowiony po zamknięciu drugiej sesji lub wygaśnięciu jej blokady (~30s).",
"pageEditor.editMode.lockedBySomeone": "Ktoś inny edytuje ten dokument",
"pageEditor.editMode.lockedDescription": "Strona jest tylko do odczytu, gdy ktoś inny ją edytuje. Twoje zmiany nie zostaną zapisane, dopóki nie skończą.",
"pageEditor.editedAt": "Ostatnia edycja: {{time}}",
"pageEditor.editedBy": "Ostatnio edytował: {{name}}",
"pageEditor.editorPlaceholder": "Naciśnij \"/\" dla AI i poleceń",
+2 -2
View File
@@ -5,8 +5,8 @@
"features.agentSelfIteration.title": "Samoiteracja agenta",
"features.assistantMessageGroup.desc": "Grupuj wiadomości agenta i wyniki wywołań narzędzi razem do wyświetlenia",
"features.assistantMessageGroup.title": "Grupowanie Wiadomości Agenta",
"features.gatewayMode.desc": "Wykonuj zadania agenta na serwerze przez Gateway WebSocket zamiast lokalnie. Umożliwia to szybsze wykonywanie i zmniejsza wykorzystanie zasobów po stronie klienta.",
"features.gatewayMode.title": "Wykonywanie agenta po stronie serwera (Gateway)",
"features.fleet.desc": "Wyświetl wpis Fleet w pasku tytułu — zestawienie obok siebie wszystkich uruchomionych zadań na Twoich agentach.",
"features.fleet.title": "Widok Fleet",
"features.groupChat.desc": "Włącz koordynację czatu grupowego z wieloma agentami.",
"features.groupChat.title": "Czat Grupowy (Wielu Agentów)",
"features.imessage.desc": "Połącz agentów z iMessage za pośrednictwem lokalnego mostka LobeHub Desktop BlueBubbles.",
+8 -2
View File
@@ -890,7 +890,7 @@
"skillStore.wantMore.feedback.title": "[Prośba o umiejętność] Podsumuj w jednym zdaniu, jakiej umiejętności potrzebujesz",
"skillStore.wantMore.reachedEnd": "To już wszystko. Nie możesz znaleźć tego, czego szukasz?",
"startConversation": "Rozpocznij rozmowę",
"storage.actions.copyAgentGroups.button": "Kopiuj do",
"storage.actions.copyAgentGroups.button": "Kopiuj do...",
"storage.actions.copyAgentGroups.desc": "Skopiuj grupy agentów i ich członków do innej przestrzeni roboczej lub konta osobistego.",
"storage.actions.copyAgentGroups.title": "Kopiowanie grup agentów",
"storage.actions.copyLobeAI.button": "Kopiuj do",
@@ -1032,6 +1032,8 @@
"tab.addCustomSkill": "Dodaj własną umiejętność",
"tab.advanced": "Zaawansowane",
"tab.advanced.appUpdates.title": "Aktualizacje aplikacji",
"tab.advanced.gatewayMode.desc": "Domyślnie uruchamiaj obsługiwane zadania agenta przez chmurę Gateway. Poszczególni agenci mogą to zmienić w menu czatu.",
"tab.advanced.gatewayMode.title": "Tryb Gateway",
"tab.advanced.toolsAndDiagnostics.title": "Narzędzia i diagnostyka",
"tab.advanced.updateChannel.canary": "Canary",
"tab.advanced.updateChannel.canaryDesc": "Uruchamiane przy każdym scaleniu PR, wiele kompilacji dziennie. Najmniej stabilne.",
@@ -1169,7 +1171,6 @@
"tools.builtins.uninstallConfirm.desc": "Czy na pewno chcesz odinstalować {{name}}? Ta umiejętność zostanie usunięta z bieżącego agenta.",
"tools.builtins.uninstallConfirm.title": "Odinstaluj {{name}}",
"tools.builtins.uninstalled": "Odinstalowano",
"tools.disabled": "Bieżący model nie obsługuje wywołań funkcji i nie może korzystać z tej umiejętności",
"tools.composio.addServer": "Dodaj serwer",
"tools.composio.authCompleted": "Uwierzytelnienie zakończone",
"tools.composio.authFailed": "Uwierzytelnienie nie powiodło się",
@@ -1186,6 +1187,10 @@
"tools.composio.notEnabled": "Usługa Composio nie jest włączona",
"tools.composio.oauthRequired": "Proszę zakończyć uwierzytelnienie OAuth w nowym oknie",
"tools.composio.pendingAuth": "Oczekujące uwierzytelnienie",
"tools.composio.reauthorize": "Ponownie autoryzuj",
"tools.composio.remove": "Usuń",
"tools.composio.removeConfirm.desc": "{{name}} zostanie trwale usunięty z Twoich połączonych usług. Tej operacji nie można cofnąć.",
"tools.composio.removeConfirm.title": "Usunąć {{name}}?",
"tools.composio.serverCreated": "Serwer utworzony pomyślnie",
"tools.composio.serverCreatedFailed": "Nie udało się utworzyć serwera",
"tools.composio.serverRemoved": "Serwer usunięty",
@@ -1238,6 +1243,7 @@
"tools.composio.servers.zendesk.readme": "Zintegruj się z Zendesk, aby zarządzać zgłoszeniami wsparcia i interakcjami z klientami. Twórz, aktualizuj i śledź zgłoszenia, uzyskuj dostęp do danych klientów i usprawniaj działania wsparcia technicznego.",
"tools.composio.tools": "narzędzia",
"tools.composio.verifyAuth": "Ukończyłem uwierzytelnienie",
"tools.disabled": "Bieżący model nie obsługuje wywołań funkcji i nie może korzystać z tej umiejętności",
"tools.lobehubSkill.authorize": "Autoryzuj",
"tools.lobehubSkill.connect": "Połącz",
"tools.lobehubSkill.connected": "Połączono",
+15
View File
@@ -16,6 +16,7 @@
"actions.favorite": "Ulubione",
"actions.import": "Importuj rozmowę",
"actions.markCompleted": "Oznacz jako ukończone",
"actions.moveToAgent": "Przenieś do innego asystenta",
"actions.openInNewTab": "Otwórz w nowej karcie",
"actions.openInNewWindow": "Otwórz w nowym oknie",
"actions.removeAll": "Usuń wszystkie tematy",
@@ -80,6 +81,9 @@
"management.bulk.deleteConfirm": "Zamierzasz usunąć {{count}} tematów. Ta operacja jest nieodwracalna.",
"management.bulk.deleteTitle": "Usunąć tematy?",
"management.bulk.favorite": "Ulubione",
"management.bulk.move": "Przenieś do asystenta",
"management.bulk.moveEmpty": "Brak innych asystentów",
"management.bulk.moveSearchPlaceholder": "Szukaj asystentów…",
"management.bulk.selectedCount_one": "{{count}} wybrany",
"management.bulk.selectedCount_other": "{{count}} wybranych",
"management.card.noPreview": "Brak dostępnego podglądu",
@@ -118,6 +122,17 @@
"management.group.noProject": "Brak projektu",
"management.group.none": "Brak",
"management.loadingMore": "Ładowanie kolejnych tematów…",
"management.moveModal.back": "Wstecz",
"management.moveModal.confirmContent_one": "Przenieść {{count}} temat do „{{title}}”?",
"management.moveModal.confirmContent_other": "Przenieść {{count}} tematy do „{{title}}”?",
"management.moveModal.confirmOk": "Przenieś",
"management.moveModal.doneOk": "Gotowe",
"management.moveModal.done_one": "{{count}} temat przeniesiony",
"management.moveModal.done_other": "{{count}} tematy przeniesione",
"management.moveModal.error": "Przeniesienie nie powiodło się, spróbuj ponownie",
"management.moveModal.goToTarget": "Przejdź do „{{title}}”",
"management.moveModal.moving": "Przenoszenie…",
"management.moveModal.title": "Przenieś tematy",
"management.searchPlaceholder": "Szukaj tematów tego agenta…",
"management.sidebarEntry": "Tematy",
"management.sort.createdAt": "Czas utworzenia",
+18
View File
@@ -44,6 +44,7 @@
"builtinCopilot": "Copiloto Integrado",
"chatList.expandMessage": "Expandir mensagem",
"chatList.longMessageDetail": "Ver detalhes",
"chatList.refreshing": "Buscando mensagens mais recentes...",
"chatMode.agent": "Agente",
"chatMode.agentCap.env": "Ambiente de execução",
"chatMode.agentCap.files": "Acesso a arquivos",
@@ -164,6 +165,7 @@
"extendParams.urlContext.title": "Extrair Conteúdo de Links da Web",
"followUpPlaceholder": "Acompanhar. Use @ para atribuir tarefas a outros agentes.",
"followUpPlaceholderHeterogeneous": "Continuar.",
"gatewayMode.title": "Modo Gateway",
"group.desc": "Avance em uma tarefa com vários Agentes em um espaço compartilhado.",
"group.memberTooltip": "Há {{count}} membros no grupo",
"group.orchestratorThinking": "Orquestrador está pensando...",
@@ -245,6 +247,7 @@
"inbox.title": "Lobe AI",
"input.addAi": "Adicionar mensagem de IA",
"input.addUser": "Adicionar mensagem de usuário",
"input.agentModeUnsupportedModel": "O modelo atual não suporta chamadas de ferramentas agentivas. Troque para um modelo com capacidade agentiva para uma melhor experiência.",
"input.costEstimate.creditsPerMillionTokens": "{{credits}} créditos/M tokens",
"input.costEstimate.hint": "Custo estimado: ~{{credits}} créditos",
"input.costEstimate.inputLabel": "Entrada",
@@ -332,6 +335,7 @@
"messages.modelCard.pricing.outputTokens": "Saída {{amount}} créditos · US${{amount}}/M",
"messages.modelCard.pricing.writeCacheInputTokens": "Gravação em cache {{amount}} créditos · US${{amount}}/M",
"messages.tokenDetails.average": "Preço médio por unidade",
"messages.tokenDetails.cacheRate": "Taxa de cache",
"messages.tokenDetails.input": "Entrada",
"messages.tokenDetails.inputAudio": "Entrada de Áudio",
"messages.tokenDetails.inputCached": "Entrada em Cache",
@@ -1093,7 +1097,21 @@
"workingPanel.review.viewMode.unified": "Alternar para visualização unificada",
"workingPanel.review.wordWrap.disable": "Desativar quebra de linha",
"workingPanel.review.wordWrap.enable": "Ativar quebra de linha",
"workingPanel.skills.actions.comingSoon": "Em breve",
"workingPanel.skills.actions.delete": "Excluir",
"workingPanel.skills.actions.rename": "Renomear",
"workingPanel.skills.actions.view": "Visualizar",
"workingPanel.skills.delete.agentConfirm": "Remover a habilidade “{{name}}” deste agente? Isso não pode ser desfeito.",
"workingPanel.skills.delete.error": "Falha ao excluir a habilidade",
"workingPanel.skills.delete.success": "Habilidade excluída",
"workingPanel.skills.delete.title": "Excluir habilidade?",
"workingPanel.skills.delete.userConfirm": "Desinstalar a habilidade “{{name}}”? Isso não pode ser desfeito.",
"workingPanel.skills.detail.title": "Detalhes da habilidade",
"workingPanel.skills.empty": "Nenhuma habilidade encontrada neste projeto",
"workingPanel.skills.rename.action": "Renomear",
"workingPanel.skills.rename.error": "Falha ao renomear a habilidade",
"workingPanel.skills.rename.placeholder": "Nome da habilidade",
"workingPanel.skills.rename.title": "Renomear habilidade",
"workingPanel.skills.section.agent": "Habilidades do agente",
"workingPanel.skills.section.project": "Habilidades do projeto",
"workingPanel.skills.section.user": "Habilidades do usuário",
+17
View File
@@ -1,4 +1,20 @@
{
"fleet.addColumn": "Adicionar coluna",
"fleet.allShown": "Todas as tarefas em execução estão exibidas",
"fleet.backToHome": "Voltar para a página inicial",
"fleet.closeColumn": "Fechar coluna",
"fleet.createTask": "Criar tarefa",
"fleet.empty": "Nenhuma tarefa aberta",
"fleet.emptyDesc": "Selecione uma tarefa em execução à esquerda ou use + para adicionar uma coluna.",
"fleet.noRunningTasks": "Nenhuma tarefa em execução",
"fleet.openInChat": "Abrir no chat",
"fleet.reply": "Responder",
"fleet.runningTasks": "Tarefas em execução",
"fleet.status.idle": "Inativo",
"fleet.status.paused": "Pausado",
"fleet.status.running": "Em execução",
"fleet.status.scheduled": "Agendado",
"fleet.tooltip": "Visualizar todos os agentes lado a lado",
"gateway.description": "Descrição",
"gateway.descriptionPlaceholder": "Opcional",
"gateway.deviceName": "Nome do Dispositivo",
@@ -26,6 +42,7 @@
"navigation.memoryIdentities": "Memória - Identidades",
"navigation.memoryPreferences": "Memória - Preferências",
"navigation.noPages": "Ainda não há páginas",
"navigation.observation": "Modo de Observação",
"navigation.onboarding": "Integração",
"navigation.page": "Página",
"navigation.pages": "Páginas",
+10
View File
@@ -95,8 +95,18 @@
"pageEditor.duplicateError": "Falha ao duplicar a página",
"pageEditor.duplicateSuccess": "Página duplicada com sucesso",
"pageEditor.editMode.checking": "Verificando a disponibilidade de edição…",
"pageEditor.editMode.draftRestoreCancel": "Descartar",
"pageEditor.editMode.draftRestoreContent": "Alterações locais não salvas da sua última sessão foram encontradas. Deseja restaurá-las?",
"pageEditor.editMode.draftRestoreOk": "Restaurar",
"pageEditor.editMode.draftRestoreTitle": "Restaurar Rascunho Não Salvo",
"pageEditor.editMode.lockLostDescription": "Edições recentes ainda não foram sincronizadas. Elas voltarão a ser salvas assim que a conexão for restabelecida.",
"pageEditor.editMode.lockLostTitle": "Bloqueio de edição temporariamente perdido",
"pageEditor.editMode.lockUnstable": "Reconectando o bloqueio de edição…",
"pageEditor.editMode.lockedByOther": "{{name}} está editando este documento",
"pageEditor.editMode.lockedBySelf": "Você está editando este documento em outra aba",
"pageEditor.editMode.lockedBySelfDescription": "As alterações serão salvas após o encerramento da outra sessão ou a expiração do bloqueio (~30s).",
"pageEditor.editMode.lockedBySomeone": "Outra pessoa está editando este documento",
"pageEditor.editMode.lockedDescription": "A página está em modo somente leitura enquanto outra pessoa edita. Suas alterações não serão salvas até que eles terminem.",
"pageEditor.editedAt": "Última edição em {{time}}",
"pageEditor.editedBy": "Última edição por {{name}}",
"pageEditor.editorPlaceholder": "Pressione \"/\" para IA e comandos",
+2 -2
View File
@@ -5,8 +5,8 @@
"features.agentSelfIteration.title": "Autoiteração do Agente",
"features.assistantMessageGroup.desc": "Agrupe mensagens do agente e os resultados das chamadas de ferramentas para exibição conjunta",
"features.assistantMessageGroup.title": "Agrupamento de Mensagens do Agente",
"features.gatewayMode.desc": "Execute tarefas de agente no servidor via Gateway WebSocket em vez de executá-las localmente. Permite uma execução mais rápida e reduz o uso de recursos do cliente.",
"features.gatewayMode.title": "Execução de Agente no Servidor (Gateway)",
"features.fleet.desc": "Exiba a entrada da Frota na barra de título — um painel lado a lado de todas as tarefas em execução entre seus agentes.",
"features.fleet.title": "Visão da Frota",
"features.groupChat.desc": "Ative a coordenação de bate-papo em grupo com múltiplos agentes.",
"features.groupChat.title": "Bate-Papo em Grupo (Multiagente)",
"features.imessage.desc": "Conectar agentes ao iMessage através da ponte local LobeHub Desktop BlueBubbles.",
+8 -2
View File
@@ -890,7 +890,7 @@
"skillStore.wantMore.feedback.title": "[Solicitação de Habilidade] Resuma em uma frase a habilidade que você precisa",
"skillStore.wantMore.reachedEnd": "Você chegou ao fim. Não encontrou o que procurava?",
"startConversation": "Iniciar Conversa",
"storage.actions.copyAgentGroups.button": "Copiar Para",
"storage.actions.copyAgentGroups.button": "Copiar para...",
"storage.actions.copyAgentGroups.desc": "Copie grupos de agentes e seus membros para outro espaço de trabalho ou conta pessoal.",
"storage.actions.copyAgentGroups.title": "Cópia de Grupos de Agentes",
"storage.actions.copyLobeAI.button": "Copiar Para",
@@ -1032,6 +1032,8 @@
"tab.addCustomSkill": "Adicionar habilidade personalizada",
"tab.advanced": "Avançado",
"tab.advanced.appUpdates.title": "Atualizações do Aplicativo",
"tab.advanced.gatewayMode.desc": "Execute tarefas de agente suportadas através do Gateway na nuvem por padrão. Agentes individuais podem substituir isso no menu de chat.",
"tab.advanced.gatewayMode.title": "Modo Gateway",
"tab.advanced.toolsAndDiagnostics.title": "Ferramentas e Diagnósticos",
"tab.advanced.updateChannel.canary": "Canary",
"tab.advanced.updateChannel.canaryDesc": "Acionado a cada merge de PR, múltiplas compilações por dia. O mais instável.",
@@ -1169,7 +1171,6 @@
"tools.builtins.uninstallConfirm.desc": "Tem certeza de que deseja desinstalar {{name}}? Essa habilidade será removida do agente atual.",
"tools.builtins.uninstallConfirm.title": "Desinstalar {{name}}",
"tools.builtins.uninstalled": "Desinstalado",
"tools.disabled": "O modelo atual não suporta chamadas de função e não pode usar a habilidade",
"tools.composio.addServer": "Adicionar Servidor",
"tools.composio.authCompleted": "Autenticação Concluída",
"tools.composio.authFailed": "Falha na Autenticação",
@@ -1186,6 +1187,10 @@
"tools.composio.notEnabled": "Serviço Composio não ativado",
"tools.composio.oauthRequired": "Por favor, conclua a autenticação OAuth na nova janela",
"tools.composio.pendingAuth": "Autenticação Pendente",
"tools.composio.reauthorize": "Reautorizar",
"tools.composio.remove": "Remover",
"tools.composio.removeConfirm.desc": "{{name}} será removido permanentemente dos seus serviços conectados. Esta ação não pode ser desfeita.",
"tools.composio.removeConfirm.title": "Remover {{name}}?",
"tools.composio.serverCreated": "Servidor criado com sucesso",
"tools.composio.serverCreatedFailed": "Falha ao criar servidor",
"tools.composio.serverRemoved": "Servidor removido",
@@ -1238,6 +1243,7 @@
"tools.composio.servers.zendesk.readme": "Integre com o Zendesk para gerenciar tickets de suporte e interações com clientes. Crie, atualize e acompanhe solicitações de suporte, acesse dados de clientes e otimize suas operações de atendimento.",
"tools.composio.tools": "ferramentas",
"tools.composio.verifyAuth": "Concluí a autenticação",
"tools.disabled": "O modelo atual não suporta chamadas de função e não pode usar a habilidade",
"tools.lobehubSkill.authorize": "Autorizar",
"tools.lobehubSkill.connect": "Conectar",
"tools.lobehubSkill.connected": "Conectado",
+15
View File
@@ -16,6 +16,7 @@
"actions.favorite": "Favoritar",
"actions.import": "Importar Conversa",
"actions.markCompleted": "Marcar como Concluída",
"actions.moveToAgent": "Mover para outro assistente",
"actions.openInNewTab": "Abrir em Nova Aba",
"actions.openInNewWindow": "Abrir em nova janela",
"actions.removeAll": "Excluir Todos os Tópicos",
@@ -80,6 +81,9 @@
"management.bulk.deleteConfirm": "Você está prestes a excluir {{count}} tópicos. Esta ação não pode ser desfeita.",
"management.bulk.deleteTitle": "Excluir tópicos?",
"management.bulk.favorite": "Favoritar",
"management.bulk.move": "Mover para assistente",
"management.bulk.moveEmpty": "Nenhum outro assistente",
"management.bulk.moveSearchPlaceholder": "Buscar assistentes…",
"management.bulk.selectedCount_one": "{{count}} selecionado",
"management.bulk.selectedCount_other": "{{count}} selecionados",
"management.card.noPreview": "Nenhuma prévia disponível",
@@ -118,6 +122,17 @@
"management.group.noProject": "Sem projeto",
"management.group.none": "Nenhum",
"management.loadingMore": "Carregando mais tópicos…",
"management.moveModal.back": "Voltar",
"management.moveModal.confirmContent_one": "Mover {{count}} tópico para “{{title}}”?",
"management.moveModal.confirmContent_other": "Mover {{count}} tópicos para “{{title}}”?",
"management.moveModal.confirmOk": "Mover",
"management.moveModal.doneOk": "Concluído",
"management.moveModal.done_one": "{{count}} tópico movido",
"management.moveModal.done_other": "{{count}} tópicos movidos",
"management.moveModal.error": "Falha ao mover, tente novamente",
"management.moveModal.goToTarget": "Ir para “{{title}}”",
"management.moveModal.moving": "Movendo…",
"management.moveModal.title": "Mover tópicos",
"management.searchPlaceholder": "Pesquisar tópicos deste agente…",
"management.sidebarEntry": "Tópicos",
"management.sort.createdAt": "Data de criação",
+18
View File
@@ -44,6 +44,7 @@
"builtinCopilot": "Встроенный Копилот",
"chatList.expandMessage": "Развернуть сообщение",
"chatList.longMessageDetail": "Просмотреть детали",
"chatList.refreshing": "Получение последних сообщений...",
"chatMode.agent": "Агент",
"chatMode.agentCap.env": "Среда выполнения",
"chatMode.agentCap.files": "Доступ к файлам",
@@ -164,6 +165,7 @@
"extendParams.urlContext.title": "Извлекать содержимое веб-ссылок",
"followUpPlaceholder": "Продолжение. Используйте @, чтобы назначать задачи другим агентам.",
"followUpPlaceholderHeterogeneous": "Продолжить.",
"gatewayMode.title": "Режим шлюза",
"group.desc": "Продвигайте задачу с помощью нескольких агентов в общем пространстве.",
"group.memberTooltip": "В группе {{count}} участников",
"group.orchestratorThinking": "Оркестратор обдумывает...",
@@ -245,6 +247,7 @@
"inbox.title": "Lobe AI",
"input.addAi": "Добавить сообщение от ИИ",
"input.addUser": "Добавить сообщение пользователя",
"input.agentModeUnsupportedModel": "Текущая модель не поддерживает вызов инструментов агента. Переключитесь на модель с возможностями агента для лучшего опыта.",
"input.costEstimate.creditsPerMillionTokens": "{{credits}} кредитов/М токенов",
"input.costEstimate.hint": "Ориентировочная стоимость: ~{{credits}} кредитов",
"input.costEstimate.inputLabel": "Ввод",
@@ -332,6 +335,7 @@
"messages.modelCard.pricing.outputTokens": "Вывод {{amount}} кредитов · ${{amount}}/M",
"messages.modelCard.pricing.writeCacheInputTokens": "Запись в кэш {{amount}} кредитов · ${{amount}}/M",
"messages.tokenDetails.average": "Средняя цена за единицу",
"messages.tokenDetails.cacheRate": "Уровень кэширования",
"messages.tokenDetails.input": "Ввод",
"messages.tokenDetails.inputAudio": "Аудио-ввод",
"messages.tokenDetails.inputCached": "Кэшированный ввод",
@@ -1093,7 +1097,21 @@
"workingPanel.review.viewMode.unified": "Переключиться на объединенный вид",
"workingPanel.review.wordWrap.disable": "Отключить перенос слов",
"workingPanel.review.wordWrap.enable": "Включить перенос слов",
"workingPanel.skills.actions.comingSoon": "Скоро будет",
"workingPanel.skills.actions.delete": "Удалить",
"workingPanel.skills.actions.rename": "Переименовать",
"workingPanel.skills.actions.view": "Просмотреть",
"workingPanel.skills.delete.agentConfirm": "Удалить навык «{{name}}» из этого агента? Это действие нельзя отменить.",
"workingPanel.skills.delete.error": "Не удалось удалить навык",
"workingPanel.skills.delete.success": "Навык удален",
"workingPanel.skills.delete.title": "Удалить навык?",
"workingPanel.skills.delete.userConfirm": "Удалить навык «{{name}}»? Это действие нельзя отменить.",
"workingPanel.skills.detail.title": "Детали навыка",
"workingPanel.skills.empty": "В этом проекте не найдено навыков",
"workingPanel.skills.rename.action": "Переименовать",
"workingPanel.skills.rename.error": "Не удалось переименовать навык",
"workingPanel.skills.rename.placeholder": "Название навыка",
"workingPanel.skills.rename.title": "Переименовать навык",
"workingPanel.skills.section.agent": "Навыки агента",
"workingPanel.skills.section.project": "Навыки проекта",
"workingPanel.skills.section.user": "Навыки пользователя",

Some files were not shown because too many files have changed in this diff Show More