Nexus搭建npm私库(角色管理、上传脚本)

news2025/1/24 14:04:07

安装Nexus

官网下载

https://www.sonatype.com/products/sonatype-nexus-oss-download

进入官网下载,最新下载方式需要输入个人信息才能下载了

image-20231206140429489

选择对应的系统进行下载

  • Windows 推荐也下载 UNIX 版本(Windows 版本配置比较难改)

image-20231206135901989

如果没有下载,点一下 click here 重新下载,下载还是很快的

image-20231206140624901

Windows安装

linux 安装步骤与启动方式跟 windows 是一样的,解压后如下图

image-20231206141655045

$ tar -zxvf nexus-3.63.0-01-unix.tar.gz
$ cd nexus-3.63.0-01/bin
$ ./nexus /run

注意:nexus 必须使用 Java1.8,我现在用的是 Java17,提示信息让咱们配置 INSTALL4J_JAVA_HOME

image-20231206145305323

$ vim nexus
INSTALL4J_JAVA_HOME_OVERRIDE=/d/Develop/Java/jdk1.8.0_161

image-20231206150123385

显示这个之后访问:http://localhost:8081/,点击 Sign In

  • 账号为 admin,密码粘贴 sonatype-work/nexus3/admin.password 里面的密码,登录成功会提示你改密码

image-20231206150205067

如果想更改启动端口,可以修改 etc/nexus-default.properties 文件

image-20231206150804664

docker安装nexus

nexus3 安装参考:

  • docker-nexus3 GitHub

镜像说明:

  • sonatype/nexus 是 nexus2 版本
  • sonatype/nexus3 是 nexus3 版本
$ docker pull sonatype/nexus3
$ docker run -d -p 8081:8081 --name nexus sonatype/nexus

# 查看 nexus 日志
$ docker logs -f nexus

注意:已经有的容器,直接 docker start 即可

  • -d:在 docker 守护线程运行这个镜像
  • -p:绑定端口,前面的端口表示宿主机端口,后面的表示容器端口
  • --restart=always:指定 docker 重启启动容器,当服务器或者 docker 进程重启之后,nexus 容器会在 docker 守护进程启动后由 docker 守护进程启动容器
  • --name <container-name>:这里是指定了容器建立后的名称
  • sonatype/nexus3 是镜像名

查看是否启动成功

docker ps

报内存不够的话输入这个

Memory: 4k page, physical 1006968k(877280k free), swap 0k(0k free)

$ docker run -d -p 8081:8081 --name nexus3 --restart=always --platform linux/amd64 -e INSTALL4J_ADD_VM_PARAMS="-Xms256M -Xmx256M -XX:MaxDirectMemorySize=2048M" sonatype/nexus3

用户权限不够的话输入如下内容,之后即可启动成功

WARNING: ************************************************************
WARNING: Detected execution as "root" user.  This is NOT recommended!
WARNING: ************************************************************
$ cd /opt/sonatype/nexus/bin
$ vi nexus.rc
run_as_user=root
$ vi /etc/profile
export RUN_AS_USER=root
$ vi nexus
run_as_root=true -> run_as_root=false

创建仓库

仓库管理

Nexus 仓库类型分为如下几种

  1. hosted:本地仓库,通常我们会部署自己的构件到这一类型的仓库,如公司的第二方库
  2. proxy:代理仓库,它们被用来代理远程的公共仓库,如 Maven 中央仓库
  3. group:仓库组,用来合并多个 hosted/proxy 仓库,当你的项目希望在多个 repository 使用资源时就不需要多次引用了,只需要引用一个 group 即可

在创建 repository 之前,最好再创建 Blob Stores 便于统一管理,默认的是 default

image-20231206151333577

选择 File,名字填写为 npm

image-20231206151428449

创建仓库

之后即可创建仓库

image-20231206151933100

一会儿会创建 npm(group)、npm(hosted)、npm(proxy) 这几个仓库

image-20231206151758355

创建npm(hosted)

hosted 宿主仓库:主要用于部署无法从公共仓库获取的构件以及自己或第三方的项目构件

  • 如果是内网情况(无法访问互联网)即可当 npm 总仓库
  • 是否允许重复推送可以根据自己项目情况来考虑

