nolo-cli 0.1.41 → 0.1.42

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.
Files changed (136) hide show
  1. package/README.md +27 -12
  2. package/agent-runtime/agentThread.ts +222 -0
  3. package/agent-runtime/agentThreadAdmission.ts +74 -0
  4. package/agent-runtime/dialogWritePlan.ts +42 -0
  5. package/agent-runtime/hostAdapter.ts +1 -0
  6. package/agent-runtime/index.ts +43 -0
  7. package/agent-runtime/localLoop.ts +31 -2
  8. package/agent-runtime/localToolPolicy.ts +1 -0
  9. package/agent-runtime/localWorkspaceTools.ts +1258 -78
  10. package/agent-runtime/noloWorkspaceTools.ts +20 -1
  11. package/agent-runtime/runtimeToolPolicy.ts +1 -5
  12. package/agent-runtime/runtimeToolSurface.ts +1 -0
  13. package/agentAliases.ts +29 -9
  14. package/agentInternalCommandEntries.ts +11 -3
  15. package/agentMachineCommands.ts +1 -0
  16. package/agentRecordCommands.ts +2 -2
  17. package/agentRecordHelpers.ts +59 -6
  18. package/agentRunCommand.ts +84 -19
  19. package/ai/agent/buildSystemPrompt.ts +14 -0
  20. package/ai/agent/cliExecutor.ts +202 -43
  21. package/ai/agent/createAgentSchema.ts +2 -2
  22. package/ai/agent/liveAudioModel.ts +1 -1
  23. package/ai/agent/streamAgentChatTurn.ts +154 -20
  24. package/ai/agent/streamAgentChatTurnUtils.ts +63 -0
  25. package/ai/ai.locale.ts +70 -0
  26. package/ai/chat/inlineImageUrlsForCustomProvider.ts +7 -2
  27. package/ai/chat/sendOpenAICompletionsRequest.ts +25 -1
  28. package/ai/llm/kimi.ts +0 -1
  29. package/ai/llm/openrouterModels.ts +27 -2
  30. package/ai/memory/policy.ts +49 -0
  31. package/ai/memory/queryShared.ts +10 -4
  32. package/ai/memory/recentRelationshipRecap.ts +4 -0
  33. package/ai/memory/remember.ts +48 -104
  34. package/ai/memory/scope.ts +74 -0
  35. package/ai/memory/types.ts +6 -0
  36. package/ai/memory/understanding.ts +10 -18
  37. package/ai/policy/runtimePolicy.ts +95 -23
  38. package/ai/tools/agent/agentTools.ts +0 -21
  39. package/ai/tools/agent/startAgentDialogTool.ts +2 -1
  40. package/ai/tools/index.ts +2 -20
  41. package/ai/tools/prepareTools.ts +171 -1
  42. package/ai/tools/readXhsProfileTool.ts +80 -69
  43. package/ai/tools/table/createTableTool.ts +11 -0
  44. package/ai/tools/toolPacks.ts +19 -0
  45. package/ai/types.ts +3 -0
  46. package/app/favorite/deletedFavoriteProjection.ts +31 -0
  47. package/app/hooks/deleteDbKey.ts +24 -3
  48. package/app/i18n/translations/interface.locale.ts +48 -16
  49. package/app/types/appSummary.ts +4 -0
  50. package/app/utils/desktopAgentRuntimeTurnClient.ts +2 -0
  51. package/auth/adminPermissions.ts +64 -4
  52. package/auth/routes.ts +5 -0
  53. package/chat/chat.locale.ts +4 -0
  54. package/chat/dialog/actions/handleSendMessageAction.ts +7 -2
  55. package/chat/dialog/dialogAttachmentCleanup.ts +135 -0
  56. package/chat/dialog/dialogSlice.ts +117 -4
  57. package/chat/messages/activityCompletion.ts +97 -0
  58. package/chat/messages/imagePayloadPersistence.ts +22 -0
  59. package/chat/messages/messageContent.ts +20 -16
  60. package/chat/messages/messageSlice.ts +20 -1
  61. package/chat/messages/toolThunks.ts +32 -11
  62. package/chat/messages/types.ts +8 -1
  63. package/chat/messages/web/toolDisplayName.ts +686 -0
  64. package/cli/agentAliases.ts +29 -9
  65. package/cli/agentInternalCommandEntries.ts +11 -3
  66. package/cli/agentMachineCommands.ts +1 -0
  67. package/cli/agentRecordCommands.ts +2 -2
  68. package/cli/agentRecordHelpers.ts +59 -6
  69. package/cli/agentRunCommand.ts +84 -19
  70. package/cli/client/agentRun.ts +65 -253
  71. package/cli/client/localRuntimeAdapter.ts +179 -53
  72. package/cli/commandRegistry.ts +7 -4
  73. package/cli/dialogAttachmentCleanup.ts +151 -0
  74. package/cli/dialogCommands.ts +163 -61
  75. package/cli/dialogInternalCommandEntries.ts +1 -1
  76. package/cli/docCreateCommands.ts +345 -0
  77. package/cli/docInternalCommandEntries.ts +11 -0
  78. package/cli/docPageHelpers.ts +80 -0
  79. package/cli/docSkillHelpers.ts +67 -0
  80. package/cli/docSpaceHelpers.ts +46 -0
  81. package/cli/internalCommandEntries.ts +4 -0
  82. package/cli/machineCommands.ts +18 -34
  83. package/cli/memoryCommands.ts +253 -0
  84. package/cli/memoryInternalCommandEntries.ts +9 -0
  85. package/cli/qoderUsageProbe.ts +252 -0
  86. package/cli/scriptCommandEntries.ts +0 -3
  87. package/cli/secretScan.ts +31 -0
  88. package/cli/tableCommands.ts +248 -13
  89. package/cli/workflowInternalCommandEntries.ts +2 -3
  90. package/client/agentRun.test.ts +68 -84
  91. package/client/agentRun.ts +65 -253
  92. package/client/localRuntimeAdapter.test.ts +505 -74
  93. package/client/localRuntimeAdapter.ts +179 -53
  94. package/commandRegistry.ts +7 -4
  95. package/connector-experimental/capabilities.ts +10 -9
  96. package/create/space/sidebarVisibleTypes.ts +19 -0
  97. package/dialogAttachmentCleanup.ts +151 -0
  98. package/dialogCommands.ts +163 -61
  99. package/dialogInternalCommandEntries.ts +1 -1
  100. package/docCreateCommands.ts +345 -0
  101. package/docInternalCommandEntries.ts +11 -0
  102. package/docPageHelpers.ts +80 -0
  103. package/docSkillHelpers.ts +67 -0
  104. package/docSpaceHelpers.ts +46 -0
  105. package/integrations/google/models.ts +4 -4
  106. package/integrations/openai/generateOpenAIRequestBody.ts +0 -3
  107. package/integrations/xhs-reader/types.ts +36 -3
  108. package/integrations/xhs-reader/url.ts +24 -1
  109. package/internalCommandEntries.ts +4 -0
  110. package/machineCommands.ts +18 -34
  111. package/memoryCommands.ts +253 -0
  112. package/memoryInternalCommandEntries.ts +9 -0
  113. package/package.json +133 -114
  114. package/qoderUsageProbe.ts +252 -0
  115. package/render/contentIcon/types.ts +167 -0
  116. package/render/page/docSlice.ts +30 -3
  117. package/render/page/types.ts +2 -0
  118. package/render/table/createTableAction.ts +4 -0
  119. package/render/table/tableSlice.ts +61 -0
  120. package/render/table/types.ts +10 -0
  121. package/scriptCommandEntries.ts +0 -3
  122. package/secretScan.ts +31 -0
  123. package/server/handlers/agentRun/types.ts +24 -23
  124. package/tableCommands.ts +248 -13
  125. package/workflowInternalCommandEntries.ts +2 -3
  126. package/ai/tools/agent/taskRunTool.ts +0 -112
  127. package/cli/client/taskRunPrompt.ts +0 -27
  128. package/cli/taskRunCommand.ts +0 -265
  129. package/client/taskRunPrompt.ts +0 -27
  130. package/integrations/xhs-reader/analyze/profileAnalyzer.ts +0 -205
  131. package/integrations/xhs-reader/backends/playwrightProfileCollector.ts +0 -624
  132. package/integrations/xhs-reader/bridge/readXhsProfileWithBridge.ts +0 -227
  133. package/integrations/xhs-reader/client/xhsWebApiClient.ts +0 -229
  134. package/integrations/xhs-reader/normalize.ts +0 -390
  135. package/integrations/xhs-reader/orchestrator.ts +0 -178
  136. package/taskRunCommand.ts +0 -265
@@ -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)(deleteDialog(contentKey)).unwrap();
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(dispatch, contentKey, preferredServerOrigin);
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
- myShares: {
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
- myShares: {
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
- myShares: {
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
- myShares: {
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: "コミュニティを見る",
@@ -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
  };
@@ -1,12 +1,45 @@
1
- export type AdminPermissions = {
2
- usageManagement?: boolean;
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?.usageManagement === true;
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,
@@ -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
- return rejectWithValue((error as Error).message);
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 (dialogKey: string, thunkApi) => {
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 deletedIds = await collectKeys(prefix, db);
323
- if (!deletedIds.length) return { ids: [] };
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) => {