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
@@ -0,0 +1,252 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+
5
+ export type QoderQuotaUsage = {
6
+ userId: string | null;
7
+ userType: string | null;
8
+ expiresAt: string | null;
9
+ expiresAtMs: number | null;
10
+ isQuotaExceeded: boolean | null;
11
+ totalUsagePercentage: number | null;
12
+ quota: {
13
+ total: number | null;
14
+ used: number | null;
15
+ remaining: number | null;
16
+ percentage: number | null;
17
+ unit: string | null;
18
+ };
19
+ source: "qoder-cli-log";
20
+ logPath?: string;
21
+ capturedAt?: string;
22
+ capturedAtMs?: number | null;
23
+ };
24
+
25
+ export type QoderUsageFreshness = "fresh" | "stale" | "uncertain";
26
+
27
+ export type QoderUsageProbeResult = {
28
+ ok: boolean;
29
+ provider: "qoder";
30
+ usage?: (QoderQuotaUsage & { logPath: string });
31
+ limitations?: string[];
32
+ freshness?: QoderUsageFreshness;
33
+ ageSeconds?: number | null;
34
+ staleThresholdSeconds?: number;
35
+ scannedLogCount?: number;
36
+ reason?: string;
37
+ error?: string;
38
+ };
39
+
40
+ const QUOTA_RESPONSE_MARKER = "/api/v2/quota/usage response:";
41
+
42
+ const asFiniteNumber = (value: unknown): number | null =>
43
+ typeof value === "number" && Number.isFinite(value) ? value : null;
44
+
45
+ const asString = (value: unknown): string | null =>
46
+ typeof value === "string" && value.trim() ? value.trim() : null;
47
+
48
+ const asBoolean = (value: unknown): boolean | null =>
49
+ typeof value === "boolean" ? value : null;
50
+
51
+ function normalizeQuotaPayload(payload: any): QoderQuotaUsage | null {
52
+ if (!payload || typeof payload !== "object") return null;
53
+ const quota = payload.userQuota && typeof payload.userQuota === "object"
54
+ ? payload.userQuota
55
+ : {};
56
+ const expiresAtMs = asFiniteNumber(payload.expiresAt);
57
+ return {
58
+ userId: asString(payload.userId),
59
+ userType: asString(payload.userType),
60
+ expiresAt: expiresAtMs != null ? new Date(expiresAtMs).toISOString() : null,
61
+ expiresAtMs,
62
+ isQuotaExceeded: asBoolean(payload.isQuotaExceeded),
63
+ totalUsagePercentage: asFiniteNumber(payload.totalUsagePercentage),
64
+ quota: {
65
+ total: asFiniteNumber(quota.total),
66
+ used: asFiniteNumber(quota.used),
67
+ remaining: asFiniteNumber(quota.remaining),
68
+ percentage: asFiniteNumber(quota.percentage),
69
+ unit: asString(quota.unit),
70
+ },
71
+ source: "qoder-cli-log",
72
+ };
73
+ }
74
+
75
+ function isRealQuotaResponseLine(line: string): boolean {
76
+ const markerIndex = line.indexOf(QUOTA_RESPONSE_MARKER);
77
+ if (markerIndex < 0) return false;
78
+ const before = line.slice(0, markerIndex);
79
+ if (!before.includes("[qoderApi]")) return false;
80
+ const jsonStart = markerIndex + QUOTA_RESPONSE_MARKER.length;
81
+ const after = line.slice(jsonStart).trimStart();
82
+ return after.startsWith("{");
83
+ }
84
+
85
+ export function parseQoderQuotaUsageFromText(text: string): QoderQuotaUsage | null {
86
+ let latest: QoderQuotaUsage | null = null;
87
+ for (const line of text.split(/\r?\n/)) {
88
+ if (!isRealQuotaResponseLine(line)) continue;
89
+ const markerIndex = line.indexOf(QUOTA_RESPONSE_MARKER);
90
+ const jsonText = line.slice(markerIndex + QUOTA_RESPONSE_MARKER.length).trim();
91
+ try {
92
+ const parsed = normalizeQuotaPayload(JSON.parse(jsonText));
93
+ if (parsed) latest = parsed;
94
+ } catch {
95
+ // Ignore malformed historical log lines.
96
+ }
97
+ }
98
+ return latest;
99
+ }
100
+
101
+ export function extractLeadingIsoTimestamp(dirName: string): string | null {
102
+ const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})(?:-(\d{3}))?([+-])(\d{2})-(\d{2})/.exec(dirName);
103
+ if (!match) return null;
104
+ const [, Y, M, D, h, m, s, ms, sign, offH, offM] = match;
105
+ const local = Date.UTC(Number(Y), Number(M) - 1, Number(D), Number(h), Number(m), Number(s), Number(ms ?? 0));
106
+ const offsetMs = (Number(offH) * 60 + Number(offM)) * 60_000;
107
+ const utcMs = sign === "+" ? local - offsetMs : local + offsetMs;
108
+ if (!Number.isFinite(utcMs)) return null;
109
+ return new Date(utcMs).toISOString();
110
+ }
111
+
112
+ type RunLogEntry = {
113
+ logPath: string;
114
+ capturedAtMs: number | null;
115
+ capturedAt: string | null;
116
+ dirName: string;
117
+ };
118
+
119
+ function listQoderRunLogs(qoderHome: string): RunLogEntry[] {
120
+ const runsDir = join(qoderHome, "logs", "runs");
121
+ if (!existsSync(runsDir)) return [];
122
+ const entries: RunLogEntry[] = [];
123
+ for (const entry of readdirSync(runsDir, { withFileTypes: true })) {
124
+ if (!entry.isDirectory()) continue;
125
+ const logPath = join(runsDir, entry.name, "qodercli.log");
126
+ if (!existsSync(logPath)) continue;
127
+ const capturedAt = extractLeadingIsoTimestamp(entry.name);
128
+ const capturedAtMs = capturedAt ? Date.parse(capturedAt) : null;
129
+ entries.push({
130
+ logPath,
131
+ capturedAtMs,
132
+ capturedAt,
133
+ dirName: entry.name,
134
+ });
135
+ }
136
+ entries.sort((a, b) => {
137
+ const aMs = a.capturedAtMs ?? statSync(a.logPath).mtimeMs;
138
+ const bMs = b.capturedAtMs ?? statSync(b.logPath).mtimeMs;
139
+ return bMs - aMs;
140
+ });
141
+ return entries;
142
+ }
143
+
144
+ export function findLatestQoderQuotaUsage(options: {
145
+ qoderHome?: string;
146
+ now?: number;
147
+ } = {}): (QoderQuotaUsage & { logPath: string; capturedAtMs: number | null; capturedAt: string | null }) | null {
148
+ const qoderHome = options.qoderHome || join(homedir(), ".qoder");
149
+ for (const run of listQoderRunLogs(qoderHome)) {
150
+ const parsed = parseQoderQuotaUsageFromText(readFileSync(run.logPath, "utf8"));
151
+ if (!parsed) continue;
152
+ return {
153
+ ...parsed,
154
+ logPath: run.logPath,
155
+ capturedAt: run.capturedAt,
156
+ capturedAtMs: run.capturedAtMs,
157
+ };
158
+ }
159
+ return null;
160
+ }
161
+
162
+ const DEFAULT_STALE_THRESHOLD_SECONDS = 6 * 60 * 60;
163
+
164
+ export function classifyQoderUsageFreshness(
165
+ usage: { capturedAtMs: number | null } | null,
166
+ options: { now?: number; staleThresholdSeconds?: number } = {},
167
+ ): QoderUsageFreshness {
168
+ if (!usage || usage.capturedAtMs == null) return "uncertain";
169
+ const now = options.now ?? Date.now();
170
+ const threshold = (options.staleThresholdSeconds ?? DEFAULT_STALE_THRESHOLD_SECONDS) * 1000;
171
+ const age = now - usage.capturedAtMs;
172
+ if (!Number.isFinite(age) || age < 0) return "uncertain";
173
+ return age > threshold ? "stale" : "fresh";
174
+ }
175
+
176
+ function readOption(args: string[], flag: string) {
177
+ const index = args.indexOf(flag);
178
+ return index >= 0 ? args[index + 1] : undefined;
179
+ }
180
+
181
+ export async function runQoderUsageCommand(
182
+ args: string[],
183
+ deps: {
184
+ env?: Record<string, string | undefined>;
185
+ output?: { write(chunk: string): unknown };
186
+ now?: () => number;
187
+ } = {},
188
+ ) {
189
+ const output = deps.output ?? process.stdout;
190
+ const agent = args[0]?.trim();
191
+ if (!agent || agent === "--help" || agent === "-h") {
192
+ output.write("Usage: nolo agent usage qoder [--qoder-home <path>] [--stale-threshold-seconds <n>]\n");
193
+ return agent ? 0 : 1;
194
+ }
195
+ if (agent.toLowerCase() !== "qoder") {
196
+ output.write(`[nolo] agent usage currently supports qoder only, received ${agent}.\n`);
197
+ return 1;
198
+ }
199
+ const qoderHome =
200
+ readOption(args, "--qoder-home") ||
201
+ deps.env?.QODER_HOME ||
202
+ join(homedir(), ".qoder");
203
+ const now = deps.now ? deps.now() : Date.now();
204
+ const thresholdArg = readOption(args, "--stale-threshold-seconds");
205
+ const staleThresholdSeconds = thresholdArg ? Number(thresholdArg) : DEFAULT_STALE_THRESHOLD_SECONDS;
206
+ const usage = findLatestQoderQuotaUsage({ qoderHome, now });
207
+ const freshness = classifyQoderUsageFreshness(usage, { now, staleThresholdSeconds });
208
+ const ageSeconds = usage?.capturedAtMs != null ? Math.max(0, Math.floor((now - usage.capturedAtMs) / 1000)) : null;
209
+
210
+ if (!usage) {
211
+ const result: QoderUsageProbeResult = {
212
+ ok: false,
213
+ provider: "qoder",
214
+ freshness,
215
+ ageSeconds,
216
+ staleThresholdSeconds,
217
+ limitations: [
218
+ "This probe only reads Qoder package Credits from local quota logs.",
219
+ "It does not currently read the Qwen3.7-Max daily free request quota; check Qoder's Usage Overview or CLI /usage for that counter.",
220
+ ],
221
+ error:
222
+ "No Qoder quota usage log found. Open `qoder`, run `/usage`, then retry this command.",
223
+ reason:
224
+ "Probe could not locate any real [qoderApi] quota response line in local run logs.",
225
+ };
226
+ output.write(JSON.stringify(result, null, 2));
227
+ output.write("\n");
228
+ return 1;
229
+ }
230
+
231
+ const result: QoderUsageProbeResult = {
232
+ ok: true,
233
+ provider: "qoder",
234
+ usage,
235
+ limitations: [
236
+ "This is package Credits usage only, parsed from Qoder's /api/v2/quota/usage log.",
237
+ "Qwen3.7-Max daily free requests are a separate counter and are not exposed in this probe's current log source.",
238
+ ],
239
+ freshness,
240
+ ageSeconds,
241
+ staleThresholdSeconds,
242
+ };
243
+ if (freshness !== "fresh") {
244
+ result.reason =
245
+ freshness === "stale"
246
+ ? `Latest local quota log is ${ageSeconds}s old (threshold ${staleThresholdSeconds}s); UI or server may show a different value.`
247
+ : "Latest local quota log has no reliable capture timestamp; treat numbers as uncertain.";
248
+ }
249
+ output.write(JSON.stringify(result, null, 2));
250
+ output.write("\n");
251
+ return freshness === "fresh" ? 0 : 2;
252
+ }
@@ -0,0 +1,167 @@
1
+ export type ContentIcon =
2
+ | { kind: "emoji"; value: string }
3
+ | {
4
+ kind: "lucide";
5
+ value:
6
+ | "file-text"
7
+ | "table"
8
+ | "layout-grid"
9
+ | "star"
10
+ | "book"
11
+ | "image"
12
+ | "folder"
13
+ | "folder-kanban"
14
+ | "kanban"
15
+ | "layers"
16
+ | "notebook"
17
+ | "clipboard-list"
18
+ | "briefcase"
19
+ | "calendar"
20
+ | "clock"
21
+ | "list-todo"
22
+ | "chart-bar"
23
+ | "chart-line"
24
+ | "database"
25
+ | "code"
26
+ | "file-code"
27
+ | "terminal"
28
+ | "settings"
29
+ | "wrench"
30
+ | "pen-tool"
31
+ | "palette"
32
+ | "sparkles"
33
+ | "brain"
34
+ | "bot"
35
+ | "message-square"
36
+ | "users"
37
+ | "globe"
38
+ | "link"
39
+ | "bookmark"
40
+ | "heart"
41
+ | "flag"
42
+ | "zap"
43
+ | "lightbulb"
44
+ | "search"
45
+ | "map"
46
+ | "shopping-bag"
47
+ | "music"
48
+ | "video"
49
+ | "blocks"
50
+ | "flower"
51
+ | "flower-2"
52
+ | "leaf"
53
+ | "sprout"
54
+ | "tree-pine"
55
+ | "trees"
56
+ | "clover"
57
+ | "vegan"
58
+ | "bean"
59
+ | "wheat"
60
+ | "apple"
61
+ | "sun"
62
+ | "cloud-sun"
63
+ | "cloud-rain"
64
+ | "droplets"
65
+ | "mountain"
66
+ | "earth"
67
+ | "shell"
68
+ | "archive"
69
+ | "award"
70
+ | "badge-check"
71
+ | "bell"
72
+ | "box"
73
+ | "boxes"
74
+ | "cake"
75
+ | "camera"
76
+ | "circle-dollar-sign"
77
+ | "circle-help"
78
+ | "coffee"
79
+ | "compass"
80
+ | "cpu"
81
+ | "crown"
82
+ | "diamond"
83
+ | "dumbbell"
84
+ | "file-archive"
85
+ | "file-image"
86
+ | "file-spreadsheet"
87
+ | "file-type"
88
+ | "flask-conical"
89
+ | "gamepad-2"
90
+ | "gift"
91
+ | "graduation-cap"
92
+ | "hammer"
93
+ | "handshake"
94
+ | "headphones"
95
+ | "house"
96
+ | "key"
97
+ | "languages"
98
+ | "laptop"
99
+ | "library"
100
+ | "mail"
101
+ | "megaphone"
102
+ | "mic"
103
+ | "monitor"
104
+ | "moon"
105
+ | "newspaper"
106
+ | "package"
107
+ | "paperclip"
108
+ | "plane"
109
+ | "puzzle"
110
+ | "route"
111
+ | "shield"
112
+ | "smile"
113
+ | "tags"
114
+ | "target"
115
+ | "telescope"
116
+ | "ticket"
117
+ | "trophy"
118
+ | "wallet"
119
+ | "workflow"
120
+ | "alarm-clock"
121
+ | "atom"
122
+ | "banknote"
123
+ | "battery-charging"
124
+ | "bluetooth"
125
+ | "bug"
126
+ | "car"
127
+ | "circle-user"
128
+ | "cloud"
129
+ | "command"
130
+ | "construction"
131
+ | "contact"
132
+ | "copy"
133
+ | "download"
134
+ | "eraser"
135
+ | "film"
136
+ | "fingerprint"
137
+ | "flame"
138
+ | "gem"
139
+ | "hard-drive"
140
+ | "hospital"
141
+ | "map-pin"
142
+ | "medal"
143
+ | "mouse-pointer-2"
144
+ | "network"
145
+ | "paintbrush"
146
+ | "pencil"
147
+ | "printer"
148
+ | "qr-code"
149
+ | "radar"
150
+ | "receipt"
151
+ | "recycle"
152
+ | "scan-line"
153
+ | "school"
154
+ | "server"
155
+ | "ship"
156
+ | "siren"
157
+ | "sliders-horizontal"
158
+ | "smartphone"
159
+ | "snowflake"
160
+ | "store"
161
+ | "sunrise"
162
+ | "sunset"
163
+ | "timer"
164
+ | "truck"
165
+ | "umbrella"
166
+ | "wifi";
167
+ };
@@ -14,6 +14,7 @@ import type { EditorContent } from "../../create/editor/utils/slateUtils";
14
14
  import { DataType } from "../../create/types";
