♻️ refactor: refactor with editor runtime

This commit is contained in:
arvinxx 2025-12-29 02:01:56 +08:00
parent 37e33b8b73
commit be2b41c792
23 changed files with 1511 additions and 293 deletions

View file

@ -185,6 +185,7 @@
"@lobechat/database": "workspace:*",
"@lobechat/desktop-bridge": "workspace:*",
"@lobechat/edge-config": "workspace:*",
"@lobechat/editor-runtime": "workspace:*",
"@lobechat/electron-client-ipc": "workspace:*",
"@lobechat/electron-server-ipc": "workspace:*",
"@lobechat/fetch-sse": "workspace:*",

View file

@ -5,19 +5,16 @@
"exports": {
".": "./src/index.ts",
"./client": "./src/client/index.ts",
"./executor": "./src/executor/index.ts",
"./executionRuntime": "./src/ExecutionRuntime/index.ts"
"./executor": "./src/executor/index.ts"
},
"main": "./src/index.ts",
"dependencies": {
"lexical": "^0.39.0"
"@lobechat/editor-runtime": "workspace:*"
},
"devDependencies": {
"@lobechat/types": "workspace:*",
"@lobehub/editor": "^3"
"@lobechat/types": "workspace:*"
},
"peerDependencies": {
"@lobehub/editor": "^3",
"@lobehub/ui": "^4",
"antd-style": "*",
"debug": "*",

View file

@ -1,145 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`PageAgentExecutionRuntime > modifyNodes > error handling > should normalize single operation to array 1`] = `
"# Title
Single op
First paragraph.
Second paragraph.
"
`;
exports[`PageAgentExecutionRuntime > modifyNodes > insert > should insert multiple nodes after same node 1`] = `
"# Title
Insert 3
Insert 2
Insert 1
First paragraph.
Second paragraph.
"
`;
exports[`PageAgentExecutionRuntime > modifyNodes > insert > should insert single node after existing node 1`] = `
"# Title
New inserted paragraph
First paragraph.
Second paragraph.
"
`;
exports[`PageAgentExecutionRuntime > modifyNodes > mixed operations > should handle insert, modify, and remove in single call 1`] = `
"# Title
# Updated title
New content
First paragraph.
Second paragraph.
"
`;
exports[`PageAgentExecutionRuntime > modifyNodes > modify > should modify existing node content 1`] = `
"# Title
First paragraph.
Modified content here
Second paragraph.
"
`;
exports[`PageAgentExecutionRuntime > modifyNodes > modify > should modify multiple nodes at once 1`] = `
"# Title
First paragraph.
Modified first
Second paragraph.
Modified second
"
`;
exports[`PageAgentExecutionRuntime > modifyNodes > remove > should remove existing node 1`] = `
"<?xml version="1.0" encoding="UTF-8"?>
<root>
<h1 id="ll63">
<span id="lqqe">Title</span>
</h1>
<p id="m7fb">
<span id="mczm">Second paragraph.</span>
</p>
</root>"
`;
exports[`PageAgentExecutionRuntime > modifyNodes > remove > should remove multiple nodes 1`] = `
"# Title
First paragraph.
Second paragraph.
"
`;
exports[`PageAgentExecutionRuntime > replaceText > should replace all occurrences by default 1`] = `
"Hi world. This is a test. Hi again. Testing the world.
"
`;
exports[`PageAgentExecutionRuntime > replaceText > should support regex first occurrence only 1`] = `
"X world. This is a test. Hello again. Testing the world.
"
`;
exports[`PageAgentExecutionRuntime > replaceText > should support regex patterns with optional groups 1`] = `
"Hello world. This is a demo. Hello again. Testing the world.
"
`;
exports[`PageAgentExecutionRuntime > replaceText > should support regex with alternation 1`] = `
"X X. This is a test. X again. Testing the X.
"
`;
exports[`PageAgentExecutionRuntime > replaceText > should support regex with character classes 1`] = `
"Guest and Guest are online.
"
`;
exports[`PageAgentExecutionRuntime > replaceText > should support regex with quantifiers 1`] = `
"Hi world! Hi again!
"
`;
exports[`PageAgentExecutionRuntime > replaceText > should support regex with word boundaries 1`] = `
"Hello universe. This is a test. Hello again. Testing the universe.
"
`;

View file

