小公司大多死在优先级不清晰

d2b5ca33bd970f64a6301fa75ae2eb22

引言

最近在和朋友在做一些项目的时候,很深刻的感受到了创业公司的一些问题。而其中让我印象最为深刻的是 —— 优先级分不清。

这也引发了我的思考,大公司为什么能成为大公司?小公司为什么总是小公司?是不是有什么是小公司一直没做好的?

思考后我的答案是:小公司往往死于优先级不清晰

作为一个小公司来说,资源不多是正常的,但不是致命的,有多少钱就办多少事就好。但如果你没有钱,却选择做一个不适合自己的事情,或不把有限的资源投放在最重要的事情上,可能最终一定会面临一事无成,最终无法达成自己的预期。

背景信息

我们当时的目标是开发一个项目。一个项目中存在主流程和辅流程。我的观点是优先关注项目的主流程,而不是辅流程。而朋友则会关注一些偏表面的体验、颜色、或一些直线流程的产品功能。这里我们存在了优先级的冲突。当然,最终还是按照我的优先级来做事。

我的观点

在绝大多数的时候,大公司是那个资源更加充沛的角色,这使得大公司可以拥有更多的资源、更多的试错可能机会。对于大公司来说,一个方向的试错,并不会导致大公司彻底死亡。同时,大公司的各种流程的积累,可以帮助大公司尽可能的做出最正确的决策(虽然这个「最」其实是在企业内的最,未必是产品的最)。

但对于小公司来说,由于资源的有限,可能一次失败面临的就是全盘皆输。因此,对于小公司来说,优先级的决策就变得弥足珍贵。对于小公司来说,如果领导者是一个聪明、经验丰富的人,还可以很好的评估项目和工作的优先级,带领大家穿越周期,最终取得预期的结果。但如果领导者不够聪明,或者精力不在做决策上,则大概率走向不好的结果。

分析和评论

不过,这个事情上来讲,也有一点悖论。对于小公司来说,决策会更加重要,但对于小公司来说,招募到一个可用之材也是更困难的事情 —— 因为人才会选择去一些胜率更高的大公司从业,获取更加明确的收益。小公司得到的人才可能往往不是那么的优秀,使的小公司的决策行为更加雪上加霜。

对于小公司来说,需要用更高的赔率去招募合适的人才,才能招到真正优秀的人来一起做事。

结论

大公司因为有标准的流程,即使决策者水平不高,依然可以借助流程提升决策的水平。但对于小公司来说,如果决策者水平不高,大概率会死在路上。好的决策,影响一切企业。


作为一个个人,如果你刚刚走进社会,有机会进入到大公司去感受到大公司的决策流程,那对于你整个从业经验来说都是会有很大帮助的。但如果你没有办法进入到大公司,那么一定要仔细遴选小公司,特别是与小公司的领导者沟通,了解小公司的人领导者是什么样的人,确认小公司的领导者是否是你认可的人。避免浪费自己的时间。

创业公司/小公司可能是每个人必经的道路。但是,我希望你能在这个路上少踩一些坑,走的更稳。

我们是不是在三战的前奏?

grayscale photo of concrete houses

一战的导火索是萨拉热窝事件,斐迪南大公夫妇被枪杀,引发了奥匈帝国向塞尔维亚宣战,成为了第一次世界大战的导火索。

二战的导火索则是德国入侵波兰,引发世界大战。而二战前局部也发生了意大利入侵埃塞俄比亚、西班牙内战、日本侵华战争。

如今,我们面临着俄乌冲突、巴以冲突,感觉仿佛三战的前奏。印巴边境也开火了。

希望我的猜测是错误的。

聊聊 APILetter 的新计划

APILetter

APILetter 从创刊号,到 S1E6,经历了一年的时间。

虽然在定更新节奏时,我就考虑到自己拖更的可能性,但确实没想到我拖更这么严重,在 2022 年,一口气更新了 3 篇,然后就是长达半年的拖更。不过,总算是把第六篇写完,算是给 Season 1 做个了结。

过去

APILetter 的出现,是源自我在研究 RESTFul 架构时发现的问题:国内有太多解释什么是 RESTFul 规范的文章,但你点进去看,篇篇都是复制粘贴。

而 API 是开发者生态中非常重要的一环,它不应该被草率的对待,开发者们值得用上更好的 API。既然没有人写关于 API 的严肃内容,那就从我开始吧。刚好我在研究相关的内容,那就写一些 API 到底应该是什么样的。

也正是抱着这个想法,我开始了一篇篇的创作,从 为什么是 RESTful API,到如何设计一个符合 RESTFul 风格规范的批量操作 OpenAPI;有务实的内容,也有务虚的内容。但不变的是希望让大家明白如何设计一个更好的面向开发者的 API。

但,Season 1 的内容也是杂乱无章的,聊过 RESTFul、聊过 API 文档、聊过 API 指标、聊过 API 报错,这样杂乱且没有主线的内容,作为博客来说,还算可以接受,但作为 Newsletter,可能就显得过于随意。因此,在 Season 2 开始,内容也会开始面向主题,我也会更早的设计不同的 Season 要讨论的话题,以便于让你可以有更好的阅读体验。

未来

在写第一篇邮件通讯时,我就注册了域名 APILetter.com,因为我知道这件事我应该会做很久,所以搞一个独立的站点是必然的事情,也是符合我习惯的事情 —— 每搞一个事情,就要给它一个独立的品牌。

而在刚开始写第一封邮件时,我是没有勇气使用独立的站点的,我总担心自己写完第一封就写不下去了。那搞一个独立的站点不过是浪费时间。所以我选择从竹白开始我的写作之旅。一年过去了,获得了还算不错的效果(至少比我想象中的要好一些)。

image
迁移到 Ghost 之前的数据

不论过程是否艰辛,总归我是完成了自己对自己的承诺,至少写完了一个 Season 的内容。

而 APILetter 完成了第一阶段的产出后,下一个阶段,我希望用更加品牌化的方式来运行这个项目,便启用了 APILetter.com 的域名),并搭建了 Ghost 来托管这个项目。

换到 Ghost,除了域名独立、程序独立、数据独立,相应的,自然也有一些好处 —— 比如可以 RSS 订阅了。如果你希望通过 RSS 的方式来订阅 APIletter, 也可以直接访问 https://www.apiletter.com/rss/ 来订阅。

Season 2:指标

在 Season 1 中,我花费了不少的篇幅来讲 OpenAPI 的设计问题。在 Season 2 ,我想专注于 OpenAPI、开发者体验中的指标问题,从企业和个人制定目标开始,到具体到某一个具体的 OpenAPI 指标评定。

目前预计会包含的内容:API 自身的指标定义、API 相关业务的指标定义、开发者体验中的一些指标定义。

除了这些指标,你还关注哪些指标?欢迎回复邮件告诉我。

总结

总之,APILetter 在新的一季里,我会尽量以更好的内容组织方式、更高的频率(但暂时承诺还是每月一封哈),来给大家分享我自己关于 API、关于开发者体验,以及一切与开发者有关的内容,希望可以帮到你。

如果你身边有对于开发者关系感兴趣或从事相关内容的朋友,欢迎你将 APILetter 介绍给他,帮助我获得更多的读者,以及,持续的写下去🥰。

为什么 OpenAPI 的设计如此重要?

APILetter

在 APILetter 的 S1E6,我想和你聊聊 OpenAPI 设计的重要性。

在整个 S1 的文章中,我用了接近 4 篇的篇幅来介绍 OpenAPI 的设计,从一开始介绍为什么要使用 RESTFul ,到 API 的错误码设计理念,辅以批量接口设计的实例,再加上最后这篇重要性的强调,三分之二的比重,意在让你深刻认识到,OpenAPI 的设计至关重要。

为什么 OpenAPI 的设计如此重要?

在企业内部工作时,常常需要找到平衡质量和速度之间的折衷方案。当项目时间非常紧迫时,往往会牺牲一些质量。但如果项目有足够的时间,就有更高的概率能够设计出一个质量更好、开发者体验更佳的 OpenAPI。

然而,OpenAPI 是一项非常重要的任务,不能马马虎虎。一个坏的设计会让团队持续在 OpenAPI 开发上投入更多的精力。

沟通难度高带来的维护成本增高问题

和企业团队内部使用的 API 不同,OpenAPI 的用户是内部团队,用户的差异决定了沟通的难度的。

在内部API中,若需要对某个API进行不兼容变更,我们可以通过比较简单的沟通完成变更的通知,并通过企业内部的优先级对齐方式来明确变更的时间和行为。

但对于OpenAPI的用户来说,不兼容的变更意味着需要通知所有的外部开发者,并确保他们完成相应的更新。然而,这个时间节点和周期并不容易控制,跨企业的优先级对齐、时间节点对齐、以及出现问题时的技术支持,都可能会导致OpenAPI变更周期变得非常长,让维护人员感到疲惫不堪。

以平稳迁移为例,通常需要1-2年的时间才能下线OpenAPI。如果没有平缓过渡,那基本上就无法下线API了。

无法下线的 OpenAPI 最终将会指向不断增加的维护成本。毕竟,即使我们可以不在旧版的 OpenAPI 中去提供新的 Feature ,但依然要针对旧版本的 OpenAPI 提供安全更新,这会导致团队的研发成本越来越高。

风格/设计不统一带来的开发者评价问题

OpenAPI并不一定非要选择 RESTFul 风格,但一定要统一设计风格。

好的设计风格一方面是开发者对于美和好的追求,另一方面也会是开发者对于你的能力和团队水平的评估纬度。开发者可以通过对于接口风格、文档质量等多个角度的评估来评价你的产品的质量和结果。

虽然开发者并非企业的绝对决策者,但对于那些高度依赖开放性和集成性的业务而言,开发者的评价尤为重要。出色的开发者评价,将会使企业在进行集成相关的决策时,更加放心地进行选择。

当然,风格/设计不统一带来的也不仅仅是开发者评价问题,还涉及到更多的维护成本问题:

  • 因为风格不统一,导致开发者对于不同的接口没有一个明确的范式,需要理解成本带来的学习成本高的问题。
  • 因为风格不统一,导致开发者需要针对不同的接口做不同的适配层,重复建设。

美与好不是一个 OpenAPI 在发布时必须要追求的,但又是你大规模获客时一个重要的对比项目。所以,在 OpenAPI 的设计早期,定义接口设计规范是一个值得投入时间和精力去思考的事情。

如果我的设计已经不好了,又该如何处理?

绝大多数人没有机会从头设计一个全新的 OpenAPI 系统,大多是需要一边跑一边换轮子的。

在这种情况下,可以考虑为你的 OpenAPI 设计一个明确的下线策略,以及,做好你需要花费一年的时间来做好这件事的准备。先定下统一的 OpenAPI 规范,并通过大版本迭代的方式,将旧版中的设计不好的 OpenAPI 重新梳理、设计、整合,并开放出新版的 OpenAPI 出去。

再将存量中设计不好的 OpenAPI 标记为 deprecated,引导增量开发者升级使用新版的 API,卡住存量的 API 的新增使用用户。

随后,只需要不断在新版的 API 中提供新的 Feature,使用自然的产品策略方式诱导开发者来升级,从而实现存量 API 的自然退役即可。

总结

和企业内部使用的 API 不同,OpenAPI 的开放属性决定了 OpenAPI 是很难退役的,因此,从一开始就朝着最好的方式来演进,可能才是最正确的做法。快速迭代,快速试错的路子,并不适合 OpenAPI 的产品形态。

《不租房的606天》书摘