15
15
  import { PageData } from "./types";
16
16
  import type { PageSkillMetadata } from "../../ai/skills/skillDocProtocol";
17
+ import type { ContentIcon } from "../contentIcon/types";
17
18
 
18
19
  // —— State 接口 ——
19
20
  export interface DocState {
@@ -22,6 +23,7 @@ export interface DocState {
22
23
  title: string | null;
23
24
  dbSpaceId: string | null;
24
25
  tags: string[] | null;
26
+ icon: ContentIcon | null;
25
27
  isReadOnly: boolean;
26
28
  isLoading: boolean;
27
29
  isInitialized: boolean;
@@ -32,6 +34,7 @@ export interface DocState {
32
34
  lastSavedAt: string | null;
33
35
  lastSavedSlateData: EditorContent | null;
34
36
  lastSavedTitle: string | null;
37
+ lastSavedIcon: ContentIcon | null;
35
38
  justSaved: boolean;
36
39
  tools: string[] | null;
37
40
  meta: PageSkillMetadata | null;
@@ -56,6 +59,7 @@ const initialState: DocState = {
56
59
  title: null,
57
60
  dbSpaceId: null,
58
61
  tags: null,
62
+ icon: null,
59
63
  isReadOnly: true,
60
64
  isLoading: false,
61
65
  isInitialized: false,
@@ -66,6 +70,7 @@ const initialState: DocState = {
66
70
  lastSavedAt: null,
67
71
  lastSavedSlateData: null,
68
72
  lastSavedTitle: null,
73
+ lastSavedIcon: null,
69
74
  justSaved: false,
70
75
  tools: null,
71
76
  meta: null,
@@ -173,6 +178,8 @@ export const docSlice = createSliceWithThunks({
173
178
  state.lastSavedTitle = action.payload.title || null;
174
179
  state.dbSpaceId = action.payload.spaceId || null;
175
180
  state.tags = action.payload.tags || null;
181
+ state.icon = action.payload.icon || null;
182
+ state.lastSavedIcon = action.payload.icon || null;
176
183
  state.isReadOnly = action.payload.isReadOnly;
177
184
  state.pageKey = action.payload.dbKey;
178
185
  state.id = action.payload.id;
@@ -209,11 +216,18 @@ export const docSlice = createSliceWithThunks({
209
216
  }
210
217
  }),
211
218
 
219
+ updateIcon: create.reducer((state, action: PayloadAction<ContentIcon | null>) => {
220
+ if (state.isInitialized && !state.isReadOnly) {
221
+ state.icon = action.payload;
222
+ state.justSaved = false;
223
+ }
224
+ }),
225
+
212
226
  saveDoc: create.asyncThunk(
213
227
  async (arg: SaveDocArgs, { dispatch, getState, rejectWithValue }) => {
214
228
  const requestedPageKey = arg.pageKey;
215
229
  const state = (getState() as any).doc;
216
- const { pageKey, slateData, dbSpaceId, meta } = state;
230
+ const { pageKey, slateData, dbSpaceId, meta, icon } = state;
217
231
 
218
232
  if (!pageKey || pageKey !== requestedPageKey) {
219
233
  return rejectWithValue("内容已切换,取消保存");
@@ -253,6 +267,7 @@ export const docSlice = createSliceWithThunks({
253
267
  title,
254
268
  tools,
255
269
  content,
270
+ icon: icon ?? null,
256
271
  ...(meta ? { meta } : {}),
257
272
  },
258
273
  })
@@ -299,6 +314,7 @@ export const docSlice = createSliceWithThunks({
299
314
  state.content = action.payload.content;
300
315
  state.lastSavedSlateData = action.payload.savedContent;
301
316
  state.lastSavedTitle = action.payload.title;
317
+ state.lastSavedIcon = state.icon;
302
318
  state.justSaved = true;
303
319
  },
304
320
  rejected: (state, action) => {
@@ -345,6 +361,8 @@ export const docSlice = createSliceWithThunks({
345
361
  state.type = action.payload.type || DataType.DOC;
346
362
  state.lastSavedSlateData = action.payload.slateData;
347
363
  state.tags = action.payload.tags || null;
364
+ state.icon = action.payload.icon || null;
365
+ state.lastSavedIcon = action.payload.icon || null;
348
366
  state.dbSpaceId = action.payload.spaceId;
349
367
  state.content = action.payload.content || null;
350
368
  state.meta = action.payload.meta || null;
@@ -357,6 +375,7 @@ export const {
357
375
  initDoc,
358
376
  updateSlate,
359
377
  updateTitle,
378
+ updateIcon,
360
379
  saveDoc,
361
380
  resetJustSavedStatus,
362
381
  setDocFocusContext,
@@ -422,14 +441,17 @@ export const selectHasPendingChanges = createSelector(
422
441
  (state: any) => state.doc.lastSavedSlateData,
423
442
  selectDocTitle,
424
443
  (state: any) => state.doc.lastSavedTitle,
444
+ (state: any) => state.doc.icon,
445
+ (state: any) => state.doc.lastSavedIcon,
425
446
  selectIsReadOnly,
426
447
  selectDocIsInitialized,
427
448
  ],
428
- (slateData, lastSavedSlateData, title, lastSavedTitle, isReadOnly, isInitialized) => {
449
+ (slateData, lastSavedSlateData, title, lastSavedTitle, icon, lastSavedIcon, isReadOnly, isInitialized) => {
429
450
  if (!isInitialized || isReadOnly) return false;
430
451
  return (
431
452
  hasSlateContentChanged(slateData, lastSavedSlateData) ||
432
- (title || "") !== (lastSavedTitle || "")
453
+ (title || "") !== (lastSavedTitle || "") ||
454
+ JSON.stringify(icon ?? null) !== JSON.stringify(lastSavedIcon ?? null)
433
455
  );
434
456
  }
435
457
  );
@@ -454,6 +476,11 @@ export const selectDocTags = createSelector(
454
476
  (doc) => doc.tags
455
477
  );
456
478
 
479
+ export const selectDocIcon = createSelector(
480
+ [selectDocState],
481
+ (doc) => doc.icon
482
+ );
483
+
457
484
  export const selectDocFocusContext = createSelector(
458
485
  [selectDocState],
459
486
  (doc) => doc.focusContext
@@ -1,5 +1,6 @@
1
1
  import { DataType } from "../../create/types"; // 确认路径
2
2
  import type { PageSkillMetadata } from "../../ai/skills/skillDocProtocol";
3
+ import type { ContentIcon } from "../contentIcon/types";
3
4
 
4
5
  export interface PageData {
5
6
  id: string; // 通常与 pageKey 中的 id 部分相同
@@ -10,6 +11,7 @@ export interface PageData {
10
11
  slateData?: any | null; // 文档真源
11
12
  spaceId: string | null; // 页面所属的 spaceId, null 表示不在任何空间
12
13
  tags?: string[]; // 页面的标签
14
+ icon?: ContentIcon | null;
13
15
  created: string; // ISO 格式创建时间
14
16
  updated_at?: string; // ISO 格式更新时间 (可选)
15
17
  tools?: string[]; // 关联的工具列表
@@ -37,6 +37,7 @@ import type {
37
37
  export interface CreateTableArgs {
38
38
  spaceId?: string;
39
39
  title?: string;
40
+ purpose?: string;
40
41
  description?: string;
41
42
  tags?: string[];
42
43
  categoryId?: string;
@@ -145,6 +146,7 @@ export const createTableAction = async (
145
146
  {
146
147
  spaceId: customSpaceId,
147
148
  title: customTitle,
149
+ purpose: customPurpose,
148
150
  description: customDescription,
149
151
  tags: customTags,
150
152
  categoryId,
@@ -177,6 +179,7 @@ export const createTableAction = async (
177
179
  });
178
180
  const title = customTitle?.trim() || defaultTitle;
179
181
 
182
+ const purpose = customPurpose?.trim() || undefined;
180
183
  const description = customDescription?.trim() || undefined;
181
184
  const tags =
182
185
  Array.isArray(customTags) && customTags.length
@@ -195,6 +198,7 @@ export const createTableAction = async (
195
198
  tableId,
196
199
  spaceId: spaceId ?? null,
197
200
  displayName: title,
201
+ purpose,
198
202
  description,
199
203
  tags,
200
204
  schemaVersion: 1,
@@ -27,6 +27,7 @@ import type { CreateTableArgs } from "./createTableAction";
27
27
  import { createTableAction } from "./createTableAction";
28
28
  import { deleteTableAction, type DeleteTableArgs } from "./deleteTableAction";
29
29
  import type { TableMeta, TableColumn } from "./types";
30
+ import type { ContentIcon } from "../contentIcon/types";
30
31
 
31
32
  /* --------------------------------------------------------------------------
32
33
  * Slice 状态
@@ -130,6 +131,12 @@ interface RenameTableArgs {
130
131
  newName: string; // 新的显示名称 displayName
131
132
  }
132
133
 
134
+ interface UpdateTableIconArgs {
135
+ tenantId: string;
136
+ tableId: string;
137
+ icon: ContentIcon | null;
138
+ }
139
+
133
140
  interface LoadTableRowsArgs {
134
141
  tenantId: string;
135
142
  tableId: string;
@@ -979,6 +986,59 @@ export const tableSlice = createSliceWithThunks({
979
986
  }
980
987
  ),
981
988
 
989
+ updateTableIcon: create.asyncThunk(
990
+ async (
991
+ args: UpdateTableIconArgs,
992
+ { dispatch, getState, rejectWithValue }
993
+ ) => {
994
+ const { tenantId, tableId, icon } = args;
995
+ const state = (getState() as RootState).table;
996
+ const meta = state.currentTable;
997
+
998
+ if (!meta || meta.tenantId !== tenantId || meta.tableId !== tableId) {
999
+ return rejectWithValue("当前没有加载对应的表定义");
1000
+ }
1001
+
1002
+ const nowIso = formatISO(new Date());
1003
+
1004
+ try {
1005
+ await dispatch(
1006
+ patch({
1007
+ dbKey: meta.dbKey,
1008
+ changes: {
1009
+ icon: icon ?? null,
1010
+ updatedAt: nowIso,
1011
+ },
1012
+ })
1013
+ ).unwrap();
1014
+
1015
+ const nextMeta: TableMeta = {
1016
+ ...meta,
1017
+ icon: icon ?? null,
1018
+ updatedAt: nowIso,
1019
+ };
1020
+
1021
+ return nextMeta;
1022
+ } catch (e: any) {
1023
+ return rejectWithValue(e.message || "更新表格图标失败");
1024
+ }
1025
+ },
1026
+ {
1027
+ pending: (state) => {
1028
+ state.error = null;
1029
+ },
1030
+ fulfilled: (state, action: PayloadAction<TableMeta>) => {
1031
+ state.currentTable = action.payload;
1032
+ },
1033
+ rejected: (state, action) => {
1034
+ state.error =
1035
+ (action.payload as string) ||
1036
+ action.error.message ||
1037
+ "更新表格图标时发生未知错误";
1038
+ },
1039
+ }
1040
+ ),
1041
+
982
1042
  /* --------------------------------------
983
1043
  * 4.4 更新单元格:updateCell
984
1044
  * ------------------------------------*/
@@ -1085,6 +1145,7 @@ export const {
1085
1145
  renameColumn,
1086
1146
  renameColumnLabel,
1087
1147
  renameTable,
1148
+ updateTableIcon,
1088
1149
  resetTable,
1089
1150
  loadTableRows,
1090
1151
  updateCell,