《独立开发者的商业指南·合同、应收与风险》7.2 开源许可证的商业边界

开源许可证不是「能不能用」的问题,而是「能不能闭源分发、要不要公开修改」的问题。本节把 MIT/Apache、LGPL、GPL/AGPL 放进一张判定表,用 LedgerLite 的 $240,240 ARR 算出 AGPL 触发网络服务时的合规成本 $85,672,并与 $20,400 的重写成本对照。

以下情境为教学假设,人物与数字不代表经核实的真实案例。

开源许可证经常被当成道德选择:要么支持开源,要么反对开源。对做生意的人,它更像一道成本选择题:这段代码能不能闭源分发、修改后要不要公开、对外提供服务时要不要把源码交给用户。三个问题的答案不同,商业边界就完全不同。

Tidecast 和 LedgerLite 的处境正好相反。Tidecast 分发的是游戏客户端,GPL 义务跟着「分发」触发;LedgerLite 提供的是订阅服务,GPL 不触发,但 AGPL 会。同样是「用了开源代码」,一个在发货时触发,一个在提供服务时触发;触发点不同,合规成本和替代方案也不同。

这一节只做商业口径的判断,不提供法律意见。许可证条款的最终解释由适用法律和具体版本决定;文中「通常理解为」指的是行业常见的商业读法,不是唯一法律结论。所有金额都是教学假设,用来比较路径,不是报价。

三档边界:宽松、弱著佐权、强著佐权

档位代表许可证闭源分发修改后公开网络服务触发商业含义
宽松MIT、Apache-2.0可以不要求不触发保留声明即可,最适合闭源产品
弱著佐权LGPL有条件可以修改库本身要公开不触发应用可闭源,库要保持可替换
强著佐权GPL不可以分发时要公开不触发分发组合作品就要交出源码
强著佐权AGPL不可以分发时要公开触发对外提供服务也要交出源码

宽松许可证的商业边界最清楚:可以闭源分发,义务主要是保留版权声明和许可文本。Apache-2.0 额外包含专利授权和 NOTICE 要求,对担心专利风险的团队更友好;MIT 更短,义务更少。两者都不会因为你把代码放进闭源产品而要求公开源码。

弱著佐权的边界在「库」和「应用」之间。LGPL 通常理解为:应用可以闭源,但 LGPL 库本身要保持可替换;如果修改了库,修改部分要公开。动态链接、库未修改时,闭源分发通常可行;静态链接或修改库,就要提供库的源码和可重链接的目标文件。这里的「通常」很重要,链接方式、语言运行时和分发形式都会改变判断。

强著佐权的边界在「分发」。GPL 通常理解为:分发包含 GPL 代码的组合作品时,要向接收者提供对应源码;内部使用和纯 SaaS 不触发,因为没有「分发」这个动作。AGPL 补上了这个缺口:即使不分发,只要修改后的程序被用来向网络用户提供服务,也要向这些用户提供源码。GPL 和 AGPL 的差别,不是「开源程度」,而是「触发点」。

判定表:用了什么许可证,能不能闭源

许可证内部使用对外提供 SaaS闭源分发修改后分发
MIT / Apache-2.0可闭源可闭源可以可以,保留声明
LGPL可闭源可闭源可以,动态链接且库未修改应用可闭源,库修改须公开
GPL可闭源可闭源不可以不可以,须提供源码
AGPL可闭源不可以,须向用户提供源码不可以不可以,须提供源码

这张表回答三个问题。第一,能不能闭源分发:MIT/Apache 可以,LGPL 有条件可以,GPL/AGPL 不可以。第二,要不要公开修改:宽松许可证不要求,弱著佐权只要求公开库的修改,强著佐权要求公开整个组合作品。第三,网络服务是否触发:只有 AGPL 把「提供服务」当成分发。

三个问题里,最容易被低估的是「修改后分发」。很多团队以为只要不改许可证文件就没事,但 GPL/AGPL 的义务跟着组合作品走;你把 GPL 库链接进闭源程序再发货,义务不会因为库是「原样使用」而消失。反过来,LGPL 允许应用闭源,但前提是库保持可替换;如果静态链接把库焊死,可替换性就没了。

还有一个常见误区:把「内部使用」当成永久安全。内部使用不触发分发义务,但一旦把内部工具交给客户、放进客户环境、或者随产品一起交付,分发就发生了。判断触发点时,看的是代码有没有离开你的控制范围,而不是看它有没有被修改。

AGPL 的合规成本:公开源码值多少钱

AGPL 的成本不是许可证费,而是「公开源码之后少赚的钱」加「持续公开的工程成本」。用 LedgerLite 做算例:年收入 $240,240,假设核心服务里有一段 AGPL 组件,向用户提供服务就要提供对应源码。教学假设下,公开源码导致 10% 的收入流失,持续 3 年;工程侧投入 160 小时,按 $85/小时计。

