上一篇博客提到我在使用 Dokploy 部署网站服务,但 Dokploy 不支持定时任务,于是只能创建普通服务,并在内部使用脚本定时执行命令。最近发现,将这些定时任务放在函数计算中执行可能是更好的选择。 我的服务都跑在阿里云上,下面介绍的也是阿里云函数计算。 创建函数 要创建一个函数,在阿里云函数界面后台,点击创建函数按钮即可,如下图所示: 在随后的界面中,选择“任务函数”类型。 然后,函数代码部分可根据需要选择类型,比如可以使用 ACR 中的 Docker 镜像。 需要注意的是,无论是上传代码还是使用 Docker 镜像,都要确保对应的代码能提供一个 HTTP 服务,因为定时任务执行的入口即是这个 HTTP 服务。 其他还有环境变量等配置,根据你的实际情况填写即可。 函数入口 使用函数计算的定时任务,需要你的代码提供一个 HTTP 服务,定时任务执行时,会以 POST 的方式请求 /invoke 路径,即类似下面这样的请求: curl -X "POST" "http://localhost:8050/invoke" \ -H 'Content-Type: application/json' \ -d $'{ "payload": "YOUR_PAYLOAD", "triggerName": "trigger-name", "triggerTime": "2025-04-27T03:12:45Z" }' 当然,真实的请求还有很多 HTTP 头信息。 你需要在代码中实现 /invoke 接口,并在其中执行定时任务。在函数计算后台,可以设置超时时间等属性。 注意其中的 payload 字段,后面在设置定时触发器时,可以自定义传入的 palyload 信息。 设置定时触发器 添加函数之后,即可在配置界面设置触发器。 函数计算支持多种触发器,在这儿,我们选择定时触发器即可。 其中最后一个字段“触发消息”,其中填写的内容即是上面 payload 参数的值。注意这儿传递的是普通字符串,而不是 JSON,收到之后可根据需要做一个解析。 如果你在同一个函数中有多个用途不同的触发器,可以通过 payload 参数进行区分。 设置好之后,在函数详情界面可以看到类似下面的图示。 如果一切顺利,定时任务就添加成功了,稍后可以在日志页面看到执行记录。 更新函数 如果你的函数使用的是 ACR 中的 Docker 镜像,当推送了新的镜像时,函数计算的版本不会自动更新,需要你登录网站后台手动修改,或者调用函数计算的 API 进行设置。 每次手动修改是一件很麻烦的事,建议使用 API,以便和现有的发布流程结合起来。你可以先安装 aliyun-cli 命令行工具,然后执行类似下面的命令: aliyun fc PUT /2023-03-30/functions/YOUR_FC --region cn-shanghai --header "Content-Type=application/json;" --body "{\"tracingConfig\":{},\"customContainerConfig\":{\"image\":\"registry-vpc.cn-shanghai.aliyuncs.com/XXX/YYY:1.2.3\"}}" 请注意将其中的参数值替换为你的项目中的值。 小结 有一些定时任务(比如清理老数据、备份用户数据等)比较耗费资源,将它们迁移到函数计算中可以减少主服务器的负担,是一个不错的实践。 函数计算是按量收费的,多数情况下,定时任务使用函数计算应该比专门买一台服务器划算,不过也不要大意,请做好优化,同时注意关注每日的用量。
之前的几年我一直在使用 K3s + Rancher 的组合来管理网站服务,不过前段时间迁移到了 Dokploy,在这儿记录一下要点。 为什么迁移? K3s + Rancher 的组合挺好,几年来一直运行稳定,不过对像我这样的非专业运维来说还是有点太复杂了,事实上几年来,我一直只在使用这个组合的一些最基础的功能。 去年看到有人介绍 Dokploy,了解了一下之后,发现它非常适合我的使用场景,同时又足够简单,于是花了一点时间做了研究,并最终决定迁移到 Dokploy。 除了 Dokploy 之外,还有 Coolify 等产品也不错,而且功能更多一些,读者朋友如果有需要也可以试一试。 云服务还是自托管? Dokploy 提供了云服务,订阅之后可通过他们的云服务管理自己的服务器。 云服务听起来是个不错的选择,可以减少自己运维的时间成本,我也花了 $4.5 订阅了一个月体验了一番。不过 Dokploy 的云服务在海外,我的服务器在国内,两者之间通讯不畅,因此体验并不是很好。 最后,我选择了自托管服务,将 Dokploy 和网站服务安装在同一个网络中。 安装 Dokploy Dokploy 的安装很简单,在一台干净的服务器上运行以下命令即可: curl -sSL https://dokploy.com/install.sh | sh 为了确保 Dokploy 能顺利运行,这台服务器建议至少要 2 CPU + 2 G 内存。 如果你的服务器在国内,安装时可能耗时较长,可以添加国内的 docker 镜像,比如修改 /etc/docker/daemon.json 文件,添加以下内容: { "registry-mirrors": [ "https://docker.1ms.run" ] } 安装完成之后,即可通过 http://{服务器 IP}:3000 的形式访问 Dokploy 后台。 添加服务器 Dokploy 成功安装后,马上就可以开始创建应用。不过,这时创建的应用会和 Dokploy 安装在同一台服务器上,你也可以在 Dokploy 后台添加新的服务器,并将应用添加到新服务器上。 个人建议用一台服务器专门运行 Dokploy,然后在 Remote Servers 面板中添加其他服务器。 添加服务器之后,还需要在 Actions 菜单中点击 Setup Server,并根据提示进行设置。 其中 Deployments 那个步骤可能耗时会很长,可以考虑点击 Modify Script,将脚本复制到对应的服务器上手动执行。 添加服务 添加完服务器之后,就可以添加项目,随后在项目中添加服务了。 添加服务这儿,最重要的一个设置是 Provider,即设置代码的来源。 Dokploy 支持多种常见的源,比如 Github,配置好之后只需向指定仓库和分支推送代码,Dokploy 就会自动拉取并构建代码,就像 Vercel 一样。 对小项目来说,这样的方式自然是很方便的,不过也可以用 Docker 作为 Provider,并使用第三方镜像服务。这样主要有两个好处: 镜像的构建工作在第三方执行,不会占用线上服务器资源; 第三方构建镜像时可以打上版本号 tag,后续回滚操作将会很方便。 我使用的是阿里云的容器镜像服务,填写方式类似下图: 更新服务 Dokploy 提供了丰富的 API,几乎所有操作都可以通过 API 完成。当某个服务需要更新时,可以登录网站手动修改相关值,也可以使用 API 更新。 比如,如果一个服务的 Provider 是 Docker,可以用类似下面的请求进行修改: curl -X "POST" "https://your-dokploy/api/application.saveDockerProvider" \ -H 'x-api-key: $YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d $'{ "applicationId": "$APP_ID", "dockerImage": "$DOCKER_URL" }' 有几个注意点: 授权头信息是 x-api-key: xxx...,而不是常见的 Authorization: Bearer xxx... 。 applicationId 的值在 URL 中,在界面上暂时没有显示。 比如某个服务的地址是 https://your-dokploy/dashboard/project/aaa/services/application/bbb,地址最后的 bbb 就是 applicationId。 通过 API 的方式,可以很方便地将服务的发布、回滚等操作集中到一处管理,或者与你现有的服务集成。 使用小结 使用 Dokploy 已经有一段时间了,整体而言还是很满意的,相对其他方案它很容易上手,且足够稳定,可用于生产环境。 不足是暂时还不支持定时任务,不过可以通过启动一个普通服务并在其中运行定时脚本的方式解决。 如果你有类似的需求,不妨也试一试 Dokploy。
最近在产品中用到了 Electron 中的 Kiosk 模式,记录一下要点。 什么是 Kiosk 模式? Kiosk 模式是一种专门为限制用户操作而设计的应用运行模式,通常用于构建锁定的全屏应用程序,禁止用户访问系统其他功能或退出应用。在这种模式下,应用程序占据整个屏幕,并且用户无法通过常见的方式(如键盘快捷键、窗口控制按钮等)退出或切换到其他应用。 Kiosk 模式的主要用途是为用户提供一个专注且受限的操作环境,避免对系统的其他部分产生干扰。 哪些场景下需要使用 Kiosk 模式? Kiosk 模式被广泛应用于以下场景: 公共信息亭:自助服务终端,如银行 ATM、自助点餐机、自助售票机。 展览展示:在博物馆、展览会、零售店中,用于展示信息或广告内容的屏幕。 教育场景:限制学生只能使用特定的教学应用,避免访问其他不必要的内容。 会议或演讲:锁定演示内容,避免误操作或退出。 数字标牌:作为广告屏幕或公告牌,循环播放内容。 当然,我在开发的是日常效率软件,并不属于以上场景。我用到 Kiosk 模式的场景主要如下。 图几截图软件 我开发并维护着一个截图软件图几,它有三种截图模式:全屏截图、窗口截图、区域截图。 其中区域截图的交互方式是:用户点击截图按钮(或按下截图快捷键),先生成当前屏幕的截图,随后显示一个全屏无边框窗口,在窗口中显示将刚刚生成的屏幕截图,同时允许用户在窗口上进行框选等操作。 这个无边框窗口就需要使用 Kiosk 模式,以免用户无意中切换窗口。当然,等用户完成或取消截图时,需要再退出或关闭对应的 Kiosk 窗口。 WonderPen 写作软件 WonderPen 写作软件最近添加了小黑屋模式,进入这种模式后,软件将全屏显示,屏蔽一切干扰,在完成预设的写作目标之前,将无法退出或切换到其他软件。 这个禁止退出的小黑屋,自然也使用了 Kiosk 模式。 Electron 中的 Kiosk 模式 在 Electron 中,将一个窗口设为 Kiosk 模式非常简单,在创建窗口时设置 kisok 属性为 true 即可。 有时,我们的窗口在创建时需要以普通模式显示,然后再在一定条件下切换为 Kiosk 模式,只需用类似下面的代码切换即可: win.setKiosk(flag) 其中 flag 是一个布尔值。 你还可以使用 win.isKiosk() 方法判断当前窗口是否为 Kiosk 模式。 在实践过程中,我发现很多时候只设置 Kiosk 属性还不太够,还需要设置 frame 等属性。以下是一个示例: const win = new BrowserWindow({ // 其他属性... closable: false, maximizable: false, minimizable: false, resizable: false, fullscreen: false, fullscreenable: false, frame: false, skipTaskbar: true, alwaysOnTop: true, useContentSize: true. autoHideMenuBar: true. movable: false. thickFrame: false. titleBarStyle: 'default', paintWhenInitiallyHidden: false, roundedCorners: false, enableLargerThanScreen: true, acceptFirstMouse: true, kiosk: true, // 其他属性... }) 即使这样设置之后,在 macOS 上有时仍会出现 Docker 栏和顶部系统菜单栏出现在 Kiosk 窗口上方的情况,因此还需要进一步设置 alwaysOnTop 的属性为 screen-saver,代码如下: win.setAlwaysOnTop(true, 'screen-saver', 1) 在 Windows 和 macOS 中,alwaysOnTop 的窗口有多种极别,按层级由低到高分别是: normal floating torn-off-menu modal-panel main-menu status pop-up-menu screen-saver 如果只是简单地 win.setAlwaysOnTop(true) ,则窗口的级别只是 floating,仍有可能被其他系统组件遮挡。 另外需要注意,在 macOS 下,太高的级别会挡住系统自带输入法的候选字窗口,如果你的 Kiosk 窗口需要用户输入,并且可能使用系统自带输入法的话,这个级别不能高于 modal-panel。 一些其他注意点 Kiosk 模式只对当前窗口有效,一个窗口只能覆盖一个屏幕,若用户有多个显示器,则需先检测显示器数量,然后创建多个 Kiosk 窗口分别覆盖。 设置 Kiosk 模式后,用户仍可以使用 Cmd+Q 这样的快捷键退出应用,因此需要在代码中监听窗口的 close 事件,并检查是否处在 Kiosk 状态,如是则阻止退出。代码类似下面这样: win.on('close', async (e: Electron.Event) => { if (win.isKiosk()) { e.preventDefault() return } // 其他逻辑 } Windows 下退出 Kiosk 模式后,窗口的大小可能会变成全屏大小,如希望退出时恢复原大小,可以在进入 Kiosk 模式之前先记住窗口大小,退出后再设置为原大小。 Kiosk 模式并不能阻止用户重启计算机。如果希望重启计算机后能自动恢复 Kiosk 状态,可以将软件设置为随系统启动,并且启动时自动进入 Kiosk 模式。
之前很长一段时间,这个博客一直在用云服务商提供的免费 SSL 证书,那个证书有一年有效期,也即一年只需要申请部署一次,因此全手动操作也不算麻烦,但现在免费 SSL 证书的有效期统一缩短为 3 个月了,意味着每 3 个月就要操作一次,这就让手动申请和部署变得麻烦起来了。 最近,我尝试了一下使用 acme.sh 申请 SSL 证书的方法,确实方便了不少,在这里记录一下。 安装 acme.sh acme.sh 是一个实现 ACME 协议的脚本,主要用途是申请或更新免费 SSL 证书。运行以下命令即可安装: curl https://get.acme.sh | sh -s email=my@example.com 更多安装方式可见官方文档:https://github.com/acmesh-official/acme.sh。 acme.sh 会被安装在 ~/.acme.sh 目录下。 手动申请证书 安装好 acme.sh 后,可以用以下命令申请证书: acme.sh --issue --dns -d mydomain.com -d "*.mydomain.com" --yes-I-know-dns-manual-mode-enough-go-ahead-please 记得把其中的 mydomain.com 换成你自己的域名。 上面的代码中,我申请了泛域名证书,所以同时添加了 mydomain.com 和 *.mydomain.com 域名。需要注意的是,*.mydomain.com 不包含 mydomain.com,如果你希望证书除了包含 www.mydomain.com 这样的二级域名,也包含 mydomain.com 的话,记得把 mydomain.com 也加上。 另外,*.mydomain.com 也不包含更深的层级,比如它包含 home.mydomain.com,但不包含 app.home.mydomain.com 。如果你需要更深层级的泛域名,需要把对应的域名也填上。 还需要注意的是最后一个参数 --yes-I-know-dns-manual-mode-enough-go-ahead-please 。acme.sh 更希望用户使用自动申请证书的方式(见下一小节),如果你确实需要手动申请,需加上这个参数,否则命令不会正常执行。 如果一切顺利,acme.sh 命令会输出两段 TXT 信息,需要你手动添加到对应域名的 DNS 解析中,以验证你确实对这个域名拥有权限。在证书申请完成之后,可以删除对应的 TXT 记录。 登录域名服务商(比如阿里云)后台,在域名解析中添加上对应的 TXT 记录,然后再运行以下命令,即可生成证书: acme.sh --renew -d mydomain.com -d "*.mydomain.com" --yes-I-know-dns-manual-mode-enough-go-ahead-please 证书会被保存在 ~/.acme.sh/ 目录下,包含以下四个文件: mydomain.com.cer 证书 mydomain.com.key 密钥 ca.cer fullchain.cer 全链路证书 其中在网站场景主要使用 fullchain.cer 文件和 mydomain.com.key 文件。 自动申请证书 可以看到,上面手动申请的步骤,主要的手动操作就是要为域名添加 TXT 记录以验证域名权限,acme.sh 支持让这个步骤自动化,即自动添加 TXT 记录,并在验证完成之后自动删除对应的记录。 以阿里云为例(如果你的域名是在阿里云注册并解析的),首先需要去阿里云控制台获取一个 AccessKey,建议专门设置一个 RAM 用户,只开通 DNS 权限。 得到 AccessKey 之后,在命令行中执行以下命令: export Ali_Key="key" export Ali_Secret="secret" 随后再执行以下命令,即可自动申请或更新证书了: acme.sh --issue --dns dns_ali -d mydomain.com -d "*.mydomain.com" 注意 --dns 参数后面的值为 dns_ali。 一切顺利的话,证书申请会自动完成,并被保存在 ~/.acme.sh/ 目录下。 其他各大域名服务商的自动申请方式类似,具体可参见官方文档。 一些注意点 如果你使用了自动申请,AccessKey 会被明文保存在 ~/.acme.sh/account.conf 文件内,如果介意,可在申请完之后修改这个文件并删除对应的 AccessKey。 另外,使用自动申请后,acme.sh 会添加一条定时任务,每天自动检查证书是否需要更新。可运行以下命令查看当前系统的定时任务列表: crontab -l 现在 acme.sh 默认使用的证书颁发机构是 ZeroSSL,还有一些其他可选机构,比如 Let's Encrypt。可以用 --set-default-ca 修改默认证书颁发机构,比如: acme.sh --set-default-ca --server letsencrypt 我没有修改 CA,在使用默认的 ZeroSSL 的证书,目前来看暂时没有遇到什么问题。 除了自动申请证书外,大部分网络服务商也支持自动上传 SSL 证书,不过这部分我还没有研究,后续如果觉得值得记录,会另外写文分享。
最近完善了一下产品的购买流程,其中的一项工作是处理来自苹果 App Store 平台的 CONSUMPTION_REQUEST 消息,在这儿记录一下要点。 消息说明 App 如果使用了苹果的内购(IAP),每当发生用户购买、续费、退款等操作时,苹果服务器都会向开发者指定的地址发送一条消息,不同的消息有不同的 notificationType 值,其中 CONSUMPTION_REQUEST 消息的意思是用户为应用内购买发起了退款请求,App Store 请求开发者服务器提供用户的消费数据,用于协助 App Store 决定是否给用户退款。 开发者可以忽略 CONSUMPTION_REQUEST 消息,也可以根据需要,在 12 小时内回应 App Store。 回应消息 要回应 CONSUMPTION_REQUEST 消息,只需向指定的地址发一个 PUT 请求即可。具体细节可见官网文档。 这个 PUT 消息的要点主要有两个: 在 Header 中添加认证 token 信息; 在 Body 中发送一个 JSON 格式的对象,向 App Store 提交对应的信息。 数据内容 我们先看 Body 中的数据内容。 根据文档,数据字段以及含义大致如下: { "accountTenure": 0, // 用户年龄段,0 表示未知 "appAccountToken": "", // 用户 uuid,由于之前没有设置,此处留空 "consumptionStatus": 0, // 消费状态,0:未知,1:未消费,2:部分消费,3:全部消费 "customerConsented": True, // 用户是否同意提供消费数据 "deliveryStatus": 0, // 交付状态,0:已成功交付 "lifetimeDollarsPurchased": 0, // 用户在应用内购买的总金额,0 表示未知 "lifetimeDollarsRefunded": 0, // 用户在应用内退款的总金额,0 表示未知 "platform": 1, // 平台,0:未知,1:苹果平台,2:其他平台 "playTime": 0, // 用户在应用内的总时间,0 表示未知 "refundPreference": 1, // 商家对退款的意见,0:未知,1:支持,2:不支持,3:不确定 "sampleContentProvided": True, // 是否已经提供了示例内容 "userStatus": 1, // 用户账号状态,0:未知,1:活跃,2:暂停,3:关闭,4:受限 } 你可以根据需要,修改对应字段的值。 请求 Header 请求 Header 中有两个必填的自定义字段,分别是: Content-Type 值固定是 application/json Authorization 值为 Bearer $jwt_token 其中 jwt_token 必须要正确填写,否则请求会返回 401 错误。 jwt_token 的具体生成说明可见官方文档,大致格式类似下面这样: Header: { "kid": "ZA12345678", "alg": "ES256", "typ": "JWT" } Payload: { "iss": "your_uuid", "iat": 1723173620, "exp": 1723183620, "aud": "appstoreconnect-v1", "bid": "your_bundle_id" } 其中 kid、iss,以及生成 JWT 时所需的私钥等几项,需要去 App Store Connect 后台生成。 JWT 私钥 如果你之前还没有生成过对应的私钥,可以前往 App Store Connect 后台的“用户和访问” → “集成” → “App 内购买项目”页面生成,如下图所示: 生成之后,可以在这个页面下载 .p8 格式的私钥。注意这个私钥只能下载一次,下载之后请妥善保存,如果不慎遗失,只能删除再重新生成一个。 上面生成 JWT 所需的 kid 对应上图中的“密钥 ID”,iss 对应“Issuer ID”,私钥即上面下载的 .p8 文件中的内容。 然后就可以用类似下面的方法生成 JWT 了: import jwt jwt_token = jwt.encode( payload, private_key, algorithm="ES256", headers=headers, ) 最后,将得到的 jwt_token 以 Bearer $jwt_token 的形式包含在请求头的 Authorization 中,发起 PUT 请求即可。 如果请求返回 202 状态码,表示请求成功了。如果是其他值,可根据错误状态再仔细检查处理。
最近在开发 Flutter 项目,其中 iOS 版 App 账号登录时,需要适配 1Password 等密码管理器,即需要告诉 1Password 等密码管理器当前 App 的登录特征信息(域名),以及应该填写界面上的哪些表单项。在这儿记录一下要点。 基本设置 我们首先要处理的,是让 App 和某个域名(通常是官网域名)关联,这样在 App 中唤起 1Password 填写密码时,1Password 才知道应该显示哪些账号。 这儿主要有三个步骤。 Apple 开发者后台设置 在 Apple 开发者后台的 Certificates, Identifiers & Profiles 页面,记得要选中 Associated Domains 选项,如下图所示: Xcode 中的设置 接下来,要在 Xcode 中为你的 App 添加关联域名,如下图所示: 在 Domains 那一栏,添加 webcredentials:你的域名 即可,比如你的域名是 test.com,那么添加 webcredentials:test.com 就行。 网站设置 最后,还需要在你的域名对应的网站上添加一个认证文件,证明指定 App 确实和当前域名相关。这个文件的文件名固定为 apple-app-site-association,可以放在网站的根目录,或者 .well-known 目录下,确保可以通过网络访问到。 这个文件的用途很多,可能还会包含一些其他字段,和密码管理器相关的主要是以下内容: { "webcredentials": { "apps": [ "TeamId.BundleId" ] } } 确保你的 apple-app-site-association 文件包含 webcredentials 字段,并将其中 apps 中的 TeamId、BundleId 换成你的真实 ID。 Flutter 中的设置 为了得到更好的登录体验,Flutter 中也要做一些设置,主要是告诉 1Password 等密码管理器需要填写哪些字段,以及各个字段分别对应什么内容。 关键代码如下: @override Widget build(BuildContext context) { // ... return Container( body: Center( child: AutofillGroup( child: Column( children: [ TextField( autofillHints: const [AutofillHints.email], decoration: InputDecoration( labelText: 'Email', ), ), TextField( autofillHints: const [AutofillHints.password], decoration: InputDecoration( labelText: 'Password', ), ), ElevatedButton( onPressed: () { // Submit the form }, child: Text('Submit'), ), ], ), ), ), ); } 其中最关键的有两处,一是需要自动填写的表单部分,需要用 AutofillGroup 组件包起来,这样 1Password 就知道哪些字段是需要自动填写的。二是 Email、用户名、密码等需要填写的字段,需要添加 autofillHints 属性,比如 autofillHints: const [AutofillHints.email],这样 1Password 才知道当前字段应该填什么内容。 完成这些设置之后,App 登录时就应该能正常适配 1Password 了。
最近几个月打了很多次牌,有时是线下聚会时和朋友玩,有时则是在手机上玩。玩得久了,逐渐发现扑克牌游戏和现实生活中的规则有一些类似之处。以下是一些感想。 手中的牌 打牌时,手中拿到什么牌非常重要,这一点很容易理解,无论你是高手还是菜鸟,如果起手就拿到一手好牌,那么只要不乱打,并且运气不是差到极点,你基本上都能赢。同样的,就算你是绝顶高手,如果拿到一把烂牌,要赢恐怕也非常难。 当然,拿到极品好牌和极品烂牌的概率都不大,很多时候,我们以及我们的对手拿到的都是中等牌,这种时候,如何组合手中的牌打出最好的效果,就看各自的技术了。 我们的人生也类似,总有一些人一开始就拿到一手好牌,比如家境良好,父母见识不凡,自身也健康聪明,因而只要自己不走错路,人生总体上会非常顺利。还有一些不那么幸运的人,出生在落后的地方贫穷的家庭,几乎没有什么可利用的资源或者助力,要获得成功可就不容易了。 拿到好牌时,不要得意忘形,因为你的成功很大程度源于运气。拿到烂牌时,也不要破罐子破摔,认真思考,尽最大努力打好手中的牌,因为只有这样你才能多一点获胜的机会。 合适的才是最好的 摸牌的时候,一般来说摸到大牌比摸到小牌更好一些,不过稍有经验就会发现,和单独的大牌相比,那些能让现有的牌组合起来的牌可能更好。比如,有一些时候,摸到一张最小的 2 可能会让你的几张散牌组成同花顺,这时对你而言 2 就比大王更好。 生活和工作中也是类似,有一些团队,单独来看每位成员可能都相对普通,但由于配合出色,于是团队整体的战斗力非常强悍。 团队招人的时候,也不是招越牛的人越好,而是要看新来的人能否让团队的整体能力得到提升。有时候,也许加入一位履历一般但却能搞定一些其他人不擅长处理的小事的成员,会让团队整体焕发新生。同样的,一位看似不重要的成员离开,也有可能打断团队内部的某种连接,让团队效率大受影响。 寻找人生伴侣也是如此,那些光彩夺目的潜在选项当然也不错,但一位能与你互补,让你成为更好的自己的伴侣,也许是更好的选择。 本钱 拿到什么牌很大程度上能决定你单局的胜负,有多少本钱则决定你能在牌桌上待多久。 以腾讯欢乐斗地主的掼蛋游戏为例,新手场单场输赢封顶是 5 万欢乐豆。即如果你手中的欢乐豆不足 5 万,那么你赢的时候手中的欢乐豆翻倍,输的时候会赔得精光。当然,实际上输赢的数额还取决于对手的豆子够不够,不过为了简化讨论,此处我们假设对手总是有足够的豆子。 这个规则很容易理解,而且看起来似乎也很公平。但在玩了很多场之后,我发现这其实是一个对水平普通且本钱较少的玩家非常不友好的规则。 假设你是一位普通水平的玩家,即你的水平处在中游,每局有 50% 的概率赢,也有 50% 的概率输,并且初期你只有 1000 欢乐豆,你会面临什么情况呢? 当你的豆子数量在 5 万以下时,无论你之前赢了多少次,只要输一次,你的豆子就被清空了。也就是说,想要让自己的豆子超过 5 万,你需要连赢 \(6\) 次,才能让自己的豆子从 1000 增长到超过 5 万的数量(\(1000 \times 2^6 = 64000\))。但是,由于你的赢率只有 50%,因此连赢 6 次的概率也只有 \(\frac{1}{2^6}\),即 \(\frac{1}{64}\) 或者 1.56% 。 但即使你有了 6.4 万豆子,你仍然不安全,只要连输两次(发生的概率为 \(\frac{1}{4}\) 或 25%,这并不是一个小概率),你就又会回到一无所有的状态。要让自己更安全一些,你可能需要至少 20 万豆子,这样只有连输四次(发生的概率为 \(\frac{1}{16}\) 或 6.25%),你才会输光所有豆子。而要从 1000 豆子变成 20 万豆子,你需要先连赢 6 次,然后在接下来保住本金的情况下再净赢至少 3 次。 粗略计算一下就可以知道,作为一名赢率只有 50% 的中等水平玩家,取得这个成就的概率不足 1%。 另一方面,如果你仍然只是一名中等水平的玩家,但你已经有 20 万甚至更多的豆子了,会怎么样呢? 由于你的输赢概率各一半,并且每次输赢都只是增减 5 万欢乐豆而不是一下子输光所有,因此你的账户余额从概率上来讲将会一直持平。期间可能会因为随机因素出现一些波动,甚至有非常小的概率你在某次大波动中余额被击穿从而破产,但更大的概率是你的资产经过波动之后又回归到初始值附近。 换一句话来说,对一名中等水平玩家来说,从底层逆袭的成功概率极小,因为在初期他需要连赢很多次才行,同时只要输一次就会完全失败。但同样水平的玩家,如果一开始就拥有大量本钱,他却有很大的概率能守住这些财富,因为他不怕输,即使输了也仍然有机会赢回来。也即本钱越少,越不容易出头,财富越多,则越容易守住财富。 这大概也是阶级固化的一种解释吧。
科幻小说《星震》是《龙蛋》的续集,讲的是人类造访一颗名为龙蛋(也叫蛋星)的中子星,行程结束准备离开时,蛋星发生了一场星球级别的大地震(星震),星球上的奇拉文明几乎毁灭,随后奇拉与人类相互帮助、相互拯救的故事。 读这部小说之前,最好先读过前作《龙蛋》,否则对故事的背景可能会难以理解。在继续写这篇读后感之前,也让我们先回顾一下《龙蛋》的前文提要以及设定。 蛋星是一颗由比太阳还要大的红巨星坍塌而成的中子星,直径只有二十公里,因此星球上的物质密度极高,引力也极为强大,根据书中的设定,蛋星表面的引力强度是地球上的 670 亿倍! 在这种环境下,蛋星上物质的形态与地球上常见物质的形态自然大不相同,那儿的各种活动不再依赖分子级别的化学反应,而是速度更快的核子级别的物理反应,于是对那颗星球上的智慧生命奇拉而言,它们的时间比我们快 100 万倍。所以,在前作《龙蛋》中,人类刚与奇拉们相遇时,奇拉们还处在石器时代,但一星期后,奇拉们的科技就已经远超人类。 然而,就在人类基本完成考察准备离开时,一场灾难突如其来,星震发生了,蛋星上的奇拉文明遭受了毁灭性的打击,无数奇拉死去,大量建筑和设备报废,只有少数幸运儿活了下来,但他们几乎不会操作和修复那些先进的设备。 不幸中的万幸是当时蛋星上空还有一些正在太空中执行任务的奇拉,这些太空奇拉受过良好的教育,依然掌握着部分先进技术,它们是奇拉恢复文明的希望。但问题是,地表的降落场地都已经在星震中被毁坏了,他们无法降落,只能先设法联系上地表的奇拉,并远程指导他们修复降落场。 一开始,一切都很顺利,但不久之后,地表上一些原本处在社会边缘的奇拉有了其他心思,他们觉得现在这种没有法律可以随心所欲的灾后世界似乎也挺好,如果让太空奇拉降落,他们是不是又要回到从前那种处处受限的状态? 一番争斗之后,这些“坏”奇拉赢了,他们建立了新的秩序,蛋星的地表文明退回了蛮荒时代。太空奇拉无法降落,只能另想办法,而唯一的办法可能会让正准备离开的地球人处于危险之中…… 最后,在太空奇拉、地表“好”奇拉以及人类的共同努力以及牺牲之下,他们终于重建了文明,而且在 100 万倍的时间流速下,很快就达到了比之前更高的高度。 整个故事对人类而言都是在一天之内发生的,但对奇拉而言却已经过去了很多代人。 比较有意思的是故事中没有一个贯穿始终的超级英雄个体,很多奇拉在为恢复文明而努力,某位奇拉在某个阶段出场很多,正当你以为 Ta 是主角时,Ta 却可能突然因为意外或战争“流逝”(故事中奇拉的死亡被称为“流逝”)了。这种安排可能会让期待超级英雄的读者读起来不那么“爽”,但掩卷细思却也觉得非常合理,因为他们面临的是星球级别的灾难,在这种灾难面前个体的力量是渺小的,只有所有人合力,一起向着一个方向前赴后继地努力才有可能取得最后的成功。 故事中也探讨了一些科幻作品中常见的问题。比如,科技文明的诞生是必然吗?作者认为并不是,故事中作者借一位奇拉的思考表达了这样的观点:蛋星上的奇拉们虽然很早就有了智慧,进入了原始文明,但如果不是人类飞船的造访和影响,也许奇拉会继续在低水平的文明中徘徊漫长时光。而如果在星震发生的时候,他们仍然没能发展到可以进入太空,那么迎接他们的将会是一场更彻底的倒退甚至毁灭。 另一个问题是,奇拉文明的科技水平已经远远超过了地球文明,为什么他们没有入侵或者消灭地球文明?故事中也给出了解释,因为奇拉文明是中子星文明,由普通物质组成的地球世界在他们眼中几乎是空的,太阳系的资源对他们来说也几乎毫无用途,换句话说就是他们和地球文明处在完全不同的生态位上,没有竞争关系。做一个不太恰当的类比的话,奇拉文明和地球文明就好像狮子和野草一样,两者所需的资源差异极大,又对彼此都没有威胁,因此完全可以和平共处。如果奇拉文明遇到另一个中子星文明,或许就不会如此和谐了。 还有一个经常出现在科幻作品中的问题是,为了活下去,我们可以吃同类的尸体吗?故事中奇拉文明和人类文明有着完全不同的道德观和选择,对奇拉文明而言这根本不是一个问题,因为在他们的文化中吃死去同类的尸体是很正常的事,即使在不缺食物的情况下,同类的尸体仍然是他们喜爱的美食,甚至宴会中最好的菜可能就是一份烤奇拉。但对地球文明来说,这却是让人难以接受的事,故事中地球人类遇到这个困境时,果断拒绝了这个选项,宁可饿死。 在本书的最后,作者还描写了奇拉文明中的一些细节,有一个细节我觉得挺有趣:奇拉文明中的建筑都是没有房顶的。 为什么会这样?作者解释说因为蛋星的引力太大了,加上大气稀薄(蛋星上的大气由电子、铁离子或其他典型星壳核子组成),因此自然界中从来没有进化出会飞的生物,大概也没啥能跳得很高的生物,于是他们的世界基本上是二维的,几乎从来不会有麻烦来自上方,所以他们只需要建四面墙把自己围起来就安全了,不需要屋顶。 总的来说,这是一部有趣的硬核科幻小说。
工作多年,被人管过也管过人,有一个不算新颖但深有感触的心得,即管理者应该要尽量避免“微操”。 “微操”一词源自游戏,指玩家对游戏中的单位进行非常细微的操作,比如在即时战略游戏中,非常细致地控制每个士兵的行为。 对一些游戏来说,“微操”是褒义词,表明玩家的水平非常高,能发挥出各个单位最大的战斗力,这很好理解,因为游戏中的单位通常智能程度不高,同时总是能不折不扣地执行玩家的指令。但在现实世界中,“微操”则通常不是什么好事。 我曾有一段比较痛苦的工作经历,当时的上司就非常喜欢“微操”,细微到产品的每个设计细节都要管,比如每个按钮的位置和文案、每个输入框的样式以及每个操作的流程他都要发表意见。 他的能力确实很强,在业内小有名气,也有过几个成功的项目,因而十分自信强势,加上有着上司的身份,在团队内威信极高,对产品的各个细节都有着最终裁决权。 刚加入团队时,我非常有干劲,尽心尽力地投入产品的设计和开发,但一段时间后,我发现我提交的方案常常会被他改动很多地方,而有时一个看似很小的改动却可能意味着之前的大量工作需要推倒重来。 一开始我陷入了自我怀疑中,是不是我的能力真的太差了?不然为什么有这么多细节不符合上司的要求?我尝试努力总结上司的各个修改逻辑,但大部分修改我实在看不出有什么规律,对我来说,哪些地方会被打回似乎是完全随机的。 反复的细节修改以及确认,让我的工作量增加了很多,精神上也疲惫不堪。 终于有一天,在再一次无奈的修改之后,我决定摆烂了。由于无法预测哪些方案可能会被打回修改,我便尽可能地不做决定或者少做决定,每一个我觉得有被打回风险的设计都尽量先去问一问他。 摆烂之后,我果然轻松了不少,很少再遇到方案被打回或者需要大量修改的情况了,因为实际上我只是在做一些不太需要动脑子的执行工作,需要决策的部分都扔回给了上司。 但这显然不是正常的或健康的状态,一段时间后,上司发现自己越来越累,于是变得愤怒,开会时常常斥责我们没有达到他的期望。我也很愧疚,尝试再次担起责任,但当我再次深入方案设计然后继续遭遇大量细节被打回后,我彻底摆烂了,并且在不久之后离开了那个团队。 后来我回顾那段工作经历,逐渐意识到了其中令彼此痛苦的根源。那些被打回修改的细节设计,真的都有必要,或者都有对错之分吗? 不可否认,上司提出的一些意见确实更好,遇到这样的修改要求时,相信任何一位合格的员工都会接受,并且心悦诚服。但在那段工作经历中,从数量上来说这样的修改只占少数,更多的则只是一些纯粹的偏好性质的改动,比如一些文案的表述方式,几个元素是左对齐还是居中对齐,一些操作流程的顺序安排等等。在上司介入的阶段,我们的方案已经经过了反复的思考和讨论,通常来说已经没有明显的问题,但上司总是能提出很多修改意见,而在我们看来这些意见和原方案相比并没有明显的优劣或者绝对的对错,无论哪种选择都有其道理,但说到底,很多方案的差异其实只是口味或习惯的不同罢了,就像有人喜欢川菜,有人喜欢淮扬菜,因此在烹饪时流程和用料不一样,仅此而已,但只要在整体上保持风格一致,做出来的就不失为一道好菜。 每个人都有自己的偏好,除非他人的思维习惯和你完全相同,否则他的方案和你预想的方案多多少少总是会有差异。员工如果是新手,管理者手把手地指导自然没有问题,但这显然不应该成为常态,当员工成长起来,有足够的经验之后,作为管理者,我想只要大方向没有问题,细节上应该允许下属自由发挥。这也是所谓的“抓大放小”、“充分放权”。 况且很多时候,作为一线员工,在产品方案上他们了解的信息可能比管理者更多,很多方案可能已经是综合考虑体验和成本之后的结果,一个看似不起眼的“小”修改意见,可能会需要几个小时甚至几个人日的工作,而连着几个“小”意见,则可能意味着成本的飙升。 喜欢“微操”、坚持按自己的喜好指挥每一个细节的管理者,从本质上来讲,要么是对员工不信任,认为员工没有能力做好工作,要么则是沉迷于操纵一切的掌控感,不愿意放权。无论哪种情况,对团队来说都是有害的。 如果管理者真的认为某个设计或者细节很重要,必须严格执行,那么最好的方式是在一开始就清晰地向员工说明,而不是等到方案都已经基本完成时才不断提出各种“重要”修改意见。 如果员工自己的创意总是被管理者修改甚至否定,那么逐渐地,员工就会放弃主动,——既然自己没有决定的权力,那么就干脆做个不动脑筋的执行者吧。最终,员工不开心,没有足够的参与感和成就感,成长有限;管理者也不开心,觉得招了一帮废物,什么都要自己来决定,让自己都没有时间去做更重要的工作了,却没有意识到造成这一切的根源可能就是自己。 在团队中,管理者天然有着更大的权力,合格的管理者应该学会控制自己的欲望,合理地使用这个权力。很多时候,并不是管得越多越细就越好,作为管理者,在准备提出意见时应该先想一想,这是必要的吗?
爷爷生于 1924 年,今年是爷爷诞辰 100 周年。 1 爷爷受过良好的教育,解放前当过老师,在山村小学教书育人,也曾一腔热血,保护过被敌人追捕的游击队员。解放后,爷爷先在县法院工作,后转向水利、航运方向,参与主导了很多复杂的工程,并逐渐成为这个领域的知名专家。 1970 年代,云南省开发澜沧江,爷爷以专家身份被聘为技术指导。那个年代的工作条件非常艰苦,在一次位于西双版纳的工程作业中发生了意外,奋战在一线的爷爷不幸负伤致残,从此不得不离开热爱的事业,提前退休回到家乡。 2 从我有记忆起,爷爷就是一个整天坐在客厅藤椅上的老头,很瘦,很慈祥,从未见他发过脾气,对谁都很和蔼。 爷爷的伤在腿上,这个伤让他失去了自由行走的能力,因此他只能天天宅在家,极少出门。 爷爷有一大箱子书,我曾好奇地翻过,但那时我还不识字,只依稀记得书中有很多复杂的图表。 有时会有一些同样头发花白的人来看爷爷,爷爷会很高兴地与他们聊很久。 没有客人时,爷爷会看书看报,还会教我唱歌,但更多的时候他就那么静静地坐在那儿,不知是在回忆还是思考。 3 也许是长年的坐卧损害了健康,爷爷去世得很早。 爷爷弥留之际被大伯接了过去。有一天母亲带我去大伯家看爷爷,我进了屋,看见爷爷正躺在病床上,见我进来,就睁大了眼睛看向我。 年幼的我还不理解死亡的含义,只觉得爷爷睁大眼睛的样子很有趣,还以为他马上就要像以前一样逗我玩了,于是嬉笑着跑了出去。 却没有意识到,那就是我和爷爷的最后一面。 4 爷爷虽然脾气温和,却是家里的定海神针。他去世一年之后,我们举家搬迁去了外省,从此故乡对我而言成为了遥远的回忆。 5 多年后的一天,回到老家大伯家小聚,期间大伯娘指着我感慨地说:“其实他最像爹。” 回想起爷爷的照片,我这才发现,原来不知不觉间,我竟真的有了几分爷爷的样子。
最近看了一些西方伪史论的言论,觉得很是荒诞,也写一点看法。 所谓西方伪史论,大体上就是认为西方的历史(部分甚至大部分)是伪造的,更深一层的思想则是认为西方也不过尔尔,没什么了不起的。 关于西方伪史的言论,一开始是一些人质疑西方历史中可能有造假的部分,这还属于正常的探讨。接着,一些人继续推进,认为古希腊的哲人比如亚里士多德等都是假的,流传下来的著作都是后人的伪作,这时的种种论点就有点经不住推敲了。再接着,既然西方那些先贤不存在,那么那些思想是哪里来的呢?有人就说全部源自古代中国,是被西方偷走的,更有甚者认为《几何原本》也是中国古人写的,现代科学也是中国古人发明的,还例举了种种所谓的“证据”,将西方文明贬得一无是处,同时将中国古代文明无限拔高。这些言论荒诞不经,错漏百出,但大概听着让人高兴,于是附和者甚多。 那些西方伪史论的鼓吹者,不知是真的相信这套理论,还是只是为了利益或者吸引流量故意语出惊人。不过,显然他们并不是最早这么做的,西方伪史论并不是什么新奇的东西,一百多年前,严复先生在《救亡决论》中就曾写道: “晚近更有一种自居名流,于西洋格致诸学,仅得诸耳剽之余,于其实际,从未讨论。意欲扬己抑人,夸张博雅,则于古书中猎取近似陈言,谓西学皆中土所已有,羌无新奇。…… “……尤可笑者,近有人略识洋务,着论西学,其言曰:「欲制胜于人,必先知其成法,而后能变通克敌。彼萃数十国人才,穷数百年智力,掷亿万赀财,而后得之,勒为成书,公诸人而不私诸己,广其学而不秘其传者,何也?彼实窃我中国古圣之绪余,精益求精,以还中国,虽欲私焉,而天有所不许也。」有此种令人呕哕议论,足见中国民智之卑。” 也就是说,早在晚清时期,“西方伪史论”、“西方科技是从中国偷的”这类言论就已经存在了,现在那些动机可疑的鼓吹者们也不过是拾人牙慧,却偏偏一个个以“独立思考”、“世人皆醉我独醒”的姿态在网络上大放厥词。 近代以来,西方领先世界已有数百年,这期间尤其是最近一百多年里,世界各国有无数学者穷尽毕生精力,不断研究总结西方领先的秘密,各类学说、著作汗牛充栋。倘若真如西方伪史论者所说,西方的科技都是从中国偷的,如此大规模的偷窃不可能没有痕迹留下来,为何一百多年来从未有人给出让人信服的证据?西方世界并不是一个整体,内部也常常互相拆台,甚至多次打得头破血流,并非铁板一块,难道他们偏偏就能在“科技是从中国偷的”这个话题上统一守口如瓶,这么多年没能让外人找到丝毫证据? 晚清时期,中华持续数千年“天朝上国”的地位被打破,国家和民族跌入深渊,这种心理上的强烈落差让人很难接受,人们迫切需要一些理论能让他们在面对西方文明时能保持一些心理优势,于是,西方伪史论应运而生。 现在我们则又走到了另一个重要的历史节点,经过无数国人的艰苦奋斗,中华文明再度崛起,已到达重回世界巅峰的前夜,但由于此前上百年落后的阴影实在太大,很多人面对西方时仍然不够自信,他们需要一种理论,证明我们配得上复兴后的地位,于是西方伪史论便再次有了土壤。 中国古代有过辉煌,留下了无数光彩夺目的思想和成果,但我们也不可否认,在近代我们落后了,西方文明有其值得学习之处。 我们确实需要重建自信,但这种自信应该实事求是,而不是建立在臆想或者对对手的无脑贬低之上。我们祖祖辈辈都在这块土地上奋斗,我们强大过,也落魄过,现在,我们通过数代人勤奋踏实的努力再次找回了一度丢失的东西,并且我们知道,即使再遭遇挫折我们也有勇气以及韧性重新站起来,这才是我们自信的底气。 而那些夸张荒诞的西方伪史论,或许能兴起一时,博得若干眼球,却终将贻笑大方,被扫入历史的垃圾堆。
之前的几年我一直在使用 K3s + Rancher 的组合来管理网站服务,不过前段时间迁移到了 Dokploy,在这儿记录一下要点。 为什么迁移? K3s + Rancher 的组合挺好,几年来一直运行稳定,不过对像我这样的非专业运维来说还是有点太复杂了,事实上几年来,我一直只在使用这个组合的一些最基础的功能。 去年看到有人介绍 Dokploy,了解了一下之后,发现它非常适合我的使用场景,同时又足够简单,于是花了一点时间做了研究,并最终决定迁移到 Dokploy。 除了 Dokploy 之外,还有 Coolify 等产品也不错,而且功能更多一些,读者朋友如果有需要也可以试一试。 云服务还是自托管? Dokploy 提供了云服务,订阅之后可通过他们的云服务管理自己的服务器。 云服务听起来是个不错的选择,可以减少自己运维的时间成本,我也花了 $4.5 订阅了一个月体验了一番。不过 Dokploy 的云服务在海外,我的服务器在国内,两者之间通讯不畅,因此体验并不是很好。 最后,我选择了自托管服务,将 Dokploy 和网站服务安装在同一个网络中。 安装 Dokploy Dokploy 的安装很简单,在一台干净的服务器上运行以下命令即可: curl -sSL https://dokploy.com/install.sh | sh 为了确保 Dokploy 能顺利运行,这台服务器建议至少要 2 CPU + 2 G 内存。 如果你的服务器在国内,安装时可能耗时较长,可以添加国内的 docker 镜像,比如修改 /etc/docker/daemon.json 文件,添加以下内容: { "registry-mirrors": [ "https://docker.1ms.run" ] } 安装完成之后,即可通过 http://{服务器 IP}:3000 的形式访问 Dokploy 后台。 添加服务器 Dokploy 成功安装后,马上就可以开始创建应用。不过,这时创建的应用会和 Dokploy 安装在同一台服务器上,你也可以在 Dokploy 后台添加新的服务器,并将应用添加到新服务器上。 个人建议用一台服务器专门运行 Dokploy,然后在 Remote Servers 面板中添加其他服务器。 添加服务器之后,还需要在 Actions 菜单中点击 Setup Server,并根据提示进行设置。 其中 Deployments 那个步骤可能耗时会很长,可以考虑点击 Modify Script,将脚本复制到对应的服务器上手动执行。 添加服务 添加完服务器之后,就可以添加项目,随后在项目中添加服务了。 添加服务这儿,最重要的一个设置是 Provider,即设置代码的来源。 Dokploy 支持多种常见的源,比如 Github,配置好之后只需向指定仓库和分支推送代码,Dokploy 就会自动拉取并构建代码,就像 Vercel 一样。 对小项目来说,这样的方式自然是很方便的,不过也可以用 Docker 作为 Provider,并使用第三方镜像服务。这样主要有两个好处: 镜像的构建工作在第三方执行,不会占用线上服务器资源; 第三方构建镜像时可以打上版本号 tag,后续回滚操作将会很方便。 我使用的是阿里云的容器镜像服务,填写方式类似下图: 更新服务 Dokploy 提供了丰富的 API,几乎所有操作都可以通过 API 完成。当某个服务需要更新时,可以登录网站手动修改相关值,也可以使用 API 更新。 比如,如果一个服务的 Provider 是 Docker,可以用类似下面的请求进行修改: curl -X "POST" "https://your-dokploy/api/application.saveDockerProvider" \ -H 'x-api-key: $YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d $'{ "applicationId": "$APP_ID", "dockerImage": "$DOCKER_URL" }' 有几个注意点: 授权头信息是 x-api-key: xxx...,而不是常见的 Authorization: Bearer xxx... 。 applicationId 的值在 URL 中,在界面上暂时没有显示。 比如某个服务的地址是 https://your-dokploy/dashboard/project/aaa/services/application/bbb,地址最后的 bbb 就是 applicationId。 通过 API 的方式,可以很方便地将服务的发布、回滚等操作集中到一处管理,或者与你现有的服务集成。 使用小结 使用 Dokploy 已经有一段时间了,整体而言还是很满意的,相对其他方案它很容易上手,且足够稳定,可用于生产环境。 不足是暂时还不支持定时任务,不过可以通过启动一个普通服务并在其中运行定时脚本的方式解决。 如果你有类似的需求,不妨也试一试 Dokploy。
最近在产品中用到了 Electron 中的 Kiosk 模式,记录一下要点。 什么是 Kiosk 模式? Kiosk 模式是一种专门为限制用户操作而设计的应用运行模式,通常用于构建锁定的全屏应用程序,禁止用户访问系统其他功能或退出应用。在这种模式下,应用程序占据整个屏幕,并且用户无法通过常见的方式(如键盘快捷键、窗口控制按钮等)退出或切换到其他应用。 Kiosk 模式的主要用途是为用户提供一个专注且受限的操作环境,避免对系统的其他部分产生干扰。 哪些场景下需要使用 Kiosk 模式? Kiosk 模式被广泛应用于以下场景: 公共信息亭:自助服务终端,如银行 ATM、自助点餐机、自助售票机。 展览展示:在博物馆、展览会、零售店中,用于展示信息或广告内容的屏幕。 教育场景:限制学生只能使用特定的教学应用,避免访问其他不必要的内容。 会议或演讲:锁定演示内容,避免误操作或退出。 数字标牌:作为广告屏幕或公告牌,循环播放内容。 当然,我在开发的是日常效率软件,并不属于以上场景。我用到 Kiosk 模式的场景主要如下。 图几截图软件 我开发并维护着一个截图软件图几,它有三种截图模式:全屏截图、窗口截图、区域截图。 其中区域截图的交互方式是:用户点击截图按钮(或按下截图快捷键),先生成当前屏幕的截图,随后显示一个全屏无边框窗口,在窗口中显示将刚刚生成的屏幕截图,同时允许用户在窗口上进行框选等操作。 这个无边框窗口就需要使用 Kiosk 模式,以免用户无意中切换窗口。当然,等用户完成或取消截图时,需要再退出或关闭对应的 Kiosk 窗口。 WonderPen 写作软件 WonderPen 写作软件最近添加了小黑屋模式,进入这种模式后,软件将全屏显示,屏蔽一切干扰,在完成预设的写作目标之前,将无法退出或切换到其他软件。 这个禁止退出的小黑屋,自然也使用了 Kiosk 模式。 Electron 中的 Kiosk 模式 在 Electron 中,将一个窗口设为 Kiosk 模式非常简单,在创建窗口时设置 kisok 属性为 true 即可。 有时,我们的窗口在创建时需要以普通模式显示,然后再在一定条件下切换为 Kiosk 模式,只需用类似下面的代码切换即可: win.setKiosk(flag) 其中 flag 是一个布尔值。 你还可以使用 win.isKiosk() 方法判断当前窗口是否为 Kiosk 模式。 在实践过程中,我发现很多时候只设置 Kiosk 属性还不太够,还需要设置 frame 等属性。以下是一个示例: const win = new BrowserWindow({ // 其他属性... closable: false, maximizable: false, minimizable: false, resizable: false, fullscreen: false, fullscreenable: false, frame: false, skipTaskbar: true, alwaysOnTop: true, useContentSize: true. autoHideMenuBar: true. movable: false. thickFrame: false. titleBarStyle: 'default', paintWhenInitiallyHidden: false, roundedCorners: false, enableLargerThanScreen: true, acceptFirstMouse: true, kiosk: true, // 其他属性... }) 即使这样设置之后,在 macOS 上有时仍会出现 Docker 栏和顶部系统菜单栏出现在 Kiosk 窗口上方的情况,因此还需要进一步设置 alwaysOnTop 的属性为 screen-saver,代码如下: win.setAlwaysOnTop(true, 'screen-saver', 1) 在 Windows 和 macOS 中,alwaysOnTop 的窗口有多种极别,按层级由低到高分别是: normal floating torn-off-menu modal-panel main-menu status pop-up-menu screen-saver 如果只是简单地 win.setAlwaysOnTop(true) ,则窗口的级别只是 floating,仍有可能被其他系统组件遮挡。 另外需要注意,在 macOS 下,太高的级别会挡住系统自带输入法的候选字窗口,如果你的 Kiosk 窗口需要用户输入,并且可能使用系统自带输入法的话,这个级别不能高于 modal-panel。 一些其他注意点 Kiosk 模式只对当前窗口有效,一个窗口只能覆盖一个屏幕,若用户有多个显示器,则需先检测显示器数量,然后创建多个 Kiosk 窗口分别覆盖。 设置 Kiosk 模式后,用户仍可以使用 Cmd+Q 这样的快捷键退出应用,因此需要在代码中监听窗口的 close 事件,并检查是否处在 Kiosk 状态,如是则阻止退出。代码类似下面这样: win.on('close', async (e: Electron.Event) => { if (win.isKiosk()) { e.preventDefault() return } // 其他逻辑 } Windows 下退出 Kiosk 模式后,窗口的大小可能会变成全屏大小,如希望退出时恢复原大小,可以在进入 Kiosk 模式之前先记住窗口大小,退出后再设置为原大小。 Kiosk 模式并不能阻止用户重启计算机。如果希望重启计算机后能自动恢复 Kiosk 状态,可以将软件设置为随系统启动,并且启动时自动进入 Kiosk 模式。
之前很长一段时间,这个博客一直在用云服务商提供的免费 SSL 证书,那个证书有一年有效期,也即一年只需要申请部署一次,因此全手动操作也不算麻烦,但现在免费 SSL 证书的有效期统一缩短为 3 个月了,意味着每 3 个月就要操作一次,这就让手动申请和部署变得麻烦起来了。 最近,我尝试了一下使用 acme.sh 申请 SSL 证书的方法,确实方便了不少,在这里记录一下。 安装 acme.sh acme.sh 是一个实现 ACME 协议的脚本,主要用途是申请或更新免费 SSL 证书。运行以下命令即可安装: curl https://get.acme.sh | sh -s email=my@example.com 更多安装方式可见官方文档:https://github.com/acmesh-official/acme.sh。 acme.sh 会被安装在 ~/.acme.sh 目录下。 手动申请证书 安装好 acme.sh 后,可以用以下命令申请证书: acme.sh --issue --dns -d mydomain.com -d "*.mydomain.com" --yes-I-know-dns-manual-mode-enough-go-ahead-please 记得把其中的 mydomain.com 换成你自己的域名。 上面的代码中,我申请了泛域名证书,所以同时添加了 mydomain.com 和 *.mydomain.com 域名。需要注意的是,*.mydomain.com 不包含 mydomain.com,如果你希望证书除了包含 www.mydomain.com 这样的二级域名,也包含 mydomain.com 的话,记得把 mydomain.com 也加上。 另外,*.mydomain.com 也不包含更深的层级,比如它包含 home.mydomain.com,但不包含 app.home.mydomain.com 。如果你需要更深层级的泛域名,需要把对应的域名也填上。 还需要注意的是最后一个参数 --yes-I-know-dns-manual-mode-enough-go-ahead-please 。acme.sh 更希望用户使用自动申请证书的方式(见下一小节),如果你确实需要手动申请,需加上这个参数,否则命令不会正常执行。 如果一切顺利,acme.sh 命令会输出两段 TXT 信息,需要你手动添加到对应域名的 DNS 解析中,以验证你确实对这个域名拥有权限。在证书申请完成之后,可以删除对应的 TXT 记录。 登录域名服务商(比如阿里云)后台,在域名解析中添加上对应的 TXT 记录,然后再运行以下命令,即可生成证书: acme.sh --renew -d mydomain.com -d "*.mydomain.com" --yes-I-know-dns-manual-mode-enough-go-ahead-please 证书会被保存在 ~/.acme.sh/ 目录下,包含以下四个文件: mydomain.com.cer 证书 mydomain.com.key 密钥 ca.cer fullchain.cer 全链路证书 其中在网站场景主要使用 fullchain.cer 文件和 mydomain.com.key 文件。 自动申请证书 可以看到,上面手动申请的步骤,主要的手动操作就是要为域名添加 TXT 记录以验证域名权限,acme.sh 支持让这个步骤自动化,即自动添加 TXT 记录,并在验证完成之后自动删除对应的记录。 以阿里云为例(如果你的域名是在阿里云注册并解析的),首先需要去阿里云控制台获取一个 AccessKey,建议专门设置一个 RAM 用户,只开通 DNS 权限。 得到 AccessKey 之后,在命令行中执行以下命令: export Ali_Key="key" export Ali_Secret="secret" 随后再执行以下命令,即可自动申请或更新证书了: acme.sh --issue --dns dns_ali -d mydomain.com -d "*.mydomain.com" 注意 --dns 参数后面的值为 dns_ali。 一切顺利的话,证书申请会自动完成,并被保存在 ~/.acme.sh/ 目录下。 其他各大域名服务商的自动申请方式类似,具体可参见官方文档。 一些注意点 如果你使用了自动申请,AccessKey 会被明文保存在 ~/.acme.sh/account.conf 文件内,如果介意,可在申请完之后修改这个文件并删除对应的 AccessKey。 另外,使用自动申请后,acme.sh 会添加一条定时任务,每天自动检查证书是否需要更新。可运行以下命令查看当前系统的定时任务列表: crontab -l 现在 acme.sh 默认使用的证书颁发机构是 ZeroSSL,还有一些其他可选机构,比如 Let's Encrypt。可以用 --set-default-ca 修改默认证书颁发机构,比如: acme.sh --set-default-ca --server letsencrypt 我没有修改 CA,在使用默认的 ZeroSSL 的证书,目前来看暂时没有遇到什么问题。 除了自动申请证书外,大部分网络服务商也支持自动上传 SSL 证书,不过这部分我还没有研究,后续如果觉得值得记录,会另外写文分享。
最近完善了一下产品的购买流程,其中的一项工作是处理来自苹果 App Store 平台的 CONSUMPTION_REQUEST 消息,在这儿记录一下要点。 消息说明 App 如果使用了苹果的内购(IAP),每当发生用户购买、续费、退款等操作时,苹果服务器都会向开发者指定的地址发送一条消息,不同的消息有不同的 notificationType 值,其中 CONSUMPTION_REQUEST 消息的意思是用户为应用内购买发起了退款请求,App Store 请求开发者服务器提供用户的消费数据,用于协助 App Store 决定是否给用户退款。 开发者可以忽略 CONSUMPTION_REQUEST 消息,也可以根据需要,在 12 小时内回应 App Store。 回应消息 要回应 CONSUMPTION_REQUEST 消息,只需向指定的地址发一个 PUT 请求即可。具体细节可见官网文档。 这个 PUT 消息的要点主要有两个: 在 Header 中添加认证 token 信息; 在 Body 中发送一个 JSON 格式的对象,向 App Store 提交对应的信息。 数据内容 我们先看 Body 中的数据内容。 根据文档,数据字段以及含义大致如下: { "accountTenure": 0, // 用户年龄段,0 表示未知 "appAccountToken": "", // 用户 uuid,由于之前没有设置,此处留空 "consumptionStatus": 0, // 消费状态,0:未知,1:未消费,2:部分消费,3:全部消费 "customerConsented": True, // 用户是否同意提供消费数据 "deliveryStatus": 0, // 交付状态,0:已成功交付 "lifetimeDollarsPurchased": 0, // 用户在应用内购买的总金额,0 表示未知 "lifetimeDollarsRefunded": 0, // 用户在应用内退款的总金额,0 表示未知 "platform": 1, // 平台,0:未知,1:苹果平台,2:其他平台 "playTime": 0, // 用户在应用内的总时间,0 表示未知 "refundPreference": 1, // 商家对退款的意见,0:未知,1:支持,2:不支持,3:不确定 "sampleContentProvided": True, // 是否已经提供了示例内容 "userStatus": 1, // 用户账号状态,0:未知,1:活跃,2:暂停,3:关闭,4:受限 } 你可以根据需要,修改对应字段的值。 请求 Header 请求 Header 中有两个必填的自定义字段,分别是: Content-Type 值固定是 application/json Authorization 值为 Bearer $jwt_token 其中 jwt_token 必须要正确填写,否则请求会返回 401 错误。 jwt_token 的具体生成说明可见官方文档,大致格式类似下面这样: Header: { "kid": "ZA12345678", "alg": "ES256", "typ": "JWT" } Payload: { "iss": "your_uuid", "iat": 1723173620, "exp": 1723183620, "aud": "appstoreconnect-v1", "bid": "your_bundle_id" } 其中 kid、iss,以及生成 JWT 时所需的私钥等几项,需要去 App Store Connect 后台生成。 JWT 私钥 如果你之前还没有生成过对应的私钥,可以前往 App Store Connect 后台的“用户和访问” → “集成” → “App 内购买项目”页面生成,如下图所示: 生成之后,可以在这个页面下载 .p8 格式的私钥。注意这个私钥只能下载一次,下载之后请妥善保存,如果不慎遗失,只能删除再重新生成一个。 上面生成 JWT 所需的 kid 对应上图中的“密钥 ID”,iss 对应“Issuer ID”,私钥即上面下载的 .p8 文件中的内容。 然后就可以用类似下面的方法生成 JWT 了: import jwt jwt_token = jwt.encode( payload, private_key, algorithm="ES256", headers=headers, ) 最后,将得到的 jwt_token 以 Bearer $jwt_token 的形式包含在请求头的 Authorization 中,发起 PUT 请求即可。 如果请求返回 202 状态码,表示请求成功了。如果是其他值,可根据错误状态再仔细检查处理。
最近在开发 Flutter 项目,其中 iOS 版 App 账号登录时,需要适配 1Password 等密码管理器,即需要告诉 1Password 等密码管理器当前 App 的登录特征信息(域名),以及应该填写界面上的哪些表单项。在这儿记录一下要点。 基本设置 我们首先要处理的,是让 App 和某个域名(通常是官网域名)关联,这样在 App 中唤起 1Password 填写密码时,1Password 才知道应该显示哪些账号。 这儿主要有三个步骤。 Apple 开发者后台设置 在 Apple 开发者后台的 Certificates, Identifiers & Profiles 页面,记得要选中 Associated Domains 选项,如下图所示: Xcode 中的设置 接下来,要在 Xcode 中为你的 App 添加关联域名,如下图所示: 在 Domains 那一栏,添加 webcredentials:你的域名 即可,比如你的域名是 test.com,那么添加 webcredentials:test.com 就行。 网站设置 最后,还需要在你的域名对应的网站上添加一个认证文件,证明指定 App 确实和当前域名相关。这个文件的文件名固定为 apple-app-site-association,可以放在网站的根目录,或者 .well-known 目录下,确保可以通过网络访问到。 这个文件的用途很多,可能还会包含一些其他字段,和密码管理器相关的主要是以下内容: { "webcredentials": { "apps": [ "TeamId.BundleId" ] } } 确保你的 apple-app-site-association 文件包含 webcredentials 字段,并将其中 apps 中的 TeamId、BundleId 换成你的真实 ID。 Flutter 中的设置 为了得到更好的登录体验,Flutter 中也要做一些设置,主要是告诉 1Password 等密码管理器需要填写哪些字段,以及各个字段分别对应什么内容。 关键代码如下: @override Widget build(BuildContext context) { // ... return Container( body: Center( child: AutofillGroup( child: Column( children: [ TextField( autofillHints: const [AutofillHints.email], decoration: InputDecoration( labelText: 'Email', ), ), TextField( autofillHints: const [AutofillHints.password], decoration: InputDecoration( labelText: 'Password', ), ), ElevatedButton( onPressed: () { // Submit the form }, child: Text('Submit'), ), ], ), ), ), ); } 其中最关键的有两处,一是需要自动填写的表单部分,需要用 AutofillGroup 组件包起来,这样 1Password 就知道哪些字段是需要自动填写的。二是 Email、用户名、密码等需要填写的字段,需要添加 autofillHints 属性,比如 autofillHints: const [AutofillHints.email],这样 1Password 才知道当前字段应该填什么内容。 完成这些设置之后,App 登录时就应该能正常适配 1Password 了。
最近几个月打了很多次牌,有时是线下聚会时和朋友玩,有时则是在手机上玩。玩得久了,逐渐发现扑克牌游戏和现实生活中的规则有一些类似之处。以下是一些感想。 手中的牌 打牌时,手中拿到什么牌非常重要,这一点很容易理解,无论你是高手还是菜鸟,如果起手就拿到一手好牌,那么只要不乱打,并且运气不是差到极点,你基本上都能赢。同样的,就算你是绝顶高手,如果拿到一把烂牌,要赢恐怕也非常难。 当然,拿到极品好牌和极品烂牌的概率都不大,很多时候,我们以及我们的对手拿到的都是中等牌,这种时候,如何组合手中的牌打出最好的效果,就看各自的技术了。 我们的人生也类似,总有一些人一开始就拿到一手好牌,比如家境良好,父母见识不凡,自身也健康聪明,因而只要自己不走错路,人生总体上会非常顺利。还有一些不那么幸运的人,出生在落后的地方贫穷的家庭,几乎没有什么可利用的资源或者助力,要获得成功可就不容易了。 拿到好牌时,不要得意忘形,因为你的成功很大程度源于运气。拿到烂牌时,也不要破罐子破摔,认真思考,尽最大努力打好手中的牌,因为只有这样你才能多一点获胜的机会。 合适的才是最好的 摸牌的时候,一般来说摸到大牌比摸到小牌更好一些,不过稍有经验就会发现,和单独的大牌相比,那些能让现有的牌组合起来的牌可能更好。比如,有一些时候,摸到一张最小的 2 可能会让你的几张散牌组成同花顺,这时对你而言 2 就比大王更好。 生活和工作中也是类似,有一些团队,单独来看每位成员可能都相对普通,但由于配合出色,于是团队整体的战斗力非常强悍。 团队招人的时候,也不是招越牛的人越好,而是要看新来的人能否让团队的整体能力得到提升。有时候,也许加入一位履历一般但却能搞定一些其他人不擅长处理的小事的成员,会让团队整体焕发新生。同样的,一位看似不重要的成员离开,也有可能打断团队内部的某种连接,让团队效率大受影响。 寻找人生伴侣也是如此,那些光彩夺目的潜在选项当然也不错,但一位能与你互补,让你成为更好的自己的伴侣,也许是更好的选择。 本钱 拿到什么牌很大程度上能决定你单局的胜负,有多少本钱则决定你能在牌桌上待多久。 以腾讯欢乐斗地主的掼蛋游戏为例,新手场单场输赢封顶是 5 万欢乐豆。即如果你手中的欢乐豆不足 5 万,那么你赢的时候手中的欢乐豆翻倍,输的时候会赔得精光。当然,实际上输赢的数额还取决于对手的豆子够不够,不过为了简化讨论,此处我们假设对手总是有足够的豆子。 这个规则很容易理解,而且看起来似乎也很公平。但在玩了很多场之后,我发现这其实是一个对水平普通且本钱较少的玩家非常不友好的规则。 假设你是一位普通水平的玩家,即你的水平处在中游,每局有 50% 的概率赢,也有 50% 的概率输,并且初期你只有 1000 欢乐豆,你会面临什么情况呢? 当你的豆子数量在 5 万以下时,无论你之前赢了多少次,只要输一次,你的豆子就被清空了。也就是说,想要让自己的豆子超过 5 万,你需要连赢 \(6\) 次,才能让自己的豆子从 1000 增长到超过 5 万的数量(\(1000 \times 2^6 = 64000\))。但是,由于你的赢率只有 50%,因此连赢 6 次的概率也只有 \(\frac{1}{2^6}\),即 \(\frac{1}{64}\) 或者 1.56% 。 但即使你有了 6.4 万豆子,你仍然不安全,只要连输两次(发生的概率为 \(\frac{1}{4}\) 或 25%,这并不是一个小概率),你就又会回到一无所有的状态。要让自己更安全一些,你可能需要至少 20 万豆子,这样只有连输四次(发生的概率为 \(\frac{1}{16}\) 或 6.25%),你才会输光所有豆子。而要从 1000 豆子变成 20 万豆子,你需要先连赢 6 次,然后在接下来保住本金的情况下再净赢至少 3 次。 粗略计算一下就可以知道,作为一名赢率只有 50% 的中等水平玩家,取得这个成就的概率不足 1%。 另一方面,如果你仍然只是一名中等水平的玩家,但你已经有 20 万甚至更多的豆子了,会怎么样呢? 由于你的输赢概率各一半,并且每次输赢都只是增减 5 万欢乐豆而不是一下子输光所有,因此你的账户余额从概率上来讲将会一直持平。期间可能会因为随机因素出现一些波动,甚至有非常小的概率你在某次大波动中余额被击穿从而破产,但更大的概率是你的资产经过波动之后又回归到初始值附近。 换一句话来说,对一名中等水平玩家来说,从底层逆袭的成功概率极小,因为在初期他需要连赢很多次才行,同时只要输一次就会完全失败。但同样水平的玩家,如果一开始就拥有大量本钱,他却有很大的概率能守住这些财富,因为他不怕输,即使输了也仍然有机会赢回来。也即本钱越少,越不容易出头,财富越多,则越容易守住财富。 这大概也是阶级固化的一种解释吧。
科幻小说《星震》是《龙蛋》的续集,讲的是人类造访一颗名为龙蛋(也叫蛋星)的中子星,行程结束准备离开时,蛋星发生了一场星球级别的大地震(星震),星球上的奇拉文明几乎毁灭,随后奇拉与人类相互帮助、相互拯救的故事。 读这部小说之前,最好先读过前作《龙蛋》,否则对故事的背景可能会难以理解。在继续写这篇读后感之前,也让我们先回顾一下《龙蛋》的前文提要以及设定。 蛋星是一颗由比太阳还要大的红巨星坍塌而成的中子星,直径只有二十公里,因此星球上的物质密度极高,引力也极为强大,根据书中的设定,蛋星表面的引力强度是地球上的 670 亿倍! 在这种环境下,蛋星上物质的形态与地球上常见物质的形态自然大不相同,那儿的各种活动不再依赖分子级别的化学反应,而是速度更快的核子级别的物理反应,于是对那颗星球上的智慧生命奇拉而言,它们的时间比我们快 100 万倍。所以,在前作《龙蛋》中,人类刚与奇拉们相遇时,奇拉们还处在石器时代,但一星期后,奇拉们的科技就已经远超人类。 然而,就在人类基本完成考察准备离开时,一场灾难突如其来,星震发生了,蛋星上的奇拉文明遭受了毁灭性的打击,无数奇拉死去,大量建筑和设备报废,只有少数幸运儿活了下来,但他们几乎不会操作和修复那些先进的设备。 不幸中的万幸是当时蛋星上空还有一些正在太空中执行任务的奇拉,这些太空奇拉受过良好的教育,依然掌握着部分先进技术,它们是奇拉恢复文明的希望。但问题是,地表的降落场地都已经在星震中被毁坏了,他们无法降落,只能先设法联系上地表的奇拉,并远程指导他们修复降落场。 一开始,一切都很顺利,但不久之后,地表上一些原本处在社会边缘的奇拉有了其他心思,他们觉得现在这种没有法律可以随心所欲的灾后世界似乎也挺好,如果让太空奇拉降落,他们是不是又要回到从前那种处处受限的状态? 一番争斗之后,这些“坏”奇拉赢了,他们建立了新的秩序,蛋星的地表文明退回了蛮荒时代。太空奇拉无法降落,只能另想办法,而唯一的办法可能会让正准备离开的地球人处于危险之中…… 最后,在太空奇拉、地表“好”奇拉以及人类的共同努力以及牺牲之下,他们终于重建了文明,而且在 100 万倍的时间流速下,很快就达到了比之前更高的高度。 整个故事对人类而言都是在一天之内发生的,但对奇拉而言却已经过去了很多代人。 比较有意思的是故事中没有一个贯穿始终的超级英雄个体,很多奇拉在为恢复文明而努力,某位奇拉在某个阶段出场很多,正当你以为 Ta 是主角时,Ta 却可能突然因为意外或战争“流逝”(故事中奇拉的死亡被称为“流逝”)了。这种安排可能会让期待超级英雄的读者读起来不那么“爽”,但掩卷细思却也觉得非常合理,因为他们面临的是星球级别的灾难,在这种灾难面前个体的力量是渺小的,只有所有人合力,一起向着一个方向前赴后继地努力才有可能取得最后的成功。 故事中也探讨了一些科幻作品中常见的问题。比如,科技文明的诞生是必然吗?作者认为并不是,故事中作者借一位奇拉的思考表达了这样的观点:蛋星上的奇拉们虽然很早就有了智慧,进入了原始文明,但如果不是人类飞船的造访和影响,也许奇拉会继续在低水平的文明中徘徊漫长时光。而如果在星震发生的时候,他们仍然没能发展到可以进入太空,那么迎接他们的将会是一场更彻底的倒退甚至毁灭。 另一个问题是,奇拉文明的科技水平已经远远超过了地球文明,为什么他们没有入侵或者消灭地球文明?故事中也给出了解释,因为奇拉文明是中子星文明,由普通物质组成的地球世界在他们眼中几乎是空的,太阳系的资源对他们来说也几乎毫无用途,换句话说就是他们和地球文明处在完全不同的生态位上,没有竞争关系。做一个不太恰当的类比的话,奇拉文明和地球文明就好像狮子和野草一样,两者所需的资源差异极大,又对彼此都没有威胁,因此完全可以和平共处。如果奇拉文明遇到另一个中子星文明,或许就不会如此和谐了。 还有一个经常出现在科幻作品中的问题是,为了活下去,我们可以吃同类的尸体吗?故事中奇拉文明和人类文明有着完全不同的道德观和选择,对奇拉文明而言这根本不是一个问题,因为在他们的文化中吃死去同类的尸体是很正常的事,即使在不缺食物的情况下,同类的尸体仍然是他们喜爱的美食,甚至宴会中最好的菜可能就是一份烤奇拉。但对地球文明来说,这却是让人难以接受的事,故事中地球人类遇到这个困境时,果断拒绝了这个选项,宁可饿死。 在本书的最后,作者还描写了奇拉文明中的一些细节,有一个细节我觉得挺有趣:奇拉文明中的建筑都是没有房顶的。 为什么会这样?作者解释说因为蛋星的引力太大了,加上大气稀薄(蛋星上的大气由电子、铁离子或其他典型星壳核子组成),因此自然界中从来没有进化出会飞的生物,大概也没啥能跳得很高的生物,于是他们的世界基本上是二维的,几乎从来不会有麻烦来自上方,所以他们只需要建四面墙把自己围起来就安全了,不需要屋顶。 总的来说,这是一部有趣的硬核科幻小说。
工作多年,被人管过也管过人,有一个不算新颖但深有感触的心得,即管理者应该要尽量避免“微操”。 “微操”一词源自游戏,指玩家对游戏中的单位进行非常细微的操作,比如在即时战略游戏中,非常细致地控制每个士兵的行为。 对一些游戏来说,“微操”是褒义词,表明玩家的水平非常高,能发挥出各个单位最大的战斗力,这很好理解,因为游戏中的单位通常智能程度不高,同时总是能不折不扣地执行玩家的指令。但在现实世界中,“微操”则通常不是什么好事。 我曾有一段比较痛苦的工作经历,当时的上司就非常喜欢“微操”,细微到产品的每个设计细节都要管,比如每个按钮的位置和文案、每个输入框的样式以及每个操作的流程他都要发表意见。 他的能力确实很强,在业内小有名气,也有过几个成功的项目,因而十分自信强势,加上有着上司的身份,在团队内威信极高,对产品的各个细节都有着最终裁决权。 刚加入团队时,我非常有干劲,尽心尽力地投入产品的设计和开发,但一段时间后,我发现我提交的方案常常会被他改动很多地方,而有时一个看似很小的改动却可能意味着之前的大量工作需要推倒重来。 一开始我陷入了自我怀疑中,是不是我的能力真的太差了?不然为什么有这么多细节不符合上司的要求?我尝试努力总结上司的各个修改逻辑,但大部分修改我实在看不出有什么规律,对我来说,哪些地方会被打回似乎是完全随机的。 反复的细节修改以及确认,让我的工作量增加了很多,精神上也疲惫不堪。 终于有一天,在再一次无奈的修改之后,我决定摆烂了。由于无法预测哪些方案可能会被打回修改,我便尽可能地不做决定或者少做决定,每一个我觉得有被打回风险的设计都尽量先去问一问他。 摆烂之后,我果然轻松了不少,很少再遇到方案被打回或者需要大量修改的情况了,因为实际上我只是在做一些不太需要动脑子的执行工作,需要决策的部分都扔回给了上司。 但这显然不是正常的或健康的状态,一段时间后,上司发现自己越来越累,于是变得愤怒,开会时常常斥责我们没有达到他的期望。我也很愧疚,尝试再次担起责任,但当我再次深入方案设计然后继续遭遇大量细节被打回后,我彻底摆烂了,并且在不久之后离开了那个团队。 后来我回顾那段工作经历,逐渐意识到了其中令彼此痛苦的根源。那些被打回修改的细节设计,真的都有必要,或者都有对错之分吗? 不可否认,上司提出的一些意见确实更好,遇到这样的修改要求时,相信任何一位合格的员工都会接受,并且心悦诚服。但在那段工作经历中,从数量上来说这样的修改只占少数,更多的则只是一些纯粹的偏好性质的改动,比如一些文案的表述方式,几个元素是左对齐还是居中对齐,一些操作流程的顺序安排等等。在上司介入的阶段,我们的方案已经经过了反复的思考和讨论,通常来说已经没有明显的问题,但上司总是能提出很多修改意见,而在我们看来这些意见和原方案相比并没有明显的优劣或者绝对的对错,无论哪种选择都有其道理,但说到底,很多方案的差异其实只是口味或习惯的不同罢了,就像有人喜欢川菜,有人喜欢淮扬菜,因此在烹饪时流程和用料不一样,仅此而已,但只要在整体上保持风格一致,做出来的就不失为一道好菜。 每个人都有自己的偏好,除非他人的思维习惯和你完全相同,否则他的方案和你预想的方案多多少少总是会有差异。员工如果是新手,管理者手把手地指导自然没有问题,但这显然不应该成为常态,当员工成长起来,有足够的经验之后,作为管理者,我想只要大方向没有问题,细节上应该允许下属自由发挥。这也是所谓的“抓大放小”、“充分放权”。 况且很多时候,作为一线员工,在产品方案上他们了解的信息可能比管理者更多,很多方案可能已经是综合考虑体验和成本之后的结果,一个看似不起眼的“小”修改意见,可能会需要几个小时甚至几个人日的工作,而连着几个“小”意见,则可能意味着成本的飙升。 喜欢“微操”、坚持按自己的喜好指挥每一个细节的管理者,从本质上来讲,要么是对员工不信任,认为员工没有能力做好工作,要么则是沉迷于操纵一切的掌控感,不愿意放权。无论哪种情况,对团队来说都是有害的。 如果管理者真的认为某个设计或者细节很重要,必须严格执行,那么最好的方式是在一开始就清晰地向员工说明,而不是等到方案都已经基本完成时才不断提出各种“重要”修改意见。 如果员工自己的创意总是被管理者修改甚至否定,那么逐渐地,员工就会放弃主动,——既然自己没有决定的权力,那么就干脆做个不动脑筋的执行者吧。最终,员工不开心,没有足够的参与感和成就感,成长有限;管理者也不开心,觉得招了一帮废物,什么都要自己来决定,让自己都没有时间去做更重要的工作了,却没有意识到造成这一切的根源可能就是自己。 在团队中,管理者天然有着更大的权力,合格的管理者应该学会控制自己的欲望,合理地使用这个权力。很多时候,并不是管得越多越细就越好,作为管理者,在准备提出意见时应该先想一想,这是必要的吗?
爷爷生于 1924 年,今年是爷爷诞辰 100 周年。 1 爷爷受过良好的教育,解放前当过老师,在山村小学教书育人,也曾一腔热血,保护过被敌人追捕的游击队员。解放后,爷爷先在县法院工作,后转向水利、航运方向,参与主导了很多复杂的工程,并逐渐成为这个领域的知名专家。 1970 年代,云南省开发澜沧江,爷爷以专家身份被聘为技术指导。那个年代的工作条件非常艰苦,在一次位于西双版纳的工程作业中发生了意外,奋战在一线的爷爷不幸负伤致残,从此不得不离开热爱的事业,提前退休回到家乡。 2 从我有记忆起,爷爷就是一个整天坐在客厅藤椅上的老头,很瘦,很慈祥,从未见他发过脾气,对谁都很和蔼。 爷爷的伤在腿上,这个伤让他失去了自由行走的能力,因此他只能天天宅在家,极少出门。 爷爷有一大箱子书,我曾好奇地翻过,但那时我还不识字,只依稀记得书中有很多复杂的图表。 有时会有一些同样头发花白的人来看爷爷,爷爷会很高兴地与他们聊很久。 没有客人时,爷爷会看书看报,还会教我唱歌,但更多的时候他就那么静静地坐在那儿,不知是在回忆还是思考。 3 也许是长年的坐卧损害了健康,爷爷去世得很早。 爷爷弥留之际被大伯接了过去。有一天母亲带我去大伯家看爷爷,我进了屋,看见爷爷正躺在病床上,见我进来,就睁大了眼睛看向我。 年幼的我还不理解死亡的含义,只觉得爷爷睁大眼睛的样子很有趣,还以为他马上就要像以前一样逗我玩了,于是嬉笑着跑了出去。 却没有意识到,那就是我和爷爷的最后一面。 4 爷爷虽然脾气温和,却是家里的定海神针。他去世一年之后,我们举家搬迁去了外省,从此故乡对我而言成为了遥远的回忆。 5 多年后的一天,回到老家大伯家小聚,期间大伯娘指着我感慨地说:“其实他最像爹。” 回想起爷爷的照片,我这才发现,原来不知不觉间,我竟真的有了几分爷爷的样子。
最近看了一些西方伪史论的言论,觉得很是荒诞,也写一点看法。 所谓西方伪史论,大体上就是认为西方的历史(部分甚至大部分)是伪造的,更深一层的思想则是认为西方也不过尔尔,没什么了不起的。 关于西方伪史的言论,一开始是一些人质疑西方历史中可能有造假的部分,这还属于正常的探讨。接着,一些人继续推进,认为古希腊的哲人比如亚里士多德等都是假的,流传下来的著作都是后人的伪作,这时的种种论点就有点经不住推敲了。再接着,既然西方那些先贤不存在,那么那些思想是哪里来的呢?有人就说全部源自古代中国,是被西方偷走的,更有甚者认为《几何原本》也是中国古人写的,现代科学也是中国古人发明的,还例举了种种所谓的“证据”,将西方文明贬得一无是处,同时将中国古代文明无限拔高。这些言论荒诞不经,错漏百出,但大概听着让人高兴,于是附和者甚多。 那些西方伪史论的鼓吹者,不知是真的相信这套理论,还是只是为了利益或者吸引流量故意语出惊人。不过,显然他们并不是最早这么做的,西方伪史论并不是什么新奇的东西,一百多年前,严复先生在《救亡决论》中就曾写道: “晚近更有一种自居名流,于西洋格致诸学,仅得诸耳剽之余,于其实际,从未讨论。意欲扬己抑人,夸张博雅,则于古书中猎取近似陈言,谓西学皆中土所已有,羌无新奇。…… “……尤可笑者,近有人略识洋务,着论西学,其言曰:「欲制胜于人,必先知其成法,而后能变通克敌。彼萃数十国人才,穷数百年智力,掷亿万赀财,而后得之,勒为成书,公诸人而不私诸己,广其学而不秘其传者,何也?彼实窃我中国古圣之绪余,精益求精,以还中国,虽欲私焉,而天有所不许也。」有此种令人呕哕议论,足见中国民智之卑。” 也就是说,早在晚清时期,“西方伪史论”、“西方科技是从中国偷的”这类言论就已经存在了,现在那些动机可疑的鼓吹者们也不过是拾人牙慧,却偏偏一个个以“独立思考”、“世人皆醉我独醒”的姿态在网络上大放厥词。 近代以来,西方领先世界已有数百年,这期间尤其是最近一百多年里,世界各国有无数学者穷尽毕生精力,不断研究总结西方领先的秘密,各类学说、著作汗牛充栋。倘若真如西方伪史论者所说,西方的科技都是从中国偷的,如此大规模的偷窃不可能没有痕迹留下来,为何一百多年来从未有人给出让人信服的证据?西方世界并不是一个整体,内部也常常互相拆台,甚至多次打得头破血流,并非铁板一块,难道他们偏偏就能在“科技是从中国偷的”这个话题上统一守口如瓶,这么多年没能让外人找到丝毫证据? 晚清时期,中华持续数千年“天朝上国”的地位被打破,国家和民族跌入深渊,这种心理上的强烈落差让人很难接受,人们迫切需要一些理论能让他们在面对西方文明时能保持一些心理优势,于是,西方伪史论应运而生。 现在我们则又走到了另一个重要的历史节点,经过无数国人的艰苦奋斗,中华文明再度崛起,已到达重回世界巅峰的前夜,但由于此前上百年落后的阴影实在太大,很多人面对西方时仍然不够自信,他们需要一种理论,证明我们配得上复兴后的地位,于是西方伪史论便再次有了土壤。 中国古代有过辉煌,留下了无数光彩夺目的思想和成果,但我们也不可否认,在近代我们落后了,西方文明有其值得学习之处。 我们确实需要重建自信,但这种自信应该实事求是,而不是建立在臆想或者对对手的无脑贬低之上。我们祖祖辈辈都在这块土地上奋斗,我们强大过,也落魄过,现在,我们通过数代人勤奋踏实的努力再次找回了一度丢失的东西,并且我们知道,即使再遭遇挫折我们也有勇气以及韧性重新站起来,这才是我们自信的底气。 而那些夸张荒诞的西方伪史论,或许能兴起一时,博得若干眼球,却终将贻笑大方,被扫入历史的垃圾堆。
看到一篇有趣的短文《增加新的柜员后会发生什么?》,讲的是排队论。 银行柜员问题 文章中讨论了这样的一个问题: 假设一家小银行只有一名柜员,每位顾客平均需要 10 分钟的服务时间,每小时预计有 5.8 位顾客到达,那么预计每位顾客的等待时间是多少? 乍一看之下,每小时有 60 分钟,但只有 5.8 位顾客到达,即平均每 10.3 分钟才有一位新顾客到达,同时每位顾客平均只需要 10 分钟的服务时间,似乎一位柜员就足够服务好这些顾客了,每位顾客都不用等待或者不会等待太久? 但事情显然不会这么简单,假设顾客到达的时间和所需的服务时长都是随机的,那么每位顾客在得到柜员服务之前,平均需要等待近 5 小时之久! 为什么会这样?其实稍加思考,我们就能发现端倪,因为那个 10 分钟只是平均时间,但每位顾客所需的实际时间差异可能会很大,有人可能只需要一两分钟,有人则可能会需要几十分钟甚至几小时,而一旦遇到一位需要长时间服务的顾客,所有后来的顾客都只能排队等待。同时,顾客的到达时间间隔也不是严格的 10.3 分钟,有时可能几十分钟甚至几个小时都没人来,有时则一下子连着来了好几位顾客。 那么,如果增加一位柜员会怎么样呢? 你或许会以为,增加了一位柜员后服务处理能力加倍,那么顾客的平均等待时间也应该对应地减半,平均每位顾客大概只需要等待两个多小时吧?但事实上,增加一位柜员后,顾客的平均等待时间会一下子下降到 3 分钟,仅为原来的 \(\frac{1}{93}\)。 也就是说,只有一位柜员时,顾客的平均等待时间可能会长得难以接受,但增加一位新的柜员之后,顾客的体验就相当良好了。 原因也很简单,当有两个柜台同时提供服务时,如果一个柜台被阻塞了,顾客可以转向另一个柜台,而不用在后面干等。当然,也存在连着来了两位需要长时间服务的顾客的情况,这时两个柜台都被阻塞,新顾客仍然需要长时间等待,但在上面讨论的这个场景中,这种情况发生的概率很低,对等待时间的平均值影响非常微小。 泊松分布和指数分布 在统计学上,顾客的到达时间一般认为符合泊松分布,顾客所需的服务时长一般认为符合指数分布,两者都是非常常见的分布。 泊松分布通常用于描述在固定时间或空间内随机发生的离散事件的次数。比如上面例子中,不断有顾客进入银行,具体什么时候有顾客进入是一个随机事件,无法预测,但我们可以估算“指定的一段时长(比如 1 小时)内有 \(n\) 位顾客到达”的概率 \(P\) 是多少,公式如下: \[P(X=n)=\frac{e^{-\lambda}\lambda^{n}}{n!}\] 其中 \(\lambda\) 表示事件发生的频率,这个频率是通过统计之前收集的数据得到的,比如本例中我们已知每小时预计有 5.8 位顾客到达,那么以 1 小时为单位,\(\lambda\) 即是 5.8。 根据这个公式,可以计算出 1 小时内有 n 位顾客到达的概率,当然,n 在 5.8 左右时概率最大,越偏离 5.8 概率越小。比如 1 小时内有 100 位顾客也是有可能的,只是这个概率会小得可忽略不计。 以下是一些常见的泊松分布的事件: 电话呼入:在一个呼叫中心,每小时接到的电话数量。 公交车到站:在固定的时间内到达某个公交站的公交车数量。 流量统计:在网站或网络节点上,每分钟的访问或数据包数量。 自然现象:如一定时间内某地区的雷击次数或降雨事件。 排队系统:例如银行柜台在特定时间内接待的顾客数量。 指数分布和泊松分布可以认为是描述相同现象的不同部分:泊松分布关注一段时间内发生 n 个随机事件的概率,而指数分布关注每两个连续的随机事件之间的时间间隔。 指数分布的公式也与泊松分布的公式相关。比如,如果想计算一段时间内某件事不发生的概率,就相当于计算上面泊松分布中 \(n=0\) 的概率: \[ \begin{align*} P(X=0)&=\frac{e^{-\lambda}\lambda^{0}}{0!} \\ &=e^{-\lambda} \end{align*} \] 而在一段时间内事件发生的概率,则是 1 减去上面的值,即: \[1-e^{-\lambda}\] 以下是一些常见的指数分布的事件: 顾客服务时间:在银行或超市收银台,每位顾客的服务时间。 机械故障:如电梯或生产线设备之间的故障时间间隔。 半衰期:放射性物质的原子衰变时间间隔。 交通事故:在特定路段上,两次交通事故之间的时间间隔。 生物学事件:如某种细胞分裂或动物捕食行为之间的时间间隔。 项目管理中的排队论 排队论在各种服务性质的工作中非常有用,除此之外,软件开发中也有很多场景可以参考。 软件的开发和维护过程与柜台服务类似,不断地有新的需求(顾客)过来,程序员(柜员)则负责处理这些需求。新需求的到达时间以及工作量都是随机的,可以大致认为分别符合泊松分布和指数分布。 团队的管理者常常会期待这样一种理想状况:每一个需求过来时都能很快得到处理,同时员工人员基本没有冗余(即没有人闲着)。但只要在软件开发团队待过就会知道,这种理想状况极少发生,大多数时候都是需求不断积压,程序员加班加点仍然难以赶上进度。 如果你仔细地测算每一个需求的工作量,以及平均每周有多少新需求,你可能会发现如果只看平均值数据,团队应该能够处理这些工作才对,但事实却是很多需求都延期了。看了上面的讨论,你现在应该知道了,很多时候工作安排并不能简单地算平均值,因为需求具有随机性,如果你希望对抗这种随机性,让新需求能尽快得到处理,一些冗余将是必须的。 当然,软件开发和柜台服务之间也有很大的不同,其中之一是开发人员通常不能简单地互换,和柜员相比,开发人员之间交接工作的成本可能会比较高。不过总体来说,排队论的思想和方法仍然是适用的。
您可以订阅此RSS以获取更多信息