dsh-feishu-notifier 0.1.0 → 0.1.2

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
@@ -43,7 +43,7 @@ pnpm dsh --profile web
43
43
  设置 → 飞书通知
44
44
  ```
45
45
 
46
- 填写飞书机器人 Webhook,点击“保存 Webhook”,然后点击“发送测试消息”验证连接。Webhook 不会包含在 npm 包中,每位用户都需要配置自己的地址。
46
+ 填写飞书机器人 Webhook,点击“保存 Webhook”,然后点击“发送测试消息”验证连接。配置会通过插件自己的 Host API 持久化到 DSH 的 `settings.yaml`,通常位于 `C:\Users\<用户名>\.dsh\settings.yaml`。真实 Webhook 不会包含在 npm 包中,每位用户都需要配置自己的地址。
47
47
 
48
48
  ## 从 GitHub 安装
49
49
 
package/lib/client.js CHANGED
@@ -8,22 +8,43 @@ window.__ModuleLoader__.load({
8
8
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
9
  let react_jsx_runtime = require("react/jsx-runtime");
10
10
  //#region src/client/FeishuSettingsSection.tsx
11
- function FeishuSettingsSection({ scope }) {
12
- const [snapshot, setSnapshot] = (0, react.useState)(scope.getSnapshot());
11
+ function FeishuSettingsSection({ controller }) {
12
+ const [view, setView] = (0, react.useState)({ status: "loading" });
13
13
  const [webhook, setWebhook] = (0, react.useState)("");
14
14
  const [busy, setBusy] = (0, react.useState)(false);
15
15
  const [status, setStatus] = (0, react.useState)("");
16
- (0, react.useEffect)(() => scope.subscribe(() => {
17
- setSnapshot(scope.getSnapshot());
18
- }), [scope]);
19
- const enabled = snapshot.value?.enabled ?? true;
20
- const saveEnabled = async (value) => {
16
+ (0, react.useEffect)(() => {
17
+ let active = true;
18
+ controller.load().then((config) => {
19
+ if (active) setView({
20
+ status: "ready",
21
+ config
22
+ });
23
+ }).catch((error) => {
24
+ if (active) setStatus(error instanceof Error ? error.message : String(error));
25
+ });
26
+ return () => {
27
+ active = false;
28
+ };
29
+ }, [controller]);
30
+ const config = view.status === "ready" ? view.config : {
31
+ enabled: true,
32
+ webhookConfigured: false
33
+ };
34
+ const save = async (patch, message) => {
35
+ setBusy(true);
21
36
  setStatus("正在保存…");
22
37
  try {
23
- await scope.set("enabled", value);
24
- setStatus(value ? "通知已开启" : "通知已关闭");
38
+ const next = await controller.update(patch);
39
+ setView({
40
+ status: "ready",
41
+ config: next
42
+ });
43
+ setStatus(message);
25
44
  } catch (error) {
26
45
  setStatus(error instanceof Error ? error.message : String(error));
46
+ } finally {
47
+ setBusy(false);
27
48
  }
28
49
  };
29
50
  const saveWebhook = async () => {
@@ -32,26 +53,14 @@ window.__ModuleLoader__.load({
32
53
  setStatus("请输入 Webhook 地址");
33
54
  return;
34
55
  }
35
- setBusy(true);
36
- setStatus("正在保存…");
37
- try {
38
- await scope.set("webhook", value);
39
- setWebhook("");
40
- setStatus("Webhook 已保存");
41
- } catch (error) {
42
- setStatus(error instanceof Error ? error.message : String(error));
43
- } finally {
44
- setBusy(false);
45
- }
56
+ await save({ webhook: value }, "Webhook 已保存到 DSH 设置文件");
57
+ setWebhook("");
46
58
  };
47
59
  const sendTest = async () => {
48
60
  setBusy(true);
49
61
  setStatus("正在发送测试消息…");
50
62
  try {
51
- const response = await fetch("/api/feishu-notifier/test", { method: "POST" });
52
- const result = await response.json();
53
- if (!response.ok || result.ok !== true) throw new Error(result.message ?? `HTTP ${String(response.status)}`);
54
- setStatus(result.message ?? "测试消息已发送");
63
+ setStatus(await controller.test());
55
64
  } catch (error) {
56
65
  setStatus(error instanceof Error ? error.message : String(error));
57
66
  } finally {
@@ -101,9 +110,10 @@ window.__ModuleLoader__.load({
101
110
  })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
102
111
  "aria-label": "启用通知",
103
112
  type: "checkbox",
104
- checked: enabled,
113
+ checked: config.enabled,
114
+ disabled: busy || view.status === "loading",
105
115
  onChange: (event) => {
106
- saveEnabled(event.target.checked);
116
+ save({ enabled: event.target.checked }, event.target.checked ? "通知已开启" : "通知已关闭");
107
117
  }
108
118
  })]
109
119
  }),
@@ -116,7 +126,8 @@ window.__ModuleLoader__.load({
116
126
  "aria-label": "Webhook 地址",
117
127
  type: "url",
118
128
  value: webhook,
119
- placeholder: "已保存的地址不会回显;输入新地址以替换",
129
+ placeholder: config.webhookConfigured ? "已配置地址;输入新地址以替换" : "请输入飞书机器人 Webhook",
130
+ disabled: busy || view.status === "loading",
120
131
  onChange: (event) => {
121
132
  setWebhook(event.target.value);
122
133
  setStatus("");
@@ -140,7 +151,7 @@ window.__ModuleLoader__.load({
140
151
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
141
152
  variant: "primary",
142
153
  size: "sm",
143
- disabled: busy || !snapshot.writable,
154
+ disabled: busy || view.status === "loading",
144
155
  onClick: () => {
145
156
  saveWebhook();
146
157
  },
@@ -148,7 +159,7 @@ window.__ModuleLoader__.load({
148
159
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
149
160
  variant: "outline",
150
161
  size: "sm",
151
- disabled: busy || !snapshot.writable,
162
+ disabled: busy || view.status === "loading" || !config.webhookConfigured,
152
163
  onClick: () => {
153
164
  sendTest();
154
165
  },
@@ -162,10 +173,6 @@ window.__ModuleLoader__.load({
162
173
  color: "var(--dsw-alias-label-secondary)"
163
174
  },
164
175
  children: status
165
- }),
166
- snapshot.status === "unavailable" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
167
- role: "alert",
168
- children: "当前连接不支持持久化设置。"
169
176
  })
170
177
  ]
171
178
  })]
@@ -173,15 +180,38 @@ window.__ModuleLoader__.load({
173
180
  }
174
181
  //#endregion
175
182
  //#region src/client/index.ts
176
- const inject = ["slots", "settingsScope"];
183
+ const inject = ["slots"];
177
184
  function apply(ctx) {
178
- const scope = ctx.settingsScope.bind({ namespace: "feishu-notifier" });
185
+ const controller = {
186
+ load: async () => {
187
+ const response = await fetch("/api/feishu-notifier/config");
188
+ const result = await response.json();
189
+ if (!response.ok || result.ok !== true || result.config === void 0) throw new Error(result.message ?? `HTTP ${String(response.status)}`);
190
+ return result.config;
191
+ },
192
+ update: async (patch) => {
193
+ const response = await fetch("/api/feishu-notifier/config", {
194
+ method: "POST",
195
+ headers: { "content-type": "application/json" },
196
+ body: JSON.stringify(patch)
197
+ });
198
+ const result = await response.json();
199
+ if (!response.ok || result.ok !== true || result.config === void 0) throw new Error(result.message ?? `HTTP ${String(response.status)}`);
200
+ return result.config;
201
+ },
202
+ test: async () => {
203
+ const response = await fetch("/api/feishu-notifier/test", { method: "POST" });
204
+ const result = await response.json();
205
+ if (!response.ok || result.ok !== true) throw new Error(result.message ?? `HTTP ${String(response.status)}`);
206
+ return result.message ?? "测试消息已发送";
207
+ }
208
+ };
179
209
  ctx.slots.inject("settings.section", () => ctx.slots.register({
180
210
  name: "settings.section",
181
211
  id: "feishu-notifier",
182
212
  order: 25,
183
213
  label: () => "飞书通知",
184
- inject: () => ({ scope })
214
+ inject: () => ({ controller })
185
215
  }, FeishuSettingsSection));
186
216
  }
187
217
  //#endregion
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["Button"],"sources":["../src/client/FeishuSettingsSection.tsx","../src/client/index.ts"],"sourcesContent":["import { useEffect, useState } from 'react'\nimport type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'\nimport type { SettingsScope } from '@deepseek-ai/dsh-client-runtime/client'\nimport { Button } from '@deepseek-ai/dsh-client-ui-primitives'\n\nexport interface FeishuSettings {\n enabled: boolean\n webhook?: string\n}\n\ninterface Injected {\n scope: SettingsScope<FeishuSettings>\n}\n\ntype Props = PropsRuntime<'settings.section'> & Injected\n\nexport function FeishuSettingsSection({ scope }: Props) {\n const [snapshot, setSnapshot] = useState(scope.getSnapshot())\n const [webhook, setWebhook] = useState('')\n const [busy, setBusy] = useState(false)\n const [status, setStatus] = useState('')\n\n useEffect(() => scope.subscribe(() => { setSnapshot(scope.getSnapshot()) }), [scope])\n\n const enabled = snapshot.value?.enabled ?? true\n const saveEnabled = async (value: boolean) => {\n setStatus('正在保存…')\n try {\n await scope.set('enabled', value)\n setStatus(value ? '通知已开启' : '通知已关闭')\n } catch (error) {\n setStatus(error instanceof Error ? error.message : String(error))\n }\n }\n\n const saveWebhook = async () => {\n const value = webhook.trim()\n if (value === '') {\n setStatus('请输入 Webhook 地址')\n return\n }\n setBusy(true)\n setStatus('正在保存…')\n try {\n await scope.set('webhook', value)\n setWebhook('')\n setStatus('Webhook 已保存')\n } catch (error) {\n setStatus(error instanceof Error ? error.message : String(error))\n } finally {\n setBusy(false)\n }\n }\n\n const sendTest = async () => {\n setBusy(true)\n setStatus('正在发送测试消息…')\n try {\n const response = await fetch('/api/feishu-notifier/test', { method: 'POST' })\n const result = await response.json() as { ok?: boolean; message?: string }\n if (!response.ok || result.ok !== true) throw new Error(result.message ?? `HTTP ${String(response.status)}`)\n setStatus(result.message ?? '测试消息已发送')\n } catch (error) {\n setStatus(error instanceof Error ? error.message : String(error))\n } finally {\n setBusy(false)\n }\n }\n\n return (\n <section style={{ maxWidth: 680, display: 'grid', gap: 24 }}>\n <div>\n <h2 style={{ margin: 0, fontSize: 24 }}>飞书通知</h2>\n <p style={{ color: 'var(--dsw-alias-label-secondary)', lineHeight: 1.6 }}>\n 当智能体等待你的批准、需要回答问题,或一轮对话结束时,向飞书机器人发送提醒。\n </p>\n </div>\n <div style={{ display: 'grid', gap: 16, padding: 20, border: '1px solid var(--dsw-alias-border-l1)', borderRadius: 16 }}>\n <label style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16 }}>\n <span>\n <strong style={{ display: 'block' }}>启用通知</strong>\n <small style={{ color: 'var(--dsw-alias-label-secondary)' }}>关闭后不会发送任何飞书消息。</small>\n </span>\n <input aria-label=\"启用通知\" type=\"checkbox\" checked={enabled} onChange={event => { void saveEnabled(event.target.checked) }} />\n </label>\n <label style={{ display: 'grid', gap: 8 }}>\n <span><strong>Webhook 地址</strong></span>\n <input\n aria-label=\"Webhook 地址\"\n type=\"url\"\n value={webhook}\n placeholder=\"已保存的地址不会回显;输入新地址以替换\"\n onChange={event => { setWebhook(event.target.value); setStatus('') }}\n style={{ minHeight: 40, padding: '0 12px', borderRadius: 10, border: '1px solid var(--dsw-alias-border-l1)', background: 'var(--dsw-alias-bg-layer-1)', color: 'inherit' }}\n />\n </label>\n <div style={{ display: 'flex', flexWrap: 'wrap', gap: 10 }}>\n <Button variant=\"primary\" size=\"sm\" disabled={busy || !snapshot.writable} onClick={() => { void saveWebhook() }}>保存 Webhook</Button>\n <Button variant=\"outline\" size=\"sm\" disabled={busy || !snapshot.writable} onClick={() => { void sendTest() }}>发送测试消息</Button>\n </div>\n {status !== '' && <p role=\"status\" style={{ margin: 0, color: 'var(--dsw-alias-label-secondary)' }}>{status}</p>}\n {snapshot.status === 'unavailable' && <p role=\"alert\">当前连接不支持持久化设置。</p>}\n </div>\n </section>\n )\n}\n","import type { ClientContext, SettingsScope } from '@deepseek-ai/dsh-client-runtime/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-settings/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-slots'\nimport { FeishuSettingsSection, type FeishuSettings } from './FeishuSettingsSection.tsx'\n\ninterface Injected {\n scope: SettingsScope<FeishuSettings>\n}\n\nexport const inject = ['slots', 'settingsScope']\n\nexport function apply(ctx: ClientContext): void {\n const scope = ctx.settingsScope.bind<FeishuSettings>({ namespace: 'feishu-notifier' })\n ctx.slots.inject('settings.section', () => ctx.slots.register({\n name: 'settings.section',\n id: 'feishu-notifier',\n order: 25,\n label: () => '飞书通知',\n inject: (): Injected => ({ scope }),\n }, FeishuSettingsSection))\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":["useState","Button"],"sources":["../src/client/FeishuSettingsSection.tsx","../src/client/index.ts"],"sourcesContent":["import { useEffect, useState } from 'react'\nimport type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'\nimport { Button } from '@deepseek-ai/dsh-client-ui-primitives'\n\nexport interface FeishuConfigView {\n enabled: boolean\n webhookConfigured: boolean\n}\n\nexport interface FeishuController {\n config?: FeishuConfigView\n load: () => Promise<FeishuConfigView>\n update: (patch: { enabled?: boolean; webhook?: string }) => Promise<FeishuConfigView>\n test: () => Promise<string>\n}\n\ntype Props = PropsRuntime<'settings.section'> & { controller: FeishuController }\n\ntype ViewState =\n | { status: 'loading' }\n | { status: 'ready'; config: FeishuConfigView }\n\nexport function FeishuSettingsSection({ controller }: Props) {\n const [view, setView] = useState<ViewState>({ status: 'loading' })\n const [webhook, setWebhook] = useState('')\n const [busy, setBusy] = useState(false)\n const [status, setStatus] = useState('')\n\n useEffect(() => {\n let active = true\n void controller.load()\n .then(config => {\n if (active) setView({ status: 'ready', config })\n })\n .catch(error => {\n if (active) setStatus(error instanceof Error ? error.message : String(error))\n })\n return () => { active = false }\n }, [controller])\n\n const config = view.status === 'ready' ? view.config : { enabled: true, webhookConfigured: false }\n\n const save = async (patch: { enabled?: boolean; webhook?: string }, message: string) => {\n setBusy(true)\n setStatus('正在保存…')\n try {\n const next = await controller.update(patch)\n setView({ status: 'ready', config: next })\n setStatus(message)\n } catch (error) {\n setStatus(error instanceof Error ? error.message : String(error))\n } finally {\n setBusy(false)\n }\n }\n\n const saveWebhook = async () => {\n const value = webhook.trim()\n if (value === '') {\n setStatus('请输入 Webhook 地址')\n return\n }\n await save({ webhook: value }, 'Webhook 已保存到 DSH 设置文件')\n setWebhook('')\n }\n\n const sendTest = async () => {\n setBusy(true)\n setStatus('正在发送测试消息…')\n try {\n setStatus(await controller.test())\n } catch (error) {\n setStatus(error instanceof Error ? error.message : String(error))\n } finally {\n setBusy(false)\n }\n }\n\n return (\n <section style={{ maxWidth: 680, display: 'grid', gap: 24 }}>\n <div>\n <h2 style={{ margin: 0, fontSize: 24 }}>飞书通知</h2>\n <p style={{ color: 'var(--dsw-alias-label-secondary)', lineHeight: 1.6 }}>\n 当智能体等待你的批准、需要回答问题,或一轮对话结束时,向飞书机器人发送提醒。\n </p>\n </div>\n <div style={{ display: 'grid', gap: 16, padding: 20, border: '1px solid var(--dsw-alias-border-l1)', borderRadius: 16 }}>\n <label style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16 }}>\n <span>\n <strong style={{ display: 'block' }}>启用通知</strong>\n <small style={{ color: 'var(--dsw-alias-label-secondary)' }}>关闭后不会发送任何飞书消息。</small>\n </span>\n <input\n aria-label=\"启用通知\"\n type=\"checkbox\"\n checked={config.enabled}\n disabled={busy || view.status === 'loading'}\n onChange={event => { void save({ enabled: event.target.checked }, event.target.checked ? '通知已开启' : '通知已关闭') }}\n />\n </label>\n <label style={{ display: 'grid', gap: 8 }}>\n <span><strong>Webhook 地址</strong></span>\n <input\n aria-label=\"Webhook 地址\"\n type=\"url\"\n value={webhook}\n placeholder={config.webhookConfigured ? '已配置地址;输入新地址以替换' : '请输入飞书机器人 Webhook'}\n disabled={busy || view.status === 'loading'}\n onChange={event => { setWebhook(event.target.value); setStatus('') }}\n style={{ minHeight: 40, padding: '0 12px', borderRadius: 10, border: '1px solid var(--dsw-alias-border-l1)', background: 'var(--dsw-alias-bg-layer-1)', color: 'inherit' }}\n />\n </label>\n <div style={{ display: 'flex', flexWrap: 'wrap', gap: 10 }}>\n <Button variant=\"primary\" size=\"sm\" disabled={busy || view.status === 'loading'} onClick={() => { void saveWebhook() }}>保存 Webhook</Button>\n <Button variant=\"outline\" size=\"sm\" disabled={busy || view.status === 'loading' || !config.webhookConfigured} onClick={() => { void sendTest() }}>发送测试消息</Button>\n </div>\n {status !== '' && <p role=\"status\" style={{ margin: 0, color: 'var(--dsw-alias-label-secondary)' }}>{status}</p>}\n </div>\n </section>\n )\n}\n","import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-settings/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-slots'\nimport { FeishuSettingsSection, type FeishuController } from './FeishuSettingsSection.tsx'\n\nexport const inject = ['slots']\n\nexport function apply(ctx: ClientContext): void {\n const controller: FeishuController = {\n load: async () => {\n const response = await fetch('/api/feishu-notifier/config')\n const result = await response.json() as { ok?: boolean; message?: string; config?: FeishuController['config'] }\n if (!response.ok || result.ok !== true || result.config === undefined) {\n throw new Error(result.message ?? `HTTP ${String(response.status)}`)\n }\n return result.config\n },\n update: async (patch) => {\n const response = await fetch('/api/feishu-notifier/config', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(patch),\n })\n const result = await response.json() as { ok?: boolean; message?: string; config?: FeishuController['config'] }\n if (!response.ok || result.ok !== true || result.config === undefined) {\n throw new Error(result.message ?? `HTTP ${String(response.status)}`)\n }\n return result.config\n },\n test: async () => {\n const response = await fetch('/api/feishu-notifier/test', { method: 'POST' })\n const result = await response.json() as { ok?: boolean; message?: string }\n if (!response.ok || result.ok !== true) throw new Error(result.message ?? `HTTP ${String(response.status)}`)\n return result.message ?? '测试消息已发送'\n },\n }\n\n ctx.slots.inject('settings.section', () => ctx.slots.register({\n name: 'settings.section',\n id: 'feishu-notifier',\n order: 25,\n label: () => '飞书通知',\n inject: (): { controller: FeishuController } => ({ controller }),\n }, FeishuSettingsSection))\n}\n"],"mappings":"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"}
package/lib/index.js CHANGED
@@ -1,12 +1,14 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
- import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
2
+ import { settingsNamespace } from "@deepseek-ai/dsh-settings";
3
3
  //#region src/index.ts
4
4
  const name = "dsh-feishu-notifier";
5
+ const inject = ["settings", "webServer"];
5
6
  const Config = z.object({
6
7
  enabled: z.boolean().default(true),
7
8
  webhook: z.string().role("secret").default("")
8
9
  });
9
10
  const SETTINGS_NAMESPACE = settingsNamespace("feishu-notifier");
11
+ const CONFIG_PATH = "/api/feishu-notifier/config";
10
12
  const TEST_PATH = "/api/feishu-notifier/test";
11
13
  function textFor(kind, detail) {
12
14
  return kind === "approval" ? `DeepSeek Harness 需要你的操作\n${detail}` : `DeepSeek Harness 对话已结束\n${detail}`;
@@ -34,10 +36,35 @@ function turnReasonText(reason) {
34
36
  default: return typeof reason === "string" ? reason : "结束原因未知";
35
37
  }
36
38
  }
37
- async function sendText(current, text) {
38
- const config = current();
39
- const webhook = config.webhook.trim();
40
- if (!config.enabled || webhook === "") return;
39
+ function configView(config) {
40
+ return {
41
+ enabled: config.enabled,
42
+ webhookConfigured: config.webhook.trim() !== ""
43
+ };
44
+ }
45
+ function writeJson(res, status, value) {
46
+ const body = JSON.stringify(value);
47
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
48
+ res.end(body);
49
+ }
50
+ async function readJson(req) {
51
+ const chunks = [];
52
+ for await (const chunk of req) chunks.push(Buffer.from(chunk));
53
+ if (chunks.length === 0) return {};
54
+ try {
55
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
56
+ } catch {
57
+ throw new Error("请求体不是有效 JSON");
58
+ }
59
+ }
60
+ function webhookOf(value) {
61
+ if (typeof value !== "string" || value.trim() === "") throw new Error("请提供 Webhook 地址");
62
+ const webhook = value.trim();
63
+ if (new URL(webhook).protocol !== "https:") throw new Error("Webhook 必须使用 HTTPS 地址");
64
+ return webhook;
65
+ }
66
+ async function sendText(config, text) {
67
+ const webhook = webhookOf(config.webhook);
41
68
  const response = await fetch(webhook, {
42
69
  method: "POST",
43
70
  headers: { "content-type": "application/json" },
@@ -57,19 +84,57 @@ async function sendText(current, text) {
57
84
  throw error;
58
85
  }
59
86
  }
60
- function writeJson(res, status, value) {
61
- const body = JSON.stringify(value);
62
- res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
63
- res.end(body);
87
+ async function handleConfig(req, res, scope) {
88
+ if (req.method === "GET") {
89
+ writeJson(res, 200, {
90
+ ok: true,
91
+ config: configView(scope.get())
92
+ });
93
+ return;
94
+ }
95
+ if (req.method !== "POST") {
96
+ res.writeHead(405, { allow: "GET, POST" });
97
+ res.end();
98
+ return;
99
+ }
100
+ try {
101
+ const body = recordOf(await readJson(req));
102
+ if (body === void 0) throw new Error("请求体必须是 JSON 对象");
103
+ const patch = {};
104
+ if (body.enabled !== void 0) {
105
+ if (typeof body.enabled !== "boolean") throw new Error("enabled 必须是布尔值");
106
+ patch.enabled = body.enabled;
107
+ }
108
+ if (body.webhook !== void 0) patch.webhook = webhookOf(body.webhook);
109
+ if (Object.keys(patch).length === 0) throw new Error("没有可保存的配置");
110
+ await scope.update(patch);
111
+ writeJson(res, 200, {
112
+ ok: true,
113
+ config: configView(scope.get())
114
+ });
115
+ } catch (error) {
116
+ writeJson(res, 400, {
117
+ ok: false,
118
+ message: error instanceof Error ? error.message : String(error)
119
+ });
120
+ }
64
121
  }
65
- async function handleTest(req, res, current) {
122
+ async function handleTest(req, res, scope) {
66
123
  if (req.method !== "POST") {
67
124
  res.writeHead(405, { allow: "POST" });
68
125
  res.end();
69
126
  return;
70
127
  }
128
+ const config = scope.get();
129
+ if (!config.enabled) {
130
+ writeJson(res, 409, {
131
+ ok: false,
132
+ message: "飞书通知当前已关闭"
133
+ });
134
+ return;
135
+ }
71
136
  try {
72
- await sendText(current, "这是一条来自 DeepSeek Harness 的飞书通知测试消息。");
137
+ await sendText(config, "这是一条来自 DeepSeek Harness 的飞书通知测试消息。");
73
138
  writeJson(res, 200, {
74
139
  ok: true,
75
140
  message: "测试消息已发送"
@@ -82,17 +147,17 @@ async function handleTest(req, res, current) {
82
147
  }
83
148
  }
84
149
  function notify(current, kind, detail) {
85
- sendText(current, textFor(kind, detail)).catch((error) => {
150
+ const config = current();
151
+ if (!config.enabled || config.webhook.trim() === "") return;
152
+ sendText(config, textFor(kind, detail)).catch((error) => {
86
153
  console.warn(`[feishu-notifier] notification failed: ${String(error)}`);
87
154
  });
88
155
  }
89
156
  function apply(ctx, config) {
90
- let current = () => config;
91
- installSettingsSection(ctx, SETTINGS_NAMESPACE, Config, config, {
92
- setSource: (source) => {
93
- current = source;
94
- },
95
- onChange: () => {}
157
+ const scope = ctx.settings.register(SETTINGS_NAMESPACE, Config, { base: config });
158
+ let current = () => scope.get();
159
+ scope.watch(() => {
160
+ current = () => scope.get();
96
161
  });
97
162
  ctx.on("approval/request", (request, next) => {
98
163
  notify(current, "approval", request.reason ?? `工具 ${request.toolName} 正在等待批准。`);
@@ -102,13 +167,16 @@ function apply(ctx, config) {
102
167
  if (event.type === "turn/end") notify(current, "turn-end", `第 ${String(event.data.turn)} 轮:${turnReasonText(event.data.reason)}`);
103
168
  if (event.type === "tool/call" && (event.data.name === "ask_user_question" || event.data.name === "exit_plan_mode")) notify(current, "approval", event.data.name === "exit_plan_mode" ? "智能体正在等待你确认计划。" : "智能体正在等待你回答问题。");
104
169
  });
105
- ctx.inject(["webServer"], (webCtx) => {
106
- webCtx.effect(() => webCtx.webServer.register({
107
- kind: "exact",
108
- path: TEST_PATH,
109
- handler: (req, res) => handleTest(req, res, current)
110
- }), "feishu-notifier: test route");
111
- });
170
+ ctx.effect(() => ctx.webServer.register({
171
+ kind: "exact",
172
+ path: CONFIG_PATH,
173
+ handler: (req, res) => handleConfig(req, res, scope)
174
+ }), "feishu-notifier: config route");
175
+ ctx.effect(() => ctx.webServer.register({
176
+ kind: "exact",
177
+ path: TEST_PATH,
178
+ handler: (req, res) => handleTest(req, res, scope)
179
+ }), "feishu-notifier: test route");
112
180
  }
113
181
  //#endregion
114
- export { Config, apply, name, turnReasonText };
182
+ export { Config, apply, inject, name, turnReasonText };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-feishu-notifier",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Send DeepSeek Harness interaction and turn notifications to a Feishu bot",
5
5
  "license": "MIT",
6
6
  "type": "module",