d2b5ca33bd970f64a6301fa75ae2eb22 1
  • 或许,真正的交流障碍不是语言,而是内心的自我设防。不能在被别人否定之前,先否定了自己
  • 工作最认真的人不是因为他们自律,而是因为他们在解决切实的问题,并且解决问题的过程让他们每天精神振奋
  • 德鲁给毕业生的人生信条是“网球”“圆圈”和“三万天”。“网球”代表我们所热衷的事情,把网球扔出去,小狗会追着网球疯狂奔跑,意思是人应该追求自己热爱的事业并为之奋斗。“圆圈”的意思是,每个人都会被身边接触最密切的五个人所影响,这五个人就是你的圈子,因此要多和给你启发的人相处。而人生只有“三万天”,我们永远没有准备好的那一天,所以想到什么就去做。
  • 我的手账里有一句留言:“最好的作品是你度过的时光。
  • 经历过富足的生活,他说:“丰富的精神世界比物质更重要。”
  • 对于我下一步是留在硅谷还是从事自由职业的困惑,他说:“辰雨,想让梦想照进现实,第一步是想清楚自己要什么;第二步是精练地表述需求,并努力争取。你有太多的恐惧,成功过一次,你就会越来越有自信。”
  • 多诺万对我说:“辰雨,勇气就是在恐惧中有所为。”
  • 离开后,我心中一直默念着他送我的三句话: 1.放下心中对拒绝的恐惧。 (Let go the fear of rejection.) 2.只要你成功一次,后面就会越来越有自信。 (Confidence is built on top of success.) 3.知道你想要什么,并主动争取。 (Know what you want and ask for it.) 火人节结束后,我收到一封邮件,落款是“你的新朋友多诺万”。
  • 早餐已经不再是一顿饭,而是打开话匣子、化解陌生感的方法,同时自带“刚刚好”的界限感。早餐不像晚餐过于隆重,时长恰到好处,房客既容易参与其中,又不会越界。
  • 25年飞龄的机长,三生三世的人生智慧
  • 我问他:“你会用一个什么词来定义自己?”他不假思索地回答:“梦想家。”
  • 对于大部分人来说,年纪越大,会越渴求陪伴。而丰富的人生经历,练就了卡尔一颗坚定的内心,屏蔽了外界的杂音,在精神世界里,从容安宁。
  • 拉斯维加斯不止有赌场,金发女郎也不都是花瓶
  • 如果不是因为工作,我很难找到一个再来这里的理由,但我想起导师的一句话:“若你感到不舒服,意味着你会学到更多。若你感到害怕,那正是机会降临的时刻。”
  • 离开拉斯维加斯时,我不再认为它是一座只充斥着堕落和罪恶的迷醉之城。撕掉游客给它的标签,这座城市向我展露出它质朴和生活化的一面。或许多一点时间了解,每个人都和这座城市一样存在着有趣的另一面,只有抛开成见,才有机会发现他们的特别之处。
  • underline
  • 工作只是定义人的一个维度,可能限制了我们看待陌生人的角度。克里斯塔也许平凡,却有超出常人的精神追求——人到中年,她挑战一成不变的工作,选择帮助犯过错的人改过自新,她的善良改变了我对罪恶之城的理解。她有经济压力大的烦恼,但也有对生活品质的追求,还十分热情好客。虽然我和克里斯塔的外表和职业都完全不同,但当我们敞开心扉沟通时,就会发现彼此的默契远大于差异。其实,每个人心里都藏着一座花园,等待你去探索。
  • 82岁的“文艺复兴”女画家,每天都是工作日
  • 身为美术系教授的托比,做事十分严谨,她还出过一本书——《画家生存指南》(The Artists’ Survival Manual:A Complete Guide to Marketing Your Work)。如果我忘记在画作上标注日期和名字,她会提醒我:“你又忘记写版权信息了。”几分钟后,她发来一封邮件,附件名为“画家如何管理知识产权”,内容来自书中的一个章节。
  • 跟我去趟火人节吧
  • 到达黑石城的时候,参与者会拿到一本《火人节手册》,上面写着:作为市民,在这座城里,你可以什么都没有,但是你需要遵守10条原则。 绝对包容(Radical Inclusion) 无条件给予(Gifting) 去商品化(Decommodification) 自力更生(Radical Self-reliance) 自我表达(Radical Self-expression) 有社区精神(Communal Effort) 承担公民责任(Civic Responsibility) 不留痕迹(Leaving No Trace) 积极参与(Participation) 活在当下(Immediacy) 正如伊莎在电话里叮嘱我的,物质准备之外,更重要的是调整好心态,要时刻记住“分享、感恩与自我表达”
  • 火人节令我豁然开朗,让很多不可能变为可能,正如我在一个艺术装置上看到的一句话:“向内看,可以找到一切答案。”(Everything you need is within you.)
  • 我想起《小王子》中狐狸说过的话:“仪式是经常被遗忘的事情,它使某个日子区别于其他日子,使某一时刻不同于其他时刻。”
  • 生活原本平淡无奇,是人赋予了它特定的意义。犹太人的晚宴让我想起火人节上绘有一双眼睛的艺术装置,上面写着:“我们也许不能改变这个世界,却可以改变看这个世界的眼睛。”
  • 或许这就是村上春树说的,“相逢的人会再相逢”。
  • 生活中我们会遇到很多人,一面之缘便成了微信联系人。或许在未来的某一天,当你遇到困难,他们也会伸手拉你一把,却不求回报,就像下雨天递上的一把伞,酷暑天送来的一瓶水。朋友圈的每一次举手之劳,于我都是雪中送炭,温暖并激励着我。
  • 利用工作间隙说走就走,不是为了潇洒,而是来自对生活的热忱和一点“贪心”。
  • 相比于结果导向的“事业”,365天住民宿是我了解世界的方式——每间民宿的个性和小瑕疵都在讲述着每扇门背后的故事。在墨西哥画家家里“吃的苦”看似是“代价”,但更是一分收获。比起在美术馆里看射灯下的画,我更喜欢被画家的作品包围,直观地感受画家的生活状态。
  • 很多次,我盯着心仪的民宿看了很久,鼠标停留在预订页面,却迟迟按不下“预订”按钮,只能默默地把房源存到心愿单。我常想:“如果金钱不是这个项目的阻力就好了。”每到此时,我会意识到,只有更加努力地工作,才能坚持过“既可朝九晚五,又能浪迹天涯”的生活。
  • 学会提问是另一种互动,遇到不懂的话题,不要乱说,可以虚心提问,让对方多讲,做个好的倾听者。 保持开放的心态,尝试并接受新鲜事物,不害怕碰壁,会收获愉快的沟通体验。
  • 美国抽象表现主义艺术大师汉斯·霍夫曼说过:“做减法的能力意味着消除不必要的事,让必要的事发声。”我的理解是做减法的过程,其实是做加法。减去不必要的事,而把时间精力分配给像提升个人修养、丰富人生体验这样真正重要的事。简单的物质生活给了我更多时间去思考、学习和倾听,或许这是我从极简旅行中获得的智慧。

如何更好的运转一个开源项目?Community Leadership Workshop 小记

d2b5ca33bd970f64a6301fa75ae2eb22 2

2023 年,在 DevRel 领域值得我高兴的事情有三:

其一,是今年继续召开的 Dev.Together,又一次和国内从事 DevRel 的小伙伴们一起交流经验,看看大家的生存情况如何,都在做什么事情。

其二,是好友 Richard 翻译的新书:《开发者关系:方法与实践》的出版。作为一个 DevRel 的从业者,开发者的身份能够让我深刻的感知到开发者的痛苦,从而帮助开发者解决问题。但我没有系统的思想,来指导我更加高效的解决问题。

其三,是 Community Leadership Workshop 的召开,可以让我学习到一些过去我不曾思考,或不曾注意到的开源社区和开发者社区问题,帮助我补全自己认知中的空白,更好的服务于开发者。

ylqcg5

国内的 DevRel 的从业者们没有太多的资料和经验可以参考,全靠摸索,因此,我也希望通过这个小记,帮你可以看到关于 DevRel 、关于开源社区的一些现状,帮你更好的处理自己的工作。

Community Leadership Workshop 简介

在进行下面具体的内容之前, 先简要介绍一下 Community Leadership Workshop ,这个是在今年的 Apache Con Asia 的会前会,由 @姜宁 组织,@Tison 和 @Richard 参与建设的小规模研讨会,主要介绍他们在企业中从事开源工作和开发者工作的经验,帮助大家更好的理解开源、开发者工作。

从主题上来讲,@姜宁 介绍的主要是企业为什么要做开源;而 Tison 则介绍了他自己在运营一些开源社区过程中的最佳实践(这部分我非常有收获),以及 @Richard 介绍的 DevRel 从业者面临的一些问题。

下面的一些问题,也会围绕着这些主题来介绍,大家可以猜猜哪些内容都是谁讲的。我从 8 小时的分享中,提供出来几个我最有收益的问题,与诸君分享。

从一个问题开始:OpenAI 的 ChatGPT 爆火,应该开源么?

这个问题直击企业开源的根本:为什么要开源?

作为一个开源人,我的下意识觉得 ChatGPT 应该开源,这样可以让他运行的更好,但理智告诉我,OpenAI 不会将 ChatGPT 开源。即使 OpenAI 已经开源了一些能力(比如 Whisper),但对于 OpenAI 来说,目前 ChatGPT 和背后的大模型,依然是其企业营收的重要组成部分。

在大模型是其核心护城墙的时候,他不会选择将其开源,就如同我们看到众多的开源企业,其核心护城河并不是其开源的产物,围绕开源产物的服务才是其真正提供的价值。

而在这部分 @姜宁 也给出了答案:

绝大多数时候,开源的往往不是行业龙头老大,而是行业的第二名,通过开源来实现差异化的竞争:「我可能不是最好的,但我是最好的开源平替,你可以低成本的将其用起来」。就如同 Meta 开源了 LLAMA,通过 LLAMA 来和 OpenAI 竞争。

你的企业到底在开源生态中的生态位是什么?

不同的企业在开源生态当中有不同的生态位,有的企业是开源项目的发起方,比如 Meta 之于 LLAMA,比如 OpenAI 之于 Whipser。有的企业则是开源项目的贡献方,比如 Red Hat 之于 Linux Kernel。

对于项目的发起方来说,他们便是整个开源项目的上游,他们通过开放源码,来在市场中占据自己的一席之地,通过设定不同的 Paywall ,来赚取收益。或者是通过提供官方的 SaaS 服务之类的,来帮助客户解决问题。

而对于项目的贡献方,则是开源生态中的下游,下游的服务一开始可能是基于上游的版本来提供服务,但随着提供的服务不同,下游的企业则需要逐步向上游提供贡献,将自己的代码贡献给上游,以避免自己维护太多的支线版本,造成比较大的维护成本。下游的企业也可以通过逐步贡献,成为一个项目的核心团队,影响项目的发展。

如果你的企业围绕着一个开源项目做事,可以好好想想自己的企业的生态位到底是什么?自己如何为项目贡献、自己如何获取收益?

开源项目的 Default 配置是什么?

我自己也是一些开源项目的维护者,也会试图去围绕开源项目去做一些事情。而在这个过程中,到底哪些是应该做的,应该如何推进,这些问题其实一直我都没有特别成型的理论框架,而这次的 Workshop,在这个问题上给我了很多建议和总结,帮助我更好的去推广项目。

  1. Home Page / 主页是最重要的一件事

很多开发者在开发项目的时候,代码放在 Github 上,就结束了自己的开源工作。殊不知,这只能算是你的代码公开可获得(Source Avaliable),如果你没配置 LICENSE,那就不算开源。

而配置了 LICENSE,也仅仅是解决了你的项目的合规问题。如果你有更高的预期,则需要做更多的工作。这里最重要的便是一个项目首页。项目首页决定了开发者如何找到你的软件和内容。开发者们会通过你的首页来了解最新的更新。

如果你的项目没有首页,那就做一个简单的 Landing Page ,并放在 Github Pages 中,作为你的项目的开始!

  1. 构建社区,而不是拉群

国内因为「私域流量」的存在,大家搞运营喜欢先弄一个微信群,但微信群的问题在于信噪比太低,里面大量重复和无意义的内容。相比之下,建设一个简单的论坛可能是更好的选择。

通过在社区中的引导,你可以让开发者们可以自助交流起来,并尽可能的复用内容,减少内容的重复建设问题。

如果你没有时间和人力去建设一个论坛,那就用 Github 自带的 Discussion 来运行一个论坛吧~

  1. 区分不同类型的开发者

在我之前看来,开发者就是开发者,是不区分类型的。但 Tison 提到,其实开发者也是分类的,有的开发者是核心开发者,有的是集成开发者,有的是应用开发者。

核心开发者往往是因为被你的开源软件的理念所引导而来的,只要软件本身的理念不发生变化,开发者们就会继续使用。

集成开发者则是在不同的系统之间构建集成,他们关注的是你的系统本身的易用性和稳定性,他们往往带着不同的目的来到你的软件当中,你需要通过 Blog、 教程、Workshop 之类的工具,帮助他快速完成集成,从而完成他的业务目标。集成开发者的开发完成后,可以帮助你的业务快速扩张,其集成的特性决定了做的是水管的工作,水管一旦建立,便源源不断的有用户进来。

应用开发者的类型和集成开发者类型差不多,不同的是应用开发者不是构建和现有系统的水管,而是挖出一个新的水池,成本和投入更大,但也能建造出更适合你的业务的水池。也是一个不错的选择。

区分这三种不同类型的开发者,对征下药,可以帮助你有效的完成开发者社区和关系的建立,从而让你事半功倍!

总结

Community Leadership Workshop 的内容实在太多了,对于我来说,每一点都值得细细的分析、拆解和理解,但毕竟文章总是要有尽头的,所以这一篇就先从这里结束啦,后续的内容,且等我拆解成一篇一篇的细节,分享给大家~

使用 Ruby 替代 Node.js 写一些脚本

ruby

根据不同的场景,我会使用不同语言来完成功能的编写。

对于一次性、低频、对于性能要求不高的批处理场景,过去我喜欢使用 Node.js 配合 NPM 来完成。

主要的原因是:

  1. Node.js 拥有丰富的包的生态,可以让我少写很多代码。
  2. npm run 命令比较短,可以方便的构建出需要的快速参数

而最近 Node.js 脚本写的太多,比较烦了,所以考虑用 Ruby 来替代 Node.js 写一些脚本,完成一些短期项目开发。

和 Node.js 相比,Ruby 有其好处,也有其坏处。好处在于

  1. 原生同步执行,我可以不用担心和关注 Callback Hell。虽然有了 async/await 时,已经好很多了。但还是原生的更好。
  2. 可以用更加简单的语法完成脚本。毕竟脚本主要还是随时修改随时可用,简短但能用的脚本可以提升脚本的可维护性。
