dsh-lark-bot 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -127,7 +127,7 @@ DSH_LARK_APP_ID=cli_xxx DSH_LARK_APP_SECRET=<secret> DSH_LARK_TENANT=feishu \
127
127
  > 卸载:`dsh plugin --profile dsh-lark remove dsh-lark-bot`。
128
128
  > Uninstall: `dsh plugin --profile dsh-lark remove dsh-lark-bot`.
129
129
 
130
- ### 4. 基本使用 | Basic usage
130
+ ### 3. 基本使用 | Basic usage
131
131
 
132
132
  在飞书里向 bot 发送普通消息即可开始工作,常用命令:
133
133
 
@@ -231,8 +231,9 @@ you answer — no extra command needed. The run-timeout watchdog pauses while a
231
231
  - `/safemode stop`:终止当前正在运行的安全模式任务(也可点击任务卡片上的 ⏹ 按钮);
232
232
  - `/safemode exit`:退出安全模式,守护重启完整 profile 并把飞书通道交还给正常形态;
233
233
 
234
- 安全模式任务有墙钟超时(`DSH_LARK_GUARDIAN_SAFE_TIMEOUT_MS`,默认 10 分钟),超时或失败都会
235
- 在卡片上给出明确终态,不会无声挂起。全程不需要命令行;dsh 恢复后守护自动断开并回归静默。安装:
234
+ 安全模式任务有**空闲超时**(`DSH_LARK_GUARDIAN_SAFE_TIMEOUT_MS`,默认 10 分钟:任务持续无
235
+ 活动事件才被终止,活跃的流式任务不会被误杀),超时或失败都会在卡片上给出明确终态,不会无声
236
+ 挂起。全程不需要命令行;dsh 恢复后守护自动断开并回归静默。安装:
236
237
 
