dsh-hooks 0.3.0 → 0.5.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
@@ -4,17 +4,19 @@ Config-driven lifecycle hooks plugin for [DeepSeek Harness](https://github.com/d
4
4
 
5
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
6
 
7
- [中文文档](README.zh.md) | [Design](#design) | [Feishu example](examples/notify-feishu.mjs) | [Web GUI 面板](packages/dsh-hooks-ui/README.md)
7
+ [中文文档](README.zh.md) | [Design](#design) | [Feishu example](examples/notify-feishu.mjs) | [Web GUI](#web-gui)
8
8
 
9
9
  ## Install
10
10
 
11
+ One package ships everything (hook engine + Web GUI settings page):
12
+
11
13
  ```sh
12
14
  dsh plugin --profile web add dsh-hooks # from npm
13
15
  # or straight from git:
14
16
  dsh plugin --profile web add github:PeterBon/dsh-hooks
15
17
  ```
16
18
 
17
- Restart `dsh web`.
19
+ Restart `dsh web`. The settings panel gains a "Hooks" section (see [Web GUI](#web-gui)).
18
20
 
19
21
  ## Configure
20
22
 
@@ -141,7 +143,7 @@ The URL may also live in the dsh process environment as `DSH_HOOKS_WEBHOOK_URL`
141
143
 
142
144
  ## Execution history
143
145
 
144
- Every hook trigger is recorded into an in-memory ring buffer (default 500 entries) and best-effort appended to `~/.dsh/dsh-hooks/history.jsonl` (0600) — for future UIs and debugging. Records never contain secrets (env vars never enter records):
146
+ Every hook trigger is recorded into an in-memory ring buffer (default 500 entries) and best-effort appended to `~/.dsh/dsh-hooks/history.jsonl` (0600) — for future UIs and debugging. The ring buffer seeds from the JSONL at startup and live-syncs new appends on every web-panel read (including appends from other dsh processes sharing the file, e.g. a task-board Host), so history survives restarts. Records never contain secrets (env vars never enter records):
145
147
 
146
148
  ```yaml
147
149
  - id: dsh-hooks
@@ -172,6 +174,17 @@ dsh-hooks dry-run tool/call --tool ssh_exec --execute # end-to-end: actually r
172
174
 
173
175
  `dry-run` reads the profile's `cordis.patch.yml` (the `id: dsh-hooks` block) and validates the config (bad regexes fail here).
174
176
 
177
+ ## Web GUI
178
+
179
+ After install, the dsh web settings panel gains a "Hooks" section (beside General and Plugins):
180
+
181
+ - **Status badges**: plugin version, hook count, history count
182
+ - **Manual tester**: pick an event (14 kinds) + reason/tool; "Simulate" shows the per-hook match report, "Execute" really triggers the matching hooks
183
+ - **Feishu connect**: scan-to-connect inside the panel — the QR code renders inline (with expiry countdown and a cancel button); after the scan the app is created, credentials + hook config are written, and the connected summary offers a one-click test card, an inline truncation-length editor (50–5000 chars, default 300), and a re-connect flow
184
+ - **Execution-history timeline**: at the bottom of the card, **collapsed by default** (an "expand" toggle opens the latest 30 triggers: time / event / command / outcome / stderr tail), refreshed every 5s
185
+
186
+ CLI/headless environments are unaffected: the browser half loads only in the web GUI and the core has no UI runtime dependencies.
187
+
175
188
  ## Web profile HTTP routes
176
189
 
177
190
  In the web profile (when the shared webServer service exists) dsh-hooks registers loopback-only `/dsh-hooks/*` routes — CLI/headless environments never see them:
@@ -181,12 +194,28 @@ In the web profile (when the shared webServer service exists) dsh-hooks register
181
194
  | `/dsh-hooks/status` | GET | plugin version, hook count, history count |
182
195
  | `/dsh-hooks/history?n=50` | GET | the latest N execution records (JSON envelope) |
183
196
  | `/dsh-hooks/test` | POST | simulate an event: `{"event":"tool/call","tool":"ssh_exec","execute":false}` returns a per-hook match report; `execute: true` actually runs the matching hooks |
197
+ | `/dsh-hooks/feishu/status` | GET | Feishu connection summary (app id / target masked, secret never leaves the server) + the scan-session snapshot + the truncation length |
198
+ | `/dsh-hooks/feishu/setup` | POST | start a scan session: `{"profile":"web","resultMaxChars":800}`; returns the QR URL / PNG data URL / expiry (409 while one is pending) |
199
+ | `/dsh-hooks/feishu/cancel` | POST | cancel the pending scan session (aborts the registerApp wait) |
200
+ | `/dsh-hooks/feishu/config` | POST | update the card truncation length: `{"resultMaxChars":800}` (50–5000); effective immediately, credentials preserved |
201
+ | `/dsh-hooks/feishu/test` | POST | send a test card with the stored credentials |
184
202
 
185
203
  Security matches dsh-aionui-panel: loopback-only, POSTs require `application/json` (blocks cross-site form CSRF). The web profile also gets a systemPrompt section announcing the plugin to agents.
186
204
 
187
205
  ## Feishu notification example
188
206
 
189
- 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:
207
+ Two ways to connect the **Web GUI scan** (recommended, no terminal) or the one-shot setup CLI. Both create the Feishu app via a QR-code scan and write the same hook config.
208
+
209
+ ### Option 1: scan in the Web GUI
210
+
211
+ Open the dsh web settings → "Hooks" section → "Feishu connect", fill in the profile (default `web`) and press the connect button:
212
+
213
+ 1. The panel shows the Feishu authorization QR code inline (with expiry countdown)
214
+ 2. Scanning creates an app named 「DSH 通知机器人」 (only the `im:message:send_as_bot` permission); the scanning user becomes the notification target
215
+ 3. The connected summary offers "send test card", an inline truncation-length editor (50–5000 chars, default 300, effective immediately), and "re-connect" (swap the bound app)
216
+ 4. Restart `dsh web` for the hooks to take effect
217
+
218
+ ### Option 2: setup CLI
190
219
 
191
220
  ```sh
192
221
  dsh-hooks feishu-setup # default profile: web
@@ -194,11 +223,13 @@ dsh-hooks feishu-setup --profile work # another profile
194
223
  dsh-hooks feishu-test # send a test card with the stored credentials
195
224
  ```
196
225
 
197
- `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:
226
+ `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.
227
+
228
+ Both options write the same files:
198
229
 
199
230
  | File | Purpose |
200
231
  | --- | --- |
201
- | `~/.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) |
232
+ | `~/.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, editable in the Web GUI) |
202
233
  | `~/.dsh/dsh-hooks/notify-feishu.mjs` | stable copy of the notify script the hooks reference |
203
234
  | `~/.dsh/profiles/<profile>/cordis.patch.yml` | dsh-hooks block: `turn/end` (completed/error/aborted) + `approval/asked` + `agent/error` card hooks |
204
235
 
@@ -206,7 +237,7 @@ Restart `dsh web` afterwards — you will get cards when turns finish, approvals
206
237
 
207
238
  ![Feishu card example](assets/screenshot-1.jpg)
208
239
 
209
- ### Manual configuration
240
+ ### Option 3: manual configuration
210
241
 
211
242
  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:
212
243
 
package/README.zh.md CHANGED
@@ -8,13 +8,15 @@
8
8
 
9
9
  ## 安装
10
10
 
11
+ 一个包搞定全部(hook 引擎 + Web GUI 设置页):
12
+
11
13
  ```sh
12
14
  dsh plugin --profile web add dsh-hooks # 从 npm 安装
13
15
  # 或直接从 git 安装:
14
16
  dsh plugin --profile web add github:PeterBon/dsh-hooks
15
17
  ```
16
18
 
17
- 重启 `dsh web` 生效。
19
+ 重启 `dsh web` 生效。安装后设置面板里会出现「Hooks」分区(见 [Web GUI](#web-gui))。
18
20
 
19
21
  ## 配置
20
22
 
@@ -123,7 +125,7 @@ dsh plugin --profile web add github:PeterBon/dsh-hooks
123
125
 
124
126
  ## 执行历史
125
127
 
126
- 每次 hook 触发都会记入内存环形缓冲(默认 500 条),并 best-effort 追加到 `~/.dsh/dsh-hooks/history.jsonl`(权限 0600)——供未来 UI 与调试使用。记录不含 secret(环境变量从不入记录):
128
+ 每次 hook 触发都会记入内存环形缓冲(默认 500 条),并 best-effort 追加到 `~/.dsh/dsh-hooks/history.jsonl`(权限 0600)——供未来 UI 与调试使用。环形缓冲在启动时从 JSONL 回填,且 Web 面板每次读取时增量同步磁盘上新增的记录(包括其他 dsh 进程的追加,如任务看板 Host),因此重启后历史不会消失。记录不含 secret(环境变量从不入记录):
127
129
 
128
130
  ```yaml
129
131
  - id: dsh-hooks
@@ -154,6 +156,17 @@ dsh-hooks dry-run tool/call --tool ssh_exec --execute # 端到端真跑匹配
154
156
 
155
157
  `dry-run` 直接读 profile 的 `cordis.patch.yml`(`id: dsh-hooks` 配置块),配置校验(非法正则等)会在这一步报错。
156
158
 
159
+ ## Web GUI
160
+
161
+ 安装后,dsh web 的设置面板里会出现「Hooks」分区(与「通用」「插件」平级):
162
+
163
+ - **状态徽章**:插件版本、hook 数、历史条数
164
+ - **手动测试**:选事件(14 类)+ reason/tool,「模拟」看逐 hook 匹配报告,「执行」真实触发
165
+ - **飞书通知**:网页内扫码连接飞书——显示二维码(含有效期倒计时、可取消),扫码后自动创建应用、写入凭据与 hook 配置;已连接后显示应用摘要,可一键发送测试卡片、调整卡片截断长度(50–5000 字符,默认 300)或重新扫码换绑
166
+ - **执行历史时间线**:位于分区底部、**默认折叠**(标题旁「展开」查看最近 30 条触发:时间 / 事件 / 命令 / 结果 / stderr 尾部),5 秒自动刷新
167
+
168
+ CLI/headless 环境完全不受影响:浏览器半只在 web 加载,核心零 UI 运行时依赖。
169
+
157
170
  ## Web profile HTTP 路由
158
171
 
159
172
  web profile 里(存在共享 webServer 服务时)dsh-hooks 自动注册 loopback-only 的 `/dsh-hooks/*` 路由——CLI/headless 环境完全无感:
@@ -163,6 +176,11 @@ web profile 里(存在共享 webServer 服务时)dsh-hooks 自动注册 loop
163
176
  | `/dsh-hooks/status` | GET | 插件版本、hook 数、历史条数 |
164
177
  | `/dsh-hooks/history?n=50` | GET | 最近 N 条执行历史(JSON envelope) |
165
178
  | `/dsh-hooks/test` | POST | 模拟事件评估:`{"event":"tool/call","tool":"ssh_exec","execute":false}` 返回逐 hook 匹配报告;`execute: true` 真跑匹配的 hook |
179
+ | `/dsh-hooks/feishu/status` | GET | 飞书连接摘要(app id / 目标均已打码,绝不返回 secret)+ 扫码会话快照 + 截断长度 |
180
+ | `/dsh-hooks/feishu/setup` | POST | 启动扫码会话:`{"profile":"web","resultMaxChars":800}`,返回二维码 URL / PNG data URL / 有效期;进行中时再次请求返回 409 |
181
+ | `/dsh-hooks/feishu/cancel` | POST | 取消进行中的扫码会话(中止 registerApp 等待) |
182
+ | `/dsh-hooks/feishu/config` | POST | 更新卡片截断长度:`{"resultMaxChars":800}`(50–5000),即时生效,保留凭据 |
183
+ | `/dsh-hooks/feishu/test` | POST | 用已存凭据发送测试卡片 |
166
184
 
167
185
  安全约定与 dsh-aionui-panel 一致:仅回环地址可达、POST 必须 `application/json`(防跨站表单 CSRF)。同时 web profile 下会向 agent 注入一段 systemPrompt 公告,说明插件存在与协作方式。
168
186
 
@@ -186,7 +204,18 @@ URL 也可放在 dsh 进程环境的 `DSH_HOOKS_WEBHOOK_URL`(不要写进配
186
204
 
187
205
  ## 飞书通知示例
188
206
 
189
- 最快的方式是一步到位的 setup CLI——扫码自动创建飞书应用并写好全部 hook 配置:
207
+ 两种接入方式任选:**Web GUI 扫码**(推荐,无需终端)或 **setup CLI**——扫码自动创建飞书应用并写好全部 hook 配置。
208
+
209
+ ### 方式一:Web GUI 扫码
210
+
211
+ 打开 dsh web 设置 → 「Hooks」分区 → 「飞书通知」,填好 profile(默认 `web`)点「扫码连接飞书」:
212
+
213
+ 1. 面板内显示飞书授权二维码(含有效期倒计时)
214
+ 2. 用飞书扫码,自动创建名为「DSH 通知机器人」的应用(仅 `im:message:send_as_bot` 权限),扫码者本人为通知接收人
215
+ 3. 连接完成后显示应用摘要,可「发送测试卡片」验证、直接修改卡片截断长度(50–5000 字符,默认 300,即时生效),「重新连接」可换绑新应用
216
+ 4. 重启 `dsh web` 生效
217
+
218
+ ### 方式二:setup CLI
190
219
 
191
220
  ```sh
192
221
  dsh-hooks feishu-setup # 默认 profile:web
@@ -194,11 +223,13 @@ dsh-hooks feishu-setup --profile work # 指定其他 profile
194
223
  dsh-hooks feishu-test # 用已存凭据发送测试卡片验证
195
224
  ```
196
225
 
197
- `feishu-setup` 会打印二维码(并在浏览器中打开),等你用飞书扫码后,自动创建名为「DSH 通知机器人」的应用(带消息发送权限),并写入:
226
+ `feishu-setup` 会打印二维码(并在浏览器中打开),等你用飞书扫码后,自动创建名为「DSH 通知机器人」的应用(带消息发送权限)。
227
+
228
+ 两种方式写入的文件相同:
198
229
 
199
230
  | 文件 | 用途 |
200
231
  | --- | --- |
201
- | `~/.dsh/dsh-hooks/feishu-config.json` | app id/secret 与你的 open_id(通知目标),权限 0600,严禁提交;`result_max_chars` 控制卡片内容截断长度(默认 300 |
232
+ | `~/.dsh/dsh-hooks/feishu-config.json` | app id/secret 与你的 open_id(通知目标),权限 0600,严禁提交;`result_max_chars` 控制卡片内容截断长度(默认 300,可在 Web GUI 中修改) |
202
233
  | `~/.dsh/dsh-hooks/notify-feishu.mjs` | hook 引用的通知脚本稳定副本 |
203
234
  | `~/.dsh/profiles/<profile>/cordis.patch.yml` | dsh-hooks 配置块:`turn/end`(completed/error/aborted)+ `approval/asked` + `agent/error` 卡片 hook |
204
235
 
@@ -206,7 +237,7 @@ dsh-hooks feishu-test # 用已存凭据发送测试卡片验证
206
237
 
207
238
  ![飞书卡片示例](assets/screenshot-1.jpg)
208
239
 
209
- ### 手动配置
240
+ ### 方式三:手动配置
210
241
 
211
242
  想自己接线?见 [`examples/notify-feishu.mjs`](examples/notify-feishu.mjs)——零依赖脚本,通过飞书**应用 API**(不需要群自定义机器人)发送回合完成 / 审批通知。配置示例:
212
243
 
package/bin/dsh-hooks.mjs CHANGED
@@ -19,22 +19,36 @@
19
19
  * turn/end + approval/asked +
20
20
  * agent/error card hooks
21
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.
22
+ * Requires Node >= 22. The shared setup logic lives in lib/feishu.js (also
23
+ * used by the web GUI's /dsh-hooks/feishu routes); this CLI half only adds
24
+ * the terminal QR rendering, the browser opener, and the argument parsing.
25
25
  */
26
- import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'node:fs'
27
26
  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
27
  import QRCode from 'qrcode'
32
- import YAML from 'yaml'
33
- import { run as notifyRun } from '../examples/notify-feishu.mjs'
34
28
  import { runDryRun } from '../lib/dry-run.js'
35
-
36
- const CONFIG_DIR = join(homedir(), '.dsh', 'dsh-hooks')
37
- export const CONFIG_PATH = join(CONFIG_DIR, 'feishu-config.json')
29
+ import {
30
+ FEISHU_CONFIG_PATH,
31
+ mergePatchYaml,
32
+ notifyScriptPath,
33
+ patchPath,
34
+ runFeishuSetup,
35
+ runFeishuTest,
36
+ setupHooks,
37
+ stableScriptPath,
38
+ writeConfig,
39
+ } from '../lib/feishu.js'
40
+
41
+ /** Backward-compatible alias (feishu-notify parity). */
42
+ export const CONFIG_PATH = FEISHU_CONFIG_PATH
43
+
44
+ export {
45
+ mergePatchYaml,
46
+ notifyScriptPath,
47
+ patchPath,
48
+ setupHooks,
49
+ stableScriptPath,
50
+ writeConfig,
51
+ }
38
52
 
39
53
  /** Open a URL in the default browser (best-effort, never throws). */
40
54
  export function openInBrowser(url) {
@@ -54,127 +68,24 @@ export function openInBrowser(url) {
54
68
  })
55
69
  }
56
70
 
57
- /** Profile patch file for a profile name. */
58
- export function patchPath(profile) {
59
- return join(homedir(), '.dsh', 'profiles', profile, 'cordis.patch.yml')
60
- }
61
-
62
- /** Which hooks the setup installs into the profile. */
63
- export function setupHooks(scriptPath) {
64
- return [
65
- { on: 'turn/end', when: 'completed', run: `node ${JSON.stringify(scriptPath)}`, timeoutMs: 30000 },
66
- { on: 'turn/end', when: 'error', run: `node ${JSON.stringify(scriptPath)}`, timeoutMs: 30000 },
67
- { on: 'turn/end', when: 'aborted', run: `node ${JSON.stringify(scriptPath)}`, timeoutMs: 30000 },
68
- { on: 'approval/asked', run: `node ${JSON.stringify(scriptPath)} --approval`, timeoutMs: 30000 },
69
- { on: 'agent/error', run: `node ${JSON.stringify(scriptPath)}`, timeoutMs: 30000 },
70
- ]
71
- }
72
-
73
- /** Absolute path of the shipped notify script (where it lives now). */
74
- export function notifyScriptPath() {
75
- return new URL('../examples/notify-feishu.mjs', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1')
76
- }
77
-
78
- /**
79
- * Resolve the stable notify-script location hooks should reference.
80
- * The npx cache (where this CLI often runs from) is ephemeral, so the
81
- * setup copies the zero-dependency script next to feishu-config.json:
82
- * ~/.dsh/dsh-hooks/notify-feishu.mjs. Re-copies on every setup so the
83
- * stable copy tracks the installed CLI version.
84
- */
85
- export function stableScriptPath(paths = {}) {
86
- return paths.notifyScript ?? join(CONFIG_DIR, 'notify-feishu.mjs')
87
- }
88
-
89
- /**
90
- * Write the credential file with 0600 perms (owner-only), matching the
91
- * feishu-notify security posture: secrets stay out of the repo and argv.
92
- */
93
- export function writeConfig(configPath, { appId, appSecret, targetType = 'open_id', targetId, resultMaxChars = 300 }) {
94
- mkdirSync(join(configPath, '..'), { recursive: true, mode: 0o700 })
95
- const doc = JSON.stringify(
96
- {
97
- app_id: appId,
98
- app_secret: appSecret,
99
- target_type: targetType,
100
- target_id: targetId,
101
- result_max_chars: resultMaxChars,
102
- },
103
- null,
104
- 2,
105
- )
106
- writeFileSync(configPath, doc + '\n', 'utf8')
107
- try {
108
- chmodSync(configPath, 0o600)
109
- } catch {
110
- // Windows: ACL-based protection; the file lives under the user profile.
111
- }
112
- }
113
-
114
- /**
115
- * Merge the dsh-hooks config block into a profile's cordis.patch.yml:
116
- * existing dsh-hooks entries keep unrelated config and get their hooks
117
- * replaced with `setupHooks`; other entries stay untouched. Idempotent.
118
- */
119
- export function mergePatchYaml(existingText, { scriptPath }) {
120
- let entries
121
- try {
122
- entries = YAML.parse(existingText || '[]\n')
123
- } catch {
124
- throw new Error('profile 的 cordis.patch.yml 解析失败,请先修复该文件')
125
- }
126
- if (!Array.isArray(entries)) throw new Error('cordis.patch.yml 顶层必须是 YAML 数组')
127
-
128
- const hooks = setupHooks(scriptPath)
129
- let found = false
130
- for (const entry of entries) {
131
- if (entry && typeof entry === 'object' && entry.id === 'dsh-hooks') {
132
- entry.name = 'dsh-hooks'
133
- entry.config = { hooks }
134
- found = true
135
- break
136
- }
137
- }
138
- if (!found) entries.push({ id: 'dsh-hooks', name: 'dsh-hooks', config: { hooks } })
139
- return YAML.stringify(entries)
140
- }
141
-
142
71
  /**
143
- * Full setup flow. `deps` is injectable for tests:
144
- * registerAppFn the official registerApp (default)
145
- * print/printErr — output sinks
146
- * openUrl — browser opener (no-op by default in tests)
147
- * paths — { configPath, patchFile, notifyScript }
148
- * Returns the created app facts (without the secret in logs).
72
+ * CLI front for the shared setup flow: print the terminal QR and open the
73
+ * authorization URL in the default browser when the code is ready.
149
74
  */
150
75
  export async function setupFeishu({
151
76
  profile = 'web',
152
- registerAppFn = registerApp,
77
+ registerAppFn,
153
78
  print = console.log,
154
79
  printErr = console.error,
155
80
  openUrl = openInBrowser,
156
81
  paths = {},
157
82
  } = {}) {
158
- const configPath = paths.configPath ?? CONFIG_PATH
159
- const patchFile = paths.patchFile ?? patchPath(profile)
160
- const notifyScript = stableScriptPath(paths)
161
-
162
- print('dsh-hooks feishu-setup')
163
- print('1/4 正在生成飞书「一键创建应用」二维码…')
164
-
165
- const result = await registerAppFn({
166
- source: 'dsh-hooks',
167
- createOnly: true,
168
- appPreset: {
169
- name: 'DSH 通知机器人',
170
- desc: 'DeepSeek Harness 会话事件通知(dsh-hooks)',
171
- },
172
- addons: {
173
- preset: false,
174
- scopes: {
175
- tenant: ['im:message:send_as_bot'],
176
- },
177
- },
83
+ return runFeishuSetup({
84
+ profile,
85
+ print,
86
+ printErr,
87
+ paths,
88
+ ...(registerAppFn !== undefined ? { registerAppFn } : {}),
178
89
  onQRCodeReady: (authorization) => {
179
90
  print('')
180
91
  print(`请用飞书扫码(${authorization.expireIn} 秒内有效),或在浏览器打开:`)
@@ -190,82 +101,11 @@ export async function setupFeishu({
190
101
  void Promise.resolve(openUrl(authorization.url)).catch(() => undefined)
191
102
  },
192
103
  })
193
-
194
- const appId = result.client_id
195
- const appSecret = result.client_secret
196
- const ownerOpenId = result.user_info?.open_id
197
- if (!appId || !appSecret) throw new Error('扫码创建未完成,未拿到应用凭证')
198
- if (!ownerOpenId) throw new Error('扫码结果缺少 open_id,请重试')
199
-
200
- print('')
201
- print(`2/4 应用创建成功:${appId}(机器人将私聊通知你)`)
202
-
203
- writeConfig(configPath, {
204
- appId,
205
- appSecret,
206
- targetType: 'open_id',
207
- targetId: ownerOpenId,
208
- resultMaxChars: 300,
209
- })
210
- print(`3/4 凭据已写入 ${configPath}(权限 0600,勿提交到仓库)`)
211
-
212
- // Copy the notify script to its stable location so hooks never
213
- // reference the ephemeral npx cache.
214
- if (!paths.notifyScript) {
215
- mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 })
216
- writeFileSync(notifyScript, readFileSync(notifyScriptPath(), 'utf8'), 'utf8')
217
- }
218
-
219
- const existing = existsSync(patchFile) ? readFileSync(patchFile, 'utf8') : '[]\n'
220
- const merged = mergePatchYaml(existing, { scriptPath: notifyScript })
221
- writeFileSync(patchFile, merged, 'utf8')
222
- print(`4/4 hook 配置已写入 ${patchFile}`)
223
-
224
- print('发送欢迎卡片验证…')
225
- try {
226
- await notifyRun({
227
- appId,
228
- appSecret,
229
- to: ownerOpenId,
230
- event: 'agent/created',
231
- sessionId: 'dsh-hooks-setup',
232
- cwd: process.cwd(),
233
- timestamp: new Date().toISOString(),
234
- })
235
- print('✅ 欢迎卡片已发送。请重启 dsh web 使 hooks 生效。')
236
- } catch (error) {
237
- printErr(`⚠ 欢迎卡片发送失败(配置已就绪,可稍后用 feishu-test 重试):${error instanceof Error ? error.message : String(error)}`)
238
- }
239
-
240
- return { appId, ownerOpenId }
241
104
  }
242
105
 
243
- /** Test credentials and send a test card to the configured target. */
106
+ /** CLI front for the shared test-card flow. */
244
107
  export async function testFeishu({ print = console.log, paths = {} } = {}) {
245
- const configPath = paths.configPath ?? CONFIG_PATH
246
- if (!existsSync(configPath)) {
247
- throw new Error(`未找到配置文件 ${configPath},请先运行 feishu-setup`)
248
- }
249
- let file
250
- try {
251
- file = JSON.parse(readFileSync(configPath, 'utf8'))
252
- } catch {
253
- throw new Error(`配置文件 ${configPath} 解析失败,请重新运行 feishu-setup`)
254
- }
255
- if (!file.app_id || !file.app_secret || !file.target_id) {
256
- throw new Error('配置文件不完整,请重新运行 feishu-setup')
257
- }
258
- await notifyRun({
259
- appId: file.app_id,
260
- appSecret: file.app_secret,
261
- to: file.target_id,
262
- event: 'agent/status',
263
- status: 'connected',
264
- sessionId: 'feishu-test',
265
- cwd: process.cwd(),
266
- timestamp: new Date().toISOString(),
267
- })
268
- print('✅ 测试卡片已发送')
108
+ return runFeishuTest({ print, paths })
269
109
  }
270
110
 
271
111
  const [, , command, ...args] = process.argv
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Typed surface of the shipped zero-dependency notify script
3
+ * (`examples/notify-feishu.mjs`) for the TypeScript half. The `.mjs` module
4
+ * resolves this `.d.mts` as its declaration, so lib code can import `run`
5
+ * without allowJs. Only the `run` entry the setup flow uses is declared.
6
+ */
7
+
8
+ /** Loose hook-context-like input the notify script merges with the config file. */
9
+ export interface NotifyFeishuContext {
10
+ appId?: string
11
+ appSecret?: string
12
+ to?: string
13
+ event?: string
14
+ sessionId?: string
15
+ sessionName?: string
16
+ cwd?: string
17
+ turn?: number | string
18
+ reason?: string
19
+ tool?: string
20
+ status?: string
21
+ error?: string
22
+ content?: string
23
+ timestamp?: string
24
+ [key: string]: unknown
25
+ }
26
+
27
+ export declare function run(
28
+ ctx: NotifyFeishuContext,
29
+ args?: string[],
30
+ configPath?: string,
31
+ ): Promise<{ kind: 'card' | 'text'; card?: unknown; text?: string }>