dsh-lark-bot 0.12.0 → 0.13.0

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
@@ -136,6 +136,7 @@ Just send a normal message to the bot in Feishu to get started. Common commands:
136
136
  | 命令 Command | 作用 Description |
137
137
  | --- | --- |
138
138
  | `/new` `/reset` | 开始新会话<br>Start a new session |
139
+ | `/newg <群名>` | 自动新建群聊(拉你入群)并开新会话,当前会话保留<br>Auto-create a group chat (with you invited) and start a fresh session there; the current session is untouched |
139
140
  | `/cd <path>` | 切换工作目录并重置会话<br>Change working directory and reset the session |
140
141
  | `/ws list` | 查看命名工作空间<br>List named workspaces |
141
142
  | `/ws save <name>` | 保存当前工作空间<br>Save the current workspace |
@@ -170,6 +171,10 @@ Just send a normal message to the bot in Feishu to get started. Common commands:
170
171
 
171
172
  Images in Feishu messages are downloaded to the local media directory and passed to dsh; text files are read and their content is injected into the task context.
172
173
 
174
+ **`/newg <群名>`**:通过飞书 API 自动新建一个私密群、把发送者拉入群,并回复群链接——在新群里发消息即为新 scope / 新会话,当前会话不受影响。需要应用具备 `im:chat` 与 `im:chat.members:write_only` 权限(在开发者后台「权限管理」申请)。
175
+
176
+ **`/newg <group name>`**: auto-creates a private group via the Feishu API, invites the sender, and replies with a group link — chatting in the new group starts a fresh scope/session while the current session is untouched. Requires the `im:chat` and `im:chat.members:write_only` scopes (apply in the developer console).
177
+
173
178
  同一 scope(私聊 / 群聊 / 话题)默认允许 **2 个任务并行**(`DSH_LARK_SCOPE_CONCURRENCY` 或
174
179
  `/concurrency` 调整):连续发来的多条消息会以独立 run 并行推进,每个 run 使用独立的 dsh
175
180
  session 与独立 runId,`/status` 展示全部运行中的 run,`/stop` 一次性终止全部任务。
