dsh-lark-bot 0.8.3 → 0.9.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 +44 -21
- package/SECURITY.md +3 -2
- package/dist/cli.js +172 -17
- package/dist/cli.js.map +1 -1
- package/dist/plugin.d.ts +14 -0
- package/dist/plugin.js +163 -10
- package/dist/plugin.js.map +1 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -87,10 +87,15 @@ npx dsh-lark-bot@latest setup --profile dsh-lark
|
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
`setup` 会自动完成:发现本机 dsh → 预批准 pnpm 构建策略 → 执行标准的
|
|
90
|
-
`dsh plugin --profile dsh-lark add dsh-lark-bot
|
|
90
|
+
`dsh plugin --profile dsh-lark add dsh-lark-bot`,并**默认同时安装「安全网守护」**——系统级
|
|
91
|
+
常驻、dsh 全部下线后仍保留飞书救援入口(核心能力之一,见下文「安全网守护」一节)。
|
|
92
|
+
一条命令即完成全部安装。
|
|
91
93
|
|
|
92
94
|
`setup` automatically: locates your dsh install → pre-approves pnpm's build policy → runs the
|
|
93
|
-
standard `dsh plugin --profile dsh-lark add dsh-lark-bot
|
|
95
|
+
standard `dsh plugin --profile dsh-lark add dsh-lark-bot`, and **also installs the safety-net
|
|
96
|
+
guardian by default** — a system-level resident process that keeps the Feishu rescue entrance
|
|
97
|
+
alive even when dsh is fully down (one of the core features; see "Safety-net guardian" below).
|
|
98
|
+
One command installs everything.
|
|
94
99
|
|
|
95
100
|
### 2. 启动并扫码绑定 | Start and bind with one scan
|
|
96
101
|
|
|
@@ -190,6 +195,11 @@ run in that scope carries the role instructions, and the role model wins below t
|
|
|
190
195
|
dsh 工具(SDK / ACP 两种 runtime 均可装配):agent 完成任务后可主动向其他群 / 话题发消息并
|
|
191
196
|
@ 指定成员,桥接进程通过 127.0.0.1 本地回调端口 + 随机 token 校验,不暴露公网。
|
|
192
197
|
|
|
198
|
+
**任务中向你提问(问答卡)**:agent 需要你拍板、确认或补充缺失信息时,会通过
|
|
199
|
+
`lark_ask_user` 工具主动向当前会话弹一张**问答卡**(单选 / 多选 / 自由文本),
|
|
200
|
+
你回答后任务自动继续——无需额外命令。问答卡等待期间任务不会被运行超时打断。
|
|
201
|
+
(与 `/ask` 的“你主动发结构化问题”方向相反:这是 agent 主动来问你。)
|
|
202
|
+
|
|
193
203
|
**Outbound mentions & cross-session notify**: the outbound contract supports `mentions` and
|
|
194
204
|
cross-chat/thread sends; `/notify <scope|chatId> <text>` pushes a report to another session
|
|
195
205
|
(admin). The agent also gets a built-in `lark_notify` dsh tool (wired into both SDK and ACP
|
|
@@ -197,8 +207,14 @@ runtime profiles): after a task finishes it can push messages to other groups/to
|
|
|
197
207
|
members. The bridge listens on 127.0.0.1 with a random per-boot token — nothing is exposed to the
|
|
198
208
|
public network.
|
|
199
209
|
|
|
200
|
-
|
|
201
|
-
|
|
210
|
+
**Mid-task questions (question cards)**: when the agent needs a decision, confirmation, or
|
|
211
|
+
missing information, it proactively sends a **question card** to the current chat via the
|
|
212
|
+
`lark_ask_user` tool (single choice / multi choice / free text) and resumes automatically once
|
|
213
|
+
you answer — no extra command needed. The run-timeout watchdog pauses while a card is waiting.
|
|
214
|
+
(This is the opposite direction of `/ask`, which is you asking the agent.)
|
|
215
|
+
|
|
216
|
+
**安全网守护(Safe-mode guardian)**:默认随 `setup` 一起安装的、独立于 dsh 进程、系统级常驻的
|
|
217
|
+
最小守护进程(Linux systemd user unit / macOS LaunchAgent / Windows 启动项)。dsh 正常运行时守护保持静默;
|
|
202
218
|
一旦 dsh 进程下线或无法 boot(例如某个第三方插件破坏了整个 profile 组合),守护自动接管飞书
|
|
203
219
|
通道,用户无需接触命令行即可发送控制信号自救:
|
|
204
220
|
|
|
@@ -212,12 +228,13 @@ public network.
|
|
|
212
228
|
全程不需要命令行;dsh 恢复后守护自动断开并回归静默。安装:
|
|
213
229
|
|
|
214
230
|
```bash
|
|
215
|
-
|
|
216
|
-
|
|
231
|
+
# 随 setup 默认安装(无需额外参数);已安装后也可单独安装 / 重装:
|
|
232
|
+
dsh-lark-bot guardian install --dsh-profile dsh-lark
|
|
217
233
|
```
|
|
234
|
+
不需要守护时,安装时加 `--no-guardian` 跳过;单独卸载用 `dsh-lark-bot guardian uninstall`。
|
|
218
235
|
|
|
219
|
-
**Safety-net guardian**:
|
|
220
|
-
independent of the dsh process. While dsh runs, the guardian stays silent; once dsh goes down or
|
|
236
|
+
**Safety-net guardian**: a minimal system-level resident process installed **by default with
|
|
237
|
+
`setup`**, independent of the dsh process. While dsh runs, the guardian stays silent; once dsh goes down or
|
|
221
238
|
fails to boot (e.g. a third-party plugin breaks the whole profile composition), the guardian
|
|
222
239
|
takes over the Feishu channel so you can self-heal without touching the command line:
|
|
223
240
|
|
|
@@ -234,9 +251,10 @@ No command line is needed for the whole rescue flow; once dsh is back, the guard
|
|
|
234
251
|
channel automatically. Install:
|
|
235
252
|
|
|
236
253
|
```bash
|
|
237
|
-
|
|
238
|
-
|
|
254
|
+
# Installed by default with setup (no extra flag); can also be installed / refreshed later:
|
|
255
|
+
dsh-lark-bot guardian install --dsh-profile dsh-lark
|
|
239
256
|
```
|
|
257
|
+
Pass `--no-guardian` to setup to skip it; remove it later with `dsh-lark-bot guardian uninstall`.
|
|
240
258
|
|
|
241
259
|
### 模型 / Provider / 凭据管理 | Models / Providers / Credentials
|
|
242
260
|
|
|
@@ -285,21 +303,25 @@ npx dsh-lark-bot@latest setup --profile dsh-lark
|
|
|
285
303
|
```
|
|
286
304
|
|
|
287
305
|
`setup` 自动完成:定位本机 dsh → 预批准 pnpm 构建策略(protobufjs)→ 执行标准
|
|
288
|
-
`dsh plugin --profile dsh-lark add dsh-lark-bot
|
|
289
|
-
|
|
306
|
+
`dsh plugin --profile dsh-lark add dsh-lark-bot`,并**默认同时安装「安全网守护」**
|
|
307
|
+
(见「安全网守护」一节;不需要时加 `--no-guardian` 跳过)。已安装时重复执行即升级到最新版。
|
|
290
308
|
|
|
291
309
|
`setup` locates your dsh, pre-approves pnpm's build policy (protobufjs) and runs the standard
|
|
292
|
-
`dsh plugin --profile dsh-lark add dsh-lark-bot`.
|
|
293
|
-
|
|
310
|
+
`dsh plugin --profile dsh-lark add dsh-lark-bot`. It **also installs the safety-net guardian by
|
|
311
|
+
default** (see "Safety-net guardian" above; pass `--no-guardian` to skip). Re-running it upgrades
|
|
312
|
+
to the latest version.
|
|
294
313
|
|
|
295
314
|
### 升级 | Upgrade
|
|
296
315
|
|
|
297
316
|
- 插件本体:重跑 `setup`(或 `dsh plugin --profile <name> add dsh-lark-bot`)拉取 npm 最新版。
|
|
317
|
+
- 安全网守护:随 `setup` 一起安装 / 升级(幂等重装),也可单独 `dsh-lark-bot guardian install`。
|
|
298
318
|
- CLI 工具(可选):`npm i -g dsh-lark-bot@latest`;使用 `npx` 时无需全局安装。
|
|
299
319
|
- 升级后重启 profile:`dsh --profile dsh-lark`。
|
|
300
320
|
|
|
301
321
|
- Plugin: re-run `setup` (or `dsh plugin --profile <name> add dsh-lark-bot`) to pull the latest
|
|
302
322
|
npm release.
|
|
323
|
+
- Safety-net guardian: installed / upgraded together with `setup` (idempotent), or standalone via
|
|
324
|
+
`dsh-lark-bot guardian install`.
|
|
303
325
|
- CLI tool (optional): `npm i -g dsh-lark-bot@latest`; not needed when using `npx`.
|
|
304
326
|
- Restart the profile after upgrading: `dsh --profile dsh-lark`.
|
|
305
327
|
|
|
@@ -391,7 +413,7 @@ tasks and session archival.
|
|
|
391
413
|
对应 runtime 并自动重建。
|
|
392
414
|
- 桥接引擎作为 dsh 插件在 dsh 进程内运行,agent 执行使用官方 dsh SDK runtime 子进程
|
|
393
415
|
(嵌套 runtime 是有意取舍,用于按工作区隔离的 runtime 池与 scope 内并行 run)。
|
|
394
|
-
|
|
416
|
+
唯一的进程级例外是默认安装的「安全网守护」——它独立于 dsh / Cordis 常驻,仅在 dsh
|
|
395
417
|
下线后接管飞书通道,正常运行时保持静默。
|
|
396
418
|
- 飞书文档评论、富文本回复为规划中能力,尚未实现。
|
|
397
419
|
- pnpm ≥ 10 的构建脚本策略由 `setup` 自动处理;手动 `dsh plugin add` 时若报
|
|
@@ -493,7 +515,7 @@ This tool runs **locally**; before installing, be aware that it accesses:
|
|
|
493
515
|
- **dsh 配置**:`/model` `/providers` `/provider` `/key` 命令按 dsh 官方存储协议读写
|
|
494
516
|
`~/.dsh/settings.yaml` 与 `~/.dsh/.credentials.yaml`(仅管理员可写;settings 只存 `apiKeyEnv`
|
|
495
517
|
引用,凭据文件权限 0600、目录 0700,字面密钥不进入 settings 或聊天记录)。
|
|
496
|
-
-
|
|
518
|
+
- **安全网守护(默认随 `setup` 安装)**:系统级常驻进程,读取 `~/.dsh-lark/config.json` 中的飞书
|
|
497
519
|
凭据;dsh 下线时接管同一 bot 的飞书长连接并扫描本机进程(仅 `ps` 命令行,不读内存);
|
|
498
520
|
`/safemode` 时在 `~/.dsh/profiles/<profile>-safe` 创建仅核心的 dsh profile 并逐条执行
|
|
499
521
|
`dsh --profile <safe> "<prompt>"` 子进程。
|
|
@@ -548,11 +570,12 @@ Common issues:
|
|
|
548
570
|
is reachable; with an existing App ID/Secret you can skip scanning via `--app-id` /
|
|
549
571
|
`--app-secret`.
|
|
550
572
|
|
|
551
|
-
|
|
552
|
-
dsh 自己的日志体系。
|
|
573
|
+
桥接引擎日志以 JSON Lines 输出到 stderr(由 dsh 宿主进程捕获;`logs/bot.log` 是 0.6.0
|
|
574
|
+
独立服务时代的遗留路径,0.7.0 起不再写入);dsh 宿主日志走 dsh 自己的日志体系。
|
|
553
575
|
|
|
554
|
-
The bridge engine logs to
|
|
555
|
-
|
|
576
|
+
The bridge engine logs JSON Lines to stderr (captured by the dsh host; `logs/bot.log` is a
|
|
577
|
+
leftover path from the 0.6.0 standalone-service era and is no longer written since 0.7.0); the
|
|
578
|
+
dsh host uses its own logging.
|
|
556
579
|
|
|
557
580
|
**回滚 / Rollback**:`dsh plugin --profile dsh-lark remove dsh-lark-bot` 后重装固定版本即可
|
|
558
581
|
(如 `dsh plugin --profile dsh-lark add dsh-lark-bot@0.6.0`);`~/.dsh-lark` 状态独立于插件
|
|
@@ -673,7 +696,7 @@ See "Community Listings" in the next section for ecosystem registration status.
|
|
|
673
696
|
|
|
674
697
|
核心思路:**飞书通道与 agent 后端解耦**。桥接层复刻 `lark-channel-bridge` 的成熟做法(WebSocket 长连接 + 流式卡片 + 会话路由),agent 后端通过 adapter 抽象,默认挂接官方 DeepSeek Harness SDK(`DSH_LARK_ADAPTER=sdk`),可选 ACP 审批模式与 legacy headless。
|
|
675
698
|
|
|
676
|
-
|
|
699
|
+
默认安装的「安全网守护」(`src/guardian/`)独立于 dsh 进程常驻:dsh 在线时静默,下线时接管飞书
|
|
677
700
|
通道接收 `/safemode` 控制信号,以仅核心 profile(`dsh-base` + `dsh-headless`)拉起受限对话
|
|
678
701
|
用于自愈,`/safemode exit` 重启完整 profile 并交还通道。
|
|
679
702
|
|
package/SECURITY.md
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
`/invite admin <open_id>` 定义);查看类命令(`/model`、`/providers`、`/key list`)开放。
|
|
38
38
|
10. **本地回调隔离**:`lark_notify` 工具的回调服务只绑定 `127.0.0.1`,每次启动生成随机
|
|
39
39
|
token 鉴权(不落盘、不进日志),请求体限 1MB;`/notify` 与角色 / 配置写命令同为管理员操作。
|
|
40
|
-
11.
|
|
40
|
+
11. **安全网守护(默认随 `setup` 安装)**:
|
|
41
41
|
- 守护是独立于 dsh / Cordis 的最小进程,只读取本地状态与进程命令行(`ps`,不读内存),
|
|
42
42
|
不导入任何 dsh 代码、不监听公网端口;
|
|
43
43
|
- dsh 在线时守护**不连接飞书**(同 app 长连接仅允许单连接,避免抢占正常通道);仅在
|
|
@@ -54,7 +54,8 @@
|
|
|
54
54
|
- 飞书凭据明文保存在本机配置文件;日志与卡片不输出真实密钥。
|
|
55
55
|
- 桥接引擎在 dsh 宿主进程内运行,凭据与 `DSH_LARK_*` 环境直接随 dsh 进程提供,无独立服务
|
|
56
56
|
环境快照文件;敏感值不进日志与卡片。
|
|
57
|
-
-
|
|
57
|
+
- 桥接引擎日志以 JSON Lines 输出到 stderr(由 dsh 宿主进程捕获),密钥字段脱敏后输出;
|
|
58
|
+
`logs/bot.log` 是 0.6.0 独立服务时代的遗留路径,0.7.0 起不再写入。
|
|
58
59
|
- 聊天命令管理的 dsh 配置按官方存储协议写入:`~/.dsh/settings.yaml`(只存 `apiKeyEnv`
|
|
59
60
|
引用,不落字面密钥)与 `~/.dsh/.credentials.yaml`(目录 0700、文件 0600)。bot 永不回显
|
|
60
61
|
密钥值;群聊中粘贴密钥会对群成员可见,建议私聊使用或改用环境变量 / dsh Web 页面录入。
|
package/dist/cli.js
CHANGED
|
@@ -226,6 +226,14 @@ function acpPatchYaml(provider, model) {
|
|
|
226
226
|
" config:",
|
|
227
227
|
" endpoint: !!js process.env.DSH_LARK_NOTIFY_URL",
|
|
228
228
|
" token: !!js process.env.DSH_LARK_NOTIFY_TOKEN",
|
|
229
|
+
"",
|
|
230
|
+
// Question-card tool (same contract as the SDK runtime).
|
|
231
|
+
"- insert:",
|
|
232
|
+
" - id: lark-ask",
|
|
233
|
+
` name: '${own.name}/ask'`,
|
|
234
|
+
" config:",
|
|
235
|
+
" endpoint: !!js process.env.DSH_LARK_ASK_URL",
|
|
236
|
+
" token: !!js process.env.DSH_LARK_NOTIFY_TOKEN",
|
|
229
237
|
""
|
|
230
238
|
].join("\n");
|
|
231
239
|
}
|
|
@@ -1323,6 +1331,15 @@ function patchYamlFor() {
|
|
|
1323
1331
|
" config:",
|
|
1324
1332
|
" endpoint: !!js process.env.DSH_LARK_NOTIFY_URL",
|
|
1325
1333
|
" token: !!js process.env.DSH_LARK_NOTIFY_TOKEN",
|
|
1334
|
+
"",
|
|
1335
|
+
// Question-card tool: the agent asks the user for decisions / missing
|
|
1336
|
+
// information; the bridge shows a card and returns the answer.
|
|
1337
|
+
"- insert:",
|
|
1338
|
+
" - id: lark-ask",
|
|
1339
|
+
` name: '${own.name}/ask'`,
|
|
1340
|
+
" config:",
|
|
1341
|
+
" endpoint: !!js process.env.DSH_LARK_ASK_URL",
|
|
1342
|
+
" token: !!js process.env.DSH_LARK_NOTIFY_TOKEN",
|
|
1326
1343
|
""
|
|
1327
1344
|
].join("\n");
|
|
1328
1345
|
}
|
|
@@ -2194,6 +2211,28 @@ var PendingQueue = class {
|
|
|
2194
2211
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
2195
2212
|
var QuestionRegistry = class {
|
|
2196
2213
|
pending = /* @__PURE__ */ new Map();
|
|
2214
|
+
settledListeners = /* @__PURE__ */ new Map();
|
|
2215
|
+
/** Number of question cards currently awaiting an answer in a scope. */
|
|
2216
|
+
pendingCount(scope) {
|
|
2217
|
+
let count = 0;
|
|
2218
|
+
for (const key of this.pending.keys()) {
|
|
2219
|
+
if (key.startsWith(`${scope}:`)) count += 1;
|
|
2220
|
+
}
|
|
2221
|
+
return count;
|
|
2222
|
+
}
|
|
2223
|
+
/** Subscribe to question settlements in a scope; returns an unsubscribe. */
|
|
2224
|
+
onSettled(scope, listener) {
|
|
2225
|
+
const listeners = this.settledListeners.get(scope) ?? /* @__PURE__ */ new Set();
|
|
2226
|
+
listeners.add(listener);
|
|
2227
|
+
this.settledListeners.set(scope, listeners);
|
|
2228
|
+
return () => {
|
|
2229
|
+
listeners.delete(listener);
|
|
2230
|
+
if (listeners.size === 0) this.settledListeners.delete(scope);
|
|
2231
|
+
};
|
|
2232
|
+
}
|
|
2233
|
+
notifySettled(scope) {
|
|
2234
|
+
for (const listener of this.settledListeners.get(scope) ?? []) listener();
|
|
2235
|
+
}
|
|
2197
2236
|
register(scope, input) {
|
|
2198
2237
|
const id = `question-${randomUUID3().replaceAll("-", "")}`;
|
|
2199
2238
|
const full = { ...input, id };
|
|
@@ -2208,6 +2247,7 @@ var QuestionRegistry = class {
|
|
|
2208
2247
|
pending.settled = true;
|
|
2209
2248
|
this.pending.delete(`${scope}:${id}`);
|
|
2210
2249
|
pending.resolve(answer);
|
|
2250
|
+
this.notifySettled(scope);
|
|
2211
2251
|
return true;
|
|
2212
2252
|
}
|
|
2213
2253
|
settleAll(scope) {
|
|
@@ -2220,6 +2260,7 @@ var QuestionRegistry = class {
|
|
|
2220
2260
|
pending.resolve(void 0);
|
|
2221
2261
|
count += 1;
|
|
2222
2262
|
}
|
|
2263
|
+
if (count > 0) this.notifySettled(scope);
|
|
2223
2264
|
return count;
|
|
2224
2265
|
}
|
|
2225
2266
|
get(scope, id) {
|
|
@@ -2879,12 +2920,26 @@ async function runAgentBatch(input) {
|
|
|
2879
2920
|
}
|
|
2880
2921
|
};
|
|
2881
2922
|
let timeoutTimer;
|
|
2923
|
+
let armTimeout;
|
|
2882
2924
|
const timeoutPromise = timeoutMs > 0 ? new Promise((resolve5) => {
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2925
|
+
armTimeout = () => {
|
|
2926
|
+
timeoutTimer = setTimeout(() => {
|
|
2927
|
+
if (timedOut) return;
|
|
2928
|
+
if (input.questions?.pendingCount(input.scope)) {
|
|
2929
|
+
armTimeout?.();
|
|
2930
|
+
return;
|
|
2931
|
+
}
|
|
2932
|
+
timedOut = true;
|
|
2933
|
+
void run.stop();
|
|
2934
|
+
resolve5();
|
|
2935
|
+
}, timeoutMs);
|
|
2936
|
+
};
|
|
2937
|
+
armTimeout();
|
|
2938
|
+
}) : void 0;
|
|
2939
|
+
const unsubscribeSettled = timeoutPromise ? input.questions?.onSettled(input.scope, () => {
|
|
2940
|
+
if (timedOut) return;
|
|
2941
|
+
if (timeoutTimer !== void 0) clearTimeout(timeoutTimer);
|
|
2942
|
+
armTimeout?.();
|
|
2888
2943
|
}) : void 0;
|
|
2889
2944
|
try {
|
|
2890
2945
|
if (timeoutPromise) {
|
|
@@ -2896,6 +2951,7 @@ async function runAgentBatch(input) {
|
|
|
2896
2951
|
await controller.update(renderCard(state, density));
|
|
2897
2952
|
} finally {
|
|
2898
2953
|
if (timeoutTimer !== void 0) clearTimeout(timeoutTimer);
|
|
2954
|
+
unsubscribeSettled?.();
|
|
2899
2955
|
}
|
|
2900
2956
|
},
|
|
2901
2957
|
replyOptions
|
|
@@ -4655,6 +4711,7 @@ var NotifyServer = class {
|
|
|
4655
4711
|
token;
|
|
4656
4712
|
deps;
|
|
4657
4713
|
url;
|
|
4714
|
+
askUrl;
|
|
4658
4715
|
constructor(deps) {
|
|
4659
4716
|
this.deps = deps;
|
|
4660
4717
|
this.token = deps.token;
|
|
@@ -4670,6 +4727,7 @@ var NotifyServer = class {
|
|
|
4670
4727
|
server.listen(this.deps.port ?? 0, "127.0.0.1", () => {
|
|
4671
4728
|
const address = server.address();
|
|
4672
4729
|
this.url = `http://127.0.0.1:${String(address.port)}/notify`;
|
|
4730
|
+
this.askUrl = `http://127.0.0.1:${String(address.port)}/ask`;
|
|
4673
4731
|
resolve5();
|
|
4674
4732
|
});
|
|
4675
4733
|
});
|
|
@@ -4690,12 +4748,38 @@ var NotifyServer = class {
|
|
|
4690
4748
|
res.end(`${JSON.stringify(body)}
|
|
4691
4749
|
`);
|
|
4692
4750
|
};
|
|
4693
|
-
if (req.method !== "POST" || req.url !== "/notify") {
|
|
4694
|
-
respond(404, { ok: false, error: "not found" });
|
|
4695
|
-
return;
|
|
4696
|
-
}
|
|
4697
4751
|
try {
|
|
4698
4752
|
const body = await readBody(req);
|
|
4753
|
+
if (req.method !== "POST") {
|
|
4754
|
+
respond(404, { ok: false, error: "not found" });
|
|
4755
|
+
return;
|
|
4756
|
+
}
|
|
4757
|
+
if (req.url === "/ask") {
|
|
4758
|
+
if (!this.deps.ask) {
|
|
4759
|
+
respond(404, { ok: false, error: "ask channel is not wired" });
|
|
4760
|
+
return;
|
|
4761
|
+
}
|
|
4762
|
+
const payload = JSON.parse(body);
|
|
4763
|
+
if (payload.token !== this.token) {
|
|
4764
|
+
respond(401, { ok: false, error: "invalid token" });
|
|
4765
|
+
return;
|
|
4766
|
+
}
|
|
4767
|
+
if (!payload.sessionId || !payload.question?.trim()) {
|
|
4768
|
+
respond(400, { ok: false, error: "sessionId and question are required" });
|
|
4769
|
+
return;
|
|
4770
|
+
}
|
|
4771
|
+
const result = await this.deps.ask(payload);
|
|
4772
|
+
if (!result.ok) {
|
|
4773
|
+
respond(404, { ok: false, ...result.error === void 0 ? {} : { error: result.error } });
|
|
4774
|
+
return;
|
|
4775
|
+
}
|
|
4776
|
+
respond(200, { ok: true, ...result.answer === void 0 ? {} : { answer: result.answer } });
|
|
4777
|
+
return;
|
|
4778
|
+
}
|
|
4779
|
+
if (req.url !== "/notify") {
|
|
4780
|
+
respond(404, { ok: false, error: "not found" });
|
|
4781
|
+
return;
|
|
4782
|
+
}
|
|
4699
4783
|
const message = JSON.parse(body);
|
|
4700
4784
|
if (message.token !== this.token) {
|
|
4701
4785
|
respond(401, { ok: false, error: "invalid token" });
|
|
@@ -4739,6 +4823,46 @@ function generateNotifyToken() {
|
|
|
4739
4823
|
return `dsh-lark-${randomBytes2(18).toString("hex")}`;
|
|
4740
4824
|
}
|
|
4741
4825
|
|
|
4826
|
+
// src/notify/ask-handler.ts
|
|
4827
|
+
function buildAskHandler(deps) {
|
|
4828
|
+
return async (payload) => {
|
|
4829
|
+
const scope = deps.sessions.scopeForSession(payload.sessionId);
|
|
4830
|
+
if (!scope) {
|
|
4831
|
+
return { ok: false, error: `unknown session: ${payload.sessionId}` };
|
|
4832
|
+
}
|
|
4833
|
+
const destination = deps.scopeDirectory.resolve(scope);
|
|
4834
|
+
if (!destination) {
|
|
4835
|
+
return { ok: false, error: `unknown scope: ${scope}` };
|
|
4836
|
+
}
|
|
4837
|
+
const kind = payload.kind ?? (payload.options && payload.options.length > 0 ? "single" : "text");
|
|
4838
|
+
const input = {
|
|
4839
|
+
kind,
|
|
4840
|
+
question: payload.question,
|
|
4841
|
+
...payload.options && payload.options.length > 0 ? { options: payload.options } : {}
|
|
4842
|
+
};
|
|
4843
|
+
const { id, promise } = deps.questions.register(scope, input);
|
|
4844
|
+
try {
|
|
4845
|
+
await deps.channel.sendCard(
|
|
4846
|
+
destination.chatId,
|
|
4847
|
+
renderQuestionCard({ ...input, id }),
|
|
4848
|
+
destination.threadId ? { threadId: destination.threadId } : void 0
|
|
4849
|
+
);
|
|
4850
|
+
} catch (error) {
|
|
4851
|
+
log.fail("ask-card", error, { scope });
|
|
4852
|
+
deps.questions.settleAll(scope);
|
|
4853
|
+
return {
|
|
4854
|
+
ok: false,
|
|
4855
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4856
|
+
};
|
|
4857
|
+
}
|
|
4858
|
+
const answer = await promise;
|
|
4859
|
+
if (answer === void 0) {
|
|
4860
|
+
return { ok: false, error: "question cancelled" };
|
|
4861
|
+
}
|
|
4862
|
+
return { ok: true, answer };
|
|
4863
|
+
};
|
|
4864
|
+
}
|
|
4865
|
+
|
|
4742
4866
|
// src/config/access-manager.ts
|
|
4743
4867
|
var AccessManager = class {
|
|
4744
4868
|
constructor(store, profileName) {
|
|
@@ -4892,6 +5016,8 @@ ${safeContent}`
|
|
|
4892
5016
|
import { readFile as readFile6 } from "fs/promises";
|
|
4893
5017
|
var SessionStore = class {
|
|
4894
5018
|
data = { chats: {} };
|
|
5019
|
+
/** Live session-id → scope index (used by the agent question-card router). */
|
|
5020
|
+
sessionScopes = /* @__PURE__ */ new Map();
|
|
4895
5021
|
saving = Promise.resolve();
|
|
4896
5022
|
pendingArchive = Promise.resolve();
|
|
4897
5023
|
path;
|
|
@@ -4929,8 +5055,22 @@ var SessionStore = class {
|
|
|
4929
5055
|
cwd,
|
|
4930
5056
|
messages: existing?.messages ?? []
|
|
4931
5057
|
};
|
|
5058
|
+
if (sessionId) this.sessionScopes.set(sessionId, scopeId);
|
|
4932
5059
|
this.schedulePersist();
|
|
4933
5060
|
}
|
|
5061
|
+
/**
|
|
5062
|
+
* Reverse lookup used by the bridge question-card router: map a live dsh
|
|
5063
|
+
* session id back to its bridge scope. Falls back to a scan of the loaded
|
|
5064
|
+
* records (e.g. after a fresh load before any live run registered).
|
|
5065
|
+
*/
|
|
5066
|
+
scopeForSession(sessionId) {
|
|
5067
|
+
const direct = this.sessionScopes.get(sessionId);
|
|
5068
|
+
if (direct !== void 0) return direct;
|
|
5069
|
+
for (const [scope, record] of Object.entries(this.data.chats)) {
|
|
5070
|
+
if (record.sessionId === sessionId) return scope;
|
|
5071
|
+
}
|
|
5072
|
+
return void 0;
|
|
5073
|
+
}
|
|
4934
5074
|
historyFor(scopeId, cwd) {
|
|
4935
5075
|
const record = this.data.chats[scopeId];
|
|
4936
5076
|
return record && record.cwd === cwd ? record.messages : [];
|
|
@@ -5599,7 +5739,19 @@ async function startBridgeEngine(options) {
|
|
|
5599
5739
|
...destination.threadId ? { threadId: destination.threadId } : {},
|
|
5600
5740
|
...payload.mentions ? { mentions: payload.mentions } : {}
|
|
5601
5741
|
});
|
|
5602
|
-
}
|
|
5742
|
+
},
|
|
5743
|
+
ask: buildAskHandler({
|
|
5744
|
+
sessions,
|
|
5745
|
+
scopeDirectory,
|
|
5746
|
+
questions,
|
|
5747
|
+
channel: {
|
|
5748
|
+
sendCard: async (chatId, card, options2) => {
|
|
5749
|
+
if (!streaming) throw new Error("bridge channel is not ready");
|
|
5750
|
+
if (!streaming.sendCard) throw new Error("bridge channel does not support cards");
|
|
5751
|
+
await streaming.sendCard(chatId, card, options2);
|
|
5752
|
+
}
|
|
5753
|
+
}
|
|
5754
|
+
})
|
|
5603
5755
|
});
|
|
5604
5756
|
process.env.DSH_LARK_NOTIFY_TOKEN = notifyToken;
|
|
5605
5757
|
const pending = new PendingQueue(
|
|
@@ -5696,6 +5848,7 @@ async function startBridgeEngine(options) {
|
|
|
5696
5848
|
larkChannel = bridge.channel;
|
|
5697
5849
|
await notifyServer.start();
|
|
5698
5850
|
process.env.DSH_LARK_NOTIFY_URL = notifyServer.url ?? "";
|
|
5851
|
+
process.env.DSH_LARK_ASK_URL = notifyServer.askUrl ?? "";
|
|
5699
5852
|
const heartbeat = startHeartbeat(
|
|
5700
5853
|
paths.profilePath(profileName, "guardian", "heartbeat.json"),
|
|
5701
5854
|
process.pid,
|
|
@@ -6136,6 +6289,7 @@ async function runSetup(options = {}) {
|
|
|
6136
6289
|
"\u672A\u627E\u5230\u672C\u673A dsh \u5B89\u88C5\uFF08`@deepseek-ai/dsh`\uFF09\u3002\u8BF7\u5148\u5B89\u88C5 DeepSeek Harness\uFF0C\u518D\u8FD0\u884C\u672C\u547D\u4EE4\u3002"
|
|
6137
6290
|
);
|
|
6138
6291
|
}
|
|
6292
|
+
const installGuardianService = options.guardian !== false;
|
|
6139
6293
|
const profileDir = join13(dshHome, "profiles", profile);
|
|
6140
6294
|
await mkdir12(profileDir, { recursive: true });
|
|
6141
6295
|
await approveBuilds(profileDir);
|
|
@@ -6144,7 +6298,7 @@ async function runSetup(options = {}) {
|
|
|
6144
6298
|
`
|
|
6145
6299
|
);
|
|
6146
6300
|
await runDshPlugin(bin, profile, packageSpec);
|
|
6147
|
-
if (
|
|
6301
|
+
if (installGuardianService) {
|
|
6148
6302
|
const install = options.installGuardianFn ?? installGuardian;
|
|
6149
6303
|
const guardianResult = await install({
|
|
6150
6304
|
env: loadRuntimeEnv(process.env),
|
|
@@ -6169,13 +6323,14 @@ async function runSetup(options = {}) {
|
|
|
6169
6323
|
"",
|
|
6170
6324
|
"\u9996\u6B21\u542F\u52A8\u4F1A\u5728\u7EC8\u7AEF\u6253\u5370\u4E8C\u7EF4\u7801\uFF0C\u7528\u98DE\u4E66 / Lark \u626B\u7801\u5B8C\u6210\u4E00\u6B21\u6027\u7ED1\u5B9A\uFF1B",
|
|
6171
6325
|
"\u7ED1\u5B9A\u540E dsh \u5373\u4EE5\u6807\u51C6\u63D2\u4EF6\u65B9\u5F0F\u52A0\u8F7D dsh-lark-bot \u7684\u6865\u63A5\u5F15\u64CE\u3002",
|
|
6172
|
-
...
|
|
6326
|
+
...installGuardianService ? [
|
|
6173
6327
|
"",
|
|
6174
|
-
"\u5B89\u5168\u7F51\u5B88\u62A4\u5DF2\u5B89\u88C5\uFF1Adsh \u4E0B\u7EBF\u540E\u4ECD\u53EF\u901A\u8FC7\u98DE\u4E66\u53D1\u9001 /safemode \u8FDB\u5165\u4EC5\u6838\u5FC3\u5B89\u5168\u6A21\u5F0F\u81EA\u6551\u3002",
|
|
6175
|
-
"\u67E5\u770B\u72B6\u6001\uFF1Adsh-lark-bot guardian status"
|
|
6328
|
+
"\u5B89\u5168\u7F51\u5B88\u62A4\u5DF2\u9ED8\u8BA4\u5B89\u88C5\uFF1Adsh \u5168\u90E8\u4E0B\u7EBF\u540E\u4ECD\u53EF\u901A\u8FC7\u98DE\u4E66\u53D1\u9001 /safemode \u8FDB\u5165\u4EC5\u6838\u5FC3\u5B89\u5168\u6A21\u5F0F\u81EA\u6551\u3002",
|
|
6329
|
+
"\u67E5\u770B\u72B6\u6001\uFF1Adsh-lark-bot guardian status",
|
|
6330
|
+
"\u4E0D\u9700\u8981\u65F6\u53EF\u8DF3\u8FC7\u5B89\u88C5\uFF1Anpx dsh-lark-bot@latest setup --no-guardian"
|
|
6176
6331
|
] : [
|
|
6177
6332
|
"",
|
|
6178
|
-
"\u63D0\u793A\uFF1A\
|
|
6333
|
+
"\u63D0\u793A\uFF1A\u672C\u6B21\u672A\u5B89\u88C5\u5B89\u5168\u7F51\u5B88\u62A4\uFF08--no-guardian\uFF09\u3002\u9700\u8981\u65F6\u968F\u65F6\u6267\u884C dsh-lark-bot guardian install\u3002"
|
|
6179
6334
|
],
|
|
6180
6335
|
""
|
|
6181
6336
|
].join("\n")
|
|
@@ -7107,10 +7262,10 @@ function buildProgram() {
|
|
|
7107
7262
|
program.name("dsh-lark-bot").description("Bridge DeepSeek Harness into Feishu / Lark").version(packageVersion(), "-v, --version");
|
|
7108
7263
|
program.command("setup").description(
|
|
7109
7264
|
"Install this package as a standard dsh profile bundle (single install path)"
|
|
7110
|
-
).option("--profile <name>", "dsh profile to install into (default: dsh-lark)").option("--guardian", "
|
|
7265
|
+
).option("--profile <name>", "dsh profile to install into (default: dsh-lark)").option("--guardian", "install the safety-net guardian service (installed by default; compatibility)").option("--no-guardian", "skip installing the safety-net guardian service (installed by default)").action(async (opts) => {
|
|
7111
7266
|
await runSetup({
|
|
7112
7267
|
...opts.profile ? { profile: opts.profile } : {},
|
|
7113
|
-
|
|
7268
|
+
guardian: opts.guardian !== false
|
|
7114
7269
|
});
|
|
7115
7270
|
});
|
|
7116
7271
|
program.command("doctor").description("Run local diagnostics").option("--profile <name>", "profile name").option("--workspace <path>", "initial working directory").option("--app-id <id>", "existing Lark/Feishu app id").option("--app-secret <secret>", "existing Lark/Feishu app secret").option("--tenant <tenant>", "feishu or lark").action(async (opts) => {
|