nolo-cli 0.1.41 → 0.1.43
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/README.md +27 -12
- package/agent-runtime/agentThread.ts +222 -0
- package/agent-runtime/agentThreadAdmission.ts +74 -0
- package/agent-runtime/dialogWritePlan.ts +42 -0
- package/agent-runtime/hostAdapter.ts +1 -0
- package/agent-runtime/index.ts +43 -0
- package/agent-runtime/localLoop.ts +31 -2
- package/agent-runtime/localToolPolicy.ts +1 -0
- package/agent-runtime/localWorkspaceTools.ts +1258 -78
- package/agent-runtime/noloWorkspaceTools.ts +20 -1
- package/agent-runtime/runtimeToolPolicy.ts +1 -5
- package/agent-runtime/runtimeToolSurface.ts +1 -0
- package/agentAliases.ts +29 -9
- package/agentInternalCommandEntries.ts +11 -3
- package/agentMachineCommands.ts +1 -0
- package/agentRecordCommands.ts +2 -2
- package/agentRecordHelpers.ts +59 -6
- package/agentRunCommand.ts +84 -19
- package/ai/agent/buildSystemPrompt.ts +14 -0
- package/ai/agent/cliExecutor.ts +202 -43
- package/ai/agent/createAgentSchema.ts +2 -2
- package/ai/agent/liveAudioModel.ts +1 -1
- package/ai/agent/streamAgentChatTurn.ts +154 -20
- package/ai/agent/streamAgentChatTurnUtils.ts +63 -0
- package/ai/ai.locale.ts +70 -0
- package/ai/chat/inlineImageUrlsForCustomProvider.ts +7 -2
- package/ai/chat/sendOpenAICompletionsRequest.ts +25 -1
- package/ai/llm/kimi.ts +0 -1
- package/ai/llm/openrouterModels.ts +27 -2
- package/ai/memory/policy.ts +49 -0
- package/ai/memory/queryShared.ts +10 -4
- package/ai/memory/recentRelationshipRecap.ts +4 -0
- package/ai/memory/remember.ts +48 -104
- package/ai/memory/scope.ts +74 -0
- package/ai/memory/types.ts +6 -0
- package/ai/memory/understanding.ts +10 -18
- package/ai/policy/runtimePolicy.ts +95 -23
- package/ai/tools/agent/agentTools.ts +0 -21
- package/ai/tools/agent/startAgentDialogTool.ts +2 -1
- package/ai/tools/index.ts +2 -20
- package/ai/tools/prepareTools.ts +171 -1
- package/ai/tools/readXhsProfileTool.ts +80 -69
- package/ai/tools/table/createTableTool.ts +11 -0
- package/ai/tools/toolPacks.ts +19 -0
- package/ai/types.ts +3 -0
- package/app/favorite/deletedFavoriteProjection.ts +31 -0
- package/app/hooks/deleteDbKey.ts +24 -3
- package/app/i18n/translations/interface.locale.ts +48 -16
- package/app/types/appSummary.ts +4 -0
- package/app/utils/desktopAgentRuntimeTurnClient.ts +2 -0
- package/auth/adminPermissions.ts +64 -4
- package/auth/routes.ts +5 -0
- package/chat/chat.locale.ts +4 -0
- package/chat/dialog/actions/handleSendMessageAction.ts +7 -2
- package/chat/dialog/dialogAttachmentCleanup.ts +135 -0
- package/chat/dialog/dialogSlice.ts +117 -4
- package/chat/messages/activityCompletion.ts +97 -0
- package/chat/messages/imagePayloadPersistence.ts +22 -0
- package/chat/messages/messageContent.ts +20 -16
- package/chat/messages/messageSlice.ts +20 -1
- package/chat/messages/toolThunks.ts +32 -11
- package/chat/messages/types.ts +8 -1
- package/chat/messages/web/toolDisplayName.ts +686 -0
- package/cli/agentAliases.ts +29 -9
- package/cli/agentInternalCommandEntries.ts +11 -3
- package/cli/agentMachineCommands.ts +1 -0
- package/cli/agentRecordCommands.ts +2 -2
- package/cli/agentRecordHelpers.ts +59 -6
- package/cli/agentRunCommand.ts +84 -19
- package/cli/client/agentRun.ts +65 -253
- package/cli/client/localRuntimeAdapter.ts +179 -53
- package/cli/commandRegistry.ts +7 -4
- package/cli/dialogAttachmentCleanup.ts +151 -0
- package/cli/dialogCommands.ts +163 -61
- package/cli/dialogInternalCommandEntries.ts +1 -1
- package/cli/docCreateCommands.ts +345 -0
- package/cli/docInternalCommandEntries.ts +11 -0
- package/cli/docPageHelpers.ts +80 -0
- package/cli/docSkillHelpers.ts +67 -0
- package/cli/docSpaceHelpers.ts +46 -0
- package/cli/internalCommandEntries.ts +4 -0
- package/cli/machineCommands.ts +18 -34
- package/cli/memoryCommands.ts +253 -0
- package/cli/memoryInternalCommandEntries.ts +9 -0
- package/cli/qoderUsageProbe.ts +252 -0
- package/cli/scriptCommandEntries.ts +0 -3
- package/cli/secretScan.ts +31 -0
- package/cli/tableCommands.ts +248 -13
- package/cli/workflowInternalCommandEntries.ts +2 -3
- package/client/agentRun.test.ts +68 -84
- package/client/agentRun.ts +65 -253
- package/client/localRuntimeAdapter.test.ts +505 -74
- package/client/localRuntimeAdapter.ts +179 -53
- package/commandRegistry.ts +7 -4
- package/connector-experimental/capabilities.ts +10 -9
- package/create/space/sidebarVisibleTypes.ts +19 -0
- package/dialogAttachmentCleanup.ts +151 -0
- package/dialogCommands.ts +163 -61
- package/dialogInternalCommandEntries.ts +1 -1
- package/docCreateCommands.ts +345 -0
- package/docInternalCommandEntries.ts +11 -0
- package/docPageHelpers.ts +80 -0
- package/docSkillHelpers.ts +67 -0
- package/docSpaceHelpers.ts +46 -0
- package/integrations/google/models.ts +4 -4
- package/integrations/openai/generateOpenAIRequestBody.ts +0 -3
- package/integrations/xhs-reader/types.ts +36 -3
- package/integrations/xhs-reader/url.ts +24 -1
- package/internalCommandEntries.ts +4 -0
- package/machineCommands.ts +18 -34
- package/memoryCommands.ts +253 -0
- package/memoryInternalCommandEntries.ts +9 -0
- package/package.json +141 -114
- package/qoderUsageProbe.ts +252 -0
- package/render/contentIcon/types.ts +167 -0
- package/render/page/docSlice.ts +30 -3
- package/render/page/types.ts +2 -0
- package/render/table/createTableAction.ts +4 -0
- package/render/table/tableSlice.ts +61 -0
- package/render/table/types.ts +10 -0
- package/scriptCommandEntries.ts +0 -3
- package/secretScan.ts +31 -0
- package/server/handlers/agentRun/types.ts +24 -23
- package/tableCommands.ts +248 -13
- package/workflowInternalCommandEntries.ts +2 -3
- package/ai/tools/agent/taskRunTool.ts +0 -112
- package/cli/client/taskRunPrompt.ts +0 -27
- package/cli/taskRunCommand.ts +0 -265
- package/client/taskRunPrompt.ts +0 -27
- package/integrations/xhs-reader/analyze/profileAnalyzer.ts +0 -205
- package/integrations/xhs-reader/backends/playwrightProfileCollector.ts +0 -624
- package/integrations/xhs-reader/bridge/readXhsProfileWithBridge.ts +0 -227
- package/integrations/xhs-reader/client/xhsWebApiClient.ts +0 -229
- package/integrations/xhs-reader/normalize.ts +0 -390
- package/integrations/xhs-reader/orchestrator.ts +0 -178
- package/taskRunCommand.ts +0 -265
package/app/hooks/deleteDbKey.ts
CHANGED
|
@@ -13,6 +13,8 @@ import { deleteDialog } from "../../chat/dialog/dialogSlice";
|
|
|
13
13
|
import { deleteTable } from "../../render/table/tableSlice";
|
|
14
14
|
import { deleteContentFromSpace } from "../../create/space/spaceSlice";
|
|
15
15
|
import { remove } from "../../database/dbSlice";
|
|
16
|
+
import { removeFavoriteLocally } from "../favorite/favoriteSlice";
|
|
17
|
+
import { resolveDeletedFavoriteProjectionRemoval } from "../favorite/deletedFavoriteProjection";
|
|
16
18
|
import type { AppDispatch } from "../store";
|
|
17
19
|
import type { RootState } from "../store";
|
|
18
20
|
|
|
@@ -25,21 +27,26 @@ type DeleteDbKeyInput =
|
|
|
25
27
|
serverOrigin?: unknown;
|
|
26
28
|
preferredServerOrigin?: unknown;
|
|
27
29
|
spaceId?: unknown;
|
|
30
|
+
includeAttachments?: unknown;
|
|
28
31
|
};
|
|
29
32
|
|
|
30
33
|
type ResolvedDeleteInput = {
|
|
31
34
|
contentKey: string;
|
|
32
35
|
preferredServerOrigin?: string;
|
|
33
36
|
inputSpaceId?: string | null;
|
|
37
|
+
includeAttachments?: boolean;
|
|
34
38
|
};
|
|
35
39
|
|
|
36
40
|
const performDirectDelete = async (
|
|
37
41
|
dispatch: AppDispatch,
|
|
38
42
|
contentKey: string,
|
|
39
|
-
preferredServerOrigin?: string
|
|
43
|
+
preferredServerOrigin?: string,
|
|
44
|
+
includeAttachments?: boolean
|
|
40
45
|
): Promise<void> => {
|
|
41
46
|
if (isDialogKey(contentKey)) {
|
|
42
|
-
await (dispatch as any)(
|
|
47
|
+
await (dispatch as any)(
|
|
48
|
+
deleteDialog({ dialogKey: contentKey, includeAttachments })
|
|
49
|
+
).unwrap();
|
|
43
50
|
return;
|
|
44
51
|
}
|
|
45
52
|
|
|
@@ -80,12 +87,14 @@ const resolveDeleteInput = (input: DeleteDbKeyInput): ResolvedDeleteInput => {
|
|
|
80
87
|
typeof input.spaceId === "string" && input.spaceId.trim().length > 0
|
|
81
88
|
? input.spaceId
|
|
82
89
|
: null;
|
|
90
|
+
const includeAttachments = input.includeAttachments === true;
|
|
83
91
|
for (const candidate of candidates) {
|
|
84
92
|
if (typeof candidate === "string" && candidate.trim()) {
|
|
85
93
|
return {
|
|
86
94
|
contentKey: candidate,
|
|
87
95
|
preferredServerOrigin,
|
|
88
96
|
inputSpaceId,
|
|
97
|
+
includeAttachments,
|
|
89
98
|
};
|
|
90
99
|
}
|
|
91
100
|
}
|
|
@@ -185,6 +194,7 @@ export const deleteDbKey =
|
|
|
185
194
|
contentKey,
|
|
186
195
|
preferredServerOrigin,
|
|
187
196
|
inputSpaceId,
|
|
197
|
+
includeAttachments,
|
|
188
198
|
} = resolveDeleteInput(input);
|
|
189
199
|
const effectiveSpaceId =
|
|
190
200
|
typeof spaceId === "string" && spaceId.trim().length > 0
|
|
@@ -200,7 +210,12 @@ export const deleteDbKey =
|
|
|
200
210
|
// 无论 space 操作结果如何,实体都必须被标记为已删除,
|
|
201
211
|
// 否则 deleteContentFromSpace 成功但跳过实体删除时,
|
|
202
212
|
// 远端活记录会在下一次 merge 时回流。
|
|
203
|
-
await performDirectDelete(
|
|
213
|
+
await performDirectDelete(
|
|
214
|
+
dispatch,
|
|
215
|
+
contentKey,
|
|
216
|
+
preferredServerOrigin,
|
|
217
|
+
includeAttachments
|
|
218
|
+
);
|
|
204
219
|
|
|
205
220
|
// 2. 如果有 spaceId,先完成本地 space 引用清理,再返回给 UI。
|
|
206
221
|
// 这仍然是 local-first 语义:
|
|
@@ -221,6 +236,12 @@ export const deleteDbKey =
|
|
|
221
236
|
});
|
|
222
237
|
}
|
|
223
238
|
|
|
239
|
+
const favoriteProjectionRemoval =
|
|
240
|
+
resolveDeletedFavoriteProjectionRemoval(contentKey);
|
|
241
|
+
if (favoriteProjectionRemoval) {
|
|
242
|
+
dispatch(removeFavoriteLocally(favoriteProjectionRemoval));
|
|
243
|
+
}
|
|
244
|
+
|
|
224
245
|
if (typeof window !== "undefined") {
|
|
225
246
|
window.dispatchEvent(new Event("nolo-user-data-updated"));
|
|
226
247
|
}
|
|
@@ -471,10 +471,18 @@ export default {
|
|
|
471
471
|
tableShareLinkCopied: "Table share link copied",
|
|
472
472
|
tableShareLinkGenerated: "Table share link generated",
|
|
473
473
|
tableShareOpen: "Open share page",
|
|
474
|
-
tableShareLoadFailed: "Failed to load table share status",
|
|
475
|
-
tableShareReplicationIssue: "Replication needs attention",
|
|
476
|
-
tableShareReplicationLag: "Replication is still catching up",
|
|
477
|
-
|
|
474
|
+
tableShareLoadFailed: "Failed to load table share status",
|
|
475
|
+
tableShareReplicationIssue: "Replication needs attention",
|
|
476
|
+
tableShareReplicationLag: "Replication is still catching up",
|
|
477
|
+
contentIcon: {
|
|
478
|
+
change: "Change icon",
|
|
479
|
+
searchPlaceholder: "Search icons, plants, tables, code...",
|
|
480
|
+
emojiSection: "Emoji",
|
|
481
|
+
iconsSection: "Icons",
|
|
482
|
+
empty: "No matching icons",
|
|
483
|
+
clear: "Clear icon",
|
|
484
|
+
},
|
|
485
|
+
myShares: {
|
|
478
486
|
title: "My Shares",
|
|
479
487
|
subtitle: "Pages, conversations, tables, and apps you've published and shared.",
|
|
480
488
|
browseCommunity: "Browse Community",
|
|
@@ -1008,10 +1016,18 @@ export default {
|
|
|
1008
1016
|
tableShareLinkCopied: "表格分享链接已复制",
|
|
1009
1017
|
tableShareLinkGenerated: "表格分享链接已生成",
|
|
1010
1018
|
tableShareOpen: "打开分享页",
|
|
1011
|
-
tableShareLoadFailed: "读取表格分享状态失败",
|
|
1012
|
-
tableShareReplicationIssue: "跨服务器同步需要处理",
|
|
1013
|
-
tableShareReplicationLag: "跨服务器同步仍在进行中",
|
|
1014
|
-
|
|
1019
|
+
tableShareLoadFailed: "读取表格分享状态失败",
|
|
1020
|
+
tableShareReplicationIssue: "跨服务器同步需要处理",
|
|
1021
|
+
tableShareReplicationLag: "跨服务器同步仍在进行中",
|
|
1022
|
+
contentIcon: {
|
|
1023
|
+
change: "更改图标",
|
|
1024
|
+
searchPlaceholder: "搜索图标、花草、表格、代码...",
|
|
1025
|
+
emojiSection: "Emoji",
|
|
1026
|
+
iconsSection: "图标",
|
|
1027
|
+
empty: "没有找到匹配的图标",
|
|
1028
|
+
clear: "清除图标",
|
|
1029
|
+
},
|
|
1030
|
+
myShares: {
|
|
1015
1031
|
title: "我的分享",
|
|
1016
1032
|
subtitle: "你发布和分享出去的页面、对话、表格与应用。",
|
|
1017
1033
|
browseCommunity: "浏览社区",
|
|
@@ -1545,10 +1561,18 @@ export default {
|
|
|
1545
1561
|
tableShareLinkCopied: "表格分享連結已複製",
|
|
1546
1562
|
tableShareLinkGenerated: "表格分享連結已生成",
|
|
1547
1563
|
tableShareOpen: "開啟分享頁",
|
|
1548
|
-
tableShareLoadFailed: "讀取表格分享狀態失敗",
|
|
1549
|
-
tableShareReplicationIssue: "跨伺服器同步需要處理",
|
|
1550
|
-
tableShareReplicationLag: "跨伺服器同步仍在進行中",
|
|
1551
|
-
|
|
1564
|
+
tableShareLoadFailed: "讀取表格分享狀態失敗",
|
|
1565
|
+
tableShareReplicationIssue: "跨伺服器同步需要處理",
|
|
1566
|
+
tableShareReplicationLag: "跨伺服器同步仍在進行中",
|
|
1567
|
+
contentIcon: {
|
|
1568
|
+
change: "更改圖示",
|
|
1569
|
+
searchPlaceholder: "搜尋圖示、花草、表格、程式碼...",
|
|
1570
|
+
emojiSection: "Emoji",
|
|
1571
|
+
iconsSection: "圖示",
|
|
1572
|
+
empty: "沒有找到符合的圖示",
|
|
1573
|
+
clear: "清除圖示",
|
|
1574
|
+
},
|
|
1575
|
+
myShares: {
|
|
1552
1576
|
title: "我的分享",
|
|
1553
1577
|
subtitle: "你發布和分享出去的頁面、對話、表格與應用。",
|
|
1554
1578
|
browseCommunity: "瀏覽社群",
|
|
@@ -2090,10 +2114,18 @@ export default {
|
|
|
2090
2114
|
tableShareLinkCopied: "テーブル共有リンクをコピーしました",
|
|
2091
2115
|
tableShareLinkGenerated: "テーブル共有リンクを生成しました",
|
|
2092
2116
|
tableShareOpen: "共有ページを開く",
|
|
2093
|
-
tableShareLoadFailed: "テーブル共有状態の読み込みに失敗しました",
|
|
2094
|
-
tableShareReplicationIssue: "レプリケーションに対応が必要です",
|
|
2095
|
-
tableShareReplicationLag: "レプリケーションはまだ反映中です",
|
|
2096
|
-
|
|
2117
|
+
tableShareLoadFailed: "テーブル共有状態の読み込みに失敗しました",
|
|
2118
|
+
tableShareReplicationIssue: "レプリケーションに対応が必要です",
|
|
2119
|
+
tableShareReplicationLag: "レプリケーションはまだ反映中です",
|
|
2120
|
+
contentIcon: {
|
|
2121
|
+
change: "アイコンを変更",
|
|
2122
|
+
searchPlaceholder: "アイコン、植物、表、コードを検索...",
|
|
2123
|
+
emojiSection: "Emoji",
|
|
2124
|
+
iconsSection: "アイコン",
|
|
2125
|
+
empty: "一致するアイコンがありません",
|
|
2126
|
+
clear: "アイコンをクリア",
|
|
2127
|
+
},
|
|
2128
|
+
myShares: {
|
|
2097
2129
|
title: "マイ共有",
|
|
2098
2130
|
subtitle: "公開・共有したページ、会話、テーブル、アプリの一覧です。",
|
|
2099
2131
|
browseCommunity: "コミュニティを見る",
|
package/app/types/appSummary.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { deriveAppIdFromRouteKey, isAppRouteKey, resolveAppRouteKey } from "../utils/appKeys";
|
|
2
|
+
import type { ContentIcon } from "../../render/contentIcon/types";
|
|
2
3
|
|
|
3
4
|
export type AppVisibility = "private" | "unlisted" | "public";
|
|
4
5
|
export type AppDeployMode = "platform";
|
|
@@ -24,6 +25,7 @@ export interface AppSummary {
|
|
|
24
25
|
modifiedOn?: string;
|
|
25
26
|
visibility?: AppVisibility;
|
|
26
27
|
deployMode?: AppDeployMode;
|
|
28
|
+
icon?: ContentIcon | null;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export interface AppSummaryRecord {
|
|
@@ -39,6 +41,7 @@ export interface AppSummaryRecord {
|
|
|
39
41
|
updatedAt?: string | number;
|
|
40
42
|
createdAt?: string | number;
|
|
41
43
|
serverOrigin?: string;
|
|
44
|
+
icon?: ContentIcon | null;
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
const toTimestamp = (value: unknown): number => {
|
|
@@ -110,5 +113,6 @@ export function toAppSummary(
|
|
|
110
113
|
modifiedOn: toIsoString(record.updatedAt ?? record.createdAt),
|
|
111
114
|
visibility: record.visibility ?? "private",
|
|
112
115
|
deployMode: record.deployMode ?? "platform",
|
|
116
|
+
icon: record.icon ?? null,
|
|
113
117
|
};
|
|
114
118
|
}
|
|
@@ -16,6 +16,7 @@ export type DesktopAgentRuntimeTurnResult =
|
|
|
16
16
|
type RunDesktopAgentRuntimeTurnArgs = {
|
|
17
17
|
agentRef: string;
|
|
18
18
|
input: AgentRuntimeMessageContent;
|
|
19
|
+
runtimeContext?: Record<string, any> | null;
|
|
19
20
|
continueDialogId?: string;
|
|
20
21
|
cwd?: string;
|
|
21
22
|
fetchImpl?: typeof fetch;
|
|
@@ -30,6 +31,7 @@ function buildDesktopAgentRuntimeTurnBody(args: RunDesktopAgentRuntimeTurnArgs)
|
|
|
30
31
|
return {
|
|
31
32
|
agentRef: args.agentRef,
|
|
32
33
|
input: args.input,
|
|
34
|
+
...(args.runtimeContext ? { runtimeContext: args.runtimeContext } : {}),
|
|
33
35
|
...(continueDialogId ? { continueDialogId } : {}),
|
|
34
36
|
...(args.cwd ? { cwd: args.cwd } : {}),
|
|
35
37
|
};
|
package/auth/adminPermissions.ts
CHANGED
|
@@ -1,12 +1,45 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
export const ADMIN_PERMISSION_DEFINITIONS = [
|
|
2
|
+
{
|
|
3
|
+
key: "usageManagement",
|
|
4
|
+
label: "用量管理",
|
|
5
|
+
shortLabel: "用量",
|
|
6
|
+
description: "查看用量报表、模型与提供商用量相关数据",
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
key: "growthStats",
|
|
10
|
+
label: "增长统计",
|
|
11
|
+
shortLabel: "增长",
|
|
12
|
+
description: "查看 7 天/30 天活跃、新增用户与增长趋势",
|
|
13
|
+
},
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
export type AdminPermissionKey = (typeof ADMIN_PERMISSION_DEFINITIONS)[number]["key"];
|
|
17
|
+
|
|
18
|
+
export type AdminPermissions = Partial<Record<AdminPermissionKey, boolean>>;
|
|
19
|
+
|
|
20
|
+
const ADMIN_PERMISSION_KEYS = new Set<AdminPermissionKey>(
|
|
21
|
+
ADMIN_PERMISSION_DEFINITIONS.map((definition) => definition.key)
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export const isAdminPermissionKey = (value: string): value is AdminPermissionKey =>
|
|
25
|
+
ADMIN_PERMISSION_KEYS.has(value as AdminPermissionKey);
|
|
4
26
|
|
|
5
27
|
export const hasUsageManagementPermission = (value: unknown): boolean => {
|
|
28
|
+
return hasAdminPermission(value, "usageManagement");
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const hasGrowthStatsPermission = (value: unknown): boolean => {
|
|
32
|
+
return hasAdminPermission(value, "growthStats");
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const hasAdminPermission = (
|
|
36
|
+
value: unknown,
|
|
37
|
+
permissionKey: AdminPermissionKey
|
|
38
|
+
): boolean => {
|
|
6
39
|
if (!value || typeof value !== "object") return false;
|
|
7
40
|
const permissions = (value as { adminPermissions?: AdminPermissions | null })
|
|
8
41
|
.adminPermissions;
|
|
9
|
-
return permissions?.
|
|
42
|
+
return permissions?.[permissionKey] === true;
|
|
10
43
|
};
|
|
11
44
|
|
|
12
45
|
export const mergeAdminPermissions = (
|
|
@@ -16,3 +49,30 @@ export const mergeAdminPermissions = (
|
|
|
16
49
|
...(current && typeof current === "object" ? (current as AdminPermissions) : {}),
|
|
17
50
|
...patch,
|
|
18
51
|
});
|
|
52
|
+
|
|
53
|
+
export const parseAdminPermissionsPatch = (
|
|
54
|
+
value: unknown
|
|
55
|
+
):
|
|
56
|
+
| { ok: true; patch: Partial<AdminPermissions> }
|
|
57
|
+
| { ok: false; error: string } => {
|
|
58
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
59
|
+
return { ok: false, error: "admin permissions patch must be an object" };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const patch: Partial<AdminPermissions> = {};
|
|
63
|
+
for (const [key, rawValue] of Object.entries(value)) {
|
|
64
|
+
if (!isAdminPermissionKey(key)) {
|
|
65
|
+
return { ok: false, error: `Unsupported admin permission: ${key}` };
|
|
66
|
+
}
|
|
67
|
+
if (typeof rawValue !== "boolean") {
|
|
68
|
+
return { ok: false, error: `${key} must be boolean` };
|
|
69
|
+
}
|
|
70
|
+
patch[key] = rawValue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (Object.keys(patch).length === 0) {
|
|
74
|
+
return { ok: false, error: "admin permissions patch cannot be empty" };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { ok: true, patch };
|
|
78
|
+
};
|
package/auth/routes.ts
CHANGED
|
@@ -42,6 +42,11 @@ export const authRoutes = {
|
|
|
42
42
|
method: "GET" as const,
|
|
43
43
|
createPath: () => `${API_VERSION}/users/usage-report`,
|
|
44
44
|
},
|
|
45
|
+
growthReport: {
|
|
46
|
+
path: `${API_VERSION}/users/growth-report`,
|
|
47
|
+
method: "GET" as const,
|
|
48
|
+
createPath: () => `${API_VERSION}/users/growth-report`,
|
|
49
|
+
},
|
|
45
50
|
providerBillingHealth: {
|
|
46
51
|
path: `${API_VERSION}/users/provider-billing-health`,
|
|
47
52
|
method: "GET" as const,
|
package/chat/chat.locale.ts
CHANGED
|
@@ -16,6 +16,7 @@ export default {
|
|
|
16
16
|
compressionError: "Image compression failed. Sending original images.",
|
|
17
17
|
compressingImages: "Compressing images...",
|
|
18
18
|
copyContent: "Copy Content",
|
|
19
|
+
copyDiagnostics: "Copy diagnostics",
|
|
19
20
|
copyFailed: "Copy failed",
|
|
20
21
|
copySuccess: "Copied successfully",
|
|
21
22
|
createDialog: "Create Dialog",
|
|
@@ -230,6 +231,7 @@ export default {
|
|
|
230
231
|
compressionError: "图片压缩失败,将发送原始图片",
|
|
231
232
|
compressingImages: "正在压缩图片...",
|
|
232
233
|
copyContent: "复制内容",
|
|
234
|
+
copyDiagnostics: "复制诊断信息",
|
|
233
235
|
copyFailed: "复制失败",
|
|
234
236
|
copySuccess: "复制成功",
|
|
235
237
|
createDialog: "创建对话",
|
|
@@ -432,6 +434,7 @@ export default {
|
|
|
432
434
|
compressionError: "圖片壓縮失敗,將發送原始圖片",
|
|
433
435
|
compressingImages: "正在壓縮圖片...",
|
|
434
436
|
copyContent: "複製內容",
|
|
437
|
+
copyDiagnostics: "複製診斷資訊",
|
|
435
438
|
copyFailed: "複製失敗",
|
|
436
439
|
copySuccess: "複製成功",
|
|
437
440
|
createDialog: "創建對話",
|
|
@@ -623,6 +626,7 @@ export default {
|
|
|
623
626
|
compressionError: "画像圧縮に失敗しました。元の画像を送信します。",
|
|
624
627
|
compressingImages: "画像を圧縮中...",
|
|
625
628
|
copyContent: "コンテンツをコピー",
|
|
629
|
+
copyDiagnostics: "診断情報をコピー",
|
|
626
630
|
copyFailed: "コピーに失敗しました",
|
|
627
631
|
copySuccess: "コピーに成功しました",
|
|
628
632
|
createDialog: "対話を作成",
|
|
@@ -190,8 +190,13 @@ export const handleSendMessageAction = async (
|
|
|
190
190
|
});
|
|
191
191
|
|
|
192
192
|
return;
|
|
193
|
-
} catch (error) {
|
|
193
|
+
} catch (error: any) {
|
|
194
194
|
console.error("handleSendMessage failed:", error);
|
|
195
|
-
|
|
195
|
+
const errorMessage = error instanceof Error
|
|
196
|
+
? error.message
|
|
197
|
+
: typeof error === "string"
|
|
198
|
+
? error
|
|
199
|
+
: error?.message || error?.error || String(error);
|
|
200
|
+
return rejectWithValue(errorMessage);
|
|
196
201
|
}
|
|
197
202
|
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const FILE_CONTENT_RE = /(?:https?:\/\/[^/"'\s]+)?\/api\/v1\/db\/file\/content\/([^?#"'\s)]+)/g;
|
|
2
|
+
|
|
3
|
+
export type DialogAttachmentMessage = {
|
|
4
|
+
id?: string;
|
|
5
|
+
dbKey?: string;
|
|
6
|
+
content?: unknown;
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type DialogAttachmentCandidate = {
|
|
11
|
+
fileId: string;
|
|
12
|
+
fileDbKey: string | null;
|
|
13
|
+
size: number | null;
|
|
14
|
+
ownerType: string | null;
|
|
15
|
+
ownerId: string | null;
|
|
16
|
+
ownerDbKey: string | null;
|
|
17
|
+
source: string | null;
|
|
18
|
+
status: "delete" | "retain";
|
|
19
|
+
reason: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type DialogAttachmentPlan = {
|
|
23
|
+
dialogId: string;
|
|
24
|
+
messageCount: number;
|
|
25
|
+
referencedFileIds: string[];
|
|
26
|
+
candidates: DialogAttachmentCandidate[];
|
|
27
|
+
deleteCandidates: DialogAttachmentCandidate[];
|
|
28
|
+
retainedCandidates: DialogAttachmentCandidate[];
|
|
29
|
+
bytesToDelete: number;
|
|
30
|
+
metadataReadFailures: Array<{ fileId: string; error: string }>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function extractFileContentIds(value: unknown): string[] {
|
|
34
|
+
const ids = new Set<string>();
|
|
35
|
+
const visit = (input: unknown) => {
|
|
36
|
+
if (typeof input === "string") {
|
|
37
|
+
for (const match of input.matchAll(FILE_CONTENT_RE)) {
|
|
38
|
+
const fileId = decodeURIComponent(match[1] ?? "").trim();
|
|
39
|
+
if (fileId) ids.add(fileId);
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (Array.isArray(input)) {
|
|
44
|
+
for (const item of input) visit(item);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (input && typeof input === "object") {
|
|
48
|
+
for (const item of Object.values(input as Record<string, unknown>)) visit(item);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
visit(value);
|
|
52
|
+
return [...ids];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function messageDbKey(dialogId: string, message: DialogAttachmentMessage) {
|
|
56
|
+
if (typeof message.dbKey === "string" && message.dbKey.trim()) {
|
|
57
|
+
return message.dbKey.trim();
|
|
58
|
+
}
|
|
59
|
+
if (typeof message.id === "string" && message.id.trim()) {
|
|
60
|
+
return `dialog-${dialogId}-msg-${message.id.trim()}`;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function asText(value: unknown) {
|
|
66
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function buildDialogAttachmentPlan(args: {
|
|
70
|
+
dialogId: string;
|
|
71
|
+
ownerId?: string | null;
|
|
72
|
+
includeUserOwnedReferenced?: boolean;
|
|
73
|
+
messages: DialogAttachmentMessage[];
|
|
74
|
+
metadataByFileId: Record<string, any>;
|
|
75
|
+
metadataReadFailures?: Array<{ fileId: string; error: string }>;
|
|
76
|
+
}): DialogAttachmentPlan {
|
|
77
|
+
const messageKeys = new Set(
|
|
78
|
+
args.messages
|
|
79
|
+
.map((message) => messageDbKey(args.dialogId, message))
|
|
80
|
+
.filter((key): key is string => Boolean(key))
|
|
81
|
+
);
|
|
82
|
+
const referencedFileIds = [
|
|
83
|
+
...new Set(args.messages.flatMap((message) => extractFileContentIds(message))),
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const candidates = referencedFileIds.map<DialogAttachmentCandidate>((fileId) => {
|
|
87
|
+
const metadata = args.metadataByFileId[fileId];
|
|
88
|
+
const ownerType = asText(metadata?.ownerType);
|
|
89
|
+
const ownerId = asText(metadata?.ownerId);
|
|
90
|
+
const ownerDbKey = asText(metadata?.ownerDbKey);
|
|
91
|
+
const fileDbKey = asText(metadata?.dbKey);
|
|
92
|
+
const source = asText(metadata?.source);
|
|
93
|
+
const ownedByDialog = ownerType === "dialog" && ownerId === args.dialogId;
|
|
94
|
+
const ownedByMessage = Boolean(ownerDbKey && messageKeys.has(ownerDbKey));
|
|
95
|
+
const userOwnedReferenced =
|
|
96
|
+
args.includeUserOwnedReferenced === true &&
|
|
97
|
+
Boolean(args.ownerId) &&
|
|
98
|
+
ownerType === "user" &&
|
|
99
|
+
ownerId === args.ownerId;
|
|
100
|
+
const canDelete = Boolean(fileDbKey && (ownedByDialog || ownedByMessage || userOwnedReferenced));
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
fileId,
|
|
104
|
+
fileDbKey,
|
|
105
|
+
size: typeof metadata?.size === "number" && Number.isFinite(metadata.size) ? metadata.size : null,
|
|
106
|
+
ownerType,
|
|
107
|
+
ownerId,
|
|
108
|
+
ownerDbKey,
|
|
109
|
+
source,
|
|
110
|
+
status: canDelete ? "delete" : "retain",
|
|
111
|
+
reason: canDelete
|
|
112
|
+
? ownedByMessage
|
|
113
|
+
? "ownerDbKey matches a message in this dialog"
|
|
114
|
+
: userOwnedReferenced
|
|
115
|
+
? "user-owned file is referenced by this dialog and explicit referenced-attachment deletion is enabled"
|
|
116
|
+
: "ownerType/ownerId matches this dialog"
|
|
117
|
+
: fileDbKey
|
|
118
|
+
? "file ownership is not exclusive to this dialog"
|
|
119
|
+
: "file metadata not found",
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const deleteCandidates = candidates.filter((candidate) => candidate.status === "delete");
|
|
124
|
+
const retainedCandidates = candidates.filter((candidate) => candidate.status === "retain");
|
|
125
|
+
return {
|
|
126
|
+
dialogId: args.dialogId,
|
|
127
|
+
messageCount: args.messages.length,
|
|
128
|
+
referencedFileIds,
|
|
129
|
+
candidates,
|
|
130
|
+
deleteCandidates,
|
|
131
|
+
retainedCandidates,
|
|
132
|
+
bytesToDelete: deleteCandidates.reduce((sum, candidate) => sum + (candidate.size ?? 0), 0),
|
|
133
|
+
metadataReadFailures: args.metadataReadFailures ?? [],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -18,6 +18,11 @@ import { clearPlan } from "../../ai/agent/planSlice";
|
|
|
18
18
|
import { clearWorkflow } from "../../ai/workflow/workflowSlice";
|
|
19
19
|
|
|
20
20
|
import { scheduleDeleteReplication } from "../../database/actions/replication";
|
|
21
|
+
import {
|
|
22
|
+
buildDialogAttachmentPlan,
|
|
23
|
+
extractFileContentIds,
|
|
24
|
+
type DialogAttachmentMessage,
|
|
25
|
+
} from "./dialogAttachmentCleanup";
|
|
21
26
|
|
|
22
27
|
// 外部 Actions
|
|
23
28
|
import { updateTokensAction } from "./actions/updateTokensAction";
|
|
@@ -46,6 +51,99 @@ const collectKeys = async (prefix: string, db: any): Promise<string[]> => {
|
|
|
46
51
|
return keys;
|
|
47
52
|
};
|
|
48
53
|
|
|
54
|
+
const collectEntries = async (prefix: string, db: any): Promise<Array<[string, any]>> => {
|
|
55
|
+
const entries: Array<[string, any]> = [];
|
|
56
|
+
let iterator = db.iterator({
|
|
57
|
+
gte: prefix,
|
|
58
|
+
lte: prefix + "\uffff",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (iterator && typeof iterator.then === "function") {
|
|
62
|
+
iterator = await iterator;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for await (const [key, value] of iterator) {
|
|
66
|
+
entries.push([key as string, value]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return entries;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const dbGetOrNull = async (db: any, key: string) => {
|
|
73
|
+
if (!db || typeof db.get !== "function") return null;
|
|
74
|
+
try {
|
|
75
|
+
return await db.get(key);
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const readLocalFileMetadata = async (db: any, fileId: string) => {
|
|
82
|
+
const direct = await dbGetOrNull(db, fileId);
|
|
83
|
+
if (direct) return direct;
|
|
84
|
+
const index = await dbGetOrNull(db, createKey("file", "id", fileId));
|
|
85
|
+
const mainKey =
|
|
86
|
+
typeof index?.mainKey === "string" && index.mainKey.trim()
|
|
87
|
+
? index.mainKey.trim()
|
|
88
|
+
: null;
|
|
89
|
+
return mainKey ? await dbGetOrNull(db, mainKey) : null;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
type DeleteDialogPayload =
|
|
93
|
+
| string
|
|
94
|
+
| {
|
|
95
|
+
dialogKey: string;
|
|
96
|
+
includeAttachments?: boolean;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const normalizeDeleteDialogPayload = (payload: DeleteDialogPayload) =>
|
|
100
|
+
typeof payload === "string"
|
|
101
|
+
? { dialogKey: payload, includeAttachments: false }
|
|
102
|
+
: {
|
|
103
|
+
dialogKey: payload.dialogKey,
|
|
104
|
+
includeAttachments: payload.includeAttachments === true,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const deleteOwnedDialogAttachments = async (args: {
|
|
108
|
+
db: any;
|
|
109
|
+
dialogId: string;
|
|
110
|
+
entries: Array<[string, any]>;
|
|
111
|
+
thunkApi: any;
|
|
112
|
+
}) => {
|
|
113
|
+
const messages: DialogAttachmentMessage[] = args.entries.map(([dbKey, value]) => ({
|
|
114
|
+
...(value && typeof value === "object" ? value : {}),
|
|
115
|
+
dbKey,
|
|
116
|
+
}));
|
|
117
|
+
const fileIds = [...new Set(messages.flatMap((message) => extractFileContentIds(message)))];
|
|
118
|
+
const metadataByFileId: Record<string, any> = {};
|
|
119
|
+
const metadataReadFailures: Array<{ fileId: string; error: string }> = [];
|
|
120
|
+
|
|
121
|
+
for (const fileId of fileIds) {
|
|
122
|
+
const metadata = await readLocalFileMetadata(args.db, fileId);
|
|
123
|
+
if (metadata) {
|
|
124
|
+
metadataByFileId[fileId] = metadata;
|
|
125
|
+
} else {
|
|
126
|
+
metadataReadFailures.push({ fileId, error: "local file metadata not found" });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const plan = buildDialogAttachmentPlan({
|
|
131
|
+
dialogId: args.dialogId,
|
|
132
|
+
messages,
|
|
133
|
+
metadataByFileId,
|
|
134
|
+
metadataReadFailures,
|
|
135
|
+
});
|
|
136
|
+
if (!plan.deleteCandidates.length) return plan;
|
|
137
|
+
|
|
138
|
+
const { deleteFileAction } = await import("../../database/actions/deleteFile");
|
|
139
|
+
for (const candidate of plan.deleteCandidates) {
|
|
140
|
+
if (candidate.fileDbKey) {
|
|
141
|
+
await deleteFileAction(candidate.fileDbKey, args.thunkApi);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return plan;
|
|
145
|
+
};
|
|
146
|
+
|
|
49
147
|
const createSliceWithThunks = buildCreateSlice({
|
|
50
148
|
creators: { asyncThunk: asyncThunkCreator },
|
|
51
149
|
});
|
|
@@ -301,7 +399,8 @@ const dialogSlice = createSliceWithThunks({
|
|
|
301
399
|
),
|
|
302
400
|
|
|
303
401
|
deleteDialog: create.asyncThunk(
|
|
304
|
-
async (
|
|
402
|
+
async (payload: DeleteDialogPayload, thunkApi) => {
|
|
403
|
+
const { dialogKey, includeAttachments } = normalizeDeleteDialogPayload(payload);
|
|
305
404
|
const { dispatch, getState, extra } = thunkApi;
|
|
306
405
|
const { db } = extra;
|
|
307
406
|
const state = getState() as RootState;
|
|
@@ -319,8 +418,22 @@ const dialogSlice = createSliceWithThunks({
|
|
|
319
418
|
// --- 无论是否是当前对话,都从当前空间删除该内容 ---
|
|
320
419
|
await (dispatch as any)(remove(dialogKey));
|
|
321
420
|
const prefix = createKey("dialog", targetDialogId, "msg");
|
|
322
|
-
const
|
|
323
|
-
|
|
421
|
+
const deletedEntries = includeAttachments
|
|
422
|
+
? await collectEntries(prefix, db)
|
|
423
|
+
: [];
|
|
424
|
+
const attachmentPlan =
|
|
425
|
+
includeAttachments && targetDialogId
|
|
426
|
+
? await deleteOwnedDialogAttachments({
|
|
427
|
+
db,
|
|
428
|
+
dialogId: targetDialogId,
|
|
429
|
+
entries: deletedEntries,
|
|
430
|
+
thunkApi,
|
|
431
|
+
})
|
|
432
|
+
: null;
|
|
433
|
+
const deletedIds = includeAttachments
|
|
434
|
+
? deletedEntries.map(([key]) => key)
|
|
435
|
+
: await collectKeys(prefix, db);
|
|
436
|
+
if (!deletedIds.length) return { dialogKey, isCurrentDialog, attachmentPlan };
|
|
324
437
|
|
|
325
438
|
const ops = deletedIds.map((key) => ({ type: "del" as const, key }));
|
|
326
439
|
|
|
@@ -341,7 +454,7 @@ const dialogSlice = createSliceWithThunks({
|
|
|
341
454
|
dispatch(clearWorkflow());
|
|
342
455
|
}
|
|
343
456
|
|
|
344
|
-
return { dialogKey, isCurrentDialog };
|
|
457
|
+
return { dialogKey, isCurrentDialog, attachmentPlan };
|
|
345
458
|
},
|
|
346
459
|
{
|
|
347
460
|
fulfilled: (state, action) => {
|