mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-18 13:25:45 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ec55f5941 | |||
| 63f13c2a31 | |||
| 3f82033939 | |||
| 95db11309e | |||
| 6e0cd5f299 | |||
| 91d684878f | |||
| 2d897cea73 | |||
| 99785d3cc7 | |||
| 1fa6f47fc9 |
@@ -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">
|
||||
|
||||
[](#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>
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}),
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+61
-1
@@ -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,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
[
|
||||
{
|
||||
"children": {},
|
||||
"date": "2026-06-17",
|
||||
"version": "2.2.6"
|
||||
},
|
||||
{
|
||||
"children": {},
|
||||
"date": "2026-05-29",
|
||||
|
||||
@@ -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": "مهارات المستخدم",
|
||||
|
||||
@@ -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": "الصفحات",
|
||||
|
||||
@@ -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": "اضغط \"/\" للوصول إلى الذكاء الاصطناعي والأوامر",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": "متصل",
|
||||
|
||||
@@ -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": "وقت الإنشاء",
|
||||
|
||||
@@ -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": "Умения на потребителя",
|
||||
|
||||
@@ -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": "Страници",
|
||||
|
||||
@@ -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": "Натиснете \"/\" за ИИ и команди",
|
||||
|
||||
@@ -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 мост.",
|
||||
|
||||
@@ -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": "Свързано",
|
||||
|
||||
@@ -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": "Време на създаване",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
"agentDefaultMessage": "Hi, I’m **{{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, I’m **{{name}}**. One sentence is enough—you're in control.",
|
||||
"agentDefaultMessageWithoutEdit": "Hi, I’m **{{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,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",
|
||||
|
||||
@@ -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 haven’t synced yet. They’ll 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": "You’re 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 won’t be saved until they’re done.",
|
||||
"pageEditor.editedAt": "Last edited on {{time}}",
|
||||
"pageEditor.editedBy": "Last edited by {{name}}",
|
||||
"pageEditor.editorPlaceholder": "Press \"/\" for AI and commands.",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "مهارتهای کاربر",
|
||||
|
||||
@@ -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": "صفحات",
|
||||
|
||||
@@ -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": "برای هوش مصنوعی و دستورات \"/\" را فشار دهید",
|
||||
|
||||
@@ -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 محلی.",
|
||||
|
||||
@@ -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": "متصل شد",
|
||||
|
||||
@@ -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": "زمان ایجاد",
|
||||
|
||||
@@ -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 à d’autres 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": "L’orchestrateur 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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
"features.agentSelfIteration.title": "Auto‑itération de l’agent",
|
||||
"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 l’agent sur le serveur via un WebSocket Gateway au lieu de les exécuter localement. Permet une exécution plus rapide et réduit l’utilisation des ressources du client.",
|
||||
"features.gatewayMode.title": "Exécution de l’agent 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.",
|
||||
|
||||
@@ -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 l’authentification",
|
||||
@@ -1186,6 +1187,10 @@
|
||||
"tools.composio.notEnabled": "Service Composio non activé",
|
||||
"tools.composio.oauthRequired": "Veuillez compléter l’authentification 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": "J’ai terminé l’authentification",
|
||||
"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é",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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à dell’agente sul server tramite Gateway WebSocket invece di eseguirle in locale. Consente un’esecuzione più rapida e riduce l’utilizzo delle risorse del client.",
|
||||
"features.gatewayMode.title": "Esecuzione dell’Agente 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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "ユーザースキル",
|
||||
|
||||
@@ -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": "ページ",
|
||||
|
||||
@@ -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 とコマンドを呼び出し",
|
||||
|
||||
@@ -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に接続します。",
|
||||
|
||||
@@ -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": "接続済み",
|
||||
|
||||
@@ -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": "作成日時",
|
||||
|
||||
@@ -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": "사용자 스킬",
|
||||
|
||||
@@ -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": "페이지",
|
||||
|
||||
@@ -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 및 명령어 사용",
|
||||
|
||||
@@ -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에 연결합니다.",
|
||||
|
||||
@@ -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": "연결됨",
|
||||
|
||||
@@ -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": "생성 시간",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 Gateway‑WebSocket 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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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ń",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user