项目Node.jsRuby
包管理器NPMGems
执行命令npm run xxx借助 Makefile 完成
第三方包的数量
异步/同步默认异步默认同步

接下来一段时间,就拿 Ruby 来跑脚本啦!

憨夺型投资者

d2b5ca33bd970f64a6301fa75ae2eb22 27

上周看了《憨夺型投资者》,其中印象最为深刻的是一句话

情况好,赢得多;情况不好,输得少

对于我们绝大多数普通人来说,这个可能是最有价值的投资建议。当然, 也是最难达成的投资建议之一。

赚大钱可遇不可求,但亏小钱确实是有可能做到的,比如:

  • 给自己的投资留足安全边际。
  • 在做事时,关注你自己的利基市场 Niche。
  • 在行业低谷期买入,等行业兴盛起来时赚钱。

用书中的原话是这样的:

这就是憨夺型投资模式的框架,具体如下:

  • 投资现有的业务
  • 收购变化缓慢的行业中经营模式简单的企业
  • 在不景气的行业里对经营不善的企业进行抄底投资
  • 投资具有持久竞争优势的行业
  • 看准有利时机投大注
  • 注重套利
  • 买入以远低于内在价值折价出售的企业
  • 寻找风险低、不确定性高的业务
  • 模仿好过创新

以及,在这本书中了解到了凯利公式,有空要好好研究一下。

在我看来,在职做独立开发者也算是憨夺型投资者的一种方式,其中的成本是你自己的时间成本,但因为你有 Day Job,并不是全职投入,所以即使失败,也不至于输的很惨。你的优势,则是你的成本足够低。

插件多次加载导致的 WordPres 后台加载缓慢

turned-on monitor

WordPress Jetpack 的一个陈年 Bug – 在 wp-Options 中生成大量的数据 中,我提到,问题的根源并不是数据库,虽然在数据库中产生了大量的数据,但并没有真正意义上拖慢系统的进程, 那到底是什么拖慢了进程?

1fp2qm

随着对系统的深入排查,我发现一个异常的事情,在进入管理后台时,出现了插件更新插件列表的情况。这个并不多见。因为管理后台的进入不应该涉及到对于插件列表的更新。此外,我发现这个查询的 SQL 巨长,且包含了大量的查询。

d2b5ca33bd970f64a6301fa75ae2eb22 32

于是评估,这个可能才是导致系统缓慢的真正原因。熟悉 WordPress 的读者一定知道, WordPress 对于插件的加载,是通过在数据库中存储了一组插件路径,每次启动时通过这组插件路径来加载插件。这样对于 WordPress 来说,可以快速加载插件。

但在这次的异常场景中,同一个内容的插件被加载了多次,就算这个插件比较小,可能依然会导致计算时间。此外,WordPress 引入插件后,会加载对应的 Hook 、 Filter 和 Action,则可能导致同一个 Action 被频繁触发,造成额外的计算量。从而使得虽然数据库查询时间不长,但整体耗时却巨长无比。

而这个问题的处理倒是也比较简单:

  1. 对 active_plugins 进行备份,避免修改跪了。
  2. 对于 active_plugins 进行清空,此时 WordPress 会彻底不加载任何插件。
  3. 仿照之前的列表,启用所有的插件。

通过对于active_plugins的清理,将启动速度成功降低至 2 秒左右,将系统性能提升了 80 %。

d2b5ca33bd970f64a6301fa75ae2eb22 36

WordPress Jetpack 的一个陈年 Bug – 在 wp-Options 中生成大量的数据

silver mercedes benz emblem on blue surface

最近在处理一个 WordPress 系统访问下降的问题时,发现了一个奇怪的现象:一个只有很少的页面的网站,数据库备份竟然足足有 9.5 GB。我当时的第一反应是:数据库性能极差导致的站点性能不好。

d2b5ca33bd970f64a6301fa75ae2eb22 29

不过,到数据库打开后发现, 虽然有大量的条目生成,但因为no autoload,所以其实并不会被自动加载到缓存中,从而也不会让网站的性能有太多的下降。

d2b5ca33bd970f64a6301fa75ae2eb22 31

想想也合理,数据库中包含了数十万条记录,如果都加载到内存里,可能 PHP 默认的 1024MB 的运行内存直接被打爆了,所以问题不在此。

不过,虽然问题的核心不是它,但如此海量的脏数据对于系统依然是无价值和无意义的,于是乎我便将这些脏数据删除,数据库的大小从 9.52 GB 骤降至 34.8 MB,进入到一个正常的数据库大小区间了。

删除脏数据的命令如下:

DELETE FROM wp_options WHERE option_name LIKE '%jpsq_sync-%'
Code language: JavaScript (javascript)

相关链接

  • https://wordpress.org/support/topic/wordpress-database-error-commands-out-of-syn/
  • https://wordpress.org/support/topic/jpsq_sync-table-constantly-generated-to-the-db/
  • https://gist.github.com/bhubbard/894040fec6421f891f1f88f2c6428ef0

《大教堂与集市》书摘