237
238
  ```bash
238
239
  # 随 setup 默认安装(无需额外参数);已安装后也可单独安装 / 重装:
@@ -258,8 +259,9 @@ takes over the Feishu channel so you can self-heal without touching the command
258
259
  - `/safemode exit`: leave safe mode — the guardian relaunches the full profile and hands the
259
260
  Feishu channel back;
260
261
 
261
- Safe-mode tasks are bounded by a wall-clock timeout
262
- (`DSH_LARK_GUARDIAN_SAFE_TIMEOUT_MS`, default 10 minutes); timeouts and failures always surface a
262
+ Safe-mode tasks are bounded by an **idle timeout**
263
+ (`DSH_LARK_GUARDIAN_SAFE_TIMEOUT_MS`, default 10 minutes: a task is stopped only after it has
264
+ been silent for the whole window, so active streaming work is never cut short); timeouts and failures always surface a
263
265
  clear terminal state on the card instead of hanging silently. No command line is needed for the
264
266
  whole rescue flow; once dsh is back, the guardian releases the channel automatically. Install:
265
267
 
@@ -494,7 +496,7 @@ Core environment variables:
494
496
  | `DSH_LARK_MAX_TOKENS` | 未设置 | SDK agent 每请求输出 token 上限<br>Per-request output token cap for SDK agents |
495
497
  | `DSH_LARK_ACCESS_DEFAULT_DENY` | `false` | 无白名单时拒绝私聊<br>Reject private chats when no allowlist is configured |
496
498
  | `DSH_LARK_EVENT_FRESHNESS_MS` | `600000` | 过期消息拒绝窗口(0 关闭)<br>Stale-message rejection window (0 disables) |
497
- | `DSH_LARK_RUN_TIMEOUT_MS` | `300000` | 单次运行墙钟超时<br>Wall-clock timeout for a single run |
499
+ | `DSH_LARK_RUN_TIMEOUT_MS` | `300000` | 单次运行空闲超时:持续无活动事件才终止(活跃任务不会被误杀)<br>Idle timeout for a single run: stops only after the run has been silent for this long |
498
500
  | `DSH_LARK_STOP_GRACE_MS` | `5000` | SIGTERM 后等待优雅退出再 SIGKILL 的宽限期<br>Grace period after SIGTERM before SIGKILL |
499
501
  | `DSH_LARK_SCOPE_CONCURRENCY` | `2` | 每个 scope 的并行任务数(1=严格串行)<br>Concurrent runs per scope (1 = strictly serial) |
500
502
  | `DSH_LARK_RETENTION_MSGS` | `40` | 每个 scope 保留的消息条数(0=全部保留)<br>Messages kept per scope (0 keeps everything) |
@@ -508,7 +510,7 @@ Core environment variables:
508
510
  | `DSH_LARK_GUARDIAN_STALE_MS` | `15000` | 心跳超时阈值,超过且无 dsh 进程则接管飞书通道<br>Heartbeat staleness threshold before channel takeover |
509
511
  | `DSH_LARK_GUARDIAN_ENGINE_DEAD_MS` | `120000` | dsh 进程存活但心跳持续超时该时长,判定桥接引擎已死并接管<br>Live dsh process with heartbeat stale this long is treated as engine-dead (takeover) |
510
512
  | `DSH_LARK_GUARDIAN_SAFE_ADAPTER` | `auto` | 安全模式引擎:`auto` 优先 SDK 流式、失败回退 headless;`sdk` 强制 SDK;`headless` 跳过预置<br>Safe-mode engine: `auto` tries the SDK streaming runtime then falls back to headless; `sdk` requires it; `headless` skips provisioning |
511
- | `DSH_LARK_GUARDIAN_SAFE_TIMEOUT_MS` | `600000` | 安全模式单任务墙钟超时(到时停止并出超时卡)<br>Safe-mode per-task wall-clock timeout (stops the run and renders a timeout card) |
513
+ | `DSH_LARK_GUARDIAN_SAFE_TIMEOUT_MS` | `600000` | 安全模式单任务空闲超时(持续无活动事件才停止并出超时卡)<br>Safe-mode per-task idle timeout (stops the run after it has been silent this long and renders a timeout card) |
512
514
  | `DSH_LARK_GUARDIAN_CARD_DENSITY` | `detailed` | 安全模式任务卡片密度(compact / standard / detailed)<br>Card density for safe-mode run cards |
513
515
 
514
516
  启动时会自动查找本机常见的 `@deepseek-ai/dsh` 安装位置。只有自动发现失败或需要指定特殊 profile 时,才需要设置这两个变量。
@@ -574,14 +576,14 @@ availability probe for the current adapter (`sdk` / `acp` / `headless` runtime h
574
576
  Common issues:
575
577
 
576
578
  - **bot 静默 / 长连接失败**:查看 stderr 上的 JSONL 日志,关注 `channel` 与 `channel-command` 类别;SDK 会自动重连。
577
- - **agent 无响应**:发送 `/status` 查看当前 scope、cwd 和 active run;发送 `/stop` 终止当前任务;超过 `DSH_LARK_RUN_TIMEOUT_MS` 时看门狗会自动终止。
579
+ - **agent 无响应**:发送 `/status` 查看当前 scope、cwd 和 active run;发送 `/stop` 终止当前任务;持续无响应超过 `DSH_LARK_RUN_TIMEOUT_MS` 时看门狗会自动终止(空闲超时,活跃任务不会被误杀)。
578
580
  - **首次扫码失败**:确认本机时间准确、网络可访问飞书开放平台;已拿到 App ID/Secret 时可用 `--app-id` / `--app-secret` 跳过扫码。
579
581
 
580
582
  - **Silent bot / long-connection failure**: check the JSONL logs on stderr, focusing on the
581
583
  `channel` and `channel-command` categories; the SDK reconnects automatically.
582
584
  - **Unresponsive agent**: send `/status` to view the scope, cwd and active run; send `/stop` to
583
- terminate the current task; the watchdog terminates it automatically after
584
- `DSH_LARK_RUN_TIMEOUT_MS`.
585
+ terminate the current task; the idle watchdog terminates it automatically after it has been
586
+ silent for `DSH_LARK_RUN_TIMEOUT_MS` (active streaming work is never cut short).
585
587
  - **First QR binding fails**: make sure the local clock is accurate and the Feishu open platform
586
588
  is reachable; with an existing App ID/Secret you can skip scanning via `--app-id` /
587
589
  `--app-secret`.
package/cordis.patch.yml CHANGED
@@ -1,12 +1,13 @@
1
1
  # dsh-lark-bot as a profile bundle.
2
-
3
- # Installed with `dsh plugin --profile <name> add dsh-lark-bot` (or the
4
- # single `dsh-lark-bot setup` command), dsh appends this package to the profile's
2
+ #
3
+ # Installed with `dsh plugin --profile <name> add dsh-lark-bot` (or the single
4
+ # `dsh-lark-bot setup` command), dsh appends this package to the profile's
5
5
  # `dsh.profile.bundles` and applies this patch on boot:
6
6
  # - `dsh-lark-bot/plugin` starts the full bridge engine IN-PROCESS (Feishu
7
7
  # channel, workspace/session layers, notify server, nested dsh SDK
8
8
  # runtime) and exposes `ctx.larkBridge` (status / stop).
9
- # - `dsh-lark-bot/notify` mounts the `lark_notify` tool for the host agent.
9
+ # - `dsh-lark-bot/notify` mounts the `lark_notify` tool for the host agent
10
+ # (mentions + cross-session messages).
10
11
  # First boot without credentials prints a QR code for one-time binding.
11
12
  # Set DSH_LARK_DISABLED=1 to keep the engine stopped.
12
13
 
package/dist/cli.js CHANGED
@@ -1146,6 +1146,7 @@ function waitWithTimeout(promise, timeoutMs) {
1146
1146
  var SdkDshAdapter = class {
1147
1147
  id = "dsh-sdk";
1148
1148
  displayName = "DeepSeek Harness (SDK)";
1149
+ resumeCapable = true;
1149
1150
  launch;
1150
1151
  provider;
1151
1152
  model;
@@ -2901,7 +2902,41 @@ async function runAgentBatch(input) {
2901
2902
  const workspace = input.workspaceManager ? await input.workspaceManager.ensure(input.scope, requestedCwd) : { cwd: requestedCwd };
2902
2903
  const cwd = workspace.cwd;
2903
2904
  const sessionId = activeBefore === 0 ? input.sessions.resumeFor(input.scope, cwd) : void 0;
2904
- const history = input.sessions.historyFor(input.scope, cwd);
2905
+ const resuming = sessionId !== void 0 && input.adapter.resumeCapable === true;
2906
+ try {
2907
+ await runAttempt(input, cwd, sessionId, resuming, replyOptions);
2908
+ } catch (error) {
2909
+ if (resuming) {
2910
+ log.warn("run-flow", "resume-fallback", { scope: input.scope, sessionId });
2911
+ input.sessions.clearSession(input.scope);
2912
+ try {
2913
+ await runAttempt(input, cwd, void 0, false, replyOptions);
2914
+ return;
2915
+ } catch (retryError) {
2916
+ log.fail("run-flow", retryError, {
2917
+ scope: input.scope,
2918
+ step: "resume-fallback"
2919
+ });
2920
+ await reportRunFailure(input, retryError, replyOptions);
2921
+ return;
2922
+ }
2923
+ }
2924
+ await reportRunFailure(input, error, replyOptions);
2925
+ }
2926
+ }
2927
+ async function reportRunFailure(input, error, replyOptions) {
2928
+ log.fail("run-flow", error, { scope: input.scope });
2929
+ try {
2930
+ await input.channel.sendMarkdown(
2931
+ input.chatId,
2932
+ `\u26A0\uFE0F agent \u8FD0\u884C\u5931\u8D25\uFF1A${errorMessage(error)}`,
2933
+ replyOptions
2934
+ );
2935
+ } catch {
2936
+ }
2937
+ }
2938
+ async function runAttempt(input, cwd, sessionId, resuming, replyOptions) {
2939
+ const history = resuming ? [] : input.sessions.historyFor(input.scope, cwd);
2905
2940
  const prompt = buildPrompt(history, input.messages, input.role);
2906
2941
  const runId = randomUUID4();
2907
2942
  const run = input.adapter.run({
@@ -2938,6 +2973,8 @@ async function runAgentBatch(input) {
2938
2973
  input.chatId,
2939
2974
  renderCard(state, density),
2940
2975
  async (controller) => {
2976
+ let timeoutTimer;
2977
+ let armTimeout;
2941
2978
  const ticker = setInterval(() => {
2942
2979
  void controller.update(renderCard(state, density, Date.now())).catch(() => {
2943
2980
  });
@@ -2956,13 +2993,13 @@ async function runAgentBatch(input) {
2956
2993
  if (event.type === "system" && event.sessionId) {
2957
2994
  input.sessions.set(input.scope, event.sessionId, event.cwd ?? cwd);
2958
2995
  }
2996
+ armTimeout?.();
2959
2997
  await controller.update(renderCard(state, density));
2960
2998
  }
2961
2999
  };
2962
- let timeoutTimer;
2963
- let armTimeout;
2964
3000
  const timeoutPromise = timeoutMs > 0 ? new Promise((resolve5) => {
2965
3001
  armTimeout = () => {
3002
+ if (timeoutTimer !== void 0) clearTimeout(timeoutTimer);
2966
3003
  timeoutTimer = setTimeout(() => {
2967
3004
  if (timedOut) return;
2968
3005
  if (input.questions?.pendingCount(input.scope)) {
@@ -3008,15 +3045,6 @@ async function runAgentBatch(input) {
3008
3045
  }).then(() => pruneArchives(input))
3009
3046
  } : {}
3010
3047
  });
3011
- } catch (error) {
3012
- log.fail("run-flow", error, { scope: input.scope, runId });
3013
- state = markInterrupted(state);
3014
- try {
3015
- await input.channel.sendMarkdown(input.chatId, `\u26A0\uFE0F agent \u8FD0\u884C\u5931\u8D25\uFF1A${errorMessage(error)}`, {
3016
- ...replyOptions
3017
- });
3018
- } catch {
3019
- }
3020
3048
  } finally {
3021
3049
  input.activeRuns.delete(input.scope, runId);
3022
3050
  if (input.approvals) {
@@ -4159,7 +4187,7 @@ var HELP = [
4159
4187
  "- `/status` \u2014 \u67E5\u770B\u5F53\u524D\u72B6\u6001",
4160
4188
  "- `/resume` \u2014 \u67E5\u770B\u5F53\u524D\u4F1A\u8BDD\u6700\u8FD1\u4E0A\u4E0B\u6587",
4161
4189
  "- `/stop` \u2014 \u7EC8\u6B62\u5F53\u524D\u4EFB\u52A1",
4162
- "- `/timeout [N|off|default]` \u2014 \u67E5\u770B\u6216\u8BBE\u7F6E\u5F53\u524D\u4F1A\u8BDD\u8FD0\u884C\u8D85\u65F6",
4190
+ "- `/timeout [N|off|default]` \u2014 \u67E5\u770B\u6216\u8BBE\u7F6E\u5F53\u524D\u4F1A\u8BDD\u7A7A\u95F2\u8D85\u65F6\uFF08\u6301\u7EED\u65E0\u6D3B\u52A8\u4E8B\u4EF6 N \u5206\u949F\u624D\u7EC8\u6B62\uFF09",
4163
4191
  "- `/concurrency [N|default]` \u2014 \u67E5\u770B\u6216\u8BBE\u7F6E\u5F53\u524D scope \u7684\u5E76\u884C\u4EFB\u52A1\u6570",
4164
4192
  "- `/role list|show <id>|set <id>|clear` \u2014 \u67E5\u770B / \u7ED1\u5B9A\u89D2\u8272",
4165
4193
  "- `/role save <id> <name> [--persona \u6587\u6848] [--model <id>] [--tools <csv>] [--rules \u6587\u6848]` \u2014 \u521B\u5EFA/\u66F4\u65B0\u89D2\u8272\uFF08\u7BA1\u7406\u5458\uFF09",
@@ -4314,18 +4342,18 @@ async function handleTimeout(args, ctx) {
4314
4342
  const minutes2 = effectiveMs > 0 ? Math.round(effectiveMs / 6e4) : 0;
4315
4343
  await reply5(
4316
4344
  ctx,
4317
- minutes2 > 0 ? `\u5F53\u524D\u4F1A\u8BDD\u8FD0\u884C\u8D85\u65F6\uFF1A${minutes2} \u5206\u949F\u3002\u53EF\u7528 \`/timeout <N|off|default>\` \u8C03\u6574\u3002` : "\u5F53\u524D\u4F1A\u8BDD\u8FD0\u884C\u8D85\u65F6\uFF1A\u5173\u95ED\u3002"
4345
+ minutes2 > 0 ? `\u5F53\u524D\u4F1A\u8BDD\u7A7A\u95F2\u8D85\u65F6\uFF1A\u6301\u7EED\u65E0\u6D3B\u52A8\u4E8B\u4EF6 ${minutes2} \u5206\u949F\u624D\u7EC8\u6B62\u3002\u53EF\u7528 \`/timeout <N|off|default>\` \u8C03\u6574\u3002` : "\u5F53\u524D\u4F1A\u8BDD\u7A7A\u95F2\u8D85\u65F6\uFF1A\u5173\u95ED\u3002"
4318
4346
  );
