Skip to content

feat(session): 增加当前群话题会话列表 - #1194

Merged
deepcoldy merged 10 commits into
deepcoldy:masterfrom
pixystone:feat/issue-1193-sessions
Sep 7, 2026
Merged

feat(session): 增加当前群话题会话列表#1194
deepcoldy merged 10 commits into
deepcoldy:masterfrom
pixystone:feat/issue-1193-sessions

Conversation

@pixystone

@pixystone pixystone commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

背景与实现

Closes #1193

  • 新增面向群成员的 /sessions,只展示当前 bot、当前群的 thread scope 会话。
  • 默认同时展示活跃与已关闭会话,沿用状态优先级排序(活跃在前、closed 在后),每页 5 条。
  • 卡片展示标题、状态、最近活跃时间、CLI/runtime 与快照时间;刷新、翻页均从 owning daemon 获取实时状态。
  • 有原生 topic AppLink 时直接回到话题;缺失 omt_ 的历史会话由中央列表后台按 owning daemon 懒回填并持久化,失败带去重、并发上限和冷却。
  • Dashboard 管理员可从列表恢复已关闭会话;恢复后同步重建原话题的等待输入卡片,再撤回已关闭卡片。
  • 普通群即使配置为“每条消息开新话题”,顶层 /sessions 仍固定在群顶层展示;真实话题内调用仍回复当前话题。
  • 同一 bot、群、调用人再次执行时先发布新列表,再 best-effort 撤回上一张列表卡;卡片指针持久化,daemon 重启后仍生效。
  • 统一 /sessions 与会话卡片的状态颜色(等待输入保持绿色),并更新 /help、中英文 i18n、命令文档和截图。

权限与安全

  • /sessions 是只读群级命令:new-topic 与 thread-reply 两条生产路由均单独按 canTalk 判权;其它 daemon 管理命令仍默认要求 canOperate
  • p2p 在读取数据前拒绝;卡片不暴露 working directory、完整 session ID 或终端链接。
  • 初次渲染及每次回调均按 larkAppId + chatId + thread scope 重新过滤;实时查询只允许认证 caller 读取其自身 bot 的行。
  • 回调绑定 invoker,并通过卡片消息的真实 chat 反查阻断转发或篡改后的跨群操作。
  • locate 与 resume 均在 daemon 侧 compare-before-use,复核 bot、群、scope 与最新 open/closed 状态;恢复动作额外要求 Dashboard admin,卡面隐藏不作为权限闸。
  • locate body 必须是普通 JSON 对象,null、标量和数组统一返回 400 body_must_be_object;既有 Dashboard {} 调用保持兼容。
  • 旧列表撤回以 (larkAppId, chatId, invokerOpenId) 隔离,不会撤回其他成员正在查看的卡片;撤回失败不影响新卡可用。

影响面

涉及公共命令路由、飞书卡片回调、Dashboard/daemon IPC、会话恢复卡片生命周期与 session 行投影。未改动 CLI adapter、PTY/tmux/ZMX/远端 backend 的执行语义;普通群 chat-scope 会话及其它 bot/群均不会进入列表。

界面

sessions command card

验证

  • 已 rebase 到最新 upstream/master37652bf0c)✅
  • bun run build
  • 受影响的 9 个测试文件:765 passed / 1 skipped ✅
    • card-handler-resume-receipt
    • command-handler
    • daemon-internal-api
    • daemon-rename-route
    • dashboard-ipc
    • group-sessions-card-dispatch
    • group-sessions-card-store
    • group-sessions-card
    • lazy-topic-link-backfill
  • git diff --check
  • 飞书本地 live 手测通过:列表范围、closed 展示与恢复、状态色、实时刷新、普通群顶层路由、旧列表撤回均符合预期 ✅

@pixystone
pixystone requested a review from deepcoldy as a code owner September 2, 2026 06:38
@deepcoldy

Copy link
Copy Markdown
Owner

