news 2026/10/27 10:12:53

ThinkRail 0.2.0 深度解析:以「Review turn」为中心的回合回执、MCP 审批与工作区生命周期

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ThinkRail 0.2.0 深度解析:以「Review turn」为中心的回合回执、MCP 审批与工作区生命周期

【免费下载链接】thinkrail

Vibe code with pi in a lightweight, real IDE that customises itself around the way you work — The Vibe You Need

项目地址:https://gitcode.com/GitHub_Trending/th/thinkrail
点击查看免费下载

ThinkRail 0.2.0 的核心变化不是"又加了几个面板",而是把 agent 的一整轮工作变成可复核、可撤销的对象:宿主在运行前后各拍一次 worktree 快照,生成一份不依赖 agent 自述的回合回执(turn receipt),再由Review turn一次性展开这一轮改动的全部文件。本文以 0.2.0 发布说明为主线,逐条结合仓库中的实现与测试,讲清每个能力的边界、参数与使用方式,包括 MCP 按调用审批、Cmd/Ctrl+K 会话切换、TODO 侧栏、Settled 搁架与新版外观。

版本要点速览

0.2.0 的公开变更可以归成六组,后文按组展开:

能力一句话说明主要落点
回合回执agent 运行前后各拍一次 worktree 快照,差值即本回合改动packages/server/src/turns/turns.ts
Review turn把该回合的改动作为独立 diff 作用域一次性打开apps/web/src/chat/turns.tsx、packages/server/src/git/SPEC.md
hunk 级审查Keep / Revert / Ask agent,进度按 hunk 与文件同一模型推进apps/web/src/panels/SPEC.md
MCP 服务器由 pi 内置引擎运行,设置只存引用,仓库自带配置需逐条批准packages/server/src/agent/mcp/SPEC.md
Cmd/Ctrl+K只列出需要你或仍在运行的会话,按项目分组、需要输入优先apps/web/src/store/selectSwitcherGroups.ts
Settled 搁架PR 合并/关闭或空闲 7 天后自动归档,可手工覆盖packages/server/src/host/settledLifecycle.ts

每一轮都有一份回执

发布说明里对痛点的描述很直接:agent 说"做完了",人却要翻整段对话去拼它改了什么——那不是 review,是考古。0.2.0 的解法是给每一轮运行配一张回执,关键约束是数字不来自 agent 自己的工具调用。

快照如何取得,且为何不碰你的 Git 状态

回执由TurnTracker生成(packages/server/src/turns/turns.ts):

  • 收到agent_start事件时,对该会话所在 worktree 调用snapshotWorktree(),把结果 promise 存进PendingRun.baseTree;
  • 收到agent_settled时再拍一次headTree,两次相等(或任一为null)则直接返回,不产生回执;
  • 两者不同才调用gitStatus()计算本轮变更集,写入turns.json并广播TurnChangeSet。

snapshotWorktree的实现要点在packages/server/src/git/SPEC.md:它把索引写进GIT_INDEX_FILE指向的临时副本,再取工作区树 oid,因此真实的 index、HEAD和 reflog 都不会移动;已跟踪文件的修改与未跟踪文件都算(被.gitignore忽略的不算)。它刻意在失败时返回null而不是抛错——"漏一次快照只损失一张回执",不能让快照失败影响 agent 运行。

这解释了发布说明里那句容易读漏的话:一次 shell 命令改写文件、一个 formatter、一次重命名,都会算进回执,因为它们改变的是工作区内容,而不是工具调用的记账。

回执的存储与合并

回合 id 由${sessionId}:${startedAt}组成;每个工作区只保留最近 30 条(TURNS_PER_WORKSPACE),按startedAt排序后截断。写入前会再次确认该工作区仍然存在,避免已删除的工作区留下悬空记录。

前端并不直接展示单条记录:matchTurnReceipt()(apps/web/src/chat/rows.ts)按时间窗口把"同一轮对话"内的多次运行合并成一个作用域——baseTree取第一次运行、headTree取最后一次,id 在多次运行时写成first..last。合并时按路径去重(后者覆盖前者),并按路径排序,因此一回里 agent 因重试/多段执行产生的多次运行,最终仍是一份不重复的文件清单。

分隔线上真正显示了什么

TurnDivider(apps/web/src/chat/turns.tsx)读取的TurnDividerData含elapsedMs / toolCount / specs / changedFiles / receipt。当回执存在时,totals会累加本次统计到的每个文件的added/removed,文案模板是chat.divider.filesChangedWithTotals(packages/locales/en.json):