项目金额口径
一次性工程成本$13,600160 小时 × $85
年度竞争损失$24,02410% × $240,240
三年竞争损失$72,072$24,024 × 3
合规总成本$85,672$13,600 + $72,072
占一年 ARR35.7%$85,672 ÷ $240,240

$85,672 是什么量级?LedgerLite 的 SDE 是 $130,240,合规成本相当于 65.8% 的 SDE;它不是一笔法务预算,而是一年多的可支配现金。这个数字还没有算持续维护:源码公开后,每次版本更新都要同步、审查、处理外部贡献,工程成本会从一次性变成经常性。

替代方案也要摆在同一张桌上。重写一个功能等价的组件,教学假设需要 240 小时,按 $85/小时是 $20,400;购买商业许可的教学假设报价是 $30,000。三条路径的差距很大:

路径成本说明
继续用 AGPL,合规$85,672工程 $13,600 + 三年竞争损失 $72,072
重写替代组件$20,400240 小时 × $85,一次性为主
购买商业许可$30,000教学假设报价,可能含年度支持
合规与重写的差额$65,272$85,672 − $20,400,相当于 27.2% 的 ARR

商业判断因此很直接:如果合规成本高于重写或商业许可,就换路径;如果替换会破坏核心功能、或者重写周期长到影响收入,合规可能是更便宜的选择。关键不是「开源好不好」,而是把三条路径放在同一时间口径上比较。上表的竞争损失按三年算,重写成本按一次性算;如果重写后还要持续维护,两者差距会缩小。

这里必须加一句边界:AGPL 是否触发、触发后要提供哪些源码,取决于具体版本、修改方式和提供服务的形式。本节用的是商业口径的保守估算,不是法律结论;真正签约或上线前,应让专业人士按实际架构判断。

Tidecast 的分发场景:客户端与服务端分开看

Tidecast 分发的是游戏客户端,触发点是「发货」。如果客户端里链接了 GPL 代码,分发游戏就要提供组合作品的源码;如果用的是 LGPL 库,动态链接、库未修改时可以保持游戏闭源,但要允许用户替换库。对买断制游戏,GPL 的源码义务会直接冲击闭源发行,LGPL 通常是更可接受的折中。

AGPL 在客户端上的触发点仍然是「分发」:把 AGPL 代码打进游戏客户端并发货,就要向玩家提供源码;玩家连接服务器这件事本身不额外触发网络条款。网络条款真正单独命中的是服务端——如果 Tidecast 的云存档或排行榜服务运行了修改后的 AGPL 代码,并向玩家提供该服务,就要向这些用户提供源码。云存档和排行榜在 Tidecast 的收入结构里是成本中心,竞争损失有限,但工程与持续公开的成本仍要计入。

LedgerLite 则是直接撞上 AGPL 的案例:订阅服务本身就是「向网络用户提供服务」,没有「不分发就没事」的空间。上面算出的 $85,672 合规成本,对应的就是这种结构;如果核心组件不能重写、商业许可也拿不到,这笔成本只能接受,或者重新设计架构把它隔离出去。

代码:许可证边界判定

下面的 oss.ts 把三档边界固化成判定函数。decideBoundary 接收许可证、使用方式和 LGPL 链接方式,返回能否闭源、是否必须公开修改、是否必须向服务用户提供源码;agplComplianceCost 把工程成本与竞争损失加总,并折算成一年 ARR 的占比。

// file: src/contract/oss.ts
// tier: L1
// verify: node --test test/contract/oss.test.ts

import { type Cents } from "../core/money.ts";
import { type Bps, applyRate, divide } from "../core/rate.ts";
import { type Result, fail, ok } from "../core/result.ts";

export type LicenseId =
  | "mit"
  | "apache_2"
  | "lgpl"
  | "gpl"
  | "agpl"
  | "proprietary";
export type LicenseTier =
  | "permissive"
  | "weak_copyleft"
  | "strong_copyleft"
  | "proprietary";
export type UseMode =
  | "internal"
  | "saas"
  | "distribute"
  | "distribute_modified";
export type LgplVariant = "separate" | "modified_or_static";

export type LicenseTerm = {
  id: LicenseId;
  label: string;
  tier: LicenseTier;
};

export const LICENSE_TERMS: Record<LicenseId, LicenseTerm> = {
  mit: { id: "mit", label: "MIT", tier: "permissive" },
  apache_2: { id: "apache_2", label: "Apache-2.0", tier: "permissive" },
  lgpl: { id: "lgpl", label: "LGPL", tier: "weak_copyleft" },
  gpl: { id: "gpl", label: "GPL", tier: "strong_copyleft" },
  agpl: { id: "agpl", label: "AGPL", tier: "strong_copyleft" },
  proprietary: {
    id: "proprietary",
    label: "专有许可",
    tier: "proprietary",
  },
};

