Vibe Coding 多人游戏(二十五)—— 部署与服务管理

deploy-scf.js

核心是一个由 AI 编写的 deploy-scf.js 脚本,纯 Node.js、零 npm 依赖。一句话触发部署全流程:

打包 → zip(带 svc/ 子目录) → 上传 SCF → 发布 → BDD 验证

脚本内部处理了之前踩过的 6 个坑(详见 P10):

1. undici@7 依赖冲突 → 去掉 functions-framework

Room-service 最开始用 @google-cloud/functions-framework 框架来承载 HTTP 函数,但这个框架的底层依赖 undici@7 在 Node 18 下行为不一致。Node 18 内置的是 undici@5,Node 20 才包含 undici@7。SCF 的运行时是 Node 18,所以 undici@7 的 API 根本不存在。去掉 functions-framework 后一切正常。

2. zip 深度 → 加 svc/ 子目录

SCF 要求代码入口在 zip 的根目录或者 svc/ 子目录下。第一次部署时,我把整个 dist 目录压缩进去,结果 SCF 启动时找不到 app.js —— 因为它在 dist/svc/app.js 而 SCF 期望的是 ./app.js。后来在 zip 里加了一层 svc/ 目录结构。

3. 权限 → .NET ZipArchive 修复 +x

SCF 的 scf_bootstrap 文件需要可执行权限。但是在 Windows 上用 ZIP 工具打包时,文件的 Unix 权限位丢失了。SCF 下载 zip 后,scf_bootstrap 是 644(不可执行),导致容器启动时提示"Permission denied"。修复方式是在打包脚本中执行 .NET ZipArchive 设置权限位,或者在 Linux 环境重新设置 +x。

4. ESM 冲突 → 删 override type: module

项目在 package.json 里有 "type": "module" 配置(为了支持 ESM),但 scf_bootstrap 是用 CommonJS(require)启动的。Node.js 会尝试用 ESM 方式解析 CJS 文件,导致 require 不被识别为函数。修复:在 SCF 部署包的 package.json 中 override 去掉 type: module

5. Module._load → 直接注入 node_modules

Room-service 的 dist 代码有直接 require("meta3d-commonlib-new/...") 的调用,这些模块是本地 ReScript 编译产物不在 npm 上。最初用 Module._load hook 来拦截 require 调用,但不同 Node.js 版本的 hook API 签名不一致。最终方案:在构建 zip 时把所需模块的 .js 文件直接复制到 node_modules/ 下,zip 仅增加 3KB。

6. 定时器残留 → 代次守卫

这是最隐蔽的坑。SCF 的 warm container 会复用实例——前一个请求的 setInterval/setTimeout 在下一个请求进来时还跑着。如果新请求又创建了新的定时器,就会出现两个定时器同时运行的奇怪状态。

解决:使用"代次守卫"(generation guard)——每次请求开始 generation++,定时器回调里检查 generation 是否匹配,不匹配就跳过。这个模式后来被应用到很多地方,包括倒计时超时逻辑和广播节流。


双环境双实例

环境 URL 参数 用途
production 正式游戏
test ?isDebug=true AI 开发测试

双环境是怎么来的?一开始只有 production,但 AI 在开发时直接部署到 production——结果兄弟用着正式版突然代码变了,产生了一次"版本混乱"的事故。后来加了一个 test 环境,AI 的自动部署只到 test,兄弟手动确认后再部署到 production。

每个环境独立部署 room1 + room2 两个实例,互不影响。room1 和 room2 做负载均衡,也提供了容灾——一个实例挂了另一个还在。生产环境每 2 秒心跳保活,避免 warm container 回收。


服务管理

本地服务端口

服务 端口 协议 启动命令
room-service 4003 WebSocket yarn dev (tsrpc-cli dev)
match-service 3000 HTTP yarn dev (tsrpc-cli dev)
webpack-dev-server 8093 HTTP 开发用 yarn webpack:dev-server

重启顺序

最重要的一条规则:先 room,再 match。 room 重启会断开 match 的 WS 连接,如果先重启 match,room 的 WS 连接找不到 match,所有与 match 相关的操作(匹配、房间路由)都会失败。

具体的重启流程由 gts-service skill 管理:

gts-service restart room1
→ 等待 room1 ready(health check 200)
gts-service restart room2
→ 等待 room2 ready
gts-service restart match
→ 等待 match ready

测试/E2E 前后

  • 测试后 → 重启服务端清除脏数据
  • E2E 测试前必须先重启 room-service + match-service(避免 WS 失连卡在"查找房间中")

有一次我跑 E2E 测试,玩家卡在"查找房间中"转了 10 分钟。排查发现是上一次 E2E 测试的 WS 连接没完全断开,服务端认为玩家还在游戏中——每一次新建房间都给了同一个"already in game"的玩家。从那以后,E2E 前重启服务端成了硬性规定。


SCF 配置

函数配置

参数 room1 / room2 match1 说明
函数类型 Web 函数 Web 函数 支持 WebSocket 协议(控制台需勾选「WebSocket 支持」)
运行时 Node.js 18 Node.js 18 SCF 最高支持到 Node 18
端口 9000 9000 scf_bootstrap 中监听 9000 端口
内存 512MB 256MB room 需要维持 WS+游戏状态,256MB 时 Full GC 频繁
超时 900s(15 分钟) 60s room 是长连接,match 是短请求
自定义静态并发 10 10 设为 >4 以容纳 3 个客户端 + 匹配服务的连接
WebSocket 空闲超时 3600s(1 小时) 不适用 1 小时后主动断开空闲连接
公网访问 开启 开启 函数 URL 配置中开启公网访问
日志投递 CLS(默认) CLS(默认) 日志投递到腾讯云日志服务,用于 gts-logs 查询
InstallDependency TRUE(配置)/ FALSE(部署时) 同上 首次部署时开启,后续依赖不变可关闭

三个函数独立部署、独立计费:

函数名 实际作用 冷启动时间
room1 处理 2 个玩家的 WebSocket 连接、游戏状态、碰撞检测 ~3-5s
room2 同上,负载均衡副本 ~3-5s
match1 房间路由、匹配队列、心跳保活 ~1-2s

scf_bootstrap 入口文件

每个 SCF 函数需要一个 scf_bootstrap 入口文件,内容示例:

#!/bin/bash
node app.js

三个关键要求:

  • Unix 格式(LF):Windows CRLF 会导致 SCF 找不到文件
  • 可执行权限:zip 中必须是 755(+x),Windows 打包会丢失 Unix 权限位
  • 端口 9000:SCF Web 函数默认监听 9000 端口,代码中的 HTTP 服务器必须绑定 process.env.PORT || 9000

为什么只能连 2 个人?

这个问题经常被问到,根源不在代码,在腾讯云 SCF 的调度模型。

核心原因:SCF Web 函数的 WebSocket 模式下,每个 WebSocket 连接默认独占一个函数实例。

SCF 对 Web 函数(开启 WebSocket)使用基于会话的调度模式——每个 WebSocket 连接建立时触发一个新实例启动,该连接的生命周期绑定到这个实例。这意味着:

  • 第 1 个客户端连接 → 实例 A
  • 第 2 个客户端连接 → 实例 B(新的实例)
  • 第 3 个客户端连接 → 实例 C(又一个新实例)

不同实例之间内存不共享——实例 A 里的游戏状态(State.ts)在实例 B 里不存在。3 个玩家各连了不同的房间实例,他们"互相看不见"。

这不是免费套餐的限制,是 SCF Web 函数的设计机制。参考腾讯云官方文档的说明:

Web 函数的 WebSocket 支持中,每个 WebSocket 连接会独占一个函数实例。当第 3 个客户端连接时,系统会自动启动一个新的实例来处理该连接,因此前两个连接与第 3 个连接的数据不会共享。 — 腾讯云 SCF 文档 - WebSocket 支持

那为什么 room1 + room2 能分别接 2 个人(共 4 人),而不是 2+2=4 个实例?

因为我们做了两件事:

  1. 设置自定义静态并发为 10 — SCF 默认的静态并发是 1(每个实例一次只能处理 1 个请求),设为 10 后,一个实例可以同时处理多个请求(包括 2 个 WebSocket 连接 + 匹配服务的通信)。但 SCF 的并发策略对 WebSocket 仍然有限制——超过一定连接数(实测 2 个)后,新连接会被分配到新实例。

  2. 匹配服务不走 WebSocket 连接房间服务 — 匹配服务和房间服务之间的通信改为读写数据库,而不是 WebSocket 直连。因为 SCF 的 WebSocket 限制不仅影响客户端到房间,也影响服务端之间的 WebSocket 通信。

所以当前架构的实际容量是:每个 room 实例最多 2 个客户端 WS 连接,room1 + room2 共 4 人上限。为什么生产环境只写"支持 2 人"?因为 room1 和 room2 做负载均衡,一局游戏只在一个 room 里跑,所以一局游戏最多 2 人。

详细记录见:多人联网部署记录 - 关于 WebSocket 连接有 >=3 个客户端后实例不共享的问题

以后怎么改进多人支持?

短期方案(等腾讯云内测): 腾讯云提供了基于会话的并发模式(内测功能),开启后可以让多个 WebSocket 连接绑定到同一个实例。配置方式:

  • 在函数配置中选择"单实例并发模式"为「基于会话」
  • 设置会话 Key 来源(如 HTTP Header、Cookie)
  • 设置单实例最大并发会话数(默认 20)

但这个功能目前处于内测阶段,需要提交内测申请才能使用。如果申请通过,一个 room 实例理论上可以支持 20 个 WebSocket 连接。

中期方案(改状态管理): 把游戏状态从内存移到 Redis 或 TDS-C(腾讯云 Serverless 数据库)。这样即使不同实例也可以共享游戏状态——实例之间通过数据库同步,而不是靠绑定到同一个实例。这是更通用的方案,不依赖 SCF 的单实例会话调度。

长期方案(架构重构): 放弃 SCF,迁移到 TKE(Serverless Kubernetes)或 CVM。SCF 的 warm container 方案在长连接场景下有天然劣势——实例回收后玩家要重新连接。对于真正的多人实时游戏(王者荣耀级别),SCF 不够用。不过对于个人开发者的 2 人小游戏,SCF 的免费额度完全够用。

内存分配经验

room-service 的 512MB 不是随便定的。最开始用 256MB,打了几局后 GC 日志显示频繁的 Full GC——每次 GC 耗时 200ms+,导致游戏卡顿。换到 512MB 后 Full GC 几乎消失,Minor GC 稳定在 10ms 以内。

match-service 只需要 256MB,它的主要工作是房间路由和匹配队列,不存游戏状态。即使 2 个 room 同时 ping 过来,它的 CPU 使用率也不到 10%。


生产运维

日志管理

gts-logs skill 自动拉取 SCF 日志,按 room/match 过滤。这是排查 bug 的最主要手段——通过日志里的 "executeCommand"、"computeCollisionDamage"、"handlePlayerMove" 等输出,能直接定位到问题。

有一次排查碰撞减血不触发的 bug,就是通过 SCF 日志发现 animationName 始终为 "idle",然后顺藤摸瓜找到了根因:handlePlayerNotMove 无条件覆盖了 animationName。如果没有日志,这种时序问题几乎不可能定位到。

热修复

发现 bug → AI 改代码 → 秒级部署(不进控制台)。从看到 bug 到修复上线,最快的一次是 3 分钟——用 gts-deploy skill 一键覆盖。

版本管理

deploy-scf.js 支持版本号标注。每次部署自动打版本标签,可回退到稳定的旧版本。有一次新版本上线后,玩家反馈"角色不动了"——回退到旧版本花了 30 秒。排查后发现是 Movement.res 的某次重构改变了朝向计算的默认值。



附录 A:deploy-scf.js 完整代码

/**
 * 部署 zip 到腾讯云 SCF(云函数)
 * 使用原生 SCF API(TC3-HMAC-SHA256 签名),不依赖额外 SDK
 *
 * 用法:
 *   node deploy-scf.js room1     # 部署 room1
 *   node deploy-scf.js all       # 并行部署全部
 */

const os = require('os');
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');
const https = require('https');

// ---- 配置 ----
const SECRET_ID = 'AKIDdL16e8c2KOWccglputqiU8cO5fMYlhcM';
const SECRET_KEY = 'a1GJHNZntyxojls2Galt8FHSp5A1g8Ul';
const REGION = 'ap-shanghai';
const DESKTOP = path.join(os.homedir(), 'Desktop');

const SERVICES = [
{
name: 'room1',
zip: 'room-service-1.zip',
// SCF API 配置参数
// 注: ProtocolType/Type 是创建时参数,不可更新
// TraceEnable 通过 UpdateFunctionConfiguration 设置会报错,需在控制台手动开启
config: {
ProtocolParams: { WSParams: { IdleTimeOut: 3600 } },
InstanceConcurrencyConfig: { MaxConcurrency: 10, DynamicEnabled: 'FALSE' },
InstallDependency: 'TRUE',
Timeout: 900,
MemorySize: 512,
// Bug 1 修复: 设置 ROOM_ID=1 让 room-service 通过环境变量识别自己是 room1
Environment: { Variables: [{ Key: 'ROOM_ID', Value: '1' }] },
},
},
{
name: 'room2',
zip: 'room-service-2.zip',
config: {
ProtocolParams: { WSParams: { IdleTimeOut: 3600 } },
InstanceConcurrencyConfig: { MaxConcurrency: 10, DynamicEnabled: 'FALSE' },
InstallDependency: 'TRUE',
Timeout: 900,
MemorySize: 512,
// Bug 1 修复: 设置 ROOM_ID=2 让 room-service 通过环境变量识别自己是 room2
Environment: { Variables: [{ Key: 'ROOM_ID', Value: '2' }] },
},
},
{
name: 'match1',
zip: 'match-service.zip',
config: {
InstallDependency: 'TRUE',
Timeout: 60,
MemorySize: 256,
},
},
];

// ================================================================
// TC3-HMAC-SHA256 签名 (腾讯云 API V3)
// 参考: https://www.tencentcloud.com/document/product/1278/61860
// ================================================================

function sha256(message) {
return crypto.createHash('sha256').update(message).digest('hex');
}

function hmacSha256(key, message) {
return crypto.createHmac('sha256', key).update(message).digest();
}

/**

  • 生成腾讯云 API v3 签名头
    */
    function sign(action, payload, timestamp) {
    const date = new Date(timestamp * 1000);
    const dateStr = date.toISOString().slice(0, 10); // YYYY-MM-DD(CredentialScope 和 HMAC 都用这个格式)
    const service = 'scf';

    // 1. CanonicalRequest
    const httpMethod = 'POST';
    const canonicalUri = '/';
    const canonicalQuery = '';
    const contentType = 'application/json; charset=utf-8';
    const canonicalHeaders =
    'content-type:' + contentType + '\n' +
    'host:scf.tencentcloudapi.com\n';
    const signedHeaders = 'content-type;host';
    const hashedRequestPayload = sha256(payload);
    const canonicalRequest =
    httpMethod + '\n' +
    canonicalUri + '\n' +
    canonicalQuery + '\n' +
    canonicalHeaders + '\n' +
    signedHeaders + '\n' +
    hashedRequestPayload;

    // 2. StringToSign
    const algorithm = 'TC3-HMAC-SHA256';
    const credentialScope = dateStr + '/' + service + '/tc3_request';
    const stringToSign =
    algorithm + '\n' +
    timestamp + '\n' +
    credentialScope + '\n' +
    sha256(canonicalRequest);

    // 3. Signing key
    const secretDate = hmacSha256('TC3' + SECRET_KEY, dateStr);
    const secretService = hmacSha256(secretDate, service);
    const secretSigning = hmacSha256(secretService, 'tc3_request');
    const signature = hmacSha256(secretSigning, stringToSign).toString('hex');

    // 4. Authorization header
    const authorization =
    algorithm + ' ' +
    'Credential=' + SECRET_ID + '/' + credentialScope + ', ' +
    'SignedHeaders=' + signedHeaders + ', ' +
    'Signature=' + signature;

    return authorization;
    }

/**

  • 调用 SCF API
    */
    function callScfApi(action, params) {
    return new Promise((resolve, reject) => {
    const timestamp = Math.floor(Date.now() / 1000);
    const payload = JSON.stringify(params);
    const authorization = sign(action, payload, timestamp);

    const options = {
    hostname: 'scf.tencentcloudapi.com',
    path: '/',
    method: 'POST',
    headers: {
    'Content-Type': 'application/json; charset=utf-8',
    'Host': 'scf.tencentcloudapi.com',
    'X-TC-Action': action,
    'X-TC-Version': '2018-04-16',
    'X-TC-Region': REGION,
    'X-TC-Timestamp': String(timestamp),
    'Authorization': authorization,
    'Content-Length': Buffer.byteLength(payload),
    },
    };

    const req = https.request(options, (res) => {
    let body = '';
    res.on('data', (chunk) => (body += chunk));
    res.on('end', () => {
    try {
    const parsed = JSON.parse(body);
    if (parsed.Response && parsed.Response.Error) {
    reject(new Error(parsed.Response.Error.Message));
    } else {
    resolve(parsed.Response);
    }
    } catch (e) {
    reject(new Error('Parse response failed: ' + body.slice(0, 200)));
    }
    });
    });

    req.on('error', reject);
    req.write(payload);
    req.end();
    });
    }