自动评审初步意见(PR #1194

先说结论:这个 PR 质量高于平均,建议小改后合入。四道安全闸(invoker 绑定、转发/篡改防护、locate 竞态闸、四条件过滤)设计正确,我们做了反变异验证——逐条改坏生产代码,对应测试都变红,说明这些闸是真在起作用而不是装饰。刻意不渲染 workingDir / 完整 session ID / 终端链接的处理也很干净,未发现越权、跨群或跨 bot 泄漏。

以下两条建议 + 一条 nit:

🟠 F1:locate 的 body 校验建议改成「对象判定」,而不只是非空判定

src/core/dashboard-ipc-server.ts 里新增的:

expected = await readJsonBody(req, 8 * 1024);

之后直接进 matchesExpectedSessionLocateScope(ctx, expected),中间没有判定 body 是不是普通对象。这带来两个问题:

null body → 500 且回裸 TypeError。 readJsonBody'null' 会返回 null,守卫里 expected.expectedLarkAppId 立刻抛异常。实测(注册真实 active session 让代码真正走到守卫,并带 {} 作为阳性对照):

body=null  => 500 {"error":"TypeError: Cannot read properties of null (reading 'expectedLarkAppId')"}
body={}    => 502(已越过守卫,进到后续解析)← 对照,证明差异来自 null 本身

② 更值得关注的是非对象 body 会静默绕过整个守卫。 42 / "str" / [] / true 不会 500,但由于 JS 装箱属性访问不抛异常,(42).expectedLarkAppId 就是 undefined,于是四个 !== undefined 检查全部跳过 → 守卫恒返回 true,形同虚设:

bogus 42      => true(放行)
bogus "hello" => true(放行)
bogus []      => true(放行)
bogus true    => true(放行)
对照:{expectedChatId:'oc_other'} => false(正常对象时守卫工作正常,确有牙)

判据不是主观偏好,而是偏离了同文件既有约定dashboard-ipc-server.ts 内 2988、3081、3848 行的同类路由都显式写了 if (body === null || typeof body !== 'object') 再往下走。

需要说明的是:这不是外部可打的洞,该端点需通过 trusted-host HMAC(locate 不在 public / core-only public / narrow-capability 任一白名单内),且浏览器路径的 proxy 调用不带 body,无法投递 null。所以严重度我们压在 🟠,属健壮性 + 错误信息卫生 + 守卫完整性问题。

建议改法(与同文件约定一致,顺带把数组也挡掉):

if (expected === null || typeof expected !== 'object' || Array.isArray(expected)) {
  return jsonRes(res, 400, { ok: false, error: 'body_must_be_object' });
}

再补一条用例同时覆盖 null 与非对象 body。已确认现有调用方兼容性不受影响:无 body 与 {} 都会被 readJsonBody 归一成 {},既有 locate rate limit 用例(无 body)保持绿。

🟡 F2:文档口径与实际权限不一致——普通群成员默认用不了 /sessions

issue #1193 要求「普通成员可用,不额外要求 Dashboard admin」。实现确实没有复用 dashboard admin gate(这点做对了),但 /sessions 一旦加入 DAEMON_COMMANDS,就会自动落到 canRunDaemonCommand 统一闸后面,而该闸默认等价于 canOperate(即 allowedUsers 白名单);只有 owner 把命令显式写进 canTalkDaemonCommands 才会降级到 canTalk。而 canTalkDaemonCommands 没有内置默认值bot-registry.ts:3189-3205 仅在配置显式提供时才解析)。

实测(带阳性对照):

owner  /sessions => true
member /sessions => false
member /restart  => false  ← 对照:与公认需要 operate 的命令表现一致

即:配置了 allowedUsers 的 bot 上,普通成员执行 /sessions 会收到 cmd_allowed_users_only这不是安全漏洞(方向偏严、fail-closed),但 /help 与中英文 slash-commands.md 都把它描述为群成员可用的命令,会造成预期落差。

建议二选一:

  1. 文档补充说明「需 owner 将 /sessions 加入 canTalkDaemonCommands 后普通成员方可使用」;
  2. 若确实希望开箱即用,则显式讨论将 /sessions 纳入默认降权名单——这属于产品决策,交由维护者判断。

⚪ N1(nit):孤立资产

docs-site/static/img/sessions-command-card.png(45KB)已加入仓库,但没有任何 md 文件引用它。建议在 slash-commands.md 中引用,或说明用途。

附:我们实际跑过的验证

  • bun run build
  • 定向 6 个文件:512 passed / 1 skippedgroup-sessions-cardgroup-sessions-card-dispatchdashboard-ipccommand-handlerslash-commands-doc-synccommand-trigger-reserved-commands
  • 另跑 dashboard-sessions-command / dashboard-sessions-ui / card-handler-c2:66 passed ✅
  • 新增两个测试文件的 it() 声明数与实跑数对齐(13 / 1),无静默不跑的用例
  • 反变异 4 枪全红:删 invoker 绑定、删转发 chat 反查、删过滤器 larkAppId 条件、删 daemon 侧 409 守卫,各自打红对应用例

补充说明:评审期间主干前进(#1172 落地),我们已在本地基于最新 origin/master rebase 后重新验证(无冲突,diff 逐字不变),未推动你的远端分支。另外我们未做飞书真机 live 验证,卡片渲染效果仅参考了你贴的截图。

另有一处可选收敛(不算 finding):新增的 escapeLarkMdsrc/im/lark/groups-card.ts 中那份实现逐字相同,属重复实现,可考虑抽共享。


以上是自动评审的初步意见,由两个 review agent 独立交叉核对后收敛(F1 的「非对象静默绕过」这一层由复审补充,比只挡 null 更值得修)。可能存在误判或遗漏,最终以维护者审阅结论为准。感谢你的贡献 🙏

@deepcoldy

Copy link
Copy Markdown
Owner

F2 选项更正(替代上一条评论里的「建议二选一」)

抱歉,上一条评论里的选项 ② 表述有误,在此撤回并给出收敛后的最终菜单

为什么撤回 ②

上一条写的是「将 /sessions 纳入默认降权名单」,但进一步核实后发现:架构里不存在「内置默认降权名单」这个落点。 canTalkDaemonCommands 今天只有配置来源——src/bot-registry.ts:3195 仅在 bots.json 显式提供该字段时才解析,没有任何内置默认值;canRunDaemonCommand 也是直接读 bot config(event-dispatcher.ts,全仓库仅 daemon.ts:1807119584 两个调用点)。

所以照字面实现 ② 需要新造「内置默认」这个概念,而它改变的是所有已部署 bot 的默认权限——收敛面是整个 fleet,不止 /sessions 一条命令。这个代价与本 PR 想解决的问题不成比例。给你指了个不存在的落点,是我的疏漏。

若要达成 issue #1193 原意,有收敛面更小的替代

选项 ③:在统一闸之前特判 /sessions,在 handler 内做 canTalk 判定。

如实标价——这不是照抄既有先例

  • 全仓库目前没有任何 daemon 命令在 handler 内部用 canTalk 判权/vc-auth/card/cot/term 虽然都在统一闸之前特判,但它们的权限闸全是 canOperatecommand-handler.ts:1252 / 1366 / 1448/vc-authdaemon.ts:17986);那些特判的目的是避免预建 worker=null 的幽灵 session/card 处的注释即「so no phantom session is created」),不是降权。
  • 两条派发路由都要特判:new-topic 与 thread-reply(即 canRunDaemonCommand 仅有的两个调用点)。目前二者本身并不对称——thread-reply 只特判了 /vc-auth/term。只改一条会导致「新话题里能用、话题内回复用不了」。
  • 需补一条钉住这个新权限口径的测试。

