mirror of
https://github.com/lobehub/lobehub
synced 2026-04-21 09:37:28 +00:00
♻️ refactor(agent-documents): fix title/documentId flow + split Inspector per action (#13940)
- extract H1 from markdown content as document title (stripped from content) - use title verbatim as filename (no extension); simplify dedup to `-2`, `-3` - AgentDocumentModel.create accepts optional title; falls back to filename - ExecutionRuntime createDocument returns documents.id (not agentDocuments.id) as state.documentId so the portal can resolve the row for openDocument - sidebar DocumentItem prefers title over filename - split AgentDocumentsInspector into 11 per-apiName components (Notebook pattern) - tests: filename util (13), ExecutionRuntime wiring (5), updated model + service Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b909e4ae20
commit
4e5db98ffc
22 changed files with 696 additions and 169 deletions
|
|
@ -17,7 +17,18 @@ import type {
|
|||
|
||||
interface AgentDocumentRecord {
|
||||
content?: string;
|
||||
/**
|
||||
* The underlying `documents` table id. Used for portal rendering
|
||||
* (opening the document in the shared EditorCanvas), which must resolve
|
||||
* the row in `documents` — distinct from `id` which is the
|
||||
* `agentDocuments` association row id.
|
||||
*/
|
||||
documentId?: string;
|
||||
filename?: string;
|
||||
/**
|
||||
* The `agentDocuments` association row id. This is what the LLM receives
|
||||
* and uses for subsequent operations (read/edit/remove/...).
|
||||
*/
|
||||
id: string;
|
||||
title?: string;
|
||||
}
|
||||
|
|
@ -174,7 +185,7 @@ export class AgentDocumentsExecutionRuntime {
|
|||
|
||||
return {
|
||||
content: `Created document "${created.title || args.title}" (${created.id}).`,
|
||||
state: { documentId: created.id },
|
||||
state: { documentId: created.documentId },
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,124 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import type { BuiltinInspectorProps } from '@lobechat/types';
|
||||
import { cx } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { highlightTextStyles, inspectorTextStyles, shinyTextStyles } from '@/styles';
|
||||
|
||||
import {
|
||||
AgentDocumentsApiName,
|
||||
type CopyDocumentArgs,
|
||||
type CreateDocumentArgs,
|
||||
type EditDocumentArgs,
|
||||
type PatchDocumentArgs,
|
||||
type ReadDocumentArgs,
|
||||
type RemoveDocumentArgs,
|
||||
type RenameDocumentArgs,
|
||||
type UpdateLoadRuleArgs,
|
||||
} from '../../../types';
|
||||
|
||||
type AgentDocumentsArgs =
|
||||
| CopyDocumentArgs
|
||||
| CreateDocumentArgs
|
||||
| EditDocumentArgs
|
||||
| PatchDocumentArgs
|
||||
| ReadDocumentArgs
|
||||
| RemoveDocumentArgs
|
||||
| RenameDocumentArgs
|
||||
| UpdateLoadRuleArgs;
|
||||
|
||||
const getInspectorSummary = (
|
||||
apiName: string,
|
||||
args?: Partial<AgentDocumentsArgs>,
|
||||
): string | undefined => {
|
||||
switch (apiName) {
|
||||
case AgentDocumentsApiName.createDocument: {
|
||||
return args && 'title' in args ? args.title : undefined;
|
||||
}
|
||||
case AgentDocumentsApiName.renameDocument: {
|
||||
return args && 'newTitle' in args ? args.newTitle : undefined;
|
||||
}
|
||||
case AgentDocumentsApiName.copyDocument: {
|
||||
if (args && 'newTitle' in args && args.newTitle) return args.newTitle;
|
||||
return args && 'id' in args ? args.id : undefined;
|
||||
}
|
||||
case AgentDocumentsApiName.readDocument:
|
||||
case AgentDocumentsApiName.editDocument:
|
||||
case AgentDocumentsApiName.patchDocument:
|
||||
case AgentDocumentsApiName.removeDocument:
|
||||
case AgentDocumentsApiName.updateLoadRule: {
|
||||
return args && 'id' in args ? args.id : undefined;
|
||||
}
|
||||
default: {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getInspectorLabel = (apiName: string, t: (...args: any[]) => string) => {
|
||||
switch (apiName) {
|
||||
case AgentDocumentsApiName.createDocument: {
|
||||
return t('builtins.lobe-agent-documents.apiName.createDocument');
|
||||
}
|
||||
case AgentDocumentsApiName.readDocument: {
|
||||
return t('builtins.lobe-agent-documents.apiName.readDocument');
|
||||
}
|
||||
case AgentDocumentsApiName.editDocument: {
|
||||
return t('builtins.lobe-agent-documents.apiName.editDocument');
|
||||
}
|
||||
case AgentDocumentsApiName.patchDocument: {
|
||||
return t('builtins.lobe-agent-documents.apiName.patchDocument');
|
||||
}
|
||||
case AgentDocumentsApiName.removeDocument: {
|
||||
return t('builtins.lobe-agent-documents.apiName.removeDocument');
|
||||
}
|
||||
case AgentDocumentsApiName.renameDocument: {
|
||||
return t('builtins.lobe-agent-documents.apiName.renameDocument');
|
||||
}
|
||||
case AgentDocumentsApiName.copyDocument: {
|
||||
return t('builtins.lobe-agent-documents.apiName.copyDocument');
|
||||
}
|
||||
case AgentDocumentsApiName.updateLoadRule: {
|
||||
return t('builtins.lobe-agent-documents.apiName.updateLoadRule');
|
||||
}
|
||||
default: {
|
||||
return apiName;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const AgentDocumentsInspector = memo<BuiltinInspectorProps<AgentDocumentsArgs>>(
|
||||
({ apiName, args, partialArgs, isArgumentsStreaming, isLoading }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
|
||||
const summary = getInspectorSummary(apiName, args || partialArgs);
|
||||
const label = getInspectorLabel(apiName, t);
|
||||
|
||||
if (isArgumentsStreaming && !summary) {
|
||||
return <div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>{label}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
inspectorTextStyles.root,
|
||||
(isArgumentsStreaming || isLoading) && shinyTextStyles.shinyText,
|
||||
)}
|
||||
>
|
||||
<span>{label}</span>
|
||||
{summary && (
|
||||
<>
|
||||
<span>: </span>
|
||||
<span className={highlightTextStyles.primary}>{summary}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
AgentDocumentsInspector.displayName = 'AgentDocumentsInspector';
|
||||
|
||||
export default AgentDocumentsInspector;
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
'use client';
|
||||
|
||||
import type { BuiltinInspectorProps } from '@lobechat/types';
|
||||
import { cx } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { highlightTextStyles, inspectorTextStyles, shinyTextStyles } from '@/styles';
|
||||
|
||||
import type { CopyDocumentArgs, CopyDocumentState } from '../../../types';
|
||||
|
||||
export const CopyDocumentInspector = memo<
|
||||
BuiltinInspectorProps<CopyDocumentArgs, CopyDocumentState>
|
||||
>(({ args, partialArgs, isArgumentsStreaming, isLoading }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
const summary = args?.newTitle || partialArgs?.newTitle || args?.id || partialArgs?.id;
|
||||
|
||||
if (isArgumentsStreaming && !summary) {
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-agent-documents.apiName.copyDocument')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
inspectorTextStyles.root,
|
||||
(isArgumentsStreaming || isLoading) && shinyTextStyles.shinyText,
|
||||
)}
|
||||
>
|
||||
<span>{t('builtins.lobe-agent-documents.apiName.copyDocument')}: </span>
|
||||
{summary && <span className={highlightTextStyles.primary}>{summary}</span>}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
CopyDocumentInspector.displayName = 'CopyDocumentInspector';
|
||||
|
||||
export default CopyDocumentInspector;
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
'use client';
|
||||
|
||||
import type { BuiltinInspectorProps } from '@lobechat/types';
|
||||
import { cx } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { highlightTextStyles, inspectorTextStyles, shinyTextStyles } from '@/styles';
|
||||
|
||||
import type { CreateDocumentArgs, CreateDocumentState } from '../../../types';
|
||||
|
||||
export const CreateDocumentInspector = memo<
|
||||
BuiltinInspectorProps<CreateDocumentArgs, CreateDocumentState>
|
||||
>(({ args, partialArgs, isArgumentsStreaming, isLoading }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
const title = args?.title || partialArgs?.title;
|
||||
|
||||
if (isArgumentsStreaming && !title) {
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-agent-documents.apiName.createDocument')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
inspectorTextStyles.root,
|
||||
(isArgumentsStreaming || isLoading) && shinyTextStyles.shinyText,
|
||||
)}
|
||||
>
|
||||
<span>{t('builtins.lobe-agent-documents.apiName.createDocument')}: </span>
|
||||
{title && <span className={highlightTextStyles.primary}>{title}</span>}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
CreateDocumentInspector.displayName = 'CreateDocumentInspector';
|
||||
|
||||
export default CreateDocumentInspector;
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
'use client';
|
||||
|
||||
import type { BuiltinInspectorProps } from '@lobechat/types';
|
||||
import { cx } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { highlightTextStyles, inspectorTextStyles, shinyTextStyles } from '@/styles';
|
||||
|
||||
import type { EditDocumentArgs, EditDocumentState } from '../../../types';
|
||||
|
||||
export const EditDocumentInspector = memo<
|
||||
BuiltinInspectorProps<EditDocumentArgs, EditDocumentState>
|
||||
>(({ args, partialArgs, isArgumentsStreaming, isLoading }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
const id = args?.id || partialArgs?.id;
|
||||
|
||||
if (isArgumentsStreaming && !id) {
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-agent-documents.apiName.editDocument')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
inspectorTextStyles.root,
|
||||
(isArgumentsStreaming || isLoading) && shinyTextStyles.shinyText,
|
||||
)}
|
||||
>
|
||||
<span>{t('builtins.lobe-agent-documents.apiName.editDocument')}: </span>
|
||||
{id && <span className={highlightTextStyles.primary}>{id}</span>}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
EditDocumentInspector.displayName = 'EditDocumentInspector';
|
||||
|
||||
export default EditDocumentInspector;
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
'use client';
|
||||
|
||||
import type { BuiltinInspectorProps } from '@lobechat/types';
|
||||
import { cx } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { inspectorTextStyles, shinyTextStyles } from '@/styles';
|
||||
|
||||
import type { ListDocumentsArgs, ListDocumentsState } from '../../../types';
|
||||
|
||||
export const ListDocumentsInspector = memo<
|
||||
BuiltinInspectorProps<ListDocumentsArgs, ListDocumentsState>
|
||||
>(({ isArgumentsStreaming, isLoading }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
inspectorTextStyles.root,
|
||||
(isArgumentsStreaming || isLoading) && shinyTextStyles.shinyText,
|
||||
)}
|
||||
>
|
||||
<span>{t('builtins.lobe-agent-documents.apiName.listDocuments')}</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
ListDocumentsInspector.displayName = 'ListDocumentsInspector';
|
||||
|
||||
export default ListDocumentsInspector;
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
'use client';
|
||||
|
||||
import type { BuiltinInspectorProps } from '@lobechat/types';
|
||||
import { cx } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { highlightTextStyles, inspectorTextStyles, shinyTextStyles } from '@/styles';
|
||||
|
||||
import type { PatchDocumentArgs, PatchDocumentState } from '../../../types';
|
||||
|
||||
export const PatchDocumentInspector = memo<
|
||||
BuiltinInspectorProps<PatchDocumentArgs, PatchDocumentState>
|
||||
>(({ args, partialArgs, isArgumentsStreaming, isLoading }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
const id = args?.id || partialArgs?.id;
|
||||
|
||||
if (isArgumentsStreaming && !id) {
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-agent-documents.apiName.patchDocument')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
inspectorTextStyles.root,
|
||||
(isArgumentsStreaming || isLoading) && shinyTextStyles.shinyText,
|
||||
)}
|
||||
>
|
||||
<span>{t('builtins.lobe-agent-documents.apiName.patchDocument')}: </span>
|
||||
{id && <span className={highlightTextStyles.primary}>{id}</span>}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
PatchDocumentInspector.displayName = 'PatchDocumentInspector';
|
||||
|
||||
export default PatchDocumentInspector;
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
'use client';
|
||||
|
||||
import type { BuiltinInspectorProps } from '@lobechat/types';
|
||||
import { cx } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { highlightTextStyles, inspectorTextStyles, shinyTextStyles } from '@/styles';
|
||||
|
||||
import type { ReadDocumentArgs, ReadDocumentState } from '../../../types';
|
||||
|
||||
export const ReadDocumentInspector = memo<
|
||||
BuiltinInspectorProps<ReadDocumentArgs, ReadDocumentState>
|
||||
>(({ args, partialArgs, pluginState, isArgumentsStreaming, isLoading }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
const summary = pluginState?.title || args?.id || partialArgs?.id;
|
||||
|
||||
if (isArgumentsStreaming && !summary) {
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-agent-documents.apiName.readDocument')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
inspectorTextStyles.root,
|
||||
(isArgumentsStreaming || isLoading) && shinyTextStyles.shinyText,
|
||||
)}
|
||||
>
|
||||
<span>{t('builtins.lobe-agent-documents.apiName.readDocument')}: </span>
|
||||
{summary && <span className={highlightTextStyles.primary}>{summary}</span>}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
ReadDocumentInspector.displayName = 'ReadDocumentInspector';
|
||||
|
||||
export default ReadDocumentInspector;
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
'use client';
|
||||
|
||||
import type { BuiltinInspectorProps } from '@lobechat/types';
|
||||
import { cx } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { highlightTextStyles, inspectorTextStyles, shinyTextStyles } from '@/styles';
|
||||
|
||||
import type { ReadDocumentByFilenameArgs, ReadDocumentByFilenameState } from '../../../types';
|
||||
|
||||
export const ReadDocumentByFilenameInspector = memo<
|
||||
BuiltinInspectorProps<ReadDocumentByFilenameArgs, ReadDocumentByFilenameState>
|
||||
>(({ args, partialArgs, isArgumentsStreaming, isLoading }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
const filename = args?.filename || partialArgs?.filename;
|
||||
|
||||
if (isArgumentsStreaming && !filename) {
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-agent-documents.apiName.readDocumentByFilename')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
inspectorTextStyles.root,
|
||||
(isArgumentsStreaming || isLoading) && shinyTextStyles.shinyText,
|
||||
)}
|
||||
>
|
||||
<span>{t('builtins.lobe-agent-documents.apiName.readDocumentByFilename')}: </span>
|
||||
{filename && <span className={highlightTextStyles.primary}>{filename}</span>}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
ReadDocumentByFilenameInspector.displayName = 'ReadDocumentByFilenameInspector';
|
||||
|
||||
export default ReadDocumentByFilenameInspector;
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
'use client';
|
||||
|
||||
import type { BuiltinInspectorProps } from '@lobechat/types';
|
||||
import { cx } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { highlightTextStyles, inspectorTextStyles, shinyTextStyles } from '@/styles';
|
||||
|
||||
import type { RemoveDocumentArgs, RemoveDocumentState } from '../../../types';
|
||||
|
||||
export const RemoveDocumentInspector = memo<
|
||||
BuiltinInspectorProps<RemoveDocumentArgs, RemoveDocumentState>
|
||||
>(({ args, partialArgs, isArgumentsStreaming, isLoading }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
const id = args?.id || partialArgs?.id;
|
||||
|
||||
if (isArgumentsStreaming && !id) {
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-agent-documents.apiName.removeDocument')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
inspectorTextStyles.root,
|
||||
(isArgumentsStreaming || isLoading) && shinyTextStyles.shinyText,
|
||||
)}
|
||||
>
|
||||
<span>{t('builtins.lobe-agent-documents.apiName.removeDocument')}: </span>
|
||||
{id && <span className={highlightTextStyles.primary}>{id}</span>}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
RemoveDocumentInspector.displayName = 'RemoveDocumentInspector';
|
||||
|
||||
export default RemoveDocumentInspector;
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
'use client';
|
||||
|
||||
import type { BuiltinInspectorProps } from '@lobechat/types';
|
||||
import { cx } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { highlightTextStyles, inspectorTextStyles, shinyTextStyles } from '@/styles';
|
||||
|
||||
import type { RenameDocumentArgs, RenameDocumentState } from '../../../types';
|
||||
|
||||
export const RenameDocumentInspector = memo<
|
||||
BuiltinInspectorProps<RenameDocumentArgs, RenameDocumentState>
|
||||
>(({ args, partialArgs, isArgumentsStreaming, isLoading }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
const newTitle = args?.newTitle || partialArgs?.newTitle;
|
||||
|
||||
if (isArgumentsStreaming && !newTitle) {
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-agent-documents.apiName.renameDocument')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
inspectorTextStyles.root,
|
||||
(isArgumentsStreaming || isLoading) && shinyTextStyles.shinyText,
|
||||
)}
|
||||
>
|
||||
<span>{t('builtins.lobe-agent-documents.apiName.renameDocument')}: </span>
|
||||
{newTitle && <span className={highlightTextStyles.primary}>{newTitle}</span>}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
RenameDocumentInspector.displayName = 'RenameDocumentInspector';
|
||||
|
||||
export default RenameDocumentInspector;
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
'use client';
|
||||
|
||||
import type { BuiltinInspectorProps } from '@lobechat/types';
|
||||
import { cx } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { highlightTextStyles, inspectorTextStyles, shinyTextStyles } from '@/styles';
|
||||
|
||||
import type { UpdateLoadRuleArgs, UpdateLoadRuleState } from '../../../types';
|
||||
|
||||
export const UpdateLoadRuleInspector = memo<
|
||||
BuiltinInspectorProps<UpdateLoadRuleArgs, UpdateLoadRuleState>
|
||||
>(({ args, partialArgs, isArgumentsStreaming, isLoading }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
const id = args?.id || partialArgs?.id;
|
||||
|
||||
if (isArgumentsStreaming && !id) {
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-agent-documents.apiName.updateLoadRule')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
inspectorTextStyles.root,
|
||||
(isArgumentsStreaming || isLoading) && shinyTextStyles.shinyText,
|
||||
)}
|
||||
>
|
||||
<span>{t('builtins.lobe-agent-documents.apiName.updateLoadRule')}: </span>
|
||||
{id && <span className={highlightTextStyles.primary}>{id}</span>}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
UpdateLoadRuleInspector.displayName = 'UpdateLoadRuleInspector';
|
||||
|
||||
export default UpdateLoadRuleInspector;
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
'use client';
|
||||
|
||||
import type { BuiltinInspectorProps } from '@lobechat/types';
|
||||
import { cx } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { highlightTextStyles, inspectorTextStyles, shinyTextStyles } from '@/styles';
|
||||
|
||||
import type { UpsertDocumentByFilenameArgs, UpsertDocumentByFilenameState } from '../../../types';
|
||||
|
||||
export const UpsertDocumentByFilenameInspector = memo<
|
||||
BuiltinInspectorProps<UpsertDocumentByFilenameArgs, UpsertDocumentByFilenameState>
|
||||
>(({ args, partialArgs, isArgumentsStreaming, isLoading }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
const filename = args?.filename || partialArgs?.filename;
|
||||
|
||||
if (isArgumentsStreaming && !filename) {
|
||||
return (
|
||||
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-agent-documents.apiName.upsertDocumentByFilename')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
inspectorTextStyles.root,
|
||||
(isArgumentsStreaming || isLoading) && shinyTextStyles.shinyText,
|
||||
)}
|
||||
>
|
||||
<span>{t('builtins.lobe-agent-documents.apiName.upsertDocumentByFilename')}: </span>
|
||||
{filename && <span className={highlightTextStyles.primary}>{filename}</span>}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
UpsertDocumentByFilenameInspector.displayName = 'UpsertDocumentByFilenameInspector';
|
||||
|
||||
export default UpsertDocumentByFilenameInspector;
|
||||
|
|
@ -1,15 +1,30 @@
|
|||
import type { BuiltinInspector } from '@lobechat/types';
|
||||
|
||||
import { AgentDocumentsApiName } from '../../types';
|
||||
import { AgentDocumentsInspector } from './AgentDocumentsInspector';
|
||||
import { CopyDocumentInspector } from './CopyDocument';
|
||||
import { CreateDocumentInspector } from './CreateDocument';
|
||||
import { EditDocumentInspector } from './EditDocument';
|
||||
import { ListDocumentsInspector } from './ListDocuments';
|
||||
import { PatchDocumentInspector } from './PatchDocument';
|
||||
import { ReadDocumentInspector } from './ReadDocument';
|
||||
import { ReadDocumentByFilenameInspector } from './ReadDocumentByFilename';
|
||||
import { RemoveDocumentInspector } from './RemoveDocument';
|
||||
import { RenameDocumentInspector } from './RenameDocument';
|
||||
import { UpdateLoadRuleInspector } from './UpdateLoadRule';
|
||||
import { UpsertDocumentByFilenameInspector } from './UpsertDocumentByFilename';
|
||||
|
||||
export const AgentDocumentsInspectors: Record<string, BuiltinInspector> = {
|
||||
[AgentDocumentsApiName.createDocument]: AgentDocumentsInspector as BuiltinInspector,
|
||||
[AgentDocumentsApiName.copyDocument]: AgentDocumentsInspector as BuiltinInspector,
|
||||
[AgentDocumentsApiName.editDocument]: AgentDocumentsInspector as BuiltinInspector,
|
||||
[AgentDocumentsApiName.patchDocument]: AgentDocumentsInspector as BuiltinInspector,
|
||||
[AgentDocumentsApiName.readDocument]: AgentDocumentsInspector as BuiltinInspector,
|
||||
[AgentDocumentsApiName.removeDocument]: AgentDocumentsInspector as BuiltinInspector,
|
||||
[AgentDocumentsApiName.renameDocument]: AgentDocumentsInspector as BuiltinInspector,
|
||||
[AgentDocumentsApiName.updateLoadRule]: AgentDocumentsInspector as BuiltinInspector,
|
||||
[AgentDocumentsApiName.copyDocument]: CopyDocumentInspector as BuiltinInspector,
|
||||
[AgentDocumentsApiName.createDocument]: CreateDocumentInspector as BuiltinInspector,
|
||||
[AgentDocumentsApiName.editDocument]: EditDocumentInspector as BuiltinInspector,
|
||||
[AgentDocumentsApiName.listDocuments]: ListDocumentsInspector as BuiltinInspector,
|
||||
[AgentDocumentsApiName.patchDocument]: PatchDocumentInspector as BuiltinInspector,
|
||||
[AgentDocumentsApiName.readDocument]: ReadDocumentInspector as BuiltinInspector,
|
||||
[AgentDocumentsApiName.readDocumentByFilename]:
|
||||
ReadDocumentByFilenameInspector as BuiltinInspector,
|
||||
[AgentDocumentsApiName.removeDocument]: RemoveDocumentInspector as BuiltinInspector,
|
||||
[AgentDocumentsApiName.renameDocument]: RenameDocumentInspector as BuiltinInspector,
|
||||
[AgentDocumentsApiName.updateLoadRule]: UpdateLoadRuleInspector as BuiltinInspector,
|
||||
[AgentDocumentsApiName.upsertDocumentByFilename]:
|
||||
UpsertDocumentByFilenameInspector as BuiltinInspector,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -268,22 +268,22 @@ describe('AgentDocumentModel', () => {
|
|||
const renamed = await agentDocumentModel.rename(created.id, 'New Name');
|
||||
|
||||
expect(renamed?.title).toBe('New Name');
|
||||
expect(renamed?.filename).toBe('New Name.md');
|
||||
expect(renamed?.filename).toBe('New Name');
|
||||
|
||||
const [doc] = await serverDB
|
||||
.select()
|
||||
.from(documents)
|
||||
.where(eq(documents.id, created.documentId));
|
||||
|
||||
expect(doc?.source).toBe(`agent-document://${agentId}/${encodeURIComponent('New Name.md')}`);
|
||||
expect(doc?.source).toBe(`agent-document://${agentId}/${encodeURIComponent('New Name')}`);
|
||||
});
|
||||
|
||||
it('should preserve typed extension when renaming', async () => {
|
||||
it('uses the new title verbatim as filename when renaming', async () => {
|
||||
const created = await agentDocumentModel.create(agentId, 'identity.md', 'hello');
|
||||
|
||||
const renamed = await agentDocumentModel.rename(created.id, 'IDENTITY 2.md');
|
||||
const renamed = await agentDocumentModel.rename(created.id, 'IDENTITY 2');
|
||||
|
||||
expect(renamed?.filename).toBe('IDENTITY 2.md');
|
||||
expect(renamed?.filename).toBe('IDENTITY 2');
|
||||
});
|
||||
|
||||
it('should copy into a new record and keep policy/template metadata', async () => {
|
||||
|
|
@ -299,7 +299,7 @@ describe('AgentDocumentModel', () => {
|
|||
expect(copied).toBeDefined();
|
||||
expect(copied?.id).not.toBe(created.id);
|
||||
expect(copied?.documentId).not.toBe(created.documentId);
|
||||
expect(copied?.filename).toBe('Copied Title.md');
|
||||
expect(copied?.filename).toBe('Copied Title');
|
||||
expect(copied?.templateId).toBe('claw');
|
||||
expect(copied?.policy?.context?.maxTokens).toBe(200);
|
||||
expect(copied?.metadata).toMatchObject({ description: 'source desc', domain: 'A' });
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { buildDocumentFilename, extractMarkdownH1Title, slugifyDocumentTitle } from '../filename';
|
||||
|
||||
describe('buildDocumentFilename', () => {
|
||||
it('returns the title as-is without appending an extension', () => {
|
||||
expect(buildDocumentFilename('Daily Brief')).toBe('Daily Brief');
|
||||
});
|
||||
|
||||
it('preserves non-ASCII titles', () => {
|
||||
expect(buildDocumentFilename('Daily Brief 提取框架')).toBe('Daily Brief 提取框架');
|
||||
});
|
||||
|
||||
it('replaces path separators to prevent traversal', () => {
|
||||
expect(buildDocumentFilename('foo/bar\\baz')).toBe('foo-bar-baz');
|
||||
});
|
||||
|
||||
it('falls back to a default when title is empty after sanitisation', () => {
|
||||
expect(buildDocumentFilename(' ')).toBe('document');
|
||||
expect(buildDocumentFilename('')).toBe('document');
|
||||
});
|
||||
|
||||
it('trims trailing dots and whitespace', () => {
|
||||
expect(buildDocumentFilename('note... ')).toBe('note');
|
||||
});
|
||||
});
|
||||
|
||||
describe('slugifyDocumentTitle', () => {
|
||||
it('lowercases and replaces spaces with hyphens', () => {
|
||||
expect(slugifyDocumentTitle('Daily Brief')).toBe('daily-brief');
|
||||
});
|
||||
|
||||
it('drops non-ascii characters', () => {
|
||||
expect(slugifyDocumentTitle('提取框架 extract')).toBe('extract');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractMarkdownH1Title', () => {
|
||||
it('extracts a leading H1 heading and strips it from content', () => {
|
||||
const result = extractMarkdownH1Title('# My Title\n\nbody line');
|
||||
expect(result).toEqual({ content: 'body line', title: 'My Title' });
|
||||
});
|
||||
|
||||
it('ignores leading blank lines before the H1', () => {
|
||||
const result = extractMarkdownH1Title('\n\n# Hi\nbody');
|
||||
expect(result.title).toBe('Hi');
|
||||
expect(result.content).toBe('body');
|
||||
});
|
||||
|
||||
it('returns the original content when no H1 is present', () => {
|
||||
const result = extractMarkdownH1Title('just body\n## Secondary');
|
||||
expect(result).toEqual({ content: 'just body\n## Secondary' });
|
||||
});
|
||||
|
||||
it('does not treat ## headings as H1', () => {
|
||||
const result = extractMarkdownH1Title('## Not H1\nbody');
|
||||
expect(result.title).toBeUndefined();
|
||||
});
|
||||
|
||||
it('handles empty H1 titles as no-op', () => {
|
||||
const result = extractMarkdownH1Title('# \nbody');
|
||||
expect(result.title).toBeUndefined();
|
||||
});
|
||||
|
||||
it('trims whitespace inside the heading', () => {
|
||||
const result = extractMarkdownH1Title('# Spaced Title \nbody');
|
||||
expect(result.title).toBe('Spaced Title');
|
||||
});
|
||||
});
|
||||
|
|
@ -138,6 +138,7 @@ export class AgentDocumentModel {
|
|||
policy?: AgentDocumentPolicy;
|
||||
policyLoad?: PolicyLoad;
|
||||
templateId?: string;
|
||||
title?: string;
|
||||
updatedAt?: Date;
|
||||
},
|
||||
): Promise<AgentDocument> {
|
||||
|
|
@ -149,10 +150,11 @@ export class AgentDocumentModel {
|
|||
policy,
|
||||
policyLoad,
|
||||
templateId,
|
||||
title: providedTitle,
|
||||
updatedAt,
|
||||
} = params ?? {};
|
||||
|
||||
const title = filename.replace(/\.[^.]+$/, '');
|
||||
const title = providedTitle?.trim() || filename.replace(/\.[^.]+$/, '');
|
||||
const stats = this.getDocumentStats(content);
|
||||
const normalizedPolicy = normalizePolicy(loadPosition, loadRules, policy);
|
||||
|
||||
|
|
@ -290,7 +292,7 @@ export class AgentDocumentModel {
|
|||
const title = newTitle.trim();
|
||||
if (!title) return existing;
|
||||
|
||||
const filename = buildDocumentFilename(title, existing.filename);
|
||||
const filename = buildDocumentFilename(title);
|
||||
const source = `agent-document://${existing.agentId}/${encodeURIComponent(filename)}`;
|
||||
|
||||
await this.db
|
||||
|
|
@ -311,10 +313,11 @@ export class AgentDocumentModel {
|
|||
|
||||
const title = newTitle?.trim();
|
||||
const filename = title
|
||||
? buildDocumentFilename(title, existing.filename)
|
||||
? buildDocumentFilename(title)
|
||||
: `copy-${Date.now()}-${existing.filename}`;
|
||||
|
||||
return this.create(existing.agentId, filename, existing.content, {
|
||||
title,
|
||||
loadPosition:
|
||||
(existing.policy?.context?.position as DocumentLoadPosition | undefined) ||
|
||||
DocumentLoadPosition.BEFORE_FIRST_USER,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
const SLUG_FALLBACK = 'document';
|
||||
const FILENAME_FALLBACK = 'document';
|
||||
|
||||
export const slugifyDocumentTitle = (title: string): string =>
|
||||
title
|
||||
|
|
@ -18,14 +18,34 @@ const sanitizeDocumentFilename = (value: string): string =>
|
|||
.replaceAll('\0', '')
|
||||
.replaceAll(/[.\s]+$/g, '');
|
||||
|
||||
export const buildDocumentFilename = (title: string, fallbackFilename = 'document.txt'): string => {
|
||||
const extensionMatch = fallbackFilename.match(/(\.[^./\\]+)$/);
|
||||
const extension = extensionMatch?.[1] || '.txt';
|
||||
const sanitizedTitle = sanitizeDocumentFilename(title);
|
||||
if (!sanitizedTitle) return `${SLUG_FALLBACK}${extension}`;
|
||||
|
||||
const typedExtensionMatch = sanitizedTitle.match(/(\.[^./\\]+)$/);
|
||||
if (typedExtensionMatch) return sanitizedTitle;
|
||||
|
||||
return `${sanitizedTitle}${extension}`;
|
||||
export const buildDocumentFilename = (title: string): string => {
|
||||
const sanitized = sanitizeDocumentFilename(title);
|
||||
return sanitized || FILENAME_FALLBACK;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract the first-level markdown heading (# Title) from content and return
|
||||
* the remaining content with that heading stripped.
|
||||
*
|
||||
* Only matches a `# ` heading that appears at the top of the document
|
||||
* (optionally preceded by blank lines). Setext-style (`===`) and indented
|
||||
* headings are not recognised.
|
||||
*/
|
||||
export const extractMarkdownH1Title = (content: string): { content: string; title?: string } => {
|
||||
const lines = content.split(/\r?\n/);
|
||||
|
||||
let i = 0;
|
||||
while (i < lines.length && lines[i].trim() === '') i += 1;
|
||||
if (i >= lines.length) return { content };
|
||||
|
||||
const headingMatch = lines[i].match(/^[ \t]*#[ \t]+(\S.*)$/);
|
||||
if (!headingMatch) return { content };
|
||||
|
||||
const title = headingMatch[1].trim();
|
||||
if (!title) return { content };
|
||||
|
||||
let j = i + 1;
|
||||
while (j < lines.length && lines[j].trim() === '') j += 1;
|
||||
|
||||
return { content: lines.slice(j).join('\n'), title };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ const DocumentItem = memo<DocumentItemProps>(({ agentId, document, isActive, mut
|
|||
const openDocument = useChatStore((s) => s.openDocument);
|
||||
const closeDocument = useChatStore((s) => s.closeDocument);
|
||||
|
||||
const title = document.filename || document.title || '';
|
||||
const title = document.title || document.filename || '';
|
||||
const description = document.description ?? undefined;
|
||||
const isWeb = document.sourceType === 'web';
|
||||
const IconComponent: LucideIcon = isWeb ? GlobeIcon : FileTextIcon;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
// @vitest-environment node
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { AgentDocumentModel, buildDocumentFilename } from '@/database/models/agentDocuments';
|
||||
import {
|
||||
AgentDocumentModel,
|
||||
buildDocumentFilename,
|
||||
extractMarkdownH1Title,
|
||||
} from '@/database/models/agentDocuments';
|
||||
import type { LobeChatDatabase } from '@/database/type';
|
||||
|
||||
import { AgentDocumentsService } from './agentDocuments';
|
||||
|
|
@ -12,6 +16,7 @@ vi.mock('@/database/models/agentDocuments', () => ({
|
|||
BEFORE_FIRST_USER: 'before_first_user',
|
||||
},
|
||||
buildDocumentFilename: vi.fn(),
|
||||
extractMarkdownH1Title: vi.fn((content: string) => ({ content })),
|
||||
}));
|
||||
|
||||
describe('AgentDocumentsService', () => {
|
||||
|
|
@ -30,7 +35,8 @@ describe('AgentDocumentsService', () => {
|
|||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
(AgentDocumentModel as any).mockImplementation(() => mockModel);
|
||||
vi.mocked(buildDocumentFilename).mockImplementation((title: string) => `${title}.md`);
|
||||
vi.mocked(buildDocumentFilename).mockImplementation((title: string) => title);
|
||||
vi.mocked(extractMarkdownH1Title).mockImplementation((content: string) => ({ content }));
|
||||
});
|
||||
|
||||
describe('createDocument', () => {
|
||||
|
|
@ -38,15 +44,17 @@ describe('AgentDocumentsService', () => {
|
|||
mockModel.findByFilename
|
||||
.mockResolvedValueOnce({ id: 'existing-doc' })
|
||||
.mockResolvedValueOnce(undefined);
|
||||
mockModel.create.mockResolvedValue({ id: 'new-doc', filename: 'note-2.md' });
|
||||
mockModel.create.mockResolvedValue({ id: 'new-doc', filename: 'note-2' });
|
||||
|
||||
const service = new AgentDocumentsService(db, userId);
|
||||
const result = await service.createDocument('agent-1', 'note', 'content');
|
||||
|
||||
expect(mockModel.findByFilename).toHaveBeenNthCalledWith(1, 'agent-1', 'note.md');
|
||||
expect(mockModel.findByFilename).toHaveBeenNthCalledWith(2, 'agent-1', 'note-2.md');
|
||||
expect(mockModel.create).toHaveBeenCalledWith('agent-1', 'note-2.md', 'content', undefined);
|
||||
expect(result).toEqual({ id: 'new-doc', filename: 'note-2.md' });
|
||||
expect(mockModel.findByFilename).toHaveBeenNthCalledWith(1, 'agent-1', 'note');
|
||||
expect(mockModel.findByFilename).toHaveBeenNthCalledWith(2, 'agent-1', 'note-2');
|
||||
expect(mockModel.create).toHaveBeenCalledWith('agent-1', 'note-2', 'content', {
|
||||
title: 'note',
|
||||
});
|
||||
expect(result).toEqual({ id: 'new-doc', filename: 'note-2' });
|
||||
});
|
||||
|
||||
it('should throw after too many filename collisions', async () => {
|
||||
|
|
@ -59,6 +67,23 @@ describe('AgentDocumentsService', () => {
|
|||
);
|
||||
expect(mockModel.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should extract H1 from markdown content as the document title', async () => {
|
||||
vi.mocked(extractMarkdownH1Title).mockReturnValueOnce({
|
||||
content: 'body',
|
||||
title: 'My Title',
|
||||
});
|
||||
mockModel.findByFilename.mockResolvedValue(undefined);
|
||||
mockModel.create.mockResolvedValue({ id: 'new-doc', filename: 'My Title' });
|
||||
|
||||
const service = new AgentDocumentsService(db, userId);
|
||||
await service.createDocument('agent-1', 'fallback', '# My Title\n\nbody');
|
||||
|
||||
expect(vi.mocked(buildDocumentFilename)).toHaveBeenCalledWith('My Title');
|
||||
expect(mockModel.create).toHaveBeenCalledWith('agent-1', 'My Title', 'body', {
|
||||
title: 'My Title',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('listDocuments', () => {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
type AgentDocumentWithRules,
|
||||
type ToolUpdateLoadRule,
|
||||
} from '@/database/models/agentDocuments';
|
||||
import { buildDocumentFilename } from '@/database/models/agentDocuments';
|
||||
import { buildDocumentFilename, extractMarkdownH1Title } from '@/database/models/agentDocuments';
|
||||
|
||||
const MAX_UNIQUE_FILENAME_ATTEMPTS = 1000;
|
||||
|
||||
|
|
@ -56,9 +56,6 @@ export class AgentDocumentsService {
|
|||
},
|
||||
) {
|
||||
const baseFilename = buildDocumentFilename(title);
|
||||
const extensionMatch = baseFilename.match(/(\.[^./\\]+)$/);
|
||||
const extension = extensionMatch?.[1] || '.txt';
|
||||
const baseName = baseFilename.slice(0, -extension.length);
|
||||
|
||||
let filename = baseFilename;
|
||||
let suffix = 2;
|
||||
|
|
@ -70,11 +67,11 @@ export class AgentDocumentsService {
|
|||
);
|
||||
}
|
||||
|
||||
filename = `${baseName}-${suffix}${extension}`;
|
||||
filename = `${baseFilename}-${suffix}`;
|
||||
suffix += 1;
|
||||
}
|
||||
|
||||
return this.agentDocumentModel.create(agentId, filename, content, params);
|
||||
return this.agentDocumentModel.create(agentId, filename, content, { ...params, title });
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -213,7 +210,9 @@ export class AgentDocumentsService {
|
|||
}
|
||||
|
||||
async createDocument(agentId: string, title: string, content: string) {
|
||||
return this.createWithUniqueFilename(agentId, title, content);
|
||||
const { title: extractedTitle, content: strippedContent } = extractMarkdownH1Title(content);
|
||||
const finalTitle = extractedTitle || title;
|
||||
return this.createWithUniqueFilename(agentId, finalTitle, strippedContent);
|
||||
}
|
||||
|
||||
async deleteDocument(documentId: string) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
import { AgentDocumentsExecutionRuntime } from '@lobechat/builtin-tool-agent-documents/executionRuntime';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { agentDocumentsRuntime } from '../agentDocuments';
|
||||
|
||||
vi.mock('@/server/services/agentDocuments');
|
||||
|
||||
describe('agentDocumentsRuntime', () => {
|
||||
it('should have correct identifier', () => {
|
||||
expect(agentDocumentsRuntime.identifier).toBe('lobe-agent-documents');
|
||||
});
|
||||
|
||||
it('should throw if userId is missing', () => {
|
||||
expect(() =>
|
||||
agentDocumentsRuntime.factory({ serverDB: {} as any, toolManifestMap: {} }),
|
||||
).toThrow('userId and serverDB are required for Agent Documents execution');
|
||||
});
|
||||
|
||||
it('should throw if serverDB is missing', () => {
|
||||
expect(() => agentDocumentsRuntime.factory({ toolManifestMap: {}, userId: 'user-1' })).toThrow(
|
||||
'userId and serverDB are required for Agent Documents execution',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AgentDocumentsExecutionRuntime.createDocument', () => {
|
||||
const makeStub = () => ({
|
||||
copyDocument: vi.fn(),
|
||||
createDocument: vi.fn(),
|
||||
editDocument: vi.fn(),
|
||||
listDocuments: vi.fn(),
|
||||
readDocument: vi.fn(),
|
||||
readDocumentByFilename: vi.fn(),
|
||||
removeDocument: vi.fn(),
|
||||
renameDocument: vi.fn(),
|
||||
updateLoadRule: vi.fn(),
|
||||
upsertDocumentByFilename: vi.fn(),
|
||||
});
|
||||
|
||||
it('returns documents.id (not agentDocuments.id) for state.documentId', async () => {
|
||||
const stub = makeStub();
|
||||
stub.createDocument.mockResolvedValue({
|
||||
documentId: 'documents-row-id',
|
||||
filename: 'daily-brief',
|
||||
id: 'agent-doc-assoc-id',
|
||||
title: 'Daily Brief',
|
||||
});
|
||||
|
||||
const runtime = new AgentDocumentsExecutionRuntime(stub);
|
||||
const result = await runtime.createDocument(
|
||||
{ content: 'body', title: 'Daily Brief' },
|
||||
{ agentId: 'agent-1' },
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.state).toEqual({ documentId: 'documents-row-id' });
|
||||
});
|
||||
|
||||
it('refuses to run without agentId', async () => {
|
||||
const stub = makeStub();
|
||||
const runtime = new AgentDocumentsExecutionRuntime(stub);
|
||||
|
||||
const result = await runtime.createDocument({ content: 'body', title: 'T' }, {});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(stub.createDocument).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue