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
|
@@ -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
|
+
};
|
package/render/page/docSlice.ts
CHANGED
|
@@ -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
|
package/render/page/types.ts
CHANGED
|
@@ -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,
|