dsh-hooks 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PeterBon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # dsh-hooks
2
+
3
+ Config-driven lifecycle hooks plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh).
4
+
5
+ Declare `event -> command` hooks directly in your profile's `cordis.patch.yml` — like Codex CLI / OpenCode hooks, but for dsh. No plugin code required.
6
+
7
+ [中文文档](README.zh.md) | [Design](#design) | [Feishu example](examples/notify-feishu.mjs)
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ dsh plugin --profile web add github:PeterBon/dsh-hooks
13
+ ```
14
+
15
+ Restart `dsh web`.
16
+
17
+ ## Configure
18
+
19
+ Add a config block to your profile's `cordis.patch.yml`:
20
+
21
+ ```yaml
22
+ - id: dsh-hooks
23
+ name: dsh-hooks
24
+ config:
25
+ hooks:
26
+ - on: 'turn/end'
27
+ when: 'completed' # optional: only completed turns
28
+ run: 'node examples/notify-feishu.mjs'
29
+ timeoutMs: 10000 # optional, default 10000
30
+ - on: 'approval/asked'
31
+ run: 'powershell -Command "Write-Output approval-requested >> hooks.log"'
32
+ ```
33
+
34
+ ## Events (v1)
35
+
36
+ | Event | When it fires | Useful context |
37
+ | --- | --- | --- |
38
+ | `turn/start` | A turn begins | session id, turn |
39
+ | `turn/end` | A turn ends (`completed` / `error` / `aborted` / `blocked` / `max-tokens` / `interrupted`) | reason, turn, duration |
40
+ | `approval/asked` | A tool call requests user approval | tool name, call id, reason |
41
+ | `agent/created` | An agent is published | session id |
42
+ | `agent/disposed` | An agent leaves the registry | session id |
43
+ | `agent/error` | The agent loop reports an error | error text |
44
+ | `agent/status` | Agent status transition | status |
45
+
46
+ The `when` filter for `turn/end` matches the `reason.kind` value (`completed`, `error`, …). Hooks for other events run unconditionally.
47
+
48
+ ## Command execution
49
+
50
+ - Each matching hook spawns `run` through the platform shell, **fire-and-forget**: failures only `console.warn`, never retried, never block the agent loop.
51
+ - Context is passed via **environment variables** (no shell injection through data):
52
+
53
+ | Variable | Meaning |
54
+ | --- | --- |
55
+ | `DSH_HOOK_EVENT` | event type, e.g. `turn/end` |
56
+ | `DSH_HOOK_SESSION_ID` | session id |
57
+ | `DSH_HOOK_SESSION_NAME` | readable session title (latest `session/title` log event, or first human prompt) |
58
+ | `DSH_HOOK_TURN` | turn number (turn events) |
59
+ | `DSH_HOOK_REASON` | turn end reason kind |
60
+ | `DSH_HOOK_TOOL` | tool name (approval events) |
61
+ | `DSH_HOOK_CALL_ID` | tool call id (approval events) |
62
+ | `DSH_HOOK_DURATION_MS` | turn duration ms (turn/end) |
63
+ | `DSH_HOOK_STATUS` | agent status (`agent/status`) |
64
+ | `DSH_HOOK_ERROR` | error text (`agent/error`, and the failure message on `turn/end` error) |
65
+ | `DSH_HOOK_CONTENT` | the turn's final assistant text (turn events) |
66
+ | `DSH_HOOK_TIMESTAMP` | ISO timestamp |
67
+
68
+ - `{{var}}` placeholders inside `run` are substituted from the same context, e.g. `run: 'echo {{DSH_HOOK_SESSION_ID}} >> log.txt'`.
69
+
70
+ ## Feishu notification example
71
+
72
+ The fastest path is the one-shot setup CLI — it creates the Feishu app for you via a QR-code scan and writes all hook config:
73
+
74
+ ```sh
75
+ dsh-hooks feishu-setup # default profile: web
76
+ dsh-hooks feishu-setup --profile work # another profile
77
+ dsh-hooks feishu-test # send a test card with the stored credentials
78
+ ```
79
+
80
+ `feishu-setup` prints a QR code (and opens it in your browser), waits for you to scan it with Feishu, then creates an app named 「DSH 通知机器人」 with message-send permission and writes:
81
+
82
+ | File | Purpose |
83
+ | --- | --- |
84
+ | `~/.dsh/dsh-hooks/feishu-config.json` | app id/secret + your open_id as the notification target (0600, never committed); `result_max_chars` sets the card content truncation (default 300) |
85
+ | `~/.dsh/dsh-hooks/notify-feishu.mjs` | stable copy of the notify script the hooks reference |
86
+ | `~/.dsh/profiles/<profile>/cordis.patch.yml` | dsh-hooks block: `turn/end` (completed/error/aborted) + `approval/asked` + `agent/error` card hooks |
87
+
88
+ Restart `dsh web` afterwards — you will get cards when turns finish, approvals are asked, or the agent errors.
89
+
90
+ ### Manual configuration
91
+
92
+ Prefer wiring it by hand? See [`examples/notify-feishu.mjs`](examples/notify-feishu.mjs) — a zero-dependency script that posts turn-completion / approval notices through the Feishu **app API** (works without a group custom bot). Configure it like:
93
+
94
+ ```yaml
95
+ - id: dsh-hooks
96
+ name: dsh-hooks
97
+ config:
98
+ hooks:
99
+ - on: 'turn/end'
100
+ when: 'completed'
101
+ run: 'node D:/path/to/examples/notify-feishu.mjs'
102
+ - on: 'approval/asked'
103
+ run: 'node D:/path/to/examples/notify-feishu.mjs --approval'
104
+ ```
105
+
106
+ with `DSH_HOOKS_FEISHU_APP_ID` / `DSH_HOOKS_FEISHU_APP_SECRET` / `DSH_HOOKS_FEISHU_TO` in the process environment (never in config files).
107
+
108
+ ## Security
109
+
110
+ Hooks execute arbitrary commands with the dsh process privileges. Only configure commands you trust. Secrets belong in environment variables or the dsh credential store — never in `cordis.patch.yml`.
111
+
112
+ ## Design
113
+
114
+ Follows the dsh plugin conventions: `dsh.bundle.patch` mounts the plugin row, the plugin listens to the durable `session/event` firehose plus agent lifecycle events, and emissions are irreversible side effects that compensate rather than block (failures warn, never retry).
115
+
116
+ ## Development
117
+
118
+ ```sh
119
+ pnpm install
120
+ pnpm run check # typecheck + test + build
121
+ ```
122
+
123
+ ## License
124
+
125
+ MIT
package/README.zh.md ADDED
@@ -0,0 +1,125 @@
1
+ # dsh-hooks
2
+
3
+ [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(dsh)的配置驱动生命周期 hooks 插件。
4
+
5
+ 直接在 profile 的 `cordis.patch.yml` 里声明「事件 → 命令」——就像 Codex CLI / OpenCode 的 hooks,但属于 dsh。不需要写插件代码。
6
+
7
+ [English](README.md) | [设计](#设计) | [飞书示例](examples/notify-feishu.mjs)
8
+
9
+ ## 安装
10
+
11
+ ```sh
12
+ dsh plugin --profile web add github:PeterBon/dsh-hooks
13
+ ```
14
+
15
+ 重启 `dsh web` 生效。
16
+
17
+ ## 配置
18
+
19
+ 在你的 profile 的 `cordis.patch.yml` 里添加配置块:
20
+
21
+ ```yaml
22
+ - id: dsh-hooks
23
+ name: dsh-hooks
24
+ config:
25
+ hooks:
26
+ - on: 'turn/end'
27
+ when: 'completed' # 可选:只在回合正常完成时触发
28
+ run: 'node examples/notify-feishu.mjs'
29
+ timeoutMs: 10000 # 可选,默认 10000
30
+ - on: 'approval/asked'
31
+ run: 'powershell -Command "Add-Content hooks.log approval-requested"'
32
+ ```
33
+
34
+ ## 事件(v1)
35
+
36
+ | 事件 | 触发时机 | 有用上下文 |
37
+ | --- | --- | --- |
38
+ | `turn/start` | 回合开始 | 会话 id、回合号 |
39
+ | `turn/end` | 回合结束(`completed` / `error` / `aborted` / `blocked` / `max-tokens` / `interrupted`) | reason、回合号、耗时 |
40
+ | `approval/asked` | 工具调用请求用户审批 | 工具名、调用 id、原因 |
41
+ | `agent/created` | Agent 发布 | 会话 id |
42
+ | `agent/disposed` | Agent 离开注册表 | 会话 id |
43
+ | `agent/error` | Agent 循环报错 | 错误文本 |
44
+ | `agent/status` | Agent 状态切换 | 状态 |
45
+
46
+ `turn/end` 的 `when` 匹配结束原因(`completed`、`error`…);其他事件的 hook 无条件执行。
47
+
48
+ ## 命令执行
49
+
50
+ - 每个命中的 hook 通过系统 shell 执行 `run`,**fire-and-forget**:失败只 `console.warn`,绝不重试、绝不阻塞 agent 循环。
51
+ - 上下文通过**环境变量**传递(数据不拼接进 shell 字符串,防注入):
52
+
53
+ | 变量 | 含义 |
54
+ | --- | --- |
55
+ | `DSH_HOOK_EVENT` | 事件类型,如 `turn/end` |
56
+ | `DSH_HOOK_SESSION_ID` | 会话 id |
57
+ | `DSH_HOOK_SESSION_NAME` | 会话可读标题(最新 `session/title` 日志事件,或首个用户消息回退) |
58
+ | `DSH_HOOK_TURN` | 回合号(回合事件) |
59
+ | `DSH_HOOK_REASON` | 回合结束原因 |
60
+ | `DSH_HOOK_TOOL` | 工具名(审批事件) |
61
+ | `DSH_HOOK_CALL_ID` | 工具调用 id(审批事件) |
62
+ | `DSH_HOOK_DURATION_MS` | 回合耗时毫秒(turn/end) |
63
+ | `DSH_HOOK_STATUS` | Agent 状态(agent/status) |
64
+ | `DSH_HOOK_ERROR` | 错误文本(agent/error,以及 turn/end 出错时的失败详情) |
65
+ | `DSH_HOOK_CONTENT` | 该回合最后一段助手回复文本(回合事件) |
66
+ | `DSH_HOOK_TIMESTAMP` | ISO 时间戳 |
67
+
68
+ - `run` 里的 `{{变量}}` 占位符会从同一上下文替换,例如 `run: 'echo {{DSH_HOOK_SESSION_ID}} >> log.txt'`。
69
+
70
+ ## 飞书通知示例
71
+
72
+ 最快的方式是一步到位的 setup CLI——扫码自动创建飞书应用并写好全部 hook 配置:
73
+
74
+ ```sh
75
+ dsh-hooks feishu-setup # 默认 profile:web
76
+ dsh-hooks feishu-setup --profile work # 指定其他 profile
77
+ dsh-hooks feishu-test # 用已存凭据发送测试卡片验证
78
+ ```
79
+
80
+ `feishu-setup` 会打印二维码(并在浏览器中打开),等你用飞书扫码后,自动创建名为「DSH 通知机器人」的应用(带消息发送权限),并写入:
81
+
82
+ | 文件 | 用途 |
83
+ | --- | --- |
84
+ | `~/.dsh/dsh-hooks/feishu-config.json` | app id/secret 与你的 open_id(通知目标),权限 0600,严禁提交;`result_max_chars` 控制卡片内容截断长度(默认 300) |
85
+ | `~/.dsh/dsh-hooks/notify-feishu.mjs` | hook 引用的通知脚本稳定副本 |
86
+ | `~/.dsh/profiles/<profile>/cordis.patch.yml` | dsh-hooks 配置块:`turn/end`(completed/error/aborted)+ `approval/asked` + `agent/error` 卡片 hook |
87
+
88
+ 完成后重启 `dsh web`——回合结束、请求审批、agent 出错时就会收到卡片通知。
89
+
90
+ ### 手动配置
91
+
92
+ 想自己接线?见 [`examples/notify-feishu.mjs`](examples/notify-feishu.mjs)——零依赖脚本,通过飞书**应用 API**(不需要群自定义机器人)发送回合完成 / 审批通知。配置示例:
93
+
94
+ ```yaml
95
+ - id: dsh-hooks
96
+ name: dsh-hooks
97
+ config:
98
+ hooks:
99
+ - on: 'turn/end'
100
+ when: 'completed'
101
+ run: 'node D:/path/to/examples/notify-feishu.mjs'
102
+ - on: 'approval/asked'
103
+ run: 'node D:/path/to/examples/notify-feishu.mjs --approval'
104
+ ```
105
+
106
+ 同时在 dsh 进程环境中提供 `DSH_HOOKS_FEISHU_APP_ID` / `DSH_HOOKS_FEISHU_APP_SECRET` / `DSH_HOOKS_FEISHU_TO`(绝不能写进配置文件)。
107
+
108
+ ## 安全
109
+
110
+ Hook 会以 dsh 进程的权限执行任意命令,只配置你信任的命令。Secret 放环境变量或 dsh 凭据存储——永远不要写进 `cordis.patch.yml`。
111
+
112
+ ## 设计
113
+
114
+ 遵循 dsh 插件约定:`dsh.bundle.patch` 挂载插件行;插件监听持久 `session/event` firehose 与 agent 生命周期事件;发射是不可逆副作用,补偿而非阻塞(失败仅警告、绝不重试)。
115
+
116
+ ## 开发
117
+
118
+ ```sh
119
+ pnpm install
120
+ pnpm run check # typecheck + test + build
121
+ ```
122
+
123
+ ## License
124
+
125
+ MIT
@@ -0,0 +1,312 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * dsh-hooks CLI — Feishu notification setup, zero manual app creation.
4
+ *
5
+ * Commands:
6
+ * dsh-hooks feishu-setup [--profile <name>] scan a QR code to create a
7
+ * Feishu bot app automatically
8
+ * (official registerApp flow),
9
+ * write credentials + hook
10
+ * config, then send a welcome
11
+ * card to the scanning user.
12
+ * dsh-hooks feishu-test verify credentials and send a
13
+ * test card to the configured
14
+ * target.
15
+ *
16
+ * The setup writes:
17
+ * ~/.dsh/dsh-hooks/feishu-config.json app_id/app_secret/target (0600)
18
+ * ~/.dsh/profiles/<name>/cordis.patch.yml dsh-hooks config block with
19
+ * turn/end + approval/asked +
20
+ * agent/error card hooks
21
+ *
22
+ * Requires Node >= 22. Dependencies: @larksuiteoapi/node-sdk (registerApp),
23
+ * qrcode (terminal QR), yaml (patch merge). Credentials never enter argv or
24
+ * the environment beyond the setup process itself.
25
+ */
26
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'node:fs'
27
+ import { spawn } from 'node:child_process'
28
+ import { homedir } from 'node:os'
29
+ import { join } from 'node:path'
30
+ import { registerApp } from '@larksuiteoapi/node-sdk'
31
+ import QRCode from 'qrcode'
32
+ import YAML from 'yaml'
33
+ import { run as notifyRun } from '../examples/notify-feishu.mjs'
34
+
35
+ const CONFIG_DIR = join(homedir(), '.dsh', 'dsh-hooks')
36
+ export const CONFIG_PATH = join(CONFIG_DIR, 'feishu-config.json')
37
+
38
+ /** Open a URL in the default browser (best-effort, never throws). */
39
+ export function openInBrowser(url) {
40
+ return new Promise((resolve) => {
41
+ const command =
42
+ process.platform === 'darwin'
43
+ ? { executable: 'open', args: [url] }
44
+ : process.platform === 'win32'
45
+ ? { executable: 'cmd', args: ['/c', 'start', '', url] }
46
+ : { executable: 'xdg-open', args: [url] }
47
+ const child = spawn(command.executable, command.args, { detached: true, stdio: 'ignore' })
48
+ child.on('error', () => resolve(undefined))
49
+ child.on('spawn', () => {
50
+ child.unref()
51
+ resolve(undefined)
52
+ })
53
+ })
54
+ }
55
+
56
+ /** Profile patch file for a profile name. */
57
+ export function patchPath(profile) {
58
+ return join(homedir(), '.dsh', 'profiles', profile, 'cordis.patch.yml')
59
+ }
60
+
61
+ /** Which hooks the setup installs into the profile. */
62
+ export function setupHooks(scriptPath) {
63
+ return [
64
+ { on: 'turn/end', when: 'completed', run: `node ${JSON.stringify(scriptPath)}`, timeoutMs: 30000 },
65
+ { on: 'turn/end', when: 'error', run: `node ${JSON.stringify(scriptPath)}`, timeoutMs: 30000 },
66
+ { on: 'turn/end', when: 'aborted', run: `node ${JSON.stringify(scriptPath)}`, timeoutMs: 30000 },
67
+ { on: 'approval/asked', run: `node ${JSON.stringify(scriptPath)} --approval`, timeoutMs: 30000 },
68
+ { on: 'agent/error', run: `node ${JSON.stringify(scriptPath)}`, timeoutMs: 30000 },
69
+ ]
70
+ }
71
+
72
+ /** Absolute path of the shipped notify script (where it lives now). */
73
+ export function notifyScriptPath() {
74
+ return new URL('../examples/notify-feishu.mjs', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1')
75
+ }
76
+
77
+ /**
78
+ * Resolve the stable notify-script location hooks should reference.
79
+ * The npx cache (where this CLI often runs from) is ephemeral, so the
80
+ * setup copies the zero-dependency script next to feishu-config.json:
81
+ * ~/.dsh/dsh-hooks/notify-feishu.mjs. Re-copies on every setup so the
82
+ * stable copy tracks the installed CLI version.
83
+ */
84
+ export function stableScriptPath(paths = {}) {
85
+ return paths.notifyScript ?? join(CONFIG_DIR, 'notify-feishu.mjs')
86
+ }
87
+
88
+ /**
89
+ * Write the credential file with 0600 perms (owner-only), matching the
90
+ * feishu-notify security posture: secrets stay out of the repo and argv.
91
+ */
92
+ export function writeConfig(configPath, { appId, appSecret, targetType = 'open_id', targetId, resultMaxChars = 300 }) {
93
+ mkdirSync(join(configPath, '..'), { recursive: true, mode: 0o700 })
94
+ const doc = JSON.stringify(
95
+ {
96
+ app_id: appId,
97
+ app_secret: appSecret,
98
+ target_type: targetType,
99
+ target_id: targetId,
100
+ result_max_chars: resultMaxChars,
101
+ },
102
+ null,
103
+ 2,
104
+ )
105
+ writeFileSync(configPath, doc + '\n', 'utf8')
106
+ try {
107
+ chmodSync(configPath, 0o600)
108
+ } catch {
109
+ // Windows: ACL-based protection; the file lives under the user profile.
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Merge the dsh-hooks config block into a profile's cordis.patch.yml:
115
+ * existing dsh-hooks entries keep unrelated config and get their hooks
116
+ * replaced with `setupHooks`; other entries stay untouched. Idempotent.
117
+ */
118
+ export function mergePatchYaml(existingText, { scriptPath }) {
119
+ let entries
120
+ try {
121
+ entries = YAML.parse(existingText || '[]\n')
122
+ } catch {
123
+ throw new Error('profile 的 cordis.patch.yml 解析失败,请先修复该文件')
124
+ }
125
+ if (!Array.isArray(entries)) throw new Error('cordis.patch.yml 顶层必须是 YAML 数组')
126
+
127
+ const hooks = setupHooks(scriptPath)
128
+ let found = false
129
+ for (const entry of entries) {
130
+ if (entry && typeof entry === 'object' && entry.id === 'dsh-hooks') {
131
+ entry.name = 'dsh-hooks'
132
+ entry.config = { hooks }
133
+ found = true
134
+ break
135
+ }
136
+ }
137
+ if (!found) entries.push({ id: 'dsh-hooks', name: 'dsh-hooks', config: { hooks } })
138
+ return YAML.stringify(entries)
139
+ }
140
+
141
+ /**
142
+ * Full setup flow. `deps` is injectable for tests:
143
+ * registerAppFn — the official registerApp (default)
144
+ * print/printErr — output sinks
145
+ * openUrl — browser opener (no-op by default in tests)
146
+ * paths — { configPath, patchFile, notifyScript }
147
+ * Returns the created app facts (without the secret in logs).
148
+ */
149
+ export async function setupFeishu({
150
+ profile = 'web',
151
+ registerAppFn = registerApp,
152
+ print = console.log,
153
+ printErr = console.error,
154
+ openUrl = openInBrowser,
155
+ paths = {},
156
+ } = {}) {
157
+ const configPath = paths.configPath ?? CONFIG_PATH
158
+ const patchFile = paths.patchFile ?? patchPath(profile)
159
+ const notifyScript = stableScriptPath(paths)
160
+
161
+ print('dsh-hooks feishu-setup')
162
+ print('1/4 正在生成飞书「一键创建应用」二维码…')
163
+
164
+ const result = await registerAppFn({
165
+ source: 'dsh-hooks',
166
+ createOnly: true,
167
+ appPreset: {
168
+ name: 'DSH 通知机器人',
169
+ desc: 'DeepSeek Harness 会话事件通知(dsh-hooks)',
170
+ },
171
+ addons: {
172
+ preset: false,
173
+ scopes: {
174
+ tenant: ['im:message:send_as_bot'],
175
+ },
176
+ },
177
+ onQRCodeReady: (authorization) => {
178
+ print('')
179
+ print(`请用飞书扫码(${authorization.expireIn} 秒内有效),或在浏览器打开:`)
180
+ print(authorization.url)
181
+ try {
182
+ QRCode.toString(authorization.url, { type: 'terminal', small: true }, (err, qr) => {
183
+ if (!err) print(qr)
184
+ })
185
+ } catch {
186
+ // Terminal QR is best-effort; the URL above always works.
187
+ }
188
+ // Never let a browser-opener failure break the scan flow.
189
+ void Promise.resolve(openUrl(authorization.url)).catch(() => undefined)
190
+ },
191
+ })
192
+
193
+ const appId = result.client_id
194
+ const appSecret = result.client_secret
195
+ const ownerOpenId = result.user_info?.open_id
196
+ if (!appId || !appSecret) throw new Error('扫码创建未完成,未拿到应用凭证')
197
+ if (!ownerOpenId) throw new Error('扫码结果缺少 open_id,请重试')
198
+
199
+ print('')
200
+ print(`2/4 应用创建成功:${appId}(机器人将私聊通知你)`)
201
+
202
+ writeConfig(configPath, {
203
+ appId,
204
+ appSecret,
205
+ targetType: 'open_id',
206
+ targetId: ownerOpenId,
207
+ resultMaxChars: 300,
208
+ })
209
+ print(`3/4 凭据已写入 ${configPath}(权限 0600,勿提交到仓库)`)
210
+
211
+ // Copy the notify script to its stable location so hooks never
212
+ // reference the ephemeral npx cache.
213
+ if (!paths.notifyScript) {
214
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 })
215
+ writeFileSync(notifyScript, readFileSync(notifyScriptPath(), 'utf8'), 'utf8')
216
+ }
217
+
218
+ const existing = existsSync(patchFile) ? readFileSync(patchFile, 'utf8') : '[]\n'
219
+ const merged = mergePatchYaml(existing, { scriptPath: notifyScript })
220
+ writeFileSync(patchFile, merged, 'utf8')
221
+ print(`4/4 hook 配置已写入 ${patchFile}`)
222
+
223
+ print('发送欢迎卡片验证…')
224
+ try {
225
+ await notifyRun({
226
+ appId,
227
+ appSecret,
228
+ to: ownerOpenId,
229
+ event: 'agent/created',
230
+ sessionId: 'dsh-hooks-setup',
231
+ cwd: process.cwd(),
232
+ timestamp: new Date().toISOString(),
233
+ })
234
+ print('✅ 欢迎卡片已发送。请重启 dsh web 使 hooks 生效。')
235
+ } catch (error) {
236
+ printErr(`⚠ 欢迎卡片发送失败(配置已就绪,可稍后用 feishu-test 重试):${error instanceof Error ? error.message : String(error)}`)
237
+ }
238
+
239
+ return { appId, ownerOpenId }
240
+ }
241
+
242
+ /** Test credentials and send a test card to the configured target. */
243
+ export async function testFeishu({ print = console.log, paths = {} } = {}) {
244
+ const configPath = paths.configPath ?? CONFIG_PATH
245
+ if (!existsSync(configPath)) {
246
+ throw new Error(`未找到配置文件 ${configPath},请先运行 feishu-setup`)
247
+ }
248
+ let file
249
+ try {
250
+ file = JSON.parse(readFileSync(configPath, 'utf8'))
251
+ } catch {
252
+ throw new Error(`配置文件 ${configPath} 解析失败,请重新运行 feishu-setup`)
253
+ }
254
+ if (!file.app_id || !file.app_secret || !file.target_id) {
255
+ throw new Error('配置文件不完整,请重新运行 feishu-setup')
256
+ }
257
+ await notifyRun({
258
+ appId: file.app_id,
259
+ appSecret: file.app_secret,
260
+ to: file.target_id,
261
+ event: 'agent/status',
262
+ status: 'connected',
263
+ sessionId: 'feishu-test',
264
+ cwd: process.cwd(),
265
+ timestamp: new Date().toISOString(),
266
+ })
267
+ print('✅ 测试卡片已发送')
268
+ }
269
+
270
+ const [, , command, ...args] = process.argv
271
+
272
+ function cliArgs(args) {
273
+ const opts = {}
274
+ for (let i = 0; i < args.length; i++) {
275
+ if (args[i] === '--profile') opts.profile = args[++i]
276
+ }
277
+ return opts
278
+ }
279
+
280
+ function isDirectRun() {
281
+ try {
282
+ return process.argv[1] !== undefined && import.meta.url === new URL(`file:///${process.argv[1].replace(/\\/g, '/')}`).href
283
+ } catch {
284
+ return false
285
+ }
286
+ }
287
+
288
+ function runCli() {
289
+ if (command === 'feishu-setup') {
290
+ const { profile } = cliArgs(args)
291
+ setupFeishu({ profile: profile ?? 'web' })
292
+ .then(() => process.exit(0))
293
+ .catch((error) => {
294
+ console.error(`✗ ${error instanceof Error ? error.message : String(error)}`)
295
+ process.exit(1)
296
+ })
297
+ } else if (command === 'feishu-test') {
298
+ testFeishu()
299
+ .then(() => process.exit(0))
300
+ .catch((error) => {
301
+ console.error(`✗ ${error instanceof Error ? error.message : String(error)}`)
302
+ process.exit(1)
303
+ })
304
+ } else {
305
+ console.error(`用法:
306
+ dsh-hooks feishu-setup [--profile <name>] 扫码创建飞书通知机器人并自动配置
307
+ dsh-hooks feishu-test 验证配置并发送测试卡片`)
308
+ process.exit(command === '--help' || command === 'help' || command === undefined ? 0 : 1)
309
+ }
310
+ }
311
+
312
+ if (isDirectRun()) runCli()
@@ -0,0 +1,7 @@
1
+ # Mount the dsh-hooks plugin in any DSH profile that installs this package.
2
+ # `insert` adds this plugin's row; `name` resolves through the profile's
3
+ # node_modules. Hook definitions live in the profile's own cordis.patch.yml
4
+ # config block (see README.zh.md), not here.
5
+ - insert:
6
+ - id: dsh-hooks
7
+ name: dsh-hooks