@@ -718,6 +723,40 @@ The same dist is also published to GitHub Packages as `@plutokeating/dsh-lark-bo
718
723
 
719
724
  See "Community Listings" in the next section for ecosystem registration status.
720
725
 
726
+ ## 作者 | Author
727
+
728
+ 本项目由 **PlutoKeating** 开发并维护。作者专注于自动化与开发者工具,习惯从真实使用场景出发
729
+ 做软件:本项目正是从“用飞书 / Lark 群聊驱动 DeepSeek Agent”的日常需求长出来的,逐步演进为
730
+ 一套带守护、自愈与一键升级能力的完整桥接方案。更多信息见个人主页:
731
+ [PlutoKeating](https://github.com/PlutoKeating)。
732
+
733
+ This project is developed and maintained by **PlutoKeating**, who focuses on automation and
734
+ developer tooling and prefers building software from real usage. It grew out of the daily need
735
+ to drive DeepSeek agents from Feishu/Lark group chats, evolving into a complete bridge with
736
+ guardian, self-healing, and one-command upgrade capabilities. See the author's profile:
737
+ [PlutoKeating](https://github.com/PlutoKeating).
738
+
739
+ ## 贡献者 | Contributors
740
+
741
+ 感谢以下贡献者(按合入 / 提交时间):
742
+
743
+ Thanks to the following contributors (by merge / submission time):
744
+
745
+ | 贡献者 Contributor | 贡献 Contribution | 状态 Status |
746
+ | :--- | :--- | :--- |
747
+ | [koprivnikarurnaa-oss](https://github.com/koprivnikarurnaa-oss) | [PR #9](https://github.com/PlutoKeating/dsh-lark-bot/pull/9):web 单写者适配器 + self-heal v2 + 守护自动重启<br>Web single-writer adapter + self-heal v2 + guardian auto-relaunch | ✅ 已合入<br>Merged |
748
+ | [Normanyin](https://github.com/Normanyin) | [PR #11](https://github.com/PlutoKeating/dsh-lark-bot/pull/11):`/newg` 自动建群命令<br>`/newg` auto-create group chat command | ✅ 已合入(cherry-pick)<br>Merged (cherry-pick) |
749
+
750
+ > 说明:GitHub 贡献者图按 commit 作者邮箱归因。PR #9 合入时的提交使用了本地通用身份
751
+ > `dsh-user <dsh-user@local>`(未绑定 GitHub 账号),因此未自动计入贡献者图;本表为仓库侧
752
+ > 的明确署名,PR #11 的提交身份已绑定其账号,合入后会自动计入。
753
+ >
754
+ > Note: GitHub's contributor graph attributes commits by author email. The commits merged via
755
+ > PR #9 carried a local generic identity (`dsh-user <dsh-user@local>`, not linked to a GitHub
756
+ > account), so they are not auto-counted in the graph; this table is the repository's explicit
757
+ > acknowledgment. PR #11's commits are authored under the contributor's linked account and will
758
+ > be credited automatically once merged.
759
+
721
760
  ## 许可与安全 | License & Security
722
761
 
723
762
  - **许可证**:GNU Affero General Public License v3.0(见 `LICENSE`)。
@@ -820,39 +859,39 @@ and relaunches the full profile on `/safemode exit`.
820
859
  | :--- | :--- | :--- |
821
860
  | [awesome-dsh-plugins](https://github.com/AdamPlatin123/awesome-dsh-plugins) | ✅ 已收录 · 运行级可用<br>Listed · runtime-verified | 社区榜单标注 `✅ 运行级可用`,2026-08-14 agent 实测通过;收录条目更新至 v0.8.0([PR #127](https://github.com/AdamPlatin123/awesome-dsh-plugins/pull/127) 已合并),v0.10.2 同步已跟进([#139](https://github.com/AdamPlatin123/awesome-dsh-plugins/issues/139) 最新评论)<br>Shown as `✅ 运行级可用` in the community leaderboard; agent-tested on 2026-08-14; v0.8.0 entry merged via [PR #127](https://github.com/AdamPlatin123/awesome-dsh-plugins/pull/127); v0.10.2 sync requested in [#139](https://github.com/AdamPlatin123/awesome-dsh-plugins/issues/139) |
822
861
  | [dshfind](https://dshfind.com/zh/plugins/PlutoKeating/dsh-lark-bot) | ✅ 已收录 · 详情页已上线<br>Listed · detail page live | 中英日韩四语详情页已上线(含安装命令与亮点),条目名称正常([issue #2](https://github.com/hikariming/dshfind/issues/2) 已关闭);v0.10.2 数据刷新已提交([issue #6](https://github.com/hikariming/dshfind/issues/6));顶部徽章 / 展示卡来自 dshfind<br>Four-language detail page is live (install command & highlights), entry name fixed ([issue #2](https://github.com/hikariming/dshfind/issues/2) closed); v0.10.2 data refresh requested in [issue #6](https://github.com/hikariming/dshfind/issues/6); the header badge / card comes from dshfind |
823
- | [omdsh-dev/community](https://github.com/orgs/omdsh-dev/discussions/12) | ✅ 已提交收录申请<br>Submission submitted | `[Plugin]` 收录申请(Discussion #12)已通过;v0.8.0 更新、六项独家亮点与 v0.10.1 / v0.10.2 稳定性修复说明均已发布在该讨论<br>`[Plugin]` submission (Discussion #12) accepted; v0.8.0 update, the six-exclusive-highlights summary and the v0.10.1 / v0.10.2 stability-fix notes are all posted there |
862
+ | [omdsh-dev/community](https://github.com/orgs/omdsh-dev/discussions/11) | ✅ 已提交收录申请<br>Submission submitted | `[Plugin]` 收录申请(Discussion #11)已通过;v0.8.0 更新、六项独家亮点与 v0.10.1 / v0.10.2 稳定性修复说明均已发布在该讨论<br>`[Plugin]` submission (Discussion #11) accepted; v0.8.0 update, the six-exclusive-highlights summary and the v0.10.1 / v0.10.2 stability-fix notes are all posted there |
824
863
 
825
864
  **更新请求进度 / Update request status**:
826
865
 
827
866
  - awesome-dsh-plugins 收录条目更新:[#127](https://github.com/AdamPlatin123/awesome-dsh-plugins/pull/127) — ✅ 已合并
828
867
  - dshfind 数据刷新请求(含条目名称异常修正):[#2](https://github.com/hikariming/dshfind/issues/2) — ✅ 已关闭,详情页已更新
829
- - omdsh-dev/community 收录讨论更新:[Discussion #12 更新评论](https://github.com/orgs/omdsh-dev/discussions/12#discussioncomment-18021998) — ✅ 已发布
868
+ - omdsh-dev/community 收录讨论更新:[Discussion #11 更新评论](https://github.com/orgs/omdsh-dev/discussions/11#discussioncomment-18021998) — ✅ 已发布
830
869
  - awesome-dsh-plugins 榜单行同步至 v0.10.2:[#139 最新评论](https://github.com/AdamPlatin123/awesome-dsh-plugins/issues/139#issuecomment-5304382615) — 📨 已提交
831
870
  - dshfind 数据刷新至 v0.10.2:[#6 跟进评论](https://github.com/hikariming/dshfind/issues/6#issuecomment-5304382647) — 📨 已提交
832
- - omdsh-dev/community v0.10.1 更新说明:[Discussion #12 评论](https://github.com/omdsh-dev/community/discussions/12#discussioncomment-18034706) — ✅ 已发布
833
- - omdsh-dev/community v0.10.2 更新说明:[Discussion #12 评论](https://github.com/omdsh-dev/community/discussions/12#discussioncomment-18034903) — ✅ 已发布
871
+ - omdsh-dev/community v0.10.1 更新说明:[Discussion #11 评论](https://github.com/orgs/omdsh-dev/discussions/11#discussioncomment-18034706) — ✅ 已发布
872
+ - omdsh-dev/community v0.10.2 更新说明:[Discussion #11 评论](https://github.com/orgs/omdsh-dev/discussions/11#discussioncomment-18034903) — ✅ 已发布
834
873
 
835
874
  **Update requests**:
836
875
 
837
876
  - awesome-dsh-plugins entry refresh: [#127](https://github.com/AdamPlatin123/awesome-dsh-plugins/pull/127) — ✅ merged
838
877
  - dshfind data-refresh request (incl. fixing the entry name): [#2](https://github.com/hikariming/dshfind/issues/2) — ✅ closed, detail page updated
839
- - omdsh-dev/community listing update: [Discussion #12 update comment](https://github.com/orgs/omdsh-dev/discussions/12#discussioncomment-18021998) — ✅ posted
878
+ - omdsh-dev/community listing update: [Discussion #11 update comment](https://github.com/orgs/omdsh-dev/discussions/11#discussioncomment-18021998) — ✅ posted
840
879
  - awesome-dsh-plugins leaderboard sync to v0.10.2: [#139 comment](https://github.com/AdamPlatin123/awesome-dsh-plugins/issues/139#issuecomment-5304382615) — 📨 submitted
841
880
  - dshfind data refresh to v0.10.2: [#6 follow-up](https://github.com/hikariming/dshfind/issues/6#issuecomment-5304382647) — 📨 submitted
842
- - omdsh-dev/community v0.10.1 update: [Discussion #12 comment](https://github.com/omdsh-dev/community/discussions/12#discussioncomment-18034706) — ✅ posted
843
- - omdsh-dev/community v0.10.2 update: [Discussion #12 comment](https://github.com/omdsh-dev/community/discussions/12#discussioncomment-18034903) — ✅ posted
881
+ - omdsh-dev/community v0.10.1 update: [Discussion #11 comment](https://github.com/orgs/omdsh-dev/discussions/11#discussioncomment-18034706) — ✅ posted
882
+ - omdsh-dev/community v0.10.2 update: [Discussion #11 comment](https://github.com/orgs/omdsh-dev/discussions/11#discussioncomment-18034903) — ✅ posted
844
883
 
845
884
  **亮点跟进 / Highlights follow-ups**(六项独家能力与 issue #6 设计实现):
846
885
 
847
886
  - awesome-dsh-plugins 榜单行同步(仓库描述 → 最新)与 agent-test 报告名称异常:[#139](https://github.com/AdamPlatin123/awesome-dsh-plugins/issues/139) — 📨 已提交(维护方已确认,等待渲染周期同步)
848
887
  - dshfind 详情页补「对话内管理模型和密钥」亮点:[#2 跟进评论](https://github.com/hikariming/dshfind/issues/2#issuecomment-5301019067) — 📨 已提交
849
- - omdsh 六项独家亮点补充(含 Guardian 设计实现):[Discussion #12 亮点评论](https://github.com/orgs/omdsh-dev/discussions/12#discussioncomment-18026370) — 📨 已提交
888
+ - omdsh 六项独家亮点补充(含 Guardian 设计实现):[Discussion #11 亮点评论](https://github.com/orgs/omdsh-dev/discussions/11#discussioncomment-18026370) — 📨 已提交
850
889
 
851
890
  **Highlights follow-ups** (six exclusive capabilities & the issue #6 design):
852
891
 
853
892
  - awesome-dsh-plugins leaderboard row sync (repo description → latest) & agent-test name anomaly: [#139](https://github.com/AdamPlatin123/awesome-dsh-plugins/issues/139) — 📨 submitted (maintainer confirmed; awaiting the snapshot/render cycle)
854
893
  - dshfind detail page: add the in-chat model/key management highlight: [#2 follow-up](https://github.com/hikariming/dshfind/issues/2#issuecomment-5301019067) — 📨 submitted
855
- - omdsh six-exclusive-highlights summary (incl. the Guardian design): [Discussion #12 highlights comment](https://github.com/orgs/omdsh-dev/discussions/12#discussioncomment-18026370) — 📨 submitted
894
+ - omdsh six-exclusive-highlights summary (incl. the Guardian design): [Discussion #11 highlights comment](https://github.com/orgs/omdsh-dev/discussions/11#discussioncomment-18026370) — 📨 submitted
856
895
 
857
896
  ## 免责声明 | Disclaimer
858
897
 
package/dist/cli.js CHANGED
@@ -167,7 +167,7 @@ var init_own_package = __esm({
167
167
  });
168
168
 
169
169
  // src/adapters/dsh/acp-runtime.ts
170
- import { spawn as spawn4 } from "child_process";
170
+ import { spawn as spawn4 } from "cross-spawn";
171
171
  import { existsSync as existsSync4, realpathSync as realpathSync2 } from "fs";
172
172
  import { readFileSync as readFileSync3 } from "fs";
173
173
  import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
@@ -1395,7 +1395,7 @@ var SdkDshAdapter = class {
1395
1395
  init_dsh_compat();
1396
1396
  init_dsh_runtime();
1397
1397
  init_own_package();
1398
- import { spawn as spawn3 } from "child_process";
1398
+ import { spawn as spawn3 } from "cross-spawn";
1399
1399
  import { existsSync as existsSync3, realpathSync } from "fs";
1400
1400
  import { readFileSync as readFileSync2 } from "fs";
1401
1401
  import { mkdir, writeFile } from "fs/promises";
@@ -2127,9 +2127,9 @@ async function writeFileAtomic(target, data, options = {}) {
2127
2127
  }
2128
2128
  }
2129
2129
  async function rmSilently(path) {
2130
- const { rm: rm4 } = await import("fs/promises");
2130
+ const { rm: rm5 } = await import("fs/promises");
2131
2131
  try {
2132
- await rm4(path, { force: true });
2132
+ await rm5(path, { force: true });
2133
2133
  } catch {
2134
2134
  }
2135
2135
  }
@@ -3289,11 +3289,16 @@ function extractQuestionAnswer(kind, value, options) {
3289
3289
  }
3290
3290
 
3291
3291
  // src/session/heal.ts
3292
- import { mkdir as mkdir5, readdir, rename as rename2 } from "fs/promises";
3292
+ import { cp, mkdir as mkdir5, readdir, rm } from "fs/promises";
3293
3293
  import { homedir as homedir5 } from "os";
3294
3294
  import { join as join8 } from "path";
3295
- var SESSION_BROKEN_RE = /id collision|corrupt session log|already has a persisted log|does not match this live session/i;
3296
- var SESSION_CORRUPT_RE = /corrupt session log|seq gap/i;
3295
+ var BROKEN_ANCHORED = /session\s+["'`]?[A-Za-z0-9_-]+["'`]?\s+already has a persisted log(?: on disk)? that does not match this live session(?: \(id collision\))?|does not match this live session\s*\(id collision\)/i;
3296
+ var CORRUPT_ANCHORED = /corrupt(?:ed)? session log|session log[^\n]{0,80}?\bseq(?:uence)? gap\b|\bseq(?:uence)? gap\b[^\n]{0,80}?session log/i;
3297
+ function classifySessionError(message) {
3298
+ if (BROKEN_ANCHORED.test(message)) return "broken";
3299
+ if (CORRUPT_ANCHORED.test(message)) return "corrupt";
3300
+ return void 0;
3301
+ }
3297
3302
  async function archiveSessionDir(sessionId) {
3298
3303
  const home = homedir5();
3299
3304
  const dshHome = process.env.DSH_HOME?.trim() || join8(home, ".dsh");
@@ -3316,12 +3321,14 @@ async function archiveSessionDir(sessionId) {
3316
3321
  return void 0;
3317
3322
  };
3318
3323
  const found = await walk(root);
3319
- if (found === void 0) return false;
3324
+ if (found === void 0) return { found: false };
3320
3325
  const larkHome = process.env.DSH_LARK_HOME?.trim() || join8(home, ".dsh-lark");
3321
3326
  const bakRoot = join8(larkHome, "_archived-sessions");
3327
+ const archivePath = join8(bakRoot, `${sessionId}-${Date.now()}`);
3322
3328
  await mkdir5(bakRoot, { recursive: true });
3323
- await rename2(found, join8(bakRoot, `${sessionId}-${Date.now()}`));
3324
- return true;
3329
+ await cp(found, archivePath, { recursive: true, force: true });
3330
+ await rm(found, { recursive: true, force: true });
3331
+ return { found: true, archivePath };
3325
3332
  }
3326
3333
 
3327
3334
  // src/bridge/run-flow.ts
@@ -3344,10 +3351,20 @@ async function runAgentBatch(input) {
3344
3351
  } catch (error) {
3345
3352
  if (resuming) {
3346
3353
  log.warn("run-flow", "resume-fallback", { scope: input.scope, sessionId });
3347
- if (sessionId !== void 0 && SESSION_CORRUPT_RE.test(errorMessage(error))) {
3354
+ if (sessionId !== void 0 && classifySessionError(errorMessage(error)) === "corrupt") {
3348
3355
  try {
3349
- await archiveSessionDir(sessionId);
3350
- } catch {
3356
+ const archived = await archiveSessionDir(sessionId);
3357
+ if (archived.archivePath !== void 0) {
3358
+ log.info("session", "heal-archived", {
3359
+ sessionId,
3360
+ archivePath: archived.archivePath
3361
+ });
3362
+ }
3363
+ } catch (archiveError) {
3364
+ log.fail("session", "heal-archive-failed", {
3365
+ sessionId,
3366
+ error: archiveError
3367
+ });
3351
3368
  }
3352
3369
  }
3353
3370
  input.sessions.clearSession(input.scope);
@@ -3428,20 +3445,32 @@ async function runAttempt(input, cwd, sessionId, resuming, replyOptions) {
3428
3445
  if (timedOut) return;
3429
3446
  state = applyEvent(state, event, stopRequested);
3430
3447
  state = { ...state, lastActivityMs: Date.now() };
3431
- if (event.type === "error" && event.terminationReason === "failed" && SESSION_BROKEN_RE.test(event.message) && !(resuming && !sawActivity)) {
3448
+ const healKind = event.type === "error" && event.terminationReason === "failed" ? classifySessionError(event.message) : void 0;
3449
+ if (event.type === "error" && event.terminationReason === "failed" && healKind !== void 0 && !(resuming && !sawActivity)) {
3432
3450
  const brokenId = input.sessions.getRaw(input.scope)?.sessionId;
3433
3451
  if (brokenId !== void 0) {
3434
- const corruptLog = SESSION_CORRUPT_RE.test(event.message);
3435
- if (corruptLog) {
3452
+ let archivePath;
3453
+ if (healKind === "corrupt") {
3436
3454
  try {
3437
- await archiveSessionDir(brokenId);
3438
- } catch {
3455
+ const archived = await archiveSessionDir(brokenId);
3456
+ archivePath = archived.archivePath;
3457
+ if (archivePath !== void 0) {
3458
+ log.info("session", "heal-archived", {
3459
+ sessionId: brokenId,
3460
+ archivePath
3461
+ });
3462
+ }
3463
+ } catch (error) {
3464
+ log.fail("session", "heal-archive-failed", {
3465
+ sessionId: brokenId,
3466
+ error
3467
+ });
3439
3468
  }
3440
3469
  }
3441
3470
  input.sessions.clear(input.scope);
3442
3471
  await input.channel.sendMarkdown(
3443
3472
  input.chatId,
3444
- corruptLog ? "\u26A0\uFE0F \u4F1A\u8BDD\u8BB0\u5F55\u635F\u574F\uFF0C\u5DF2\u81EA\u52A8\u5F52\u6863\u5E76\u91CD\u7F6E\u3002\u8BF7\u91CD\u65B0\u53D1\u9001\u4F60\u7684\u6D88\u606F\u3002" : "\u26A0\uFE0F \u4F1A\u8BDD\u72B6\u6001\u5F02\u5E38\uFF0C\u5DF2\u91CD\u7F6E\u4F1A\u8BDD\u6620\u5C04\uFF08\u5386\u53F2\u65E5\u5FD7\u4FDD\u7559\uFF0C\u672A\u5220\u9664\uFF09\u3002\u8BF7\u91CD\u65B0\u53D1\u9001\u4F60\u7684\u6D88\u606F\u3002",
3473
+ healKind === "corrupt" ? `\u26A0\uFE0F \u4F1A\u8BDD\u8BB0\u5F55\u635F\u574F\uFF0C\u5DF2\u5F52\u6863\u5E76\u91CD\u7F6E\uFF08\u5F52\u6863\uFF1A\`${archivePath ?? "\u5F52\u6863\u5931\u8D25"}\`\uFF09\u3002\u8BF7\u91CD\u65B0\u53D1\u9001\u4F60\u7684\u6D88\u606F\u3002` : "\u26A0\uFE0F \u4F1A\u8BDD\u72B6\u6001\u5F02\u5E38\uFF0C\u5DF2\u91CD\u7F6E\u4F1A\u8BDD\u6620\u5C04\uFF08\u5386\u53F2\u65E5\u5FD7\u4FDD\u7559\uFF0C\u672A\u5220\u9664\uFF09\u3002\u8BF7\u91CD\u65B0\u53D1\u9001\u4F60\u7684\u6D88\u606F\u3002",
3445
3474
  { ...replyOptions }
3446
3475
  );
3447
3476
  void run.stop();
@@ -3519,20 +3548,32 @@ async function runAttempt(input, cwd, sessionId, resuming, replyOptions) {
3519
3548
  state = markInterrupted(state);
3520
3549
  if (resuming && !sawActivity) throw error;
3521
3550
  const runErrorText = errorMessage(error);
3522
- if (SESSION_BROKEN_RE.test(runErrorText)) {
3551
+ const healKind = classifySessionError(runErrorText);
3552
+ if (healKind !== void 0) {
3523
3553
  const brokenSessionId = input.sessions.getRaw(input.scope)?.sessionId;
3524
3554
  if (brokenSessionId !== void 0) {
3525
- const corruptLog = SESSION_CORRUPT_RE.test(runErrorText);
3526
- if (corruptLog) {
3555
+ let archivePath;
3556
+ if (healKind === "corrupt") {
3527
3557
  try {
3528
- await archiveSessionDir(brokenSessionId);
3529
- } catch {
3558
+ const archived = await archiveSessionDir(brokenSessionId);
3559
+ archivePath = archived.archivePath;
3560
+ if (archivePath !== void 0) {
3561
+ log.info("session", "heal-archived", {
3562
+ sessionId: brokenSessionId,
3563
+ archivePath
3564
+ });
3565
+ }
3566
+ } catch (archiveError) {
3567
+ log.fail("session", "heal-archive-failed", {
3568
+ sessionId: brokenSessionId,
3569
+ error: archiveError
3570
+ });
3530
3571
  }
3531
3572
  }
3532
3573
  input.sessions.clear(input.scope);
3533
3574
  await input.channel.sendMarkdown(
3534
3575
  input.chatId,
3535
- corruptLog ? "\u26A0\uFE0F \u4F1A\u8BDD\u8BB0\u5F55\u635F\u574F\uFF0C\u5DF2\u81EA\u52A8\u5F52\u6863\u5E76\u91CD\u7F6E\u3002\u8BF7\u91CD\u65B0\u53D1\u9001\u4F60\u7684\u6D88\u606F\u3002" : "\u26A0\uFE0F \u4F1A\u8BDD\u72B6\u6001\u5F02\u5E38\uFF0C\u5DF2\u91CD\u7F6E\u4F1A\u8BDD\u6620\u5C04\uFF08\u5386\u53F2\u65E5\u5FD7\u4FDD\u7559\uFF0C\u672A\u5220\u9664\uFF09\u3002\u8BF7\u91CD\u65B0\u53D1\u9001\u4F60\u7684\u6D88\u606F\u3002",
3576
+ healKind === "corrupt" ? `\u26A0\uFE0F \u4F1A\u8BDD\u8BB0\u5F55\u635F\u574F\uFF0C\u5DF2\u5F52\u6863\u5E76\u91CD\u7F6E\uFF08\u5F52\u6863\uFF1A\`${archivePath ?? "\u5F52\u6863\u5931\u8D25"}\`\uFF09\u3002\u8BF7\u91CD\u65B0\u53D1\u9001\u4F60\u7684\u6D88\u606F\u3002` : "\u26A0\uFE0F \u4F1A\u8BDD\u72B6\u6001\u5F02\u5E38\uFF0C\u5DF2\u91CD\u7F6E\u4F1A\u8BDD\u6620\u5C04\uFF08\u5386\u53F2\u65E5\u5FD7\u4FDD\u7559\uFF0C\u672A\u5220\u9664\uFF09\u3002\u8BF7\u91CD\u65B0\u53D1\u9001\u4F60\u7684\u6D88\u606F\u3002",
3536
3577
  { ...replyOptions }
3537
3578
  );
3538
3579
  return;
@@ -3648,7 +3689,7 @@ function errorMessage(error) {
3648
3689
  }
3649
3690
 
3650
3691
  // src/config/dsh-config.ts
3651
- import { mkdir as mkdir6, readFile as readFile3, rm, writeFile as writeFile4 } from "fs/promises";
3692
+ import { mkdir as mkdir6, readFile as readFile3, rm as rm2, writeFile as writeFile4 } from "fs/promises";
3652
3693
  import { homedir as homedir6 } from "os";
3653
3694
  import { dirname as dirname4, join as join9 } from "path";
3654
3695
  import { Document, parseDocument } from "yaml";
@@ -3776,7 +3817,7 @@ async function withFileLock(filename, operation) {
3776
3817
  try {
3777
3818
  return await operation();
3778
3819
  } finally {
3779
- await rm(lockPath, { force: true }).catch(() => void 0);
3820
+ await rm2(lockPath, { force: true }).catch(() => void 0);
3780
3821
  }
3781
3822
  }
3782
3823
  var DshProviderManager = class {
@@ -4681,6 +4722,7 @@ var HELP = [
4681
4722
  "**dsh-lark-bot \u547D\u4EE4**",
4682
4723
  "",
4683
4724
  "- `/new` `/reset` \u2014 \u5F00\u59CB\u65B0\u4F1A\u8BDD",
4725
+ "- `/newg <\u7FA4\u540D>` \u2014 \u81EA\u52A8\u65B0\u5EFA\u7FA4\u804A\uFF08\u62C9\u4F60\u5165\u7FA4\uFF09\u5E76\u5F00\u65B0\u4F1A\u8BDD\uFF0C\u5F53\u524D\u4F1A\u8BDD\u4FDD\u7559",
4684
4726
  "- `/cd <path>` \u2014 \u5207\u6362\u5DE5\u4F5C\u76EE\u5F55\u5E76\u91CD\u7F6E\u4F1A\u8BDD",
4685
4727
  "- `/ws list|save <name>|use <name>|remove <name>` \u2014 \u7BA1\u7406\u5DE5\u4F5C\u7A7A\u95F4",
4686
4728
  "- `/status` \u2014 \u67E5\u770B\u5F53\u524D\u72B6\u6001",
@@ -4720,6 +4762,54 @@ async function handleNew(_args, ctx) {
4720
4762
  interrupted > 0 ? `\u5DF2\u4E2D\u65AD ${String(interrupted)} \u4E2A\u4EFB\u52A1\u5E76\u5F00\u59CB\u65B0\u4F1A\u8BDD\u3002` : "\u5DF2\u5F00\u59CB\u65B0\u4F1A\u8BDD\u3002"
4721
4763
  );
4722
4764
  }
4765
+ var MAX_GROUP_NAME_LENGTH = 60;
4766
+ function groupAppLink(chatId) {
4767
+ return `https://applink.feishu.cn/client/chat/open?chatId=${encodeURIComponent(chatId)}`;
4768
+ }
4769
+ async function handleNewGroup(args, ctx) {
4770
+ const name = args.trim();
4771
+ if (!name) {
4772
+ await reply5(ctx, "\u7528\u6CD5\uFF1A`/newg <\u7FA4\u540D>` \u2014 \u81EA\u52A8\u65B0\u5EFA\u7FA4\u804A\u5E76\u5F00\u59CB\u65B0\u4F1A\u8BDD");
4773
+ return;
4774
+ }
4775
+ if (name.length > MAX_GROUP_NAME_LENGTH) {
4776
+ await reply5(
4777
+ ctx,
4778
+ `\u7FA4\u540D\u8FC7\u957F\uFF08\u4E0A\u9650 ${String(MAX_GROUP_NAME_LENGTH)} \u5B57\u7B26\uFF0C\u5F53\u524D ${String(name.length)}\uFF09\u3002`
4779
+ );
4780
+ return;
4781
+ }
4782
+ if (!ctx.channel.createChat) {
4783
+ await reply5(ctx, "\u5F53\u524D\u6E20\u9053\u4E0D\u652F\u6301\u81EA\u52A8\u5EFA\u7FA4\u3002");
4784
+ return;
4785
+ }
4786
+ if (!ctx.senderId) {
4787
+ await reply5(ctx, "\u65E0\u6CD5\u8BC6\u522B\u53D1\u9001\u8005 open_id\uFF0C\u4E0D\u80FD\u81EA\u52A8\u5EFA\u7FA4\u3002");
4788
+ return;
4789
+ }
4790
+ try {
4791
+ const { chatId } = await ctx.channel.createChat({
4792
+ name,
4793
+ chatType: "private",
4794
+ chatMode: "group",
4795
+ inviteUserIds: [ctx.senderId],
4796
+ userIdType: "open_id"
4797
+ });
4798
+ await reply5(
4799
+ ctx,
4800
+ [
4801
+ `\u2705 \u5DF2\u521B\u5EFA\u7FA4\u804A\uFF1A**${name}**`,
4802
+ `- \u7FA4 ID\uFF1A\`${chatId}\``,
4803
+ `- \u5DF2\u5C06\u4F60\u52A0\u5165\u7FA4\u804A\uFF0C\u65B0\u4F1A\u8BDD\u5C06\u5728\u7FA4\u91CC\u81EA\u52A8\u5F00\u59CB\uFF08\u5F53\u524D\u4F1A\u8BDD\u4E0D\u53D7\u5F71\u54CD\uFF09`,
4804
+ "",
4805
+ `\u{1F449} [\u6253\u5F00\u7FA4\u804A](${groupAppLink(chatId)})`
4806
+ ].join("\n")
4807
+ );
4808
+ } catch (error) {
4809
+ const message = error instanceof Error ? error.message : String(error);
4810
+ await reply5(ctx, `\u274C \u5EFA\u7FA4\u5931\u8D25\uFF1A\`${message}\``);
4811
+ }
4812
+ }
4723
4813
  async function handleCd(args, ctx) {
4724
4814
  const path = args.trim();
4725
4815
  if (!path) {
@@ -4998,6 +5088,7 @@ async function handleHelp(_args, ctx) {
4998
5088
  var handlers = {
4999
5089
  "/new": handleNew,
5000
5090
  "/reset": handleNew,
5091
+ "/newg": handleNewGroup,
5001
5092
  "/cd": handleCd,
5002
5093
  "/ws": handleWs,
5003
5094
  "/status": handleStatus,
@@ -5053,6 +5144,9 @@ function adaptLarkChannel(channel) {
5053
5144
  async sendCard(chatId, card, options) {
5054
5145
  await channel.send(chatId, { card }, toLarkSendOptions(options));
5055
5146
  },
5147
+ async createChat(opts) {
5148
+ return channel.createChat(opts);
5149
+ },
5056
5150
  async streamCard(chatId, initial, producer, options) {
5057
5151
  await channel.stream(
5058
5152
  chatId,
@@ -6714,7 +6808,7 @@ import { join as join15 } from "path";
6714
6808
  // src/guardian/install.ts
6715
6809
  init_own_package();
6716
6810
  init_process();
6717
- import { mkdir as mkdir12, readFile as readFile12, rm as rm2, writeFile as writeFile6 } from "fs/promises";
6811
+ import { mkdir as mkdir12, readFile as readFile12, rm as rm3, writeFile as writeFile6 } from "fs/promises";
6718
6812
  import { existsSync as existsSync5 } from "fs";
6719
6813
  import { homedir as homedir8 } from "os";
6720
6814
  import { join as join14 } from "path";
@@ -6944,7 +7038,7 @@ async function uninstallGuardian(options) {
6944
7038
  if (process.platform === "linux") {
6945
7039
  await run("systemctl", ["--user", "disable", "--now", `${GUARDIAN_LABEL}.service`]);
6946
7040
  const unitPath = join14(root, ".config", "systemd", "user", `${GUARDIAN_LABEL}.service`);
6947
- await rm2(unitPath, { force: true });
7041
+ await rm3(unitPath, { force: true });
6948
7042
  await run("systemctl", ["--user", "daemon-reload"]);
6949
7043
  messages.push("systemd user unit \u5DF2\u79FB\u9664\u5E76\u505C\u6B62\u3002");
6950
7044
  } else if (process.platform === "darwin") {
@@ -6952,7 +7046,7 @@ async function uninstallGuardian(options) {
6952
7046
  const plistPath = join14(root, "Library", "LaunchAgents", `${label}.plist`);
6953
7047
  const uid = typeof options.userOverride === "string" ? options.userOverride : String(process.getuid?.() ?? 501);
6954
7048
  await run("launchctl", ["bootout", `gui/${uid}`, plistPath]);
6955
- await rm2(plistPath, { force: true });
7049
+ await rm3(plistPath, { force: true });
6956
7050
  messages.push("launchd LaunchAgent \u5DF2\u79FB\u9664\u3002");
6957
7051
  } else if (process.platform === "win32") {
6958
7052
  const startupDir = join14(
@@ -6963,11 +7057,11 @@ async function uninstallGuardian(options) {
6963
7057
  "Programs",
6964
7058
  "Startup"
6965
7059
  );
6966
- await rm2(join14(startupDir, `${GUARDIAN_LABEL}.cmd`), { force: true });
7060
+ await rm3(join14(startupDir, `${GUARDIAN_LABEL}.cmd`), { force: true });
6967
7061
  messages.push("Windows \u542F\u52A8\u9879\u5DF2\u79FB\u9664\u3002");
6968
7062
  } else {
6969
7063
  const fallbackPath = join14(root, ".dsh-lark", "guardian", `${GUARDIAN_LABEL}.sh`);
6970
- await rm2(fallbackPath, { force: true });
7064
+ await rm3(fallbackPath, { force: true });
6971
7065
  messages.push("\u542F\u52A8\u811A\u672C\u5DF2\u79FB\u9664\u3002");
6972
7066
  }
6973
7067
  messages.push("\u5B88\u62A4\u72B6\u6001\u6587\u4EF6\uFF08~/.dsh-lark/guardian.json\uFF09\u5DF2\u4FDD\u7559\uFF0C\u53EF\u968F\u65F6\u91CD\u65B0\u5B89\u88C5\u3002");
@@ -7324,11 +7418,11 @@ function packageSpecFor(name, version) {
7324
7418
  // src/upgrade/runtime.ts
7325
7419
  init_own_package();
7326
7420
  import { existsSync as existsSync7 } from "fs";
7327
- import { mkdir as mkdir15, rm as rm3, symlink } from "fs/promises";
7421
+ import { mkdir as mkdir15, rm as rm4, symlink } from "fs/promises";
7328
7422
  import { join as join18 } from "path";
7329
7423
  init_acp_runtime();
7330
7424
  async function defaultRelink(linkPath, target) {
7331
- await rm3(linkPath, { recursive: true, force: true });
7425
+ await rm4(linkPath, { recursive: true, force: true });
7332
7426
  await mkdir15(join18(linkPath, ".."), { recursive: true });
7333
7427
  await symlink(target, linkPath, process.platform === "win32" ? "junction" : "dir");
7334
7428
  }
@@ -7848,6 +7942,8 @@ var MAX_TRANSCRIPT_ENTRIES = 30;
7848
7942
  var SAFE_RUN_TIMEOUT_MS = 10 * 6e4;
7849
7943
  var SAFE_SDK_PROVISION_TIMEOUT_MS = 3 * 6e4;
7850
7944
  var SAFE_CARD_TICK_MS = 5e3;
7945
+ var RELAUNCH_COOLDOWN_MS = 6e4;
7946
+ var RELAUNCH_READY_TIMEOUT_MS = 15e3;
7851
7947
  var GuardianService = class {
7852
7948
  options;
7853
7949
  state;
@@ -7858,6 +7954,8 @@ var GuardianService = class {
7858
7954
  transcripts = /* @__PURE__ */ new Map();
7859
7955
  downStreak = 0;
7860
7956
  lastRelaunchAt;
7957
+ relaunchPending = false;
7958
+ relaunchStartedAt;
7861
7959
  lastHeartbeatFreshAt;
7862
7960
  timer;
7863
7961
  ticking = false;
@@ -7871,6 +7969,8 @@ var GuardianService = class {
7871
7969
  pollMs: options.pollMs ?? 2e3,
7872
7970
  staleMs: options.staleMs ?? 15e3,
7873
7971
  engineDeadMs: options.engineDeadMs ?? 12e4,
7972
+ relaunchCooldownMs: options.relaunchCooldownMs ?? RELAUNCH_COOLDOWN_MS,
7973
+ relaunchReadyTimeoutMs: options.relaunchReadyTimeoutMs ?? RELAUNCH_READY_TIMEOUT_MS,
7874
7974
  takeoverGracePolls: options.takeoverGracePolls ?? 2,
7875
7975
  sendDelayMs: options.sendDelayMs ?? 600,
7876
7976
  safeAdapterMode: options.safeAdapterMode ?? "auto",
@@ -8004,6 +8104,15 @@ var GuardianService = class {
8004
8104
  this.dshUp = up;
8005
8105
  if (up) {
8006
8106
  this.downStreak = 0;
8107
+ if (this.relaunchPending || this.state.relaunchedPid !== void 0) {
8108
+ this.relaunchPending = false;
8109
+ this.relaunchStartedAt = void 0;
8110
+ this.state.relaunchedPid = void 0;
8111
+ await this.save();
8112
+ this.log().info("guardian", "bridge-relaunch-ready", {
8113
+ dshProfile: this.state.dshProfile
8114
+ });
8115
+ }
8007
8116
  await this.setSeenUp();
8008
8117
  if (this.state.mode !== "standby" || this.channel !== void 0) {
8009
8118
  await this.stopAllSafeRuns();
@@ -8018,22 +8127,61 @@ var GuardianService = class {
8018
8127
  if (!this.state.profileSeenUp) return;
8019
8128
  this.downStreak += 1;
8020
8129
  if (this.downStreak < this.options.takeoverGracePolls) return;
8130
+ if (this.relaunchPending) {
8131
+ const ready = heartbeatFresh || await (this.options.findProcess ?? findProfileProcess)(
8132
+ this.state.dshProfile
8133
+ ) !== void 0;
8134
+ if (ready) {
8135
+ this.relaunchPending = false;
8136
+ this.relaunchStartedAt = void 0;
8137
+ this.log().info("guardian", "bridge-relaunch-ready", {
8138
+ dshProfile: this.state.dshProfile
8139
+ });
8140
+ return;
8141
+ }
8142
+ if (now - (this.relaunchStartedAt ?? now) > this.options.relaunchReadyTimeoutMs) {
8143
+ this.relaunchPending = false;
8144
+ this.relaunchStartedAt = void 0;
8145
+ this.state.relaunchedPid = void 0;
8146
+ await this.save();
8147
+ this.log().fail("guardian", new Error("bridge relaunch did not come up within the readiness window"), {
8148
+ dshProfile: this.state.dshProfile
8149
+ });
8150
+ } else {
8151
+ return;
8152
+ }
8153
+ }
8021
8154
  let relaunchedNow = false;
8022
8155
  try {
8023
- const now2 = (this.options.now ?? Date.now)();
8024
- if (this.lastRelaunchAt === void 0 || now2 - this.lastRelaunchAt > 6e4) {
8156
+ if (this.lastRelaunchAt === void 0 || now - this.lastRelaunchAt > this.options.relaunchCooldownMs) {
8025
8157
  const bin = this.dshBin;
8026
- if (bin && !processAlive) {
8027
- const spawn7 = this.options.spawnDetachedFn ?? spawnDetached;
8028
- const spawned = spawn7("node", [bin, "--profile", this.state.dshProfile]);
8029
- if (spawned.pid !== void 0) {
8030
- this.state.relaunchedPid = spawned.pid;
8031
- this.lastRelaunchAt = now2;
8032
- relaunchedNow = true;
8033
- await this.save();
8034
- this.log().info("guardian", "bridge-relaunched", {
8035
- pid: spawned.pid,
8036
- dshProfile: this.state.dshProfile
8158
+ if (bin) {
8159
+ const stillDown = await (this.options.findProcess ?? findProfileProcess)(
8160
+ this.state.dshProfile
8161
+ );
8162
+ if (stillDown === void 0) {
8163
+ const spawn7 = this.options.spawnDetachedFn ?? spawnDetached;
8164
+ const spawned = spawn7("node", [
8165
+ bin,
8166
+ "--profile",
8167
+ this.state.dshProfile
8168
+ ]);
8169
+ if (spawned.pid !== void 0) {
8170
+ this.state.relaunchedPid = spawned.pid;
8171
+ this.lastRelaunchAt = now;
8172
+ this.relaunchPending = true;
8173
+ this.relaunchStartedAt = now;
8174
+ relaunchedNow = true;
8175
+ await this.save();
8176
+ this.log().info("guardian", "bridge-relaunched", {
8177
+ pid: spawned.pid,
8178
+ dshProfile: this.state.dshProfile
8179
+ });
8180
+ }
8181
+ } else {
8182
+ this.log().warn("guardian", "bridge-relaunch-skipped", {
8183
+ dshProfile: this.state.dshProfile,
8184
+ pid: stillDown.pid
8037
8185
  });
8038
8186
  }
8039
8187
  }
@@ -8359,7 +8507,12 @@ var GuardianService = class {
8359
8507
  }
8360
8508
  const spawn7 = this.options.spawnDetachedFn ?? spawnDetached;
8361
8509
  const spawned = spawn7("node", [bin, "--profile", this.state.dshProfile]);
8362
- if (spawned.pid !== void 0) this.state.relaunchedPid = spawned.pid;
8510
+ if (spawned.pid !== void 0) {
8511
+ this.state.relaunchedPid = spawned.pid;
8512
+ this.lastRelaunchAt = (this.options.now ?? Date.now)();
8513
+ this.relaunchPending = true;
8514
+ this.relaunchStartedAt = this.lastRelaunchAt;
8515
+ }
8363
8516
  await this.save();
8364
8517
  await this.sendMarkdown(
8365
8518
  msg.chatId,