dsh-caduceus 0.1.0 → 0.4.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 +108 -72
- package/README.zh.md +131 -0
- package/dist/bridge/bridge.d.ts +9 -0
- package/dist/bridge/bridge.js +155 -5
- package/dist/bridge/types.d.ts +22 -0
- package/dist/commands/parse.d.ts +0 -11
- package/dist/commands/parse.js +5 -1
- package/dist/config.d.ts +52 -1
- package/dist/config.js +29 -0
- package/dist/gate.d.ts +3 -1
- package/dist/gate.js +6 -5
- package/dist/index.js +20 -1
- package/dist/interactive/approval.d.ts +9 -0
- package/dist/interactive/approval.js +85 -0
- package/dist/interactive/cards.d.ts +36 -0
- package/dist/interactive/cards.js +125 -0
- package/dist/interactive/pending.d.ts +29 -0
- package/dist/interactive/pending.js +39 -0
- package/dist/interactive/question.d.ts +25 -0
- package/dist/interactive/question.js +86 -0
- package/dist/interactive/runtime.d.ts +44 -0
- package/dist/interactive/runtime.js +147 -0
- package/dist/interactive/types.d.ts +50 -0
- package/dist/interactive/types.js +25 -0
- package/dist/mentions.d.ts +15 -0
- package/dist/mentions.js +21 -0
- package/dist/transport/feishu-api.d.ts +9 -1
- package/dist/transport/lark.d.ts +7 -1
- package/dist/transport/lark.js +34 -2
- package/dist/transport/normalize.d.ts +22 -22
- package/dist/transport/normalize.js +83 -3
- package/dist/transport/reliable.d.ts +5 -1
- package/dist/transport/reliable.js +12 -0
- package/dist/transport/types.d.ts +15 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,96 +1,132 @@
|
|
|
1
1
|
# dsh-caduceus
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[中文](README.zh.md) / **English**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/dsh-caduceus)
|
|
6
|
+
[](https://github.com/ArtomYuan/dsh-caduceus)
|
|
7
|
+
[](LICENSE)
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
**Use DeepSeek Harness in Feishu/Lark the way you use an agent — one topic, one session, a visible title, one card per turn.**
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
> Scope: the engine is still dsh. The agent loop, tools, sandbox and skills are unchanged; this package adds the conversation semantics and the chat surface around them.
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
dsh plugin --profile web add dsh-caduceus --allow-build=protobufjs
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
### 路径 2:先授权再装(不带 flag 也能装)
|
|
13
|
+
## What you get
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
- **One scope, one session** — p2p, topic, and group-per-user sessions are kept separate.
|
|
16
|
+
- **Visible titles** — generated from the first prompt and shown on the turn card.
|
|
17
|
+
- **One card per turn** — status and answer live in the same card, edited in place; the final answer is not a second message.
|
|
18
|
+
- **11 commands** — `/new`, `/sessions`, `/rename`, `/resume`, `/adopt`, `/status`, `/stop`, `/doctor`, `/selfcheck`, `/persona`, `/help`.
|
|
19
|
+
- **Persona** — `SOUL.md` is synced into the `AGENTS.md` block, automatically at startup.
|
|
20
|
+
- **Observability** — two-stage self-check, `/doctor`, and in-chat degradation warnings.
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
## Looks like
|
|
25
23
|
|
|
26
|
-
```
|
|
27
|
-
|
|
24
|
+
```text
|
|
25
|
+
飞书会话 · 过程
|
|
26
|
+
会话:重构登录流程 · **运行中** · 步数 4 · 工具 3 · 已运行 12s
|
|
27
|
+
最近:bash
|
|
28
|
+
(the answer streams inside this card; at turn end the same card is frozen as 完成)
|
|
28
29
|
```
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
This is a text sketch, not a screenshot. At turn end the card shows the terminal state and the complete answer; a degraded card (preset not mounted) is marked `(preset 未挂载)`.
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
npm pack
|
|
34
|
-
dsh plugin --profile web add ./dsh-caduceus-0.1.0.tgz --allow-build=protobufjs
|
|
35
|
-
```
|
|
33
|
+
## Requirements
|
|
36
34
|
|
|
37
|
-
|
|
35
|
+
| Requirement | Version / note |
|
|
36
|
+
|:--|:--|
|
|
37
|
+
| dsh | `>=0.2.1-rc.2` |
|
|
38
|
+
| Node.js | `>=22.13` |
|
|
39
|
+
| pnpm | `>=10` (pnpm 11 tested) |
|
|
40
|
+
| Feishu / Lark app | One app can have **one long-connection consumer** at a time |
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
`ERR_PNPM_ALLOW_BUILD_MISSING_PACKAGE` 并安装失败。
|
|
41
|
-
- **为什么需要放行**:`protobufjs` 是飞书 SDK 的传递依赖;pnpm ≥10 默认不执行依赖包的安装/构建脚本,
|
|
42
|
-
未放行时安装会以 `ERR_PNPM_IGNORED_BUILDS: Ignored build scripts: protobufjs@7.6.6` 失败。
|
|
43
|
-
- **确认装上了**:安装后执行
|
|
42
|
+
## Install
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
dsh --profile web --dump-config | grep dsh-caduceus
|
|
47
|
-
```
|
|
44
|
+
1. Add the package (the build flag is required, see the note below):
|
|
48
45
|
|
|
49
|
-
|
|
46
|
+
```bash
|
|
47
|
+
dsh plugin --profile web add dsh-caduceus --allow-build=protobufjs
|
|
48
|
+
```
|
|
50
49
|
|
|
51
|
-
|
|
50
|
+
2. In the profile's `cordis.patch.yml`, give the `caduceus-gateway` row your Feishu credentials
|
|
51
|
+
(`appId` / `appSecret`, or `FEISHU_APP_ID` / `FEISHU_APP_SECRET`).
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
3. Restart dsh from the host side. The log should contain `feishu long connection started`.
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
> **pnpm ≥10 note**: `protobufjs` is a transitive dependency of the Feishu SDK, and pnpm ≥10 does not
|
|
56
|
+
> run dependency install/build scripts by default. Use `--allow-build=protobufjs` **with the equals
|
|
57
|
+
> sign**; the space form (`--allow-build protobufjs`) fails with
|
|
58
|
+
> `ERR_PNPM_ALLOW_BUILD_MISSING_PACKAGE`. Without the flag the install fails with
|
|
59
|
+
> `ERR_PNPM_IGNORED_BUILDS: Ignored build scripts: protobufjs@7.6.6`.
|
|
60
|
+
> Alternatively, pre-authorize in the profile's `pnpm-workspace.yaml`:
|
|
56
61
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
| `groupSessionsPerUser` | `true` | 普通群按发送者隔离会话;话题不受影响 |
|
|
62
|
-
| `busyMode` | `queue` | 群里忙时的投递:`queue` / `steer` / `interrupt` |
|
|
63
|
-
| `cardThrottleMs` | `400` | 过程卡 patch 节流(毫秒) |
|
|
64
|
-
| `personaAutoSync` | `true` | 启动时把 SOUL 同步到 `$DSH_HOME/AGENTS.md` |
|
|
65
|
-
| `agentPreset` | `standard` | 新会话挂载的 dsh agent preset |
|
|
66
|
-
| `logLevel` | `info` | 文件日志级别:`info` / `debug` |
|
|
62
|
+
```yaml
|
|
63
|
+
onlyBuiltDependencies:
|
|
64
|
+
- protobufjs
|
|
65
|
+
```
|
|
67
66
|
|
|
68
|
-
|
|
67
|
+
> Confirm the install with `dsh --profile web --dump-config | grep dsh-caduceus` — expect at least 2 lines.
|
|
69
68
|
|
|
70
|
-
##
|
|
69
|
+
## Configure
|
|
71
70
|
|
|
72
|
-
|
|
|
71
|
+
| Key | Default | Description |
|
|
72
|
+
|:--|:--|:--|
|
|
73
|
+
| `appId` / `appSecret` | — | Feishu app credentials; `FEISHU_APP_ID` / `FEISHU_APP_SECRET` also work |
|
|
74
|
+
| `defaultCwd` | process cwd | Working directory for new sessions |
|
|
75
|
+
| `groupSessionsPerUser` | `true` | Isolate ordinary groups per sender; topics are unaffected |
|
|
76
|
+
| `busyMode` | `queue` | Busy delivery: `queue` / `steer` / `interrupt` |
|
|
77
|
+
| `cardThrottleMs` | `400` | Process-card patch throttle (ms) |
|
|
78
|
+
| `personaAutoSync` | `true` | Sync SOUL into `$DSH_HOME/AGENTS.md` at startup |
|
|
79
|
+
| `agentPreset` | `standard` | dsh agent preset mounted for new sessions |
|
|
80
|
+
| `logLevel` | `info` | File log level: `info` / `debug` |
|
|
81
|
+
| `approvalCard` | `true` | Send interactive approval cards (`allow once` / `reject`) |
|
|
82
|
+
| `questionCard` | `true` | Send interactive question cards; single-select ≤4 options get buttons |
|
|
83
|
+
| `interactiveTimeoutMs` | `300000` | How long an approval/question card waits before it expires |
|
|
84
|
+
| `approvalPolicy` | `ask` | Session approval policy: `ask` (card confirmation, default) or `never` |
|
|
85
|
+
| `reactions` | `true` | Emoji reactions on messages: `OnIt` while running, `DONE`/`CROSS_MARK` at turn end |
|
|
86
|
+
| `reactionOnStart` | `OnIt` | Reaction added when a user message is accepted |
|
|
87
|
+
| `reactionOnDone` | `DONE` | Reaction added when a turn completes |
|
|
88
|
+
| `reactionOnFail` | `CROSS_MARK` | Reaction added when a turn errors/interrupts (falls back to `THINKING`) |
|
|
89
|
+
| `gateRequireBotIdentity` | `true` | Group @ gate fails closed when the bot open id is unknown |
|
|
90
|
+
|
|
91
|
+
## Commands
|
|
92
|
+
|
|
93
|
+
| Command | Description |
|
|
73
94
|
|:--|:--|
|
|
74
|
-
| `/new` |
|
|
75
|
-
| `/sessions [--all]` |
|
|
76
|
-
| `/rename
|
|
77
|
-
| `/resume [id
|
|
78
|
-
| `/adopt <id
|
|
79
|
-
| `/
|
|
80
|
-
| `/
|
|
81
|
-
| `/
|
|
82
|
-
| `/
|
|
83
|
-
| `/persona [show\|sync]` |
|
|
84
|
-
| `/help` |
|
|
85
|
-
|
|
86
|
-
##
|
|
87
|
-
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
-
|
|
92
|
-
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
| `/new` | Materialize a new session immediately; keep the old one |
|
|
96
|
+
| `/sessions [--all]` | List the current session, or every session of this chat |
|
|
97
|
+
| `/rename <title>` | Rename the current session |
|
|
98
|
+
| `/resume [id\|tail]` | Resume the previous session, or switch by full id / ≥8-char tail |
|
|
99
|
+
| `/adopt <id\|tail>` | Adopt an unowned historical session into the current scope |
|
|
100
|
+
| `/status` | Current session status plus the self-check summary |
|
|
101
|
+
| `/stop` | Interrupt the running turn |
|
|
102
|
+
| `/doctor [--json]` | Health report: runtime / Feishu / persona / session / storage / versions |
|
|
103
|
+
| `/selfcheck` | Re-run both self-check stages and print every item |
|
|
104
|
+
| `/persona [show\|sync]` | Show or sync the SOUL persona block |
|
|
105
|
+
| `/help` | Command table |
|
|
106
|
+
|
|
107
|
+
## Troubleshooting
|
|
108
|
+
|
|
109
|
+
- **No `feishu long connection started` in the log**: run `/doctor` first, then check the file log at `$DSH_HOME/caduceus/caduceus.log`.
|
|
110
|
+
- **`/doctor`**: shows runtime, Feishu, persona, session and storage checks; `/doctor --json` gives the same report for scripts or support.
|
|
111
|
+
- **In-chat warnings**: self-check failures are posted into the conversation as `⚠ 自检异常 …(详情 /doctor)`; recovery posts `✓ 自检已恢复`.
|
|
112
|
+
- **`preset:applied` fails**: the agent was created without a preset, so it has no tools and no persona. Check `agentPreset` and that the `agentPresets` service is available.
|
|
113
|
+
- **`persona:fresh` fails**: `SOUL.md` and the block in `$DSH_HOME/AGENTS.md` differ. Run `/persona sync` or let the next startup auto-sync.
|
|
114
|
+
- **Install failed with `ERR_PNPM_IGNORED_BUILDS` / `ERR_PNPM_ALLOW_BUILD_MISSING_PACKAGE`**: see the pnpm note under Install.
|
|
115
|
+
|
|
116
|
+
## Limitations
|
|
117
|
+
|
|
118
|
+
- One Feishu/Lark app supports one long-connection consumer; do not run the same app in two dsh instances.
|
|
119
|
+
- Command turns (`/doctor`, `/status`, `/sessions`, …) do not go through the agent, so tool counts may read `pending` until a normal message is sent.
|
|
120
|
+
- The persona projection targets `$DSH_HOME/AGENTS.md`; a legacy block in `$DSH_AGENTS_HOME/AGENTS.md` is only reported and never deleted.
|
|
121
|
+
- The file log rotates at 5 MB and keeps 3 files.
|
|
122
|
+
- There is no separate Web UI install wizard; installation is through the dsh CLI.
|
|
123
|
+
|
|
124
|
+
## Docs
|
|
125
|
+
|
|
126
|
+
- [docs/DESIGN.md](docs/DESIGN.md) — design and surface semantics
|
|
127
|
+
- [docs/VERIFY.md](docs/VERIFY.md) — isolated-profile verification steps
|
|
128
|
+
- [docs/](docs/) — slice notes (`SLICE*.md`)
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
[MIT](LICENSE) © 2026 ArtomYuan
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# dsh-caduceus
|
|
2
|
+
|
|
3
|
+
[English](README.md) / **中文**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/dsh-caduceus)
|
|
6
|
+
[](https://github.com/ArtomYuan/dsh-caduceus)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
**在飞书/Lark 里像用 agent 一样用 DeepSeek Harness——一个话题一个会话,标题可见,每回合一张卡。**
|
|
10
|
+
|
|
11
|
+
> 作用域:引擎仍是 dsh。agent loop、工具、沙箱、skills 都不变;本包只提供会话语义与聊天界面。
|
|
12
|
+
|
|
13
|
+
## What you get(能拿到什么)
|
|
14
|
+
|
|
15
|
+
- **一个 scope 一个会话** — 私聊、话题、普通群按发送者隔离。
|
|
16
|
+
- **标题可见** — 由第一条 prompt 生成,显示在回合卡上。
|
|
17
|
+
- **每回合一张卡** — 状态与答案在同一张卡里,原地编辑;终稿不再另发一条消息。
|
|
18
|
+
- **11 条命令** — `/new`、`/sessions`、`/rename`、`/resume`、`/adopt`、`/status`、`/stop`、`/doctor`、`/selfcheck`、`/persona`、`/help`。
|
|
19
|
+
- **人格** — `SOUL.md` 同步到 `AGENTS.md` 的标记块,启动时自动执行。
|
|
20
|
+
- **可观测性** — 两阶段自检、`/doctor`、对话内的降级提示。
|
|
21
|
+
|
|
22
|
+
## Looks like(长什么样)
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
飞书会话 · 过程
|
|
26
|
+
会话:重构登录流程 · **运行中** · 步数 4 · 工具 3 · 已运行 12s
|
|
27
|
+
最近:bash
|
|
28
|
+
(正文在这张卡里滚动;收尾时同一张卡定格为「完成」)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
这是文本示意,不是截图。收尾时卡内显示终态与完整答案;降级(preset 未挂载)时状态行会标 `(preset 未挂载)`。
|
|
32
|
+
|
|
33
|
+
## Requirements(前置要求)
|
|
34
|
+
|
|
35
|
+
| 要求 | 版本 / 说明 |
|
|
36
|
+
|:--|:--|
|
|
37
|
+
| dsh | `>=0.2.1-rc.2` |
|
|
38
|
+
| Node.js | `>=22.13` |
|
|
39
|
+
| pnpm | `>=10`(实测 11) |
|
|
40
|
+
| 飞书/Lark 应用 | 一个应用同一时间只能有**一路长连接**消费者 |
|
|
41
|
+
|
|
42
|
+
## Install(安装)
|
|
43
|
+
|
|
44
|
+
1. 装包(必须带 build flag,说明见下):
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
dsh plugin --profile web add dsh-caduceus --allow-build=protobufjs
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
2. 在 profile 的 `cordis.patch.yml` 里,给 `caduceus-gateway` 这一行填飞书凭证
|
|
51
|
+
(`appId` / `appSecret`,或用 `FEISHU_APP_ID` / `FEISHU_APP_SECRET`)。
|
|
52
|
+
|
|
53
|
+
3. 由宿主侧重启 dsh。日志里应出现 `feishu long connection started`。
|
|
54
|
+
|
|
55
|
+
> **pnpm ≥10 注意**:`protobufjs` 是飞书 SDK 的传递依赖;pnpm ≥10 默认不执行依赖包的安装/构建脚本。
|
|
56
|
+
> 必须用**等号**形式 `--allow-build=protobufjs`;空格形式(`--allow-build protobufjs`)会报
|
|
57
|
+
> `ERR_PNPM_ALLOW_BUILD_MISSING_PACKAGE`。不带 flag 会以
|
|
58
|
+
> `ERR_PNPM_IGNORED_BUILDS: Ignored build scripts: protobufjs@7.6.6` 失败。
|
|
59
|
+
> 也可以在 profile 的 `pnpm-workspace.yaml` 里预先放行:
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
onlyBuiltDependencies:
|
|
63
|
+
- protobufjs
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
> 安装后用 `dsh --profile web --dump-config | grep dsh-caduceus` 确认——应至少 2 行。
|
|
67
|
+
|
|
68
|
+
## Configure(配置)
|
|
69
|
+
|
|
70
|
+
| 配置 | 默认 | 说明 |
|
|
71
|
+
|:--|:--|:--|
|
|
72
|
+
| `appId` / `appSecret` | — | 飞书应用凭证;也可用 `FEISHU_APP_ID` / `FEISHU_APP_SECRET` |
|
|
73
|
+
| `defaultCwd` | 当前进程 cwd | 新建会话的工作目录 |
|
|
74
|
+
| `groupSessionsPerUser` | `true` | 普通群按发送者隔离会话;话题不受影响 |
|
|
75
|
+
| `busyMode` | `queue` | 忙碌时投递:`queue` / `steer` / `interrupt` |
|
|
76
|
+
| `cardThrottleMs` | `400` | 过程卡 patch 节流(毫秒) |
|
|
77
|
+
| `personaAutoSync` | `true` | 启动时把 SOUL 同步到 `$DSH_HOME/AGENTS.md` |
|
|
78
|
+
| `agentPreset` | `standard` | 新会话挂载的 dsh agent preset |
|
|
79
|
+
| `logLevel` | `info` | 文件日志级别:`info` / `debug` |
|
|
80
|
+
| `approvalCard` | `true` | 发交互审批卡(允许一次 / 拒绝) |
|
|
81
|
+
| `questionCard` | `true` | 发交互选项卡;单选且 ≤4 个选项时显示按钮 |
|
|
82
|
+
| `interactiveTimeoutMs` | `300000` | 审批/选项卡等待多久后过期 |
|
|
83
|
+
| `approvalPolicy` | `ask` | 会话审批策略:`ask`(弹卡确认,默认)或 `never` |
|
|
84
|
+
| `reactions` | `true` | 消息表情反馈:处理中 `OnIt`,结束 `DONE`/`CROSS_MARK` |
|
|
85
|
+
| `reactionOnStart` | `OnIt` | 收到用户消息时添加的表情 |
|
|
86
|
+
| `reactionOnDone` | `DONE` | 回合完成时添加的表情 |
|
|
87
|
+
| `reactionOnFail` | `CROSS_MARK` | 回合出错/中断时添加的表情(失败退回 `THINKING`) |
|
|
88
|
+
| `gateRequireBotIdentity` | `true` | bot open id 未知时群 @ 门禁 fail-closed(false 恢复旧放行) |
|
|
89
|
+
|
|
90
|
+
## 命令
|
|
91
|
+
|
|
92
|
+
| 命令 | 说明 |
|
|
93
|
+
|:--|:--|
|
|
94
|
+
| `/new` | 立即创建新会话;旧会话保留 |
|
|
95
|
+
| `/sessions [--all]` | 列出当前会话,或本聊天的全部会话 |
|
|
96
|
+
| `/rename <标题>` | 重命名当前会话 |
|
|
97
|
+
| `/resume [id\|尾号]` | 恢复上一条,或按完整 id / ≥8 位尾号切换 |
|
|
98
|
+
| `/adopt <id\|尾号>` | 把无归属的历史会话收养到当前 scope |
|
|
99
|
+
| `/status` | 当前会话状态 + 自检摘要 |
|
|
100
|
+
| `/stop` | 中断正在运行的一轮 |
|
|
101
|
+
| `/doctor [--json]` | 体检报告:运行时 / 飞书 / 人格 / 会话 / 存储 / 版本 |
|
|
102
|
+
| `/selfcheck` | 立即重跑两阶段自检并输出逐项明细 |
|
|
103
|
+
| `/persona [show\|sync]` | 查看或同步 SOUL 人格块 |
|
|
104
|
+
| `/help` | 命令表 |
|
|
105
|
+
|
|
106
|
+
## Troubleshooting(故障排查)
|
|
107
|
+
|
|
108
|
+
- **日志里没有 `feishu long connection started`**:先发 `/doctor`,再看文件日志 `$DSH_HOME/caduceus/caduceus.log`。
|
|
109
|
+
- **`/doctor`**:逐项显示运行时、飞书、人格、会话、存储状态;`/doctor --json` 可给脚本或求助对象。
|
|
110
|
+
- **对话内提示**:自检失败会发 `⚠ 自检异常 …(详情 /doctor)`;恢复会发 `✓ 自检已恢复`。
|
|
111
|
+
- **`preset:applied` 失败**:agent 没挂 preset,等于没工具、没人格;检查 `agentPreset` 与 `agentPresets` 服务。
|
|
112
|
+
- **`persona:fresh` 失败**:`SOUL.md` 与 `$DSH_HOME/AGENTS.md` 的块不一致;发 `/persona sync` 或等下次启动自动同步。
|
|
113
|
+
- **安装报 `ERR_PNPM_IGNORED_BUILDS` / `ERR_PNPM_ALLOW_BUILD_MISSING_PACKAGE`**:见 Install 的 pnpm 说明。
|
|
114
|
+
|
|
115
|
+
## Limitations(限制)
|
|
116
|
+
|
|
117
|
+
- 一个飞书/Lark 应用只支持一路长连接消费者;不要在两个 dsh 实例里跑同一个应用。
|
|
118
|
+
- 命令回合(`/doctor`、`/status`、`/sessions` 等)不经过 agent,工具数在发正常消息前可能显示 `pending`。
|
|
119
|
+
- 人格投影目标是 `$DSH_HOME/AGENTS.md`;旧位置 `$DSH_AGENTS_HOME/AGENTS.md` 的块只会被提示,不会自动删除。
|
|
120
|
+
- 文件日志 5 MB 轮转,保留 3 份。
|
|
121
|
+
- 没有独立的 Web UI 安装向导;安装走 dsh CLI。
|
|
122
|
+
|
|
123
|
+
## Docs(文档)
|
|
124
|
+
|
|
125
|
+
- [docs/DESIGN.md](docs/DESIGN.md) — 设计与界面语义
|
|
126
|
+
- [docs/VERIFY.md](docs/VERIFY.md) — 隔离 profile 验证步骤
|
|
127
|
+
- [docs/](docs/) — 各刀记录(`SLICE*.md`)
|
|
128
|
+
|
|
129
|
+
## License(许可)
|
|
130
|
+
|
|
131
|
+
[MIT](LICENSE) © 2026 ArtomYuan
|
package/dist/bridge/bridge.d.ts
CHANGED
|
@@ -19,6 +19,15 @@ export declare class Bridge {
|
|
|
19
19
|
handleSessionEvent(session: {
|
|
20
20
|
readonly id: string;
|
|
21
21
|
}, event: StatusEventLike): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Send an interactive card the same way process cards go out: reply into the
|
|
24
|
+
* originating topic/thread when the session still has a live scope. Falls
|
|
25
|
+
* back to a chat send only when the scope state is gone.
|
|
26
|
+
*/
|
|
27
|
+
sendInteractiveCard(sessionId: string, card: FeishuCard): Promise<{
|
|
28
|
+
readonly messageId: string;
|
|
29
|
+
readonly threadId?: string;
|
|
30
|
+
}>;
|
|
22
31
|
}
|
|
23
32
|
/** Build the process card for one snapshot (exported for render tests). */
|
|
24
33
|
export declare function progressCardForSnapshot(snapshot: StatusSnapshot, sessionTitle: string | undefined, recentEvent: string, tail: string, now: number, presetMissing?: boolean): FeishuCard;
|
package/dist/bridge/bridge.js
CHANGED
|
@@ -81,6 +81,8 @@ export class Bridge {
|
|
|
81
81
|
#selfCheck;
|
|
82
82
|
#agentCtxBySession = new Map();
|
|
83
83
|
#toolsCheckedSessions = new Set();
|
|
84
|
+
#approvalMissingWarned = false;
|
|
85
|
+
#botIdentityErrorLogged = false;
|
|
84
86
|
/** Last fail-id set already notified into the conversation. */
|
|
85
87
|
#selfCheckNotifiedSignature;
|
|
86
88
|
/** Session ids whose preset mount failed (shown as `(preset 未挂载)`). */
|
|
@@ -88,6 +90,7 @@ export class Bridge {
|
|
|
88
90
|
#sessionTitleProvider;
|
|
89
91
|
#queue = new ScopeQueue();
|
|
90
92
|
#states = new Map();
|
|
93
|
+
#chatIdBySession = new Map();
|
|
91
94
|
#scopeBySession = new Map();
|
|
92
95
|
constructor(options) {
|
|
93
96
|
this.#options = options;
|
|
@@ -117,6 +120,12 @@ export class Bridge {
|
|
|
117
120
|
if (sessionId !== undefined)
|
|
118
121
|
this.#scopeBySession.set(sessionId, key);
|
|
119
122
|
}
|
|
123
|
+
if (options.interactive !== undefined && options.rootCtx !== undefined) {
|
|
124
|
+
options.interactive.registerRoot(options.rootCtx, {
|
|
125
|
+
ownsSession: (sessionId) => this.ownsSession(sessionId),
|
|
126
|
+
contextFor: (sessionId) => this.#interactiveContext(sessionId),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
120
129
|
}
|
|
121
130
|
/** True when a session id belongs to a scope this bridge renders. */
|
|
122
131
|
ownsSession(sessionId) {
|
|
@@ -124,6 +133,17 @@ export class Bridge {
|
|
|
124
133
|
}
|
|
125
134
|
/** Process one inbound message (serialized per scope). */
|
|
126
135
|
async handleInbound(message) {
|
|
136
|
+
// Bot identity may have failed at startup; retry on the next group message
|
|
137
|
+
// before deciding, so a late bot/v3/info success restores normal gating.
|
|
138
|
+
if (message.chatType === 'group' && (this.#transport.getBotOpenId?.() ?? '').length === 0) {
|
|
139
|
+
try {
|
|
140
|
+
await this.#transport.refreshBotOpenId?.();
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
this.#logger.warn(`bot identity refresh failed: ${errorMessage(error)}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const botOpenId = this.#transport.getBotOpenId?.();
|
|
127
147
|
const decision = evaluateGate({
|
|
128
148
|
chatType: message.chatType,
|
|
129
149
|
chatId: message.chatId,
|
|
@@ -131,9 +151,8 @@ export class Bridge {
|
|
|
131
151
|
senderOpenId: message.senderOpenId,
|
|
132
152
|
text: message.text,
|
|
133
153
|
...(message.mentions === undefined ? {} : { mentions: message.mentions }),
|
|
134
|
-
...(
|
|
135
|
-
|
|
136
|
-
: { botOpenId: this.#transport.getBotOpenId?.() }),
|
|
154
|
+
...(botOpenId === undefined ? {} : { botOpenId }),
|
|
155
|
+
requireBotIdentity: this.#config.gateRequireBotIdentity !== false,
|
|
137
156
|
allowedChats: this.#config.allowedChats,
|
|
138
157
|
allowedUsers: this.#config.allowedUsers,
|
|
139
158
|
});
|
|
@@ -142,6 +161,12 @@ export class Bridge {
|
|
|
142
161
|
...(message.threadId === undefined ? {} : { threadId: message.threadId }),
|
|
143
162
|
senderOpenId: message.senderOpenId,
|
|
144
163
|
});
|
|
164
|
+
if (!decision.accept && decision.reason === 'bot-identity-unknown') {
|
|
165
|
+
if (!this.#botIdentityErrorLogged) {
|
|
166
|
+
this.#botIdentityErrorLogged = true;
|
|
167
|
+
this.#logger.error('gate: bot identity unknown; group @ messages are rejected until resolved');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
145
170
|
if (!decision.accept)
|
|
146
171
|
return;
|
|
147
172
|
await this.#queue.run(scopeKey(message.chatType, message.chatId, message.threadId, message.senderOpenId, this.#config.groupSessionsPerUser), () => this.#processInbound(message));
|
|
@@ -218,6 +243,7 @@ export class Bridge {
|
|
|
218
243
|
async #processInbound(message) {
|
|
219
244
|
const state = this.#stateFor(message);
|
|
220
245
|
state.lastInboundMessageId = message.messageId;
|
|
246
|
+
state.ownerOpenId = message.senderOpenId;
|
|
221
247
|
// Startup-stage failures have no session to notify yet; the first message
|
|
222
248
|
// with a session catches up here (once per fail combination).
|
|
223
249
|
await this.#maybeNotifySelfCheck(state);
|
|
@@ -226,6 +252,8 @@ export class Bridge {
|
|
|
226
252
|
await this.#handleSlash(state, message.text);
|
|
227
253
|
return;
|
|
228
254
|
}
|
|
255
|
+
state.reactionQueue.push(message.messageId);
|
|
256
|
+
this.#startReaction(state, message.messageId);
|
|
229
257
|
await this.#deliverTurn(state, message.text);
|
|
230
258
|
}
|
|
231
259
|
async #handleSlash(state, raw) {
|
|
@@ -447,6 +475,7 @@ export class Bridge {
|
|
|
447
475
|
if (notice !== undefined)
|
|
448
476
|
await this.#replyText(state, notice);
|
|
449
477
|
}
|
|
478
|
+
await this.#finishReactions(state, kind);
|
|
450
479
|
state.lastOutput = '';
|
|
451
480
|
}
|
|
452
481
|
#finalCardInput(state, snapshot, body) {
|
|
@@ -698,11 +727,20 @@ export class Bridge {
|
|
|
698
727
|
async #switchSession(state, target, verb = '已恢复会话') {
|
|
699
728
|
const current = state.sessionId ?? this.#sessionMap.get(state.key);
|
|
700
729
|
try {
|
|
701
|
-
const
|
|
730
|
+
const persistedPreset = this.#sessionPresetLookup?.(target);
|
|
731
|
+
const { agent } = await this.#agents.resume({
|
|
732
|
+
resumeSessionId: target,
|
|
733
|
+
setup: async (agentCtx) => {
|
|
734
|
+
this.#agentCtxBySession.set(target, agentCtx);
|
|
735
|
+
if (persistedPreset === undefined)
|
|
736
|
+
await this.#mountPreset(target, agentCtx);
|
|
737
|
+
},
|
|
738
|
+
});
|
|
702
739
|
if (current !== undefined && current !== target) {
|
|
703
740
|
state.previousSessionId = current;
|
|
704
741
|
state.previousSessionAt = this.#lastActivityAt(state) ?? this.#now();
|
|
705
742
|
}
|
|
743
|
+
this.#applyApprovalPolicy(agent);
|
|
706
744
|
this.#sessionMap.set(state.key, target);
|
|
707
745
|
state.sessionId = target;
|
|
708
746
|
state.pendingCreate = false;
|
|
@@ -775,6 +813,45 @@ export class Bridge {
|
|
|
775
813
|
this.#agentPresetsCache = service;
|
|
776
814
|
return service;
|
|
777
815
|
}
|
|
816
|
+
/**
|
|
817
|
+
* Apply the configured session approval policy to a freshly created/resumed
|
|
818
|
+
* agent. Missing service warns once; setPolicy errors never block the turn.
|
|
819
|
+
*/
|
|
820
|
+
#applyApprovalPolicy(agent) {
|
|
821
|
+
const policy = this.#options.approvalPolicy ?? 'ask';
|
|
822
|
+
this.#logger.info(`agent approval policy=${policy}`);
|
|
823
|
+
const provider = this.#options.approvalProvider;
|
|
824
|
+
if (provider === undefined)
|
|
825
|
+
return;
|
|
826
|
+
const service = provider();
|
|
827
|
+
if (service === undefined) {
|
|
828
|
+
if (!this.#approvalMissingWarned) {
|
|
829
|
+
this.#approvalMissingWarned = true;
|
|
830
|
+
this.#logger.warn('approval service unavailable; session approval policy not applied');
|
|
831
|
+
}
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
try {
|
|
835
|
+
service.setPolicy(agent, policy);
|
|
836
|
+
}
|
|
837
|
+
catch (error) {
|
|
838
|
+
this.#logger.warn(`agent approval policy=${policy} failed: ${errorMessage(error)}`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
#interactiveContext(sessionId) {
|
|
842
|
+
const key = this.#scopeBySession.get(sessionId);
|
|
843
|
+
if (key === undefined)
|
|
844
|
+
return undefined;
|
|
845
|
+
const state = this.#states.get(key);
|
|
846
|
+
if (state === undefined || state.ownerOpenId === undefined)
|
|
847
|
+
return undefined;
|
|
848
|
+
return {
|
|
849
|
+
scopeKey: state.key,
|
|
850
|
+
chatId: state.chatId,
|
|
851
|
+
ownerOpenId: state.ownerOpenId,
|
|
852
|
+
sessionId,
|
|
853
|
+
};
|
|
854
|
+
}
|
|
778
855
|
/**
|
|
779
856
|
* Mount the configured agent preset in the unpublished agent scope.
|
|
780
857
|
* Idempotent for resumes: a context that already reports a composed preset is
|
|
@@ -827,6 +904,7 @@ export class Bridge {
|
|
|
827
904
|
catch (error) {
|
|
828
905
|
this.#logger.warn(`ownership write failed for ${sessionId}: ${errorMessage(error)}`);
|
|
829
906
|
}
|
|
907
|
+
this.#applyApprovalPolicy(agent);
|
|
830
908
|
await this.#runSelfCheckStageB(state, sessionId);
|
|
831
909
|
return { agent };
|
|
832
910
|
}
|
|
@@ -1020,6 +1098,7 @@ export class Bridge {
|
|
|
1020
1098
|
},
|
|
1021
1099
|
});
|
|
1022
1100
|
this.#bindSession(state, String(agent.session.id));
|
|
1101
|
+
this.#applyApprovalPolicy(agent);
|
|
1023
1102
|
await this.#runSelfCheckStageB(state, sessionId);
|
|
1024
1103
|
return { agent, sessionId, cwd };
|
|
1025
1104
|
}
|
|
@@ -1045,8 +1124,77 @@ export class Bridge {
|
|
|
1045
1124
|
this.#scopeBySession.delete(id);
|
|
1046
1125
|
}
|
|
1047
1126
|
this.#scopeBySession.set(sessionId, state.key);
|
|
1127
|
+
this.#chatIdBySession.set(sessionId, state.chatId);
|
|
1048
1128
|
state.sessionId = sessionId;
|
|
1049
1129
|
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Send an interactive card the same way process cards go out: reply into the
|
|
1132
|
+
* originating topic/thread when the session still has a live scope. Falls
|
|
1133
|
+
* back to a chat send only when the scope state is gone.
|
|
1134
|
+
*/
|
|
1135
|
+
async sendInteractiveCard(sessionId, card) {
|
|
1136
|
+
const key = this.#scopeBySession.get(sessionId);
|
|
1137
|
+
const state = key === undefined ? undefined : this.#states.get(key);
|
|
1138
|
+
if (state === undefined) {
|
|
1139
|
+
const chatId = this.#chatIdBySession.get(sessionId) ?? '';
|
|
1140
|
+
this.#logger.warn(`interactive: no scope state for ${sessionId}; falling back to chat send`);
|
|
1141
|
+
return this.#transport.sendCard(chatId, card);
|
|
1142
|
+
}
|
|
1143
|
+
const sent = await this.#sendNewCard(state, card);
|
|
1144
|
+
return {
|
|
1145
|
+
...sent,
|
|
1146
|
+
...(state.threadId === undefined ? {} : { threadId: state.threadId }),
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
#startReaction(state, messageId) {
|
|
1150
|
+
if (this.#config.reactions === false)
|
|
1151
|
+
return;
|
|
1152
|
+
const promise = this.#safeAddReaction(messageId, this.#config.reactionOnStart ?? 'OnIt');
|
|
1153
|
+
state.reactionStarts.set(messageId, promise);
|
|
1154
|
+
void promise;
|
|
1155
|
+
}
|
|
1156
|
+
/** Remove the start reaction, then add the terminal reaction (best effort). */
|
|
1157
|
+
async #finishReactions(state, turnKind) {
|
|
1158
|
+
if (this.#config.reactions === false)
|
|
1159
|
+
return;
|
|
1160
|
+
const messageId = state.reactionQueue.shift();
|
|
1161
|
+
if (messageId === undefined)
|
|
1162
|
+
return;
|
|
1163
|
+
const start = state.reactionStarts.get(messageId);
|
|
1164
|
+
state.reactionStarts.delete(messageId);
|
|
1165
|
+
const reactionId = start === undefined ? undefined : await start;
|
|
1166
|
+
if (reactionId !== undefined)
|
|
1167
|
+
await this.#safeRemoveReaction(messageId, reactionId);
|
|
1168
|
+
const completed = turnKind === 'completed';
|
|
1169
|
+
const emoji = completed
|
|
1170
|
+
? (this.#config.reactionOnDone ?? 'DONE')
|
|
1171
|
+
: (this.#config.reactionOnFail ?? 'CROSS_MARK');
|
|
1172
|
+
const added = await this.#safeAddReaction(messageId, emoji);
|
|
1173
|
+
if (!completed && added === undefined && emoji !== 'THINKING') {
|
|
1174
|
+
await this.#safeAddReaction(messageId, 'THINKING');
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
async #safeAddReaction(messageId, emojiType) {
|
|
1178
|
+
try {
|
|
1179
|
+
return await this.#transport.addReaction(messageId, emojiType);
|
|
1180
|
+
}
|
|
1181
|
+
catch (error) {
|
|
1182
|
+
this.#logReactionSkipped(error);
|
|
1183
|
+
return undefined;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
async #safeRemoveReaction(messageId, reactionId) {
|
|
1187
|
+
try {
|
|
1188
|
+
await this.#transport.removeReaction(messageId, reactionId);
|
|
1189
|
+
}
|
|
1190
|
+
catch (error) {
|
|
1191
|
+
this.#logReactionSkipped(error);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
#logReactionSkipped(error) {
|
|
1195
|
+
const code = error.code;
|
|
1196
|
+
this.#logger.debug(`reaction skipped: ${typeof code === 'number' ? code : 'unknown'} ${errorMessage(error)}`);
|
|
1197
|
+
}
|
|
1050
1198
|
#stateFor(message) {
|
|
1051
1199
|
const key = scopeKey(message.chatType, message.chatId, message.threadId, message.senderOpenId, this.#config.groupSessionsPerUser);
|
|
1052
1200
|
const existing = this.#states.get(key);
|
|
@@ -1061,6 +1209,8 @@ export class Bridge {
|
|
|
1061
1209
|
running: false,
|
|
1062
1210
|
status: new StatusTracker(),
|
|
1063
1211
|
lastOutput: '',
|
|
1212
|
+
reactionQueue: [],
|
|
1213
|
+
reactionStarts: new Map(),
|
|
1064
1214
|
};
|
|
1065
1215
|
this.#states.set(key, created);
|
|
1066
1216
|
return created;
|
|
@@ -1080,7 +1230,7 @@ export class Bridge {
|
|
|
1080
1230
|
}
|
|
1081
1231
|
async #sendNewCard(state, card) {
|
|
1082
1232
|
if (state.lastInboundMessageId !== undefined) {
|
|
1083
|
-
return this.#transport.reply(state.chatId, state.lastInboundMessageId, { kind: 'card', card }, { threadId: state.threadId });
|
|
1233
|
+
return this.#transport.reply(state.chatId, state.lastInboundMessageId, { kind: 'card', card }, state.threadId === undefined ? {} : { threadId: state.threadId });
|
|
1084
1234
|
}
|
|
1085
1235
|
return this.#transport.sendCard(state.chatId, card);
|
|
1086
1236
|
}
|