安全面我们已核过,是有界的:p2p 被 handler 的 getChatModeStrict 拦在数据面之前;canTalk 用户可见的仅是话题标题 / 状态 / CLI 名,这些在群内本就可见;卡片回调的 invoker 绑定与转发反查原样生效;locate 限本群话题且有 per-session 限流。

最终菜单(以此为准)

  • ① 只改文档:零代码零风险。补一句说明「owner 需将 /sessions 加入 canTalkDaemonCommands 后普通成员方可使用」即可——该字段除 bots.json 外,Dashboard 的「Slash 命令权限」区块也有可视化编辑入口(bot-defaults-page.tsxSlashCommandPermissionsSection),对 owner 并不算高门槛。代价是 issue feat(session): 增加当前群活跃话题会话列表与回跳命令 #1193 的「普通成员开箱可用」未达成。
  • ③ 闸前特判 + handler 内 canTalk:达成原意,收敛面仅限 /sessions 一条命令,但要开一个新的权限先例(两条路由 + 测试)。

交由你与维护者定夺——这属于产品决策,我们不代为选择。F2 之外的 F1(locate 的 body 对象判定)与 N1(孤立 PNG)建议不变。

以上仍是自动评审的初步意见,最终以维护者审阅结论为准

@pixystone
pixystone force-pushed the feat/issue-1193-sessions branch from 0c1f5bf to b8c752c Compare September 3, 2026 07:06
@pixystone