"N files changed · +A −B"

只要存在receipt且注入了onReviewTurn,分隔线上就会出现Review turn按钮(data-testid="turn-divider-review"),点击时把receipt.scope交给上层。也就是说,按钮携带的是一个已经固化的作用域对象,而不是"去 Changes 面板看看现在有什么"。

Review turn:一次读完一轮改动

turn作用域的定义

Review turn 打开的是GitDiffScope中的turn变体。按packages/server/src/git/SPEC.md的描述,resolveDiffRange()是"什么算 diff"的唯一实现,turn作用域对应:

git diff <baseTree> <headTree>

不包含未跟踪文件,两侧都是turns模块拍下的树快照;两个 oid 都会做形状校验并验证为^{tree},因此被 prune 掉的快照会以UNKNOWN_COMMIT拒绝,客户端据此把作用域重置,并展示本地化文案changes.review.turnGone("That agent turn's snapshot is no longer in this repository.")。e2e/changes-review-turn.spec.ts正好覆盖了这个场景:即使 Changes 工具没有挂载,Review turn 也能直接打开该回合的审查标签页。

从源码结构看,这个作用域的生命周期是"人工不可变"的:只要树对象还在仓库里,你回过头发起的审查看到的仍然是当时那一轮的内容,不会被后续 agent 的写入污染。

两种视图模式,共享一份进度

Changes 审查标签页有两个视图模式(apps/web/src/panels/SPEC.md):

  • Stacked:连续列表,所有文件垂直排列,一次滚动读完;
  • One file:一次只显示一个文件的上一页/下一页(Prev / Next)与n / N计数。

两者读取的是同一个模型——作用域的有序文件列表、viewed、activePath、折叠覆盖、Split | Inline、¶ 等状态——所以切换模式不会丢失已审进度。实现上也刻意拒绝"每种模式一个 tab kind",理由正是会丢进度。

在 Stacked 视图下,Markdown 仍按渲染后的文本呈现,代码走普通 diff;Next unreviewed(J,可回绕)跳到下一个未读文件,底部进度条显示n of N reviewed。

hunk 级分诊:Keep、Revert、Ask agent

在可变作用域(branch、uncommitted、pinned)里,每个 hunk 的工具条会多出Keep,与已有的 Revert、Ask agent 并列。这里的实现细节值得单独说明:

  • 被 Keep 的 hunk 记录在标签页的kept[path]下,键是内容键——hunk 的删除文本与新增文本的哈希,内容完全相同时还会带上"重复组大小 + 原始侧位置"。后果是:相邻 hunk 的 revert 导致行号漂移不影响判断,而 agent 再次改写同一 hunk 时该判断立即失效。
  • 保留一个文件的最后一个 hunk 会原子地把该文件标为 viewed;这是一个"动作边界"而非渲染规则,所以被显式取消 viewed 的文件不会因为虚拟化或切换模式又变回已读。
  • 每个 hunk 还带+A −B统计与Ask agent about this hunk:后者打开 worktree 侧的评论器,带上修改侧的lineRange、精确的diffHunk头,并把光标放在Please revise this change:之后;纯删除块锚定到工作区里前一条仍然存在的行(若在最顶部则锚定第 1 行),并写明它指向被删除的行。
  • 刻意没有 Revert all:宿主没有原子的多文件丢弃能力,一个批量破坏性操作只有在服务端具备该能力后才值得配一个快捷键。

发布说明提到的"粗糙边缘"在源码里都能对上号:单回合视图是只读的,Keep 只在分支 diff(All changes)上生效;超过 400 行的文件、lock 文件与生成文件默认折叠;作用域内文件数超过 50 时出现可关闭的提示,建议切到 One file——并且没有任何模式会自动切换,因为审查中途自己重排会让人失去位置。

回执式撤销:宿主拥有每一次写入

Keep 是一个纯客户端模型,而 Revert 是真正的写操作。0.2.0 的写入路径集中在packages/server/src/changes/changes.ts,其设计可以概括为:Git 和工作区是权威,客户端只描述"它看到了什么",宿主用自己的读取结果重新推导改动。

参数与作用域

export interface RevertChangeParams { workspaceId: string; path: string; scope: GitDiffScope; target: RevertTarget; // { kind: "range", original, modified } | { kind: "file" } expect: { originalHash: string | null; modifiedHash: string | null }; }

几个不可让步的规则:

  • 作用域只解析一次,且必须落到工作区:originalSide()通过resolveDiffRange解析范围,若modifiedRef !== null(即修改侧是某个 commit)则抛SCOPE_IMMUTABLE——那种 diff 描述的对象不在工作区里,没有可回退的东西。
  • 原始侧按resolvedOriginalOid读取,而不是按 ref 读取,避免"加载 → 校验 → 写入"这一趟里HEAD被提交挪动。
  • expect里的两个哈希是强制的,不是建议值。与本次读取算出的 sha-256 任一不符就是STALE_VIEW,一个字节都不写。因为 agent 不会为了一次回退而暂停,所以 CAS 就是全部保护。
  • 范围回退只对文本生效:字节型一侧,或工作区文件缺失,都是RANGE_INVALID;非文本走整文件回退。符号链接与"只有 mode 变化"的情况被拒绝为UNSUPPORTED_CHANGE,工作区身份用lstat读取,不会跟随链接。

整文件回退的三种情形

按两侧的实际内容判断(不是读 status 单词):

  1. 两侧都存在→ 写入原始字节(按字节写,所以二进制文件也被覆盖);
  2. 原始侧不存在(新增/未跟踪)→ 先把 CAS 校验过的工作区文件重命名成同目录下的隐藏 claim(.thinkrail-revert-<ulid>),再把 claim 移到系统回收站。重命名发生在同步校验阶段,这样后续 agent 重新创建的路径不会变成本次异步删除的目标;
  3. 工作区不存在(删除)→ 恢复原始字节,必要时创建缺失的父目录。

重命名文件按它的两半分别回退:新路径进回收站,旧路径恢复——因为这正是两个路径各自两侧所表达的事实。

回执与撤销

每次写操作都返回一个ChangeReceipt,其 id 同时就是撤销令牌。宿主机在工作区内存一个内存环形缓冲:

  • 上限为每个工作区 20 条回执、最多 64 MiB 字节,按最旧优先淘汰,但最新一条永远保留(哪怕它自己就超了字节预算),因为刚弹出来的 Undo toast 必须可用;
  • 每条记录保存该路径的before字节与 mode——恢复路径原状就是所有操作的逆操作,因此不需要额外的逆负载。被移到回收站的文件通过"把字节写回去"来撤销,而不是伸手进回收站(没有可移植的做法);
  • 回执只存在内存中,重启即失效;未知 id 抛RECEIPT_UNKNOWN,客户端把它当作"信息"(刷新、"已无法撤销")而非失败。

undoChange()做同样的 CAS:先用expect.modifiedHash校验当前文件,再在两侧都已知 mode 时校验after.mode——相同字节配一个新的可执行位,仍然是用户没看到的变更。撤销成功后消耗旧回执并产生一个undo回执,它本身也能再撤销一次,于是 redo 就是同一个操作而不是第二套机制。

在线协议层,这两个动作被包装在按工作区的变更锁里(packages/server/src/host/handlers.ts):

return withChangeLock(p.workspaceId, async () => ({ receipt: await revertChange(p) })); return withChangeLock(p.workspaceId, async () => ({ receipt: await undoChange(p) }));

客户端侧则在成功时弹出 8 秒 toast,其 Undo 会带上回执 id 与receipt.after.hash发起change.undo;遇到STALE_VIEW会重新加载该文件并提示"此文件已变,请重新查看 diff"。

MCP 服务器:能跑起来,也要能问清楚

引擎归 pi,宿主只管配置与守卫

ThinkRail 不再自建 MCP 客户端。按packages/server/src/agent/mcp/SPEC.md的边界划分:连接、OAuth、工具执行都留在 pi 的内置引擎里,ThinkRail 负责的是配置层(与 pi 同样地读取两份mcp.json、在多客户端长驻进程里安全地改写)、两个仅宿主需要的派生量(exposure 归一化、用于仓库服务器审批的逐条指纹),以及会话层的守卫扩展。仓库根目录的package.json把@earendil-works/pi-coding-agent固定在 catalog 的1.2.0,与发布说明里的 "pi 1.2.0" 对应。

五个预设与"只存引用"的写入规则

apps/web/src/panels/mcpPresets.ts里的预设是五个,均已核对并锁定版本(不使用@latest):

预设连接方式说明
Context7https://mcp.context7.com/mcp,exposure: "direct"无鉴权
DeepWikihttps://mcp.deepwiki.com/mcp,exposure: "direct"无鉴权,可询问任意公开 GitHub 仓库
Linearhttps://mcp.linear.app/mcp/readonly只读端点,添加后 OAuth 登录
Sentryhttps://mcp.sentry.dev/mcpOAuth;带modifiesData告知"该服务器可能修改数据"
Playwrightnpx -y @playwright/mcp@0.0.83,enabled: false以禁用的用户级条目添加,并对当前项目写 override(optInPerProject)

设置页的三种添加方式(预设、表单、粘贴 JSON)共享同一条硬规则:只存引用。按apps/web/src/panels/SPEC.md的描述,任何 env/header 值必须是${VAR}/$VAR模板或!command(pi 的语法),否则是校验错误并给出改写提示;URL 不允许携带 user info 或凭据命名的查询参数(pi 会原样发送 URL,所以正确做法是放进 header)。GitHub 保持为"配方"而不是预设,因为gh auth token的 scope 未经验证——配方填的是只读端点加Authorization: !t=$(gh auth token) && echo "Bearer $t",未登录时它会失败而不是发送空 token。

由于每个!command在每次聊天连接时都会在宿主同步执行,保存前会有一个复核步骤列出所有命令与!值,并提示保持其快速(例如读取缓存中的 token)。

仓库自带的.pi/mcp.json必须逐条批准

项目文件里的条目只有在项目记录的mcpApprovals[name]等于其当前指纹时才会被接纳。指纹的定义在config.ts:对名称 + 该条目原始 JSON 的规范化形式(递归排序键,toolExposure除外,因为其顺序有语义)做 SHA-256,且永不基于解析后的值。这样做的直接收益是:审批绑定的是"将要运行的东西",而不是某个运行期解析结果。

设置页的Review & approve会列出真正要跑的内容——宿主脱敏后的命令行或 URL、工作目录、每一个!command、env/header 的名字及其引用——再用渲染该行时的指纹去批准;若文件在这之后变了,宿主会拒绝,界面重新读取并保留原指纹,绝不自动重试已变更的内容。

四种回答与"记住这一次"的作用范围

会话层的守卫在sessionHost.ts:对每个mcp__*调用,若工具未声明readOnlyHint: true,就会在该调用自己的工具卡片上提问,并完整列出参数。选项顺序固定为:

Deny · Allow once · Allow in this chat · Always allow

顺序本身是设计约束:把"长期授权"放在 Deny 旁边,在手机上堆叠布局时容易误触。各自的语义边界很清楚:

  • Allow in this chat存在宿主对象里,能扛住资源重载,随会话结束而消失;
  • Always allow只在loaded()能识别该工具所属服务器时提供,调用grantTool(server, mcpTargetOf(config), toolName),且不写入本次会话的临时集合——因此在设置里撤销后,已打开的聊天在下一次调用时立刻受影响;
  • 授权记录存在项目记录中,而不是.pi/mcp.json,仓库无法把自己"预授权";
  • 授权是否仍有效,取决于target(生效条目的command + args或url的 SHA-256,按原样)是否仍然匹配:改 env、header 或 exposure 不影响,换命令或换 URL 则重新询问。

除 Deny 之外的一切回答——关闭对话框、用户直接用一条消息回复了问题、中止、没有 UI——都按拒绝处理并给出原因。

顺带一个通用改进:扩展对话框(权限门、确认、/mcp login)现在都在聊天内询问,不再以覆盖整个应用的模态框出现。

Cmd/Ctrl+K:只跳到正在等你的那个聊天

多个 agent 并行时,难点不是切换,而是发现谁在等你。Cmd/Ctrl+K打开的切换器只包含"需要你"或"仍在运行"的会话。

判定逻辑在apps/web/src/store/selectSwitcherGroups.ts,getSessionAttention()的优先级即排序键:

needs-input(0) → error(1) → unread(2) → running(3)
  • needsInput !== null即视为需要输入,排最前;
  • completionUnread && completion.outcome === "failed"记为error;
  • 其余未读完成记为unread;
  • isSessionWorking(state)记为running;
  • 四者都不满足的会话不会出现在列表里。

分组按projects的顺序输出,项目内会话按上面的优先级排序;needsAttention只要存在任一非running的会话即为真,用于在组标题上显示提示点。发布说明中"包含本窗口还没打开过的工作区"在useSessionSwitcher.ts里实现得很有分寸:对话框打开时,对未加载的项目补发workspace.list、对涉及的工作区补发session.list,前者刻意不受"已关闭"影响(路由激活依赖这份列表),后者才受取消标志约束。

计划搬到侧栏

TODO 计划从弹出的 popover 与 header chip 迁移到TODO List侧栏(apps/web/src/panels/PlanRailPane.tsx),与 Changes、Review 并列,作用域是当前聊天,分 Up next / Now / Done 三段。完整 Plan 页面仍是一次点击之遥,并内联展示每个已完成步骤的摘要与验证。

一个容易被忽略的细节是摘要的形状:计划摘要必须写成 PR 正文的三段式## Summary / ## Changes / ## Testing。这条约束写在packages/pi-todos/tools/planSummary.ts的工具描述里——它要求的是"整个计划的累计交接说明",跨所有任务与所有完成记录,若之前已有摘要则扩展而不是收窄到最后一步;## Changes只写迁移、范围漂移、依赖增删、重命名等计划级的横切说明,不复述步骤标题(自动生成的## Plan段已经列了)。因为形状即约定,宿主可以原样喂给renderPrBody,发布说明里的"PR skill 会用到它"由此落地。

结束的工作区自己靠边停

工作区列表过去只增不减。0.2.0 给每个项目加了一个Settled搁架,自动归档的触发条件有二:PR 合并或关闭,或空闲超过设定天数。

配置项与默认值

packages/contracts/src/domain.ts定义了取值范围与默认值:

export const SETTLE_IDLE_DAYS = { min: 1, max: 365, default: 7 } as const;

设置界面(apps/web/src/panels/WorkspacesSettings.tsx)把它暴露成1 / 3 / 7(默认)/ 14 天 / 从不五档,写入时走settings.update { settleIdleDays };null表示永不自动归档。手动覆盖通过两个协议方法完成,它们受WORKSPACE_SETTLE_PROTOCOL_VERSION(当前为 78)门控:

  • workspace.settle与workspace.unsettle,分别写settledOverride = "settled" | "active";
  • Default 工作区不允许被归档(setSettledOverride会直接抛错)。

"活跃"的定义

发布说明特别强调:活跃指做了事,不是看了一眼。recordWorkspaceActivity()(packages/server/src/workspaces/workspaces.ts)在会话进入running时被打点,同时 HEAD 变化也会记录活动;而"看一眼"不会。补充的历史数据由settledLifecycle.ts的backfillActivity()完成:先取该工作区最近一次会话更新时间,非默认工作区再退到.git的 mtime,最后才用当前时间兜底。

评审状态刷新为何有节奏

自动归档依赖 PR 状态,但不可能每轮都去问远端。reviewRefreshRows()把行分成三档:

  • 活着的行每轮都刷;
  • 已因空闲被归档的行最多每 30 分钟刷一次(IDLE_REVIEW_REFRESH_MS),这样"在 ThinkRail 之外开的 PR"仍能被发现;
  • 已经合并/关闭且带changedAt的行、以及被手工标为 settled 的行不再刷;被手工标为 active 或 PR 仍开着的行总是刷。

整体一轮的并发上限是 3(REVIEW_REFRESH_CONCURRENCY),正常的评审刷新间隔常量为 5 分钟(REVIEW_REFRESH_INTERVAL_MS);同一项目的多轮扫描通过passesInFlight去重,不会叠跑。

界面侧的行为也在apps/web/src/panels/SPEC.md里写死了几条防"惊吓"的规则:Settled · N折叠头默认收起且状态存在浏览器里;已归档行单行瘦身显示,带merged/closed/idle 2w/by you的原因 chip;每次浏览器首次自动归档会弹一次 info toast,且要等数量稳定两秒才报(升级后的批量回填会连着来很多推送,通知该数这一批而不是第一条);分区用共享的 30 秒时钟重算,让行在没有任何点击时也能跨过空闲窗口。

新外观与真正的性能改动

导航被移到边缘工具轨:Projects、Specs、Files、Changes、Review 不再占据中部的横向空间,布局更密、行高更紧。设置页、加载器重做,通知在各处统一,模型选择器把 provider 分组区分开来;新增两套主题,可从仓库里的定义文件确认:

  • apps/web/src/themes/bundled/minimal-light.theme.json
  • apps/web/src/themes/bundled/minimal-dark.theme.json

发布说明还提到"切换面板不再重新挂载整个应用,过去每次点击约 1,900 次组件挂载"。仓库中没有对应的基准脚本可以复现这个数字,因此更稳妥的读法是:这是该版本给出的背景量级,真正可验证的部分是同一段说明里的另外三条工程改动,它们都能在源码里找到对应设计——大文件与大型 diff 走虚拟化(审查列表是每文件一项的react-virtuoso扁平列表,并有estimatedSectionHeight(change) = 232 + 40 × 变更行数、上限 20 000 px 的占位估算,避免骨架屏尺寸偏差把整表重估导致跳错文件),聊天代码高亮移出主线程(shiki经lib/highlighter/lib/highlightCode在 worker 中运行,流式围栏显示上一次高亮加新到达的纯文本),以及流式渲染的重绘范围收敛。

更小的改进及其实现依据