/**

  • 部署一个函数
    */
    async function deployOne(svc) {
    const zipPath = path.join(DESKTOP, svc.zip);
    if (!fs.existsSync(zipPath)) {
    throw new Error('zip not found: ' + zipPath);
    }

    const zipBuffer = fs.readFileSync(zipPath);
    const base64Code = zipBuffer.toString('base64');

    process.stdout.write(' [1/3] uploading code... ');
    // InstallDependency: FALSE — node_modules 已打包进 zip,不需要 SCF 安装
    // 如果以后手动更新函数时可能需要安装依赖,保留配置中的 InstallDependency: 'TRUE' 作为默认配置
    await callScfApi('UpdateFunctionCode', {
    FunctionName: svc.name,
    Code: { ZipFile: base64Code },
    InstallDependency: 'FALSE',
    });
    console.log('OK');

    process.stdout.write(' [2/3] waiting for activation... ');
    // 轮询直到函数状态 active(npm install 可能需 2-3 分钟,给 240s)
    for (let i = 0; i < 120; i++) {
    await sleep(2000);
    const res = await callScfApi('GetFunction', { FunctionName: svc.name });
    if (res.Status === 'Active') {
    console.log('OK');
    break;
    }
    if (i === 119) console.log('(timeout, continuing)');
    }

    process.stdout.write(' [3/3] configuring... ');
    // 逐项配置,每项之间等待函数 Active
    // fix-batchC: 收集配置失败项,最后累计报错
    let configFailures = [];
    const configEntries = Object.entries(svc.config);
    for (let i = 0; i < configEntries.length; i++) {
    const [key, value] = configEntries[i];
    if (i > 0) {
    // 等待上一项配置生效
    for (let j = 0; j < 30; j++) {
    await sleep(2000);
    const res = await callScfApi('GetFunction', { FunctionName: svc.name });
    if (res.Status === 'Active') break;
    }
    }
    try {
    await callScfApi('UpdateFunctionConfiguration', {
    FunctionName: svc.name,
    [key]: value,
    });
    process.stdout.write(key + ' ');
    } catch (e) {
    // fix-batchC: 不静默跳过,记录失败项
    process.stdout.write(key + '(FAIL:' + e.message + ') ');
    configFailures.push({ key, error: e.message });
    }
    }
    console.log('');

    // fix-batchC: 如果有配置失败,输出汇总
    if (configFailures.length > 0) {
    console.error(' ⚠ ' + svc.name + ' config failures: ' + configFailures.map(f => f.key).join(', '));
    }
    }

function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

// ---- main ----
async function main() {
const target = process.argv[2];
const targets = target === 'all' ? SERVICES : SERVICES.filter((s) => s.name === target);

if (targets.length === 0) {
console.error('Usage: node deploy-scf.js <room1|room2|match1|all>');
process.exit(1);
}

console.log('Deploying ' + targets.length + ' function(s): ' + targets.map((s) => s.name).join(', '));

// 并行部署:SCF 是独立函数,各自上传+激活+配置,互不影响
let ok = 0,
fail = 0;
const results = await Promise.all(targets.map(async (svc) => {
console.log('');
console.log('--- ' + svc.name + ' ---');
try {
await deployOne(svc);
return true;
} catch (e) {
console.error(' ✗ ' + svc.name + ': ' + e.message);
return false;
}
}));
ok = results.filter(Boolean).length;
fail = results.filter((r) => !r).length;

console.log('');
console.log('Done: ' + ok + ' OK, ' + fail + ' failed');
// fix-batchC: 全局失败时 exit 1
if (fail > 0) {
process.exit(1);
}
}

main().catch((e) => {
console.error('Fatal:', e.message);
process.exit(1);
});


附录 B:gts-logs Skill

---
name: "gts-logs"
description: "抓取并分析GTS-Play SCF服务端日志(room1/room2/match1)"
---

gts-logs — 抓取并分析 SCF 服务端日志