Copy link
Copy Markdown
Contributor Author

已按评论更新,并 rebase 到最新 upstream/master

  • F1:locate body 现在要求普通 JSON 对象,null、标量和数组统一返回 400 body_must_be_object;新增 5 组路由测试。
  • F2:采用选项 ③。new-topic 与 thread-reply 两条生产入口都在统一 daemon 管理闸前单独按 canTalk 判定 /sessions;其它 daemon 命令的 canOperate 默认权限不变。新增两条入口的 talk-only 正向用例和无 canTalk 反向用例,并断言不会创建 phantom session 或读取 session 列表。
  • N1:中英文 slash command 文档均已引用卡片截图。

本地验证:相关 6 文件 405 passed;locate IPC 7 passed / 192 skipped;bun run build 通过。对应提交:b8c752cbe。

@deepcoldy

Copy link
Copy Markdown
Owner

二轮评审:b8c752cbe 三条建议均已妥善处理 ✅

在最新提交上重新跑了验证,F1 / F2 / N1 全部解决,且 F2 的实现质量超出预期

F1 — 已修,且按最优形态修

if (body === null || typeof body !== 'object' || Array.isArray(body)) {
  return jsonRes(res, 400, { ok: false, error: 'body_must_be_object' });
}

采用的是对象判定而非仅 null 判定 —— 这同时堵住了「非对象 body(42 / "str" / [])因装箱属性访问不抛异常而静默绕过整个守卫」这条更隐蔽的路径。与同文件 2988 / 3081 / 3848 行的既有约定一致。

F2 — 已解决,实现干净

新增 canTalkForGroupSessions,在两条派发路由各做闸前特判后走 evaluateTalk / evaluateBotTalk。逐条核过:

  • 闸位正确:置于 grantRestrictedSlashCommandText 之后,与 /vc-auth 同位置,配额与限制腿不被绕过;
  • 两条路由都改了handleNewTopicAdmittedhandleThreadReplyAdmitted 均已特判 —— 二者原本并不对称(此前只有 /vc-auth/term 两边都有),只改一条会导致「新话题能用、话题内回复用不了」,这个坑没有踩到;
  • 未自创谓词:bot 发送方分叉走 evaluateBotTalk,与 canRunDaemonCommand 内部逻辑逐字同款;
  • owner 不受影响evaluateTalkallowedUsers 腿仍先命中;
  • p2p 仍被拦:handler 的 getChatModeStrict 在读取任何 session 行之前就拒绝。