image-20231206153443838

创建npm(proxy)

proxy 代理仓库:代理公共的远程仓库

image-20231206152954330

创建npm(group)

group 仓库组:通过仓库组统一管理多个仓库,这样我们在项目中直接请求仓库组即可请求到仓库组管理的多个仓库

  • hosted 和 proxy 这两个都弄好了之后,在通过 group 聚合提供统一的访问地址

image-20231206153604374

测试

指定 npm 的 registry 为自己的 group 地址,安装 express 依赖

image-20231206155638015

proxy 地址由于没有对应依赖会去镜像源地址下载,之后 proxy 仓库就有对应依赖了

image-20231206155807939

创建推送包角色

添加角色用户

点击 Roles,添加角色权限,搜索 npm- 之后点击 Transfer All 把所有权限都附上即可

  • 可以根据对应需求赋对应权限,比如只让上传不让修改只赋 edit 即可

image-20231206161750869

之后创建对应用户,并设置对应权限

image-20231206161935295

点击 Realms,添加 npm Bearer Token Realm,不然 npm publish 会报 401

image-20231206164136875

添加推送账号

https://blog.sonatype.com/using-sonatype-nexus-repository-3-part-2-npm-packages

添加推送账号,常用如下两种方法

npm adduser

示例:使用 npm adduser 方法

$ npm adduser
Username: admin
Password: admin123
Email: (this IS public): any@email.com

推荐:加 --scope 限制作用域,加 --registry 限制仓库地址

$ npm adduser --registry=http://localhost:8081/repository/npm_hosted/ --scope=@mynpm
npm notice Log in on http://localhost:8081/repository/npm_hosted/
Username: npm
Email: (this IS public) ll@123.com
Logged in to scope @mynpm on http://localhost:8081/repository/npm_hosted/.
$ npm publish --scope=@mynpm

image-20231206170241797

使用.npmrc

不添加推送账号也可以使用 .npmrc,创建 .npmrc 文件,将私库地址粘贴过来

  • 如果想免密登陆推送可以加上 _auth,加密规则:user:password -> base64
registry=http://localhost:8081/repository/npm_hosted/
# 只是举例,不推荐使用 admin 用户
_auth=YWRtaW46YWRtaW4xMjM=
email=any@email.com

_auth 加密方式:

  • 可以使用浏览器自带的方法 window.btoawindow.atob

    image-20230613101343312

  • 还可以使用 linux base64 命令

    image-20230613101349194

registry 也可以通过配置 package.json 来实现

{
  "publishConfig": {
    "registry": "http://localhost:8081/repository/npm_hosted/"
  }
}

下载依赖包

python下载

  • 根据 resolved 字段进行下载
# -*-coding:utf-8-*-
import json
import os
import urllib.request
from pathlib import Path

def node_modules(file_dir):
    # 通过递归遍历 node_modules 每个子包的 package.json 解析下载链接
    links = []
    for root, dirs, files in os.walk(file_dir):
        if 'package.json' in files:
            package_json_file = os.path.join(root, 'package.json')
            try:
                with open(package_json_file, 'r', encoding='UTF-8') as load_f:
                    load_dict = json.load(load_f)
                    if '_resolved' in load_dict.keys():
                        links.append(load_dict['_resolved'])
            except Exception as e:
                print(package_json_file)
                print('Error:', e)
    return links

def package_lock(package_lock_path):
    # 通过递归遍历 package-lock.json 解析下载链接
    links = []
    with open(package_lock_path, 'r', encoding='UTF-8') as load_f:
        load_dict = json.load(load_f)
        search(load_dict, "resolved", links)
    return links

def search(json_object, key, links):
    # 遍历查找指定的key
    for k in json_object:
        if k == key:
            links.append(json_object[k])
        if isinstance(json_object[k], dict):
            search(json_object[k], key, links)
        if isinstance(json_object[k], list):
            for item in json_object[k]:
                if isinstance(item, dict):
                    search(item, key, links)


