studylens 0.1.2 → 0.1.4
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 +46 -118
- package/bin/studylens.js +11 -7
- package/config/llm-config.template.json +9 -9
- package/config/prompts.json +7 -7
- package/core/extractor.js +70 -70
- package/core/llm-provider.js +8 -4
- package/core/wiki-storage.js +414 -414
- package/package.json +67 -62
- package/portal/dist/assets/{index-BdS0V2DX.js → index-C1LyiykQ.js} +18 -18
- package/portal/dist/index.html +12 -12
- package/portal/package.json +28 -28
- package/server/index.js +5 -2
- package/core/llm-provider.test.js +0 -92
package/portal/dist/index.html
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="zh-CN">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>StudyLens</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
</head>
|
|
9
|
-
<body>
|
|
10
|
-
<div id="root"></div>
|
|
11
|
-
</body>
|
|
12
|
-
</html>
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>StudyLens</title>
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-C1LyiykQ.js"></script>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
package/portal/package.json
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "studylens-portal",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"private": true,
|
|
5
|
-
"type": "module",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"dev": "vite --port 3001",
|
|
8
|
-
"build": "vite build",
|
|
9
|
-
"test": "vitest run",
|
|
10
|
-
"test:watch": "vitest"
|
|
11
|
-
},
|
|
12
|
-
"dependencies": {
|
|
13
|
-
"react": "^18.3.1",
|
|
14
|
-
"react-dom": "^18.3.1",
|
|
15
|
-
"react-force-graph-2d": "^1.25.0",
|
|
16
|
-
"react-router-dom": "^7.14.2"
|
|
17
|
-
},
|
|
18
|
-
"devDependencies": {
|
|
19
|
-
"@testing-library/jest-dom": "^6.9.1",
|
|
20
|
-
"@testing-library/react": "^16.3.2",
|
|
21
|
-
"@types/react": "^18.3.0",
|
|
22
|
-
"@vitejs/plugin-react": "^4.3.0",
|
|
23
|
-
"@vitest/coverage-v8": "^4.1.5",
|
|
24
|
-
"jsdom": "^29.1.1",
|
|
25
|
-
"vite": "^5.4.0",
|
|
26
|
-
"vitest": "^4.1.5"
|
|
27
|
-
}
|
|
28
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "studylens-portal",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite --port 3001",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"test": "vitest run",
|
|
10
|
+
"test:watch": "vitest"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"react": "^18.3.1",
|
|
14
|
+
"react-dom": "^18.3.1",
|
|
15
|
+
"react-force-graph-2d": "^1.25.0",
|
|
16
|
+
"react-router-dom": "^7.14.2"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
20
|
+
"@testing-library/react": "^16.3.2",
|
|
21
|
+
"@types/react": "^18.3.0",
|
|
22
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
23
|
+
"@vitest/coverage-v8": "^4.1.5",
|
|
24
|
+
"jsdom": "^29.1.1",
|
|
25
|
+
"vite": "^5.4.0",
|
|
26
|
+
"vitest": "^4.1.5"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/server/index.js
CHANGED
|
@@ -11,7 +11,7 @@ const app = express();
|
|
|
11
11
|
app.use(cors());
|
|
12
12
|
app.use(express.json({ limit: '10mb' }));
|
|
13
13
|
|
|
14
|
-
const uploadDir = path.join(__dirname, '..', 'uploads');
|
|
14
|
+
const uploadDir = process.env.STUDYLENS_UPLOAD_DIR || path.join(__dirname, '..', 'uploads');
|
|
15
15
|
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
|
|
16
16
|
const upload = multer({ dest: uploadDir, limits: { fileSize: 20 * 1024 * 1024 } });
|
|
17
17
|
|
|
@@ -530,9 +530,12 @@ app.post('/api/llm/test', async (req, res) => {
|
|
|
530
530
|
return res.json({ ok, message: ok ? 'Agent Maestro is reachable' : 'Agent Maestro is not reachable' });
|
|
531
531
|
}
|
|
532
532
|
|
|
533
|
+
const provider = llm.buildProvider(providerName, providerCfg);
|
|
534
|
+
if (!provider) return res.status(400).json({ error: `Unsupported provider: ${providerName}` });
|
|
535
|
+
|
|
533
536
|
const result = await llm.callLLM(
|
|
534
537
|
[{ role: 'user', content: 'Reply with exactly: OK' }],
|
|
535
|
-
{ maxTokens: 16, providers: [
|
|
538
|
+
{ maxTokens: 16, providers: [provider] }
|
|
536
539
|
);
|
|
537
540
|
res.json({ ok: true, message: `Response: ${result.slice(0, 100)}` });
|
|
538
541
|
} catch (err) {
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
const { extractJSON } = require('./llm-provider');
|
|
2
|
-
|
|
3
|
-
describe('extractJSON', () => {
|
|
4
|
-
// Regression: double-escaped regex bug (commit adc4ff5)
|
|
5
|
-
// Previously \s was written as \\s in source, causing regex to look for literal \s chars
|
|
6
|
-
it('strips markdown code fences with whitespace', () => {
|
|
7
|
-
const input = '```json\n[{"title": "Test"}]\n```';
|
|
8
|
-
const result = extractJSON(input, { isArray: true });
|
|
9
|
-
expect(result).toEqual([{ title: 'Test' }]);
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it('strips code fences with varying whitespace', () => {
|
|
13
|
-
const input = '```json \n{"answer": "hello"}\n``` ';
|
|
14
|
-
const result = extractJSON(input);
|
|
15
|
-
expect(result).toEqual({ answer: 'hello' });
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('returns null when no JSON found', () => {
|
|
19
|
-
expect(extractJSON('no json here', { isArray: true })).toBeNull();
|
|
20
|
-
expect(extractJSON('just some text')).toBeNull();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('parses clean JSON array', () => {
|
|
24
|
-
const result = extractJSON('[{"a": 1}, {"a": 2}]', { isArray: true });
|
|
25
|
-
expect(result).toEqual([{ a: 1 }, { a: 2 }]);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('parses clean JSON object', () => {
|
|
29
|
-
const result = extractJSON('{"answer": "hello", "cards": []}');
|
|
30
|
-
expect(result).toEqual({ answer: 'hello', cards: [] });
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('repairs trailing commas', () => {
|
|
34
|
-
const result = extractJSON('{"a": 1, "b": 2,}');
|
|
35
|
-
expect(result).toEqual({ a: 1, b: 2 });
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('repairs unescaped newlines in string values', () => {
|
|
39
|
-
const input = '{"answer": "line1\nline2", "cards": []}';
|
|
40
|
-
const result = extractJSON(input);
|
|
41
|
-
expect(result).toEqual({ answer: 'line1\nline2', cards: [] });
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
// Regression: AI decomposition returning empty (expandEntry)
|
|
45
|
-
it('uses repairKeys fallback when JSON has curly/smart quotes', () => {
|
|
46
|
-
const input = '[{“title”: “Foo”, “content”: “Bar”, “category”: “Baz”}]';
|
|
47
|
-
const result = extractJSON(input, { isArray: true, repairKeys: ['title', 'content', 'category'] });
|
|
48
|
-
expect(result).toEqual([{ title: 'Foo', content: 'Bar', category: 'Baz' }]);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('uses repairKeys for question/category pairs', () => {
|
|
52
|
-
const input = '[{“question”: “What is X?”, “category”: “概念”}]';
|
|
53
|
-
const result = extractJSON(input, { isArray: true, repairKeys: ['question', 'category'] });
|
|
54
|
-
expect(result).toEqual([{ question: 'What is X?', category: '概念' }]);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('repairs unescaped double quotes inside string values', () => {
|
|
58
|
-
const input = '[{"title": "形成\\"树瘤\\"的原因", "content": "正常"}]';
|
|
59
|
-
const raw = input.replace(/\\"/g, '"').replace(',"', '", "');
|
|
60
|
-
const mangled = '[{"title": "形成"树瘤"的原因", "content": "正常"}]';
|
|
61
|
-
const result = extractJSON(mangled, { isArray: true });
|
|
62
|
-
expect(result).not.toBeNull();
|
|
63
|
-
expect(result[0].title).toContain('树瘤');
|
|
64
|
-
expect(result[0].content).toBe('正常');
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('repairs multiple unescaped quotes in one string', () => {
|
|
68
|
-
const input = '{"answer": "He said "hello" and she said "bye"", "ok": true}';
|
|
69
|
-
const result = extractJSON(input);
|
|
70
|
-
expect(result).not.toBeNull();
|
|
71
|
-
expect(result.answer).toContain('hello');
|
|
72
|
-
expect(result.ok).toBe(true);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('handles already-escaped quotes without double-escaping', () => {
|
|
76
|
-
const input = '{"answer": "He said \\"hello\\"", "ok": true}';
|
|
77
|
-
const result = extractJSON(input);
|
|
78
|
-
expect(result).toEqual({ answer: 'He said "hello"', ok: true });
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('extracts JSON embedded in surrounding text', () => {
|
|
82
|
-
const input = 'Here is the result:\n[{"id": 1}]\nDone.';
|
|
83
|
-
const result = extractJSON(input, { isArray: true });
|
|
84
|
-
expect(result).toEqual([{ id: 1 }]);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('prefers array match when isArray is true', () => {
|
|
88
|
-
const input = '{"wrapper": [{"a": 1}]}';
|
|
89
|
-
const result = extractJSON(input, { isArray: true });
|
|
90
|
-
expect(result).toEqual([{ a: 1 }]);
|
|
91
|
-
});
|
|
92
|
-
});
|