触发词

  • 看日志
  • 查日志
  • 日志
  • logs

前置条件

  • 工作目录:&lt;project&gt;/packages\meta3d-platform-publish
  • 日志来源:腾讯云 CLS(日志服务),通过 logs-scf.js 脚本查询
  • 三个服务共用同一个 CLS 日志主题:806996fb-c4fc-4de3-8fc6-41c0cdab83f2

流程

Step 1: 问兄弟看哪个服务的日志

> 看哪个服务的日志?room1 / room2 / match1

可加参数:

  • --limit N:返回条数(默认 20)
  • --hours N:最近 N 小时(默认 1)

示例:看 room1 最近2小时的50条日志

Step 2: 执行日志抓取

# room1
yarn logs_room1           # 默认 20 条 × 最近 1 小时

# 带参数(通过 gulp task 不支持直接传参,改用直接调脚本)
node scripts/logs-scf.js room1 --limit 50 --hours 2
</code></pre>
<h3>Step 3: 分析日志内容</h3>
<p>抓取后自动分析以下内容:</p>
<table>
<thead>
<tr>
<th>分析项</th>
<th>说明</th>
</tr>
</thead>
<tbody><tr>
<td>错误(Error/ERR/Exception)</td>
<td>代码执行错误,标注行数 + 错误类型</td>
</tr>
<tr>
<td>警告(Warning/WARN)</td>
<td>潜在问题的警告信息</td>
</tr>
<tr>
<td>模块加载失败</td>
<td><code>Cannot find module</code>、<code>ERR_REQUIRE_ESM</code> 等</td>
</tr>
<tr>
<td>连接异常</td>
<td>WebSocket 断开、超时等</td>
</tr>
<tr>
<td>崩溃重启</td>
<td><code>exit</code>、<code>OOM</code>、<code>timeout</code> 等</td>
</tr>
</tbody></table>
<h3>Step 4: 输出分析报告</h3>
<p><strong>不贴原始日志</strong>,只给分析结果:</p>
<pre><code>[room1 日志分析] 最近1小时 × 20条

⚠️ 警告: 2 条
  - &quot;WebSocket idle timeout&quot; × 2(正常行为,空闲连接超时断开)

✅ 正常: 18 条
  - 连接建立/断开、心跳、消息处理
</code></pre>
<p>如有错误/异常,附上处理方案:</p>
<ul>
<li><code>Cannot find module</code> → 检查 zip 是否缺少 node_modules,重新打包部署</li>
<li><code>ERR_REQUIRE_ESM</code> → 检查 <code>@rescript/runtime</code> 的 package.json 是否被复制进 zip</li>
<li><code>WebSocket timeout</code> → 正常行为,非错误</li>
<li><code>OOM / timeout</code> → 考虑增大 SCF 内存或超时配置</li>
</ul>
<h3>Step 5: 通知兄弟</h3>
<ul>
<li><strong>双通道通知</strong>:桌面消息 + 飞书通知(≤10字)</li>
<li>告知日志分析结论,如有问题问兄弟是否处理</li>
</ul>
<h2>注意事项</h2>
<ul>
<li>日志通过 CLS <code>SearchLog</code> API 查询,端点 <code>cls.tencentcloudapi.com</code></li>
<li>不支持查看实时流式日志(SCF Web 函数不走 <code>GetFunctionLogs</code> API)</li>
<li>日志可能有几分钟延迟(CLS 投递延迟)</li>
<li>如查询返回空结果,尝试加大 <code>--hours</code> 参数</li>
</ul>
<h2>参考</h2>
<ul>
<li>日志脚本:<code>packages/meta3d-platform-publish/scripts/logs-scf.js</code></li>
<li>CLS 日志主题 ID: <code>806996fb-c4fc-4de3-8fc6-41c0cdab83f2</code></li>
<li>日志集 ID: <code>f55dcb46-e178-4ecb-8443-3ad42d323040</code></li>
</ul>
<pre><code>
---
下期讲 **P26:OpenClaw 工具链全景**——16 个 Skill 全家桶。

**下一篇:[Vibe Coding 多人游戏(二十六)—— OpenClaw 工具链全景](https://www.cnblogs.com/chaogex/p/21195307)**
</code></pre>
posted @ 2026-07-08 11:38  杨元超  阅读(10)  评论(0)    收藏  举报