export const LICENSE_IDS = Object.keys(LICENSE_TERMS) as LicenseId[];

export type BoundaryDecision = {
  license: LicenseId;
  label: string;
  tier: LicenseTier;
  mode: UseMode;
  canCloseSource: boolean;
  mustPublishChanges: boolean;
  mustOfferServiceSource: boolean;
  noticeRequired: boolean;
  note: string;
};

function isUseMode(value: string): value is UseMode {
  return (
    value === "internal" ||
    value === "saas" ||
    value === "distribute" ||
    value === "distribute_modified"
  );
}

/**
 * Commercial reading, not a legal opinion. "Distribute" means the licensed
 * code becomes part of a combined work shipped to users. Using a component as
 * a separate process can move the boundary; that separation has to be real.
 */
export function decideBoundary(
  license: LicenseId,
  mode: UseMode,
  lgplVariant: LgplVariant = "separate",
): Result<BoundaryDecision> {
  if (!Object.hasOwn(LICENSE_TERMS, license)) {
    return fail(`unknown license: ${license}`);
  }
  if (!isUseMode(mode)) {
    return fail(`unknown use mode: ${mode}`);
  }

  const term = LICENSE_TERMS[license];
  let canCloseSource = true;
  let mustPublishChanges = false;
  let mustOfferServiceSource = false;
  let note = "";

  if (license === "mit" || license === "apache_2") {
    note =
      license === "apache_2"
        ? "保留版权与 NOTICE,附带的专利授权随分发生效"
        : "保留版权与许可声明,可闭源分发";
  } else if (license === "lgpl") {
    if (mode === "distribute" || mode === "distribute_modified") {
      if (lgplVariant === "modified_or_static") {
        mustPublishChanges = true;
        note =
          "应用层可闭源;LGPL 库及其修改须提供源码与可重链接目标文件";
      } else {
        note = "动态链接且库未修改时可闭源,须允许用户替换该库";
      }
    } else {
      note = "内部使用或网络服务不触发 LGPL 的分发义务";
    }
  } else if (license === "gpl") {
    if (mode === "distribute" || mode === "distribute_modified") {
      canCloseSource = false;
      mustPublishChanges = true;
      note = "分发组合作品须向接收者提供对应源码";
    } else {
      note = "内部使用或网络服务不触发 GPL 的分发义务";
    }
  } else if (license === "agpl") {
    if (
      mode === "saas" ||
      mode === "distribute" ||
      mode === "distribute_modified"
    ) {
      canCloseSource = false;
      mustPublishChanges = true;
      mustOfferServiceSource = mode === "saas";
      note =
        mode === "saas"
          ? "向网络用户提供服务即须提供对应源码"
          : "分发与网络服务均触发源码义务";
    } else {
      note = "仅内部使用不触发 AGPL 的源码义务";
    }
  } else {
    note = "自有代码可按专有条款分发;第三方专有组件以授权合同为准";
  }

  return ok({
    license,
    label: term.label,
    tier: term.tier,
    mode,
    canCloseSource,
    mustPublishChanges,
    mustOfferServiceSource,
    noticeRequired: license !== "proprietary",
    note,
  });
}

export type AgplCostInput = {
  annualRevenueCents: Cents;
  years: number;
  engineeringHours: number;
  hourlyRateCents: Cents;
  revenueLossBps: Bps;
};

export type AgplCost = {
  engineeringCents: Cents;
  lostRevenueCents: Cents;
  totalCents: Cents;
  revenueShareBps: Bps;
};

/**
 * The AGPL network trigger has two costs: one-time engineering to publish and
 * keep publishing source, and the revenue exposed when competitors can copy the
 * service layer. Both are teaching assumptions.
 */
export function agplComplianceCost(input: AgplCostInput): Result<AgplCost> {
  if (
    !Number.isInteger(input.annualRevenueCents) ||
    input.annualRevenueCents <= 0
  ) {
    return fail("annual revenue must be a positive integer amount in cents");
  }
  if (!Number.isInteger(input.years) || input.years <= 0 || input.years > 10) {
    return fail("years must be an integer between 1 and 10");
  }
  if (
    !Number.isInteger(input.engineeringHours) ||
    input.engineeringHours < 0
  ) {
    return fail("engineering hours must be a non-negative integer");
  }
  if (
    !Number.isInteger(input.hourlyRateCents) ||
    input.hourlyRateCents < 0
  ) {
    return fail("hourly rate must be a non-negative integer amount in cents");
  }
  if (
    !Number.isInteger(input.revenueLossBps) ||
    input.revenueLossBps < 0 ||
    input.revenueLossBps > 10000
  ) {
    return fail("revenue loss must be between 0 and 10000 bps");
  }

  const engineeringCents = input.engineeringHours * input.hourlyRateCents;
  const lostRevenueCents =
    applyRate(input.annualRevenueCents, input.revenueLossBps) * input.years;
  const totalCents = engineeringCents + lostRevenueCents;

  return ok({
    engineeringCents,
    lostRevenueCents,
    totalCents,
    revenueShareBps: divide(totalCents, input.annualRevenueCents),
  });
}