def download_file(path, store_path, flag):
    # 根据下载链接下载
    if not Path(store_path).exists():
        os.makedirs(store_path, int('0755'))
    links = []
    if path.endswith("package-lock.json"):
        links = package_lock(path)
    else:
        links = node_modules(path)
    print("link resolved number:" + str(len(links)))

    for url in links:
        try:
            filename = url.split('/')[-1]
            index = filename.find('?')
            # 去掉 ? 参数和 # 哈希
            if index > 0:
                filename = filename[:index]
            index = filename.find('#')
            if index > 0:
                filename = filename[:index]
            filepath = os.path.join(store_path, filename)
            if not Path(filepath).exists():
                print("download:" + url)
                # 以防以后对请求头做限制
                opener = urllib.request.build_opener()
                opener.addheaders = [('User-agent', 'Mozilla/5.0')]
                urllib.request.install_opener(opener)
                if flag:
                    new_path = os.path.join(os.getcwd(), 'nodes')
                    if not Path(new_path).exists():
                        os.makedirs(new_path, int('0755'))
                    filepath = os.path.join(new_path, filename)
                urllib.request.urlretrieve(url, filepath)
            # else:
            # print("file already exists:", filename)
        except Exception as e:
            print('Error Url:' + url)
            print('Error:', e)


if __name__ == '__main__':
    # 通过 xxx 文件解析对应依赖树
    download_link = os.path.join(os.getcwd(), 'package-lock.json')
    # 下载文件存放的路径
    download_path = os.path.join(os.getcwd(), 'node')
    # 下载文件是否存放到一个新的路径里,默认存放到 nodes 里
    download_flag = True
    download_file(download_link, download_path, download_flag)
    print("ok")

node下载

  • 使用 npm pack 命令,下载 .tgz 文件
const shell = require('shelljs')
const fs = require('fs')

function download(fileNames = []) {
  shell.cd('download')
  let count = 0
  fileNames.forEach(fileName => {
    const fileExec = shell.exec(`npm pack ${fileName}`, { async: true, silent: true })
    fileExec.stdout
      .on('data', () => {
        ++count
        shell.echo(`>>> ${fileName} 下载完成...`)
        if (count === fileNames.length) {
          shell.cd('..')
          shell.exit(0)
        }
      })
      .on('err', () => {
        ++count
        shell.echo(`>>> ${fileName} 下载失败!!!...`)
        if (count === fileNames.length) {
          shell.cd('..')
          shell.exit(0)
        }
      })
  })
}

function downloadByPackageJsonLockFile(depLockJsonFile = {}) {
  const nMap = new Map()
  const NotMap = new Map()
  // 总的nodes文件夹,方便下次避免重复下载
  const downloadedDir = './nodes'
  const downloadedArr = fs.readdirSync(downloadedDir)

  function getAllList(depJson) {
    if (depJson) {
      Object.keys(depJson).forEach(dep => {
        const depWithVersion = `${dep}@${depJson[dep].version}`
        let tgzFormat = `${dep}-${depJson[dep].version}.tgz`
        // eg: @babel/code-frame-7.14.5.tgz -> babel-code-frame-7.14.5.tgz
        tgzFormat = dep.startsWith('@')
          ? tgzFormat.split('/').join('-').slice(1)
          : tgzFormat
        if (!nMap.has(depWithVersion) && !downloadedArr.includes(tgzFormat)) {
          nMap.set(depWithVersion, true)
          getAllList(depJson[dep].dependencies)
        } else if (downloadedArr.includes(tgzFormat) && !NotMap.has(tgzFormat)) {
          NotMap.set(tgzFormat, true)
        }
      })
    }
  }

  getAllList(depLockJsonFile.dependencies)
  shell.echo(
    `一共${
      Array.from(NotMap.keys()).length
    }个依赖包已在${downloadedDir}目录下存在,不需要重复下载:\n`
  )
  shell.echo(`>>> 无需下载列表: \n - ${Array.from(NotMap.keys()).join('\n - ')}...\n`)
  shell.echo(`一共${Array.from(nMap.keys()).length}个依赖包待下载\n`)
  shell.echo(`>>> 待下载列表: \n - ${Array.from(nMap.keys()).join('\n - ')}...`)
  download(Array.from(nMap.keys()))
}

const pkgLock = require('./package-lock')
downloadByPackageJsonLockFile(pkgLock)

推送依赖包

shell单线程推送

  • 只使用一个线程推送,我比较常用,一般推送的包不会很多
