Skip to content

Releases: amlyczz/dsh-lark-link

v0.6.0

Choose a tag to compare

@amlyczz amlyczz released this 17 Oct 19:45

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)

Choose a tag to compare

@amlyczz amlyczz released this 12 Oct 07:17

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)

Choose a tag to compare

@amlyczz amlyczz released this 21 Sep 07:54

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(对齐 SDK UserInfo)

验证

  • 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)

Choose a tag to compare

@amlyczz amlyczz released this 18 Sep 09:10

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

Choose a tag to compare

@amlyczz amlyczz released this 25 Aug 04:12

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

Choose a tag to compare

@amlyczz amlyczz released this 20 Aug 11:19

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

Choose a tag to compare

@amlyczz amlyczz released this 20 Aug 09:54

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(用户主动取消 ≠ 卡死),下一条消息继续原会话上下文。
  • 图片:模型找不到图片(双根因)
    1. ctx.get("attachments") 在插件加载时被一次性快照——DSH 附件服务若晚于插件挂载(Cordis 加载顺序),永远是 undefined,imageRef 从不生成,ImageBlock 从不入 content,模型对图片一无所知。改为 live getter(attachmentsRef,违反的正是 bridge-context 自己头部声明的 GETTER 原则)。
    2. 即使无 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:path join + 现有 isAbsoluteAny/relative 判定(round-1 GH #7 已覆盖 UNC/盘符路径),无硬编码分隔符。
  • 非视觉模型发图报错「本轮运行失败 UNSUPPORTED_CONTENT」(qwen3.8-27b / DeepSeek 等):
    1. 正则漏匹配 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 等各类非视觉模型报错。
    2. 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),正确唤醒新一轮纯文本执行(带本地落盘图片路径),用户正常获得回复。
    3. 双重记忆与切模型重置:同时按会话 (imageUnsupportedKeys) 与模型 (imageUnsupportedModels) 记忆非视觉属性;后续发图直接走纯文本落盘路径(避免每张图都走一遍报错重试);使用 /model 切换模型时自动清除会话标记,切回视觉模型时可恢复 ImageBlock。
  • 流式卡片: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> 与卡片编号严格一致)、空态与「新起会话/切工作区」提示、恢复成功回执显示会话起始时间。

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)(新增 permissionMode dep);③ 桥自身默认仍为 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 agentDefaultModel service. The bridge's poll detected the change and syncModelFollowers() actively pushed the new default into every conversation that had never run /model itself, 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 /model or the default it had at first use). Feishu-side /model was already per-conversation and is unchanged.

Fix: bridge fails to start — dangling @larksuiteoapi/node-sdk symlink

  • Root cause: node_modules/@larksuiteoapi/node-sdk was a symlink pointing into a deleted sibling project, so buildLarkClient's dynamic import threw "Cannot find package" and startBridge silently bailed (startBlocker set, no status write, no WS connection). Feishu messages were never received.
  • Fix: replaced the dangling symlink with a real npm install of 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 disposeIdle removed an agent, the generations counter was not bumped. The next ensureAgent() 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.ts disposeIdle now synchronously removes from tracked, bumps generations, and THEN fire-and-forgets the async dispose. conversation-manager.ts tracks agentId per 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 / mode overrides are now scoped per conversation key (persisted conversation-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 /model override) 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-/new bug); 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; chatType resolved from the route table instead of hardcoded p2p.
  • lark_send_local_file resolves the per-conversation workspace.

Other

  • Fixed /lark restart pgrep pattern to match the dsh web invocation.
  • 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 项修复

针对「消息不丢失 + 各种中断补发 + 稳定」的安全网加固:

  • #1 修复:outbox 磁盘无限膨胀 —— outbox.prune() 从未被调用,done/fatal envelope 超保留期(默认 7 天)永不清理。现 start() 自调度周期清理(约 1h,可 pruneIntervalMs 覆盖),磁盘 segments 不再无限增长。
  • **#2...
Read more

v0.3.3

Choose a tag to compare

@amlyczz amlyczz released this 17 Aug 18:53

Fix: GUI model switch no longer leaks into other conversations / workspaces

  • Root cause: a model switch in the DSH Web GUI writes the GLOBAL agentDefaultModel service. The bridge's poll detected the change and actively pushed the new default into every conversation that had never run /model itself, 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 /model was already per-conversation and is unchanged.

Fix: bridge fails to start — dangling @larksuiteoapi/node-sdk symlink

  • Root cause: node_modules/@larksuiteoapi/node-sdk was a symlink pointing into a deleted sibling project, so buildLarkClient's dynamic import threw "Cannot find package" and startBridge silently bailed — Feishu messages were never received.
  • Fix: replaced the dangling symlink with a real npm install of 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

Choose a tag to compare

@amlyczz amlyczz released this 17 Aug 14:53

Docs

  • README mascot image now uses an absolute URL so it renders on npmjs.com (npm does not resolve relative image paths). No functional changes.

v0.3.1

Choose a tag to compare

@amlyczz amlyczz released this 17 Aug 10:25

What's New

Per-conversation workspace / model / mode + hung-turn recovery

  • workspace / model / mode overrides are now scoped per conversation key (persisted conversation-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-/new bug).
  • 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_file resolves the per-conversation workspace.

Fixes

  • Session id collision on idle disposal (GH #5): disposeIdle now bumps the generations counter before async dispose, so the next ensureAgent() mints a fresh session id instead of colliding with the persisted log (which silently dropped the first message).
  • Fixed /lark restart pgrep pattern to match the dsh web invocation.
  • 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