@geminilight/mindos 0.6.7 → 0.6.8
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/app/app/api/ask/route.ts +35 -2
- package/app/app/api/file/route.ts +27 -0
- package/app/app/api/setup/check-port/route.ts +18 -13
- package/app/components/ImportModal.tsx +566 -61
- package/app/components/ask/AskContent.tsx +6 -1
- package/app/hooks/useAiOrganize.ts +338 -0
- package/app/hooks/useFileImport.ts +39 -2
- package/app/lib/i18n-en.ts +39 -2
- package/app/lib/i18n-zh.ts +39 -2
- package/app/next-env.d.ts +1 -1
- package/app/package.json +1 -1
- package/bin/cli.js +15 -9
- package/package.json +1 -1
- package/scripts/release.sh +1 -1
|
@@ -401,7 +401,12 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
401
401
|
messages: requestMessages,
|
|
402
402
|
currentFile,
|
|
403
403
|
attachedFiles,
|
|
404
|
-
uploadedFiles: upload.localAttachments
|
|
404
|
+
uploadedFiles: upload.localAttachments.map(f => ({
|
|
405
|
+
name: f.name,
|
|
406
|
+
content: f.content.length > 20_000
|
|
407
|
+
? f.content.slice(0, 20_000) + '\n\n[...truncated to first ~20000 chars]'
|
|
408
|
+
: f.content,
|
|
409
|
+
})),
|
|
405
410
|
}),
|
|
406
411
|
signal: controller.signal,
|
|
407
412
|
});
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useRef } from 'react';
|
|
4
|
+
import type { LocalAttachment, Message } from '@/lib/types';
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Types
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
export type OrganizePhase = 'idle' | 'organizing' | 'done' | 'error';
|
|
11
|
+
|
|
12
|
+
export interface OrganizeFileChange {
|
|
13
|
+
action: 'create' | 'update' | 'unknown';
|
|
14
|
+
path: string;
|
|
15
|
+
toolCallId: string;
|
|
16
|
+
/** Whether the tool call completed successfully */
|
|
17
|
+
ok: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** User-facing stage hint derived from SSE events */
|
|
21
|
+
export type OrganizeStageHint =
|
|
22
|
+
| 'connecting'
|
|
23
|
+
| 'analyzing'
|
|
24
|
+
| 'reading'
|
|
25
|
+
| 'thinking'
|
|
26
|
+
| 'writing';
|
|
27
|
+
|
|
28
|
+
export interface AiOrganizeState {
|
|
29
|
+
phase: OrganizePhase;
|
|
30
|
+
changes: OrganizeFileChange[];
|
|
31
|
+
/** Current tool being executed (for live progress display) */
|
|
32
|
+
currentTool: { name: string; path: string } | null;
|
|
33
|
+
/** User-facing stage hint with optional context (e.g. file being read) */
|
|
34
|
+
stageHint: { stage: OrganizeStageHint; detail?: string } | null;
|
|
35
|
+
/** AI's text summary of what it did */
|
|
36
|
+
summary: string;
|
|
37
|
+
error: string | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// SSE stream parser — extracts file operations from /api/ask stream
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Strip model chain-of-thought tags that should never be shown to users.
|
|
46
|
+
* Handles both complete `<thinking>...</thinking>` blocks and unclosed trailing tags.
|
|
47
|
+
*/
|
|
48
|
+
export function stripThinkingTags(text: string): string {
|
|
49
|
+
let cleaned = text.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '');
|
|
50
|
+
cleaned = cleaned.replace(/<thinking>[\s\S]*$/gi, '');
|
|
51
|
+
return cleaned.trim();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const CLIENT_TRUNCATE_CHARS = 20_000;
|
|
55
|
+
|
|
56
|
+
const FILE_WRITE_TOOLS = new Set([
|
|
57
|
+
'create_file', 'write_file', 'batch_create_files',
|
|
58
|
+
'append_to_file', 'insert_after_heading', 'update_section',
|
|
59
|
+
'edit_lines', 'delete_file', 'rename_file', 'move_file', 'append_csv',
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
const FILE_READ_TOOLS = new Set([
|
|
63
|
+
'read_file', 'read_lines', 'search', 'list_files',
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Derive a user-facing stage hint from an SSE event.
|
|
68
|
+
* Returns null for events that don't change the stage (e.g. tool_end, done).
|
|
69
|
+
*/
|
|
70
|
+
export function deriveStageHint(
|
|
71
|
+
eventType: string,
|
|
72
|
+
toolName: string | undefined,
|
|
73
|
+
args: unknown,
|
|
74
|
+
): { stage: OrganizeStageHint; detail?: string } | null {
|
|
75
|
+
if (eventType === 'text_delta') {
|
|
76
|
+
return { stage: 'analyzing' };
|
|
77
|
+
}
|
|
78
|
+
if (eventType === 'tool_start' && toolName) {
|
|
79
|
+
if (FILE_WRITE_TOOLS.has(toolName)) {
|
|
80
|
+
return { stage: 'writing', detail: extractPathFromArgs(toolName, args) || undefined };
|
|
81
|
+
}
|
|
82
|
+
if (FILE_READ_TOOLS.has(toolName)) {
|
|
83
|
+
const detail = (args && typeof args === 'object' && 'path' in args && typeof (args as Record<string, unknown>).path === 'string')
|
|
84
|
+
? (args as Record<string, unknown>).path as string
|
|
85
|
+
: undefined;
|
|
86
|
+
return { stage: 'reading', detail };
|
|
87
|
+
}
|
|
88
|
+
return { stage: 'analyzing' };
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function extractPathFromArgs(toolName: string, args: unknown): string {
|
|
94
|
+
if (!args || typeof args !== 'object') return '';
|
|
95
|
+
const a = args as Record<string, unknown>;
|
|
96
|
+
if (typeof a.path === 'string') return a.path;
|
|
97
|
+
if (typeof a.from_path === 'string') return a.from_path;
|
|
98
|
+
if (toolName === 'batch_create_files' && Array.isArray(a.files)) {
|
|
99
|
+
return (a.files as Array<{ path?: string }>)
|
|
100
|
+
.map(f => f.path ?? '')
|
|
101
|
+
.filter(Boolean)
|
|
102
|
+
.join(', ');
|
|
103
|
+
}
|
|
104
|
+
return '';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function consumeOrganizeStream(
|
|
108
|
+
body: ReadableStream<Uint8Array>,
|
|
109
|
+
onProgress: (state: Partial<AiOrganizeState> & { summary?: string }) => void,
|
|
110
|
+
signal?: AbortSignal,
|
|
111
|
+
): Promise<{ changes: OrganizeFileChange[]; summary: string; toolCallCount: number }> {
|
|
112
|
+
const reader = body.getReader();
|
|
113
|
+
const decoder = new TextDecoder();
|
|
114
|
+
let buffer = '';
|
|
115
|
+
|
|
116
|
+
const changes: OrganizeFileChange[] = [];
|
|
117
|
+
const pendingTools = new Map<string, { name: string; path: string; action: 'create' | 'update' | 'unknown' }>();
|
|
118
|
+
let summary = '';
|
|
119
|
+
let toolCallCount = 0;
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
while (true) {
|
|
123
|
+
if (signal?.aborted) break;
|
|
124
|
+
const { done, value } = await reader.read();
|
|
125
|
+
if (done) break;
|
|
126
|
+
|
|
127
|
+
buffer += decoder.decode(value, { stream: true });
|
|
128
|
+
const lines = buffer.split('\n');
|
|
129
|
+
buffer = lines.pop() ?? '';
|
|
130
|
+
|
|
131
|
+
for (const line of lines) {
|
|
132
|
+
const trimmed = line.trim();
|
|
133
|
+
if (!trimmed.startsWith('data:')) continue;
|
|
134
|
+
const jsonStr = trimmed.slice(5).trim();
|
|
135
|
+
if (!jsonStr) continue;
|
|
136
|
+
|
|
137
|
+
let event: Record<string, unknown>;
|
|
138
|
+
try { event = JSON.parse(jsonStr); } catch { continue; }
|
|
139
|
+
|
|
140
|
+
const type = event.type as string;
|
|
141
|
+
|
|
142
|
+
switch (type) {
|
|
143
|
+
case 'tool_start': {
|
|
144
|
+
const toolName = event.toolName as string;
|
|
145
|
+
const toolCallId = event.toolCallId as string;
|
|
146
|
+
const args = event.args;
|
|
147
|
+
toolCallCount++;
|
|
148
|
+
|
|
149
|
+
const hint = deriveStageHint(type, toolName, args);
|
|
150
|
+
if (hint) onProgress({ stageHint: hint });
|
|
151
|
+
|
|
152
|
+
if (FILE_WRITE_TOOLS.has(toolName)) {
|
|
153
|
+
const path = extractPathFromArgs(toolName, args);
|
|
154
|
+
let action: 'create' | 'update' | 'unknown' = 'update';
|
|
155
|
+
if (toolName === 'create_file' || toolName === 'batch_create_files') action = 'create';
|
|
156
|
+
else if (toolName === 'delete_file' || toolName === 'rename_file' || toolName === 'move_file') action = 'unknown';
|
|
157
|
+
pendingTools.set(toolCallId, { name: toolName, path, action });
|
|
158
|
+
onProgress({ currentTool: { name: toolName, path } });
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
case 'tool_end': {
|
|
164
|
+
const toolCallId = event.toolCallId as string;
|
|
165
|
+
const isError = !!event.isError;
|
|
166
|
+
const pending = pendingTools.get(toolCallId);
|
|
167
|
+
if (pending) {
|
|
168
|
+
if (pending.name === 'batch_create_files') {
|
|
169
|
+
for (const p of pending.path.split(', ').filter(Boolean)) {
|
|
170
|
+
changes.push({ action: pending.action, path: p, toolCallId, ok: !isError });
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
changes.push({ action: pending.action, path: pending.path, toolCallId, ok: !isError });
|
|
174
|
+
}
|
|
175
|
+
pendingTools.delete(toolCallId);
|
|
176
|
+
onProgress({ changes: [...changes], currentTool: null });
|
|
177
|
+
}
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
case 'text_delta': {
|
|
182
|
+
summary += (event.delta as string) ?? '';
|
|
183
|
+
onProgress({ stageHint: { stage: 'analyzing' }, summary: stripThinkingTags(summary) });
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
case 'error': {
|
|
188
|
+
throw new Error((event.message as string) || 'AI organize failed');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
case 'done':
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
} finally {
|
|
197
|
+
reader.releaseLock();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return { changes, summary: stripThinkingTags(summary), toolCallCount };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// Hook
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
export function useAiOrganize() {
|
|
208
|
+
const [phase, setPhase] = useState<OrganizePhase>('idle');
|
|
209
|
+
const [changes, setChanges] = useState<OrganizeFileChange[]>([]);
|
|
210
|
+
const [currentTool, setCurrentTool] = useState<{ name: string; path: string } | null>(null);
|
|
211
|
+
const [stageHint, setStageHint] = useState<{ stage: OrganizeStageHint; detail?: string } | null>(null);
|
|
212
|
+
const [summary, setSummary] = useState('');
|
|
213
|
+
const [error, setError] = useState<string | null>(null);
|
|
214
|
+
const [toolCallCount, setToolCallCount] = useState(0);
|
|
215
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
216
|
+
const lastEventRef = useRef<number>(0);
|
|
217
|
+
|
|
218
|
+
const start = useCallback(async (files: LocalAttachment[], prompt: string) => {
|
|
219
|
+
setPhase('organizing');
|
|
220
|
+
setChanges([]);
|
|
221
|
+
setCurrentTool(null);
|
|
222
|
+
setStageHint({ stage: 'connecting' });
|
|
223
|
+
setSummary('');
|
|
224
|
+
setError(null);
|
|
225
|
+
setToolCallCount(0);
|
|
226
|
+
lastEventRef.current = Date.now();
|
|
227
|
+
|
|
228
|
+
const controller = new AbortController();
|
|
229
|
+
abortRef.current = controller;
|
|
230
|
+
|
|
231
|
+
const messages: Message[] = [{ role: 'user', content: prompt }];
|
|
232
|
+
|
|
233
|
+
const truncatedFiles = files.map(f => ({
|
|
234
|
+
name: f.name,
|
|
235
|
+
content: f.content.length > CLIENT_TRUNCATE_CHARS
|
|
236
|
+
? f.content.slice(0, CLIENT_TRUNCATE_CHARS) + '\n\n[...truncated to first ~20000 chars]'
|
|
237
|
+
: f.content,
|
|
238
|
+
}));
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const res = await fetch('/api/ask', {
|
|
242
|
+
method: 'POST',
|
|
243
|
+
headers: { 'Content-Type': 'application/json' },
|
|
244
|
+
body: JSON.stringify({
|
|
245
|
+
messages,
|
|
246
|
+
uploadedFiles: truncatedFiles,
|
|
247
|
+
maxSteps: 15,
|
|
248
|
+
}),
|
|
249
|
+
signal: controller.signal,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
if (!res.ok) {
|
|
253
|
+
let errorMsg = `Request failed (${res.status})`;
|
|
254
|
+
try {
|
|
255
|
+
const errBody = await res.json() as { error?: { message?: string } | string; message?: string };
|
|
256
|
+
if (typeof errBody?.error === 'string') errorMsg = errBody.error;
|
|
257
|
+
else if (typeof errBody?.error === 'object' && errBody.error?.message) errorMsg = errBody.error.message;
|
|
258
|
+
else if (errBody?.message) errorMsg = errBody.message as string;
|
|
259
|
+
} catch {}
|
|
260
|
+
throw new Error(errorMsg);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (!res.body) throw new Error('No response body');
|
|
264
|
+
|
|
265
|
+
const result = await consumeOrganizeStream(
|
|
266
|
+
res.body,
|
|
267
|
+
(partial) => {
|
|
268
|
+
lastEventRef.current = Date.now();
|
|
269
|
+
if (partial.changes) setChanges(partial.changes);
|
|
270
|
+
if (partial.currentTool !== undefined) setCurrentTool(partial.currentTool);
|
|
271
|
+
if (partial.stageHint) setStageHint(partial.stageHint);
|
|
272
|
+
if (partial.summary !== undefined) setSummary(partial.summary);
|
|
273
|
+
},
|
|
274
|
+
controller.signal,
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
setChanges(result.changes);
|
|
278
|
+
setSummary(result.summary);
|
|
279
|
+
setToolCallCount(result.toolCallCount);
|
|
280
|
+
setCurrentTool(null);
|
|
281
|
+
setPhase('done');
|
|
282
|
+
} catch (err) {
|
|
283
|
+
if ((err as Error).name === 'AbortError') {
|
|
284
|
+
setPhase('idle');
|
|
285
|
+
} else {
|
|
286
|
+
setError((err as Error).message);
|
|
287
|
+
setPhase('error');
|
|
288
|
+
}
|
|
289
|
+
} finally {
|
|
290
|
+
abortRef.current = null;
|
|
291
|
+
}
|
|
292
|
+
}, []);
|
|
293
|
+
|
|
294
|
+
const abort = useCallback(() => {
|
|
295
|
+
abortRef.current?.abort();
|
|
296
|
+
}, []);
|
|
297
|
+
|
|
298
|
+
const undoAll = useCallback(async (): Promise<number> => {
|
|
299
|
+
const createdFiles = changes.filter(c => c.action === 'create' && c.ok);
|
|
300
|
+
let reverted = 0;
|
|
301
|
+
for (const file of createdFiles) {
|
|
302
|
+
try {
|
|
303
|
+
const res = await fetch('/api/file', {
|
|
304
|
+
method: 'POST',
|
|
305
|
+
headers: { 'Content-Type': 'application/json' },
|
|
306
|
+
body: JSON.stringify({ op: 'delete_file', path: file.path }),
|
|
307
|
+
});
|
|
308
|
+
if (res.ok) reverted++;
|
|
309
|
+
} catch {}
|
|
310
|
+
}
|
|
311
|
+
return reverted;
|
|
312
|
+
}, [changes]);
|
|
313
|
+
|
|
314
|
+
const reset = useCallback(() => {
|
|
315
|
+
setPhase('idle');
|
|
316
|
+
setChanges([]);
|
|
317
|
+
setCurrentTool(null);
|
|
318
|
+
setStageHint(null);
|
|
319
|
+
setSummary('');
|
|
320
|
+
setError(null);
|
|
321
|
+
setToolCallCount(0);
|
|
322
|
+
lastEventRef.current = 0;
|
|
323
|
+
}, []);
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
phase,
|
|
327
|
+
changes,
|
|
328
|
+
currentTool,
|
|
329
|
+
stageHint,
|
|
330
|
+
summary,
|
|
331
|
+
error,
|
|
332
|
+
toolCallCount,
|
|
333
|
+
start,
|
|
334
|
+
abort,
|
|
335
|
+
undoAll,
|
|
336
|
+
reset,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
@@ -4,7 +4,7 @@ import { useState, useCallback, useRef } from 'react';
|
|
|
4
4
|
import { ALLOWED_IMPORT_EXTENSIONS } from '@/lib/core/file-convert';
|
|
5
5
|
|
|
6
6
|
export type ImportIntent = 'archive' | 'digest';
|
|
7
|
-
export type ImportStep = 'select' | 'archive_config' | 'importing' | 'done';
|
|
7
|
+
export type ImportStep = 'select' | 'archive_config' | 'importing' | 'done' | 'organizing' | 'organize_review';
|
|
8
8
|
export type ConflictMode = 'skip' | 'rename' | 'overwrite';
|
|
9
9
|
|
|
10
10
|
export interface ImportFile {
|
|
@@ -31,6 +31,37 @@ function formatSize(bytes: number): string {
|
|
|
31
31
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
function fileToBase64(file: File): Promise<string> {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const reader = new FileReader();
|
|
37
|
+
reader.onload = () => {
|
|
38
|
+
const dataUrl = reader.result as string;
|
|
39
|
+
const commaIdx = dataUrl.indexOf(',');
|
|
40
|
+
resolve(commaIdx >= 0 ? dataUrl.slice(commaIdx + 1) : '');
|
|
41
|
+
};
|
|
42
|
+
reader.onerror = () => reject(reader.error ?? new Error('FileReader error'));
|
|
43
|
+
reader.readAsDataURL(file);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function extractPdfText(file: File): Promise<string> {
|
|
48
|
+
const base64 = await fileToBase64(file);
|
|
49
|
+
if (!base64) throw new Error('Empty PDF file');
|
|
50
|
+
|
|
51
|
+
const res = await fetch('/api/extract-pdf', {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: { 'Content-Type': 'application/json' },
|
|
54
|
+
body: JSON.stringify({ name: file.name, dataBase64: base64 }),
|
|
55
|
+
});
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
const err = await res.json().catch(() => ({}));
|
|
58
|
+
throw new Error((err as { error?: string }).error || 'PDF extraction failed');
|
|
59
|
+
}
|
|
60
|
+
const data = await res.json() as { text?: string; extracted?: boolean };
|
|
61
|
+
if (!data.extracted || !data.text) throw new Error('No text extracted from PDF');
|
|
62
|
+
return data.text;
|
|
63
|
+
}
|
|
64
|
+
|
|
34
65
|
export function useFileImport() {
|
|
35
66
|
const [files, setFiles] = useState<ImportFile[]>([]);
|
|
36
67
|
const [step, setStep] = useState<ImportStep>('select');
|
|
@@ -85,7 +116,13 @@ export function useFileImport() {
|
|
|
85
116
|
for (const f of newFiles) {
|
|
86
117
|
if (f.error) continue;
|
|
87
118
|
try {
|
|
88
|
-
const
|
|
119
|
+
const ext = getExt(f.name);
|
|
120
|
+
let text: string;
|
|
121
|
+
if (ext === '.pdf') {
|
|
122
|
+
text = await extractPdfText(f.file);
|
|
123
|
+
} else {
|
|
124
|
+
text = await f.file.text();
|
|
125
|
+
}
|
|
89
126
|
setFiles(prev => prev.map(p =>
|
|
90
127
|
p.name === f.name && p.size === f.size
|
|
91
128
|
? { ...p, content: text, loading: false }
|
package/app/lib/i18n-en.ts
CHANGED
|
@@ -718,10 +718,47 @@ export const en = {
|
|
|
718
718
|
dropOverlay: 'Drop files to import into knowledge base',
|
|
719
719
|
dropOverlayFormats: 'Supports .md .txt .pdf .csv .json .yaml .html',
|
|
720
720
|
onboardingHint: 'Already have notes? Import files →',
|
|
721
|
-
digestPromptSingle: (name: string) =>
|
|
722
|
-
|
|
721
|
+
digestPromptSingle: (name: string, targetSpace?: string) => {
|
|
722
|
+
const loc = targetSpace ? ` under the "${targetSpace}" space` : '';
|
|
723
|
+
return `The user uploaded "${name}". You MUST:\n1. Read the content from the "USER-UPLOADED FILES" section above\n2. Extract and reorganize the key information into well-structured Markdown notes\n3. Save the result${loc} in the knowledge base — create new files or update existing ones as appropriate\n\nDo NOT just reply with a text summary. You must actually write to the knowledge base.`;
|
|
724
|
+
},
|
|
725
|
+
digestPromptMulti: (n: number, targetSpace?: string) => {
|
|
726
|
+
const loc = targetSpace ? ` under the "${targetSpace}" space` : '';
|
|
727
|
+
return `The user uploaded ${n} files. You MUST:\n1. Read their content from the "USER-UPLOADED FILES" section above\n2. Extract and reorganize the key information into well-structured Markdown notes\n3. Save the results${loc} in the knowledge base — create new files or update existing ones as appropriate\n\nDo NOT just reply with a text summary. You must actually write to the knowledge base.`;
|
|
728
|
+
},
|
|
723
729
|
arrowTo: '→',
|
|
724
730
|
remove: 'Remove',
|
|
731
|
+
conflictsFound: (n: number) => `${n} file${n !== 1 ? 's' : ''} already exist${n === 1 ? 's' : ''}`,
|
|
732
|
+
organizeTitle: 'AI Organizing',
|
|
733
|
+
organizeProcessing: 'AI is analyzing and organizing your files...',
|
|
734
|
+
organizeConnecting: 'Connecting to AI...',
|
|
735
|
+
organizeAnalyzing: 'AI is analyzing your files...',
|
|
736
|
+
organizeReading: (detail?: string) => detail ? `Reading ${detail}...` : 'Reading files...',
|
|
737
|
+
organizeThinking: 'AI is thinking deeply...',
|
|
738
|
+
organizeWriting: (detail?: string) => detail ? `Writing ${detail}...` : 'Writing files...',
|
|
739
|
+
organizeElapsed: (seconds: number) => {
|
|
740
|
+
const m = Math.floor(seconds / 60);
|
|
741
|
+
const s = seconds % 60;
|
|
742
|
+
return `${m}:${s.toString().padStart(2, '0')}`;
|
|
743
|
+
},
|
|
744
|
+
organizeCancel: 'Cancel',
|
|
745
|
+
organizeMinimize: 'Continue browsing',
|
|
746
|
+
organizeExpand: 'View',
|
|
747
|
+
organizeCreating: (path: string) => `Creating ${path}`,
|
|
748
|
+
organizeUpdating: (path: string) => `Updating ${path}`,
|
|
749
|
+
organizeReviewTitle: 'Organization Complete',
|
|
750
|
+
organizeErrorTitle: 'Organization Failed',
|
|
751
|
+
organizeReviewDesc: (n: number) => `AI organized your files into ${n} change${n !== 1 ? 's' : ''}`,
|
|
752
|
+
organizeCreated: 'Created',
|
|
753
|
+
organizeUpdated: 'Updated',
|
|
754
|
+
organizeFailed: 'Failed',
|
|
755
|
+
organizeNoChanges: 'AI analyzed your files but made no changes.',
|
|
756
|
+
organizeToolCallsInfo: (n: number) => `AI executed ${n} operation${n > 1 ? 's' : ''} — check knowledge base for updates`,
|
|
757
|
+
organizeError: 'Organization failed',
|
|
758
|
+
organizeRetry: 'Retry',
|
|
759
|
+
organizeDone: 'Done',
|
|
760
|
+
organizeUndoAll: 'Undo All',
|
|
761
|
+
organizeUndoSuccess: (n: number) => `Reverted ${n} file${n !== 1 ? 's' : ''}`,
|
|
725
762
|
},
|
|
726
763
|
dirView: {
|
|
727
764
|
gridView: 'Grid view',
|
package/app/lib/i18n-zh.ts
CHANGED
|
@@ -742,10 +742,47 @@ export const zh = {
|
|
|
742
742
|
dropOverlay: '松开鼠标,导入文件到知识库',
|
|
743
743
|
dropOverlayFormats: '支持 .md .txt .pdf .csv .json .yaml .html',
|
|
744
744
|
onboardingHint: '已有笔记?导入文件到知识库 →',
|
|
745
|
-
digestPromptSingle: (name: string) =>
|
|
746
|
-
|
|
745
|
+
digestPromptSingle: (name: string, targetSpace?: string) => {
|
|
746
|
+
const loc = targetSpace ? `"${targetSpace}" 空间下` : '知识库中合适的位置';
|
|
747
|
+
return `用户上传了「${name}」。你必须:\n1. 从上方「USER-UPLOADED FILES」区域读取文件内容\n2. 提取和重新整理关键信息为结构清晰的 Markdown 笔记\n3. 将整理后的内容保存到${loc}——可以创建新文件,也可以更新已有文件\n\n不要只做文字回复。你必须实际写入知识库。`;
|
|
748
|
+
},
|
|
749
|
+
digestPromptMulti: (n: number, targetSpace?: string) => {
|
|
750
|
+
const loc = targetSpace ? `"${targetSpace}" 空间下` : '知识库中合适的位置';
|
|
751
|
+
return `用户上传了 ${n} 个文件。你必须:\n1. 从上方「USER-UPLOADED FILES」区域读取它们的内容\n2. 提取和重新整理关键信息为结构清晰的 Markdown 笔记\n3. 将整理后的内容保存到${loc}——可以创建新文件,也可以更新已有文件\n\n不要只做文字回复。你必须实际写入知识库。`;
|
|
752
|
+
},
|
|
747
753
|
arrowTo: '→',
|
|
748
754
|
remove: '移除',
|
|
755
|
+
conflictsFound: (n: number) => `${n} 个文件已存在`,
|
|
756
|
+
organizeTitle: 'AI 整理中',
|
|
757
|
+
organizeProcessing: 'AI 正在分析和整理你的文件...',
|
|
758
|
+
organizeConnecting: '正在连接 AI...',
|
|
759
|
+
organizeAnalyzing: 'AI 正在分析你的文件...',
|
|
760
|
+
organizeReading: (detail?: string) => detail ? `正在阅读 ${detail}...` : '正在阅读文件...',
|
|
761
|
+
organizeThinking: 'AI 正在深度思考...',
|
|
762
|
+
organizeWriting: (detail?: string) => detail ? `正在写入 ${detail}...` : '正在写入文件...',
|
|
763
|
+
organizeElapsed: (seconds: number) => {
|
|
764
|
+
const m = Math.floor(seconds / 60);
|
|
765
|
+
const s = seconds % 60;
|
|
766
|
+
return `${m}:${s.toString().padStart(2, '0')}`;
|
|
767
|
+
},
|
|
768
|
+
organizeCancel: '取消',
|
|
769
|
+
organizeMinimize: '继续浏览',
|
|
770
|
+
organizeExpand: '查看',
|
|
771
|
+
organizeCreating: (path: string) => `正在创建 ${path}`,
|
|
772
|
+
organizeUpdating: (path: string) => `正在更新 ${path}`,
|
|
773
|
+
organizeReviewTitle: '整理完成',
|
|
774
|
+
organizeErrorTitle: '整理失败',
|
|
775
|
+
organizeReviewDesc: (n: number) => `AI 整理了 ${n} 处变更`,
|
|
776
|
+
organizeCreated: '已创建',
|
|
777
|
+
organizeUpdated: '已更新',
|
|
778
|
+
organizeFailed: '失败',
|
|
779
|
+
organizeNoChanges: 'AI 分析了你的文件,但没有做任何更改。',
|
|
780
|
+
organizeToolCallsInfo: (n: number) => `AI 执行了 ${n} 个操作 — 请检查知识库查看更新`,
|
|
781
|
+
organizeError: '整理失败',
|
|
782
|
+
organizeRetry: '重试',
|
|
783
|
+
organizeDone: '完成',
|
|
784
|
+
organizeUndoAll: '撤销全部',
|
|
785
|
+
organizeUndoSuccess: (n: number) => `已撤销 ${n} 个文件`,
|
|
749
786
|
},
|
|
750
787
|
dirView: {
|
|
751
788
|
gridView: '网格视图',
|
package/app/next-env.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="next" />
|
|
2
2
|
/// <reference types="next/image-types/global" />
|
|
3
|
-
import "./.next/
|
|
3
|
+
import "./.next/types/routes.d.ts";
|
|
4
4
|
|
|
5
5
|
// NOTE: This file should not be edited
|
|
6
6
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
package/app/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "next dev -p ${MINDOS_WEB_PORT:-3456}",
|
|
7
7
|
"prebuild": "node ../scripts/gen-renderer-index.js",
|
|
8
|
-
"build": "next build",
|
|
8
|
+
"build": "next build --webpack",
|
|
9
9
|
"start": "next start -p ${MINDOS_WEB_PORT:-3456}",
|
|
10
10
|
"lint": "eslint",
|
|
11
11
|
"test": "vitest run",
|
package/bin/cli.js
CHANGED
|
@@ -140,7 +140,7 @@ if (cmd === '--version' || cmd === '-v') {
|
|
|
140
140
|
|
|
141
141
|
const isDaemon = process.argv.includes('--daemon') || (!cmd && isDaemonMode());
|
|
142
142
|
const isVerbose = process.argv.includes('--verbose');
|
|
143
|
-
const extra = process.argv.slice(3).filter(a => a !== '--daemon' && a !== '--verbose').join(' ');
|
|
143
|
+
const extra = process.argv.slice(3).filter(a => a !== '--daemon' && a !== '--verbose' && a !== '--turbo').join(' ');
|
|
144
144
|
|
|
145
145
|
const commands = {
|
|
146
146
|
// ── onboard ────────────────────────────────────────────────────────────────
|
|
@@ -267,10 +267,12 @@ const commands = {
|
|
|
267
267
|
// ── dev ────────────────────────────────────────────────────────────────────
|
|
268
268
|
dev: async () => {
|
|
269
269
|
loadConfig();
|
|
270
|
+
if (!process.env.MINDOS_WEB_PORT) process.env.MINDOS_WEB_PORT = '3456';
|
|
271
|
+
if (!process.env.MINDOS_MCP_PORT) process.env.MINDOS_MCP_PORT = '8781';
|
|
270
272
|
process.env.MINDOS_CLI_PATH = resolve(ROOT, 'bin', 'cli.js');
|
|
271
273
|
process.env.MINDOS_NODE_BIN = process.execPath;
|
|
272
|
-
const webPort = process.env.MINDOS_WEB_PORT
|
|
273
|
-
const mcpPort = process.env.MINDOS_MCP_PORT
|
|
274
|
+
const webPort = process.env.MINDOS_WEB_PORT;
|
|
275
|
+
const mcpPort = process.env.MINDOS_MCP_PORT;
|
|
274
276
|
await assertPortFree(Number(webPort), 'web');
|
|
275
277
|
await assertPortFree(Number(mcpPort), 'mcp');
|
|
276
278
|
ensureAppDeps();
|
|
@@ -303,8 +305,10 @@ const commands = {
|
|
|
303
305
|
console.warn(yellow('Warning: daemon mode not supported on this platform. Falling back to foreground.'));
|
|
304
306
|
} else {
|
|
305
307
|
loadConfig();
|
|
306
|
-
|
|
307
|
-
|
|
308
|
+
if (!process.env.MINDOS_WEB_PORT) process.env.MINDOS_WEB_PORT = '3456';
|
|
309
|
+
if (!process.env.MINDOS_MCP_PORT) process.env.MINDOS_MCP_PORT = '8781';
|
|
310
|
+
const webPort = process.env.MINDOS_WEB_PORT;
|
|
311
|
+
const mcpPort = process.env.MINDOS_MCP_PORT;
|
|
308
312
|
console.log(cyan(`Installing MindOS as a background service (${platform})...`));
|
|
309
313
|
await runGatewayCommand('install');
|
|
310
314
|
// install() already starts the service via launchctl bootstrap + RunAtLoad=true.
|
|
@@ -334,8 +338,10 @@ const commands = {
|
|
|
334
338
|
}
|
|
335
339
|
}
|
|
336
340
|
loadConfig();
|
|
337
|
-
|
|
338
|
-
|
|
341
|
+
if (!process.env.MINDOS_WEB_PORT) process.env.MINDOS_WEB_PORT = '3456';
|
|
342
|
+
if (!process.env.MINDOS_MCP_PORT) process.env.MINDOS_MCP_PORT = '8781';
|
|
343
|
+
const webPort = process.env.MINDOS_WEB_PORT;
|
|
344
|
+
const mcpPort = process.env.MINDOS_MCP_PORT;
|
|
339
345
|
|
|
340
346
|
// ── Auto-migrate user-rules.md to root user-skill-rules.md ─────────────
|
|
341
347
|
try {
|
|
@@ -382,7 +388,7 @@ const commands = {
|
|
|
382
388
|
console.log(yellow('Building MindOS (first run or new version detected)...\n'));
|
|
383
389
|
cleanNextDir();
|
|
384
390
|
run('node scripts/gen-renderer-index.js', ROOT);
|
|
385
|
-
run(`${NEXT_BIN} build`, resolve(ROOT, 'app'));
|
|
391
|
+
run(`${NEXT_BIN} build --webpack`, resolve(ROOT, 'app'));
|
|
386
392
|
writeBuildStamp();
|
|
387
393
|
}
|
|
388
394
|
const mcp = spawnMcp(isVerbose);
|
|
@@ -406,7 +412,7 @@ const commands = {
|
|
|
406
412
|
ensureAppDeps();
|
|
407
413
|
cleanNextDir();
|
|
408
414
|
run('node scripts/gen-renderer-index.js', ROOT);
|
|
409
|
-
run(`${NEXT_BIN} build ${extra}`, resolve(ROOT, 'app'));
|
|
415
|
+
run(`${NEXT_BIN} build --webpack ${extra}`, resolve(ROOT, 'app'));
|
|
410
416
|
writeBuildStamp();
|
|
411
417
|
},
|
|
412
418
|
|
package/package.json
CHANGED
package/scripts/release.sh
CHANGED