  • 设计访谈默认回归:每个功能、行为变更或缺陷修复都从 agent 反问开始并更新 spec,且必须展示它否决的备选方案,而不是默默决定。
  • 不可信项目不再加载自己的资源:未信任项目不会加载其.pi设置、扩展、技能、提示与SYSTEM.md,信任提示会写明"信任后会加载什么"。项目记录的piResourceTrust在授予信任时才变为granted,撤销时两者一起收回——所以这是一个真实的加载门,而不是提示语。
  • Settings › Extensions 每项一个开关:对应AppConfig.disabledExtensions,写入走settings.update { config: { disabledExtensions } }。
  • 每个聊天的子 agent 并发是设置项:SUBAGENT_MAX_CONCURRENT = { min: 1, max: 16, default: 4 },既有全局subagentMaxConcurrent,也支持工作区级subagentMaxConcurrentOverride,变更对运行中的聊天生效。
  • 从项目右键菜单重命名项目:仅当宿主协议不低于PROJECT_RENAME_PROTOCOL_VERSION时出现。
  • 聊天可回退到默认模型打开:保存的模型不可用时用当前默认值打开,只有在真正发送消息时才改写保存值。
  • Cmd/Ctrl+, 打开设置(桌面端)。
  • pi 1.2.0:与根目录package.json中 catalog 的固定版本一致。

使用提示与版本前提

  • 协议门控:hunk 级的 Revert 需要当前 welcome 广播了CHANGE_MUTATIONS_PROTOCOL_VERSION(75)且携带双哈希的 diff 元数据先落地;Settled 搁架与 settle 控件需要WORKSPACE_SETTLE_PROTOCOL_VERSION(78)。连到更旧的宿主时,界面会保留原始扁平列表,不显示搁架与归档控件。
  • 审查路径的取舍:想逐个 hunk 决策就留在Stacked(或分支 diff 的 All changes);单回合 Review turn 视图是只读的,适合"先通读一轮",把取舍放到分支 diff 上做。
  • 撤销的时限:回执只在宿主内存里,重启后 toast 还在也会变成RECEIPT_UNKNOWN;回收站里的文件仍可从系统回收站找回。
  • MCP 的密钥管理:不要把明文写进mcp.json——用${VAR}或!command,把凭据放进 header 而不是 URL。
  • 继续深入:模块级设计约束见 changes 模块说明、git 模块说明、MCP 层说明;前端面板与审查交互见 panels 说明;端到端回归可参考 changes-review-turn.spec.ts、changes-review.spec.ts、mcp-approval.live.spec.ts、settled-shelf.spec.ts 与 session-switcher.spec.ts。

【免费下载链接】thinkrail

Vibe code with pi in a lightweight, real IDE that customises itself around the way you work — The Vibe You Need

项目地址:https://gitcode.com/GitHub_Trending/th/thinkrail
点击查看免费下载

相关推荐

上一篇:Optocam Zero屏幕校准系统:7个滑杆修复白点、饱和度、Gamma与背光
下一篇:官方演示 vs 社区复刻:同一个 VoiceBox,谁更值得装进项目

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 10:11:51

Windows+Mac 双平台 Codex 免配置安装保姆教程

前言 做开发的朋友应该都有体会&#xff0c;想要本地部署 Codex&#xff0c;最麻烦的不是软件本身&#xff0c;而是前期环境准备。手动安装 Node.js、处理依赖版本冲突&#xff0c;还要打开终端输入多条命令&#xff0c;稍有不慎就会报错&#xff0c;很多新手卡在这一步直接放…

作者头像 李华
网站建设 2026/10/27 10:10:56

漫画拼版教程:ComfyUI 6格图合成漫画长图,Canva拼图+PS拼版双方案

你是不是已经把 6 格图都画好了、对白也加上了,但打开文件夹一看——6 张图还是散落的,发给朋友对方得一张一张点开? 别慌,我也经历过这个阶段,直到我把 6 张图拼成一张长图,才真正感觉“漫画做出来了”。 这篇就干一件事:把 6 张独立的图,按顺序拼成一张完整的漫画长图…

作者头像 李华