d2b5ca33bd970f64a6301fa75ae2eb22 28
  • 他教导我:要尊重能力,要珍视和捍卫自由,特别是:昆虫才讲究技能专一。
  • 要想对世界做出实质性的改变,开源需要做到这两点:一是要让人们广泛使用开源软件;二是要让用户知道并理解这种软件开发模式能给他们带来的益处。
  • 现在第一版已经停印了,其修订版和增补版是《新黑客字典》(The New Hacker’s Dictionary),由MIT出版社于1996年出版(3rd edition,ISBN 0-262-68092-0)。
  • 1.好的软件作品,往往源自于开发者的个人需要。
  • 优秀的程序员知道写什么,卓越的程序员知道改写(和重用)什么。
  • 卓越程序员们有个很重要的特征是“建设性懒惰”,他们知道人们要的是结果而不是勤奋,而从一个部分可行的方案开始,明显要比从零开始容易得多。
  • 3.“计划好扔掉一个吧,迟早你会这么做的。”(Fred Brooks,《人月神话》第11章)
  • 或者可以这么说:在你第一次把问题解决的时候,你往往并不了解这个问题,第二次你才可能知道怎么把事情做好。所以,如果你想做对事情,至少要再做一次。 1
  • 4.如果你有正确的态度,有趣的事情自然会找到你。
  • 当你对一个程序不再感兴趣时,你最后的责任就是把它交给一个可以胜任的接棒者。
  • 6.把你的用户当成开发合作者对待,如果想让代码质量快速提升并有效排错,这是最省心的途径。
  • 不只是Emacs,还有其他一些软件产品也使用了两层架构和两级用户群,内核使用大教堂模式开发,工具箱(toolbox)使用集市模式开发,比如数据分析和可视化展现的商业化工具MATLAB就是这样,MATLAB和其他类似产品的用户们发现,创新、酝酿和行动最频繁发生的地方总是在产品的开放部分,而这部分的改进也总是由庞大而多样化的用户群完成。
  • 尽早和尽量频繁发布是Linux开发模式中至关重要的一部分,绝大多数开发者(包括我)都习惯性地认为:除非是很小的项目,这么做有害无益,因为软件的早期版本几乎都是问题版本(buggy version),如果早早发布,恐怕会耗尽用户们的耐心。
  • Linus的直接目标就是将投入排错和开发的“人时”(person-hour)最大化,即便这样做可能导致代码不稳定,或者可能因为一些难以消除的严重bug导致用户群流失,Linus也在所不惜,他相信:
  • 大教堂建筑者看来,bug是棘手的、难以发现的、隐藏在深处的,要经过几个人数月的全心投入和仔细检查,才能有点信心说已经剔除了所有错误。而发布间隔越长,倘若等待已久的发布版本并不完美,人们的失望就越发不可避免。
  • 这里隐含的问题是开发者和测试者对程序有着不匹配的思维模式,测试者是从外往内看,程序员是从内往外看。对于不开放源码的软件开发,开发者与测试者往往局限于自己的角色,各说各话,都对对方倍感沮丧。
  • 传统软件开发在组织结构上的根本问题由Brooks定律一语道破:“在一个已经延期的项目上增加人手,只会让项目更加延期。”
  • Brooks定律指出,随着开发人员数目的增长,项目复杂度和沟通成本按照人数的平方增加,而工作成果只会呈线性增长。
  • 聪明的数据结构配上愚笨的代码,远比反过来要好得多。
  • Brooks在《人月神话》的第9章里说:“让我看你的流程图但不让我看表,我会仍然搞不明白。给我看你的表,一般我就不再需要你的流程图了,表能让人一目了然。”历经30年的术语/文化变迁,这个道理依旧没变。
  • 先说说前面我提到的用这个项目验证我所发现的关于Linus成功的理论,(你可能会问)我是如何做的呢?有这么几个办法: ·我尽早发布并频繁发布(几乎从来没有低于10天一次的频率,在高强度开发阶段会一天一次)。 ·我把每一个因fetchmail联系我的人都加到beta列表(是指beta测试人员邮件列表——译者注)中。 ·每次发布新版本时,我都向beta列表发送朋友对话般的通知,鼓励他们参与。 ·我听取beta测试者们的意见,征求他们关于设计决策的看法,当他们发来补丁和反馈时给他们以热情回应。 这些简单措施立刻收到了回报。
  • 如果你把beta测试者当做最珍贵的资源对待,他们就会成为你最珍贵的资源。
  • 11.仅次于拥有好主意的是,识别来自用户的好主意,有时后者会更好。
  • 很有趣的是,如果你发自内心地谦逊,并承认你欠别人很多,你将很快发现世界会这样对待你:他们认为是你发明了整个软件,而且你对自己的天赋有着得体的谦虚。我们可以看到这一点在Linus身上体现得有多好!
  • 通常,那些最有突破性和最有创新力的解决方案来自于你认识到你对问题的基本观念是错的。
  • 设计上的完美不是没有东西可以再加,而是没有东西可以再减。”
  • 当你的代码变得既好又简单,你就知道你做对了,
  • 如果你采用快速迭代开发模式,开发和改进过程就可能成为排错过程的一个特例——修复软件原先在功能或概念上的“疏漏型bug”(bug of omission)。
  • 15.写网关类软件时,尽可能不要干扰数据流,而且绝不要扔掉信息,除非接收方强迫你这么做。
  • 我并不是因此而不喜欢英语语法,相反,提及它正是为了打破传统观念。有了更便宜的计算资源,简洁就不该成为最终目标。对现如今的计算机语言来说,是否便于人类使用要比是否节省计算资源更重要。
  • 当你的语言还远不是图灵完备(Turing-complete)的时候,语法糖[4]会让你受益良多。
  • 17.系统的安全性只取决于它所拥有的秘密。谨防虚假的秘密。
  • 当开始建设社区的时候,你需要拿出一个像样的承诺。程序此时并不需要特别好,它可以简陋、有错、不完整,文档可以少得可怜。但它至少要做到:(a)能运行,(b)让潜在的合作开发者相信,这个软件在可预见的未来,能演变成一个非常棒的东西。
  • 在软件设计上表现得聪明而有原创性,容易养成一个习惯——在应该保持软件健壮性和简单性的时候,你往往下意识把它弄得既华丽又复杂。
  • 此言不虚:最好的程序一开始只是作者对自己每天遭遇问题的个人解决方案,程序流传开来则是因为作者遇到的问题成了一大类用户的典型问题。
  • 想要解决一个有趣的问题,先去找一个让你感兴趣的问题。
  • 一个在封闭项目中只靠自己的开发者,将远远落后于这种开发者:他们知道如何创建一个开放的、有改进能力的环境,在这个环境中,上百人(甚至上千人)反馈并提供设计空间拓展、代码贡献、bug定位以及软件的其他改进。
  • “齐心协力”正是Linux这种项目所需要的——对Internet上(可以看成是无政府主义者的天堂)的志愿者们使用“命令原则”是根本行不通的。
  • Linux黑客们致力于最大化的“效用函数”,其目的并不是经典意义上的经济价值,而是自我满足和黑客声望这些无形的东西。(有人把这种动机称为“利他”,但他们忽视了一个事实,即“利他”本身是“利他者”自我满足的外在表现。)
  • fetchmail和Linux核心项目都表明,如果对参与者的“自我”做适当奖赏,一个优秀的开发者或协调者可以利用Internet获取多开发者的好处,而不会让项目陷入混乱不堪。
  • 19.如果开发协调者有一个至少像Internet这样好的沟通媒介,并且知道如何不靠强制来领导,那么多人合作必然强于单兵作战。
  • 未来软件产业的经济关键是服务价值。
  • 一些人认为购买传统模式产品会带来这样的保障:如果项目出错,有人会负责,并为可能的损失买单。
  • 即便很常见,也不要因为可以起诉某人就觉得心安,你想要的不是官司,而是能用的软件。
  • 为弄明白这点,我们需要了解软件开发管理者是如何看待自己工作的,我有位朋友看上去在这方面做得很好,她说软件管理有五个功能: ·明确目标并让大家朝同一个方向努力。 ·监督并确保关键细节不被遗漏。 ·激励人们去做那些乏味但必要的“体力活”。 ·组织人员部署并获得最佳生产力。 ·调配项目所需的资源。 显然所有这些目标都是有价值的,但在开源模式及其所在的社会语境中,人们会惊奇地发现这些目标毫无意义,我们按颠倒过来的顺序分析。
  • 如果传统、闭源、严格管理模式的软件开发真的想靠这种由“无聊”部分组成的马其诺防线来防御,那么它之所以在某个应用领域能继续生存下去,只是因为还没人发现这些问题是真正有趣的,并且还没人发现迂回包抄的路径。一旦有开源力量介入这些领域,用户就会发现终于有人是因为问题自身的魅力而去解决它的,就像其他所有需要创造力的工作,若论激励效果,问题自身的魅力比单纯的金钱要有效得多。
  • 软件工程中最广为人知的一条大众定理是:传统软件项目中的60%到70%,要么是从未被完成,要么被他们的用户拒绝。如果这个比例还算靠谱的话(我还没见过任何一个有经验的项目管理者对此提出过异议),那么大多数项目把目标设定得要么太不现实,要么完全错误。
  • 如果你在工作过程中感到恐惧和厌恶(即便你以自嘲的形式来表达——比如悬挂呆伯特玩偶),就应该意识到过程已经出了问题。快乐、幽默和玩兴是真正的资产,前面我之所以写“快乐部落”(happy horde)并不是为了首字母押韵,而用一只憨态可掬的企鹅作为Linux吉祥物也绝不仅仅是为了搞笑。
  • 极度热忱的人可能会说:“自由软件是我的生命!我活着就是为了创造有用的、优美的程序和信息资源,并把它们贡献给社会。”中热忱度的人可能会说:“开源是件好事,我愿意花大量时间帮助它成功。”低热忱度的人则可能说:“开源有时候还不错,我也玩这个,我尊敬那些创造它的人。” 差异还体现在敌对性上:反对商业软件,以及反对那些试图支配商业软件市场的公司。
  • 他们有效定义了“自由软件”概念,并有意赋予其对抗意味(后来出现的“开放源码”叫法则有意避免这点)。
  • Linux快速成长的额外好处是吸引了一大批新黑客,他们对Linux忠心耿耿,而把FSF计划视为过气的兴趣,尽管这一波黑客将Linux系统称为“GNU一代的选择”,他们中的大多数更愿意效仿Torvalds而不是Stallman。
  • 1997年,“Debian自由软件准则”提炼了这些共同要素,并形成了开放源码定义(OSD,参见http://www.opensource.org)。 定义指出,开源许可证必须保护任何个人或团体无条件修改开源软件(以及发布修改后软件版本)的权利。
  • 所以,OSD(以及与OSD一致的版权声明,如GPL、BSD许可证、Perl的艺术许可证(Artistic License))隐含的规则是“任何人能干任何事”(anyone can hack anything),没有任何事情可以阻止人们获取任意开源产品(如自由软件基金会的gcc编译器)、复制其源码、推进其向不同方向演进,并都可声称是该产品。
  • 这种演进上的分化称为“分支”(fork),分支最重要的特点是它派生出一个随后不能交换代码的竞争项目,并导致开发社区潜在的分裂。(
  • ·分化一个项目会遇到强大的社会压力,只有在极为必要的情况下才使用,而且要重新命名和做出大量的公开解释。
  • ·在没有项目主持人认可的情况下发布更新是令人不悦的,除非是特殊情况(如本质上不重要的移植bug修复)。 ·在项目历史、致谢表或维护列表中移除某个人的名字是绝对不可以的,除非当事人明确表示同意。
  • 一个软件项目的“所有者”就是在社区中众所周知的对软件版本改动有唯一发布权的那个人。
  • 开源活动确实存在一种帮助人们变得更有钱的可能,但也只是对这种可能提供有价值的线索而已。有时,某人在黑客文化中获得的声誉会在真实世界中产生经济意义:比如带来一个更好的工作机会、一份咨询合同或一纸出版协议。
  • 在分析“声誉竞争”前顺便提一下,我并不是要贬低或忽视这种纯粹美学上的满足:设计优美的软件并让它运行。黑客们都经历过这种满足并乐在其中。如果某人没有这种意义上的动力,他根本就不可能成为一名黑客,正如不喜欢音乐的人永远不会成为作曲家一样。
  • 本文第一版在互联网上发布后,一位匿名读者评论道:“别为名声工作,如果你做得好,名声将伴随结果而来”。
  • 我们已经了解心智层开垦的产出,就是黑客礼物文化中的同侪声望以及所有它带来的二次收益和额外作用。
  • 从这个理解出发,我们可以把黑客所沿袭的Lockean财产权习惯看做是一种将声誉激励最大化的手段——确保同侪将名誉赋给应得之人,而不会赋给不该得到的人。
  • ·项目产生分支是不好的,因为分化前的项目贡献者会面临声誉风险,若要控制该风险,他们只能在分化后的两个子项目上同时保持活跃。(通常这是不现实的,因为它让人困惑或难以实施)。
  • ·发布“流氓”补丁(或者更糟糕的“流氓”二进制文件)会让项目所有者陷入声誉风险,即便官方代码是完美的,所有者仍然会因补丁中的bug而被抨击(见书后注释4)。 ·偷偷将某人的名字从项目中移除,是黑客文化中最极端的恶行。这相当窃贼偷盗了受害者赠予的礼物,并说是他自己的。
  • 一位读者曾指出,分支很少能有一个以上的后代存活下来(活下来是指能长期拥有一定的“市场份额”),这促使项目所有参与方合作并避免分化,因为如果产生分化,很难预先知道谁会落败,一旦落败,就只能看着他们曾经大量的工作完全消失或者默默凋零。 他还指出一个不争的事实,即分支很容易产生争论和对抗,这足以引发对项目团队的社会压力。争论和对抗都会妨碍团队合作,而团队合作正是每个贡献者要达到自己目标所必须的。
  • 这揭示了黑客文化一个有趣的方面,它有意识地不信任或者看不起“自我主义”或者基于自我的动机。“自我推销”往往会遭到批判,即便整个社区可能从中获得好处。
  • 相比之下,在黑客社区中,一个人的作品就是他的宣言。这里有着严格的精英意识(技术最好的人胜出),这里的信条是让质量说话,让黑客最自豪的是代码“好使”(just works),是让任何称职程序员都能看到的好东西,所以,黑客文化的知识库增长迅猛。
  • 出于非常类似的原因,抨击作者而非代码是不合常规的,这一点微妙而有趣,黑客们会没有顾忌地在意识形态或个人差异上互相攻击,但从未听说有哪个黑客曾公开攻击另一个人的技术能力(
  • 攻击他人能力的禁忌(学术圈没有这个禁忌)比起自我表现禁忌(这一点学术圈也有)更有揭示意义,因为我们可以将其关联到学术圈与黑客圈在沟通和支撑结构的差异上。
  • 谈吐柔和也是有用的,如果某人希望成为一个成功项目的维护者,他必须让社区信服他良好的判断力,因为维护者的主要工作是判断他人的代码,谁愿意将代码贡献给一个明显不能正确判断他们自己代码质量的人?或者一个试图从项目中沽名钓誉的人?潜在的贡献者希望项目领导人在客观采用他人代码时,能够谦逊而有风度地说:“是的,这个的确比我的代码好,就用这个了”——然后将荣誉给予应得之人。
  • 从全球看来,这两个倾向(“填补空白”和“类别杀手”)是开源项目发展的总体趋势。
  • 在第三个千年的开始,我们大可预言开源会转向最后一块处女地——写给非技术人员的程序。
  • 声誉竞争模型解释了一个常被引用的格言,即“自称是黑客不代表你就是黑客,只有其他黑客认为你是黑客,你才是黑客”
  • 如果它不能像我所预期的那样工作,那就不是好的——不管它多么聪明和有原创性。
  • 这条规则使得开源软件倾向于长期停留在beta版,开发者只有在确信软件不会有很多问题时,才会发布1.0版。开源世界的1.0版意味“开发者愿意拿自己的名誉赌它好使”,而闭源世界的1.0版则意味着“如果你很谨慎,不要用这版”。
  • 在心智层的拓展性工作要比在某功能域内(对现有作品)的重复性工作好。
  • 能进入主要发行版的作品比不能进入的好。在所有主要发行版中都包含的作品最令人尊敬。
  • 使用”是最真实的赞美,类别杀手比同类竞争者好。
  • 如果作品好到没人再想使用其他备选,作者将会获得巨大的威望。那些被最广泛使用的原创型类别杀手,会被纳入所有的主要发行版中,并获得最大可能的同侪尊重。成功做到这点超过一次的人,将会被人们半开玩笑、半认真地称为“大神”(demigods)。
  • 相比那些只挑有趣和简单工作的人,长期致力于艰苦和乏味工作(如调试、写文档)的人更令人钦佩。
  • 重要的功能扩展比低层次的修补好。 这条规则似乎是针对一次性工作的评价。相对于修补bug而言,给软件增加功能特性有可能得到更多回报——除非这个bug异常令人厌恶或者难以寻找,因为将这种bug找出来本身就证明了非凡的技术和才能。但当这些工作是长期行为的话,一个长期关注和排除bug(甚至是普通bug)的人,其地位要高于那些花费相近工作量在增加简单功能上的人。
  • 财产权不仅仅是社会约定或游戏,而是至关重要的防范暴力冲突的进化机制。(
  • 所有权声明(就像领土标记)是一种述行式(performative)行为,一种宣布防御边界的方法。
  • 黑客们常说“责任背后是权力”,一个合作开发者在承担起维护某个子系统的责任后,通常有机会掌控子系统及其对外接口的实现,其决策仅受项目领导人(同时也是架构师)的修正。
  • 这个领域的其他研究者把目光投向了黑客们非常在意的自主性和创意自由度问题,“如果一个人越是感受到自主性受限”,罗切斯特大学心理学副教授Richard Ryan说,“其创造力就会越少。” 对任何一个任务,如果它更像是手段而不是它本身的话,往往会降低人们的积极性。即便是赢取比赛或获得同侪尊敬,如果觉得获取胜利只是为了求得回报,也一样会觉得没意思(这也许可以解释为什么黑客文化禁止那些毫不隐瞒的对尊重的追求和索取)。
  • 最终,当自由市场经济开始创造出足够的财富盈余时,大量程序员可以生活在后稀缺的礼物文化中,而软件产品的工业\工厂模式注定走向衰亡。
  • 事实上,获取最高软件生产力的药方看上去自相矛盾而又颇具禅意:如果你想获得最有效率的产品,你必须放弃促进程序员生产力。做好他们的后勤,让他们自己做主,并忘掉最后期限。
  • 指出技术会变得越来越便宜和有效,早期设计中需要投入的物理资源被越来越多地替换成信息内容。
  • 在尝试使用经济学分析软件产品时,大多数人会想当然地运用“工厂模型”,它建立在如下基本假设之上: ·大多数开发者的薪金由软件销售价值支付。 ·软件销售价值和它的开发投入成比例。 换句话说,人们十分倾向于假设软件具备典型批量商品的价值特点,但这些假设都可以被证伪。
  • 相比之下,当一个软件产品供应商退出市场(或仅仅是产品不再延续)后,消费者愿意为其产品支付的价格很快会降低到零,而不管其理论上的使用价值或该类产品的开发成本如何。(要想检验一下这个论断,可以看看你附近软件商店里的打折专柜)。
  • 换句话说,软件很大程度上是一个服务行业,虽然长期以来都毫无根据地被错认为是制造行业。
  • 这个问题很像是F.A.Hayek所提“计算问题”的翻版——它需要一个超能力(superbeing)存在:既能评估补丁的实用价值,又能可信地设定其价格以促成交易。
  • 开源项目的复杂性和沟通成本基本上完全是参与开发人数的函数,大多数从来不看源码的终端用户实际上并不会带来什么成本,但会增加项目邮件列表上愚蠢问题的比例,好在可以通过维护FAQ(常见问题)列表较为轻松地解决这个问题,而且通常大可不必理会那些明显没有读过FAQ的提问者(这些已经是通行做法)。
  • 也许是,也许不是。真正应该考虑的问题是:你从分散开发负担中获取的益处是否超过了因(“搭便车”行为导致的)竞争加剧而带来的损失,一些人往往在这个权衡中失算:(a)忽略了社区开发带来的功能改进。(b)不把已经支出的开发成本当做沉没成本。根据假设,不管怎样,你都是要付出开发成本的,所以把它归入开放源码(如果你这样认为)的成本是不对的。
  • 另一个经常被提及的担心是,将某些特别的财会功能开源,会不会导致商业机密方案的泄露?其实这不关开源闭源的事,这是糟糕设计带来的问题。财会软件如果编写得当,商业知识是不会在代码中体现的,它应该由一个模型(schema)或描述语言表达,然后由财会引擎执行实现(作为很相近的一个例子,考虑数据模型将业务知识和数据库引擎相分离的做法),这种功能上的分离使你不但可以保护住王冠上的宝石(即你的商业模型),还能从开放引擎中获得最大收益。
  • 使用价值和销售价值之间的差别,让我们注意到这样一个关键事实:在从闭源转向开源的过程中,受到威胁的仅仅是销售价值,而非使用价值。
  • 困难存在于开源开发社会契约的内在特点。有三个相辅相成的原因,使得主流的开源许可证不允许对对开源软件使用、再发布和修改施加限制,从而影响直接销售收入的获取。要理解这些原因,我们必须研究许可证演变所处的社会语境,也即互联网黑客文化(http://www.tuxedo.org/~esr/faqs/hacker-howto.html)。
  • 第一个原因与“对等性”有关,大多数开源开发者并不反对别人利用他们的礼物获利,只是要求不能有任何人(代码创始人可能会例外)站在一个特权地位上牟利。
  • 第二个原因则与“非有意后果”有关。黑客已经观察到,那些对商业使用或销售进行限制并收费(这是最常见的征费方式,乍看上去并无不妥)的许可证有着令人扫兴的效果。特别是这条规定给某些活动(如将开源软件系列发布在便宜的CD-ROM上)笼上了一层法律阴影,而这些活动正是我们非常愿意鼓励的事。更普遍地讲,如果对软件的使用/销售/修改/发布(以及其他在许可证中描述的复杂情况)加以限制,会使人们总是小心翼翼防范那些不确定的和潜在的法律风险(当人们接触的软件包越多,这个问题就越严重)。这个结果是有害的,因此,在强大的社会压力下,许可证将会变得越来越简单和无限制。
  • 与保持同侪评价这种礼物文化动力(“开垦心智层”一文中所描述的)相关。
  • 根据“大教堂与集市”一文的分析,开源获取高收益的条件大约有如下几种:(a)当可靠性/稳定性/可扩展性至关重要时,(b)没有其他方法比独立同行评审能更便捷易行地验证设计和实现正确性时(多数稍具规模的程序都适用这条)。 当软件对消费者越来越重要时,消费者会在理性上希望避开垄断供应者,这导致他们对开源的兴趣变大(开源供应商的市场竞争力会因此增强),所以,另一个判断标准是:(c)当软件成为对业务起关键作用的资产(比如存在于很多企业的MIS部门中)时。
  • 我们注意到,提供独特或高度差异化服务的供应商更担心其他竞争者拷贝他们的方法,而关键算法和知识库已经公开化时就不会这样。所以,(e)当关键方法(或能实现同等功能的方法)属于公共知识时,开源更可能胜出。
  • 这样的公司是最没有必要开源的:它拥有自己独特的能创造价值的软件技术(完全不满足(e)),它对故障不是很敏感(a),它有其他办法(不通过独立的同行评审)验证软件的正确性(b),它不是关键业务(c),也不会因为网络效应或人们普遍使用而获得价值上的实质增长(
  • 这个问题之所以严重,是因为对任意给定种类的软件产品,开源合作能够吸引的用户群和专家群都是有限的,而社区往往有黏性,如果两个在功能上大致等同的产品先后开源,先开源的往往会吸引最多的用户和最有激情的合作开发者,后开源的只能吃剩饭。社区之所以有黏性,是因为用户对软件已经熟悉,而开发者已经在代码上投入了太多时间。
  • 概括来说,基础架构的开放和共享,使每个参与者都得到了竞争上的好处,一是参与者能以较低成本生产出可扩展的产品和服务,二是参与者的市场定位可以让客户放心,他们很少会面临这样的尴尬境遇:由于供应商更改了战略或战术,导致产品被抛弃而无人照管。
  • 有时候,要想成为一只更大的青蛙,最佳办法就是让水池更快变大,这就是技术公司参与公开标准(完全可以将开源软件看成是可执行标准)的经济原因。
  • 开源似乎注定要成为一种普遍的做法,究其原因,更多是源自于客户需要和市场压力,而非供应端的效率。
  • 传统软件产业的战略家们是无法理解这种行为的(不顾其市场增长效应),因为他们成长在将(通过专利和商业秘密保护起来的)知识产权看成企业王冠上宝石的文化之中。为什么资助一项研究,却让每个竞争对手都可以无偿享用其结果呢?
  • 做正确的事”会给公司带来什么好处?答案本身既不令人惊讶也不难以验证,其他产业里也有这种看上去大公无私的行为,这些公司相信他们换来的是名声。
  • 为分析软件市场自身,有必要将软件服务按技术标准化程度进行分类,而这和软件服务的市场化(commoditize)程度存在密切关系。 这种分类与人们通常所说的“应用”(完全没有市场化、已开放的技术标准太弱或不存在)、“基础架构”(市场化服务、强标准)和“中间件”(部分市场化、有效但不完全的技术标准)有着很好的对应。在今天,典型的例子是字处理软件(应用)、TCP/IP协议栈(基础架构)和数据库引擎(中间件)。
  • 我们早先所做的收益回报分析表明:基础架构、应用和中间件将会以不同的方式变革,并展现出不同的开、闭源并存及平衡现象。在特定软件领域,开源能否流行,将取决于软件是否有实质性的网络效应、软件失效的代价如何以及软件作为资本货物的业务关键性程度。 如果将这个启发式分析方法运用到软件市场的各个部分(而不是单个产品),我们可以做出如下的大胆预言:
  • 基础架构(互联网、Web、操作系统、跨越竞争者界限的低层通信软件)将会几乎全部开源,并由用户联盟和盈利性发布/服务机构(如Redhat所扮演的角色)共同维护。
  • 应用,则非常倾向于继续封闭。当一个未公开算法或技术的使用价值足够高(且软件不稳定带来的相关成本足够低、供应商垄断带来的相关风险足可容忍)时,用户会继续为此类闭源软件付费。这种情况最有可能发生在自成一体的垂直市场应用中(其网络效应也较弱)。前面提到的锯木软件就是一例,1999年最热门和最有前景的生物识别软件则是另一例。
  • 中间件(像数据库、开发工具或可定制的应用协议栈顶端)将处于开闭源混杂的状态,这类软件走向闭源还是开源,似乎更取决于软件失效的代价,代价越高,其走向开放的市场压力就越大。
  • 他认为技术进步的趋势是更小、更轻和更有效,能够让人们“费力越来越少,收获越来越多,以至于最终可以毫不费力地获得所有东西”。
  • 虽然还处于早期阶段,但对我来说,推进战略所需的详细战术已经非常清楚了(我们在首次会议中明确讨论了这些战术),关键点如下。 1.忘掉自底向上,开始自顶向下
  • 网景的这次突破行动采用了相反的做法:战略决策者(Jim Barksdale)拿定主意,然后向下属强制推行这个愿景。
  • 2.Linux是我们最好的例证 我们必须大力宣扬Linux。是的,开源世界里还有其他一些不错的东西,这场运动也会向它们致敬,但Linux有着最好的知名度,有着最广泛的软件库,以及最大的开发社区。如果Linux都不能帮助突破,说实话,其他的就更指望不上了。
  • 抓住财富500强 除了财富500强,市场中另有一部分也很能花钱(最明显的例子是小企业和自由职业者),但这部分市场过于分散而且很难抓住。财富500强不只是有钱,而且有集中的和相对容易获取的钱,因此软件产业在很大程度上会按照财富500强的意愿行事。所以,我们首先应该说服财富500强。 4.赢得那些效劳财富500强的有威望媒体 把目标选定为财富500强,意味着我们需要赢得那些给上层决策者和投资人营造舆论环境的媒体。特别是《纽约时报》、《华尔街日报》、《经济学人》、《福布斯》以及《巴伦周刊》等等。 从这点看来,争取技术行业刊物是必要的,但远远不够,若要席卷华尔街,一个重要和基本的条件是先鼓动起精英主流媒体。
  • 5.说服黑客,游击市场 很明显,说服黑客社区自身与说服主流一样重要。如果只是一个或几个代表言之凿凿而大多数草根黑客并不买账,那可就差点意思了。 6.使用“Open Source”认证标识,保持纯净度 我们面临的一个威胁是:微软或其他大供应商可能会采取“拥抱并拓展”(embrace and extend)策略破坏“Open Source”一词,使它失去我们要传达的理念。所以Bruce Perens和我一开始就决定把这一术语注册成认证标识并把它和“开源定义”(Open Source Definition,也即Debian Free Software Guidelines的拷贝)绑定。这样我们可以利用法律诉讼的威慑力吓跑那些可能的滥用者。
  • 行业刊物很明显对开源也有了更正确的认识,正如Zawinski那句名言所说的:“开源(很伟大,但它)并不能点石成金。”
  • 两者最根本的区别是:黑客搞建设,骇客搞破坏。
  • 做一名黑客有很多乐趣,但这是一种需要努力才能获得的乐趣。而努力需要动力,成功运动员的动力来自于控制自己身体和超越自己过往生理极限的愉悦。
  • 在黑客文化中,假名是失败者的标识。
  • Peter Seebach维护着一个优秀的黑客FAQ(http://www.plethora.net/~seebs/faqs/hacker.html),用来帮助那些不懂得如何与黑客相处的管理者。我写的“黑客圈简史”(http://www.tuxedo.org/~esr/writings/hacker-history/hacker-history.html)和“大教堂与集市”(http://www.tuxedo.org/~esr/writings/cathedral-bazaar/index.html),对Linux开发和开源文化如何运转做了阐述,在“开垦心智层”(http://www.tuxedo.org/~esr/writings/homesteading/)中则对此话题做了更直接的探讨。

《憨夺型投资者》书摘

d2b5ca33bd970f64a6301fa75ae2eb22 27
  • 要知道,这是一个业务模式非常稳定的生意,长期以来,现金流和利润率已经经过了市场和时间的验证,这不复杂。
  • 他努力工作、赚钱养家,坚持储蓄,然后用所有的积蓄投入到一桩只赚不赔的生意里。
  • 他的从商经历属于典型的“少投注、投大注、只挑最好的投”。这也体现了低风险、高回报投资的特点:情况好,赢得多;情况不好,输得少。
  • 如果布兰森可以在几乎没有额外资本投入的前提下,投资建立维珍大西洋航空公司,那么你也肯定能够在你选定的行业中用最少的资本开始创业。你所需要的就是用一流的创意和妥善的方案来弥补资本不足的缺憾。
  • 情况好,赢得多;情况不好,输得少。
  • 如果你简单地用一下马尔瓦公式来衡量,就会发现,投资前你一定要弄明白两件事情: ·迅速拒绝你面前的投资要求; ·从最低额度的投资开始,经过几十年的发展,你会变得非常有钱。 该说的都说了。
  • 帕特尔老爹和巴菲特的出身和创业背景差异巨大,但却最终得出了相同的结论:投资变化缓慢行业中的经营模式简单的企业。
  • 确定投资对象的关键问题不是评估行业对社会的影响力或者判断行业的增长前景,而是要确定任何给定公司的竞争优势,最重要的是这种竞争优势是可持续性的。那些具有持久竞争优势的产品和服务能给投资者带来更加丰厚的回报。
  • 对我们而言,投资行为就好比我们去和赌马彩金下注系统展开博弈。我们下注是希望有一半的概率获胜,从而赢得3倍的回报。你要寻找的就是有明显代价差异的赌博。这就是投资行为本身的含义。你得清楚赌博概率和相应的回报要远远高于你的赔率。这就是价值投资。
  • 低风险和高不确定性对投资者而言是最佳拍档。
  • 这就是憨夺型投资模式的框架,具体如下: (1)投资现有的业务 (2)收购变化缓慢的行业中经营模式简单的企业 (3)在不景气的行业里对经营不善的企业进行抄底投资 (4)投资具有持久竞争优势的行业 (5)看准有利时机投大注 (6)注重套利 (7)买入以远低于内在价值折价出售的企业 (8)寻找风险低、不确定性高的业务
  • (9)模仿好过创新
  • 拥有某些企业的所有权是创造财富最好的办法。无须你持续投入时间和精力,反而还有最广泛的选择权,摩擦成本很低,购买一些上市企业的股票,显然是低风险、高回报的憨夺型投资模式。
  • 对付这种两难局面的憨夺型投资模式非常简单:只投资简单的企业,按照保守估算,这些企业未来的现金流很容易预测。什么样的企业经营模式较为简单?仁者见仁,智者见智。
  • 简约是非常有利的竞争优势。亨利·梭罗(Henry Thoreau)早早就意识到这一点,他曾说:“我们的生活因为太多的琐事而纷扰不断……简单点,再简单点。”
  • 为了打赢这场心理战,最有效的武器就是买进经营模式简单的企业,我会说服自己为何这样做赚钱的概率更高,一下子赔很多钱的概率会很低。我甚至把整个论证过程写下来。如果这个论证过程一个自然段都写不完,那么这笔投资就会出现问题。如果要我打开Excel表格,进行一连串数字的验证,那么这绝对是对我的投资想法亮红灯的时候。
  • 在正确地认定市场经常有效后,(学术界和华尔街的投资人士)错误地得出市场永远都是有效的结论。市场经常有效和永远有效之间有着天壤之别。
  • 市场不可能完全有效,因为人类控制着买卖关系确定的定价机制。人类会在极端恐惧和极端贪婪这两种情绪之间摇摆不定。当集体极端恐惧出现时,资产的定价就会低于其内在价值;当集体极端贪婪出现时,资产的定价就会高于其内在价值。
  • 我们如何确定哪些企业和行业处于低潮?有很多种方法,我们在这里先介绍六种。 (1)如果你每天都有阅读财经新闻的习惯,你会发现很多有关上市企业的信息。很多这些新闻报道反映了特定行业或者企业的负面信息。
  • (2)《价值线》(Value Line)每周都会发布一期过去13个星期里股票价格跌幅最大的简讯,名列其中的股票可谓是低迷公司的集锦,每次发布40家公司的名单,这些公司在短短3个月的时间里经历了从20%~70%不等的股价跌幅,跌幅最大的企业很有可能就是最不景气的企业,同时发布的信息还包括同期市盈率最低、面值贬值最大、收益最高的企业名录等。并非所有榜上有名的企业都病入膏肓,但是如果企业交易的市盈率达到3,那么它就值得我们进一步分析和观察。
  • (3)《投资组合播报》(Portfolio Reports)(见www.portfolioreports.com)每月会出一份报告,其中披露了80位最负盛名的价值经理最青睐的十大股票名称。该份报告的信息源是各种不同依法需要公开的机构投资者公报等。《
  • (4)如果你不想出钱订阅《投资组合播报》,你可以直接通过查阅机构投资者发布的公报,比如《美国证监会表13-F》(SEC Form 13-F)。
  • (6)最后,你还可以阅读乔尔·格林布拉特撰写的《赢得市场手册》(The Little Book That Beats the Market)。
  • 投资者会竭尽全力利用一切商机来获得超额的利润。令人讽刺的事情是在追逐最佳利润的同时,他们也扼杀了产生超额利润的源泉。
  • 我们如何知道一家企业有其隐性的竞争优势,如何确定这些竞争优势呢?通过查阅其财务报表,通常能够看出点端倪。拥有持久竞争优势的企业,就像那个在C镇上开理发店的理发师一样,能够从投资的资本中获得较高的回报。资产负债表能够告诉我们企业使用的资本。收益和现金流量表能够表明这些资本带来的回报。
  • 我们假设你可以用1美元去投注,你可以得到下面几种回报的可能。 ·80%的概率,赚21美元; ·10%的概率,赚7.5美元; ·10%的概率,赔掉所有。
  • 平均收益/最佳收益=每次应该投注的金额
  • 威廉·庞德斯通(William Poundstone)曾写过一本好书,名为《财富公式》(Fortune’s Formula)1,很值得一读。庞德斯通精辟地描述了凯利公式的内涵。美盛集团(Legg Mason)的迈克尔·莫布森(Michael Mauboussin)写了一篇论文2来阐明凯利公式。他这样说,如果你掷硬币,正面朝上,你能获得2美元;正面朝下,需要花费1美元。如果你碰到这样的情况,你该拿多少钱来投注?
  • 明智的投资人会在出现有利于自身的投资机会的时候,下大注。他们有了较好的回报期望时,他们就会下大注。其他时间,他们都不会出手。就这么简单。
  • 在投资中,永远没有稳赚一说。就算是当今最有潜力的蓝筹股,明天也有衰落的时候。投资就看回报,就像玩21点一样。
  • 同理,如果你投资了某只价位过高或者过低的股票,最终股价会回归到与其内在价值差不多的水平,从中投资者可以获利或者失利。我们可以把它视为投资的黄金规律,并充分尊重这一规律。因此,如果我们能确定某个企业在未来两三年的内在价值水平,且能以比较低的价位折价购买,我们的投资利润就能保证。在确定具体下注的数额时,凯利公式非常有用。
  • 憨夺型投资方式的关键就是少投注、投大注、看准时机投注。凯利公式也支持这种方法。这种方法在股票市场中进行被动投资非常管用。最后,正如查理·芒格经常所说的:“逆向投资,得反其道而行。”
  • 凯利公式会告诉我们投资的上限比例,我们就能比较妥善地把握实现财富目标的最佳时机。想要尽快致富,还没有赔光的风险,憨夺型投资是最好的办法。如果你投资的比例超过凯利公式提示的结果,可以肯定的是,如果你不断重复投资,你肯定会把自己的资产输得一分不剩。
  • 投资和赌博一样,主要是看有怎样的回报。看准投资成本和风险偏低、回报偏高的投资机遇,集中下注,这是创造财富的关键。首先要用凯利公式来确定投资额占净资产额的上限。由于在股权市场中有多项下注机会,凯利公式无法解决的投资波动问题可以通过集中投资组合缓解。
  • 由于在股权市场中有多项下注机会,凯利公式无法解决的投资波动问题可以通过集中投资组合缓解。
  • 这两个例子里,企业都建立了一个品牌,拥有稳定的客源和收入。即使最初的套利利差小时,企业依然可以依靠品牌的力量继续存在发展。然而,未来几十年里,企业的增长率要快过总体经济增长率,将变得很难。
  • 创业者发现现有的套利机会,由此创办伟大的企业,套利机会可以成为推动创业的动力。
  • 巴菲特先生特别擅长投资具有持久竞争优势和套利利差的企业。尽管如此,即使在伯克希尔公司,有些绝佳的企业也丧失了它们的竞争优势,比如蓝筹印花公司(Blue Chip Stamps)和世界图书出版公司(World Book)。
  • 我们知道所有憨夺型套利利差最终都会消失。这里的关键问题是:套利利差持续的时间和竞争优势的可持续性。正如巴菲特先生所说的: 投资的关键不是评估一个行业如何影响社会,或者其增长的速度,而是确定公司的竞争优势是什么,最重要的是这种竞争优势的可持续性。
  • 所有的竞争优势最终会消失。即使看似能长久的憨夺型套利利差也会消失,但这并不意味着我们就不投资,或者投资无法得到较好的回报了。我们需要明白竞争优势持续的时间是10个月还是10年。套利利差越大越好,持续的时间越久越好。憨夺型和传统型套利交易的区别主要在于套利利差的持久性和具体的价差。
  • 一定要寻找各种套利机会。通过套利,你能在毫无风险的情况下获得较高的投资回报。请充分利用这种低风险、高回报的套利交易的利差。
  • 格雷厄姆对于安全边际的痴迷是可以理解的。尽可能降低失利的风险,同时提高盈利的概率是非常有效的手段。正因为如此,巴菲特先生的净资产超过了400亿美元。他就是用追求风险最低、回报最大的投资方法来积累现在的财富的。通常情况下,资产的交易价格等于或者高于其内在价值。这里的关键是耐心等待,等到资产的市场价位跌到远远低于内在价值的水平。
  • 对我而言,任何有关技术的投资,只要行业性质不可预计或者变化很快,5秒钟不到,我就会立马回绝。
  • 快速变化的行业是投资的天敌,这就是为什么憨夺型创业者只关注那些长远来看变化不大的行业。
  • 微软公司对于外部发生的创新反应迅猛,都能以迅雷不及掩耳的速度消除潜在的危险。他们在投资创新行动前,会先弄清楚客户对别家公司创新成果的认可程度。这是非常有效的战略。一位曾经在微软公司工作的管理人员告诉我,微软一旦设定了明确的目标,就能出成果。公司仿制Netware或者Lotus1-2-3时,对产品的外观或者收入有明确的规定。这就是明确树立、实现目标的典范。
  • 而每次微软公司试图引领行业潮流或者创新的时候,就会状况百出。它所倡导的“.Net”项目,计划一直不清楚,开展多年也没有什么进展。微软的Vista操作系统应该是具有革命意义的产品,要是它赶不上苹果当前的产品,我也不会感到惊讶。
  • 微软擅长模仿和业务开拓。在跟踪敌方产品、消除潜在威胁方面,成功率高达90%。谷歌与微软的战斗最终结果如何,我们无法预测。微软公司现在有6万多名员工,它虽然一向反对官僚作风,但是这样庞大的机构也不免落入俗套。如果现在我只有两个选择,要么投资谷歌,要么投资微软,我会毫不犹豫地选择微软。这是一场创新和模仿者之间的竞争。优秀的模仿者能够成就长存的事业;而创新充满了变数,克隆是确定的。
  • 实际上,如果我们在两年后以高于40万美元的价格出售这个加油站,这样的回报比我们持有10年,并在10年后以100万美元的价格出售而言,很难去弥补那些复利不执行带来的资本损失。虽说如此,我们依然要非常有耐心,但也不能无止境地等待下去。我的结论是:两三年是让赔本的投资者止损的最佳等待时间。
  • 有关于价值投资最好的书是乔尔·格林布拉特撰写的《赢得市场手册》。我会默认阅读本书的人已经读过《赢得市场手册》。
  • 三年后,如果投资依然不见好转,原因通常是我们对企业的内在价值或者内在价值关键的推动因素的判断失误,也有可能多年来公司的内在价值的确下降了。一旦三年的期限已过,不要犹豫承担已有的损失。吃一堑长一智。这样的损失会告诉你如何成为更好的投资者。虽然我们学习他人的教训能提高自己,但是自己体验到的教训能够最大限度地推动自己的成长。随着时间的流逝,不断地从自己的失败经验中吸取教训,你会逐渐发现你成功逃过查克拉乌约阵的概率越来越高。
  • 在击败查克拉乌约阵后,要退出来很简单。在买入股票后三年内,会出现股价和内在价值交汇的时候,这就是我们获得较高年化收益的机会。每当股价和公司内在价值的差距小于10%时,一定要赶紧卖出退场。当市场价格一旦逼近其内在价值的时候,就要赶紧卖出股票。唯一例外的情况是税收的考虑。如果你在追寻短期的回报,你应该持股,一直到实现了长期收益或者股价超过内在价值的数额足以支付额外的税金为止。
  • 世界上的很多财富都是因为集中持有某一个公司的股票而获得的。如果你了解这个企业的情况,你不需要持有很多其他公司的股票。
  • 真正的投资机遇很少见,因此一旦出现,你就要好好把握,一定要拿出你财富的一大部分去投资。
  • 有句话说得好,“少投注、投大注、看准了再投注”。当你认定机会非常有利时,你要倾尽所有,全力以赴。凯利公式会引导你到底买入持有多少股票。请以凯利公式提示的数额1/4为准。
  • 我很喜欢读斯文森写的《不落俗套的成功:最好的个人投资方法》(Unconventional Success:A fundamental Approach to Personal Investment)一书。
  • 神奇公式能提使你憨夺每美元只需用50美分支付的投资机会。我们可以大大简化投资过程,只要分析一下神奇公式提示的股票。随着时间的流逝,我们也能成为大富翁。我极力向大家推荐这种方法。这种方法很简单。你可以在小水桶里钓鱼,结果比绝大多数指数投资回报都好。
  • 憨夺型投资者只投资简单熟悉的行业。这点要求足以排除99%的投资选择。像阿周那一样,我们必须集中精力研究那些简单熟悉的企业信息,我们必须在力所能及的范围内做出正确判断,而不受那些外在的噪声干扰。在我们熟悉的范围内,阅读有关书籍、报纸杂志、公司报表和行业信息等,说不定就能碰到一些有利于我们的公司信息,如果我们觉得这个公司有投资的潜力,公司的股价会比其内在价值低很多,这就表明我们买入这只股票的时间到了。

领证,其实并不会有什么不同

d2b5ca33bd970f64a6301fa75ae2eb22 26

晚上睡觉的时候,老婆问我:“领证了感觉好像也没啥变化?”

我回答:“是的,的确没有什么变化”。


老婆过去一直认为领证是一个很重要的事情。从她的视角来看,领证让她觉得天津这个城市将会进一步给予她价值和存在感。

但在我看来,领证其实不会令我们的生活有太多的不同。领证并不会改变我们之间相处的模式、领证也并不会改变我们的生活状态。今天晚上该吃小鸡炖蘑菇,依然还是小鸡炖蘑菇。

另一个视角来看,领证也带来了我们生活中的不同。我们的心态开始发生变化,我们不再是一个独立的个体。我们开始有了一个新的身份:Some one‘s 老公 / 老婆。我们不再只为自己。我们可以成为彼此在医院需要签署病危通知书时的家属。

但无论如何,生活总是在向更好的方向演进下去。太阳照常升起,生活总是要日常的过下去。

一个人的永续职业

fountain pen on black lined paper

每个人在这个社会上都有一个属于自己的职业,这个职业成为别人认知我们的标签。

在过去的几十年里,我们看到,一个人可以从刚毕业做一份工作,一直做到退休。但在如今快节奏的时代当中,我们往往看到的是一个人加入一家公司,工作几年后,又离开这家公司,到一家新的公司去。

我们很难有一个固定的工作和职业来描述自己 —— 你到底是什么职业?

职业带来的收入是其次,关键是职业会给人不同的认知,以及职业是一种身份认同,我们需要依靠身份认同,在社会当中找到自己的位置。

那到底有没有什么职业是可以永续存在的?

对我而言,有三:

  1. 写作者:写作是一个相对稀缺的能力,且随着 AIGC 的出现,越来越多的人不擅长写作。写作的价值反而在提升。且写作对我来说,是一个持续表达的事情,无论如何,我都将持续写作下去。
  2. 创造者:我之前就说过,软件工程师是这个时代的手艺人,你可以使用自己的技能,来创造出一些新鲜的产品出来,去解决别人的问题,打造不同的产品和生态。创造者与你的年龄,工作时间无关,只与你自己是否还愿意创造有关。
  3. 投资者:投资只关乎你买入的资产,并跟随你的资产不断成长。与你的年龄,工作时间等都无关系。任何人都可以成为投资者,也可以一辈子成为投资者。

使用飞书消息卡片变量功能,批量数据快速录入消息卡片

0c0ca4a0ac1f249860b29e295dd55260

在开发短链助手的时候,我需要实现一个查看当前用户创建的所有短链接的能力。这个依然希望通过消息卡片来完成。而作为一个 JSON,想要构建一套合适的内容,就变得十分的麻烦和复杂。

ql9bov

解构消息卡片

我要发送的消息卡片当中,可以区分为动态内容和静态内容,对于静态内容,我可能长期都不会变化,而静态内容,则会根据用户的数据发生变化。

wsyp71

如果整体都放在代码中生成,我就需要有一段又臭又长的代码来维护其中的变化的 JSON ,而我希望整个代码的简洁,不要有比较长的代码只是用来生成卡片的逻辑,所以就用上了消息卡片的新功能:循环对象数组。

而进一步看动态内容,则我们可以将其视为是变量 A 和变量 B 在不断的被重复赋予,最终形成了一行一行的结果。

而我们想要实现这样功能。首先,需要在卡片搭建工具中创建一个循环对象数组,并将其绑定在一个「多列布局」上。

dg2nvw

绑定完成后,你的多列布局就有了被循环的可能性。
h96z3l

接下来你需要在多列布局中去构建你的每一行的结果,并在对应的位置绑定上变量,比如我这里就给多列布局防止了一个 Markdown 文本组件,并在这个文本组件中,填入了 ${source} 作为变量 A 进行填充。

e3w12l

当你根据你的需要,构建出需要的卡片结构后,点击右上角的保存并发布,就可以准备写代码来实现批量发送数据的逻辑了。

代码片段

这里的逻辑不复杂,首先需要从数据库中提取出需要用用作列表循环的数据,这里以 data.data 为例,data.data 是一个包含了 Object 的 Array,其中每一个 Object 都有 Postfix 和 Link 两个字段。这两个字段就是我们稍后要塞在卡片中的。

       // data.data = [{Postfix:"a",Link:"https://amazon.cn"}]
        let links = data.data.map(item => {
          return {
            source: `[${item.Postfix}](https://link.feishu.io/${item.Postfix})`,
            target: item.Link
          }
        })
        await client.request({
          method: "POST",
          url: "https://open.feishu.cn/open-apis/im/v1/messages",
          data: {
            receive_id: ctx.body.event.operator.operator_id.open_id,
            msg_type: 'interactive',
            content: JSON.stringify({
              "type": "template",
              "data": {
                "template_id": "ctp_AAmFBm5vnHfs",
                "template_variable": {
                  "CONTENT": links
                }
              }
            }),
          },
          params: {
            receive_id_type: 'open_id',
          },
        })
Code language: JavaScript (javascript)

最终我们构建出来,发给飞书服务器的 JSON 其实是这样子的,这段 JSON 就会和我们在卡片搭建工具中构建的 JSON 租和,自动进行拼接,从而实现我们想要的循环效果。

{
  "type": "template",
  "data": {
    "template_id": "ctp_AAmFBm5vnHfs",
    "template_variable": {
      "CONTENT": [
        {
          "source":"a",
          "target":"https://amazon.cn"
        },
        {
          "source":"b",
          "target":"https://baidu.com"
        }
      ]
    }
  }
}
Code language: JSON / JSON with Comments (json)

文章中构建的出的卡片

构建出的卡片 JSON 是这样的,方便你参考:

{
  "elements": [
    {
      "tag": "markdown",
      "content": "你创建的链接如下:"
    },
    {
      "tag": "column_set",
      "flex_mode": "none",
      "background_style": "grey",
      "columns": [
        {
          "tag": "column",
          "width": "weighted",
          "weight": 1,
          "vertical_align": "top",
          "elements": [
            {
              "tag": "div",
              "text": {
                "content": "${source}",
                "tag": "lark_md"
              }
            }
          ]
        },
        {
          "tag": "column",
          "width": "weighted",
          "weight": 4,
          "vertical_align": "top",
          "elements": [
            {
              "tag": "div",
              "text": {
                "content": "${target}",
                "tag": "lark_md"
              }
            }
          ]
        }
      ],
      "_varloop": "${CONTENT}"
    }
  ],
  "header": {
    "template": "turquoise",
    "title": {
      "content": "链接清单",
      "tag": "plain_text"
    }
  },
  "card_link": {
    "url": "",
    "pc_url": "",
    "android_url": "",
    "ios_url": ""
  }
}
Code language: JSON / JSON with Comments (json)

完整代码参考

import cloud from '@lafjs/cloud'
import axios from 'axios'

let appid = "";
let secret = ""

const lark = require('@larksuiteoapi/node-sdk');

const client = new lark.Client({
  appId: appid,
  appSecret: secret
});


export default async function (ctx: FunctionContext) {

  console.log("event",ctx.body);

  if (ctx.body.challenge) {
    return ctx.body
  }

  if (Object.hasOwn(ctx.body, "action") && ctx.body.action) {
    if (ctx.body.action.name != "submit") return { code: 1 };
    try {
      // function to create link

      if (status == 200) {
        return JSON.stringify({
          "type": "template",
          "data": {
            "template_id": "ctp_AAmFBm5vnlt0",
            "template_variable": {
              "source": ctx.body.action.form_value.postfix,
              "target": ctx.body.action.form_value.link
            }
          }
        })
      }
      return {};
    } catch (e) {
      return JSON.stringify({
        "type": "template",
        "data": {
          "template_id": "ctp_AAmFBm5vZYuo",
          "template_variable": {
            "POSTFIX": ctx.body.action.form_value.postfix
          }
        }
      });
    }
  }

  if (Object.hasOwn(ctx.body, "header") && ctx.body.header.event_type == 'application.bot.menu_v6') {
    // 处理按钮
    if (ctx.body.event.event_key == "help") {
      try {
        let content = JSON.stringify({
          template_id: "ctp_AAmFBFOpYX0S"
        });
        await client.request({
          method: "POST",
          url: "https://open.feishu.cn/open-apis/im/v1/messages",
          data: {
            receive_id: ctx.body.event.operator.operator_id.open_id,
            msg_type: 'interactive',
            content: JSON.stringify({
              "type": "template",
              "data": {
                "template_id": "ctp_AAmFBFOpYX0S",
              }
            }),
          },
          params: {
            receive_id_type: 'open_id',
          },
        })
        return {};
      } catch (e) {
        console.log(`key: ${ctx.body.event.event_key}, user:${ctx.body.event.operator.operator_id.open_id},error`, e);
        return {};
      }
    }
    if (ctx.body.event.event_key == "mylink") {
      try {
        // function to get all my link 
        let links = data.data.map(item => {
          return {
            source: `[${item.Postfix}](https://link.feishu.io/${item.Postfix})`,
            target: item.Link
          }
        })
        await client.request({
          method: "POST",
          url: "https://open.feishu.cn/open-apis/im/v1/messages",
          data: {
            receive_id: ctx.body.event.operator.operator_id.open_id,
            msg_type: 'interactive',
            content: JSON.stringify({
              "type": "template",
              "data": {
                "template_id": "ctp_AAmFBm5vnHfs",
                "template_variable": {
                  "CONTENT": links
                }
              }
            }),
          },
          params: {
            receive_id_type: 'open_id',
          },
        })
        return {};
      } catch (e) {
        console.log(`key: ${ctx.body.event.event_key}, user:${ctx.body.event.operator.operator_id.open_id},error`, e);
        return {};
      }
    }
    if (ctx.body.event.event_key == "create") {
      try {
        await client.request({
          method: "POST",
          url: "https://open.feishu.cn/open-apis/im/v1/messages",
          data: {
            receive_id: ctx.body.event.operator.operator_id.open_id,
            msg_type: 'interactive',
            content: "{\"header\":{\"template\":\"turquoise\",\"title\":{\"content\":\"创建短链接\",\"tag\":\"plain_text\"}},\"elements\":[{\"tag\":\"form\",\"name\":\"form_1\",\"elements\":[{\"tag\":\"input\",\"name\":\"postfix\",\"placeholder\":{\"tag\":\"plain_text\",\"content\":\"请输入后缀\"},\"max_length\":10,\"label\":{\"tag\":\"plain_text\",\"content\":\"请输入后缀:\"},\"label_position\":\"left\",\"value\":{\"k\":\"v\"}},{\"tag\":\"input\",\"name\":\"link\",\"placeholder\":{\"tag\":\"plain_text\",\"content\":\"请输入要跳转链接\"},\"label\":{\"tag\":\"plain_text\",\"content\":\"请输入要跳转链接:\"},\"label_position\":\"left\",\"value\":{\"k\":\"v\"}},{\"action_type\":\"form_submit\",\"name\":\"submit\",\"tag\":\"button\",\"text\":{\"content\":\"提交\",\"tag\":\"lark_md\"},\"type\":\"primary\",\"confirm\":{\"title\":{\"tag\":\"plain_text\",\"content\":\"创建短链接\"},\"text\":{\"tag\":\"plain_text\",\"content\":\"确认提交吗\"}}}]}]}",
          },
          params: {
            receive_id_type: 'open_id',
          },
        })
        return {};
      } catch (e) {
        console.log(`key: ${ctx.body.event.event_key}, user:${ctx.body.event.operator.operator_id.open_id},error`, e);
        return {};
      }
    }
  }
  return { data: 'hi, laf' }
}
Code language: JavaScript (javascript)

使用飞书消息卡片模板,减少代码硬编码 JSON

0c0ca4a0ac1f249860b29e295dd55260

在开发短链助手时,一个很大的痛苦的点是我希望通过消息卡片来完成开发者的交互,这意味着我需要有大量的行为是和消息卡片来完成的。而消息卡片又不同于 HTML,是一个比较明确的 DSL。消息卡片更多是基于 JSON 提供的一套 Schema,将其放在代码中管理也是一个非常麻烦的事情。

好在最近飞书开放平台迭代了消息卡片模板的功能,我可以不用把 JSON 存在代码中,而是只在代码中存一个 Template ID ,从而降低我在代码中维护这段 JSON 的难度。

在卡片构建工具中新建卡片

首先,你需要打开消息卡片搭建工具,并在其中创建一个新的卡片(你可以使用其提供的卡片组的能力,来管理你的卡片们)。比如我就要这个卡片组来管理短链助手和其他场景的卡片。

7rmbt4

创建卡片完成后,你可以在 UI 上点击保存并发布,你就将你的卡片消息模板发布到了飞书的服务器。

o69het

此时,你就可以在代码中使用了。点击页面中间的 ID,复制消息卡片模板 ID,将你的调用代码替换为对应的逻辑即可。

lv64d7

使用模板需要注意,将消息卡片中的 Content 从过去的卡片内容,替换为 template 的 JSON。比如,使用卡片 JSON 发送的时候,我们发送的数据可能是这样的:

{
    "receive_id": "oc_820faa21d7ed275b53d1727a0feaa917",
    "content": "{\"config\":{\"wide_screen_mode\":true},\"elements\":[{\"alt\":{\"content\":\"\",\"tag\":\"plain_text\"},\"img_key\":\"img_7ea74629-9191-4176-998c-2e603c9c5e8g\",\"tag\":\"img\"},{\"tag\":\"div\",\"text\":{\"content\":\"你是否曾因为一本书而产生心灵共振,开始感悟人生?\\n你有哪些想极力推荐给他人的珍藏书单?\\n\\n加入 **4·23 飞书读书节**,分享你的**挚爱书单**及**读书笔记**,**赢取千元读书礼**!\\n\\n📬 填写问卷,晒出你的珍藏好书\\n😍 想知道其他人都推荐了哪些好书?马上[入群围观](https://open.feishu.cn/)\\n📝 用[读书笔记模板](https://open.feishu.cn/)(桌面端打开),记录你的心得体会\\n🙌 更有惊喜特邀嘉宾 4月12日起带你共读\",\"tag\":\"lark_md\"}},{\"actions\":[{\"tag\":\"button\",\"text\":{\"content\":\"立即推荐好书\",\"tag\":\"plain_text\"},\"type\":\"primary\",\"url\":\"https://open.feishu.cn/\"},{\"tag\":\"button\",\"text\":{\"content\":\"查看活动指南\",\"tag\":\"plain_text\"},\"type\":\"default\",\"url\":\"https://open.feishu.cn/\"}],\"tag\":\"action\"}],\"header\":{\"template\":\"turquoise\",\"title\":{\"content\":\"📚晒挚爱好书,赢读书礼金\",\"tag\":\"plain_text\"}}}",
    "msg_type": "interactive"
}
Code language: JSON / JSON with Comments (json)

而在使用模板时,我们只需要很短的内容就可以:

{
      "receive_id": "ou_7d8a6exxxxccs",
      "msg_type": "interactive",
      "content": "{\"type\": \"template\", \"data\": { \"template_id\": \"ctp_xxxxxxxxxxxx\", \"template_variable\": {\"article_title\": \"这是文章标题内容\"} } }"
  }
Code language: JSON / JSON with Comments (json)

这样你就可以把过去又臭又长的 JSON 变为一个简短小巧的 Template ID 来完成。

一些 Tips

在使用模板时,如果你的模板比较多,那么管理这些模板会比较成问题,一个比较好的办法是你可以考虑把 template ID 的编辑链接放在你的代码注释里,这样当你需要编辑 JSON 的时候,只需要点击代码中的链接就可以跳过来编辑了。

       client.request({
          method: "POST",
          url: "https://open.feishu.cn/open-apis/im/v1/messages",
          data: {
            receive_id: ctx.body.event.operator.operator_id.open_id,
            msg_type: 'interactive',
            content: JSON.stringify({
              "type": "template",
              "data": {
                "template_id": "ctp_AAmFBFOpYX0S", // https://open.feishu.cn/tool/cardbuilder?templateId=ctp_AAmFBFOpYX0S
              }
            }),
          },
          params: {
            receive_id_type: 'open_id',
          },
        })
Code language: CSS (css)

如何巧用飞书消息卡片输入框实现一套业务交互逻辑

0c0ca4a0ac1f249860b29e295dd55260

飞书开放平台最近开始内测了输入框的能力,基于输入框,为消息卡片提供了进一步业务系统打通的可能性,你可以不需要开发一整个网页应用,只需要借助飞书机器人和飞书消息卡片,就可以实现一套业务交互逻辑。

流程图示意

w0052z

目标说明

这里首先确定要实现的逻辑:这里我要做的是一个短链接应用,功能很简单,点击下方的机器人菜单,并在弹出的窗口中输入对应的短链接后缀和要跳转的链接,点击确定就会帮我创建一个短链接。

4rbg8i

具体效果如下:

dqk8d2

如果后缀已经被占用,则展示如下内容:

50tl60

在实现这个功能时,我首先使用了飞书提供的输入框组件的能力和表单组件能力,来实现整个业务交互,当然,你也可以根据业务形态,来选择合适的组件,构成一整个输入表单。

实现逻辑

整体的功能可以分为三步:

  1. 点击按钮:机器人需要响应点击事件,并发送一个带有输入框的消息卡片。
  2. 验证卡片输入内容:消息卡片中提供了输入框,但是用户的输入是否我们能用,需要设计一些验证的能力。
  3. 反馈用户是否创建成功:当我们创建成功后,需要给开发者提示,告诉他是否已经创建成功,帮助他结束整个流程。

接下来就是具体的实现步骤了。

点击按钮并回复卡片

首先,我先是使用了机器人的菜单功能,来实现在机器人底部配置菜单。你需要访问飞书开发者后台,找到机器人能力中的「机器人自定义菜单」,就可以配置一个机器人的自定义菜单了。机器人菜单支持跳转到指定链接,或者是推送事件,我选择推送事件,这样我就可以在服务端响应用户的创建的行为。这里我设定了事件内容为 create ,便于后续处理。

ogypfv

机器人菜单的处理则可以参考机器人菜单使用说明,通过订阅「机器人自定义事件」来完成对于相应行为的接受和对应的处理。

这部分的处理逻辑可以参考如下代码

// 判断请求体当中是否有 header 字段 && 来源的事件是否是机器人菜单
if (Object.hasOwn(ctx.body, "header") && ctx.body.header.event_type == 'application.bot.menu_v6') {

    // 请求的事件是否是创建短链接对应的事件。
    if (ctx.body.event.event_key == "create") {
      try {
        await client.request({
          method: "POST",
          url: "https://open.feishu.cn/open-apis/im/v1/messages",
          data: {
            receive_id: ctx.body.event.operator.operator_id.open_id, // 从事件体中提取事件的触发人
            msg_type: 'interactive',
            content: "", // 推送卡片 JSON
          },
          params: {
            receive_id_type: 'open_id',
          },
        })
        return {};
      } catch (e) {
        console.log(`key: ${ctx.body.event.event_key}, user:${ctx.body.event.operator.operator_id.open_id},error`, e);
        return {};
      }
    }
  }
Code language: JavaScript (javascript)

对卡片输入内容进行校验

在完成卡片响应的设定后,接下来我实现的是校验的逻辑,这里分为两层:第一层是客户端可以完成的校验:比如短链接应该少于 10 个字符。第二层是只有客户端才能完成的校验。

1. 在本地校验文件长短

如果每次发起请求都需要发送到服务端进行校验,则有比较高的校验成本。好在消息卡片提供了本地校验的能力,你可以通过 max_length 字段来验证输入框长短.

这里我是使用输入框组件的字段,来验证输入的内容长度不得大于 10 。

du1glf

2. 输入两个参数才发起请求

在消息卡片的输入框组件中,只要输入内容就会发现校验,因此我不能直接使用输入框组件,而是需要借助 form 组件,来实现用户输入两个内容再手动发起提交。则具体我构建的卡片 JSON 是这样的。

{
  "header": {
    "template": "turquoise",
    "title": {
      "content": "创建短链接",
      "tag": "plain_text"
    }
  },
  "elements": [
    {
      "tag": "form",
      "name": "form_1",
      "elements": [
        {
          "tag": "input",
          "name": "postfix",
          "placeholder": {
            "tag": "plain_text",
            "content": "请输入后缀"
          },
          "max_length": 10,
          "label": {
            "tag": "plain_text",
            "content": "请输入后缀:"
          },
          "label_position": "left",
          "value": {
            "k": "v"
          }
        },
        {
          "tag": "input",
          "name": "link",
          "placeholder": {
            "tag": "plain_text",
            "content": "请输入要跳转链接"
          },
          "label": {
            "tag": "plain_text",
            "content": "请输入要跳转链接:"
          },
          "label_position": "left",
          "value": {
            "k": "v"
          }
        },
        {
          "action_type": "form_submit",
          "name": "submit",
          "tag": "button",
          "text": {
            "content": "提交",
            "tag": "lark_md"
          },
          "type": "primary",
          "confirm": {
            "title": {
              "tag": "plain_text",
              "content": "创建短链接"
            },
            "text": {
              "tag": "plain_text",
              "content": "确认提交吗"
            }
          }
        }
      ]
    }
  ]
}
Code language: JSON / JSON with Comments (json)

这部分的关键是用 form 组件包裹 Input 组件,从而规避了 Input 组件输入内容就会发送到服务端校验的问题。

7yro9r

3. 在服务端验证有无

这部分逻辑我在实现的时候相对简单,没有专门去进行校验(主要是因为我的短链接服务和机器人是两个不同的服务),而是通过短链服务返回 200 还是 401 来判断是否出现了重复的问题,所以这里只是简单的使用了一个 try catch 来完成校验。

需要注意的是,这里你会注意到,返回是直接返回了一段 JSON String,这是因为触发这个事件是通过消息卡片的回调能力,如果你在消息卡片的回调能力返回一个 JSON,就会直接把 UI 层面的卡片渲染为你返回的卡片结果。靠着这个功能,我来实现的成功与失败返回不同的内容。

if (Object.hasOwn(ctx.body, "action") && ctx.body.action) {    
    try {
      // create link

      if (status == 200) {
        return JSON.stringify({
          "type": "template",
          "data": {
            "template_id": "ctp_AAmFBm5vnlt0",
            "template_variable": {
              "source": ctx.body.action.form_value.postfix,
              "target": ctx.body.action.form_value.link
            }
          }
        })
      }
      return {};
    } catch (e) {
      return JSON.stringify({
        "type": "template",
        "data": {
          "template_id": "ctp_AAmFBm5vZYuo",
          "template_variable": {
            "POSTFIX": ctx.body.action.form_value.postfix
          }
        }
      });
    }
  }
Code language: JavaScript (javascript)

完整代码参考

整个机器人的部分的代码只有 170 余行,不多,供你参考

import cloud from '@lafjs/cloud'
import axios from 'axios'

let appid = "";
let secret = ""

const lark = require('@larksuiteoapi/node-sdk');

const client = new lark.Client({
  appId: appid,
  appSecret: secret
});


export default async function (ctx: FunctionContext) {

  console.log("event",ctx.body);

  if (ctx.body.challenge) {
    return ctx.body
  }

  if (Object.hasOwn(ctx.body, "action") && ctx.body.action) {
    if (ctx.body.action.name != "submit") return { code: 1 };
    try {
      // function to create link

      if (status == 200) {
        return JSON.stringify({
          "type": "template",
          "data": {
            "template_id": "ctp_AAmFBm5vnlt0",
            "template_variable": {
              "source": ctx.body.action.form_value.postfix,
              "target": ctx.body.action.form_value.link
            }
          }
        })
      }
      return {};
    } catch (e) {
      return JSON.stringify({
        "type": "template",
        "data": {
          "template_id": "ctp_AAmFBm5vZYuo",
          "template_variable": {
            "POSTFIX": ctx.body.action.form_value.postfix
          }
        }
      });
    }
  }

  if (Object.hasOwn(ctx.body, "header") && ctx.body.header.event_type == 'application.bot.menu_v6') {
    // 处理按钮
    if (ctx.body.event.event_key == "help") {
      try {
        let content = JSON.stringify({
          template_id: "ctp_AAmFBFOpYX0S"
        });
        await client.request({
          method: "POST",
          url: "https://open.feishu.cn/open-apis/im/v1/messages",
          data: {
            receive_id: ctx.body.event.operator.operator_id.open_id,
            msg_type: 'interactive',
            content: JSON.stringify({
              "type": "template",
              "data": {
                "template_id": "ctp_AAmFBFOpYX0S",
              }
            }),
          },
          params: {
            receive_id_type: 'open_id',
          },
        })
        return {};
      } catch (e) {
        console.log(`key: ${ctx.body.event.event_key}, user:${ctx.body.event.operator.operator_id.open_id},error`, e);
        return {};
      }
    }
    if (ctx.body.event.event_key == "mylink") {
      try {
        // function to get all my link 
        let links = data.data.map(item => {
          return {
            source: `[${item.Postfix}](https://link.feishu.io/${item.Postfix})`,
            target: item.Link
          }
        })
        await client.request({
          method: "POST",
          url: "https://open.feishu.cn/open-apis/im/v1/messages",
          data: {
            receive_id: ctx.body.event.operator.operator_id.open_id,
            msg_type: 'interactive',
            content: JSON.stringify({
              "type": "template",
              "data": {
                "template_id": "ctp_AAmFBm5vnHfs",
                "template_variable": {
                  "CONTENT": links
                }
              }
            }),
          },
          params: {
            receive_id_type: 'open_id',
          },
        })
        return {};
      } catch (e) {
        console.log(`key: ${ctx.body.event.event_key}, user:${ctx.body.event.operator.operator_id.open_id},error`, e);
        return {};
      }
    }
    if (ctx.body.event.event_key == "create") {
      try {
        await client.request({
          method: "POST",
          url: "https://open.feishu.cn/open-apis/im/v1/messages",
          data: {
            receive_id: ctx.body.event.operator.operator_id.open_id,
            msg_type: 'interactive',
            content: "{\"header\":{\"template\":\"turquoise\",\"title\":{\"content\":\"创建短链接\",\"tag\":\"plain_text\"}},\"elements\":[{\"tag\":\"form\",\"name\":\"form_1\",\"elements\":[{\"tag\":\"input\",\"name\":\"postfix\",\"placeholder\":{\"tag\":\"plain_text\",\"content\":\"请输入后缀\"},\"max_length\":10,\"label\":{\"tag\":\"plain_text\",\"content\":\"请输入后缀:\"},\"label_position\":\"left\",\"value\":{\"k\":\"v\"}},{\"tag\":\"input\",\"name\":\"link\",\"placeholder\":{\"tag\":\"plain_text\",\"content\":\"请输入要跳转链接\"},\"label\":{\"tag\":\"plain_text\",\"content\":\"请输入要跳转链接:\"},\"label_position\":\"left\",\"value\":{\"k\":\"v\"}},{\"action_type\":\"form_submit\",\"name\":\"submit\",\"tag\":\"button\",\"text\":{\"content\":\"提交\",\"tag\":\"lark_md\"},\"type\":\"primary\",\"confirm\":{\"title\":{\"tag\":\"plain_text\",\"content\":\"创建短链接\"},\"text\":{\"tag\":\"plain_text\",\"content\":\"确认提交吗\"}}}]}]}",
          },
          params: {
            receive_id_type: 'open_id',
          },
        })
        return {};
      } catch (e) {
        console.log(`key: ${ctx.body.event.event_key}, user:${ctx.body.event.operator.operator_id.open_id},error`, e);
        return {};
      }
    }
  }
  return { data: 'hi, laf' }
}
Code language: JavaScript (javascript)