Repository navigation
Releases: amlyczz/dsh-lark-link
Release list
v0.6.0
0.6.0
Feature: 设置页与插件详情页复用统一控制台 & 移除侧边栏图标
- 干掉左侧侧栏图标:移除 sidebar.footer.action 挂载点,彻底避免与其它插件(如 dsh-cost-meter)排版打架、变形及挤占空间问题。
- 设置页与插件详情页统一控制台:注入 settings.section 以及 plugins.bundle.config / plugins.row.config,100% 界面与功能复用,零冗余。
- 全新品牌 Logo 与图标系统:原创设计并绘制了 dsh-lark-link 官方矢量 SVG 图标(�ssets/icon.svg、icon.svg、�ssets/logo.svg)。
- 入站 WAL 记录全生命周期压实:原子快照写入并清理旧分片,彻底杜绝已删除记录复活隐患。
- 跨平台与 Windows 兼容性修复:修复 empDir、Windows 权限位及路径解析。
- 四级脱敏与 UTF-8 安全字节截断:全面保护敏感凭证与杜绝卡片乱码。
v0.5.6 — duplicate reply fix (GH #15), replay switch & WAL lifecycle (GH #14)
Fix: 同一条 assistant 回复被投递两次 (GH #15)
session/event订阅泄漏:agent/error 退订函数覆盖了 session/event 退订函数,agent 重建后同一会话存在多个 live 订阅 → 同一回复投递多次。现在两个退订函数组合为一条,任何销毁路径完整退订。- 幂等键失效:assistant 投递的
dedupeKey含Date.now(),outbox 幂等完全失效。现在使用 DSH 稳定的AssistantMessage.id作为幂等键;turn/end rescue 增加并发守卫。
Feature: 入站补发开关 + WAL 全生命周期治理 (GH #14)
replay配置组{ auto, retentionMinutes, maxAttempts }:/lark-config replay.auto=false、插件 config、DSH_LARK_AUTO_REPLAY=0三种方式关闭启动自动补发(默认开,零丢失现状不变)。/lark replay:手动补发 /list查看 WAL /drop <messageId>丢弃 /auto on|off切换。- WAL 不变量"要么可重放、要么终态 failed":超窗/耗尽的记录不再永久滞留且不可见,
prune()标记 failed(/status可见),7 天后清理。 replay.retentionMinutes/replay.maxAttempts可配(默认 30 / 2)。
Full Changelog: v0.5.5...v0.5.6
v0.5.5 — persist setup user_info + contact scope (GH #12)
Feature: setup 持久化 user_info 并申请用户基本信息权限 (GH #12)
/lark setup 注册时返回的扫码者 user_info(open_id + tenant_brand)此前被丢弃;本版将其写入凭据 blob,并为新建应用增加 contact:user.base:readonly,配套插件可直接用 open_id 解析姓名/头像(无需用户先发消息)。
变更
- setup 成功后持久化
userInfo;无/空载荷不写该键(历史 blob 与手动 appId/Secret 通道形状不变) parseCredentials透传userInfo(避免逐字段重建时静默丢失)SETUP_SCOPES+=contact:user.base:readonly- 类型:
user_info补全open_id(对齐 SDKUserInfo)
验证
tsc --noEmit通过npm test:275 passed / 0 failed
Closes #12
Full Changelog: v0.5.4...v0.5.5
v0.5.4 — fix default agentPreset code→ptc (GH #11)
Fix: 默认 agentPreset code 不是 DSH preset id — 开箱即坏 (GH #11)
DSH 内置 agent-presets roster 是 standard | ptc | minimal | cordis,没有 code。此前默认值、UI roster、文档注释都写了 code,且别名映射方向写反(把合法的 ptc 改写成非法的 code),导致未显式覆盖 agentPreset 的会话创建 agent 时直接抛错,飞书侧收不到任何回复。
变更
- 默认值:
DEFAULT_CONFIG.agentPreset从"code"改为"ptc" - 别名映射方向修正:
code → ptc(此前是ptc → code)。旧配置 / 会话 override / 历史 session log 里的code在使用点统一经normalizeAgentPreset()归一 - UI roster:
AGENT_PRESETS/ 内存后端 PTC 行 id 修正为"ptc" /mode兼容:/mode code按ptc处理;live roster 可用而当前配置 id 不在其中时会提示可用列表- 未知 preset 警告:创建 agent 时若 roster 可用且解析后的 id 不在其中,记 warning
- 测试:新增归一化与 create/resume 回归测试;不再把
"code"固化成 DSH 期望值
验证
tsc --noEmit通过npm test:269 passed / 0 failed
Closes #11
Full Changelog: v0.5.3...v0.5.4
v0.5.2
Fix: agent 完成但飞书无回复 — 事件丢失兜底、补发可救回、失败可见 (GH #9)
turn/end 兜底投递(rescue)
此前 delivered 只依赖 assistant/message → outbox.enqueue 这一条路径——桥进程在工具调用期间重载、订阅竞态或 enqueue 异常时,agent 轮次已完成但 outbox 无任何消息,飞书侧静默无回复。现在 dsh-adapter 会记住本轮最终 assistant 文本并附加在 turn/end 事件上(finalText);event-forwarder 在本轮没有任何 durable 投递时于 turn/end 一次性把最终回复送入 durable outbox,并作为第二条独立的 delivered 确认路径回调 onDelivered。空输出 / No response. 不会误触发,已投递的轮次不会重复发送。
启动补发先查会话日志(replay-salvage)
WAL 记录重放不再盲目重新调用 agent(重放会整个重跑安装/长任务、烧 token)。启动重放前先检查该会话持久化日志中、请求接受时间之后的最终 assistant 输出——存在则直接从日志补投回复并标记 delivered(幂等 dedupe key wal-salvage:<messageId>),无可用输出才回落到重新分发。
补发耗尽后标记 failed(不再伪装 accepted)
重放次数达到上限的 WAL 记录现在转为终态 failed 并持久化;/status、状态详情与 Web 面板新增 补发失败/入站补发失败(inboundFailed)计数——inboundPending=0 不再掩盖「已耗尽重放次数但仍未回答」的记录。事后救回(delivered)仍优先于 failed。
事件订阅时序回归护栏
新增回归测试固化 conversation-manager 的不变量——agent.onEvent() 必须在任何 followup() 之前绑定,保证 turn 内同步发出的事件不会因订阅时序丢失。
Full Changelog: v0.5.1...v0.5.2
v0.4.1 — CardKit 2.0 schema fix, fallback safety, group @bot routing
Fixes in v0.4.1
- CardKit 2.0 请求体规范修复:移除 config 内非法的 summary 字段,增加 update_multi: true,markdown 空内容增加占位,彻底修复流式卡片创建报 400 的问题。
- 流式降级兜底吞回复 Bug 修复:修复流式卡片报错 disposed 后 finalize 未抛错导致 event-forwarder 误判已投递并吞掉最终回复的问题。现保证任何流式卡片异常均 100% 回退至 durable outbox 完整回复。
- 群聊 @机器人 命令识别与 Prompt 清理:支持过滤群聊中用户 @机器人(包括 标签与 @_user_1 占位符)前缀,使所有命令在群聊中均可正常执行,同时 Agent 接收到纯净 Prompt。
v0.4.0 — resume/streaming overhaul, media retention, cross-platform path fixes
Changelog
0.4.0
Follow-up fixes (发布前自测反馈)
- dsh 重启后飞书发消息「新建会话」→ 现在续接同一会话:重启后会话 id 不再随机漂移。每个会话的
activeSessionId持久化到conversation-overrides.json;ensureAgent在创建前先agents.resume(activeSessionId)续接(create 撞「already exists」时同样回退 resume),只有 resume 失败(会话被清理)才新建。之前runNonce每次启动随机 → 重启后同一条消息必开新会话行,对话上下文全部丢失。 /stop后再对话「新起一轮会话」→ 现在同一会话继续:/stop只取消当前 turn(conversations.stop(key)),不再 rotate/dispose 会话,也不清activeSessionId;silent 恢复逻辑显式排除aborted(用户主动取消 ≠ 卡死),下一条消息继续原会话上下文。- 图片:模型找不到图片(双根因)
ctx.get("attachments")在插件加载时被一次性快照——DSH 附件服务若晚于插件挂载(Cordis 加载顺序),永远是undefined,imageRef从不生成,ImageBlock 从不入 content,模型对图片一无所知。改为 live getter(attachmentsRef,违反的正是 bridge-context 自己头部声明的 GETTER 原则)。- 即使无 imageRef / 非视觉模型,图片也不再静默消失:本地保存路径折入文本(
[用户发送了图片,已保存到本地: …]),模型可用工具读取;post 富文本内嵌的{tag:"img"}图片现在逐个提取下载(之前只提 text_run 文字,图全丢)。图片文件名改用imageKey后缀(确定性、多图不碰撞)。
- 入站附件改存系统临时目录 + TTL 自动清理(默认 7 天):图片/文件此前永久累积在
~/.dsh/lark-link/inbound/media/(状态目录)。现在默认落os.tmpdir()下的dsh-lark-link/inbound/(Linux/macOS/Windows 各自的系统临时目录,OS 亦可随时回收),新增attachments.retentionHours(默认 168h = 7 天,/lark-config attachments.retentionHours=48热改生效;0 = 永久保留)与attachments.dir(自定义根目录,重启生效)。启动即清扫一次(清掉上次运行遗留)+ 每小时按 mtime 清扫过期文件。旧目录~/.dsh/lark-link/inbound/(484K)已不再写入,可手动删除。跨平台加固:① 落盘文件名统一sanitizeAttachmentName(Windows 保留字符<>:"/\\|?*、控制字符、尾点/尾空格全部替换,中文保留)——macOS 截图名里的:不再产生 Windows 非法文件名;② 清扫删除带force + maxRetries(Windows 上文件被占用时 EBUSY/EPERM 短暂重试,下次清扫兜底);③ 全程node:pathjoin + 现有isAbsoluteAny/relative判定(round-1 GH #7 已覆盖 UNC/盘符路径),无硬编码分隔符。 - 非视觉模型发图报错「本轮运行失败 UNSUPPORTED_CONTENT」(qwen3.8-27b / DeepSeek 等):
- 正则漏匹配 DeepSeek:DeepSeek 抛出
The DeepSeek chat-completions adapter does not support image content.(含content而非input),旧正则仅匹配image input导致 DeepSeek 报错未被捕获。现提取isImageUnsupportedError全面匹配 DeepSeek、pi-ai(qwen/glm 等)、UNSUPPORTED_CONTENT、unsupported_content_type等各类非视觉模型报错。 agent/error中同步followup()导致重试死锁未唤醒:在dsh-agent-loop中,throwError()触发agent/error时 agent 仍处在"running"phase 且未 abort,此时同步调用followup(retry)虽把消息放入inbox.nextTurn,但wakeDriver(false)因非 idle 且非 abort 不会置wakeRequested = true。随后 turn 抛错结束,kick()finally 看到wakeRequested === false遂直接进入 idle,重试消息永久卡在 inbox 不被执行。现改为await agent.whenIdle()停稳后再followup(retry),正确唤醒新一轮纯文本执行(带本地落盘图片路径),用户正常获得回复。- 双重记忆与切模型重置:同时按会话 (
imageUnsupportedKeys) 与模型 (imageUnsupportedModels) 记忆非视觉属性;后续发图直接走纯文本落盘路径(避免每张图都走一遍报错重试);使用/model切换模型时自动清除会话标记,切回视觉模型时可恢复 ImageBlock。
- 正则漏匹配 DeepSeek:DeepSeek 抛出
- 流式卡片:create 请求体多包一层
data导致 API 拒绝。官方POST /cardkit/v1/cards请求体是扁平的{type:"card_json", data:"<字符串化卡片JSON>"}(已核对官方文档请求体示例),此前实现包成{data:{type,data}}必然 4xx → 卡片永远出不来。另新增一次性失败提示:卡片创建失败时在聊天里说明原因(缺 CardKit 权限/客户端过旧等),不再静默回退普通消息(内容本来就不丢,现在原因可见)。 - /resume 选择卡片可用性 + 友好化:
- 修按钮点击 bug:lark-link 会话 id 满是冒号,卡片回调按第一个冒号切 op,
resume:lark-link:dm:…被截成dm:…→ 永远「未找到会话」。按钮 op 现在传 URI 编码的 id,匹配侧解码并保留原始/前缀/后缀三种兜底。 - 卡片重做:相对时间(
5 分钟前/3 天前)、preset 徽标、当前会话列为禁用行(无编号)、序号只数可恢复行(/resume <n>与卡片编号严格一致)、空态与「新起会话/切工作区」提示、恢复成功回执显示会话起始时间。
- 修按钮点击 bug:lark-link 会话 id 满是冒号,卡片回调按第一个冒号切 op,
Feature: Feishu-side /resume — pick up historical sessions of the current workspace
/resume(无参数)渲染当前会话工作区的历史会话卡片(时间 + 模式 + 可点按钮);/resume <序号|id前缀>(或点按钮)恢复该会话。- 列表双通道:优先
sessionPersistence服务头(精确 cwd 过滤、排除 subagent 子会话、带存储 preset),服务不可用时回退文件扫描(<DSH_HOME>/sessions/<projectKey(cwd)>,projectKey编码逐字移植自 dsh-session-persistence-jsonl)。 - 恢复走 DSH 官方
agents.resume({resumeSessionId})(factory 内部sessionPersistence.prepare),setup 组装复用 ensureAgent 全链路,并用会话存储的 preset 重组(与 GUI resume 的 preset-unchanged 断言一致)。旧 agent rotate 式 detach(不 dispose,GUI 会话行与日志保留)。 - 新增
DshSessionBackend.resumeAgent(memory 后端同步实现),/resume注册为 Tier-1 桥命令(优先于 DSH 自带 /resume 的文本回复)。
Fix: /lark-config streaming.enabled=true 报「不可热改」+ 流式管线从未接线
- 根因 1:
/lark-config只匹配白名单顶级键,点路径streaming.enabled永远进不去 —— 新增buildHotReloadPatch()解析点路径为嵌套 patch(未知子键/超深层级显式报错)。 - 根因 2:即使热改成功,
streamFor().ensureStream是返回undefined的桩,且cardkit-stream.ts的载荷与官方 CardKit v1 API 不符(无 receive/deliver、缺 element_id、sequence 语义错误)。 - 修复:
cardkit-stream.ts按官方文档重写 ——POST /cardkit/v1/cards建实体(config.streaming_mode+element_id)→im/v1/messages以{"type":"card","data":{"card_id"}}投递(实体仅可发一次)→PUT /cards/:id/elements/:element_id/content全量文本打字机 → finalize 先PATCH …/settings关流再PUT /cards/:id定稿(失败 re-throw 落回 outbox,内容不丢);sequence同卡严格递增。lark 客户端新增 5 个 cardkit 方法(经sdkClient.request),index.ts 按会话缓存 handle(同轮复用,disposed 后换新)。
Fix: Windows 绝对路径被 /workspace 与文件工具全量拒绝 (GH #7)
- 根因:
startsWith("/")判绝对路径 —— Windows 盘符(D:\…)与 UNC(\\server\…)全部被误判为相对路径:/workspace永远回「无效路径」,lark_send_local_file把盘符路径 join 到工作区后再以「路径不在工作区内」拒绝。 - 修复:新增
common/paths.ts——resolveWorkspaceTarget/resolveInWorkspacePath用node:path的isAbsolute(外加 win32 形状识别,跨平台兜底)与relative()做包含性检查;两处调用点已切换。
Fix: 桥的 permissionMode 不再污染宿主全局权限默认 (GH #8)
- 根因:启动与每次
/permission都把桥的permissionMode写进宿主settings.permission.defaultPreset(@deepseek-ai/dsh-permission-presets将其作为全部署未来会话的默认),装上插件即静默把整个部署翻成danger-full-access。 - 修复(issue 内已验证的三段式):① 删除
syncDefaultPermission及其全部调用;②dsh-adapter在 create/resume 后对该会话应用permissionPresets.apply(session, mode, cb → approval.setPolicy)(新增permissionModedep);③ 桥自身默认仍为 full access(仅辖桥会话)。/permission现在只影响桥会话,回执文案注明「仅桥接会话生效」。
Spec
.spec/2026-08-18-feishu-resume-streaming-gh-issues-spec.md—— 全部变更先 spec 后 TDD(每个修复含复现测试)。
0.3.3
Fix: GUI model switch no longer leaks into other conversations / workspaces
- Root cause: a model switch in the DSH Web GUI writes the GLOBAL
agentDefaultModelservice. The bridge's poll detected the change andsyncModelFollowers()actively pushed the new default into every conversation that had never run/modelitself, plus sent a "default model changed" notification. Result: switching the model in one chat (or one workspace) silently changed other chats and other workspaces. - Fix: removed the follower push + notification. A GUI-side switch now only affects conversations created AFTER the switch; every existing conversation keeps its own model (whether set via
/modelor the default it had at first use). Feishu-side/modelwas already per-conversation and is unchanged.
Fix: bridge fails to start — dangling @larksuiteoapi/node-sdk symlink
- Root cause:
node_modules/@larksuiteoapi/node-sdkwas a symlink pointing into a deleted sibling project, sobuildLarkClient's dynamic import threw "Cannot find package" andstartBridgesilently bailed (startBlocker set, no status write, no WS connection). Feishu messages were never received. - Fix: replaced the dangling symlink with a real
npm installof the SDK. Verified end-to-end with real credentials (bot info + WS long connection).
0.3.2
Docs
- README mascot image now uses an absolute URL so it renders on npmjs.com (npm does not resolve relative image paths).
0.3.1
Bugfix: session id collision on idle disposal (GH #5)
- Root cause: when
disposeIdleremoved an agent, thegenerationscounter was not bumped. The nextensureAgent()reused the same session id, which collided with the persisted log left by the disposed agent — DSH rejected the first turn with "id collision", silently dropping the message. - Fix:
dsh-adapter.tsdisposeIdlenow synchronously removes fromtracked, bumpsgenerations, and THEN fire-and-forgets the async dispose.conversation-manager.tstracksagentIdper hook and re-attaches the fan-out listener when the agent changes. - Regression test:
manager: idle sweep then message still gets reply (stale hook after disposal)
New: per-conversation workspace / model / mode + hung-turn recovery
workspace/model/modeoverrides are now scoped per conversation key (persistedconversation-overrides.json); other chats no longer follow a switch when their agent is rebuilt (idle TTL,/new, maxSessions pressure).- Live model entries are per-key mutable objects for
installModelSelection; follower chats (no/modeloverride) track the bridge default — and the deployment default is polled so a GUI-side model switch is adopted and announced in Feishu. - Watchdog: an EMPTY assistant message no longer disarms the turn watchdog (fixes the hung-chat-until-
/newbug); text chunks and tool events refresh the deadline. - Card action callbacks get unique pseudo message ids so a second click on the same card (model picker) is not swallowed by outbox dedupe;
chatTyperesolved from the route table instead of hardcoded p2p. lark_send_local_fileresolves the per-conversation workspace.
Other
- Fixed
/lark restartpgrep pattern to match thedsh webinvocation. - Updated Feishu group invite link.
0.3.0
新增:入站请求补发(服务中断/插件更新/dsh 重启也能把没回答的消息补回来)
-
多选意图确认卡片:
ask_user_question的multi_select: true现在会渲染飞书 schema 2.0 的multi_select_static下拉多选 + 提交按钮,回调经uqam:<id>op 回传选中的多个选项(此前会被单选的按钮卡片忽略多选标记)。(GH #5) -
核心保证:一条用户消息被 Agent 接受但处理到一半(进程被杀 / 插件热更新 / dsh 重启)时,之前会静默丢失(dedupe 已消费、断连补偿只覆盖 WS 掉线窗口)。现在这类请求会持久化进 Inbound WAL(
<state>/inbound-wal/),重启后自动重新触发、把回答补发给用户。 -
实现:新增
src/inbound/inbound-wal.ts(崩溃安全的 JSONL 耐久日志,复用 outbox 的wx+rename 落盘纪律);message-handler 在交给 Agent 前只记录纯文本请求(媒体/命令不记);turn 的可交付输出落盘(outbox 入队 / 流式卡定稿)即标完成;启动时对账,把"已接受但未交付"的请求重新投递(handleCompensated,跳过 dedupe)。 -
防空转:每个请求限重放次数(默认 2 次)+ 重放时间窗(默认 30 分钟),损坏/超时的请求不会无限循环。
-
透明可见:
/status与 Web 面板新增「补发」计数,显示待补发条数,补完后归零。 -
边界:仅纯文本请求纳入补发;指令/图片/文件等不可靠重放对象不记录。幂等(dedupeKey + 已交付跳过)保证不会重复回答。
可靠性 / 稳定 · 5 项修复
针对「消息不丢失 + 各种中断补发 + 稳定」的安全网加固:
v0.3.3
Fix: GUI model switch no longer leaks into other conversations / workspaces
- Root cause: a model switch in the DSH Web GUI writes the GLOBAL
agentDefaultModelservice. The bridge's poll detected the change and actively pushed the new default into every conversation that had never run/modelitself, plus sent a "default model changed" notification. Result: switching the model in one chat (or one workspace) silently changed other chats and other workspaces. - Fix: removed the follower push + notification. A GUI-side switch now only affects conversations created AFTER the switch; every existing conversation keeps its own model. Feishu-side
/modelwas already per-conversation and is unchanged.
Fix: bridge fails to start — dangling @larksuiteoapi/node-sdk symlink
- Root cause:
node_modules/@larksuiteoapi/node-sdkwas a symlink pointing into a deleted sibling project, sobuildLarkClient's dynamic import threw "Cannot find package" andstartBridgesilently bailed — Feishu messages were never received. - Fix: replaced the dangling symlink with a real
npm installof the SDK. Verified end-to-end with real credentials (bot info + WS long connection).
Full changelog: v0.3.2...v0.3.3
v0.3.2
v0.3.1
What's New
Per-conversation workspace / model / mode + hung-turn recovery
workspace/model/modeoverrides are now scoped per conversation key (persistedconversation-overrides.json); other chats no longer follow a switch when their agent is rebuilt (idle TTL,/new, maxSessions pressure).- Live model entries are per-key mutable objects for
installModelSelection; follower chats track the bridge default — and the deployment default is polled so a GUI-side model switch is adopted and announced in Feishu. - Watchdog: an EMPTY assistant message no longer disarms the turn watchdog (fixes the hung-chat-until-
/newbug). - Card action callbacks get unique pseudo message ids so a second click on the same card (model picker) is not swallowed by outbox dedupe.
lark_send_local_fileresolves the per-conversation workspace.
Fixes
- Session id collision on idle disposal (GH #5):
disposeIdlenow bumps thegenerationscounter before async dispose, so the nextensureAgent()mints a fresh session id instead of colliding with the persisted log (which silently dropped the first message). - Fixed
/lark restartpgrep pattern to match thedsh webinvocation. - Updated Feishu group invite link.
Docs & CI
- README rewritten: npm/CI badges, "Why dsh-lark-link" highlights, configuration table, corrected install commands.
- Added GitHub Actions CI (typecheck + 162 tests + build) and npm Publish workflow.
Full changelog: v0.3.0...v0.3.1
dsh plugin --profile web update dsh-lark-link --latest --ignore-scripts