代码里的判定是商业口径,不是法律意见。distribute 指许可证代码成为交付给用户的组合作品的一部分;如果只是作为独立进程或内部工具使用,边界可能不同,但这种隔离必须是真实的,不能只画在架构图上。LGPL 的 modified_or_static 分支把「应用可闭源、库修改须公开」拆成两个布尔值,是为了避免把「库的义务」误当成「整个应用的义务」。

LedgerLite 的参数跑出来是:工程成本 $13,600,三年竞争损失 $72,072,合规总成本 $85,672,占一年 ARR 35.7%。这组数字不包含持续维护,也不包含替换方案的迁移风险;它只回答「如果继续用 AGPL 并合规,第一刀切掉多少」。真实决策还要把重写周期、客户流失和架构隔离成本放进来。

小结

  • 开源许可证的商业边界由三个问题决定:能不能闭源分发、修改后要不要公开、网络服务是否触发。
  • 宽松许可证(MIT/Apache-2.0)可以闭源分发,主要义务是保留声明;Apache 额外有专利授权与 NOTICE 要求。
  • 弱著佐权(LGPL)允许应用闭源,但库要保持可替换;修改库或静态链接会带来源码与可重链接义务。
  • 强著佐权(GPL)在分发组合作品时要求提供源码;内部使用和纯 SaaS 通常不触发。
  • AGPL 把触发点从「分发」扩展到「向网络用户提供服务」;这是它和 GPL 的核心商业差别。
  • LedgerLite 的 AGPL 合规成本(教学假设)是 $85,672,相当于一年 ARR 的 35.7%、SDE 的 65.8%;重写替代组件是 $20,400,差额 $65,272。
  • 决策口径是把合规、重写、商业许可放在同一时间尺度上比较;许可证条款的最终解释不是本节能给出的结论。

阅读导航:上一节:7.1 商标、著作权、专利与商业秘密 · 下一节:7.3 IP 归属、转让与员工发明 。

常见错误与边界

  1. 把开源当成「免费」。 许可证费是零,合规成本不是零;AGPL 的竞争损失和持续公开成本可能远高于购买商业许可。
  2. 以为 GPL 也管 SaaS。 GPL 通常按分发触发,纯 SaaS 不触发;这正是 AGPL 出现的原因。
  3. 把 AGPL 当成「更严格的 GPL」。 两者的核心差别是触发点:AGPL 把网络提供服务纳入义务,不只是分发。
  4. 忽略 LGPL 的链接方式。 动态链接、库未修改时可以闭源;静态链接或修改库会带来库的源码与可重链接义务。
  5. 只看许可证文件,不看依赖树。 一个间接依赖的 GPL 组件也可能改变整个交付物的边界;依赖清单要包含许可证字段。
  6. 把「内部使用」当成永久安全。 代码一旦交付给客户、放进客户环境或随产品分发,分发就可能已经发生。
  7. 用法律结论的口吻做商业判断。 许可证解释取决于版本、修改方式和适用法律;本节用的是「通常理解为」的商业口径。
  8. 只算一次性工程成本。 公开源码后每次版本更新都要同步与审查;合规成本是经常性的,不是一次性。
  9. 忽略替换方案的迁移风险。 重写成本低不等于总成本低;重写周期、功能回退和客户流失都要计入。
  10. 把教学假设当成报价。 $85/小时、10% 流失、240 小时重写都是用于比较的假设,真实数字必须按项目和市场重新估算。

开源许可证不改变代码的质量,却会改变代码能不能变成闭源收入。判断顺序应该是:先看触发点,再看义务,最后算合规与替代的成本差。下一节讨论 IP 归属:即使许可证没问题,如果资产从一开始就不属于公司,后面的授权和出售仍然会卡住。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《移动游戏产品的架构·商业化与发行工程》附录 C 商业化与发行评审清单
  2. 《移动游戏产品的架构·商业化与发行工程》附录 B 双案例商业化事实表
  3. 《移动游戏产品的架构·商业化与发行工程》附录 A 商业化与发行术语速查