4319
4347
  return;
4320
4348
  }
4321
4349
  if (input === "off") {
4322
4350
  ctx.runPolicies.set(ctx.scope, 0);
4323
- await reply5(ctx, "\u5DF2\u5173\u95ED\u5F53\u524D\u4F1A\u8BDD\u8FD0\u884C\u8D85\u65F6\u3002");
4351
+ await reply5(ctx, "\u5DF2\u5173\u95ED\u5F53\u524D\u4F1A\u8BDD\u7A7A\u95F2\u8D85\u65F6\u3002");
4324
4352
  return;
4325
4353
  }
4326
4354
  if (input === "default") {
4327
4355
  ctx.runPolicies.clear(ctx.scope);
4328
- await reply5(ctx, "\u5DF2\u6062\u590D\u9ED8\u8BA4\u8FD0\u884C\u8D85\u65F6\u3002");
4356
+ await reply5(ctx, "\u5DF2\u6062\u590D\u9ED8\u8BA4\u7A7A\u95F2\u8D85\u65F6\u3002");
4329
4357
  return;
4330
4358
  }
4331
4359
  const minutes = Number(input);
@@ -4334,7 +4362,7 @@ async function handleTimeout(args, ctx) {
4334
4362
  return;
4335
4363
  }
4336
4364
  ctx.runPolicies.set(ctx.scope, minutes * 6e4);
