@code2rich/jpage 1.5.0 → 1.5.1
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/.github/workflows/ci.yml +3 -1
- package/.github/workflows/release.yml +87 -0
- package/CLAUDE.md +3 -1
- package/README.md +26 -2
- package/bin/commands/ls.js +3 -1
- package/bin/commands/update.js +74 -0
- package/bin/jpage.js +7 -2
- package/docs/RELEASING.md +209 -0
- package/docs/skill-integration-design.md +384 -0
- package/eslint.config.mjs +2 -0
- package/lib/csp.js +8 -2
- package/lib/render.js +9 -2
- package/lib/templates.js +1 -1
- package/package.json +4 -4
- package/public/css/style.css +128 -1
- package/public/index.html +51 -3
- package/public/js/app.js +8 -6
- package/public/js/pages/content-templates.js +1 -1
- package/public/js/pages/home.js +218 -9
- package/public/js/pages/landing.js +1 -1
- package/public/js/pages/preview.js +1 -1
- package/public/js/utils.js +15 -7
- package/routes/skills.js +77 -3
- package/server.js +10 -3
- package/skills/jpage-presentation/INSTALL.md +50 -0
- package/skills/jpage-presentation/README.md +71 -0
- package/skills/jpage-presentation/SKILL.md +226 -0
- package/skills/jpage-presentation/assets/plugin/highlight/monokai.css +71 -0
- package/skills/jpage-presentation/assets/plugin/highlight/plugin.js +439 -0
- package/skills/jpage-presentation/assets/plugin/notes/notes.js +1 -0
- package/skills/jpage-presentation/assets/reveal-base.css +9 -0
- package/skills/jpage-presentation/assets/reveal.js +9 -0
- package/skills/jpage-presentation/assets/themes/academic.css +68 -0
- package/skills/jpage-presentation/assets/themes/business.css +64 -0
- package/skills/jpage-presentation/assets/themes/creative.css +81 -0
- package/skills/jpage-presentation/assets/themes/minimal.css +117 -0
- package/skills-registry.js +0 -6
- package/test/dispatch-bench.js +0 -3
- package/test/integration/cli.test.js +93 -0
- package/test/integration/skills.test.js +27 -5
- package/test/perf-harness.js +0 -9
- package/test/unit/fts.test.js +0 -1
- package/.claude/settings.local.json +0 -68
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jpage-presentation · 创意主题 (creative)
|
|
3
|
+
* 高饱和渐变 + 几何无衬线,适合产品发布、创意提案、活动 keynote。
|
|
4
|
+
*/
|
|
5
|
+
:root {
|
|
6
|
+
--r-background-color: #0f0e17;
|
|
7
|
+
--r-main-font: "Inter", -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
8
|
+
--r-main-color: #fffffe;
|
|
9
|
+
--r-heading-font: "Inter", -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
|
|
10
|
+
--r-heading-color: #fffffe;
|
|
11
|
+
--r-heading-text-transform: none;
|
|
12
|
+
--r-heading-font-weight: 800;
|
|
13
|
+
--r-heading-letter-spacing: -0.02em;
|
|
14
|
+
--r-link-color: #ff8906;
|
|
15
|
+
--r-link-color-hover: #f25f4c;
|
|
16
|
+
--r-selection-background-color: #e53170;
|
|
17
|
+
--r-selection-color: #fff;
|
|
18
|
+
--r-code-font: "JetBrains Mono", "SF Mono", Menlo, monospace;
|
|
19
|
+
--r-section-number-color: #ff8906;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.reveal {
|
|
23
|
+
font-size: 32px;
|
|
24
|
+
font-weight: 400;
|
|
25
|
+
background: linear-gradient(135deg, #0f0e17 0%, #1a1830 50%, #232946 100%);
|
|
26
|
+
background-attachment: fixed;
|
|
27
|
+
}
|
|
28
|
+
.reveal h1, .reveal h2, .reveal h3, .reveal h4 {
|
|
29
|
+
letter-spacing: -0.02em;
|
|
30
|
+
}
|
|
31
|
+
.reveal h1 {
|
|
32
|
+
font-size: 2.4em;
|
|
33
|
+
background: linear-gradient(90deg, #ff8906, #e53170);
|
|
34
|
+
-webkit-background-clip: text;
|
|
35
|
+
background-clip: text;
|
|
36
|
+
-webkit-text-fill-color: transparent;
|
|
37
|
+
}
|
|
38
|
+
.reveal h2 { font-size: 1.7em; color: #ff8906; }
|
|
39
|
+
.reveal h3 { font-size: 1.2em; color: #e53170; }
|
|
40
|
+
.reveal .slides section.cover h1 {
|
|
41
|
+
font-size: 3em;
|
|
42
|
+
}
|
|
43
|
+
.reveal .slides section.cover .subtitle {
|
|
44
|
+
color: #a7a9be;
|
|
45
|
+
font-size: 0.85em;
|
|
46
|
+
margin-top: 0.5em;
|
|
47
|
+
}
|
|
48
|
+
.reveal .slides section.divider {
|
|
49
|
+
text-align: center;
|
|
50
|
+
}
|
|
51
|
+
.reveal .slides section.divider h2 {
|
|
52
|
+
font-size: 2.2em;
|
|
53
|
+
background: linear-gradient(90deg, #ff8906, #e53170);
|
|
54
|
+
-webkit-background-clip: text;
|
|
55
|
+
background-clip: text;
|
|
56
|
+
-webkit-text-fill-color: transparent;
|
|
57
|
+
}
|
|
58
|
+
.reveal ul li::marker { color: #ff8906; }
|
|
59
|
+
.reveal ol li::marker { color: #e53170; font-weight: 700; }
|
|
60
|
+
.reveal strong { color: #ff8906; }
|
|
61
|
+
.reveal em { color: #e53170; }
|
|
62
|
+
.reveal blockquote {
|
|
63
|
+
border-left: 4px solid #e53170;
|
|
64
|
+
background: rgba(229, 49, 112, 0.1);
|
|
65
|
+
color: #fffffe;
|
|
66
|
+
padding: 0.5em 1em;
|
|
67
|
+
border-radius: 0 8px 8px 0;
|
|
68
|
+
}
|
|
69
|
+
.reveal pre {
|
|
70
|
+
background: rgba(255, 255, 255, 0.05);
|
|
71
|
+
border: 1px solid rgba(255, 137, 6, 0.3);
|
|
72
|
+
font-size: 0.55em;
|
|
73
|
+
border-radius: 8px;
|
|
74
|
+
}
|
|
75
|
+
.reveal code { color: #ff8906; }
|
|
76
|
+
.reveal a { text-decoration: underline; text-decoration-color: #e53170; }
|
|
77
|
+
.reveal table th {
|
|
78
|
+
background: rgba(229, 49, 112, 0.2);
|
|
79
|
+
border-bottom: 2px solid #e53170;
|
|
80
|
+
}
|
|
81
|
+
.reveal table td { border-bottom: 1px solid rgba(255, 255, 255, 0.1); }
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jpage-presentation · 极简主题 (minimal)
|
|
3
|
+
* 黑白 + 一个强调色 + 大字号留白,keynote/苹果风,适合极简主义、产品哲学分享。
|
|
4
|
+
*/
|
|
5
|
+
:root {
|
|
6
|
+
--r-background-color: #ffffff;
|
|
7
|
+
--r-main-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
8
|
+
--r-main-color: #1d1d1f;
|
|
9
|
+
--r-heading-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
10
|
+
--r-heading-color: #1d1d1f;
|
|
11
|
+
--r-heading-text-transform: none;
|
|
12
|
+
--r-heading-font-weight: 600;
|
|
13
|
+
--r-heading-letter-spacing: -0.03em;
|
|
14
|
+
--r-link-color: #0071e3;
|
|
15
|
+
--r-link-color-hover: #0058b9;
|
|
16
|
+
--r-selection-background-color: #0071e3;
|
|
17
|
+
--r-selection-color: #fff;
|
|
18
|
+
--r-code-font: "SF Mono", Menlo, Consolas, monospace;
|
|
19
|
+
--r-section-number-color: #86868b;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.reveal {
|
|
23
|
+
font-size: 34px;
|
|
24
|
+
font-weight: 400;
|
|
25
|
+
letter-spacing: -0.01em;
|
|
26
|
+
}
|
|
27
|
+
.reveal h1, .reveal h2, .reveal h3, .reveal h4 {
|
|
28
|
+
letter-spacing: -0.03em;
|
|
29
|
+
}
|
|
30
|
+
.reveal h1 { font-size: 2.6em; font-weight: 700; }
|
|
31
|
+
.reveal h2 { font-size: 1.8em; font-weight: 600; }
|
|
32
|
+
.reveal h3 { font-size: 1.25em; font-weight: 600; }
|
|
33
|
+
.reveal .slides section {
|
|
34
|
+
text-align: left;
|
|
35
|
+
padding: 0 0.5em;
|
|
36
|
+
}
|
|
37
|
+
.reveal .slides section.cover {
|
|
38
|
+
text-align: center;
|
|
39
|
+
}
|
|
40
|
+
.reveal .slides section.cover h1 {
|
|
41
|
+
font-size: 3.2em;
|
|
42
|
+
font-weight: 700;
|
|
43
|
+
margin-bottom: 0.2em;
|
|
44
|
+
}
|
|
45
|
+
.reveal .slides section.cover .subtitle {
|
|
46
|
+
color: #86868b;
|
|
47
|
+
font-size: 0.8em;
|
|
48
|
+
font-weight: 400;
|
|
49
|
+
}
|
|
50
|
+
.reveal .slides section.divider {
|
|
51
|
+
text-align: center;
|
|
52
|
+
}
|
|
53
|
+
.reveal .slides section.divider h2 {
|
|
54
|
+
font-size: 2.4em;
|
|
55
|
+
font-weight: 700;
|
|
56
|
+
}
|
|
57
|
+
.reveal ul, .reveal ol {
|
|
58
|
+
margin-left: 0;
|
|
59
|
+
list-style: none;
|
|
60
|
+
}
|
|
61
|
+
.reveal ul li {
|
|
62
|
+
position: relative;
|
|
63
|
+
padding-left: 1.2em;
|
|
64
|
+
margin-bottom: 0.5em;
|
|
65
|
+
}
|
|
66
|
+
.reveal ul li::before {
|
|
67
|
+
content: "";
|
|
68
|
+
position: absolute;
|
|
69
|
+
left: 0;
|
|
70
|
+
top: 0.55em;
|
|
71
|
+
width: 6px;
|
|
72
|
+
height: 6px;
|
|
73
|
+
border-radius: 50%;
|
|
74
|
+
background: #0071e3;
|
|
75
|
+
}
|
|
76
|
+
.reveal ol { counter-reset: item; }
|
|
77
|
+
.reveal ol li {
|
|
78
|
+
counter-increment: item;
|
|
79
|
+
padding-left: 1.8em;
|
|
80
|
+
position: relative;
|
|
81
|
+
margin-bottom: 0.5em;
|
|
82
|
+
}
|
|
83
|
+
.reveal ol li::before {
|
|
84
|
+
content: counter(item, decimal-leading-zero);
|
|
85
|
+
position: absolute;
|
|
86
|
+
left: 0;
|
|
87
|
+
color: #0071e3;
|
|
88
|
+
font-weight: 600;
|
|
89
|
+
font-size: 0.8em;
|
|
90
|
+
top: 0.15em;
|
|
91
|
+
}
|
|
92
|
+
.reveal strong { color: #0071e3; font-weight: 600; }
|
|
93
|
+
.reveal blockquote {
|
|
94
|
+
border-left: none;
|
|
95
|
+
font-size: 1.2em;
|
|
96
|
+
font-weight: 300;
|
|
97
|
+
color: #1d1d1f;
|
|
98
|
+
text-align: center;
|
|
99
|
+
padding: 0;
|
|
100
|
+
width: 100%;
|
|
101
|
+
}
|
|
102
|
+
.reveal pre {
|
|
103
|
+
background: #f5f5f7;
|
|
104
|
+
border: none;
|
|
105
|
+
border-radius: 12px;
|
|
106
|
+
font-size: 0.5em;
|
|
107
|
+
padding: 1em 1.2em;
|
|
108
|
+
}
|
|
109
|
+
.reveal code { color: #1d1d1f; }
|
|
110
|
+
.reveal table { font-size: 0.65em; border-collapse: collapse; }
|
|
111
|
+
.reveal th, .reveal td {
|
|
112
|
+
border: none;
|
|
113
|
+
border-bottom: 1px solid #d2d2d7;
|
|
114
|
+
padding: 0.6em 0.8em;
|
|
115
|
+
}
|
|
116
|
+
.reveal th { color: #86868b; font-weight: 600; font-size: 0.85em; }
|
|
117
|
+
.reveal .progress { color: #0071e3; }
|
package/skills-registry.js
CHANGED
|
@@ -53,12 +53,6 @@ function parseFrontmatter(text) {
|
|
|
53
53
|
return { meta, body: m[2] };
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
function dirSize(root) {
|
|
57
|
-
let total = 0;
|
|
58
|
-
for (const f of walkFiles(root)) total += f.size;
|
|
59
|
-
return total;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
56
|
function readSkillMeta(skillName) {
|
|
63
57
|
const skillRoot = path.join(SKILLS_DIR, skillName);
|
|
64
58
|
if (!fs.existsSync(skillRoot) || !fs.statSync(skillRoot).isDirectory()) return null;
|
package/test/dispatch-bench.js
CHANGED
|
@@ -8,7 +8,6 @@ process.env.ADMIN_USER = 'admin';
|
|
|
8
8
|
process.env.ADMIN_PASSWORD = 'testpassword123';
|
|
9
9
|
process.env.MCP_TOKEN = 'bench-mcp-token';
|
|
10
10
|
|
|
11
|
-
const path = require('path');
|
|
12
11
|
const fs = require('fs');
|
|
13
12
|
// 清理临时目录
|
|
14
13
|
fs.rmSync(process.env.JPAGE_DATA_DIR, { recursive: true, force: true });
|
|
@@ -22,7 +21,6 @@ const express = require('express');
|
|
|
22
21
|
async function main() {
|
|
23
22
|
const app = express();
|
|
24
23
|
app.use(express.json());
|
|
25
|
-
let calls = 0;
|
|
26
24
|
// 模拟 requireAuth:解析 Bearer token(模拟一次 DB 查询的延迟 ~真实场景)
|
|
27
25
|
app.use((req, res, next) => {
|
|
28
26
|
// 模拟鉴权开销(DB token 查询):真实场景约 0.3-0.8ms,这里用同步 CPU 占用近似
|
|
@@ -30,7 +28,6 @@ async function main() {
|
|
|
30
28
|
next();
|
|
31
29
|
});
|
|
32
30
|
app.get('/api/files', (req, res) => {
|
|
33
|
-
calls++;
|
|
34
31
|
res.json({ files: [{ id: 1, name: 'a' }, { id: 2, name: 'b' }], pagination: { total: 2 } });
|
|
35
32
|
});
|
|
36
33
|
|
|
@@ -308,3 +308,96 @@ test('CLI: --help 打印帮助', async () => {
|
|
|
308
308
|
assert.match(s.out(), /jpage —— 即页命令行/);
|
|
309
309
|
assert.match(s.out(), /upload/);
|
|
310
310
|
});
|
|
311
|
+
|
|
312
|
+
// --- update ---
|
|
313
|
+
// update 纯本地操作(npm 自更新),不调后端 API,用注入的 npmExec 假执行器,
|
|
314
|
+
// 避免测试真的跑 npm。默认让 view 返回一个比当前版本更高的版本号。
|
|
315
|
+
function makeFakeNpmExec(calls, { latestVersion } = {}) {
|
|
316
|
+
const current = require('../../package.json').version;
|
|
317
|
+
const latest = latestVersion !== undefined ? latestVersion : bumpVersion(current);
|
|
318
|
+
return (args) => {
|
|
319
|
+
calls.push(args);
|
|
320
|
+
if (args[0] === 'view') return latest + '\n';
|
|
321
|
+
return ''; // install 不输出
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// 给 x.y.z 的 patch 位 +1,造一个"比当前新"的版本号(保证不等)。
|
|
326
|
+
function bumpVersion(v) {
|
|
327
|
+
const [a, b, c] = v.split('.').map(Number);
|
|
328
|
+
return `${a}.${b}.${c + 1}`;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
test('CLI update: 发现新版本 → 自动更新', async () => {
|
|
332
|
+
const s = makeSinks();
|
|
333
|
+
const calls = [];
|
|
334
|
+
await run(['update'], ctx(s, {
|
|
335
|
+
env: {}, cwd: os.tmpdir(), npmExec: makeFakeNpmExec(calls),
|
|
336
|
+
}));
|
|
337
|
+
assert.strictEqual(s.code(), 0);
|
|
338
|
+
assert.match(s.out(), /发现新版本/);
|
|
339
|
+
assert.match(s.out(), /已更新/);
|
|
340
|
+
// 第二次调用是 install,应含 -g 和 @latest
|
|
341
|
+
const installCall = calls.find((a) => a[0] === 'install');
|
|
342
|
+
assert.ok(installCall, '应触发 npm install');
|
|
343
|
+
assert.ok(installCall.includes('-g'), '应全局安装');
|
|
344
|
+
assert.ok(installCall.includes('@code2rich/jpage@latest'), '应装 latest');
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test('CLI update: --check 只查不更新', async () => {
|
|
348
|
+
const s = makeSinks();
|
|
349
|
+
const calls = [];
|
|
350
|
+
await run(['update', '--check'], ctx(s, {
|
|
351
|
+
env: {}, cwd: os.tmpdir(), npmExec: makeFakeNpmExec(calls),
|
|
352
|
+
}));
|
|
353
|
+
assert.strictEqual(s.code(), 0);
|
|
354
|
+
assert.match(s.out(), /发现新版本/);
|
|
355
|
+
assert.doesNotMatch(s.out(), /已更新/);
|
|
356
|
+
// 只应有一次 view,不应有 install
|
|
357
|
+
assert.ok(calls.find((a) => a[0] === 'view'), '应查版本');
|
|
358
|
+
assert.ok(!calls.find((a) => a[0] === 'install'), '--check 不应触发 install');
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test('CLI update: 已是最新版', async () => {
|
|
362
|
+
const s = makeSinks();
|
|
363
|
+
const calls = [];
|
|
364
|
+
const current = require('../../package.json').version;
|
|
365
|
+
await run(['update'], ctx(s, {
|
|
366
|
+
env: {}, cwd: os.tmpdir(), npmExec: makeFakeNpmExec(calls, { latestVersion: current }),
|
|
367
|
+
}));
|
|
368
|
+
assert.strictEqual(s.code(), 0);
|
|
369
|
+
assert.match(s.out(), /已是最新版/);
|
|
370
|
+
assert.ok(!calls.find((a) => a[0] === 'install'), '无需 install');
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test('CLI update: --registry 透传给 npm', async () => {
|
|
374
|
+
const s = makeSinks();
|
|
375
|
+
const calls = [];
|
|
376
|
+
await run(['update', '--check', '--registry', 'https://registry.npmmirror.com'], ctx(s, {
|
|
377
|
+
env: {}, cwd: os.tmpdir(), npmExec: makeFakeNpmExec(calls),
|
|
378
|
+
}));
|
|
379
|
+
const viewCall = calls.find((a) => a[0] === 'view');
|
|
380
|
+
assert.ok(viewCall.includes('--registry'), 'view 应带 --registry');
|
|
381
|
+
assert.ok(viewCall.includes('https://registry.npmmirror.com'), 'registry 值应透传');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test('CLI update: --registry 缺值 → UsageError 退出 2', async () => {
|
|
385
|
+
const s = makeSinks();
|
|
386
|
+
const calls = [];
|
|
387
|
+
await run(['update', '--registry'], ctx(s, {
|
|
388
|
+
env: {}, cwd: os.tmpdir(), npmExec: makeFakeNpmExec(calls),
|
|
389
|
+
}));
|
|
390
|
+
assert.strictEqual(s.code(), 2);
|
|
391
|
+
assert.match(s.err(), /用法/);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test('CLI update: 不需要 token(无 token 也能跑)', async () => {
|
|
395
|
+
const s = makeSinks();
|
|
396
|
+
const calls = [];
|
|
397
|
+
// 故意不传 token、cwd 指向 /tmp(排除 .env 的 MCP_TOKEN)
|
|
398
|
+
await run(['update', '--check'], ctx(s, {
|
|
399
|
+
env: {}, cwd: os.tmpdir(), npmExec: makeFakeNpmExec(calls),
|
|
400
|
+
}));
|
|
401
|
+
assert.strictEqual(s.code(), 0);
|
|
402
|
+
assert.doesNotMatch(s.err(), /token/);
|
|
403
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Skills 集成测试:列表 / 详情 / 下载 zip / mcp/config
|
|
2
|
-
// 挂载点 /api(/skills、/skills/:name、/skills/:name/download、/mcp/config)。
|
|
1
|
+
// Skills 集成测试:列表 / 详情 / 下载 zip / mcp/config 结构 / cli 指南。全部 requireAuth。
|
|
2
|
+
// 挂载点 /api(/skills、/skills/:name、/skills/:name/download、/mcp/config、/cli/guide)。
|
|
3
3
|
// 依赖仓库内 skills/jpage-upload/SKILL.md(内置 skill)。
|
|
4
4
|
const test = require('node:test');
|
|
5
5
|
const assert = require('node:assert');
|
|
@@ -85,20 +85,42 @@ test('GET /api/mcp/config → 200,含 config.mcpServers.jpage', async () => {
|
|
|
85
85
|
assert.ok(Array.isArray(res.body.tokens));
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
-
test('GET /api/mcp/config → 200,含多客户端 configs
|
|
88
|
+
test('GET /api/mcp/config → 200,含多客户端 configs 数组(仅 MCP 客户端)', async () => {
|
|
89
89
|
const res = await agent.get('/api/mcp/config');
|
|
90
90
|
assert.strictEqual(res.status, 200);
|
|
91
91
|
assert.ok(Array.isArray(res.body.configs));
|
|
92
|
-
// 5
|
|
92
|
+
// 5 项:全部为 MCP 客户端(CLI 走独立的 /api/cli/guide,不再混入此处)
|
|
93
93
|
assert.strictEqual(res.body.configs.length, 5);
|
|
94
94
|
const ids = res.body.configs.map(c => c.id);
|
|
95
95
|
for (const id of ['claude-code', 'claude-desktop', 'cursor', 'zcode', 'generic']) {
|
|
96
96
|
assert.ok(ids.includes(id), `configs 应包含 ${id}`);
|
|
97
97
|
}
|
|
98
|
-
//
|
|
98
|
+
// CLI 不应再出现在 MCP 配置里
|
|
99
|
+
assert.ok(!ids.includes('cli'), 'configs 不应再包含 cli(已独立为 /api/cli/guide)');
|
|
100
|
+
// 每项含 label / path
|
|
99
101
|
res.body.configs.forEach(c => {
|
|
100
102
|
assert.ok(c.label, `${c.id} 应有 label`);
|
|
101
103
|
assert.ok('path' in c, `${c.id} 应有 path`);
|
|
104
|
+
});
|
|
105
|
+
// 每项都是 MCP 客户端,config.mcpServers.jpage 必有
|
|
106
|
+
res.body.configs.forEach(c => {
|
|
102
107
|
assert.ok(c.config && c.config.mcpServers && c.config.mcpServers.jpage, `${c.id} config 应含 mcpServers.jpage`);
|
|
103
108
|
});
|
|
104
109
|
});
|
|
110
|
+
|
|
111
|
+
test('GET /api/cli/guide → 200,返回 CLI 用法指南(与 MCP 并列的独立入口)', async () => {
|
|
112
|
+
const res = await agent.get('/api/cli/guide');
|
|
113
|
+
assert.strictEqual(res.status, 200);
|
|
114
|
+
assert.strictEqual(res.body.enabled, true);
|
|
115
|
+
assert.ok(typeof res.body.baseUrl === 'string' && res.body.baseUrl.length > 0, 'baseUrl 应为非空');
|
|
116
|
+
assert.ok(typeof res.body.guideHtml === 'string' && res.body.guideHtml.length > 0, 'guideHtml 应为非空 HTML');
|
|
117
|
+
assert.ok(res.body.guideHtml.includes('jpage'), 'guideHtml 应含 jpage 说明');
|
|
118
|
+
assert.ok(typeof res.body.guideText === 'string' && res.body.guideText.length > 0, 'guideText 应为非空文档');
|
|
119
|
+
// guideText 里 baseUrl 应已被替换为实际服务地址(不含 <baseUrl> 占位)
|
|
120
|
+
assert.ok(!res.body.guideText.includes('<baseUrl>'), 'guideText 不应残留 baseUrl 占位符');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('未登录 GET /api/cli/guide → 401', async () => {
|
|
124
|
+
const res = await request(env.app).get('/api/cli/guide');
|
|
125
|
+
assert.strictEqual(res.status, 401);
|
|
126
|
+
});
|
package/test/perf-harness.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
// 用法: node test/perf-harness.js [PORT]
|
|
3
3
|
// 退出码 0 = 全部通过, 非 0 = 有失败
|
|
4
4
|
const http = require('http');
|
|
5
|
-
const crypto = require('crypto');
|
|
6
5
|
|
|
7
6
|
const PORT = parseInt(process.argv[2] || process.env.PORT || '8890', 10);
|
|
8
7
|
const HOST = '127.0.0.1';
|
|
@@ -40,14 +39,6 @@ function req(method, path, { body, headers = {}, raw, formData } = {}) {
|
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
// 多部分表单:单字段 file
|
|
43
|
-
function multipart(boundary, field) {
|
|
44
|
-
const CRLF = '\r\n';
|
|
45
|
-
const pre = `--${boundary}${CRLF}Content-Disposition: form-data; name="file"; filename="${field.filename}"${CRLF}Content-Type: ${field.type || 'text/markdown'}${CRLF}${CRLF}`;
|
|
46
|
-
const post = `${CRLF}--${boundary}--${CRLF}`;
|
|
47
|
-
const body = Buffer.concat([Buffer.from(pre, 'utf8'), Buffer.from(field.content, 'utf8'), Buffer.from(post, 'utf8')]);
|
|
48
|
-
return { body, headers: { 'Content-Type': `multipart/form-data; boundary=${boundary}` } };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
42
|
async function run() {
|
|
52
43
|
console.log(`\n=== jpage 验证套件 (port ${PORT}) ===\n`);
|
|
53
44
|
|
package/test/unit/fts.test.js
CHANGED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(docker-compose up *)",
|
|
5
|
-
"Bash(docker compose up *)",
|
|
6
|
-
"Bash(lsof -ti:8858)",
|
|
7
|
-
"Bash(xargs kill -9)",
|
|
8
|
-
"Bash(ssh *)",
|
|
9
|
-
"Bash(docker compose ps *)",
|
|
10
|
-
"Bash(git add *)",
|
|
11
|
-
"Bash(git commit -m ' *)",
|
|
12
|
-
"Bash(git push *)",
|
|
13
|
-
"Bash(systemctl list-units *)",
|
|
14
|
-
"Bash(/snap/bin/docker ps *)",
|
|
15
|
-
"Bash(snap run *)",
|
|
16
|
-
"Bash(docker network *)",
|
|
17
|
-
"Bash(curl -s -o /dev/null -w \"HTTP %{http_code} in %{time_total}s\\\\n\" http://127.0.0.1:8858/ --max-time 5)",
|
|
18
|
-
"Bash(curl -s http://127.0.0.1:8858/ --max-time 5)",
|
|
19
|
-
"Bash(pm2 list *)",
|
|
20
|
-
"Bash(DOCKER_HOST=unix:///var/run/docker.sock docker ps -a --format '{{.ID}} {{.Names}} {{.Status}}')",
|
|
21
|
-
"Bash(tmux ls *)",
|
|
22
|
-
"Bash(screen -ls)",
|
|
23
|
-
"Bash(systemctl is-active *)",
|
|
24
|
-
"Read(//app/**)",
|
|
25
|
-
"Bash(set -e)",
|
|
26
|
-
"Bash(useradd -m -s /bin/bash jpage)",
|
|
27
|
-
"Bash(chpasswd)",
|
|
28
|
-
"Read(//home/**)",
|
|
29
|
-
"Bash(getent passwd *)",
|
|
30
|
-
"Bash(python3 *)",
|
|
31
|
-
"Bash(mkdir -p /home/jpage/jpage)",
|
|
32
|
-
"Bash(rsync -a /root/prod/jpage/ /home/jpage/jpage/)",
|
|
33
|
-
"Bash(sed -i -E 's/^[[:space:]]+//; s/[[:space:]]+#.*$//' /home/jpage/jpage/.env)",
|
|
34
|
-
"Bash(grep -nE '\\\\$' /home/jpage/jpage/.env)",
|
|
35
|
-
"Bash(sed -E 's/\\(ADMIN_PASSWORD|SESSION_SECRET|MCP_TOKEN|SMTP_PASS\\)=.*/\\\\1=<SECRET>/' /home/jpage/jpage/.env)",
|
|
36
|
-
"Bash(git commit *)",
|
|
37
|
-
"Bash(git config *)",
|
|
38
|
-
"Bash(git ls-remote *)",
|
|
39
|
-
"Bash(echo \"exit=$?\")",
|
|
40
|
-
"Bash(git remote *)",
|
|
41
|
-
"Bash(git *)",
|
|
42
|
-
"Bash(npm run *)",
|
|
43
|
-
"Bash(npm start *)",
|
|
44
|
-
"Bash(npm install *)",
|
|
45
|
-
"Bash(curl -s http://localhost:8858/)",
|
|
46
|
-
"Bash(curl -s -o /dev/null -w 'GET / -> %{http_code} \\(%{size_download} bytes, %{time_total}s\\)\\\\n' http://localhost:8858/)",
|
|
47
|
-
"Bash(curl -s -o /dev/null -w 'GET /api/auth/me -> %{http_code}\\\\n' http://localhost:8858/api/auth/me)",
|
|
48
|
-
"Bash(curl -s -o /dev/null -w 'GET /dist/app-DVM5JB4N.js -> %{http_code}\\\\n' http://localhost:8858/dist/app-DVM5JB4N.js)",
|
|
49
|
-
"Bash(curl -s -o /dev/null -w 'GET /dist/style-360db6ef.css -> %{http_code}\\\\n' http://localhost:8858/dist/style-360db6ef.css)",
|
|
50
|
-
"Bash(curl -s -o /dev/null -w 'GET /dist/app-HVUECAAZ.js -> %{http_code}\\\\n' http://localhost:8858/dist/app-HVUECAAZ.js)",
|
|
51
|
-
"Bash(curl -sS -m 4 -o /dev/null -w \"HTTP: %{http_code}\\\\n\" http://127.0.0.1:8858/)",
|
|
52
|
-
"Bash(ps -eo pid,etime,comm,args)",
|
|
53
|
-
"Bash(awk '$3==\"node\"')",
|
|
54
|
-
"Bash(bash -n .env)",
|
|
55
|
-
"Bash(curl -sS -m 5 -o /dev/null -w \"HTTP %{http_code}\\\\n\" http://127.0.0.1:8858/)",
|
|
56
|
-
"Bash(curl -sS -m 5 -o /dev/null -w \"HTTP %{http_code}\\\\n\" http://127.0.0.1:8858/api/auth/me)",
|
|
57
|
-
"Bash(curl -sS -m 5 http://127.0.0.1:8858/api/auth/registration-status)",
|
|
58
|
-
"Bash(ps -eo pid,ppid,etime,comm,args)",
|
|
59
|
-
"Bash(awk '$4==\"node\" && /server\\\\.js/')",
|
|
60
|
-
"Read(//tmp/**)",
|
|
61
|
-
"Bash(xargs -I{} echo \"警告出现次数: {}(应为 0)\")"
|
|
62
|
-
]
|
|
63
|
-
},
|
|
64
|
-
"enableAllProjectMcpServers": true,
|
|
65
|
-
"enabledMcpjsonServers": [
|
|
66
|
-
"task-master-ai"
|
|
67
|
-
]
|
|
68
|
-
}
|