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,270 @@
|
|
|
1
|
+
// 文件: render/table/createTableAction.ts
|
|
2
|
+
|
|
3
|
+
import { formatISO } from "date-fns";
|
|
4
|
+
import { ulid } from "ulid";
|
|
5
|
+
import i18n from "app/i18n";
|
|
6
|
+
|
|
7
|
+
import type { RootState, AppDispatch } from "app/store";
|
|
8
|
+
import { selectUserId } from "auth/authSlice";
|
|
9
|
+
import {
|
|
10
|
+
addContentToSpace,
|
|
11
|
+
selectCurrentSpaceId,
|
|
12
|
+
} from "create/space/spaceSlice";
|
|
13
|
+
import { write } from "database/dbSlice";
|
|
14
|
+
import { metaKey, rowKey } from "database/keys";
|
|
15
|
+
import { DataType } from "create/types";
|
|
16
|
+
|
|
17
|
+
import type {
|
|
18
|
+
TableColumn,
|
|
19
|
+
TableColumnType,
|
|
20
|
+
CreateTableColumnInput,
|
|
21
|
+
TableMeta,
|
|
22
|
+
} from "./types";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 创建表时的输入参数:
|
|
26
|
+
* - spaceId 可选:不传则用当前 Space
|
|
27
|
+
* - title 可选:不传用默认文案(表显示名)
|
|
28
|
+
* - description 可选:表用途说明(包括每行代表什么,给人和 AI 看)
|
|
29
|
+
* - tags 可选:关键词标签列表(由 Agent 自动填比较合理)
|
|
30
|
+
* - categoryId 可选:Space 内分类
|
|
31
|
+
* - columns 可选:表字段定义(对象数组)
|
|
32
|
+
* - 不传或为空时,自动使用默认两列:
|
|
33
|
+
* 1) title / 标题(主字段)
|
|
34
|
+
* 2) note / 备注
|
|
35
|
+
* - withDefaultRows 可选:是否创建两行示例数据
|
|
36
|
+
*/
|
|
37
|
+
export interface CreateTableArgs {
|
|
38
|
+
spaceId?: string;
|
|
39
|
+
title?: string;
|
|
40
|
+
description?: string;
|
|
41
|
+
tags?: string[];
|
|
42
|
+
categoryId?: string;
|
|
43
|
+
columns?: CreateTableColumnInput[];
|
|
44
|
+
withDefaultRows?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 默认列定义:标题 + 备注
|
|
48
|
+
const DEFAULT_COLUMNS: Omit<TableColumn, "id">[] = [
|
|
49
|
+
{
|
|
50
|
+
name: "title",
|
|
51
|
+
label: "标题",
|
|
52
|
+
type: "text",
|
|
53
|
+
description: "这一行记录的主题或名称。",
|
|
54
|
+
isPrimary: true,
|
|
55
|
+
required: true,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "note",
|
|
59
|
+
label: "备注",
|
|
60
|
+
type: "text",
|
|
61
|
+
description: "对该条目的补充说明。",
|
|
62
|
+
required: false,
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 将 CreateTableColumnInput[] 归一化成 TableColumn[]
|
|
68
|
+
* - 为每一列生成 id
|
|
69
|
+
* - 清洗 name / label / type / description / isPrimary / required / options
|
|
70
|
+
* - 如果结果为空,则使用 DEFAULT_COLUMNS
|
|
71
|
+
*/
|
|
72
|
+
const normalizeColumns = (
|
|
73
|
+
inputColumns: CreateTableColumnInput[] | undefined
|
|
74
|
+
): TableColumn[] => {
|
|
75
|
+
const base = Array.isArray(inputColumns) ? inputColumns : [];
|
|
76
|
+
|
|
77
|
+
const normalized: TableColumn[] = base
|
|
78
|
+
.map((c) => {
|
|
79
|
+
if (!c || typeof c.name !== "string") return null;
|
|
80
|
+
const name = c.name.trim();
|
|
81
|
+
if (!name) return null;
|
|
82
|
+
|
|
83
|
+
const col: TableColumn = {
|
|
84
|
+
id: c.id && typeof c.id === "string" && c.id.trim() ? c.id.trim() : ulid(),
|
|
85
|
+
name,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
if (typeof c.label === "string" && c.label.trim()) {
|
|
89
|
+
col.label = c.label.trim();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (typeof c.type === "string") {
|
|
93
|
+
col.type = c.type as TableColumnType;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (typeof c.description === "string" && c.description.trim()) {
|
|
97
|
+
col.description = c.description.trim();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (typeof c.isPrimary === "boolean") {
|
|
101
|
+
col.isPrimary = c.isPrimary;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (typeof c.required === "boolean") {
|
|
105
|
+
col.required = c.required;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (Array.isArray(c.options)) {
|
|
109
|
+
const opts = c.options
|
|
110
|
+
.map((opt) => (typeof opt === "string" ? opt.trim() : ""))
|
|
111
|
+
.filter(Boolean);
|
|
112
|
+
if (opts.length) {
|
|
113
|
+
col.options = opts;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return col;
|
|
118
|
+
})
|
|
119
|
+
.filter(Boolean) as TableColumn[];
|
|
120
|
+
|
|
121
|
+
if (normalized.length > 0) {
|
|
122
|
+
// 确保有一个主字段:如果没有 isPrimary,就把第一列设为主字段
|
|
123
|
+
if (!normalized.some((c) => c.isPrimary)) {
|
|
124
|
+
normalized[0].isPrimary = true;
|
|
125
|
+
}
|
|
126
|
+
return normalized;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 未提供列时,使用默认:标题 + 备注
|
|
130
|
+
return DEFAULT_COLUMNS.map((c) => ({
|
|
131
|
+
...c,
|
|
132
|
+
id: ulid(),
|
|
133
|
+
}));
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 类似 createPageAction:
|
|
138
|
+
* 1) 基于当前 userId 生成 tableId + metaKey
|
|
139
|
+
* 2) 写入 TABLE meta
|
|
140
|
+
* 3) 可选:写入两行 TABLE_ROW 默认数据
|
|
141
|
+
* 4) 如果有 spaceId(或当前 Space),自动 addContentToSpace
|
|
142
|
+
* 5) 返回 dbKey(路由用)
|
|
143
|
+
*/
|
|
144
|
+
export const createTableAction = async (
|
|
145
|
+
{
|
|
146
|
+
spaceId: customSpaceId,
|
|
147
|
+
title: customTitle,
|
|
148
|
+
description: customDescription,
|
|
149
|
+
tags: customTags,
|
|
150
|
+
categoryId,
|
|
151
|
+
columns,
|
|
152
|
+
withDefaultRows = true,
|
|
153
|
+
}: CreateTableArgs = {},
|
|
154
|
+
{ dispatch, getState }: { dispatch: AppDispatch; getState: () => RootState }
|
|
155
|
+
): Promise<string> => {
|
|
156
|
+
const state = getState();
|
|
157
|
+
const userId = selectUserId(state);
|
|
158
|
+
if (!userId) throw new Error("User ID not found.");
|
|
159
|
+
|
|
160
|
+
// Important:
|
|
161
|
+
// View-mode-based scoping belongs to the UI entry layer (currently the sidebar-top
|
|
162
|
+
// create button), not to this action.
|
|
163
|
+
// This action preserves explicit caller intent:
|
|
164
|
+
// - prefer `spaceId` when a caller passes it;
|
|
165
|
+
// - otherwise fall back to the current selected space only.
|
|
166
|
+
const spaceId = customSpaceId ?? selectCurrentSpaceId(state);
|
|
167
|
+
|
|
168
|
+
const tableId = ulid();
|
|
169
|
+
|
|
170
|
+
const now = new Date();
|
|
171
|
+
const nowIso = formatISO(now);
|
|
172
|
+
|
|
173
|
+
const dbKey = metaKey(userId, tableId);
|
|
174
|
+
|
|
175
|
+
const defaultTitle = i18n.t("table:defaultTitle", {
|
|
176
|
+
defaultValue: "新建表格",
|
|
177
|
+
});
|
|
178
|
+
const title = customTitle?.trim() || defaultTitle;
|
|
179
|
+
|
|
180
|
+
const description = customDescription?.trim() || undefined;
|
|
181
|
+
const tags =
|
|
182
|
+
Array.isArray(customTags) && customTags.length
|
|
183
|
+
? customTags
|
|
184
|
+
.map((t) => (typeof t === "string" ? t.trim() : ""))
|
|
185
|
+
.filter(Boolean)
|
|
186
|
+
: undefined;
|
|
187
|
+
|
|
188
|
+
// 1) 归一化列定义
|
|
189
|
+
const finalColumns: TableColumn[] = normalizeColumns(columns);
|
|
190
|
+
|
|
191
|
+
// 2) 写入表 meta(TABLE)
|
|
192
|
+
const tableMeta: TableMeta = {
|
|
193
|
+
dbKey,
|
|
194
|
+
tenantId: userId,
|
|
195
|
+
tableId,
|
|
196
|
+
spaceId: spaceId ?? null,
|
|
197
|
+
displayName: title,
|
|
198
|
+
description,
|
|
199
|
+
tags,
|
|
200
|
+
schemaVersion: 1,
|
|
201
|
+
columns: finalColumns,
|
|
202
|
+
views: [], // 后续可以在 UI 中新增视图
|
|
203
|
+
triggers: [], // 后续可以在 UI/配置中新增触发器
|
|
204
|
+
aiConfig: undefined,
|
|
205
|
+
createdAt: nowIso,
|
|
206
|
+
updatedAt: nowIso,
|
|
207
|
+
type: DataType.TABLE,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
await dispatch(
|
|
211
|
+
write({
|
|
212
|
+
data: tableMeta,
|
|
213
|
+
customKey: dbKey,
|
|
214
|
+
})
|
|
215
|
+
).unwrap();
|
|
216
|
+
|
|
217
|
+
// 3) 可选:写入两行默认数据(TABLE_ROW)
|
|
218
|
+
if (withDefaultRows) {
|
|
219
|
+
const defaultRows = [
|
|
220
|
+
{
|
|
221
|
+
title: "示例一",
|
|
222
|
+
note: "你可以在这里记录任何内容,例如任务、想法或配置项。",
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
title: "示例二",
|
|
226
|
+
note: '双击单元格开始编辑,右上角可以添加字段和行。',
|
|
227
|
+
},
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
await Promise.all(
|
|
231
|
+
defaultRows.map(async (values) => {
|
|
232
|
+
const { dbKey: rowKeyStr, rowId } = rowKey.create(userId, tableId);
|
|
233
|
+
|
|
234
|
+
const row = {
|
|
235
|
+
dbKey: rowKeyStr,
|
|
236
|
+
tenantId: userId,
|
|
237
|
+
tableId,
|
|
238
|
+
rowId,
|
|
239
|
+
createdAt: nowIso,
|
|
240
|
+
updatedAt: nowIso,
|
|
241
|
+
type: DataType.TABLE_ROW as const,
|
|
242
|
+
...values,
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
await dispatch(
|
|
246
|
+
write({
|
|
247
|
+
data: row,
|
|
248
|
+
customKey: rowKeyStr,
|
|
249
|
+
})
|
|
250
|
+
).unwrap();
|
|
251
|
+
})
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 4) 如果在某个 Space 中,把表挂到 Space.contents(关键点)
|
|
256
|
+
if (spaceId) {
|
|
257
|
+
await dispatch(
|
|
258
|
+
addContentToSpace({
|
|
259
|
+
spaceId,
|
|
260
|
+
contentKey: dbKey,
|
|
261
|
+
type: DataType.TABLE, // ContentType 里要有 "table"
|
|
262
|
+
title,
|
|
263
|
+
categoryId,
|
|
264
|
+
})
|
|
265
|
+
).unwrap();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// 5) 返回 dbKey,用于路由跳转 /meta-{tenantId}-{tableId}
|
|
269
|
+
return dbKey;
|
|
270
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// 文件路径: render/table/deleteTableAction.ts
|
|
2
|
+
|
|
3
|
+
import type { RootState, AppDispatch } from "app/store";
|
|
4
|
+
import { getRuntimeServerContext } from "database/runtimeServerContext";
|
|
5
|
+
import { SEPARATOR, createKey } from "database/keys";
|
|
6
|
+
import { scheduleDeleteReplication } from "database/actions/replication";
|
|
7
|
+
|
|
8
|
+
export interface DeleteTableArgs {
|
|
9
|
+
dbKey: string; // 形如:meta-{tenantId}-{tableId}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 从表的 meta dbKey 中解析 tenantId / tableId
|
|
14
|
+
* 约定:meta-{tenantId}-{tableId}
|
|
15
|
+
*/
|
|
16
|
+
const parseMetaKey = (dbKey: string): { tenantId: string; tableId: string } => {
|
|
17
|
+
const parts = dbKey.split(SEPARATOR);
|
|
18
|
+
if (parts[0] !== "meta" || parts.length < 3) {
|
|
19
|
+
throw new Error(`非法表 key:${dbKey}`);
|
|
20
|
+
}
|
|
21
|
+
const tenantId = parts[1];
|
|
22
|
+
const tableId = parts.slice(2).join(SEPARATOR);
|
|
23
|
+
return { tenantId, tableId };
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 通用的「按前缀收集所有 key」
|
|
28
|
+
* 范式与 deleteDialogMsgsAction 中的 collectKeys 保持一致。
|
|
29
|
+
*
|
|
30
|
+
* 说明:
|
|
31
|
+
* - prefix 为某类实体的起始前缀,例如:
|
|
32
|
+
* row-{tenantId}-{tableId}-
|
|
33
|
+
* idx-{tenantId}-{tableId}-
|
|
34
|
+
* view-{tenantId}-{tableId}-
|
|
35
|
+
*/
|
|
36
|
+
const collectKeysByPrefix = async (db: any, prefix: string): Promise<string[]> => {
|
|
37
|
+
const keys: string[] = [];
|
|
38
|
+
|
|
39
|
+
for await (const [key] of db.iterator({
|
|
40
|
+
gte: prefix,
|
|
41
|
+
lte: prefix + "\uffff",
|
|
42
|
+
})) {
|
|
43
|
+
keys.push(key as string);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return keys;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 删除整张表(本地 + 同步服务器):
|
|
51
|
+
*
|
|
52
|
+
* 本地:
|
|
53
|
+
* 1) 从 meta dbKey 中解析 tenantId / tableId
|
|
54
|
+
* 2) 删除该表的所有行:row-{tenantId}-{tableId}-{rowId}
|
|
55
|
+
* 3) 删除该表的所有索引:idx-{tenantId}-{tableId}-...
|
|
56
|
+
* 4) 删除该表的所有视图:view-{tenantId}-{tableId}-{viewId}
|
|
57
|
+
* 6) 删除表本身的 meta:meta-{tenantId}-{tableId}
|
|
58
|
+
*
|
|
59
|
+
* 远端:
|
|
60
|
+
* - 使用 syncWithServers + noloDeleteRequest,
|
|
61
|
+
* 调用后端统一 DELETE 接口:
|
|
62
|
+
* DELETE /db/{metaDbKey}?type=table
|
|
63
|
+
*
|
|
64
|
+
* 说明:
|
|
65
|
+
* - 当前实现假定:一张逻辑表严格属于单个 tenant:
|
|
66
|
+
* 对于任意 meta-{tenantId}-{tableId},
|
|
67
|
+
* 所有行满足 row.tenantId === tenantId 且 row.tableId === tableId。
|
|
68
|
+
* - 不做历史兼容处理,如有旧数据可通过清库 / 迁移脚本处理。
|
|
69
|
+
*/
|
|
70
|
+
export const deleteTableAction = async (
|
|
71
|
+
{ dbKey }: DeleteTableArgs,
|
|
72
|
+
{
|
|
73
|
+
dispatch,
|
|
74
|
+
getState,
|
|
75
|
+
extra,
|
|
76
|
+
}: {
|
|
77
|
+
dispatch: AppDispatch;
|
|
78
|
+
getState: () => RootState;
|
|
79
|
+
extra: { db: any };
|
|
80
|
+
}
|
|
81
|
+
): Promise<string> => {
|
|
82
|
+
const { db } = extra;
|
|
83
|
+
const state = getState();
|
|
84
|
+
const { currentServer, syncServers } = getRuntimeServerContext(state);
|
|
85
|
+
|
|
86
|
+
const { tenantId, tableId } = parseMetaKey(dbKey);
|
|
87
|
+
|
|
88
|
+
// 1) 本地收集所有行 key:row-{tenantId}-{tableId}-{rowId}
|
|
89
|
+
const rowPrefix = createKey("row", tenantId, tableId, "");
|
|
90
|
+
const rowKeys = await collectKeysByPrefix(db, rowPrefix);
|
|
91
|
+
|
|
92
|
+
// 2) 本地收集所有索引 key:idx-{tenantId}-{tableId}-...
|
|
93
|
+
const idxPrefix = createKey("idx", tenantId, tableId, "");
|
|
94
|
+
const idxKeys = await collectKeysByPrefix(db, idxPrefix);
|
|
95
|
+
|
|
96
|
+
// 3) 本地收集所有视图 key:view-{tenantId}-{tableId}-{viewId}
|
|
97
|
+
const viewPrefix = createKey("view", tenantId, tableId, "");
|
|
98
|
+
const viewKeys = await collectKeysByPrefix(db, viewPrefix);
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
// 5) 需要删除的 key:行 + 索引 + 视图 + 触发器 + meta 自身
|
|
102
|
+
const keysToDelete = Array.from(
|
|
103
|
+
new Set<string>([
|
|
104
|
+
...rowKeys,
|
|
105
|
+
...idxKeys,
|
|
106
|
+
...viewKeys,
|
|
107
|
+
dbKey,
|
|
108
|
+
])
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
if (keysToDelete.length > 0) {
|
|
112
|
+
const ops = keysToDelete.map((key) => ({
|
|
113
|
+
type: "del" as const,
|
|
114
|
+
key,
|
|
115
|
+
}));
|
|
116
|
+
await db.batch(ops);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 6) local-first:本地先删除,远端整表删除在后台收敛。
|
|
120
|
+
scheduleDeleteReplication({
|
|
121
|
+
currentServer,
|
|
122
|
+
syncServers,
|
|
123
|
+
dbKey,
|
|
124
|
+
deleteOptions: { type: "table" as const },
|
|
125
|
+
state,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return dbKey;
|
|
129
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { DataType } from "create/types";
|
|
2
|
+
import { rowKey } from "database/keys";
|
|
3
|
+
|
|
4
|
+
const getRowTimestamp = (row: any): number => {
|
|
5
|
+
if (!row || typeof row !== "object") return 0;
|
|
6
|
+
const updatedAt = Date.parse(row.updatedAt ?? "");
|
|
7
|
+
if (Number.isFinite(updatedAt) && updatedAt > 0) return updatedAt;
|
|
8
|
+
const createdAt = Date.parse(row.createdAt ?? "");
|
|
9
|
+
return Number.isFinite(createdAt) && createdAt > 0 ? createdAt : 0;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const shouldReplaceMergedRow = (nextRow: any, currentRow: any): boolean => {
|
|
13
|
+
const nextTs = getRowTimestamp(nextRow);
|
|
14
|
+
const currentTs = getRowTimestamp(currentRow);
|
|
15
|
+
if (nextTs !== currentTs) return nextTs > currentTs;
|
|
16
|
+
return Boolean(nextRow?.deletedAt) && !currentRow?.deletedAt;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const mergeTableRows = (...rowLists: any[][]): any[] => {
|
|
20
|
+
const merged = new Map<string, any>();
|
|
21
|
+
|
|
22
|
+
for (const rowList of rowLists) {
|
|
23
|
+
for (const row of rowList) {
|
|
24
|
+
const dbKey = row?.dbKey;
|
|
25
|
+
if (!dbKey) continue;
|
|
26
|
+
const existing = merged.get(dbKey);
|
|
27
|
+
if (!existing || shouldReplaceMergedRow(row, existing)) {
|
|
28
|
+
merged.set(dbKey, row);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return Array.from(merged.values());
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const loadLocalTableRows = async (
|
|
37
|
+
db: any,
|
|
38
|
+
tenantId: string,
|
|
39
|
+
tableId: string
|
|
40
|
+
): Promise<any[]> => {
|
|
41
|
+
if (!db || typeof db.iterator !== "function") {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const rows: any[] = [];
|
|
46
|
+
const { gte, lte } = rowKey.range(tenantId, tableId);
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
for await (const [, value] of db.iterator({ gte, lte })) {
|
|
50
|
+
if (value?.type === DataType.TABLE_ROW) {
|
|
51
|
+
rows.push(value);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return rows;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const fetchTableRowsFromServer = async (
|
|
62
|
+
server: string,
|
|
63
|
+
tenantId: string,
|
|
64
|
+
tableId: string,
|
|
65
|
+
headers: Record<string, string>
|
|
66
|
+
): Promise<any[]> => {
|
|
67
|
+
const res = await fetch(`${server}/rpc/listTableRows`, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers,
|
|
70
|
+
body: JSON.stringify({ tenantId, tableId }),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (!res.ok) {
|
|
74
|
+
let msg = `加载表 ${tableId} 行失败(${res.status})`;
|
|
75
|
+
try {
|
|
76
|
+
const err = await res.json();
|
|
77
|
+
if (err && typeof err.message === "string") {
|
|
78
|
+
msg = err.message;
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// ignore
|
|
82
|
+
}
|
|
83
|
+
throw new Error(msg);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const data = await res.json();
|
|
87
|
+
if (!Array.isArray(data)) {
|
|
88
|
+
throw new Error("服务器返回格式错误:预期为数组");
|
|
89
|
+
}
|
|
90
|
+
return data;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const cacheMergedTableRows = async (db: any, mergedRows: any[]) => {
|
|
94
|
+
if (!db) return;
|
|
95
|
+
|
|
96
|
+
await Promise.all(
|
|
97
|
+
mergedRows.map(async (mergedRow: any) => {
|
|
98
|
+
if (!mergedRow?.dbKey) return;
|
|
99
|
+
try {
|
|
100
|
+
const localRow = await db.get(mergedRow.dbKey).catch(() => null);
|
|
101
|
+
if (!localRow) {
|
|
102
|
+
await db.put(mergedRow.dbKey, mergedRow);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const serverTs = new Date(
|
|
107
|
+
mergedRow.updatedAt ?? mergedRow.createdAt ?? 0
|
|
108
|
+
).getTime();
|
|
109
|
+
const localTs = new Date(
|
|
110
|
+
localRow.updatedAt ?? localRow.createdAt ?? 0
|
|
111
|
+
).getTime();
|
|
112
|
+
const shouldOverwrite =
|
|
113
|
+
serverTs > localTs ||
|
|
114
|
+
(serverTs === localTs &&
|
|
115
|
+
Boolean(mergedRow.deletedAt) &&
|
|
116
|
+
!Boolean(localRow.deletedAt));
|
|
117
|
+
|
|
118
|
+
if (shouldOverwrite) {
|
|
119
|
+
await db.put(mergedRow.dbKey, mergedRow);
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
// Ignore local cache errors.
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export const fetchAndCacheTableRows = async ({
|
|
129
|
+
db,
|
|
130
|
+
tenantId,
|
|
131
|
+
tableId,
|
|
132
|
+
token,
|
|
133
|
+
remoteServers = [],
|
|
134
|
+
}: {
|
|
135
|
+
db: any;
|
|
136
|
+
tenantId: string;
|
|
137
|
+
tableId: string;
|
|
138
|
+
token?: string | null;
|
|
139
|
+
remoteServers?: string[];
|
|
140
|
+
}): Promise<any[]> => {
|
|
141
|
+
const headers: Record<string, string> = {
|
|
142
|
+
"Content-Type": "application/json",
|
|
143
|
+
};
|
|
144
|
+
if (token) {
|
|
145
|
+
headers.Authorization = `Bearer ${token}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const localRows = await loadLocalTableRows(db, tenantId, tableId);
|
|
149
|
+
const remoteResults = await Promise.allSettled(
|
|
150
|
+
remoteServers.map((server) =>
|
|
151
|
+
fetchTableRowsFromServer(server, tenantId, tableId, headers)
|
|
152
|
+
)
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const fulfilledRemoteRows = remoteResults
|
|
156
|
+
.filter(
|
|
157
|
+
(result): result is PromiseFulfilledResult<any[]> =>
|
|
158
|
+
result.status === "fulfilled"
|
|
159
|
+
)
|
|
160
|
+
.map((result) => result.value);
|
|
161
|
+
|
|
162
|
+
if (fulfilledRemoteRows.length === 0 && localRows.length === 0) {
|
|
163
|
+
const firstFailure = remoteResults.find(
|
|
164
|
+
(result): result is PromiseRejectedResult =>
|
|
165
|
+
result.status === "rejected"
|
|
166
|
+
);
|
|
167
|
+
throw new Error(firstFailure?.reason?.message || "加载表行失败");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const mergedRows = mergeTableRows(localRows, ...fulfilledRemoteRows);
|
|
171
|
+
await cacheMergedTableRows(db, mergedRows);
|
|
172
|
+
|
|
173
|
+
return mergedRows.filter((row) => !row?.deletedAt);
|
|
174
|
+
};
|