需要说明的是,这在本仓库属于首个在 handler 层用 canTalk 判权的 daemon 命令(既有 /vc-auth/card/cot/term 的闸前特判内部闸全是 canOperate,那些特判的目的是避免预建幽灵 session 而非降权)。鉴于 /sessions 是纯读、且展示的元数据在群内本就可见,我们认为这个先例是合理的;只是提请维护者注意,这是一条新的权限口径,值得在合入时确认认可。

N1 — 已修

两份 slash-commands.md 均已引用该 PNG,孤立资产问题消除。

本轮验证

  • bun run build
  • 6 个相关测试文件:601 passed / 1 skipped ✅(group-sessions-cardgroup-sessions-card-dispatchdashboard-ipcdaemon-rename-routecommand-handlerslash-commands-doc-sync
  • 反变异验证新权限闸:将 canTalkForGroupSessions 改为恒 return true 后,daemon-rename-route.test.ts两条路由的拒绝用例双双变红 —— 证明新闸确实生效,且两条路由都有测试覆盖,不是单边验证。

合入前提请注意

当前 PR head 已落后主干:force-push 时 rebase 到了 cf3cdcd18,而 origin/master 现已到 e9d2ca28c#1215 已合入)。建议合入前重新核对基线。


结论:三条评审意见均已妥善处理,从代码质量角度我们这边没有新的阻断项。 唯一需要维护者拍板的是上面提到的「新权限口径」是否认可。

以上仍是自动评审的初步意见,最终以维护者审阅结论为准。感谢快速响应 🙏

@pixystone
pixystone force-pushed the feat/issue-1193-sessions branch from b8c752c to 582804f Compare September 4, 2026 08:20
@pixystone pixystone changed the title feat(session): 增加当前群活跃话题会话列表 feat(session): 增加当前群话题会话列表 Sep 4, 2026
@pixystone

Copy link
Copy Markdown
Contributor Author

已整理并推送当前完整版本,head 为 582804f8a,同时 rebase 到最新 upstream/master37652bf0c)。

相较二轮评审时的版本,本轮新增/收敛:

  • 活跃与 closed 会话统一展示,活跃优先;管理员可直接恢复 closed。
  • 刷新/翻页读取 owning daemon 实时状态,修复 working 状态刷新缺失。
  • 缺失原生 topic id 的历史会话支持后台懒回填跳转链接。
  • 恢复后重建原话题等待输入卡片并撤回 closed 卡片。
  • /sessions 在普通群 new-topic 模式下仍留在群顶层。
  • 同一调用人的上一张 /sessions 卡片持久化追踪并 best-effort 撤回。
  • 等待输入状态色与会话卡片统一为绿色。

最终验证:bun run build 通过;9 个受影响测试文件 765 passed / 1 skipped;git diff --check 通过;飞书 live 手测通过。PR 描述已同步为当前实现与安全边界,烦请 Review。

@deepcoldy

Copy link
Copy Markdown
Owner

三轮评审:新增改动质量在线,1 条建议 + 1 条 rebase 提醒

先说结论:代码层面没有阻断项。 这轮从「只读列表」扩展到「可操作管理卡」(含首个写操作 resume),权限边界处理得很稳 —— 我们特别检查了容易出问题的地方,都没踩:

  • resume 的 admin 闸在 Route B 调用之前,非管理员点不动,也不会先发一次无用请求;
  • canResume 只作 UI affordance,回调侧独立再判(代码注释也写明了这点)—— 没有把按钮可见性当权限边界;
  • isDashboardAdmin fail-closed(admin 名单为空时无人可 resume);
  • 撤回旧卡的 key 是 (larkAppId, chatId, invokerOpenId) 三元组 —— A 再次查询不会撤掉 B 正在使用的卡;
  • 顺序是「先发新卡 → 成功后 best-effort 撤旧卡」,整段 try/catch,撤回失败只 warn,不会把一次成功查询变成报错。

