@code2rich/jpage 1.5.0
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/.claude/settings.local.json +68 -0
- package/.dockerignore +8 -0
- package/.env.example +56 -0
- package/.github/workflows/ci.yml +43 -0
- package/CLAUDE.md +280 -0
- package/Dockerfile +44 -0
- package/LICENSE +21 -0
- package/README.md +433 -0
- package/README_EN.md +399 -0
- package/bin/args.js +64 -0
- package/bin/client.js +93 -0
- package/bin/commands/_shared.js +54 -0
- package/bin/commands/cat.js +23 -0
- package/bin/commands/ls.js +44 -0
- package/bin/commands/mv.js +20 -0
- package/bin/commands/rm.js +22 -0
- package/bin/commands/skills.js +70 -0
- package/bin/commands/star.js +23 -0
- package/bin/commands/tags.js +97 -0
- package/bin/commands/upload.js +84 -0
- package/bin/commands/url.js +25 -0
- package/bin/commands/whoami.js +29 -0
- package/bin/config.js +85 -0
- package/bin/jpage.js +168 -0
- package/build.js +112 -0
- package/docker-compose.yml +26 -0
- package/docs/api.md +438 -0
- package/docs/design/005-custom-modal.md +296 -0
- package/docs/design/013-file-version-history.md +324 -0
- package/docs/design/billing-system.md +600 -0
- package/docs/design/db-index-and-healthcheck.md +176 -0
- package/docs/design/loading-states.md +209 -0
- package/docs/virtual-hosting-feasibility.md +453 -0
- package/eslint.config.mjs +172 -0
- package/lib/auth-state.js +15 -0
- package/lib/categories.js +20 -0
- package/lib/crypto.js +85 -0
- package/lib/csp.js +66 -0
- package/lib/db.js +53 -0
- package/lib/dispatch.js +103 -0
- package/lib/fts.js +81 -0
- package/lib/middleware/auth.js +114 -0
- package/lib/middleware/files.js +42 -0
- package/lib/paths.js +9 -0
- package/lib/render-cache.js +48 -0
- package/lib/render.js +157 -0
- package/lib/templates.js +149 -0
- package/lib/util.js +66 -0
- package/lib/view-counts.js +59 -0
- package/lib/zip.js +192 -0
- package/logger.js +16 -0
- package/mailer.js +34 -0
- package/mcp/constants.js +16 -0
- package/mcp/resources.js +74 -0
- package/mcp/server.js +43 -0
- package/mcp/tools-categories.js +56 -0
- package/mcp/tools-content-templates.js +59 -0
- package/mcp/tools-files.js +245 -0
- package/mcp/tools-tags.js +41 -0
- package/mcp/tools-versions.js +57 -0
- package/mcp/transport.js +183 -0
- package/mcp/util.js +63 -0
- package/mcp-server.js +20 -0
- package/migrations/001_init_schema.js +25 -0
- package/migrations/002_add_share_key.js +33 -0
- package/migrations/003_add_roles_and_tokens.js +28 -0
- package/migrations/004_add_version_history.js +32 -0
- package/migrations/005_tags_starred_categories.js +49 -0
- package/migrations/006_zip_bundle.js +17 -0
- package/migrations/007_add_file_type_uploaded_by_indexes.js +7 -0
- package/migrations/008_add_fts5.js +6 -0
- package/migrations/009_add_link_visits.js +20 -0
- package/migrations/010_add_templates_system.js +34 -0
- package/migrations/011_content_templates.js +233 -0
- package/migrations/012_add_email_and_verification.js +35 -0
- package/migrations/013_add_token_encrypted.js +14 -0
- package/migrations.js +65 -0
- package/package.json +63 -0
- package/public/css/style.css +2915 -0
- package/public/index.html +855 -0
- package/public/js/api.js +22 -0
- package/public/js/app.js +94 -0
- package/public/js/components/dialog.js +106 -0
- package/public/js/components/toast.js +13 -0
- package/public/js/pages/content-templates.js +330 -0
- package/public/js/pages/home.js +1903 -0
- package/public/js/pages/landing.js +158 -0
- package/public/js/pages/login.js +175 -0
- package/public/js/pages/preview.js +713 -0
- package/public/js/theme.js +44 -0
- package/public/js/utils.js +67 -0
- package/routes/admin.js +136 -0
- package/routes/auth.js +365 -0
- package/routes/categories.js +90 -0
- package/routes/content-templates.js +215 -0
- package/routes/files/_shared.js +112 -0
- package/routes/files/associations.js +94 -0
- package/routes/files/crud.js +139 -0
- package/routes/files/detail-serve.js +178 -0
- package/routes/files/index.js +38 -0
- package/routes/files/list.js +200 -0
- package/routes/files/overwrite.js +114 -0
- package/routes/files/upload.js +204 -0
- package/routes/files/versions.js +166 -0
- package/routes/files.js +16 -0
- package/routes/skills.js +93 -0
- package/routes/tags.js +65 -0
- package/routes/tokens.js +110 -0
- package/routes/users.js +120 -0
- package/server.js +372 -0
- package/skills/jpage-content-template/SKILL.md +98 -0
- package/skills/jpage-upload/SKILL.md +247 -0
- package/skills-registry.js +135 -0
- package/templates/academic.html +41 -0
- package/templates/dark-pro.html +41 -0
- package/templates/default.html +56 -0
- package/templates/github.html +67 -0
- package/test/browser-harness.js +125 -0
- package/test/dispatch-bench.js +74 -0
- package/test/helpers/setup.js +45 -0
- package/test/integration/admin.test.js +108 -0
- package/test/integration/auth.test.js +93 -0
- package/test/integration/categories.test.js +103 -0
- package/test/integration/cli.test.js +310 -0
- package/test/integration/content-templates.test.js +147 -0
- package/test/integration/files-security.test.js +248 -0
- package/test/integration/files.test.js +139 -0
- package/test/integration/share.test.js +79 -0
- package/test/integration/skills.test.js +104 -0
- package/test/integration/tags.test.js +84 -0
- package/test/integration/tokens.test.js +89 -0
- package/test/integration/users.test.js +138 -0
- package/test/mcp-harness.js +152 -0
- package/test/perf-bench.js +108 -0
- package/test/perf-harness.js +198 -0
- package/test/run-server.sh +15 -0
- package/test/unit/cli-args.test.js +88 -0
- package/test/unit/cli-config.test.js +89 -0
- package/test/unit/crypto.test.js +100 -0
- package/test/unit/fts.test.js +52 -0
- package/test/unit/render-cache.test.js +76 -0
- package/test/unit/util.test.js +81 -0
- package/test/unit/zip.test.js +164 -0
package/mcp/constants.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// MCP 模块共享常量。从 mcp-server.js 提取,行为保持不变。
|
|
2
|
+
|
|
3
|
+
// 资源(jpage://file/{id})返回正文的大小上限:超过则提示改用 get_file_content 工具。
|
|
4
|
+
const RESOURCE_MAX_BYTES = 256 * 1024;
|
|
5
|
+
|
|
6
|
+
// upload_file 工具允许的文件扩展名(含 ZIP)。
|
|
7
|
+
const ALLOWED_EXTS = ['.html', '.htm', '.md', '.markdown', '.zip'];
|
|
8
|
+
|
|
9
|
+
// 上传单文件大小上限(与 routes/files 上传限制一致)。
|
|
10
|
+
const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
RESOURCE_MAX_BYTES,
|
|
14
|
+
ALLOWED_EXTS,
|
|
15
|
+
MAX_FILE_SIZE,
|
|
16
|
+
};
|
package/mcp/resources.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// MCP 资源:jpage://files(全部文件元数据)/ jpage://file/{id}(单文件内容)。
|
|
2
|
+
// 从 mcp-server.js 提取,行为保持不变。
|
|
3
|
+
|
|
4
|
+
const { ResourceTemplate } = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
5
|
+
const { RESOURCE_MAX_BYTES } = require('./constants');
|
|
6
|
+
|
|
7
|
+
function registerResources(server, { api }) {
|
|
8
|
+
// --- jpage://files ---
|
|
9
|
+
server.registerResource(
|
|
10
|
+
'files',
|
|
11
|
+
'jpage://files',
|
|
12
|
+
{
|
|
13
|
+
title: 'All Files',
|
|
14
|
+
description: 'jpage 中所有文件的元数据列表(id, name, type, size, is_public, created_at)。适用于快速浏览文件概况,无需逐个查询。',
|
|
15
|
+
mimeType: 'application/json',
|
|
16
|
+
},
|
|
17
|
+
async () => {
|
|
18
|
+
const data = await api.get('/api/files');
|
|
19
|
+
return {
|
|
20
|
+
contents: [
|
|
21
|
+
{
|
|
22
|
+
uri: 'jpage://files',
|
|
23
|
+
mimeType: 'application/json',
|
|
24
|
+
text: JSON.stringify(data.files, null, 2),
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// --- jpage://file/{id} ---
|
|
32
|
+
server.registerResource(
|
|
33
|
+
'file',
|
|
34
|
+
new ResourceTemplate('jpage://file/{id}', { list: undefined }),
|
|
35
|
+
{
|
|
36
|
+
title: 'Single File Content',
|
|
37
|
+
description:
|
|
38
|
+
'单文件内容(资源)。仅当文件 ≤ 256KB 时返回正文;超过则返回提示,让模型改用 get_file_content 工具。适用于 AI 上下文注入或轻量内容查看。',
|
|
39
|
+
mimeType: 'text/plain',
|
|
40
|
+
},
|
|
41
|
+
async (uri, vars) => {
|
|
42
|
+
const id = Number(vars.id);
|
|
43
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
44
|
+
throw new Error(`Invalid file id: ${vars.id}`);
|
|
45
|
+
}
|
|
46
|
+
const data = await api.get(`/api/files/${id}/content`);
|
|
47
|
+
const size = Buffer.byteLength(data.content, 'utf-8');
|
|
48
|
+
if (size > RESOURCE_MAX_BYTES) {
|
|
49
|
+
return {
|
|
50
|
+
contents: [
|
|
51
|
+
{
|
|
52
|
+
uri: uri.href,
|
|
53
|
+
mimeType: 'text/plain',
|
|
54
|
+
text:
|
|
55
|
+
`文件过大 (${size} 字节 > ${RESOURCE_MAX_BYTES} 字节),请改用 get_file_content 工具读取完整内容。`,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const mimeType = data.file_type === 'markdown' ? 'text/markdown' : 'text/html';
|
|
61
|
+
return {
|
|
62
|
+
contents: [
|
|
63
|
+
{
|
|
64
|
+
uri: uri.href,
|
|
65
|
+
mimeType,
|
|
66
|
+
text: data.content,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { registerResources };
|
package/mcp/server.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// MCP server 工厂:创建 McpServer 并注册全部 tools + resources。
|
|
2
|
+
// 从 mcp-server.js 提取,行为保持不变。
|
|
3
|
+
//
|
|
4
|
+
// 注册顺序 = 原文件顺序(list/upload/content/delete/rename/url/versions/tags/star/
|
|
5
|
+
// categories/resources/content-templates)。顺序对 MCP 协议无影响,仅为对照方便。
|
|
6
|
+
|
|
7
|
+
const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
8
|
+
const pkgVersion = require('../package.json').version;
|
|
9
|
+
const { registerFileTools } = require('./tools-files');
|
|
10
|
+
const { registerVersionTools } = require('./tools-versions');
|
|
11
|
+
const { registerTagTools } = require('./tools-tags');
|
|
12
|
+
const { registerCategoryTools } = require('./tools-categories');
|
|
13
|
+
const { registerContentTemplateTools } = require('./tools-content-templates');
|
|
14
|
+
const { registerResources } = require('./resources');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 创建并配置 MCP server(注册 17 tools + 2 resources)。
|
|
18
|
+
* @param {object} opts
|
|
19
|
+
* @param {number} opts.port
|
|
20
|
+
* @param {object} opts.api - 进程内 dispatcher 客户端({get,post,put,del})
|
|
21
|
+
* @param {string} opts.mcpIp
|
|
22
|
+
* @param {string} opts.protocol
|
|
23
|
+
* @returns {McpServer}
|
|
24
|
+
*/
|
|
25
|
+
function createMcpServer({ port, api, mcpIp, protocol }) {
|
|
26
|
+
const server = new McpServer(
|
|
27
|
+
{ name: 'jpage', version: pkgVersion },
|
|
28
|
+
{ capabilities: {} }
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const ctx = { api, port, mcpIp, protocol };
|
|
32
|
+
|
|
33
|
+
registerFileTools(server, ctx);
|
|
34
|
+
registerVersionTools(server, ctx);
|
|
35
|
+
registerTagTools(server, ctx);
|
|
36
|
+
registerCategoryTools(server, ctx);
|
|
37
|
+
registerResources(server, ctx);
|
|
38
|
+
registerContentTemplateTools(server, ctx);
|
|
39
|
+
|
|
40
|
+
return server;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { createMcpServer };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// MCP 分类类工具:list_categories / create_category / set_file_category。
|
|
2
|
+
// 从 mcp-server.js 提取,行为保持不变。
|
|
3
|
+
|
|
4
|
+
const { z } = require('zod');
|
|
5
|
+
const { textResult } = require('./util');
|
|
6
|
+
|
|
7
|
+
function registerCategoryTools(server, { api }) {
|
|
8
|
+
// --- list_categories ---
|
|
9
|
+
server.registerTool(
|
|
10
|
+
'list_categories',
|
|
11
|
+
{
|
|
12
|
+
title: 'List Categories',
|
|
13
|
+
description: '列出所有分类及其文件数量。',
|
|
14
|
+
inputSchema: {},
|
|
15
|
+
},
|
|
16
|
+
async () => {
|
|
17
|
+
const data = await api.get('/api/categories');
|
|
18
|
+
return textResult(data.categories);
|
|
19
|
+
}
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// --- create_category ---
|
|
23
|
+
server.registerTool(
|
|
24
|
+
'create_category',
|
|
25
|
+
{
|
|
26
|
+
title: 'Create Category',
|
|
27
|
+
description: '创建一个新分类(文件夹)。',
|
|
28
|
+
inputSchema: {
|
|
29
|
+
name: z.string().min(1).describe('分类名称'),
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
async ({ name }) => {
|
|
33
|
+
const data = await api.post('/api/categories', { name });
|
|
34
|
+
return textResult(data);
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// --- set_file_category ---
|
|
39
|
+
server.registerTool(
|
|
40
|
+
'set_file_category',
|
|
41
|
+
{
|
|
42
|
+
title: 'Set File Category',
|
|
43
|
+
description: '设置文件所属分类。传 null 或不传 categoryId 表示移除分类。',
|
|
44
|
+
inputSchema: {
|
|
45
|
+
fileId: z.number().int().positive().describe('文件 id'),
|
|
46
|
+
categoryId: z.number().int().positive().nullable().optional().describe('分类 id,null 表示移除分类'),
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
async ({ fileId, categoryId }) => {
|
|
50
|
+
await api.put(`/api/files/${fileId}/category`, { categoryId: categoryId ?? null });
|
|
51
|
+
return textResult({ fileId, categoryId: categoryId ?? null });
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { registerCategoryTools };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// MCP 内容模板类工具:list_content_templates / get_content_template。
|
|
2
|
+
// 从 mcp-server.js 提取,行为保持不变。
|
|
3
|
+
|
|
4
|
+
const { z } = require('zod');
|
|
5
|
+
const { textResult } = require('./util');
|
|
6
|
+
|
|
7
|
+
function registerContentTemplateTools(server, { api }) {
|
|
8
|
+
// --- list_content_templates ---
|
|
9
|
+
server.registerTool(
|
|
10
|
+
'list_content_templates',
|
|
11
|
+
{
|
|
12
|
+
title: 'List Content Templates',
|
|
13
|
+
description: '列出内容模板市场的模板。返回模板元数据(不含完整内容),供 AI 选择合适的风格参考。支持按场景、文件类型、关键词筛选。',
|
|
14
|
+
inputSchema: {
|
|
15
|
+
scene: z.string().optional().describe('按使用场景筛选:dashboard, report, resume, landing, note, presentation, card, email, other'),
|
|
16
|
+
fileType: z.enum(['html', 'markdown']).optional().describe('按文件类型筛选'),
|
|
17
|
+
keyword: z.string().optional().describe('按标题或描述搜索'),
|
|
18
|
+
sort: z.enum(['use_count', 'created_at']).optional().describe('排序方式,默认 use_count(最热门优先)'),
|
|
19
|
+
limit: z.number().optional().describe('返回数量,默认 10,最大 20'),
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
async ({ scene, fileType, keyword, sort, limit }) => {
|
|
23
|
+
const params = new URLSearchParams();
|
|
24
|
+
if (scene) params.set('scene', scene);
|
|
25
|
+
if (fileType) params.set('fileType', fileType);
|
|
26
|
+
if (keyword) params.set('keyword', keyword);
|
|
27
|
+
if (sort) params.set('sort', sort);
|
|
28
|
+
if (limit) params.set('limit', String(Math.min(limit, 20)));
|
|
29
|
+
const qs = params.toString();
|
|
30
|
+
const data = await api.get('/api/content-templates' + (qs ? '?' + qs : ''));
|
|
31
|
+
return textResult(data.templates);
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// --- get_content_template ---
|
|
36
|
+
server.registerTool(
|
|
37
|
+
'get_content_template',
|
|
38
|
+
{
|
|
39
|
+
title: 'Get Content Template',
|
|
40
|
+
description: '获取指定内容模板的完整样例内容。AI 拿到样例后,应学习其风格、布局和结构,生成风格一致但内容全新的作品。',
|
|
41
|
+
inputSchema: {
|
|
42
|
+
id: z.number().int().positive().describe('模板 ID(list_content_templates 返回的 id)'),
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
async ({ id }) => {
|
|
46
|
+
const data = await api.get(`/api/content-templates/${id}/content`);
|
|
47
|
+
await api.post(`/api/content-templates/${id}/use`).catch(() => {});
|
|
48
|
+
return textResult({
|
|
49
|
+
id: data.id,
|
|
50
|
+
title: data.title,
|
|
51
|
+
file_type: data.file_type,
|
|
52
|
+
content: data.content,
|
|
53
|
+
hint: '请学习此样例的风格和结构,生成风格一致但内容全新的作品。不要复制样例的具体文字内容。',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { registerContentTemplateTools };
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// MCP 文件类工具:list_files / upload_file / get_file_content / delete_file /
|
|
2
|
+
// rename_file / get_file_url / star_file / unstar_file。
|
|
3
|
+
// 从 mcp-server.js 提取,行为保持不变。
|
|
4
|
+
//
|
|
5
|
+
// 统一接口:导出 register(server, ctx),ctx = { api, port, mcpIp, protocol }。
|
|
6
|
+
// api 是 lib/dispatch 的进程内客户端({get,post,put,del}),与 fetch 客户端同形。
|
|
7
|
+
|
|
8
|
+
const { z } = require('zod');
|
|
9
|
+
const { textResult, applyTagsAndCategory } = require('./util');
|
|
10
|
+
const { ALLOWED_EXTS, MAX_FILE_SIZE } = require('./constants');
|
|
11
|
+
|
|
12
|
+
function registerFileTools(server, { api, port, mcpIp, protocol }) {
|
|
13
|
+
// --- list_files ---
|
|
14
|
+
server.registerTool(
|
|
15
|
+
'list_files',
|
|
16
|
+
{
|
|
17
|
+
title: 'List Files',
|
|
18
|
+
description: '列出 jpage 中存储的所有 HTML/Markdown 文件元数据。适用于查看已上传文件列表、确认上传结果、或决定后续操作目标。支持分页和排序。',
|
|
19
|
+
inputSchema: {
|
|
20
|
+
page: z.number().optional().describe('页码(默认 1)'),
|
|
21
|
+
limit: z.number().optional().describe('每页数量(默认 20,最大 100)'),
|
|
22
|
+
sort: z.enum(['updated_at', 'created_at', 'original_name', 'size']).optional().describe('排序字段(默认 updated_at)'),
|
|
23
|
+
order: z.enum(['asc', 'desc']).optional().describe('排序方向(默认 desc)'),
|
|
24
|
+
keyword: z.string().optional().describe('按文件名搜索'),
|
|
25
|
+
category: z.string().optional().describe('按分类 ID 筛选,"uncategorized" 表示未分类'),
|
|
26
|
+
tag: z.string().optional().describe('按标签 ID 筛选'),
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
async ({ page, limit, sort, order, keyword, category, tag }) => {
|
|
30
|
+
const params = new URLSearchParams();
|
|
31
|
+
if (page) params.set('page', page);
|
|
32
|
+
if (limit) params.set('limit', Math.min(limit, 100));
|
|
33
|
+
if (sort) params.set('sort', sort);
|
|
34
|
+
if (order) params.set('order', order);
|
|
35
|
+
if (keyword) params.set('keyword', keyword);
|
|
36
|
+
if (category) params.set('category', category);
|
|
37
|
+
if (tag) params.set('tag', tag);
|
|
38
|
+
const qs = params.toString();
|
|
39
|
+
const data = await api.get('/api/files' + (qs ? '?' + qs : ''));
|
|
40
|
+
return textResult({ files: data.files, pagination: data.pagination });
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// --- upload_file ---
|
|
45
|
+
server.registerTool(
|
|
46
|
+
'upload_file',
|
|
47
|
+
{
|
|
48
|
+
title: 'Upload File',
|
|
49
|
+
description:
|
|
50
|
+
'上传 HTML、Markdown 或 ZIP 文件到 jpage。ZIP 支持两种模式:网站包(含 index.html + 资源,作为整体预览)和批量上传(多个独立 HTML/MD,各自创建记录)。' +
|
|
51
|
+
'非 ZIP 文件类型按扩展名自动识别。返回的 url 字段是可公开访问的预览地址。适用于将生成的报告、笔记、可视化页面等内容上传分享。',
|
|
52
|
+
inputSchema: {
|
|
53
|
+
name: z.string().describe('文件名,需带扩展名,例如 "report.html" 或 "note.md"'),
|
|
54
|
+
content: z.string().describe('文件正文,UTF-8 字符串'),
|
|
55
|
+
isPublic: z
|
|
56
|
+
.boolean()
|
|
57
|
+
.optional()
|
|
58
|
+
.describe('是否公开可访问(无需登录)。默认 true。'),
|
|
59
|
+
overwriteFileId: z
|
|
60
|
+
.number()
|
|
61
|
+
.int()
|
|
62
|
+
.positive()
|
|
63
|
+
.optional()
|
|
64
|
+
.describe('显式指定覆盖目标文件 ID。提供时调用覆盖上传 API(POST /api/files/:id/overwrite-json),不提供时走同名自动覆盖逻辑。'),
|
|
65
|
+
tags: z
|
|
66
|
+
.array(z.string())
|
|
67
|
+
.optional()
|
|
68
|
+
.describe('标签名列表,上传后自动设置。标签不存在时自动创建。'),
|
|
69
|
+
categoryId: z
|
|
70
|
+
.number()
|
|
71
|
+
.int()
|
|
72
|
+
.positive()
|
|
73
|
+
.optional()
|
|
74
|
+
.describe('分类 id,上传后将文件归入该分类。'),
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
async ({ name, content, isPublic, overwriteFileId, tags, categoryId }) => {
|
|
78
|
+
const ext = (name.match(/\.[^.]+$/) || [''])[0].toLowerCase();
|
|
79
|
+
|
|
80
|
+
// ZIP 文件:content 为 base64 编码
|
|
81
|
+
if (ext === '.zip') {
|
|
82
|
+
const buf = Buffer.from(content, 'base64');
|
|
83
|
+
if (buf.length > MAX_FILE_SIZE) {
|
|
84
|
+
return textResult(`ZIP 文件过大 (${buf.length} 字节),上限 50MB`, { isError: true });
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const data = await api.post('/api/files/upload-zip-base64', {
|
|
88
|
+
name,
|
|
89
|
+
content,
|
|
90
|
+
isPublic: isPublic ?? true,
|
|
91
|
+
});
|
|
92
|
+
if (data.type === 'batch') {
|
|
93
|
+
for (const f of data.files) {
|
|
94
|
+
await applyTagsAndCategory(api, f.id, tags, categoryId);
|
|
95
|
+
}
|
|
96
|
+
return textResult({
|
|
97
|
+
type: 'batch',
|
|
98
|
+
count: data.count,
|
|
99
|
+
files: data.files,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
await applyTagsAndCategory(api, data.id, tags, categoryId);
|
|
103
|
+
return textResult({
|
|
104
|
+
...data,
|
|
105
|
+
url: data.share_key ? `${protocol}://${mcpIp}:${port}/s/${data.share_key}` : `${protocol}://${mcpIp}:${port}/api/files/${data.id}/render`,
|
|
106
|
+
});
|
|
107
|
+
} catch (e) {
|
|
108
|
+
return textResult(`ZIP 上传失败: ${e.message}`, { isError: true });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!ALLOWED_EXTS.includes(ext)) {
|
|
113
|
+
return textResult(
|
|
114
|
+
`不支持的文件扩展名: ${ext}。仅允许 ${ALLOWED_EXTS.join(', ')}`,
|
|
115
|
+
{ isError: true }
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
const size = Buffer.byteLength(content, 'utf-8');
|
|
119
|
+
if (size > MAX_FILE_SIZE) {
|
|
120
|
+
return textResult(`文件过大 (${size} 字节),上限 50MB`, { isError: true });
|
|
121
|
+
}
|
|
122
|
+
const uploadPath = overwriteFileId
|
|
123
|
+
? `/api/files/${overwriteFileId}/overwrite-json`
|
|
124
|
+
: '/api/files/upload-json';
|
|
125
|
+
const data = await api.post(uploadPath, {
|
|
126
|
+
name,
|
|
127
|
+
content,
|
|
128
|
+
isPublic: isPublic ?? true,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await applyTagsAndCategory(api, data.id, tags, categoryId);
|
|
132
|
+
|
|
133
|
+
return textResult({
|
|
134
|
+
...data,
|
|
135
|
+
url: data.share_key ? `${protocol}://${mcpIp}:${port}/s/${data.share_key}` : `${protocol}://${mcpIp}:${port}/api/files/${data.id}/render`,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// --- get_file_content ---
|
|
141
|
+
server.registerTool(
|
|
142
|
+
'get_file_content',
|
|
143
|
+
{
|
|
144
|
+
title: 'Get File Content',
|
|
145
|
+
description: '读取指定 id 的文件原始内容(UTF-8 文本)。适用于查看或编辑已有文件内容,不限文件大小。不支持网站包(bundle)类型的 ZIP——list_files 中 is_bundle=1 的文件请改用 get_file_url 获取预览链接。',
|
|
146
|
+
inputSchema: {
|
|
147
|
+
id: z.number().int().positive().describe('文件 id(list_files 返回的 id 字段)'),
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
async ({ id }) => {
|
|
151
|
+
const data = await api.get(`/api/files/${id}/content`);
|
|
152
|
+
return textResult({
|
|
153
|
+
id: data.id,
|
|
154
|
+
original_name: data.original_name,
|
|
155
|
+
file_type: data.file_type,
|
|
156
|
+
size: Buffer.byteLength(data.content, 'utf-8'),
|
|
157
|
+
content: data.content,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// --- delete_file ---
|
|
163
|
+
server.registerTool(
|
|
164
|
+
'delete_file',
|
|
165
|
+
{
|
|
166
|
+
title: 'Delete File',
|
|
167
|
+
description: '删除指定 id 的文件(同时移除数据库记录与磁盘文件)。适用于清理不需要的页面。此操作不可撤销。',
|
|
168
|
+
inputSchema: {
|
|
169
|
+
id: z.number().int().positive().describe('文件 id'),
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
async ({ id }) => {
|
|
173
|
+
const data = await api.del(`/api/files/${id}`);
|
|
174
|
+
return textResult({ id, ...data });
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// --- rename_file ---
|
|
179
|
+
server.registerTool(
|
|
180
|
+
'rename_file',
|
|
181
|
+
{
|
|
182
|
+
title: 'Rename File',
|
|
183
|
+
description: '修改指定 id 的文件名(仅 original_name 字段,不影响磁盘存储名)。适用于修正文件名或更改显示标题。',
|
|
184
|
+
inputSchema: {
|
|
185
|
+
id: z.number().int().positive().describe('文件 id'),
|
|
186
|
+
name: z.string().min(1).describe('新文件名,需带扩展名'),
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
async ({ id, name }) => {
|
|
190
|
+
const data = await api.put(`/api/files/${id}`, { name });
|
|
191
|
+
return textResult({ id, name, ...data });
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// --- get_file_url ---
|
|
196
|
+
server.registerTool(
|
|
197
|
+
'get_file_url',
|
|
198
|
+
{
|
|
199
|
+
title: 'Get File Public URL',
|
|
200
|
+
description: '返回指定 id 的公开预览短链接(/s/:key)。适用于获取分享链接,无需读取文件内容。',
|
|
201
|
+
inputSchema: {
|
|
202
|
+
id: z.number().int().positive().describe('文件 id'),
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
async ({ id }) => {
|
|
206
|
+
const data = await api.get(`/api/files/${id}`);
|
|
207
|
+
const url = data.share_key ? `${protocol}://${mcpIp}:${port}/s/${data.share_key}` : `${protocol}://${mcpIp}:${port}/api/files/${id}/render`;
|
|
208
|
+
return textResult({ id, url, share_key: data.share_key || null });
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// --- star_file ---
|
|
213
|
+
server.registerTool(
|
|
214
|
+
'star_file',
|
|
215
|
+
{
|
|
216
|
+
title: 'Star File',
|
|
217
|
+
description: '收藏指定文件。',
|
|
218
|
+
inputSchema: {
|
|
219
|
+
fileId: z.number().int().positive().describe('文件 id'),
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
async ({ fileId }) => {
|
|
223
|
+
await api.post(`/api/files/${fileId}/star`);
|
|
224
|
+
return textResult({ fileId, starred: true });
|
|
225
|
+
}
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// --- unstar_file ---
|
|
229
|
+
server.registerTool(
|
|
230
|
+
'unstar_file',
|
|
231
|
+
{
|
|
232
|
+
title: 'Unstar File',
|
|
233
|
+
description: '取消收藏指定文件。',
|
|
234
|
+
inputSchema: {
|
|
235
|
+
fileId: z.number().int().positive().describe('文件 id'),
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
async ({ fileId }) => {
|
|
239
|
+
await api.del(`/api/files/${fileId}/star`);
|
|
240
|
+
return textResult({ fileId, starred: false });
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
module.exports = { registerFileTools };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// MCP 标签类工具:list_tags / add_tags_to_file。
|
|
2
|
+
// 从 mcp-server.js 提取,行为保持不变。
|
|
3
|
+
|
|
4
|
+
const { z } = require('zod');
|
|
5
|
+
const { textResult, resolveTagIds } = require('./util');
|
|
6
|
+
|
|
7
|
+
function registerTagTools(server, { api }) {
|
|
8
|
+
// --- list_tags ---
|
|
9
|
+
server.registerTool(
|
|
10
|
+
'list_tags',
|
|
11
|
+
{
|
|
12
|
+
title: 'List Tags',
|
|
13
|
+
description: '列出所有标签及其关联文件数量。',
|
|
14
|
+
inputSchema: {},
|
|
15
|
+
},
|
|
16
|
+
async () => {
|
|
17
|
+
const data = await api.get('/api/tags');
|
|
18
|
+
return textResult(data.tags);
|
|
19
|
+
}
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// --- add_tags_to_file ---
|
|
23
|
+
server.registerTool(
|
|
24
|
+
'add_tags_to_file',
|
|
25
|
+
{
|
|
26
|
+
title: 'Add Tags to File',
|
|
27
|
+
description: '为指定文件设置标签(替换现有标签)。标签不存在时会自动创建。',
|
|
28
|
+
inputSchema: {
|
|
29
|
+
fileId: z.number().int().positive().describe('文件 id'),
|
|
30
|
+
tags: z.array(z.string()).describe('标签名列表,如 ["报告", "Q3"]'),
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
async ({ fileId, tags }) => {
|
|
34
|
+
const tagIds = await resolveTagIds(api, tags);
|
|
35
|
+
await api.put(`/api/files/${fileId}/tags`, { tagIds });
|
|
36
|
+
return textResult({ fileId, tags });
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { registerTagTools };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// MCP 版本类工具:list_file_versions / restore_file_version。
|
|
2
|
+
// 从 mcp-server.js 提取,行为保持不变。
|
|
3
|
+
|
|
4
|
+
const { z } = require('zod');
|
|
5
|
+
const { textResult, formatSize, formatTime } = require('./util');
|
|
6
|
+
|
|
7
|
+
function registerVersionTools(server, { api }) {
|
|
8
|
+
// --- list_file_versions ---
|
|
9
|
+
server.registerTool(
|
|
10
|
+
'list_file_versions',
|
|
11
|
+
{
|
|
12
|
+
title: 'List File Versions',
|
|
13
|
+
description: '列出指定文件的版本历史,包括当前版本信息。适用于查看文件修改历史、确认版本数量、或决定是否恢复到某个历史版本。',
|
|
14
|
+
inputSchema: {
|
|
15
|
+
fileId: z.number().positive().describe('文件 ID'),
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
async ({ fileId }) => {
|
|
19
|
+
const data = await api.get(`/api/files/${fileId}/versions`);
|
|
20
|
+
const current = data.current;
|
|
21
|
+
const versions = data.versions || [];
|
|
22
|
+
const currentSize = formatSize(current.size);
|
|
23
|
+
const updatedAt = formatTime(current.updated_at);
|
|
24
|
+
const lines = [`文件 #${fileId} 版本历史(共 ${versions.length} 个历史版本):`];
|
|
25
|
+
lines.push(`当前版本: ${currentSize}, 更新于 ${updatedAt}`);
|
|
26
|
+
for (const v of versions) {
|
|
27
|
+
const vSize = formatSize(v.size);
|
|
28
|
+
const vTime = formatTime(v.created_at);
|
|
29
|
+
lines.push(`v${v.version}: ${vSize}, ${vTime} [查看 | 恢复 | 删除]`);
|
|
30
|
+
}
|
|
31
|
+
return textResult(lines.join('\n'));
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// --- restore_file_version ---
|
|
36
|
+
server.registerTool(
|
|
37
|
+
'restore_file_version',
|
|
38
|
+
{
|
|
39
|
+
title: 'Restore File Version',
|
|
40
|
+
description: '恢复指定文件到某个历史版本。恢复后当前版本会被保存为新的历史版本,目标历史版本的内容成为新的当前版本。适用于撤销误修改或回退到之前的版本。',
|
|
41
|
+
inputSchema: {
|
|
42
|
+
fileId: z.number().positive().describe('文件 ID'),
|
|
43
|
+
version: z.number().positive().describe('要恢复的版本号'),
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
async ({ fileId, version }) => {
|
|
47
|
+
const data = await api.post(`/api/files/${fileId}/versions/${version}/restore`);
|
|
48
|
+
return textResult({
|
|
49
|
+
fileId,
|
|
50
|
+
restoredVersion: version,
|
|
51
|
+
...data,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { registerVersionTools };
|