@geminilight/mindos 0.5.10 → 0.5.11
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/settings/test-key/route.ts +111 -0
- package/app/app/api/sync/route.ts +16 -31
- package/app/components/settings/AiTab.tsx +120 -2
- package/app/instrumentation.ts +19 -0
- package/app/lib/i18n.ts +18 -0
- package/app/next.config.ts +1 -1
- package/bin/cli.js +8 -1
- package/bin/lib/sync.js +61 -11
- package/package.json +4 -2
- package/assets/images/demo-flow-dark.png +0 -0
- package/assets/images/demo-flow-light.png +0 -0
- package/assets/images/demo-flow-zh-dark.png +0 -0
- package/assets/images/demo-flow-zh-light.png +0 -0
- package/assets/images/gui-sync-cv.png +0 -0
- package/assets/images/wechat-qr.png +0 -0
- package/mcp/package-lock.json +0 -1717
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
3
|
+
import { effectiveAiConfig } from '@/lib/settings';
|
|
4
|
+
|
|
5
|
+
const TIMEOUT = 10_000;
|
|
6
|
+
|
|
7
|
+
type ErrorCode = 'auth_error' | 'model_not_found' | 'rate_limited' | 'network_error' | 'unknown';
|
|
8
|
+
|
|
9
|
+
function classifyError(status: number, body: string): { code: ErrorCode; error: string } {
|
|
10
|
+
if (status === 401 || status === 403) return { code: 'auth_error', error: 'Invalid API key' };
|
|
11
|
+
if (status === 404) return { code: 'model_not_found', error: 'Model not found' };
|
|
12
|
+
if (status === 429) return { code: 'rate_limited', error: 'Rate limited' };
|
|
13
|
+
// Try to extract error message from response body
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(body);
|
|
16
|
+
const msg = parsed?.error?.message || parsed?.error || '';
|
|
17
|
+
if (typeof msg === 'string' && msg.length > 0) return { code: 'unknown', error: msg.slice(0, 200) };
|
|
18
|
+
} catch { /* not JSON */ }
|
|
19
|
+
return { code: 'unknown', error: `HTTP ${status}` };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function testAnthropic(apiKey: string, model: string): Promise<{ ok: boolean; latency?: number; code?: ErrorCode; error?: string }> {
|
|
23
|
+
const start = Date.now();
|
|
24
|
+
const ctrl = new AbortController();
|
|
25
|
+
const timer = setTimeout(() => ctrl.abort(), TIMEOUT);
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: {
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
'x-api-key': apiKey,
|
|
32
|
+
'anthropic-version': '2023-06-01',
|
|
33
|
+
},
|
|
34
|
+
body: JSON.stringify({ model, max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),
|
|
35
|
+
signal: ctrl.signal,
|
|
36
|
+
});
|
|
37
|
+
const latency = Date.now() - start;
|
|
38
|
+
if (res.ok) return { ok: true, latency };
|
|
39
|
+
const body = await res.text();
|
|
40
|
+
return { ok: false, ...classifyError(res.status, body) };
|
|
41
|
+
} catch (e: unknown) {
|
|
42
|
+
if (e instanceof Error && e.name === 'AbortError') return { ok: false, code: 'network_error', error: 'Request timed out' };
|
|
43
|
+
return { ok: false, code: 'network_error', error: e instanceof Error ? e.message : 'Network error' };
|
|
44
|
+
} finally {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function testOpenAI(apiKey: string, model: string, baseUrl: string): Promise<{ ok: boolean; latency?: number; code?: ErrorCode; error?: string }> {
|
|
50
|
+
const start = Date.now();
|
|
51
|
+
const ctrl = new AbortController();
|
|
52
|
+
const timer = setTimeout(() => ctrl.abort(), TIMEOUT);
|
|
53
|
+
const url = (baseUrl || 'https://api.openai.com/v1').replace(/\/+$/, '') + '/chat/completions';
|
|
54
|
+
try {
|
|
55
|
+
const res = await fetch(url, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: {
|
|
58
|
+
'Content-Type': 'application/json',
|
|
59
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify({ model, max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),
|
|
62
|
+
signal: ctrl.signal,
|
|
63
|
+
});
|
|
64
|
+
const latency = Date.now() - start;
|
|
65
|
+
if (res.ok) return { ok: true, latency };
|
|
66
|
+
const body = await res.text();
|
|
67
|
+
return { ok: false, ...classifyError(res.status, body) };
|
|
68
|
+
} catch (e: unknown) {
|
|
69
|
+
if (e instanceof Error && e.name === 'AbortError') return { ok: false, code: 'network_error', error: 'Request timed out' };
|
|
70
|
+
return { ok: false, code: 'network_error', error: e instanceof Error ? e.message : 'Network error' };
|
|
71
|
+
} finally {
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function POST(req: NextRequest) {
|
|
77
|
+
try {
|
|
78
|
+
const body = await req.json();
|
|
79
|
+
const { provider, apiKey, model, baseUrl } = body as {
|
|
80
|
+
provider?: string;
|
|
81
|
+
apiKey?: string;
|
|
82
|
+
model?: string;
|
|
83
|
+
baseUrl?: string;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
if (provider !== 'anthropic' && provider !== 'openai') {
|
|
87
|
+
return NextResponse.json({ ok: false, code: 'unknown', error: 'Invalid provider' }, { status: 400 });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Resolve actual API key: use provided key, fallback to config/env for masked or missing
|
|
91
|
+
const cfg = effectiveAiConfig();
|
|
92
|
+
let resolvedKey = apiKey || '';
|
|
93
|
+
if (!resolvedKey || resolvedKey === '***set***') {
|
|
94
|
+
resolvedKey = provider === 'anthropic' ? cfg.anthropicApiKey : cfg.openaiApiKey;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!resolvedKey) {
|
|
98
|
+
return NextResponse.json({ ok: false, code: 'auth_error', error: 'No API key configured' });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const resolvedModel = model || (provider === 'anthropic' ? cfg.anthropicModel : cfg.openaiModel);
|
|
102
|
+
|
|
103
|
+
const result = provider === 'anthropic'
|
|
104
|
+
? await testAnthropic(resolvedKey, resolvedModel)
|
|
105
|
+
: await testOpenAI(resolvedKey, resolvedModel, baseUrl || cfg.openaiBaseUrl);
|
|
106
|
+
|
|
107
|
+
return NextResponse.json(result);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
return NextResponse.json({ ok: false, code: 'unknown', error: String(err) }, { status: 500 });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -95,25 +95,13 @@ export async function POST(req: NextRequest) {
|
|
|
95
95
|
return NextResponse.json({ error: 'Sync already configured' }, { status: 400 });
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
// Build the effective remote URL (inject token for HTTPS)
|
|
99
|
-
let effectiveRemote = remote;
|
|
100
|
-
if (isHTTPS && body.token) {
|
|
101
|
-
try {
|
|
102
|
-
const urlObj = new URL(remote);
|
|
103
|
-
urlObj.username = 'oauth2';
|
|
104
|
-
urlObj.password = body.token;
|
|
105
|
-
effectiveRemote = urlObj.toString();
|
|
106
|
-
} catch {
|
|
107
|
-
return NextResponse.json({ error: 'Invalid remote URL' }, { status: 400 });
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
98
|
const branch = body.branch?.trim() || 'main';
|
|
112
99
|
|
|
113
|
-
// Call CLI's sync init
|
|
100
|
+
// Call CLI's sync init — pass clean remote + token separately (never embed token in URL)
|
|
114
101
|
try {
|
|
115
102
|
const cliPath = resolve(process.cwd(), '..', 'bin', 'cli.js');
|
|
116
|
-
const args = ['sync', 'init', '--non-interactive', '--remote',
|
|
103
|
+
const args = ['sync', 'init', '--non-interactive', '--remote', remote, '--branch', branch];
|
|
104
|
+
if (body.token) args.push('--token', body.token);
|
|
117
105
|
|
|
118
106
|
await new Promise<void>((res, rej) => {
|
|
119
107
|
execFile('node', [cliPath, ...args], { timeout: 30000 }, (err, stdout, stderr) => {
|
|
@@ -132,23 +120,20 @@ export async function POST(req: NextRequest) {
|
|
|
132
120
|
if (!isGitRepo(mindRoot)) {
|
|
133
121
|
return NextResponse.json({ error: 'Not a git repository' }, { status: 400 });
|
|
134
122
|
}
|
|
135
|
-
//
|
|
136
|
-
try {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
123
|
+
// Delegate to CLI for unified conflict handling
|
|
124
|
+
try {
|
|
125
|
+
const cliPath = resolve(process.cwd(), '..', 'bin', 'cli.js');
|
|
126
|
+
await new Promise<void>((res, rej) => {
|
|
127
|
+
execFile('node', [cliPath, 'sync', 'now'], { timeout: 60000 }, (err, stdout, stderr) => {
|
|
128
|
+
if (err) rej(new Error(stderr?.trim() || err.message));
|
|
129
|
+
else res();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
return NextResponse.json({ ok: true });
|
|
133
|
+
} catch (err: unknown) {
|
|
134
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
135
|
+
return NextResponse.json({ error: errMsg }, { status: 500 });
|
|
147
136
|
}
|
|
148
|
-
const state = loadSyncState();
|
|
149
|
-
state.lastSync = new Date().toISOString();
|
|
150
|
-
writeFileSync(SYNC_STATE_PATH, JSON.stringify(state, null, 2) + '\n');
|
|
151
|
-
return NextResponse.json({ ok: true });
|
|
152
137
|
}
|
|
153
138
|
|
|
154
139
|
case 'on': {
|
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
4
|
+
import { AlertCircle, Loader2 } from 'lucide-react';
|
|
4
5
|
import type { AiSettings, ProviderConfig, SettingsData } from './types';
|
|
5
6
|
import { Field, Select, Input, EnvBadge, ApiKeyInput } from './Primitives';
|
|
6
7
|
|
|
8
|
+
type TestState = 'idle' | 'testing' | 'ok' | 'error';
|
|
9
|
+
type ErrorCode = 'auth_error' | 'model_not_found' | 'rate_limited' | 'network_error' | 'unknown';
|
|
10
|
+
|
|
11
|
+
interface TestResult {
|
|
12
|
+
state: TestState;
|
|
13
|
+
latency?: number;
|
|
14
|
+
error?: string;
|
|
15
|
+
code?: ErrorCode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function errorMessage(t: any, code?: ErrorCode): string {
|
|
19
|
+
switch (code) {
|
|
20
|
+
case 'auth_error': return t.settings.ai.testKeyAuthError;
|
|
21
|
+
case 'model_not_found': return t.settings.ai.testKeyModelNotFound;
|
|
22
|
+
case 'rate_limited': return t.settings.ai.testKeyRateLimited;
|
|
23
|
+
case 'network_error': return t.settings.ai.testKeyNetworkError;
|
|
24
|
+
default: return t.settings.ai.testKeyUnknown;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
7
28
|
interface AiTabProps {
|
|
8
29
|
data: SettingsData;
|
|
9
30
|
updateAi: (patch: Partial<AiSettings>) => void;
|
|
@@ -15,13 +36,76 @@ export function AiTab({ data, updateAi, t }: AiTabProps) {
|
|
|
15
36
|
const envVal = data.envValues ?? {};
|
|
16
37
|
const provider = data.ai.provider;
|
|
17
38
|
|
|
18
|
-
|
|
39
|
+
// --- Test key state ---
|
|
40
|
+
const [testResult, setTestResult] = useState<Record<string, TestResult>>({});
|
|
41
|
+
const okTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
42
|
+
const prevProviderRef = useRef(provider);
|
|
43
|
+
|
|
44
|
+
// Reset test result when provider changes
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (prevProviderRef.current !== provider) {
|
|
47
|
+
prevProviderRef.current = provider;
|
|
48
|
+
setTestResult({});
|
|
49
|
+
if (okTimerRef.current) { clearTimeout(okTimerRef.current); okTimerRef.current = undefined; }
|
|
50
|
+
}
|
|
51
|
+
}, [provider]);
|
|
52
|
+
|
|
53
|
+
// Cleanup ok timer
|
|
54
|
+
useEffect(() => () => { if (okTimerRef.current) clearTimeout(okTimerRef.current); }, []);
|
|
55
|
+
|
|
56
|
+
const handleTestKey = useCallback(async (providerName: 'anthropic' | 'openai') => {
|
|
57
|
+
const prov = data.ai.providers?.[providerName] ?? {} as ProviderConfig;
|
|
58
|
+
setTestResult(prev => ({ ...prev, [providerName]: { state: 'testing' } }));
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const body: Record<string, string> = { provider: providerName };
|
|
62
|
+
if (prov.apiKey) body.apiKey = prov.apiKey;
|
|
63
|
+
if (prov.model) body.model = prov.model;
|
|
64
|
+
if (providerName === 'openai' && prov.baseUrl) body.baseUrl = prov.baseUrl;
|
|
65
|
+
|
|
66
|
+
const res = await fetch('/api/settings/test-key', {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: { 'Content-Type': 'application/json' },
|
|
69
|
+
body: JSON.stringify(body),
|
|
70
|
+
});
|
|
71
|
+
const json = await res.json();
|
|
72
|
+
|
|
73
|
+
if (json.ok) {
|
|
74
|
+
setTestResult(prev => ({ ...prev, [providerName]: { state: 'ok', latency: json.latency } }));
|
|
75
|
+
// Auto-clear after 5s
|
|
76
|
+
if (okTimerRef.current) clearTimeout(okTimerRef.current);
|
|
77
|
+
okTimerRef.current = setTimeout(() => {
|
|
78
|
+
setTestResult(prev => ({ ...prev, [providerName]: { state: 'idle' } }));
|
|
79
|
+
}, 5000);
|
|
80
|
+
} else {
|
|
81
|
+
setTestResult(prev => ({
|
|
82
|
+
...prev,
|
|
83
|
+
[providerName]: { state: 'error', error: json.error, code: json.code },
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
setTestResult(prev => ({
|
|
88
|
+
...prev,
|
|
89
|
+
[providerName]: { state: 'error', code: 'network_error', error: 'Network error' },
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
}, [data.ai.providers]);
|
|
93
|
+
|
|
94
|
+
// Reset test result when key changes
|
|
95
|
+
const patchProviderWithReset = useCallback((name: 'anthropic' | 'openai', patch: Partial<ProviderConfig>) => {
|
|
96
|
+
if ('apiKey' in patch) {
|
|
97
|
+
setTestResult(prev => ({ ...prev, [name]: { state: 'idle' } }));
|
|
98
|
+
}
|
|
19
99
|
updateAi({
|
|
20
100
|
providers: {
|
|
21
101
|
...data.ai.providers,
|
|
22
102
|
[name]: { ...data.ai.providers?.[name], ...patch },
|
|
23
103
|
},
|
|
24
104
|
});
|
|
105
|
+
}, [data.ai.providers, updateAi]);
|
|
106
|
+
|
|
107
|
+
function patchProvider(name: 'anthropic' | 'openai', patch: Partial<ProviderConfig>) {
|
|
108
|
+
patchProviderWithReset(name, patch);
|
|
25
109
|
}
|
|
26
110
|
|
|
27
111
|
const anthropic = data.ai.providers?.anthropic ?? { apiKey: '', model: '' };
|
|
@@ -31,6 +115,38 @@ export function AiTab({ data, updateAi, t }: AiTabProps) {
|
|
|
31
115
|
const activeEnvKey = provider === 'anthropic' ? env.ANTHROPIC_API_KEY : env.OPENAI_API_KEY;
|
|
32
116
|
const missingApiKey = !activeApiKey && !activeEnvKey;
|
|
33
117
|
|
|
118
|
+
// Test button helper
|
|
119
|
+
const renderTestButton = (providerName: 'anthropic' | 'openai', hasKey: boolean, hasEnv: boolean) => {
|
|
120
|
+
const result = testResult[providerName] ?? { state: 'idle' as TestState };
|
|
121
|
+
const disabled = result.state === 'testing' || (!hasKey && !hasEnv);
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div className="flex items-center gap-2 mt-1.5">
|
|
125
|
+
<button
|
|
126
|
+
type="button"
|
|
127
|
+
disabled={disabled}
|
|
128
|
+
onClick={() => handleTestKey(providerName)}
|
|
129
|
+
className="inline-flex items-center gap-1.5 px-2.5 py-1 text-xs rounded-md border border-border text-muted-foreground hover:text-foreground hover:border-foreground/20 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
|
130
|
+
>
|
|
131
|
+
{result.state === 'testing' ? (
|
|
132
|
+
<>
|
|
133
|
+
<Loader2 size={12} className="animate-spin" />
|
|
134
|
+
{t.settings.ai.testKeyTesting}
|
|
135
|
+
</>
|
|
136
|
+
) : (
|
|
137
|
+
t.settings.ai.testKey
|
|
138
|
+
)}
|
|
139
|
+
</button>
|
|
140
|
+
{result.state === 'ok' && result.latency != null && (
|
|
141
|
+
<span className="text-xs text-success">{t.settings.ai.testKeyOk(result.latency)}</span>
|
|
142
|
+
)}
|
|
143
|
+
{result.state === 'error' && (
|
|
144
|
+
<span className="text-xs text-error">✗ {errorMessage(t, result.code)}</span>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
|
|
34
150
|
return (
|
|
35
151
|
<div className="space-y-5">
|
|
36
152
|
<Field label={<>{t.settings.ai.provider} <EnvBadge overridden={env.AI_PROVIDER} /></>}>
|
|
@@ -60,6 +176,7 @@ export function AiTab({ data, updateAi, t }: AiTabProps) {
|
|
|
60
176
|
value={anthropic.apiKey}
|
|
61
177
|
onChange={v => patchProvider('anthropic', { apiKey: v })}
|
|
62
178
|
/>
|
|
179
|
+
{renderTestButton('anthropic', !!anthropic.apiKey, !!env.ANTHROPIC_API_KEY)}
|
|
63
180
|
</Field>
|
|
64
181
|
</>
|
|
65
182
|
) : (
|
|
@@ -79,6 +196,7 @@ export function AiTab({ data, updateAi, t }: AiTabProps) {
|
|
|
79
196
|
value={openai.apiKey}
|
|
80
197
|
onChange={v => patchProvider('openai', { apiKey: v })}
|
|
81
198
|
/>
|
|
199
|
+
{renderTestButton('openai', !!openai.apiKey, !!env.OPENAI_API_KEY)}
|
|
82
200
|
</Field>
|
|
83
201
|
<Field
|
|
84
202
|
label={<>{t.settings.ai.baseUrl} <EnvBadge overridden={env.OPENAI_BASE_URL} /></>}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export async function register() {
|
|
2
|
+
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
|
3
|
+
const { readFileSync } = await import('fs');
|
|
4
|
+
const { join, resolve } = await import('path');
|
|
5
|
+
const { homedir } = await import('os');
|
|
6
|
+
try {
|
|
7
|
+
const configPath = join(homedir(), '.mindos', 'config.json');
|
|
8
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
9
|
+
if (config.sync?.enabled && config.mindRoot) {
|
|
10
|
+
// Resolve absolute path to avoid Turbopack bundling issues
|
|
11
|
+
const syncModule = resolve(process.cwd(), '..', 'bin', 'lib', 'sync.js');
|
|
12
|
+
const { startSyncDaemon } = await import(/* webpackIgnore: true */ syncModule);
|
|
13
|
+
await startSyncDaemon(config.mindRoot);
|
|
14
|
+
}
|
|
15
|
+
} catch {
|
|
16
|
+
// Sync not configured or failed to start — silently skip
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
package/app/lib/i18n.ts
CHANGED
|
@@ -119,6 +119,15 @@ export const messages = {
|
|
|
119
119
|
resetToEnv: 'Reset to env value',
|
|
120
120
|
restoreFromEnv: 'Restore from env',
|
|
121
121
|
noApiKey: 'API key is not set. AI features will be unavailable until you add one.',
|
|
122
|
+
testKey: 'Test',
|
|
123
|
+
testKeyTesting: 'Testing...',
|
|
124
|
+
testKeyOk: (ms: number) => `\u2713 ${ms}ms`,
|
|
125
|
+
testKeyAuthError: 'Invalid API key',
|
|
126
|
+
testKeyModelNotFound: 'Model not found',
|
|
127
|
+
testKeyRateLimited: 'Rate limited, try again later',
|
|
128
|
+
testKeyNetworkError: 'Network error',
|
|
129
|
+
testKeyNoKey: 'No API key configured',
|
|
130
|
+
testKeyUnknown: 'Test failed',
|
|
122
131
|
},
|
|
123
132
|
appearance: {
|
|
124
133
|
readingFont: 'Reading font',
|
|
@@ -514,6 +523,15 @@ export const messages = {
|
|
|
514
523
|
resetToEnv: '恢复为环境变量',
|
|
515
524
|
restoreFromEnv: '从环境变量恢复',
|
|
516
525
|
noApiKey: 'API 密钥未设置,AI 功能暂不可用,请在此填写。',
|
|
526
|
+
testKey: '测试',
|
|
527
|
+
testKeyTesting: '测试中...',
|
|
528
|
+
testKeyOk: (ms: number) => `\u2713 ${ms}ms`,
|
|
529
|
+
testKeyAuthError: 'API Key 无效',
|
|
530
|
+
testKeyModelNotFound: '模型不存在',
|
|
531
|
+
testKeyRateLimited: '请求频率限制,稍后重试',
|
|
532
|
+
testKeyNetworkError: '网络错误',
|
|
533
|
+
testKeyNoKey: '未配置 API Key',
|
|
534
|
+
testKeyUnknown: '测试失败',
|
|
517
535
|
},
|
|
518
536
|
appearance: {
|
|
519
537
|
readingFont: '正文字体',
|
package/app/next.config.ts
CHANGED
|
@@ -3,7 +3,7 @@ import path from "path";
|
|
|
3
3
|
|
|
4
4
|
const nextConfig: NextConfig = {
|
|
5
5
|
transpilePackages: ['github-slugger'],
|
|
6
|
-
serverExternalPackages: ['pdfjs-dist', 'pdf-parse'],
|
|
6
|
+
serverExternalPackages: ['pdfjs-dist', 'pdf-parse', 'chokidar'],
|
|
7
7
|
outputFileTracingRoot: path.join(__dirname),
|
|
8
8
|
turbopack: {
|
|
9
9
|
root: path.join(__dirname),
|
package/bin/cli.js
CHANGED
|
@@ -803,7 +803,14 @@ ${bold('Examples:')}
|
|
|
803
803
|
}
|
|
804
804
|
|
|
805
805
|
if (sub === 'now') {
|
|
806
|
-
|
|
806
|
+
try {
|
|
807
|
+
console.log(dim('Pulling...'));
|
|
808
|
+
manualSync(mindRoot);
|
|
809
|
+
console.log(green('✔ Sync complete'));
|
|
810
|
+
} catch (err) {
|
|
811
|
+
console.error(red(err.message));
|
|
812
|
+
process.exit(1);
|
|
813
|
+
}
|
|
807
814
|
return;
|
|
808
815
|
}
|
|
809
816
|
|
package/bin/lib/sync.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
4
5
|
import { CONFIG_PATH, MINDOS_DIR } from './constants.js';
|
|
5
6
|
import { bold, dim, cyan, green, red, yellow } from './colors.js';
|
|
6
7
|
|
|
@@ -129,12 +130,24 @@ function autoPull(mindRoot) {
|
|
|
129
130
|
}
|
|
130
131
|
}
|
|
131
132
|
}
|
|
133
|
+
|
|
134
|
+
// Retry any pending pushes (handles previous push failures)
|
|
135
|
+
try {
|
|
136
|
+
const unpushed = gitExec('git rev-list --count @{u}..HEAD', mindRoot);
|
|
137
|
+
if (parseInt(unpushed) > 0) {
|
|
138
|
+
execSync('git push', { cwd: mindRoot, stdio: 'pipe' });
|
|
139
|
+
saveSyncState({ ...loadSyncState(), lastSync: new Date().toISOString(), lastError: null });
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
// No upstream tracking or push failed — ignore silently, autoCommitAndPush handles primary pushes
|
|
143
|
+
}
|
|
132
144
|
}
|
|
133
145
|
|
|
134
146
|
// ── Exported API ────────────────────────────────────────────────────────────
|
|
135
147
|
|
|
136
148
|
let activeWatcher = null;
|
|
137
149
|
let activePullInterval = null;
|
|
150
|
+
let activeShutdownHandler = null;
|
|
138
151
|
|
|
139
152
|
/**
|
|
140
153
|
* Interactive sync init — configure remote git repo
|
|
@@ -187,18 +200,43 @@ export async function initSync(mindRoot, opts = {}) {
|
|
|
187
200
|
try { execSync('git checkout -b main', { cwd: mindRoot, stdio: 'pipe' }); } catch {}
|
|
188
201
|
}
|
|
189
202
|
|
|
203
|
+
// 1b. Ensure .gitignore exists
|
|
204
|
+
const gitignorePath = resolve(mindRoot, '.gitignore');
|
|
205
|
+
if (!existsSync(gitignorePath)) {
|
|
206
|
+
writeFileSync(gitignorePath, [
|
|
207
|
+
'# MindOS auto-generated',
|
|
208
|
+
'.DS_Store',
|
|
209
|
+
'Thumbs.db',
|
|
210
|
+
'*.tmp',
|
|
211
|
+
'*.bak',
|
|
212
|
+
'*.swp',
|
|
213
|
+
'*.sync-conflict',
|
|
214
|
+
'node_modules/',
|
|
215
|
+
'.obsidian/',
|
|
216
|
+
'',
|
|
217
|
+
].join('\n'), 'utf-8');
|
|
218
|
+
}
|
|
219
|
+
|
|
190
220
|
// Handle token for HTTPS
|
|
191
221
|
if (token && remoteUrl.startsWith('https://')) {
|
|
192
222
|
const urlObj = new URL(remoteUrl);
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
223
|
+
// Choose credential helper by platform
|
|
224
|
+
const platform = process.platform;
|
|
225
|
+
let helper;
|
|
226
|
+
if (platform === 'darwin') helper = 'osxkeychain';
|
|
227
|
+
else if (platform === 'win32') helper = 'manager';
|
|
228
|
+
else helper = 'store';
|
|
229
|
+
try { execSync(`git config credential.helper '${helper}'`, { cwd: mindRoot, stdio: 'pipe' }); } catch {}
|
|
230
|
+
// Store the credential via git credential approve
|
|
198
231
|
try {
|
|
199
232
|
const credInput = `protocol=${urlObj.protocol.replace(':', '')}\nhost=${urlObj.host}\nusername=oauth2\npassword=${token}\n\n`;
|
|
200
233
|
execSync('git credential approve', { cwd: mindRoot, input: credInput, stdio: 'pipe' });
|
|
201
234
|
} catch {}
|
|
235
|
+
// For 'store' helper, restrict file permissions AFTER credential file is created
|
|
236
|
+
if (helper === 'store') {
|
|
237
|
+
const credFile = resolve(process.env.HOME || homedir(), '.git-credentials');
|
|
238
|
+
try { execSync(`chmod 600 "${credFile}"`, { stdio: 'pipe' }); } catch {}
|
|
239
|
+
}
|
|
202
240
|
}
|
|
203
241
|
|
|
204
242
|
// 4. Set remote
|
|
@@ -257,6 +295,7 @@ export async function initSync(mindRoot, opts = {}) {
|
|
|
257
295
|
* Start file watcher + periodic pull
|
|
258
296
|
*/
|
|
259
297
|
export async function startSyncDaemon(mindRoot) {
|
|
298
|
+
if (activeWatcher) return null; // already running — idempotent guard
|
|
260
299
|
const config = loadSyncConfig();
|
|
261
300
|
if (!config.enabled) return null;
|
|
262
301
|
if (!mindRoot || !isGitRepo(mindRoot)) return null;
|
|
@@ -281,10 +320,20 @@ export async function startSyncDaemon(mindRoot) {
|
|
|
281
320
|
// Pull on startup
|
|
282
321
|
autoPull(mindRoot);
|
|
283
322
|
|
|
323
|
+
// Graceful shutdown: flush pending changes before exit
|
|
324
|
+
const gracefulShutdown = () => {
|
|
325
|
+
if (commitTimer) { clearTimeout(commitTimer); commitTimer = null; }
|
|
326
|
+
try { autoCommitAndPush(mindRoot); } catch {}
|
|
327
|
+
stopSyncDaemon();
|
|
328
|
+
};
|
|
329
|
+
process.on('SIGTERM', gracefulShutdown);
|
|
330
|
+
process.on('SIGINT', gracefulShutdown);
|
|
331
|
+
|
|
284
332
|
activeWatcher = watcher;
|
|
285
333
|
activePullInterval = pullInterval;
|
|
334
|
+
activeShutdownHandler = gracefulShutdown;
|
|
286
335
|
|
|
287
|
-
return { watcher, pullInterval };
|
|
336
|
+
return { watcher, pullInterval, gracefulShutdown };
|
|
288
337
|
}
|
|
289
338
|
|
|
290
339
|
/**
|
|
@@ -299,6 +348,11 @@ export function stopSyncDaemon() {
|
|
|
299
348
|
clearInterval(activePullInterval);
|
|
300
349
|
activePullInterval = null;
|
|
301
350
|
}
|
|
351
|
+
if (activeShutdownHandler) {
|
|
352
|
+
process.removeListener('SIGTERM', activeShutdownHandler);
|
|
353
|
+
process.removeListener('SIGINT', activeShutdownHandler);
|
|
354
|
+
activeShutdownHandler = null;
|
|
355
|
+
}
|
|
302
356
|
}
|
|
303
357
|
|
|
304
358
|
/**
|
|
@@ -336,14 +390,10 @@ export function getSyncStatus(mindRoot) {
|
|
|
336
390
|
*/
|
|
337
391
|
export function manualSync(mindRoot) {
|
|
338
392
|
if (!mindRoot || !isGitRepo(mindRoot)) {
|
|
339
|
-
|
|
340
|
-
process.exit(1);
|
|
393
|
+
throw new Error('Not a git repository. Run `mindos sync init` first.');
|
|
341
394
|
}
|
|
342
|
-
console.log(dim('Pulling...'));
|
|
343
395
|
autoPull(mindRoot);
|
|
344
|
-
console.log(dim('Committing & pushing...'));
|
|
345
396
|
autoCommitAndPush(mindRoot);
|
|
346
|
-
console.log(green('✔ Sync complete'));
|
|
347
397
|
}
|
|
348
398
|
|
|
349
399
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geminilight/mindos",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.11",
|
|
4
4
|
"description": "MindOS — Human-Agent Collaborative Mind System. Local-first knowledge base that syncs your mind to all AI Agents via MCP.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mindos",
|
|
@@ -54,8 +54,10 @@
|
|
|
54
54
|
"!assets/capture-demo.mjs",
|
|
55
55
|
"!assets/demo-flow.html",
|
|
56
56
|
"!assets/demo-flow-zh.html",
|
|
57
|
+
"!assets/images",
|
|
57
58
|
"!mcp/node_modules",
|
|
58
|
-
"!mcp/dist"
|
|
59
|
+
"!mcp/dist",
|
|
60
|
+
"!mcp/package-lock.json"
|
|
59
61
|
],
|
|
60
62
|
"scripts": {
|
|
61
63
|
"setup": "node scripts/setup.js",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|