nx-mcp-server 1.0.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.
@@ -0,0 +1,80 @@
1
+ import QRCode from 'qrcode';
2
+ import sharp from 'sharp';
3
+ import {saveOutput,loadImage} from '../utils/output.js';
4
+
5
+ export async function generateQrcode(args){
6
+ const {content,margin=4,darkColor='#000000',lightColor='#ffffff',errorLevel='M',logo,outputDir,watermark}=args;
7
+ let width=args.width||256;
8
+
9
+ // 直接输出 PNG buffer(无需 canvas)
10
+ const qrBuf=await QRCode.toBuffer(content,{
11
+ width,
12
+ margin,
13
+ color:{dark:darkColor,light:lightColor},
14
+ errorCorrectionLevel:errorLevel,
15
+ type:'png',
16
+ });
17
+
18
+ const composites=[];
19
+
20
+ // 如果有 logo,先用白色圆角背景遮挡 QR 码图案,再叠加 logo
21
+ if(logo){
22
+ const logoSize=Math.floor(width*0.22);
23
+ const bgPad=Math.floor(logoSize*0.18);
24
+ const bgSize=logoSize+bgPad*2;
25
+ const logoImg=await loadImage(logo);
26
+
27
+ // 白色圆角背景 —— 清除 QR 码中间图案,让 logo 清晰可辨
28
+ const whiteBg=Buffer.from(
29
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${bgSize}" height="${bgSize}"><rect width="${bgSize}" height="${bgSize}" rx="${bgPad}" fill="#ffffff"/></svg>`
30
+ );
31
+
32
+ // Logo 渲染(SVG 填充分色,同时让背景透明)
33
+ const logoBuf=await logoImg
34
+ .resize(logoSize,logoSize,{fit:'inside',background:{r:255,g:255,b:255,alpha:0}})
35
+ .png().toBuffer();
36
+
37
+ const bgX=Math.floor((width-bgSize)/2);
38
+ const bgY=Math.floor((width-bgSize)/2);
39
+ const logoX=Math.floor((width-logoSize)/2);
40
+ const logoY=Math.floor((width-logoSize)/2);
41
+
42
+ composites.push({input:whiteBg,top:bgY,left:bgX});
43
+ composites.push({input:logoBuf,top:logoY,left:logoX});
44
+ }
45
+
46
+ // 文字水印(可选,一站式生成美化二维码)
47
+ if(watermark&&watermark.text){
48
+ const wm=watermark;
49
+ const wmFontSize=wm.fontSize||14;
50
+ const wmColor=wm.color||darkColor;
51
+ const wmOpacity=wm.opacity!=null?wm.opacity:0.4;
52
+ const wmPos=wm.position||'bottom';
53
+
54
+ const textY=wmPos==='top'?wmFontSize+8:width-wmFontSize/2;
55
+ const alpha=Math.round(wmOpacity*255).toString(16).padStart(2,'0');
56
+
57
+ const wmSvg=Buffer.from(
58
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${width}"><text x="${width/2}" y="${textY}" font-size="${wmFontSize}" fill="${wmColor}${alpha}" text-anchor="middle" font-family="sans-serif">${escapeXml(wm.text)}</text></svg>`
59
+ );
60
+ composites.push({input:wmSvg,top:0,left:0});
61
+ }
62
+
63
+ const finalBuf=composites.length>0
64
+ ?await sharp(qrBuf).composite(composites.map(c=>({...c,blend:'over'}))).png().toBuffer()
65
+ :qrBuf;
66
+
67
+ const saved=await saveOutput(finalBuf,'qrcode.png',outputDir,'url');
68
+ return {
69
+ success:true,
70
+ ...saved,
71
+ content,
72
+ format:'qrcode',
73
+ hasLogo:!!logo,
74
+ hasWatermark:!!(watermark&&watermark.text),
75
+ };
76
+ }
77
+
78
+ function escapeXml(s){
79
+ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
80
+ }
@@ -0,0 +1,52 @@
1
+ import {tongyiText2Image} from '../providers/tongyi.js';
2
+ import {saveOutput} from '../utils/output.js';
3
+ import axios from 'axios';
4
+
5
+ export async function generateSmart(args){
6
+ const images=await tongyiText2Image({
7
+ apiKey:args.apiKey||'',
8
+ prompt:args.prompt,
9
+ negativePrompt:args.negativePrompt||'',
10
+ width:args.width||1024,
11
+ height:args.height||1024,
12
+ style:args.style||'',
13
+ model:args.model||'wanx-v1',
14
+ numImages:args.numImages||1,
15
+ });
16
+
17
+ const results=[];
18
+ for(const url of images.images){
19
+ // 始终下载到本地,确保用户能直接看到图片
20
+ const res=await axios.get(url,{responseType:'arraybuffer',timeout:60000});
21
+ const buf=Buffer.from(res.data);
22
+ const saved=await saveOutput(buf,'ai_generated.png',args.outputDir,'url');
23
+
24
+ if(args.outputFormat==='base64'){
25
+ const b64=buf.toString('base64');
26
+ results.push({
27
+ ...saved,
28
+ base64:`data:image/png;base64,${b64}`,
29
+ mimeType:'image/png',
30
+ width:args.width||1024,
31
+ height:args.height||1024,
32
+ });
33
+ }else{
34
+ results.push({
35
+ ...saved,
36
+ width:args.width||1024,
37
+ height:args.height||1024,
38
+ });
39
+ }
40
+ }
41
+
42
+ return {
43
+ success:true,
44
+ model:images.model,
45
+ prompt:images.prompt,
46
+ style:args.style||'',
47
+ size:`${args.width||1024}×${args.height||1024}`,
48
+ count:results.length,
49
+ images:results,
50
+ message:`成功生成 ${results.length} 张图片,已保存到本地 output 目录`,
51
+ };
52
+ }
@@ -0,0 +1,68 @@
1
+ import sharp from 'sharp';
2
+ import {saveOutput,loadImage} from '../utils/output.js';
3
+
4
+ export async function stitchImages(args){
5
+ const {sources,columns=2,gap=0,background='#ffffff',align='center',format='png',outputDir}=args;
6
+
7
+ if(!sources||sources.length<2){
8
+ throw new Error('至少需要 2 张图片进行拼接');
9
+ }
10
+
11
+ // 加载所有图片
12
+ const images=[];
13
+ for(const src of sources){
14
+ const img=await loadImage(src);
15
+ const meta=await img.metadata();
16
+ const buf=await img.png().toBuffer();
17
+ images.push({buffer:buf,width:meta.width,height:meta.height});
18
+ }
19
+
20
+ // 计算网格布局
21
+ const rows=Math.ceil(images.length/columns);
22
+ const cellWidth=Math.max(...images.map(i=>i.width))+gap;
23
+ const cellHeight=Math.max(...images.map(i=>i.height))+gap;
24
+ const totalWidth=cellWidth*columns+gap;
25
+ const totalHeight=cellHeight*rows+gap;
26
+
27
+ // 拼接
28
+ const composites=[];
29
+ for(let i=0;i<images.length;i++){
30
+ const img=images[i];
31
+ const row=Math.floor(i/columns);
32
+ const col=i%columns;
33
+ const cellX=gap+col*cellWidth;
34
+ const cellY=gap+row*cellHeight;
35
+
36
+ // 在单元格内对齐
37
+ let left=cellX,top=cellY;
38
+ if(align==='center'){
39
+ left+=Math.floor((cellWidth-gap-img.width)/2);
40
+ top+=Math.floor((cellHeight-gap-img.height)/2);
41
+ }else if(align==='bottom'){
42
+ top+=cellHeight-gap-img.height;
43
+ }
44
+
45
+ composites.push({input:img.buffer,top,left});
46
+ }
47
+
48
+ const pipeline=sharp({create:{width:totalWidth,height:totalHeight,channels:4,background}})
49
+ .composite(composites.map(c=>({...c,blend:'over'})));
50
+
51
+ let result;
52
+ if(format==='jpeg') result=pipeline.jpeg();
53
+ else if(format==='webp') result=pipeline.webp();
54
+ else result=pipeline.png();
55
+
56
+ const buffer=await result.toBuffer();
57
+ const filename=`stitch.${format}`;
58
+ const saved=await saveOutput(buffer,filename,outputDir,'url');
59
+
60
+ return {
61
+ success:true,
62
+ ...saved,
63
+ grid:`${columns}×${rows}`,
64
+ totalImages:images.length,
65
+ totalWidth,
66
+ totalHeight,
67
+ };
68
+ }
@@ -0,0 +1,88 @@
1
+ import sharp from 'sharp';
2
+ import {saveOutput,loadImage} from '../utils/output.js';
3
+
4
+ const POSITIONS={
5
+ topLeft:{gravity:'northwest'},
6
+ topRight:{gravity:'northeast'},
7
+ bottomLeft:{gravity:'southwest'},
8
+ bottomRight:{gravity:'southeast'},
9
+ center:{gravity:'centre'},
10
+ };
11
+
12
+ export async function addWatermark(args){
13
+ const {src,type,content,imageSrc,position='bottomRight',margin=20,opacity=0.5,fontSize=36,color='#ffffff',angle=-30,outputDir}=args;
14
+
15
+ const base=await loadImage(src);
16
+ const meta=await base.metadata();
17
+
18
+ if(type==='image'&&imageSrc){
19
+ // 图片水印
20
+ const wmImg=await loadImage(imageSrc);
21
+ const wmMeta=await wmImg.metadata();
22
+ const maxWmSize=Math.min(meta.width,meta.height)*0.2;
23
+ const scale=Math.min(maxWmSize/wmMeta.width,maxWmSize/wmMeta.height,1);
24
+ const wmBuf=await wmImg
25
+ .resize(Math.round(wmMeta.width*scale),Math.round(wmMeta.height*scale))
26
+ .ensureAlpha(opacity)
27
+ .png().toBuffer();
28
+
29
+ const pos=POSITIONS[position]||POSITIONS.bottomRight;
30
+ const result=await base
31
+ .composite([{input:wmBuf,...pos,top:margin,left:margin,blend:'over'}])
32
+ .png().toBuffer();
33
+
34
+ const saved=await saveOutput(result,'watermarked.png',outputDir,'url');
35
+ return {success:true,...saved,type:'image'};
36
+ }
37
+
38
+ if(type==='text'&&content){
39
+ // 文字水印
40
+ const textSvg=await createTextOverlay(meta.width,meta.height,content,position,margin,opacity,fontSize,color,angle);
41
+ const result=await base
42
+ .composite([{input:textSvg,top:0,left:0,blend:'over'}])
43
+ .png().toBuffer();
44
+
45
+ const saved=await saveOutput(result,'watermarked.png',outputDir,'url');
46
+ return {success:true,...saved,type:'text',content};
47
+ }
48
+
49
+ throw new Error(`水印类型 ${type} 需要对应的 content 或 imageSrc`);
50
+ }
51
+
52
+ async function createTextOverlay(width,height,text,position,margin,opacity,fontSize,color,angle){
53
+ const alpha=Math.round(opacity*255).toString(16).padStart(2,'0');
54
+ const textColor=`${color}${alpha}`;
55
+
56
+ let svg='';
57
+ if(position==='tile'){
58
+ // 平铺模式:旋转文字重复排列
59
+ const spacing=fontSize*4;
60
+ svg=`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">`;
61
+ for(let y=spacing;y<height+spacing;y+=spacing){
62
+ for(let x=-spacing;x<width+spacing;x+=spacing*2){
63
+ svg+=`<text x="${x}" y="${y}" font-size="${fontSize}" fill="${textColor}" transform="rotate(${angle},${x},${y})" opacity="${opacity}">${escapeXml(text)}</text>`;
64
+ }
65
+ }
66
+ svg+='</svg>';
67
+ }else{
68
+ // 固定位置
69
+ const pos=POSITIONS[position]||POSITIONS.bottomRight;
70
+ let x=margin,y=margin;
71
+ if(pos.gravity.includes('east')) x=width-margin;
72
+ else if(pos.gravity.includes('west')) x=margin;
73
+ if(pos.gravity.includes('south')) y=height-margin;
74
+ else if(pos.gravity.includes('north')) y=margin+fontSize;
75
+
76
+ const anchor=pos.gravity.includes('west')?'start':pos.gravity.includes('east')?'end':'middle';
77
+ if(anchor==='middle') x=width/2;
78
+ if(pos.gravity.includes('north')&&pos.gravity.includes('south')) y=height/2;
79
+
80
+ svg=`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"><text x="${x}" y="${y}" font-size="${fontSize}" fill="${textColor}" text-anchor="${anchor}" opacity="${opacity}">${escapeXml(text)}</text></svg>`;
81
+ }
82
+
83
+ return Buffer.from(svg);
84
+ }
85
+
86
+ function escapeXml(s){
87
+ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
88
+ }
@@ -0,0 +1,27 @@
1
+ // 生成渐变背景的 SVG overlay
2
+ export function gradientSvg(width,height,config){
3
+ const {type='linear',direction='to bottom',stops}=config;
4
+ if(!stops||!stops.length) return null;
5
+
6
+ let gradientDef;
7
+ if(type==='linear'){
8
+ const dirs={
9
+ 'to bottom':{x1:'0%',y1:'0%',x2:'0%',y2:'100%'},
10
+ 'to right':{x1:'0%',y1:'0%',x2:'100%',y2:'0%'},
11
+ 'to bottom right':{x1:'0%',y1:'0%',x2:'100%',y2:'100%'},
12
+ 'to bottom left':{x1:'100%',y1:'0%',x2:'0%',y2:'100%'},
13
+ 'to top':{x1:'0%',y1:'100%',x2:'0%',y2:'0%'},
14
+ };
15
+ const d=dirs[direction]||dirs['to bottom'];
16
+ gradientDef=`<linearGradient id="g" ${Object.entries(d).map(([k,v])=>`${k}="${v}"`).join(' ')}>${stops.map(s=>`<stop offset="${s.offset||'0%'}" stop-color="${s.color}"/>`).join('')}</linearGradient>`;
17
+ }else{
18
+ gradientDef=`<radialGradient id="g" cx="50%" cy="50%">${stops.map(s=>`<stop offset="${s.offset||'0%'}" stop-color="${s.color}"/>`).join('')}</radialGradient>`;
19
+ }
20
+
21
+ return Buffer.from(`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"><defs>${gradientDef}</defs><rect width="${width}" height="${height}" fill="url(#g)"/></svg>`);
22
+ }
23
+
24
+ // 创建纯色 SVG 背景
25
+ export function solidBgSvg(width,height,color){
26
+ return Buffer.from(`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"><rect width="${width}" height="${height}" fill="${color}"/></svg>`);
27
+ }
@@ -0,0 +1,73 @@
1
+ import {writeFile,mkdir} from 'fs/promises';
2
+ import {join,dirname} from 'path';
3
+ import {existsSync} from 'fs';
4
+
5
+ const DEFAULT_OUTPUT_DIR=join(process.cwd(),'output');
6
+
7
+ // 确保输出目录存在,返回实际输出路径
8
+ export async function resolveOutput(filename,outputDir){
9
+ const dir=outputDir||DEFAULT_OUTPUT_DIR;
10
+ if(!existsSync(dir)) await mkdir(dir,{recursive:true});
11
+ const ts=Date.now();
12
+ const stem=filename.replace(/\.[^.]+$/,'');
13
+ const ext=filename.split('.').pop();
14
+ return join(dir,`${stem}_${ts}.${ext}`);
15
+ }
16
+
17
+ // 将 buffer 写入文件,返回路径和 base64
18
+ export async function saveOutput(buffer,filename,outputDir,returnType='url'){
19
+ const filePath=await resolveOutput(filename,outputDir);
20
+ await writeFile(filePath,buffer);
21
+
22
+ const result={path:filePath,size:buffer.length};
23
+
24
+ if(returnType==='base64'){
25
+ result.base64=`data:image/${filename.split('.').pop()};base64,${buffer.toString('base64')}`;
26
+ }
27
+ if(returnType==='url'){
28
+ result.url=`file://${filePath.replace(/\\/g,'/')}`;
29
+ }
30
+
31
+ return result;
32
+ }
33
+
34
+ // 从路径或 URL 加载图片为 sharp 对象
35
+ import sharp from 'sharp';
36
+ import axios from 'axios';
37
+
38
+ export async function loadImage(src){
39
+ if(src.startsWith('http://')||src.startsWith('https://')){
40
+ const res=await axios.get(src,{responseType:'arraybuffer'});
41
+ return sharp(Buffer.from(res.data));
42
+ }
43
+ return sharp(src);
44
+ }
45
+
46
+ // 解析颜色字符串为 {r,g,b,a} 或 sharp 可接受的格式
47
+ export function parseColor(color){
48
+ if(!color||color==='transparent') return {r:0,g:0,b:0,alpha:0};
49
+ if(color.startsWith('#')){
50
+ const hex=color.slice(1);
51
+ if(hex.length===3){
52
+ const [r,g,b]=hex.split('').map(c=>parseInt(c+c,16));
53
+ return {r,g,b,alpha:1};
54
+ }
55
+ if(hex.length===6){
56
+ return {
57
+ r:parseInt(hex.slice(0,2),16),
58
+ g:parseInt(hex.slice(2,4),16),
59
+ b:parseInt(hex.slice(4,6),16),
60
+ alpha:1,
61
+ };
62
+ }
63
+ if(hex.length===8){
64
+ return {
65
+ r:parseInt(hex.slice(0,2),16),
66
+ g:parseInt(hex.slice(2,4),16),
67
+ b:parseInt(hex.slice(4,6),16),
68
+ alpha:parseInt(hex.slice(6,8),16)/255,
69
+ };
70
+ }
71
+ }
72
+ return {r:255,g:255,b:255,alpha:1};
73
+ }