上轮三条也已全部闭环:F1(locate 对象判定)✅、F2(/sessions canTalk 闸,两条路由都改了)✅、N1(PNG 已在中英文文档第 39 行引用)✅。

🟡 F3(建议修,非阻断):resume 路由的 null body 会 500,且会话实际已恢复

const body = await readJsonBody<>(req, 4_096).catch(() => ({}));

const shouldReconcileStreamingCard = body.reconcileStreamingCard === true &&    // 2953 行

readJsonBody('null') 不抛异常、返回 null,所以 .catch() 不触发,随后 body.reconcileStreamingCard 抛 TypeError → 500。这是 F1 同一形状在新路由上的复现。

值得注意的是它比单纯的健壮性问题重一点:路由顺序是 readJsonBody(2886) → findSessionRecordresumeSession(2898,此时会话已真正恢复)body.reconcileStreamingCard(2953,抛错)。也就是说调用方收到 500「恢复失败」,但会话其实已经活了 —— 是一个会误导使用者的错误状态,而非单纯的入参报错。

不过可达性有限:能投递非对象 body 的只有 Route B(需 HMAC),与 F1 同威胁模型;现有全部四个调用方(CLI 直连、Dashboard sessions-page、Dashboard sessions-card、/sessions 群卡)都不会发 null。所以我们判定为建议修、不阻断

修法与你在 locate 里已经用过的一致:

const rec = body && typeof body === 'object' && !Array.isArray(body) ? body : {};

再补一条 body=null 不 500 的用例即可。

⚠️ rebase 提醒:DAEMON_COMMANDS.size 会撞一次「两侧都对但并集不对」

当前分支落后 origin/master 若干 commit,其中 #1247 引入了 /quote。我们本地 rebase 到最新主干时出现 5 处冲突,都是 /quote/sessions 抢同一行的机械并集(passthrough-commands.tscommand-handler.ts 的 help 列表、中英文 slash-commands.mdcommand-handler.test.ts),逐一保留两侧即可。

但其中一处不能简单「保留两侧」test/command-handler.test.tsexpect(DAEMON_COMMANDS.size).toBe(37) —— master 侧因 /quote 是 37、你这侧因 /sessions 也是 37,并集实际是 38;同时该文件的 expected 数组在你这侧没有 /quote。照搬任何一侧都会得到红测试,需要手工改成 38 并把 /quote 补进 expected 数组。提前说一声,免得 rebase 时费时间排查。

我们跑过的验证

  • bun run build
  • 10 个相关测试文件:769 passed / 1 skipped ✅;另跑 3 个回归文件 98 passed ✅(以上均在「rebase 到最新主干后的真实合并树」上跑)
  • 新增测试文件声明数与实跑数对齐(it() 21/1/3 → 实跑 28/1/3,多出的来自 it.each 展开,非静默跳过)
  • 反变异验证:移除 resume 的 isDashboardAdmin 闸 → 对应用例变红;撤回 key 去掉 invokerOpenId → 对应用例变红。两道新守卫都确实生效。

⚪ 两条只提示、不必在本 PR 处理

  • group-sessions-card-store.json 无上限无清理,每个 (bot, 群, 人) 约 150B —— 真实规模下几十 KB,不构成问题,但长期只增。
  • lazy-topic-link-backfillfailedUntil Map 只 set 不 delete,进程内每个失败过的 sessionId 永久占一条。同样量级极小。
  • 一处状态口径小漂移(不影响正确性):流式卡片侧有 ds.usageLimit → 'limited' 覆盖(worker-pool.ts:854),列表侧 composeRowFromActive 没有。在「usage-limited 但 screen 仍是 working」的窗口内,卡片显示 limited、列表显示 working。方向不危险,看你是否想统一。

以上是自动评审的初步意见,最终以维护者审阅结论为准。这轮改动的权限设计和测试覆盖都挺扎实,辛苦 🙏