@ -7,7 +7,9 @@ import { Trans, useTranslation } from 'react-i18next';
import { shinyTextStyles } from '@/styles';
import type { EditTitleArgs, EditTitleState } from '../../../types';
import type { EditTitleArgs } from '@lobechat/editor-runtime';
import type { EditTitleState } from '../../../types';
const styles = createStaticStyles(({ css, cssVar }) => ({
highlight: css`

View file

@ -9,7 +9,9 @@ import { useTranslation } from 'react-i18next';
import { shinyTextStyles } from '@/styles';
import type { InitDocumentArgs, InitDocumentState } from '../../../types';
import type { InitDocumentArgs } from '@lobechat/editor-runtime';
import type { InitDocumentState } from '../../../types';
import { AnimatedNumber } from '../../components/AnimatedNumber';
const styles = createStaticStyles(({ css, cssVar }) => ({

View file

@ -9,7 +9,9 @@ import { useTranslation } from 'react-i18next';
import { shinyTextStyles } from '@/styles';
import type { ModifyNodesArgs, ModifyNodesState } from '../../../types';
import type { ModifyNodesArgs } from '@lobechat/editor-runtime';
import type { ModifyNodesState } from '../../../types';
const styles = createStaticStyles(({ css, cssVar }) => ({
insert: css`

View file

@ -9,7 +9,9 @@ import { useTranslation } from 'react-i18next';
import { shinyTextStyles } from '@/styles';
import type { ReplaceTextArgs, ReplaceTextState } from '../../../types';
import type { ReplaceTextArgs } from '@lobechat/editor-runtime';
import type { ReplaceTextState } from '../../../types';
const styles = createStaticStyles(({ css, cssVar }) => ({
arrow: css`

View file

@ -1,13 +1,13 @@
import type { EditorRuntime } from '@lobechat/editor-runtime';
import type { BuiltinToolContext } from '@lobechat/types';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { PageAgentExecutionRuntime } from '../ExecutionRuntime';
import { PageAgentIdentifier } from '../types';
import { PageAgentExecutor } from './index';
describe('PageAgentExecutor', () => {
let executor: PageAgentExecutor;
let mockRuntime: PageAgentExecutionRuntime;
let mockRuntime: EditorRuntime;
const mockContext = {} as BuiltinToolContext;
beforeEach(() => {
@ -18,7 +18,7 @@ describe('PageAgentExecutor', () => {
initPage: vi.fn(),
modifyNodes: vi.fn(),
replaceText: vi.fn(),
} as unknown as PageAgentExecutionRuntime;
} as unknown as EditorRuntime;
executor = new PageAgentExecutor(mockRuntime);
});

View file

@ -1,16 +1,18 @@
import { BaseExecutor, type BuiltinToolResult } from '@lobechat/types';
import { PageAgentExecutionRuntime } from '../ExecutionRuntime';
import type {
EditTitleArgs,
EditTitleState,
EditorRuntime,
GetPageContentArgs,
GetPageContentState,
InitDocumentArgs,
InitDocumentState,
ModifyNodesArgs,
ModifyNodesState,
ReplaceTextArgs,
} from '@lobechat/editor-runtime';
import { BaseExecutor, type BuiltinToolResult } from '@lobechat/types';
import type {
EditTitleState,
GetPageContentState,
InitDocumentState,
ModifyNodesState,
ReplaceTextState,
} from '../types';
import { PageAgentIdentifier } from '../types';
@ -39,7 +41,7 @@ const PageAgentApiName = {
/**
* Page Agent Executor
*
* Wraps the PageAgentExecutionRuntime to provide a unified executor interface
* Wraps the EditorRuntime to provide a unified executor interface
* that follows the BaseExecutor pattern used by other builtin tools.
*
* Note: Page Agent is a client-side tool that directly manipulates the Lexical editor.
@ -53,9 +55,9 @@ class PageAgentExecutor extends BaseExecutor<typeof PageAgentApiName> {
* The execution runtime instance
* This is a singleton that should be configured with an editor instance externally
*/
private runtime: PageAgentExecutionRuntime;
private runtime: EditorRuntime;
constructor(runtime: PageAgentExecutionRuntime) {
constructor(runtime: EditorRuntime) {
super();
this.runtime = runtime;
}

View file

@ -1,21 +1,25 @@
// Re-export runtime types from @lobechat/editor-runtime
export type {
EditTitleArgs,
GetPageContentArgs,
InitDocumentArgs,
ModifyInsertOperation,
ModifyNodesArgs,
ModifyOperation,
ModifyOperationResult,
ModifyRemoveOperation,
ModifyUpdateOperation,
ReplaceTextArgs,
} from '@lobechat/editor-runtime';
export { PageAgentManifest } from './manifest';
export { systemPrompt } from './systemRole';
export {
DocumentApiName,
type EditTitleArgs,
type EditTitleState,
type GetPageContentArgs,
type GetPageContentState,
type InitDocumentArgs,
type InitDocumentState,
type ModifyInsertOperation,
type ModifyNodesArgs,
type ModifyNodesState,
type ModifyOperation,
type ModifyOperationResult,
type ModifyRemoveOperation,
type ModifyUpdateOperation,
PageAgentIdentifier,
type ReplaceTextArgs,
type ReplaceTextState,
} from './types';

View file

@ -22,65 +22,6 @@ export const DocumentApiName = {
};
/* eslint-enable sort-keys-fix/sort-keys-fix */
// ============ Initialize Args ============
export interface InitDocumentArgs {
markdown: string;
}
// ============ Document Metadata Args ============
export interface EditTitleArgs {
title: string;
}
// ============ Query & Search Args ============
export interface GetPageContentArgs {
format?: 'xml' | 'markdown' | 'both';
}
// ============ Unified Modify Nodes Args ============
/** Insert operation: insert a node before or after a reference node */
export type ModifyInsertOperation =
| {
action: 'insert';
afterId: string;
litexml: string;
}
| {
action: 'insert';
beforeId: string;
litexml: string;
};
/** Remove operation: remove a node by ID */
export interface ModifyRemoveOperation {
action: 'remove';
id: string;
}
/** Modify operation: update existing nodes by their IDs (embedded in litexml) */
export interface ModifyUpdateOperation {
action: 'modify';
litexml: string | string[];
}
/** Union type for all modify operations */
export type ModifyOperation = ModifyInsertOperation | ModifyRemoveOperation | ModifyUpdateOperation;
/** Args for the unified modifyNodes API */
export interface ModifyNodesArgs {
operations: ModifyOperation[];
}
// ============ Text Operations Args ============
export interface ReplaceTextArgs {
newText: string;
nodeIds?: string[];
replaceAll?: boolean;
searchText: string;
useRegex?: boolean;
}
// ============ State Types for Renders ============
export interface GetPageContentState {
@ -95,15 +36,12 @@ export interface GetPageContentState {
xml?: string;
}
/** Result of a single modify operation */
export interface ModifyOperationResult {
action: 'insert' | 'remove' | 'modify';
error?: string;
success: boolean;
}
export interface ModifyNodesState {
results: ModifyOperationResult[];
results: Array<{
action: 'insert' | 'remove' | 'modify';
error?: string;
success: boolean;
}>;
successCount: number;
totalCount: number;
}
@ -126,37 +64,3 @@ export interface EditTitleState {
newTitle: string;
previousTitle: string;
}
// ============ Runtime Result Types ============
// These are the raw result types returned by Runtime methods
// Executor is responsible for converting these to BuiltinToolResult format
export interface InitPageRuntimeResult {
extractedTitle?: string;
nodeCount: number;
}
export interface EditTitleRuntimeResult {
newTitle: string;
previousTitle: string;
}
export interface GetPageContentRuntimeResult {
charCount?: number;
documentId: string;
lineCount?: number;
markdown?: string;
title: string;
xml?: string;
}
export interface ModifyNodesRuntimeResult {
results: ModifyOperationResult[];
successCount: number;
totalCount: number;
}
export interface ReplaceTextRuntimeResult {
modifiedNodeIds: string[];
replacementCount: number;
}

View file

@ -0,0 +1,20 @@
{
"name": "@lobechat/editor-runtime",
"version": "1.0.0",
"private": true,
"exports": {
".": "./src/index.ts"
},
"main": "./src/index.ts",
"dependencies": {
"@lobechat/prompts": "workspace:*"
},
"devDependencies": {
"@lobehub/editor": "^3",
"lexical": "^0.39.0"
},
"peerDependencies": {
"@lobehub/editor": "^3",
"debug": "*"
}
}

View file

@ -14,17 +14,20 @@ import type {
ModifyOperationResult,
ReplaceTextArgs,
ReplaceTextRuntimeResult,
} from '../types';
} from './types';
const log = debug('page:page-agent');
const log = debug('lobe:editor-runtime');
/**
* Page Agent Execution Runtime
* Handles the execution logic for all Page Agent (Document) APIs
*
* See `packages/builtin-agents/src/agents/page-agent/README.md` for more detailsd
* Editor Execution Runtime
* Handles the execution logic for editor operations including:
* - Document initialization
* - Title management
* - Content retrieval
* - Node modifications (insert, modify, remove)
* - Text replacement
*/
export class PageAgentExecutionRuntime {
export class EditorRuntime {
private editor: IEditor | null = null;
private titleSetter: ((title: string) => void) | null = null;
private titleGetter: (() => string) | null = null;

View file

@ -0,0 +1,182 @@
import {
CommonPlugin,
type IEditor,
Kernel,
LitexmlPlugin,
MarkdownPlugin,
moment,
} from '@lobehub/editor';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { EditorRuntime } from '../EditorRuntime';
import editAllFixture from './fixtures/edit-all.json';
import removeFixture from './fixtures/remove.json';
describe('EditorRuntime - Real Cases', () => {
let runtime: EditorRuntime;
let editor: IEditor;
let mockTitleSetter: ReturnType<typeof vi.fn>;
let mockTitleGetter: ReturnType<typeof vi.fn>;
beforeEach(() => {
editor = new Kernel();
editor.registerPlugins([CommonPlugin, MarkdownPlugin, LitexmlPlugin]);
editor.initNodeEditor();
runtime = new EditorRuntime();
runtime.setEditor(editor);
mockTitleSetter = vi.fn();
mockTitleGetter = vi.fn().mockReturnValue('Test Title');
runtime.setTitleHandlers(mockTitleSetter, mockTitleGetter);
});
describe('modifyNodes - batch modify all paragraphs', () => {
it('should modify all 16 paragraphs in a single call', async () => {
// Initialize editor with the JSON fixture
editor.setDocument('json', editAllFixture);
await moment();
// Get the XML to verify initial state
const xmlBefore = editor.getDocument('litexml') as unknown as string;
const paragraphMatches = [...xmlBefore.matchAll(/<p id="([^"]+)"/g)];
expect(paragraphMatches.length).toBe(16);
// Extract paragraph IDs from the XML
const paragraphIds = paragraphMatches.map((m) => m[1]);
const result = await runtime.modifyNodes({
operations: [
{
action: 'modify',
litexml: `<p id="${paragraphIds[0]}">(雨点敲打着咖啡馆的玻璃窗,像无数细小的手指在弹奏着无声的钢琴。林晓坐在靠窗的位置,手中的咖啡已经凉了,她却浑然不觉。这是她第三次来到这家咖啡馆,每次都是同样的位置,同样的时间。)</p>`,
},
{
action: 'modify',
litexml: `<p id="${paragraphIds[1]}">(窗外雨丝如帘,街灯昏黄。咖啡馆内灯光柔和,墙上挂着旧照片,书架上摆满了书。空气里是咖啡香和旧书纸的味道。)</p>`,
},
{
action: 'modify',
litexml: `<p id="${paragraphIds[2]}">林晓:(内心独白)第一次来的时候,也是这样的雨夜。那天我刚结束一段五年的感情,整个人像是被掏空了。我点了一杯美式咖啡,就这样坐着,看着窗外的雨,直到打烊。</p>`,
},
{
action: 'modify',
litexml: `<p id="${paragraphIds[3]}">林晓:(继续独白)第二次来的时候,我遇到了他。穿着灰色风衣,坐在对面的位置。他一直在看书,偶尔抬头看看窗外。他的手指修长,翻书的动作优雅从容。</p>`,
},
{
action: 'modify',
litexml: `<p id="${paragraphIds[4]}">林晓:(独白)今天,我又来了。雨还是那样下着,咖啡馆还是那样安静。我不知道自己在期待什么,也许只是习惯了这种孤独的仪式感。</p>`,
},
{
action: 'modify',
litexml: `<p id="${paragraphIds[5]}">(门上的风铃响了,有人推门进来。林晓下意识地抬头,心跳突然漏了一拍。)</p>`,
},
{
action: 'modify',
litexml: `<p id="${paragraphIds[6]}">(风铃叮咚作响。一个身影站在门口,雨伞滴着水,灯光勾勒出他的轮廓。)</p>`,
},
{
action: 'modify',
litexml: `<p id="${paragraphIds[7]}">林晓:(低声)是他。</p>`,
},
{
action: 'modify',
litexml: `<p id="${paragraphIds[8]}">(他收起雨伞,抖了抖身上的水珠,然后径直走向她。这一次,他没有坐在对面的位置,而是在她面前停了下来。)</p>`,
},
{
action: 'modify',
litexml: `<p id="${paragraphIds[9]}">陈默:(声音低沉温和)我可以坐这里吗?</p>`,
},
{
action: 'modify',
litexml: `<p id="${paragraphIds[10]}">(林晓点了点头,喉咙有些发干。窗外的雨声似乎变小了,咖啡馆里的音乐也变得清晰起来。)</p>`,
},
{
action: 'modify',
litexml: `<p id="${paragraphIds[11]}">陈默:(眼中带着笑意)我注意到你每次都在这里。</p>`,
},
{
action: 'modify',
litexml: `<p id="${paragraphIds[12]}">林晓:(凝视着他,内心独白)他的眼睛是深褐色的,像秋天的落叶,温暖而深邃。他的鼻梁挺直,唇线分明,微笑时眼角有细微的皱纹,更添了几分沧桑感。我忽然觉得这个人似曾相识,却又分明是第一次见面。</p>`,
},
{
action: 'modify',
litexml: `<p id="${paragraphIds[13]}">陈默:(伸出手)我叫陈默。很高兴终于能和你说话。我观察你三次了,每次你都这样静静地坐着看雨,若有所思,若有所待。</p>`,
},
{
action: 'modify',
litexml: `<p id="${paragraphIds[14]}">(林晓握住他的手,感受到他掌心的温度。他的手温暖,却有薄茧,像是经常写字或弹琴的人。雨还在下,但咖啡馆里忽然觉得温暖如春,先前的孤独寂寥,竟然悄然消散了。)</p>`,
},
{
action: 'modify',
litexml: `<p id="${paragraphIds[15]}">(旁白)也许,有些相遇注定要在雨夜发生。就像有些故事,注定要从一句简单的问候开始。</p>`,
},
],
});
await moment();
// Verify all operations succeeded
expect(result.successCount).toBe(16);
expect(result.totalCount).toBe(16);
expect(result.results.every((r) => r.success)).toBe(true);
expect(result.results.every((r) => r.action === 'modify')).toBe(true);
// Verify the content was modified
const markdown = editor.getDocument('markdown') as unknown as string;
expect(markdown).toContain('雨点敲打着咖啡馆的玻璃窗');
expect(markdown).toContain('林晓:(低声)是他。');
expect(markdown).toContain('陈默:(声音低沉温和)我可以坐这里吗?');
expect(markdown).toContain('也许,有些相遇注定要在雨夜发生');
expect(markdown).toMatchSnapshot();
});
});
describe('modifyNodes - batch remove paragraphs', () => {
it('should remove 7 paragraphs in a single call', async () => {
// Initialize editor with the JSON fixture
editor.setDocument('json', removeFixture);
await moment();
// Get paragraph count before removal
const xmlBefore = editor.getDocument('litexml') as unknown as string;
const paragraphsBefore = [...xmlBefore.matchAll(/<p id="([^"]+)"/g)];
const initialCount = paragraphsBefore.length;
const result = await runtime.modifyNodes({
operations: [
{ action: 'remove', id: 'wps3' },
{ action: 'remove', id: 'w936' },
{ action: 'remove', id: 'vse9' },
{ action: 'remove', id: 'sp45' },
{ action: 'remove', id: 's8f8' },
{ action: 'remove', id: 'rrqb' },
{ action: 'remove', id: 'plu1' },
],
});
await moment();
// Verify all operations succeeded
expect(result.successCount).toBe(7);
expect(result.totalCount).toBe(7);
expect(result.results.every((r) => r.success)).toBe(true);
expect(result.results.every((r) => r.action === 'remove')).toBe(true);
// Verify paragraphs were removed
const xmlAfter = editor.getDocument('litexml') as unknown as string;
const paragraphsAfter = [...xmlAfter.matchAll(/<p id="([^"]+)"/g)];
expect(paragraphsAfter.length).toBe(initialCount - 7);
// Verify the removed IDs are no longer present
expect(xmlAfter).not.toContain('id="wps3"');
expect(xmlAfter).not.toContain('id="w936"');
expect(xmlAfter).not.toContain('id="vse9"');
expect(xmlAfter).not.toContain('id="sp45"');
expect(xmlAfter).not.toContain('id="s8f8"');
expect(xmlAfter).not.toContain('id="rrqb"');
expect(xmlAfter).not.toContain('id="plu1"');
expect(xmlAfter).toMatchSnapshot();
});
});
});

View file

@ -9,10 +9,10 @@ import {
import { resetRandomKey } from 'lexical';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { PageAgentExecutionRuntime } from './index';
import { EditorRuntime } from '../EditorRuntime';
describe('PageAgentExecutionRuntime', () => {
let runtime: PageAgentExecutionRuntime;
describe('EditorRuntime', () => {
let runtime: EditorRuntime;
let editor: IEditor;
let mockTitleSetter: ReturnType<typeof vi.fn>;
let mockTitleGetter: ReturnType<typeof vi.fn>;
@ -23,7 +23,7 @@ describe('PageAgentExecutionRuntime', () => {
editor.registerPlugins([CommonPlugin, MarkdownPlugin, LitexmlPlugin]);
editor.initNodeEditor();
runtime = new PageAgentExecutionRuntime();
runtime = new EditorRuntime();
runtime.setEditor(editor);
mockTitleSetter = vi.fn();
@ -515,7 +515,7 @@ describe('PageAgentExecutionRuntime', () => {
});
it('should return undefined when no document ID is set', () => {
const newRuntime = new PageAgentExecutionRuntime();
const newRuntime = new EditorRuntime();
expect(newRuntime.getCurrentDocId()).toBeUndefined();
});

View file

@ -0,0 +1,37 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`EditorRuntime - Real Cases > modifyNodes - batch modify all paragraphs > should modify all 16 paragraphs in a single call 1`] = `
"(雨点敲打着咖啡馆的玻璃窗,像无数细小的手指在弹奏着无声的钢琴。林晓坐在靠窗的位置,手中的咖啡已经凉了,她却浑然不觉。这是她第三次来到这家咖啡馆,每次都是同样的位置,同样的时间。)
(窗外雨丝如帘,街灯昏黄。咖啡馆内灯光柔和,墙上挂着旧照片,书架上摆满了书。空气里是咖啡香和旧书纸的味道。)
林晓:(内心独白)第一次来的时候,也是这样的雨夜。那天我刚结束一段五年的感情,整个人像是被掏空了。我点了一杯美式咖啡,就这样坐着,看着窗外的雨,直到打烊。
林晓:(继续独白)第二次来的时候,我遇到了他。穿着灰色风衣,坐在对面的位置。他一直在看书,偶尔抬头看看窗外。他的手指修长,翻书的动作优雅从容。
林晓:(独白)今天,我又来了。雨还是那样下着,咖啡馆还是那样安静。我不知道自己在期待什么,也许只是习惯了这种孤独的仪式感。
(门上的风铃响了,有人推门进来。林晓下意识地抬头,心跳突然漏了一拍。)
(风铃叮咚作响。一个身影站在门口,雨伞滴着水,灯光勾勒出他的轮廓。)
林晓:(低声)是他。
(他收起雨伞,抖了抖身上的水珠,然后径直走向她。这一次,他没有坐在对面的位置,而是在她面前停了下来。)
陈默:(声音低沉温和)我可以坐这里吗?
(林晓点了点头,喉咙有些发干。窗外的雨声似乎变小了,咖啡馆里的音乐也变得清晰起来。)
陈默:(眼中带着笑意)我注意到你每次都在这里。
林晓:(凝视着他,内心独白)他的眼睛是深褐色的,像秋天的落叶,温暖而深邃。他的鼻梁挺直,唇线分明,微笑时眼角有细微的皱纹,更添了几分沧桑感。我忽然觉得这个人似曾相识,却又分明是第一次见面。
陈默:(伸出手)我叫陈默。很高兴终于能和你说话。我观察你三次了,每次你都这样静静地坐着看雨,若有所思,若有所待。
(林晓握住他的手,感受到他掌心的温度。他的手温暖,却有薄茧,像是经常写字或弹琴的人。雨还在下,但咖啡馆里忽然觉得温暖如春,先前的孤独寂寥,竟然悄然消散了。)
(旁白)也许,有些相遇注定要在雨夜发生。就像有些故事,注定要从一句简单的问候开始。
"
`;

View file

@ -0,0 +1,133 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`EditorRuntime > modifyNodes > error handling > should normalize single operation to array 1`] = `
"# Title
Single op
First paragraph.
Second paragraph.
"
`;
exports[`EditorRuntime > modifyNodes > insert > should insert multiple nodes after same node 1`] = `
"# Title
Insert 1
Insert 2
Insert 3
First paragraph.
Second paragraph.
"
`;
exports[`EditorRuntime > modifyNodes > insert > should insert single node after existing node 1`] = `
"# Title
New inserted paragraph
First paragraph.
Second paragraph.
"
`;
exports[`EditorRuntime > modifyNodes > mixed operations > should handle insert, modify, and remove in single call 1`] = `
"# Updated title
New content
First paragraph.
Second paragraph.
"
`;
exports[`EditorRuntime > modifyNodes > modify > should modify existing node content 1`] = `
"# Title
Modified content here
Second paragraph.
"
`;
exports[`EditorRuntime > modifyNodes > modify > should modify multiple nodes at once 1`] = `
"# Title
Modified first
Modified second
"
`;
exports[`EditorRuntime > modifyNodes > remove > should remove existing node 1`] = `
"<?xml version="1.0" encoding="UTF-8"?>
<root>
<h1 id="ll63">
<span id="lqqe">Title</span>
</h1>
<p id="m7fb">
<span id="mczm">Second paragraph.</span>
</p>
</root>"
`;
exports[`EditorRuntime > modifyNodes > remove > should remove multiple nodes 1`] = `
"# Title
"
`;
exports[`EditorRuntime > replaceText > should replace all occurrences by default 1`] = `
"Hi world. This is a test. Hi again. Testing the world.
"
`;
exports[`EditorRuntime > replaceText > should support regex first occurrence only 1`] = `
"X world. This is a test. Hello again. Testing the world.
"
`;
exports[`EditorRuntime > replaceText > should support regex patterns with optional groups 1`] = `
"Hello world. This is a demo. Hello again. Testing the world.
"
`;
exports[`EditorRuntime > replaceText > should support regex with alternation 1`] = `
"X X. This is a test. X again. Testing the X.
"
`;
exports[`EditorRuntime > replaceText > should support regex with character classes 1`] = `
"Guest and Guest are online.
"
`;
exports[`EditorRuntime > replaceText > should support regex with quantifiers 1`] = `
"Hi world! Hi again!
"
`;
exports[`EditorRuntime > replaceText > should support regex with word boundaries 1`] = `
"Hello universe. This is a test. Hello again. Testing the universe.
"
`;

View file

@ -0,0 +1,364 @@
{
"root": {
"children": [
{
"children": [
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": "雨点敲打咖啡馆之玻璃窗,若无数纤指弹奏无声之琴。林晓坐于窗边,手中咖啡已凉,而浑然不觉。此乃其第三次至此咖啡馆,每回皆同一位置,同一时辰。",
"type": "text",
"version": 1,
"id": "183"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "paragraph",
"version": 1,
"textFormat": 0,
"textStyle": "",
"id": "182"
},
{
"children": [
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": "窗外雨丝如帘,街灯昏黄,映照雨滴如珠帘垂落。咖啡馆内灯光柔和,墙上挂旧时照片,书架列满古籍,空气中咖啡香与旧书纸香交织。",
"type": "text",
"version": 1,
"id": "138"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "paragraph",
"version": 1,
"textFormat": 0,
"textStyle": "",
"id": "137"
},
{
"children": [
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": "忆其初至之时,亦如是雨夜。彼日方了结五载之情,身心若被掏空。馆中奏轻柔爵士之乐,空气中弥漫咖啡豆之香与雨水之湿气。彼点美式咖啡一杯,遂静坐观窗外之雨,直至打烊。",
"type": "text",
"version": 1,
"id": "178"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "paragraph",
"version": 1,
"textFormat": 0,
"textStyle": "",
"id": "177"
},
{
"children": [
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": "二次来时,遇彼男子。身着灰色风衣,身形颀长,坐于对面之位。其面如冠玉,眉目清朗,虽不言不语,自有儒雅之气。二人之间虽隔一桌,却似隔整个宇宙。彼未视林晓,唯专注观手中之书,偶抬头望窗外。林晓见其手指修长如玉,翻书之态优雅从容,颇有君子之风。",
"type": "text",
"version": 1,
"id": "130"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "paragraph",
"version": 1,
"textFormat": 0,
"textStyle": "",
"id": "129"
},
{
"children": [
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": "今日,彼复至。雨仍如是下,咖啡馆依旧寂静。林晓不知己所期待者何,或已习惯此孤独之仪式。",
"type": "text",
"version": 1,
"id": "173"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "paragraph",
"version": 1,
"textFormat": 0,
"textStyle": "",
"id": "172"
},
{
"children": [
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": "门上风铃响,有人推门而入。林晓下意识抬头,心跳忽漏一拍。",
"type": "text",
"version": 1,
"id": "168"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "paragraph",
"version": 1,
"textFormat": 0,
"textStyle": "",
"id": "167"
},
{
"children": [
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": "风铃叮咚,如清泉击石。林晓抬眼望去,见一身影立于门口,雨伞滴水成线,灯光勾勒其轮廓,若水墨画中走出之人。",
"type": "text",
"version": 1,
"id": "135"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "paragraph",
"version": 1,
"textFormat": 0,
"textStyle": "",
"id": "134"
},
{
"children": [
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": "是他。",
"type": "text",
"version": 1,
"id": "24"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "paragraph",
"version": 1,
"textFormat": 0,
"textStyle": "",
"id": "23"
},
{
"children": [
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": "彼收雨伞,抖身上水珠,径走向林晓。此番,未坐对面之位,而于其面前止步。",
"type": "text",
"version": 1,
"id": "163"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "paragraph",
"version": 1,
"textFormat": 0,
"textStyle": "",
"id": "162"
},
{
"children": [
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": "“吾可坐此处乎?”其声低沉温和,如山涧清泉。",
"type": "text",
"version": 1,
"id": "158"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "paragraph",
"version": 1,
"textFormat": 0,
"textStyle": "",
"id": "157"
},
{
"children": [
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": "林晓点头,喉中微干。窗外雨声似渐小,馆中音乐愈显清晰。",
"type": "text",
"version": 1,
"id": "153"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "paragraph",
"version": 1,
"textFormat": 0,
"textStyle": "",
"id": "152"
},
{
"children": [
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": "“吾观汝每回皆在此处,”彼言,眼中含笑,“吾亦然。”",
"type": "text",
"version": 1,
"id": "148"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "paragraph",
"version": 1,
"textFormat": 0,
"textStyle": "",
"id": "147"
},
{
"children": [
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": "林晓不知何言,唯凝视之。彼目深褐色,若秋日落叶,温暖深邃,似藏无尽故事。其鼻梁挺直,唇线分明,微笑时眼角微纹,更添几分沧桑韵味。林晓忽觉此人似曾相识,却又分明初见。",
"type": "text",
"version": 1,
"id": "125"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "paragraph",
"version": 1,
"textFormat": 0,
"textStyle": "",
"id": "124"
},
{
"children": [
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": "“吾名陈默,”彼伸手,指节分明,掌心温暖,“甚悦终能与汝言。观汝三回,每回皆静坐观雨,若有所思,若有所待。”",
"type": "text",
"version": 1,
"id": "120"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "paragraph",
"version": 1,
"textFormat": 0,
"textStyle": "",
"id": "119"
},
{
"children": [
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": "林晓握其手,感掌心之温,心中忽生异样。彼手虽温,却有薄茧,似是常执笔或抚琴之人。雨仍下,然咖啡馆中忽觉温暖如春,先前之孤独寂寥,竟悄然消散。",
"type": "text",
"version": 1,
"id": "115"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "paragraph",
"version": 1,
"textFormat": 0,
"textStyle": "",
"id": "114"
},
{
"children": [
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": "或曰,有些相遇注定于雨夜发生。如有些故事,注定始于一句简单问候。",
"type": "text",
"version": 1,
"id": "143"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "paragraph",
"version": 1,
"textFormat": 0,
"textStyle": "",
"id": "142"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "root",
"version": 1,
"id": "root"
}
}

View file

@ -0,0 +1,591 @@
{
"root": {
"children": [
{
"children": [
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": "杭州一座被诗意浸润的千年古都静静地依偎在钱塘江畔宛如一幅徐徐展开的水墨长卷。这里不仅是浙江省的政治、经济、文化中心更是中国七大古都之一承载着2200余年的历史记忆。从南宋临安的繁华盛景到今日数字经济的创新高地杭州始终以\"人间天堂\"的美誉,向世界展示着东方文明的独特魅力。",
"type": "text",
"version": 1,
"id": "4"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "paragraph",
"version": 1,
"textFormat": 0,
"textStyle": "",
"id": "3"
},
{
"children": [
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": "地理与气候",
"type": "text",
"version": 1,
"id": "6"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "heading",
"version": 1,
"tag": "h2",
"id": "5"
},
{
"children": [
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": "杭州地处钱塘江下游京杭大运河南端东临杭州湾西接天目山。全市总面积16850平方公里常住人口超过1200万。杭州属于亚热带季风气候四季分明雨量充沛年平均气温17.8℃,气候宜人。",
"type": "text",
"version": 1,
"id": "8"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "paragraph",
"version": 1,
"textFormat": 0,
"textStyle": "",
"id": "7"
},
{
"children": [
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": "历史文化",
"type": "text",
"version": 1,
"id": "10"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "heading",
"version": 1,
"tag": "h2",
"id": "9"
},
{
"children": [
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": "杭州是吴越文化和南宋文化的发源地之一。公元1138年南宋定都临安今杭州使其成为当时世界上最繁华的城市之一。杭州拥有丰富的历史文化遗产包括西湖文化景观、京杭大运河、良渚古城遗址等世界文化遗产。",
"type": "text",
"version": 1,
"id": "12"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "paragraph",
"version": 1,
"textFormat": 0,
"textStyle": "",
"id": "11"
},
{
"children": [
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": "杭州的历史可以追溯到新石器时代的良渚文化约公元前3300-前2300年良渚古城遗址的发现证明了这里是中国早期文明的重要发源地之一。良渚文化以精美的玉器、发达的水利系统和复杂的社会结构著称2019年被列入世界文化遗产名录。",
"type": "text",
"version": 1,
"id": "74"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "paragraph",
"version": 1,
"textFormat": 0,
"textStyle": "",
"id": "73"
},
{
"children": [
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": "春秋战国时期杭州属于吴越之地。公元589年隋文帝设杭州取\"余杭\"之名,寓意\"禹航\",相传大禹治水时曾在此停航。隋炀帝开凿京杭大运河后,杭州成为南北交通枢纽,经济文化迅速发展。",
"type": "text",
"version": 1,
"id": "71"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "paragraph",
"version": 1,
"textFormat": 0,
"textStyle": "",
"id": "70"
},
{
"children": [
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": "五代十国时期吴越国907-978年定都杭州钱镠王实施保境安民政策兴修水利扩建城池奠定了杭州\"东南形胜,三吴都会\"的基础。吴越国时期佛教兴盛,灵隐寺、净慈寺等名刹相继建成。",
"type": "text",
"version": 1,
"id": "68"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "paragraph",
"version": 1,
"textFormat": 0,
"textStyle": "",
"id": "67"
},
{
"children": [
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": "南宋时期1127-1279年是杭州历史上的黄金时代。宋室南渡后定都临安今杭州使其成为当时世界上人口最多、经济最繁荣的城市之一。马可·波罗在游记中称杭州为\"世界上最美丽华贵之天城\"。南宋时期杭州的工商业、文化艺术、科学技术都达到了空前的高度:",
"type": "text",
"version": 1,
"id": "65"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "paragraph",
"version": 1,
"textFormat": 0,
"textStyle": "",
"id": "64"
},
{
"children": [
{
"children": [
{
"detail": 0,
"format": 1,
"mode": "normal",
"style": "",
"text": "经济繁荣",
"type": "text",
"version": 1,
"id": "52"
},
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": ":丝绸、瓷器、茶叶贸易发达,出现了世界上最早的纸币\"交子\"",
"type": "text",
"version": 1,
"id": "53"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "listitem",
"version": 1,
"value": 1,
"id": "51"
},
{
"children": [
{
"detail": 0,
"format": 1,
"mode": "normal",
"style": "",
"text": "文化鼎盛",
"type": "text",
"version": 1,
"id": "55"
},
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": ":宋词达到艺术高峰,苏轼、柳永、李清照等文人雅士云集",
"type": "text",
"version": 1,
"id": "56"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "listitem",
"version": 1,
"value": 2,
"id": "54"
},
{
"children": [
{
"detail": 0,
"format": 1,
"mode": "normal",
"style": "",
"text": "科技创新",
"type": "text",
"version": 1,
"id": "58"
},
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": ":活字印刷术、指南针、火药等重大发明得到广泛应用",
"type": "text",
"version": 1,
"id": "59"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "listitem",
"version": 1,
"value": 3,
"id": "57"
},
{
"children": [
{
"detail": 0,
"format": 1,
"mode": "normal",
"style": "",
"text": "城市建设",
"type": "text",
"version": 1,
"id": "61"
},
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": ":形成了\"前朝后市\"的格局,御街、清河坊等商业区繁华异常",
"type": "text",
"version": 1,
"id": "62"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "listitem",
"version": 1,
"value": 4,
"id": "60"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "list",
"version": 1,
"listType": "bullet",
"start": 1,
"tag": "ul",
"id": "50"
},
{
"children": [
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": "元明清时期,杭州虽不再是都城,但仍是江南重要的经济文化中心。明代杭州的丝绸业更加发达,\"杭纺\"名扬天下;清代康乾盛世期间,康熙、乾隆皇帝多次南巡驻跸杭州,题诗作画,进一步提升了西湖的文化地位。",
"type": "text",
"version": 1,
"id": "48"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "paragraph",
"version": 1,
"textFormat": 0,
"textStyle": "",
"id": "47"
},
{
"children": [
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": "近代以来杭州在中国现代化进程中扮演着重要角色。1861年太平天国战争后杭州开始近代化建设民国时期成为浙江省会改革开放后杭州依托西湖美景和历史文化底蕴发展成为国际知名的旅游城市和创新创业中心。",
"type": "text",
"version": 1,
"id": "45"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "paragraph",
"version": 1,
"textFormat": 0,
"textStyle": "",
"id": "44"
},
{
"children": [
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": "杭州的文化遗产不仅体现在物质层面,更融入了城市的精神气质:",
"type": "text",
"version": 1,
"id": "42"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "paragraph",
"version": 1,
"textFormat": 0,
"textStyle": "",
"id": "41"
},
{
"children": [
{
"children": [
{
"detail": 0,
"format": 1,
"mode": "normal",
"style": "",
"text": "茶文化",
"type": "text",
"version": 1,
"id": "29"
},
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": ":龙井茶被誉为\"中国十大名茶\"之首,茶道、茶艺成为杭州生活美学的重要组成部分",
"type": "text",
"version": 1,
"id": "30"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "listitem",
"version": 1,
"value": 1,
"id": "28"
},
{
"children": [
{
"detail": 0,
"format": 1,
"mode": "normal",
"style": "",
"text": "丝绸文化",
"type": "text",
"version": 1,
"id": "32"
},
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": ":杭州素有\"丝绸之府\"美誉,丝绸制作技艺被列入国家级非物质文化遗产",
"type": "text",
"version": 1,
"id": "33"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "listitem",
"version": 1,
"value": 2,
"id": "31"
},
{
"children": [
{
"detail": 0,
"format": 1,
"mode": "normal",
"style": "",
"text": "诗词文化",
"type": "text",
"version": 1,
"id": "35"
},
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": ":白居易、苏轼、林逋等文人留下的诗词歌赋,使杭州成为\"诗画江南\"的典范",
"type": "text",
"version": 1,
"id": "36"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "listitem",
"version": 1,
"value": 3,
"id": "34"
},
{
"children": [
{
"detail": 0,
"format": 1,
"mode": "normal",
"style": "",
"text": "佛教文化",
"type": "text",
"version": 1,
"id": "38"
},
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": ":灵隐寺、净慈寺、天竺三寺等佛教圣地,见证了杭州千年佛教传承",
"type": "text",
"version": 1,
"id": "39"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "listitem",
"version": 1,
"value": 4,
"id": "37"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "list",
"version": 1,
"listType": "bullet",
"start": 1,
"tag": "ul",
"id": "27"
},
{
"children": [
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": "西湖风光",
"type": "text",
"version": 1,
"id": "14"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "heading",
"version": 1,
"tag": "h2",
"id": "13"
},
{
"children": [
{
"detail": 0,
"format": 0,
"mode": "normal",
"style": "",
"text": "西湖是杭州的灵魂,也是中国最著名的风景名胜之一。西湖十景(如苏堤春晓、断桥残雪、雷峰夕照等)闻名遐迩。西湖不仅自然风光秀丽,更承载着深厚的文化内涵,历代文人墨客在此留下了无数诗词歌赋。",
"type": "text",
"version": 1,
"id": "16"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "paragraph",
"version": 1,
"textFormat": 0,
"textStyle": "",
"id": "15"
}
],
"direction": "ltr",
"format": "",
"indent": 0,
"type": "root",
"version": 1,
"id": "root"
}
}

View file

@ -0,0 +1,2 @@
export { EditorRuntime } from './EditorRuntime';
export * from './types';

View file

@ -0,0 +1,97 @@
// ============ Initialize Args ============
export interface InitDocumentArgs {
markdown: string;
}
// ============ Document Metadata Args ============
export interface EditTitleArgs {
title: string;
}
// ============ Query & Search Args ============
export interface GetPageContentArgs {
format?: 'xml' | 'markdown' | 'both';
}
// ============ Unified Modify Nodes Args ============
/** Insert operation: insert a node before or after a reference node */
export type ModifyInsertOperation =
| {
action: 'insert';
afterId: string;
litexml: string;
}
| {
action: 'insert';
beforeId: string;
litexml: string;
};
/** Remove operation: remove a node by ID */
export interface ModifyRemoveOperation {
action: 'remove';
id: string;
}
/** Modify operation: update existing nodes by their IDs (embedded in litexml) */
export interface ModifyUpdateOperation {
action: 'modify';
litexml: string | string[];
}
/** Union type for all modify operations */
export type ModifyOperation = ModifyInsertOperation | ModifyRemoveOperation | ModifyUpdateOperation;
/** Args for the unified modifyNodes API */
export interface ModifyNodesArgs {
operations: ModifyOperation[];
}
// ============ Text Operations Args ============
export interface ReplaceTextArgs {
newText: string;
nodeIds?: string[];
replaceAll?: boolean;
searchText: string;
useRegex?: boolean;
}
// ============ Runtime Result Types ============
/** Result of a single modify operation */
export interface ModifyOperationResult {
action: 'insert' | 'remove' | 'modify';
error?: string;
success: boolean;
}
export interface InitPageRuntimeResult {
extractedTitle?: string;
nodeCount: number;
}
export interface EditTitleRuntimeResult {
newTitle: string;
previousTitle: string;
}
export interface GetPageContentRuntimeResult {
charCount?: number;
documentId: string;
lineCount?: number;
markdown?: string;
title: string;
xml?: string;
}
export interface ModifyNodesRuntimeResult {
results: ModifyOperationResult[];
successCount: number;
totalCount: number;
}
export interface ReplaceTextRuntimeResult {
modifiedNodeIds: string[];
replacementCount: number;
}

View file

@ -0,0 +1,18 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'lcov', 'text-summary'],
},
environment: 'happy-dom',
globals: true,
server: {
deps: {
// Inline @emoji-mart packages to avoid ESM JSON import issues
inline: [/@emoji-mart/, /@lobehub\/ui/],
},
},
},
});

View file

@ -4,11 +4,11 @@
* Creates and exports the PageAgentExecutor instance for registration.
* Also exports the runtime for editor instance injection.
*/
import { PageAgentExecutionRuntime } from '@lobechat/builtin-tool-page-agent/executionRuntime';
import { EditorRuntime } from '@lobechat/editor-runtime';
import { PageAgentExecutor } from '@lobechat/builtin-tool-page-agent/executor';
// Create singleton instance of the runtime
export const pageAgentRuntime = new PageAgentExecutionRuntime();
export const pageAgentRuntime = new EditorRuntime();
// Create executor instance with the runtime
export const pageAgentExecutor = new PageAgentExecutor(pageAgentRuntime);