nolo-cli 0.1.21 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-runtime/agentRecordConfig.ts +4 -0
- package/agent-runtime/hostAdapter.ts +2 -0
- package/agent-runtime/index.ts +7 -0
- package/agent-runtime/localLoop.ts +2 -0
- package/agent-runtime/platformChatProvider.ts +3 -0
- package/agent-runtime/runtimeToolPolicy.ts +92 -0
- package/agent-runtime/types.ts +42 -0
- package/agentRunCommand.ts +74 -1
- package/agentRuntimeCommands.ts +17 -89
- package/ai/agent/streamAgentChatTurn.ts +104 -20
- package/ai/chat/fetchUtils.native.ts +2 -0
- package/ai/chat/fetchUtils.ts +2 -0
- package/ai/chat/sendOpenAICompletionsRequest.ts +56 -0
- package/ai/chat/sendOpenAIResponseRequest.ts +64 -0
- package/ai/llm/kimi.ts +1 -1
- package/ai/llm/providers.ts +3 -0
- package/ai/llm/reasoningModels.ts +1 -0
- package/ai/skills/skillDocProtocol.ts +95 -3
- package/ai/taskRun/taskRunProtocol.ts +1 -0
- package/ai/tools/agent/agentTools.ts +17 -0
- package/ai/tools/agent/startAgentDialogTool.ts +53 -0
- package/ai/tools/modelUsageTools.ts +5 -0
- package/client/agentRun.test.ts +257 -7
- package/client/agentRun.ts +133 -34
- package/client/localRuntimeAdapter.test.ts +2 -0
- package/client/localRuntimeAdapter.ts +15 -2
- package/database/actions/common.ts +4 -3
- package/database/config.ts +19 -0
- package/machineCommands.ts +400 -45
- package/package.json +4 -2
- package/render/canvas/canvasEditContext.ts +127 -0
- package/render/canvas/canvasRuntime.ts +57 -0
- package/render/canvas/canvasSnapshotParser.ts +76 -0
- package/render/canvas/canvasTree.ts +308 -0
- package/render/canvas/types.ts +46 -0
- package/render/layout/deleteBehavior.ts +52 -0
- package/render/layout/mainLayoutSidebar.ts +17 -0
- package/render/layout/mainLayoutViewMode.ts +56 -0
- package/render/layout/topbarUtils.ts +87 -0
- package/render/layout/useDevReloadPending.ts +30 -0
- package/render/page/createPageAction.ts +183 -0
- package/render/page/docSlice.ts +468 -0
- package/render/page/server/createPage.ts +174 -0
- package/render/page/server/handleCreatePage.ts +91 -0
- package/render/page/server/index.ts +4 -0
- package/render/page/types.ts +17 -0
- package/render/page/useKeyboardSave.ts +48 -0
- package/render/styles/zIndex.ts +12 -0
- package/render/surf/WeatherIconStyles.ts +17 -0
- package/render/surf/color.ts +9 -0
- package/render/surf/config.ts +46 -0
- package/render/surf/screens/style.ts +1 -0
- package/render/surf/styles/ToggleButtonStyles.ts +8 -0
- package/render/surf/utils/groupedWeatherData.ts +32 -0
- package/render/surf/weatherUtils.ts +50 -0
- package/render/table/activityColumns.ts +6 -0
- package/render/table/createTableAction.ts +270 -0
- package/render/table/deleteTableAction.ts +129 -0
- package/render/table/fetchAndCacheTableRows.ts +174 -0
- package/render/table/tableSlice.ts +1106 -0
- package/render/table/tableView.ts +289 -0
- package/render/table/toolValueUtils.ts +363 -0
- package/render/table/types.ts +252 -0
- package/render/table/useCreateTable.ts +72 -0
- package/render/table/useTable.ts +61 -0
- package/render/table/utils/tableSerialization.ts +50 -0
- package/render/web/elements/artifactPreviewCode.ts +43 -0
- package/render/web/elements/artifactRuntimePreload.ts +52 -0
- package/render/web/elements/codeBlockAutoPreview.ts +10 -0
- package/render/web/elements/mermaidPreview.ts +21 -0
- package/render/web/ui/useInlineEdit.ts +135 -0
- package/tableCommands.ts +42 -5
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// packages/render/layout/useDevReloadPending.ts
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 订阅 window.__DEV_RELOAD_PENDING__
|
|
6
|
+
* - 有新构建待刷新时返回 true
|
|
7
|
+
* - 自举保护期(suppress count > 0)内返回 false(静默等待)
|
|
8
|
+
*/
|
|
9
|
+
export const useDevReloadPending = (): boolean => {
|
|
10
|
+
const [pending, setPending] = useState(false);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (typeof window === "undefined") return;
|
|
14
|
+
|
|
15
|
+
const check = () => {
|
|
16
|
+
const w = window as any;
|
|
17
|
+
const isPending = w.__DEV_RELOAD_PENDING__ === true;
|
|
18
|
+
const suppressed =
|
|
19
|
+
typeof w.__DEV_RELOAD_SUPPRESS_COUNT__ === "number" &&
|
|
20
|
+
w.__DEV_RELOAD_SUPPRESS_COUNT__ > 0;
|
|
21
|
+
setPending(isPending && !suppressed);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
check();
|
|
25
|
+
const id = setInterval(check, 500);
|
|
26
|
+
return () => clearInterval(id);
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
return pending;
|
|
30
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// 文件路径: render/page/createPageAction.ts
|
|
2
|
+
import { selectUserId } from "auth/authSlice";
|
|
3
|
+
import {
|
|
4
|
+
addContentToSpace,
|
|
5
|
+
selectCurrentSpaceId,
|
|
6
|
+
} from "create/space/spaceSlice";
|
|
7
|
+
import { createPageKey } from "database/keys";
|
|
8
|
+
import i18n from "app/i18n";
|
|
9
|
+
import { DataType } from "create/types";
|
|
10
|
+
import type { RootState, AppDispatch } from "app/store";
|
|
11
|
+
import { write } from "database/dbSlice";
|
|
12
|
+
import type { PageData } from "./types";
|
|
13
|
+
import { format } from "date-fns";
|
|
14
|
+
|
|
15
|
+
// 新增:把 markdown 转为 Slate 的工具 & 类型
|
|
16
|
+
import { markdownToSlate } from "create/editor/transforms/markdownToSlate";
|
|
17
|
+
import {
|
|
18
|
+
createEmptyParagraph,
|
|
19
|
+
splitSlateTitleAndBody,
|
|
20
|
+
type EditorContent,
|
|
21
|
+
} from "create/editor/utils/slateUtils";
|
|
22
|
+
import { slateToRenderMarkdown } from "create/editor/transforms/slateToRenderMarkdown";
|
|
23
|
+
import { parseSkillDocProtocol } from "ai/skills/skillDocProtocol";
|
|
24
|
+
import { buildSkillSummaryMarker } from "ai/skills/skillSummaryMarker";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 归一化 / 过滤 categoryId
|
|
28
|
+
*
|
|
29
|
+
* 设计目标:
|
|
30
|
+
* - 允许正常的、看起来像 ID 的字符串通过;
|
|
31
|
+
* - 对 AI 乱编的 “读书笔记分类” / “分类一” / 超短数字 等,直接当成 undefined。
|
|
32
|
+
*
|
|
33
|
+
* 注意:这里的规则可以按你实际的 ID 生成规则调整。
|
|
34
|
+
*/
|
|
35
|
+
const normalizeCategoryId = (raw?: string): string | undefined => {
|
|
36
|
+
const trimmed = raw?.trim();
|
|
37
|
+
if (!trimmed) return undefined;
|
|
38
|
+
|
|
39
|
+
// 1) 过滤掉包含空格或非 ASCII 字符的情况
|
|
40
|
+
// 比如 “读书笔记分类” 会被直接判为无效
|
|
41
|
+
const asciiNoSpace = /^[\x20-\x7E]+$/; // 可见 ASCII 字符
|
|
42
|
+
if (!asciiNoSpace.test(trimmed)) return undefined;
|
|
43
|
+
|
|
44
|
+
// 2) 简单做个长度下限,避免 "1"、"abc" 这种明显不是 ID 的值
|
|
45
|
+
// 如果你的真实 ID 很短,可以把 8 调小一点
|
|
46
|
+
if (trimmed.length < 8) return undefined;
|
|
47
|
+
|
|
48
|
+
// 3) 如果你有更具体规则(如 UUID / nanoid),可以在这里再加一层匹配
|
|
49
|
+
// const idLike = /^[0-9a-zA-Z_-]{10,64}$/;
|
|
50
|
+
// if (!idLike.test(trimmed)) return undefined;
|
|
51
|
+
|
|
52
|
+
return trimmed;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const createPageAction = async (
|
|
56
|
+
{
|
|
57
|
+
categoryId,
|
|
58
|
+
spaceId: customSpaceId,
|
|
59
|
+
title: initialTitle,
|
|
60
|
+
addMomentTag,
|
|
61
|
+
content,
|
|
62
|
+
slateData,
|
|
63
|
+
}: {
|
|
64
|
+
categoryId?: string;
|
|
65
|
+
spaceId?: string;
|
|
66
|
+
title?: string;
|
|
67
|
+
addMomentTag?: boolean;
|
|
68
|
+
content?: string; // 这里仍然接收 markdown 文本
|
|
69
|
+
slateData?: any; // 如果外部已经算好 Slate,可以直接传进来
|
|
70
|
+
} = {},
|
|
71
|
+
{ dispatch, getState }: { dispatch: AppDispatch; getState: () => RootState }
|
|
72
|
+
): Promise<string> => {
|
|
73
|
+
const state = getState();
|
|
74
|
+
const userId = selectUserId(state);
|
|
75
|
+
if (!userId) throw new Error("User ID not found.");
|
|
76
|
+
|
|
77
|
+
// Important:
|
|
78
|
+
// View-mode-based "create into current space vs no space" is decided by the UI entry
|
|
79
|
+
// that triggers creation (currently the sidebar-top create button).
|
|
80
|
+
// This action must preserve explicit caller intent:
|
|
81
|
+
// - if `spaceId` is provided, use it;
|
|
82
|
+
// - otherwise only fall back to the current selected space.
|
|
83
|
+
const spaceId = customSpaceId ?? selectCurrentSpaceId(state);
|
|
84
|
+
const { dbKey, id } = createPageKey.create(userId);
|
|
85
|
+
|
|
86
|
+
const now = new Date();
|
|
87
|
+
const dateStr = format(now, "yyyy-MM-dd HH:mm");
|
|
88
|
+
const defaultTitle = i18n.t("page:defaultTitleFormat", {
|
|
89
|
+
defaultValue: "{{date}} 的笔记",
|
|
90
|
+
date: dateStr,
|
|
91
|
+
});
|
|
92
|
+
let title = initialTitle?.trim() || defaultTitle;
|
|
93
|
+
|
|
94
|
+
const tags = addMomentTag ? ["moment"] : undefined;
|
|
95
|
+
let pageMeta: PageData["meta"] | undefined;
|
|
96
|
+
|
|
97
|
+
// ====== 根据 slateData / content 决定真正写入的 Slate 结构 ======
|
|
98
|
+
let initialSlateData: EditorContent;
|
|
99
|
+
|
|
100
|
+
if (slateData) {
|
|
101
|
+
// 1. 如果外部已经传了 Slate,直接使用(兼容旧调用)
|
|
102
|
+
initialSlateData = slateData as EditorContent;
|
|
103
|
+
} else if (content) {
|
|
104
|
+
const parsedProtocol = parseSkillDocProtocol(content);
|
|
105
|
+
const normalizedContent = parsedProtocol.content;
|
|
106
|
+
pageMeta = parsedProtocol.meta;
|
|
107
|
+
console.log("content", content);
|
|
108
|
+
// 2. 如果有 markdown content,优先尝试用 markdownToSlate 转成结构化 Slate
|
|
109
|
+
try {
|
|
110
|
+
const parsed = markdownToSlate(normalizedContent);
|
|
111
|
+
console.log("parsed", parsed);
|
|
112
|
+
|
|
113
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
114
|
+
const split = splitSlateTitleAndBody(parsed, initialTitle);
|
|
115
|
+
title = initialTitle?.trim() || split.title || defaultTitle;
|
|
116
|
+
initialSlateData = split.body;
|
|
117
|
+
} else {
|
|
118
|
+
// 解析结果为空时,退回到简单的纯文本段落
|
|
119
|
+
initialSlateData = [
|
|
120
|
+
{ type: "paragraph", children: [{ text: normalizedContent }] },
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
} catch (e) {
|
|
124
|
+
// 解析失败兜底:仍然保留原始文本,但当成普通段落
|
|
125
|
+
console.error(
|
|
126
|
+
"[createPageAction] markdownToSlate failed, fallback to plain text:",
|
|
127
|
+
e
|
|
128
|
+
);
|
|
129
|
+
initialSlateData = [
|
|
130
|
+
{ type: "paragraph", children: [{ text: normalizedContent }] },
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
// 3. 完全没有内容,创建一个空白页面
|
|
135
|
+
initialSlateData = [createEmptyParagraph()];
|
|
136
|
+
}
|
|
137
|
+
// ====== 关键改动结束 ======
|
|
138
|
+
|
|
139
|
+
// 关键防御:对传入的 categoryId 做归一化 / 过滤
|
|
140
|
+
const safeCategoryId = normalizeCategoryId(categoryId);
|
|
141
|
+
|
|
142
|
+
const pageData: PageData = {
|
|
143
|
+
dbKey,
|
|
144
|
+
id,
|
|
145
|
+
type: DataType.DOC,
|
|
146
|
+
title,
|
|
147
|
+
spaceId,
|
|
148
|
+
slateData: initialSlateData,
|
|
149
|
+
// `content` 只作为只读展示缓存 / legacy bridge,真源仍是 `slateData`。
|
|
150
|
+
content:
|
|
151
|
+
typeof content === "string"
|
|
152
|
+
? parseSkillDocProtocol(content, pageMeta).content ||
|
|
153
|
+
slateToRenderMarkdown(initialSlateData)
|
|
154
|
+
: slateToRenderMarkdown(initialSlateData),
|
|
155
|
+
tags,
|
|
156
|
+
created: now.toISOString(),
|
|
157
|
+
...(pageMeta ? { meta: pageMeta } : {}),
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const skillSummary = buildSkillSummaryMarker(pageMeta);
|
|
161
|
+
|
|
162
|
+
await dispatch(write({ data: pageData, customKey: dbKey })).unwrap();
|
|
163
|
+
|
|
164
|
+
if (spaceId) {
|
|
165
|
+
(dispatch as any)(
|
|
166
|
+
(addContentToSpace as any)({
|
|
167
|
+
contentKey: dbKey,
|
|
168
|
+
type: DataType.DOC,
|
|
169
|
+
spaceId,
|
|
170
|
+
title,
|
|
171
|
+
// 只使用经过 normalize 的 safeCategoryId
|
|
172
|
+
categoryId: safeCategoryId,
|
|
173
|
+
...(skillSummary ? { skillSummary } : {}),
|
|
174
|
+
})
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (typeof window !== "undefined") {
|
|
179
|
+
window.dispatchEvent(new Event("nolo-user-data-updated"));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return dbKey;
|
|
183
|
+
};
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
// 文件: render/page/docSlice.ts
|
|
2
|
+
|
|
3
|
+
import { formatISO } from "date-fns";
|
|
4
|
+
import {
|
|
5
|
+
asyncThunkCreator,
|
|
6
|
+
buildCreateSlice,
|
|
7
|
+
createListenerMiddleware,
|
|
8
|
+
PayloadAction,
|
|
9
|
+
createSelector,
|
|
10
|
+
} from "@reduxjs/toolkit";
|
|
11
|
+
import { readAndWait, patch } from "database/dbSlice";
|
|
12
|
+
import { updateContentTitle } from "create/space/spaceSlice";
|
|
13
|
+
import type { EditorContent } from "create/editor/utils/slateUtils";
|
|
14
|
+
import { DataType } from "create/types";
|
|
15
|
+
import { PageData } from "./types";
|
|
16
|
+
import type { PageSkillMetadata } from "ai/skills/skillDocProtocol";
|
|
17
|
+
|
|
18
|
+
// —— State 接口 ——
|
|
19
|
+
export interface DocState {
|
|
20
|
+
content: string | null;
|
|
21
|
+
slateData: EditorContent | null;
|
|
22
|
+
title: string | null;
|
|
23
|
+
dbSpaceId: string | null;
|
|
24
|
+
tags: string[] | null;
|
|
25
|
+
isReadOnly: boolean;
|
|
26
|
+
isLoading: boolean;
|
|
27
|
+
isInitialized: boolean;
|
|
28
|
+
error: string | null;
|
|
29
|
+
pageKey: string | null;
|
|
30
|
+
isSaving: boolean;
|
|
31
|
+
saveError: string | null;
|
|
32
|
+
lastSavedAt: string | null;
|
|
33
|
+
lastSavedSlateData: EditorContent | null;
|
|
34
|
+
lastSavedTitle: string | null;
|
|
35
|
+
justSaved: boolean;
|
|
36
|
+
tools: string[] | null;
|
|
37
|
+
meta: PageSkillMetadata | null;
|
|
38
|
+
id: string | null;
|
|
39
|
+
type: DataType | null;
|
|
40
|
+
focusContext: {
|
|
41
|
+
isFocused: boolean;
|
|
42
|
+
isCollapsed: boolean;
|
|
43
|
+
anchorPath: number[];
|
|
44
|
+
anchorOffset: number;
|
|
45
|
+
focusPath: number[];
|
|
46
|
+
focusOffset: number;
|
|
47
|
+
selectedText: string | null;
|
|
48
|
+
blockType: string | null;
|
|
49
|
+
} | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// —— 初始状态 ——
|
|
53
|
+
const initialState: DocState = {
|
|
54
|
+
content: null,
|
|
55
|
+
slateData: null,
|
|
56
|
+
title: null,
|
|
57
|
+
dbSpaceId: null,
|
|
58
|
+
tags: null,
|
|
59
|
+
isReadOnly: true,
|
|
60
|
+
isLoading: false,
|
|
61
|
+
isInitialized: false,
|
|
62
|
+
error: null,
|
|
63
|
+
pageKey: null,
|
|
64
|
+
isSaving: false,
|
|
65
|
+
saveError: null,
|
|
66
|
+
lastSavedAt: null,
|
|
67
|
+
lastSavedSlateData: null,
|
|
68
|
+
lastSavedTitle: null,
|
|
69
|
+
justSaved: false,
|
|
70
|
+
tools: null,
|
|
71
|
+
meta: null,
|
|
72
|
+
id: null,
|
|
73
|
+
type: null,
|
|
74
|
+
focusContext: null,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const deepEqualEditorContent = (a: any, b: any): boolean => {
|
|
78
|
+
if (a === b) return true;
|
|
79
|
+
if (a == null || b == null) return false;
|
|
80
|
+
if (typeof a !== "object" || typeof b !== "object") return false;
|
|
81
|
+
|
|
82
|
+
const isArrayA = Array.isArray(a);
|
|
83
|
+
const isArrayB = Array.isArray(b);
|
|
84
|
+
if (isArrayA || isArrayB) {
|
|
85
|
+
if (!isArrayA || !isArrayB || a.length !== b.length) return false;
|
|
86
|
+
return a.every((item, index) => deepEqualEditorContent(item, b[index]));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const keysA = Object.keys(a);
|
|
90
|
+
const keysB = Object.keys(b);
|
|
91
|
+
if (keysA.length !== keysB.length) return false;
|
|
92
|
+
|
|
93
|
+
return keysA.every((key) =>
|
|
94
|
+
Object.prototype.hasOwnProperty.call(b, key) && deepEqualEditorContent(a[key], b[key])
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const hasSlateContentChanged = (
|
|
99
|
+
newContent: EditorContent | null,
|
|
100
|
+
oldContent: EditorContent | null
|
|
101
|
+
) => {
|
|
102
|
+
if (newContent === oldContent) return false;
|
|
103
|
+
if (!newContent || !oldContent) return true;
|
|
104
|
+
if (newContent.length !== oldContent.length) return true;
|
|
105
|
+
return !deepEqualEditorContent(newContent, oldContent);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
interface InitDocArgs {
|
|
109
|
+
pageKey: string;
|
|
110
|
+
isReadOnly: boolean;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface InitDocPayload extends PageData {
|
|
114
|
+
isReadOnly: boolean;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
interface SaveDocArgs {
|
|
118
|
+
pageKey: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const createSliceWithThunks = buildCreateSlice({
|
|
122
|
+
creators: { asyncThunk: asyncThunkCreator },
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
export const docSlice = createSliceWithThunks({
|
|
126
|
+
name: "doc",
|
|
127
|
+
initialState,
|
|
128
|
+
reducers: (create): any => ({
|
|
129
|
+
createDoc: create.asyncThunk(async (args: any, thunkApi: any) => {
|
|
130
|
+
const { createPageAction } = await import("./createPageAction");
|
|
131
|
+
return createPageAction(args, thunkApi);
|
|
132
|
+
}),
|
|
133
|
+
|
|
134
|
+
initDoc: create.asyncThunk(
|
|
135
|
+
async (args: InitDocArgs, { dispatch, rejectWithValue }) => {
|
|
136
|
+
const { pageKey, isReadOnly } = args;
|
|
137
|
+
try {
|
|
138
|
+
const readAction = await (dispatch as any)(readAndWait(pageKey));
|
|
139
|
+
|
|
140
|
+
if (readAndWait.fulfilled.match(readAction) && readAction.payload) {
|
|
141
|
+
const data = readAction.payload as PageData;
|
|
142
|
+
|
|
143
|
+
// docSlice 仅处理 DataType.DOC
|
|
144
|
+
if (data.type !== DataType.DOC) {
|
|
145
|
+
return rejectWithValue(`加载的内容 ${pageKey} 不是文档类型 (${data.type})`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { ...data, isReadOnly };
|
|
149
|
+
} else {
|
|
150
|
+
const msg =
|
|
151
|
+
(readAction.payload as any)?.message || `无法加载文档 ${pageKey}`;
|
|
152
|
+
return rejectWithValue(msg);
|
|
153
|
+
}
|
|
154
|
+
} catch (e: any) {
|
|
155
|
+
return rejectWithValue(e.message || `初始化文档 ${pageKey} 时出错`);
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
pending: (state, action) => {
|
|
160
|
+
Object.assign(state, initialState);
|
|
161
|
+
state.isLoading = true;
|
|
162
|
+
state.pageKey = action.meta.arg.pageKey;
|
|
163
|
+
state.isReadOnly = action.meta.arg.isReadOnly;
|
|
164
|
+
},
|
|
165
|
+
fulfilled: (state, action: PayloadAction<InitDocPayload>) => {
|
|
166
|
+
state.isLoading = false;
|
|
167
|
+
state.isInitialized = true;
|
|
168
|
+
state.error = null;
|
|
169
|
+
state.content = action.payload.content || null;
|
|
170
|
+
state.slateData = action.payload.slateData || null;
|
|
171
|
+
state.lastSavedSlateData = action.payload.slateData || null;
|
|
172
|
+
state.title = action.payload.title || null;
|
|
173
|
+
state.lastSavedTitle = action.payload.title || null;
|
|
174
|
+
state.dbSpaceId = action.payload.spaceId || null;
|
|
175
|
+
state.tags = action.payload.tags || null;
|
|
176
|
+
state.isReadOnly = action.payload.isReadOnly;
|
|
177
|
+
state.pageKey = action.payload.dbKey;
|
|
178
|
+
state.id = action.payload.id;
|
|
179
|
+
state.type = action.payload.type;
|
|
180
|
+
const payload = action.payload as any;
|
|
181
|
+
state.lastSavedAt = payload.updatedAt || payload.updated_at || null;
|
|
182
|
+
state.tools = action.payload.tools || null;
|
|
183
|
+
state.meta = action.payload.meta || null;
|
|
184
|
+
},
|
|
185
|
+
rejected: (state, action) => {
|
|
186
|
+
state.isLoading = false;
|
|
187
|
+
state.isInitialized = true;
|
|
188
|
+
state.error =
|
|
189
|
+
(action.payload as string) ||
|
|
190
|
+
action.error.message ||
|
|
191
|
+
"初始化文档时发生未知错误";
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
),
|
|
195
|
+
|
|
196
|
+
updateSlate: create.reducer(
|
|
197
|
+
(state, action: PayloadAction<EditorContent>) => {
|
|
198
|
+
if (state.isInitialized && !state.isReadOnly) {
|
|
199
|
+
state.slateData = action.payload;
|
|
200
|
+
state.justSaved = false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
),
|
|
204
|
+
|
|
205
|
+
updateTitle: create.reducer((state, action: PayloadAction<string>) => {
|
|
206
|
+
if (state.isInitialized && !state.isReadOnly) {
|
|
207
|
+
state.title = action.payload;
|
|
208
|
+
state.justSaved = false;
|
|
209
|
+
}
|
|
210
|
+
}),
|
|
211
|
+
|
|
212
|
+
saveDoc: create.asyncThunk(
|
|
213
|
+
async (arg: SaveDocArgs, { dispatch, getState, rejectWithValue }) => {
|
|
214
|
+
const requestedPageKey = arg.pageKey;
|
|
215
|
+
const state = (getState() as any).doc;
|
|
216
|
+
const { pageKey, slateData, dbSpaceId, meta } = state;
|
|
217
|
+
|
|
218
|
+
if (!pageKey || pageKey !== requestedPageKey) {
|
|
219
|
+
return rejectWithValue("内容已切换,取消保存");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!slateData) {
|
|
223
|
+
return rejectWithValue("内容为空,无法保存");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const [
|
|
227
|
+
{ extractTitleFromSlate, extractMentionsFromSlate },
|
|
228
|
+
{ slateToRenderMarkdown },
|
|
229
|
+
{ buildSkillSummaryMarker },
|
|
230
|
+
] = await Promise.all([
|
|
231
|
+
import("create/editor/utils/slateUtils"),
|
|
232
|
+
import("create/editor/transforms/slateToRenderMarkdown"),
|
|
233
|
+
import("ai/skills/skillSummaryMarker"),
|
|
234
|
+
]);
|
|
235
|
+
const title =
|
|
236
|
+
(typeof state.title === "string" && state.title.trim()) ||
|
|
237
|
+
extractTitleFromSlate(slateData) ||
|
|
238
|
+
"未命名页面";
|
|
239
|
+
const tools = extractMentionsFromSlate(slateData);
|
|
240
|
+
const skillSummary = buildSkillSummaryMarker(meta);
|
|
241
|
+
// `content` 是只读展示缓存,保存时从当前 Slate 重新生成。
|
|
242
|
+
const content = slateToRenderMarkdown(slateData);
|
|
243
|
+
const now = new Date();
|
|
244
|
+
const updatedAt = formatISO(now);
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
await (dispatch as (action: any) => any)(
|
|
248
|
+
patch({
|
|
249
|
+
dbKey: pageKey,
|
|
250
|
+
changes: {
|
|
251
|
+
updatedAt,
|
|
252
|
+
slateData,
|
|
253
|
+
title,
|
|
254
|
+
tools,
|
|
255
|
+
content,
|
|
256
|
+
...(meta ? { meta } : {}),
|
|
257
|
+
},
|
|
258
|
+
})
|
|
259
|
+
).unwrap();
|
|
260
|
+
|
|
261
|
+
if (dbSpaceId) {
|
|
262
|
+
await (dispatch as (action: any) => any)(
|
|
263
|
+
(updateContentTitle as any)({
|
|
264
|
+
spaceId: dbSpaceId,
|
|
265
|
+
contentKey: pageKey,
|
|
266
|
+
title,
|
|
267
|
+
skillSummary,
|
|
268
|
+
})
|
|
269
|
+
).unwrap();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (typeof window !== "undefined") {
|
|
273
|
+
window.dispatchEvent(new Event("nolo-user-data-updated"));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return { updatedAt, title, savedContent: slateData, content };
|
|
277
|
+
} catch (e: any) {
|
|
278
|
+
return rejectWithValue(e.message || "保存失败");
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
pending: (state) => {
|
|
283
|
+
state.isSaving = true;
|
|
284
|
+
state.saveError = null;
|
|
285
|
+
state.justSaved = false;
|
|
286
|
+
},
|
|
287
|
+
fulfilled: (
|
|
288
|
+
state,
|
|
289
|
+
action: PayloadAction<{
|
|
290
|
+
updatedAt: string;
|
|
291
|
+
title: string;
|
|
292
|
+
savedContent: any;
|
|
293
|
+
content: string;
|
|
294
|
+
}>
|
|
295
|
+
) => {
|
|
296
|
+
state.isSaving = false;
|
|
297
|
+
state.lastSavedAt = action.payload.updatedAt;
|
|
298
|
+
state.title = action.payload.title;
|
|
299
|
+
state.content = action.payload.content;
|
|
300
|
+
state.lastSavedSlateData = action.payload.savedContent;
|
|
301
|
+
state.lastSavedTitle = action.payload.title;
|
|
302
|
+
state.justSaved = true;
|
|
303
|
+
},
|
|
304
|
+
rejected: (state, action) => {
|
|
305
|
+
state.isSaving = false;
|
|
306
|
+
state.saveError =
|
|
307
|
+
(action.payload as string) || action.error.message || "未知错误";
|
|
308
|
+
state.justSaved = false;
|
|
309
|
+
},
|
|
310
|
+
}
|
|
311
|
+
),
|
|
312
|
+
|
|
313
|
+
resetJustSavedStatus: create.reducer((state) => {
|
|
314
|
+
state.justSaved = false;
|
|
315
|
+
}),
|
|
316
|
+
|
|
317
|
+
setDocFocusContext: create.reducer(
|
|
318
|
+
(state, action: PayloadAction<DocState["focusContext"]>) => {
|
|
319
|
+
state.focusContext = action.payload;
|
|
320
|
+
}
|
|
321
|
+
),
|
|
322
|
+
|
|
323
|
+
toggleReadOnly: create.reducer((state) => {
|
|
324
|
+
state.isReadOnly = !state.isReadOnly;
|
|
325
|
+
}),
|
|
326
|
+
setReadOnly: create.reducer((state, action: PayloadAction<boolean>) => {
|
|
327
|
+
state.isReadOnly = action.payload;
|
|
328
|
+
}),
|
|
329
|
+
resetDoc: create.reducer((state) => {
|
|
330
|
+
Object.assign(state, initialState);
|
|
331
|
+
}),
|
|
332
|
+
updateDocTags: create.reducer((state, action: PayloadAction<string[]>) => {
|
|
333
|
+
if (state.isInitialized) state.tags = action.payload;
|
|
334
|
+
}),
|
|
335
|
+
previewDoc: create.reducer((state, action: PayloadAction<any>) => {
|
|
336
|
+
Object.assign(state, initialState);
|
|
337
|
+
state.isInitialized = true;
|
|
338
|
+
state.isLoading = false;
|
|
339
|
+
state.isReadOnly = true;
|
|
340
|
+
state.slateData = action.payload.slateData;
|
|
341
|
+
state.title = action.payload.title;
|
|
342
|
+
state.lastSavedTitle = action.payload.title;
|
|
343
|
+
state.pageKey = action.payload.dbKey;
|
|
344
|
+
state.id = action.payload.id;
|
|
345
|
+
state.type = action.payload.type || DataType.DOC;
|
|
346
|
+
state.lastSavedSlateData = action.payload.slateData;
|
|
347
|
+
state.tags = action.payload.tags || null;
|
|
348
|
+
state.dbSpaceId = action.payload.spaceId;
|
|
349
|
+
state.content = action.payload.content || null;
|
|
350
|
+
state.meta = action.payload.meta || null;
|
|
351
|
+
}),
|
|
352
|
+
}),
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
export const {
|
|
356
|
+
createDoc,
|
|
357
|
+
initDoc,
|
|
358
|
+
updateSlate,
|
|
359
|
+
updateTitle,
|
|
360
|
+
saveDoc,
|
|
361
|
+
resetJustSavedStatus,
|
|
362
|
+
setDocFocusContext,
|
|
363
|
+
toggleReadOnly,
|
|
364
|
+
setReadOnly,
|
|
365
|
+
resetDoc,
|
|
366
|
+
updateDocTags,
|
|
367
|
+
previewDoc,
|
|
368
|
+
} = docSlice.actions;
|
|
369
|
+
|
|
370
|
+
const selectDocState = (state: any) => state.doc;
|
|
371
|
+
|
|
372
|
+
export const selectDoc = (state: any) => state.doc;
|
|
373
|
+
|
|
374
|
+
export const selectSlateData = createSelector(
|
|
375
|
+
[selectDocState],
|
|
376
|
+
(doc) => doc.slateData
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
export const selectDocIsLoading = createSelector(
|
|
380
|
+
[selectDocState],
|
|
381
|
+
(doc) => doc.isLoading
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
export const selectIsReadOnly = createSelector(
|
|
385
|
+
[selectDocState],
|
|
386
|
+
(doc) => doc.isReadOnly
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
export const selectDocIsInitialized = createSelector(
|
|
390
|
+
[selectDocState],
|
|
391
|
+
(doc) => doc.isInitialized
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
export const selectDocError = createSelector(
|
|
395
|
+
[selectDocState],
|
|
396
|
+
(doc) => doc.error
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
export const selectIsSaving = createSelector(
|
|
400
|
+
[selectDocState],
|
|
401
|
+
(doc) => doc.isSaving
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
export const selectSaveError = createSelector(
|
|
405
|
+
[selectDocState],
|
|
406
|
+
(doc) => doc.saveError
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
export const selectJustSaved = createSelector(
|
|
410
|
+
[selectDocState],
|
|
411
|
+
(doc) => doc.justSaved
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
export const selectDocTitle = createSelector(
|
|
415
|
+
[selectDocState],
|
|
416
|
+
(doc) => doc.title
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
export const selectHasPendingChanges = createSelector(
|
|
420
|
+
[
|
|
421
|
+
selectSlateData,
|
|
422
|
+
(state: any) => state.doc.lastSavedSlateData,
|
|
423
|
+
selectDocTitle,
|
|
424
|
+
(state: any) => state.doc.lastSavedTitle,
|
|
425
|
+
selectIsReadOnly,
|
|
426
|
+
selectDocIsInitialized,
|
|
427
|
+
],
|
|
428
|
+
(slateData, lastSavedSlateData, title, lastSavedTitle, isReadOnly, isInitialized) => {
|
|
429
|
+
if (!isInitialized || isReadOnly) return false;
|
|
430
|
+
return (
|
|
431
|
+
hasSlateContentChanged(slateData, lastSavedSlateData) ||
|
|
432
|
+
(title || "") !== (lastSavedTitle || "")
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
export const selectDocSpaceId = createSelector(
|
|
438
|
+
[selectDocState],
|
|
439
|
+
(doc) => doc.dbSpaceId
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
export const selectDocId = createSelector(
|
|
443
|
+
[selectDocState],
|
|
444
|
+
(doc) => doc.id
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
export const selectDocKey = createSelector(
|
|
448
|
+
[selectDocState],
|
|
449
|
+
(doc) => doc.pageKey
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
export const selectDocTags = createSelector(
|
|
453
|
+
[selectDocState],
|
|
454
|
+
(doc) => doc.tags
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
export const selectDocFocusContext = createSelector(
|
|
458
|
+
[selectDocState],
|
|
459
|
+
(doc) => doc.focusContext
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
export const selectLastSavedAt = createSelector(
|
|
463
|
+
[selectDocState],
|
|
464
|
+
(doc) => doc.lastSavedAt
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
export default docSlice.reducer;
|
|
468
|
+
export const docListenerMiddleware = createListenerMiddleware();
|