#!/bin/bash
# 待publish文件夹地址
PACKAGE_PATH=./download
# 前端私库地址
REPOSITORY=http://localhost:8081/repository/npm_hosted/
npm login --registry=$REPOSITORY
for file in $PACKAGE_PATH/*.tgz; do
 npm publish --registry=$REPOSITORY $file
done

node多线程推送

多线程推送,上传包较多可以使用,如果包过多可能会导致电脑卡死

let fs = require('fs')
let path = require('path')
const { exec } = require('child_process')

// 前端私库地址
const registry = 'http://localhost:8081/repository/npm_hosted/'
const publishPosition = `npm publish --registry=${registry}`
// 待publish文件夹地址
const filesDir = './download'

fs.readdir(filesDir, (errs, files) => {
  files.forEach(file => {
    fs.stat(filesDir + file, function (err, stats) {
      if (stats.isFile()) {
        const fullFilePath = path.resolve(__dirname, filesDir + file)
        console.log(fullFilePath + ' publish 开始')
        exec(publishPosition + ' ' + fullFilePath, function (error, stdout, stderr) {
          if (error) {
            console.error(fullFilePath + ' publish 失败')
          } else {
            console.error(fullFilePath + ' publish 成功')
          }
        })
      }
    })
  })
})

nexus API上传

官网信息见: https://help.sonatype.com/repomanager3/rest-and-integration-api/components-api

#!/bin/bash
# 待publish文件夹地址
PACKAGE_PATH=./download
# 前端私库服务地址
PUBLISH_RESTFUL=http://localhost:8081/service/rest/v1/components?repository=npm_hosted

echo ">>> 文件所在目录:$PACKAGE_PATH <<<"
dir=$(ls -l $PACKAGE_PATH | awk '/.tgz$/ {print $NF}')
cd $PACKAGE_PATH

for file in $dir
do
  echo ">>> $PACKAGE_PATH/$file 上传开始 \n"
  ret=`curl -u admin:admin123 -X POST "$PUBLISH_RESTFUL" -H "Accept: application/json" -H "Content-Type: multipart/form-data" -F "npm.asset=@$file;type=application/x-compressed"`
  echo $ret
  echo ">>> $PACKAGE_PATH/$file 上传完成 \n"
done

问题

内网上传问题

  1. 分析依赖锁时,有些包命名重复,导致下载错误

    比如:下载 parse-json@4.0.0 链接,既有可能下载的是 @types/parse-json 也有可能下载就是 parse-json,如果按文件名进行覆盖则会导致少传包

  2. 上传包时,有些包的 package.json 里面配置 publishConfig

    image-20230823160618665

    • 比如:archiver-5.0.0 里面配置了如上 publishConfig。在内网情况下,使用 npm i --registry=xx,是会报错的,内网无法访问到 https://registry.npmjs.org/
      • 目前我想到的解决方案是:把 archiver-5.0.0.tgz 解压,之后解压 archiver-5.0.0.tar,修改 package.json 里面的 publishConfig,之后执行 npm publish
    • 比如:builtins@1.0.3 里面也配置了 publishConfig
    • 比如:ahooks@3.7.8ahooks-v3-count-1.0.0hoist-non-react-statics@3.3.2(react-redux@8 的依赖)

    特殊

    • 比如:simple-update-notifier@2.0.0 也是里面配置了 publishConfig,这个包里 scripts 命令里还会有 prepare 钩子,需要先把它去掉
    • 比如:@ant-design/icons@4.8.1 这个比较有特殊性,它依赖了 @ant-design/icons-svg@4.3.1,这两个包都配置了 publishConfig,所以需要单独推送,但是这个包里 scripts 命令里配置了 prepublishOnly 钩子,需要先把它去掉
    • 比如:antd@5.8.4antd@4.24.13 同时配置了 prepare、prepublishOnly、postpublish 钩子,需要先把它们去掉
  3. 分析依赖锁时,包下载不下来,这个就只能用笨方法(缺什么依赖,npm i 之后把对应包 tgz 包下载下来)

    比如:

    • @vue/cli 需要 @types/inquirer@8.1.3@types/accepts@1.3.5body-parser@1.19.2qs@6.9.7@vitejs/plugin-react@4.0.3@types/html-minifier-terser@6.0.0serve-index@1.9.1
    • 其实这个是和第一个原因是一样的都是名重复了,导致下载不下来

其他问题

E400

仓库不允许重复推送会报 400,改为 allow redeploy

image-20231206165353153

E401

没有权限涉及情况较多:

  1. 账号密码不对
  2. 检查对应角色是否有对应权限
  3. 比如 npm 推送以什么方式校验,npm Bearer Token Realm
  4. token 是否过期,去根目录修改 .npmrc 文件,将过期 token 删除

image-20231206162906061

E403

禁止上传,group 仓库禁止上传,上传到 hosted 即可

image-20231206164947825

E503

仓库离线,改为在线即可

image-20231206165252356

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1297753.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

TrustZone之SMC异常

作为支持两个安全状态的一部分&#xff0c;该架构包括了Secure Monitor Call&#xff08;SMC&#xff09;指令。执行SMC会引发Secure Monitor Call异常&#xff0c;该异常目标是EL3。 通常&#xff0c;SMC用于请求服务&#xff0c;可以是来自驻留在EL3中的固件&#xff0c;也可…

Android之Binder原理剖析

一&#xff1a;Binder的全面介绍 binder的出现 George Hoffman当时任Be公司的工程师&#xff0c;他启动了一个名为OpenBinder 的项目&#xff0c;在Be公司被ParmSource公司收购后&#xff0c; OpenBinder 由Dinnie Hackborn继续开发&#xff0c;后来成为管理ParmOS6 Cobalt O…

springboot_ssm_java学位论文盲审系统

本系统主要实现用户登录验证&#xff0c;用户使用邮箱&#xff0c;密码和选择身份进行登录&#xff0c;用户查看个人中心&#xff0c;提交论文&#xff0c;发表留言和问题反馈。用户在线注册。学生模块功能实现&#xff1a;学生注册&#xff0c;查看信息&#xff0c;修改资料&a…

Vue项目中实现浏览器标签页名字的动态修改

修改router/index.js文件 路由条目下面添加meta属性 meta:{title:DevOps运维平台 }示例 使用Vue的全局守卫函数beforeEach&#xff0c;在路由切换前动态修改浏览器标签页名字 router.beforeEach((to,from,next) > {document.title to.meta.titlenext() })

HSV算法及其改进 (附代码)

1.HSV HSV是一种颜色空间&#xff0c;它由色相&#xff08;Hue&#xff09;、饱和度&#xff08;Saturation&#xff09;和明度&#xff08;Value&#xff09;三个参数组成。HSV模型中&#xff0c;色相表示颜色的种类&#xff0c;饱和度表示颜色的纯度&#xff0c;明度表示颜色…

YOLOv8 YoLov8l 模型输出及水果识别

&#x1f368; 本文为[&#x1f517;365天深度学习训练营学习记录博客 &#x1f366; 参考文章&#xff1a;365天深度学习训练营 &#x1f356; 原作者&#xff1a;[K同学啊 | 接辅导、项目定制] &#x1f680; 文章来源&#xff1a;[K同学的学习圈子](https://www.yuque.com/m…

认识线程和创建线程

目录 1.认识多线程 1.1线程的概念 1.2进程和线程 1.2.1进程和线程用图描述关系 1.2.2进程和线程的区别 1.3Java 的线程和操作系统线程的关系 2.创建线程 2.1继承 Thread 类 2.2实现 Runnable 接口 2.3匿名内部类创建 Thread 子类对象 2.4匿名内部类创建 Runnable 子类对…

LLM之Agent(五)| AgentTuning:清华大学与智谱AI提出AgentTuning提高大语言模型Agent能力

​论文地址&#xff1a;https://arxiv.org/pdf/2310.12823.pdf Github地址&#xff1a;https://github.com/THUDM/AgentTuning 在ChatGPT带来了大模型的蓬勃发展&#xff0c;开源LLM层出不穷&#xff0c;虽然这些开源的LLM在各自任务中表现出色&#xff0c;但是在真实环境下作…

1.cloud-微服务架构编码构建

1.微服务cloud整体聚合父工程 1.1 New Project 1.2 Maven选版本 1.3 字符编码 1.4 注解生效激活 主要为lombok中的Data 1.5 java编译版本选8 1.6 File Type过滤 *.hprof;*.idea;*.iml;*.pyc;*.pyo;*.rbc;*.yarb;*~;.DS_Store;.git;.hg;.svn;CVS;__pycache__;_svn;vssver.scc;v…

嵌入式C语言代码的基本编写规范要求

编码规范&#xff0c;没有最好&#xff0c;只有最合适&#xff0c;有但不执行不如没有。1 编码原则 1.1 可读性原则 &#xff08;1&#xff09;清晰第一 清晰性是易于维护程序必须具备的特征。维护期变更代码的成本远远大于开发期&#xff0c;编写程序应该以人为本&#xff0c…

vue3中子组件调用父组件的方法

<script lang"ts" setup>前提 父组件&#xff1a; 子组件&#xff1a; const emit defineEmits([closeson]) 在子组件的方法中使用&#xff1a; emit(closeson)

Spring Boot学习随笔- 集成JSP模板(配置视图解析器)、整合Mybatis(@MapperScan注解的使用)

学习视频&#xff1a;【编程不良人】2021年SpringBoot最新最全教程 第五章、JSP模板集成 5.1 引入JSP依赖 <!--引入jsp解析依赖--> <!--C标签库--> <dependency><groupId>jstl</groupId><artifactId>jstl</artifactId><version&…

ES6之Symbol

ES6中为我们新增了一个原始数据类型Symbol&#xff0c;让我为大家介绍一下吧&#xff01; Symbol它表示是独一无二的值 Symbol要如何创建 第一种创建方式&#xff1a; let sy Symbol()第二种创建方式&#xff1a; let sy Symbol.for()具体独一无二在哪呢&#xff1f;它们的地…

【MyBatis系列】MyBatis字符串问题

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

Java中的IO流①——IO流的体系、字节流、try...catch异常处理

概述 IO流的分类 IO流的体系 这四个类都是抽象类&#xff0c;所以需要实现类对象才能使用---> 字节流 FileInputStream--> 书写细节 代码示范 此时文件a.txt内容为abcde 使用char强转和read方法调用五次read方法--> public static void main(String[] args) throws IO…

JavaScript-节点操作

节点操作 DOM节点 DOM节点&#xff1a;DOM树里每一个内容都称之为节点 节点类型&#xff1a; 元素节点 所有的标签 比如body、divhtml时跟节点 属性节点 所有的属性&#xff0c;比如href 文本节点 所有的文本 其他 查找节点 节点的关系&#xff1a;针对的找亲戚返回的都是…

redis中缓存雪崩,缓存穿透,缓存击穿等

缓存雪崩 由于原有缓存失效&#xff08;或者数据未加载到缓存中&#xff09;&#xff0c;新缓存未到期间&#xff08;缓存正常从Redis中获取&#xff0c;如下图&#xff09;所有原本应该访问缓存的请求都去查询数据库了&#xff0c;而对数据库CPU和内存造成巨大压力&#xff0c…

H264视频编码原理

说到视频&#xff0c;我们首先想到的可能就是占内存。我们知道一个视频是由一连串图像序列组成的&#xff0c;视频中图像一般是 YUV 格式。假设有一个电影视频&#xff0c;分辨率是 1080P&#xff0c;帧率是 25fps&#xff0c;并且时长是 2 小时&#xff0c;如果不做视频压缩的…

每日一练【三数之和】

一、题目描述 15. 三数之和 给你一个整数数组 nums &#xff0c;判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i ! j、i ! k 且 j ! k &#xff0c;同时还满足 nums[i] nums[j] nums[k] 0 。请 你返回所有和为 0 且不重复的三元组。 注意&#xff1a;答案中不可…

【Java】构建哈夫曼树和输出哈夫曼编码

问题背景 一个单位有12个部门&#xff0c;每个部门都有一部电话&#xff0c;但是整个单位只有一根外线&#xff0c;当有电话打过来的时候&#xff0c;由转接员转到内线电话&#xff0c;已知各部门使用外线电话的频率为&#xff08;次/天&#xff09;&#xff1a;5 20 10 12 8 …