pixystone and others added 9 commits September 6, 2026 15:15
- daemon 新增 POST /api/sessions/:id/resolve-thread-id:仅重读本地 owned
  session,thread + om_ root + 缺合法 omt_ 时才查飞书,经 fillNativeTopicId
  持久化并发布 feishuThreadLink SSE patch(active/persisted/closed 均覆盖)。
- 中央 GET /api/sessions 返回快照后后台按 larkAppId 路由 owning daemon 触发
  回填,非阻塞、in-flight 去重、失败 5min 冷却、并发上限 4。
- 新增 lazy-topic-link-backfill 及单测。

Co-authored-by: TRAE CLI <traecli@bytedance.com>
@pixystone
pixystone force-pushed the feat/issue-1193-sessions branch from 582804f to bb97f6e Compare September 6, 2026 15:18
@pixystone

Copy link
Copy Markdown
Contributor Author

已完成本轮修订:

  • rebase 到 upstream/mastere834388c3),保留 /quote/sessionsDAEMON_COMMANDS 为 38。
  • 修复 resume 路由对 null / 数组 / 标量 JSON body 的处理:在恢复状态副作用前统一归一为空配置,避免会话恢复成功后访问属性导致 500。
  • 新增 null body 回归:验证 HTTP 200、ok: true,且会话实际恢复;既有 reconcileStreamingCard: true 用例保持通过。

验证:bun run test test/dashboard-ipc.test.ts --run(242 passed, 1 skipped)、bun run test test/command-handler.test.ts --run(302 passed)、bun run buildgit diff --check

最新提交:bb97f6e5a

@deepcoldy
deepcoldy merged commit 97233b1 into deepcoldy:master Sep 7, 2026
7 of 8 checks passed
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown

🚀 Released in v3.19.1

deepcoldy added a commit that referenced this pull request Sep 9, 2026
)

`docs-deploy` 自 #1194 起 8 次 run 全部失败,零成功。报错是 rspress 的
`Dead image found`:文档里 `![..](/img/foo.png)` 会让 rspress 去
`docs-site/docs/public/img/` 找(`rspress.config.ts` 是 `root: 'docs'`),
而截图实际提交在 `docs-site/static/img/`,没有任何步骤把它们拷过去。
`markdown.link.checkDeadLinks` 开着,于是一张缺图就让整个站构建失败。

`docs/public` 本来就是「构建前生成」的目录(已在 .gitignore 里,`prebuild`
的 prepare-assets.mjs 会往里拷 logo),只是这个脚本只管了 logo。改成整个
`static/img/` 目录一起拷,以后加截图不用再动这个脚本——现存 4 张死图
(sessions-command-card、quota-fallback-dashboard、
quota-fallback-cycle-recovery-dashboard、streaming-card-button-settings,
分别由 #1194 / #1283 / #1323 引入)一并修好。

`static/img` 仍是唯一事实来源;生成出来的 `docs/public/img/` 加进 .gitignore,
避免同一批图在仓库里存两份。目录不存在时按 ENOENT 跳过而不是让 prebuild 崩掉:
没有截图的构建是合法的,该由 rspress 的 checkDeadLinks 去管「文档引用了但缺图」。

验证(本地无法跑 rspress:worktree 内不装依赖):
- `node scripts/prepare-assets.mjs` rc=0,5 张图拷进 docs/public/img/,与源逐字节相同
- 文档里 4 个 `/img/` 引用全部解析到实际文件(unresolved=0)
- 删掉 static/img 重跑 rc=0(不再 ENOENT 崩),恢复后仍拷齐 5 张
- 生成目录已被 .gitignore 忽略(`git check-ignore` 命中)
真正的绿要靠 CI 跑一次 docs-deploy 确认。

Co-authored-by: Claude Code <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(session): 增加当前群活跃话题会话列表与回跳命令

2 participants