4337
- await reply5(ctx, `\u5DF2\u8BBE\u7F6E\u5F53\u524D\u4F1A\u8BDD\u8FD0\u884C\u8D85\u65F6\uFF1A${minutes} \u5206\u949F\u3002`);
4365
+ await reply5(ctx, `\u5DF2\u8BBE\u7F6E\u5F53\u524D\u4F1A\u8BDD\u7A7A\u95F2\u8D85\u65F6\uFF1A\u6301\u7EED\u65E0\u6D3B\u52A8\u4E8B\u4EF6 ${minutes} \u5206\u949F\u624D\u7EC8\u6B62\u3002`);
4338
4366
  }
4339
4367
  async function handleConcurrency(args, ctx) {
4340
4368
  const input = args.trim();
@@ -5180,6 +5208,16 @@ var SessionStore = class {
5180
5208
  const record = this.data.chats[scopeId];
5181
5209
  return record && record.cwd === cwd ? record.sessionId : void 0;
5182
5210
  }
5211
+ /**
5212
+ * Drop the native dsh session binding for a scope. The transcript is kept so
5213
+ * the next run can start a fresh session and replay history from the store.
5214
+ */
5215
+ clearSession(scopeId) {
5216
+ const existing = this.data.chats[scopeId];
5217
+ if (!existing) return;
5218
+ this.data.chats[scopeId] = { ...existing, sessionId: void 0 };
5219
+ this.schedulePersist();
5220
+ }
5183
5221
  async flush() {
5184
5222
  await this.pendingArchive;
5185
5223
  await this.saving;
@@ -7185,6 +7223,7 @@ var GuardianService = class {
7185
7223
  }, SAFE_CARD_TICK_MS);
7186
7224
  ticker.unref?.();
7187
7225
  let timeoutTimer;
7226
+ let armTimeout;
7188
7227
  try {
7189
7228
  const consume = async () => {
7190
7229
  for await (const event of run.events) {
@@ -7201,20 +7240,25 @@ var GuardianService = class {
7201
7240
  this.sessionIds.set(scope, event.sessionId);
7202
7241
  }
7203
7242
  if (event.type === "error") errorText = event.message;
7243
+ armTimeout?.();
7204
7244
  await controller.update(renderCard(state, density, Date.now()));
7205
7245
  }
7206
7246
  };
7207
7247
  const timeoutPromise = timeoutMs > 0 ? new Promise((resolve5) => {
7208
- timeoutTimer = setTimeout(() => {
7209
- if (timedOut) return;
7210
- timedOut = true;
7211
- state = markIdleTimeout(
7212
- state,
7213
- Math.round(timeoutMs / 6e4)
7214
- );
7215
- void run.stop();
7216
- resolve5();
7217
- }, timeoutMs);
7248
+ armTimeout = () => {
7249
+ if (timeoutTimer !== void 0) clearTimeout(timeoutTimer);
7250
+ timeoutTimer = setTimeout(() => {
7251
+ if (timedOut) return;
7252
+ timedOut = true;
7253
+ state = markIdleTimeout(
7254
+ state,
7255
+ Math.round(timeoutMs / 6e4)
7256
+ );
7257
+ void run.stop();
7258
+ resolve5();
7259
+ }, timeoutMs);
7260
+ };
7261
+ armTimeout();
7218
7262
  }) : void 0;
7219
7263
  if (timeoutPromise) {
7220
7264
  await Promise.race([consume(), timeoutPromise]);