dsh-feishu-notifier 0.1.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/LICENSE +21 -0
- package/README.md +67 -0
- package/cordis.yml +5 -0
- package/lib/client.js +194 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +114 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-feishu-notifier contributors
|
|
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,67 @@
|
|
|
1
|
+
# dsh-feishu-notifier
|
|
2
|
+
|
|
3
|
+
一个 DeepSeek Harness Bundle:当智能体请求用户批准、等待用户回答问题或确认计划,以及一轮对话结束时,向飞书机器人发送文本通知。
|
|
4
|
+
|
|
5
|
+
## 使用要求
|
|
6
|
+
|
|
7
|
+
- DeepSeek Harness `0.1.0-rc.5` 或兼容版本。
|
|
8
|
+
- Node.js `22.19+`。
|
|
9
|
+
- 在 Web 设置中配置飞书自定义机器人 Webhook。
|
|
10
|
+
|
|
11
|
+
## 本地开发
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
pnpm install
|
|
15
|
+
pnpm run build
|
|
16
|
+
pnpm pack:check
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
构建会生成:
|
|
20
|
+
|
|
21
|
+
- `lib/index.js`:Node/Cordis Loader 入口;
|
|
22
|
+
- `lib/client.js`:Web 浏览器插件入口。
|
|
23
|
+
|
|
24
|
+
## 从 npm 安装
|
|
25
|
+
|
|
26
|
+
将插件发布到 npm 后,用户可以直接使用以下命令安装到 `web` Profile:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
dsh plugin --profile web add dsh-feishu-notifier
|
|
30
|
+
dsh --profile web
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
如果使用源码方式运行 DeepSeek Harness,则将 `dsh` 替换为 `pnpm dsh`:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
pnpm dsh plugin --profile web add dsh-feishu-notifier
|
|
37
|
+
pnpm dsh --profile web
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
启动后打开:
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
设置 → 飞书通知
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
填写飞书机器人 Webhook,点击“保存 Webhook”,然后点击“发送测试消息”验证连接。Webhook 不会包含在 npm 包中,每位用户都需要配置自己的地址。
|
|
47
|
+
|
|
48
|
+
## 从 GitHub 安装
|
|
49
|
+
|
|
50
|
+
建议使用 release tag 或 commit SHA,确保安装内容可复现:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
pnpm dsh plugin --profile demo add github:blooming-fang/dsh-feishu-notifier#v0.1.0
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
从 GitHub 安装时会执行 `prepare` 构建插件。只应安装可信仓库中的代码;pnpm 可能要求在 Profile 的 workspace 配置中加入 `allowBuilds` 授权。
|
|
57
|
+
|
|
58
|
+
## 通知事件
|
|
59
|
+
|
|
60
|
+
插件会发送以下通知:
|
|
61
|
+
|
|
62
|
+
- 用户审批请求;
|
|
63
|
+
- `ask_user_question` 用户问题请求;
|
|
64
|
+
- `exit_plan_mode` 计划确认请求;
|
|
65
|
+
- `turn/end` 对话轮次结束事件,并将完成、错误、取消和中断原因转换为可读文本。
|
|
66
|
+
|
|
67
|
+
Webhook 属于敏感设置,保存后不会返回给浏览器。不要把真实 Webhook 地址提交到 GitHub 或发布到 npm。
|
package/cordis.yml
ADDED
package/lib/client.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-feishu-notifier",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
9
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
10
|
+
//#region src/client/FeishuSettingsSection.tsx
|
|
11
|
+
function FeishuSettingsSection({ scope }) {
|
|
12
|
+
const [snapshot, setSnapshot] = (0, react.useState)(scope.getSnapshot());
|
|
13
|
+
const [webhook, setWebhook] = (0, react.useState)("");
|
|
14
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
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) => {
|
|
21
|
+
setStatus("正在保存…");
|
|
22
|
+
try {
|
|
23
|
+
await scope.set("enabled", value);
|
|
24
|
+
setStatus(value ? "通知已开启" : "通知已关闭");
|
|
25
|
+
} catch (error) {
|
|
26
|
+
setStatus(error instanceof Error ? error.message : String(error));
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const saveWebhook = async () => {
|
|
30
|
+
const value = webhook.trim();
|
|
31
|
+
if (value === "") {
|
|
32
|
+
setStatus("请输入 Webhook 地址");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
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
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const sendTest = async () => {
|
|
48
|
+
setBusy(true);
|
|
49
|
+
setStatus("正在发送测试消息…");
|
|
50
|
+
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 ?? "测试消息已发送");
|
|
55
|
+
} catch (error) {
|
|
56
|
+
setStatus(error instanceof Error ? error.message : String(error));
|
|
57
|
+
} finally {
|
|
58
|
+
setBusy(false);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
62
|
+
style: {
|
|
63
|
+
maxWidth: 680,
|
|
64
|
+
display: "grid",
|
|
65
|
+
gap: 24
|
|
66
|
+
},
|
|
67
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
68
|
+
style: {
|
|
69
|
+
margin: 0,
|
|
70
|
+
fontSize: 24
|
|
71
|
+
},
|
|
72
|
+
children: "飞书通知"
|
|
73
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
74
|
+
style: {
|
|
75
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
76
|
+
lineHeight: 1.6
|
|
77
|
+
},
|
|
78
|
+
children: "当智能体等待你的批准、需要回答问题,或一轮对话结束时,向飞书机器人发送提醒。"
|
|
79
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
80
|
+
style: {
|
|
81
|
+
display: "grid",
|
|
82
|
+
gap: 16,
|
|
83
|
+
padding: 20,
|
|
84
|
+
border: "1px solid var(--dsw-alias-border-l1)",
|
|
85
|
+
borderRadius: 16
|
|
86
|
+
},
|
|
87
|
+
children: [
|
|
88
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
89
|
+
style: {
|
|
90
|
+
display: "flex",
|
|
91
|
+
alignItems: "center",
|
|
92
|
+
justifyContent: "space-between",
|
|
93
|
+
gap: 16
|
|
94
|
+
},
|
|
95
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
|
|
96
|
+
style: { display: "block" },
|
|
97
|
+
children: "启用通知"
|
|
98
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", {
|
|
99
|
+
style: { color: "var(--dsw-alias-label-secondary)" },
|
|
100
|
+
children: "关闭后不会发送任何飞书消息。"
|
|
101
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
102
|
+
"aria-label": "启用通知",
|
|
103
|
+
type: "checkbox",
|
|
104
|
+
checked: enabled,
|
|
105
|
+
onChange: (event) => {
|
|
106
|
+
saveEnabled(event.target.checked);
|
|
107
|
+
}
|
|
108
|
+
})]
|
|
109
|
+
}),
|
|
110
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
111
|
+
style: {
|
|
112
|
+
display: "grid",
|
|
113
|
+
gap: 8
|
|
114
|
+
},
|
|
115
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "Webhook 地址" }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
116
|
+
"aria-label": "Webhook 地址",
|
|
117
|
+
type: "url",
|
|
118
|
+
value: webhook,
|
|
119
|
+
placeholder: "已保存的地址不会回显;输入新地址以替换",
|
|
120
|
+
onChange: (event) => {
|
|
121
|
+
setWebhook(event.target.value);
|
|
122
|
+
setStatus("");
|
|
123
|
+
},
|
|
124
|
+
style: {
|
|
125
|
+
minHeight: 40,
|
|
126
|
+
padding: "0 12px",
|
|
127
|
+
borderRadius: 10,
|
|
128
|
+
border: "1px solid var(--dsw-alias-border-l1)",
|
|
129
|
+
background: "var(--dsw-alias-bg-layer-1)",
|
|
130
|
+
color: "inherit"
|
|
131
|
+
}
|
|
132
|
+
})]
|
|
133
|
+
}),
|
|
134
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
135
|
+
style: {
|
|
136
|
+
display: "flex",
|
|
137
|
+
flexWrap: "wrap",
|
|
138
|
+
gap: 10
|
|
139
|
+
},
|
|
140
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
141
|
+
variant: "primary",
|
|
142
|
+
size: "sm",
|
|
143
|
+
disabled: busy || !snapshot.writable,
|
|
144
|
+
onClick: () => {
|
|
145
|
+
saveWebhook();
|
|
146
|
+
},
|
|
147
|
+
children: "保存 Webhook"
|
|
148
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
149
|
+
variant: "outline",
|
|
150
|
+
size: "sm",
|
|
151
|
+
disabled: busy || !snapshot.writable,
|
|
152
|
+
onClick: () => {
|
|
153
|
+
sendTest();
|
|
154
|
+
},
|
|
155
|
+
children: "发送测试消息"
|
|
156
|
+
})]
|
|
157
|
+
}),
|
|
158
|
+
status !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
159
|
+
role: "status",
|
|
160
|
+
style: {
|
|
161
|
+
margin: 0,
|
|
162
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
163
|
+
},
|
|
164
|
+
children: status
|
|
165
|
+
}),
|
|
166
|
+
snapshot.status === "unavailable" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
167
|
+
role: "alert",
|
|
168
|
+
children: "当前连接不支持持久化设置。"
|
|
169
|
+
})
|
|
170
|
+
]
|
|
171
|
+
})]
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
//#endregion
|
|
175
|
+
//#region src/client/index.ts
|
|
176
|
+
const inject = ["slots", "settingsScope"];
|
|
177
|
+
function apply(ctx) {
|
|
178
|
+
const scope = ctx.settingsScope.bind({ namespace: "feishu-notifier" });
|
|
179
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
180
|
+
name: "settings.section",
|
|
181
|
+
id: "feishu-notifier",
|
|
182
|
+
order: 25,
|
|
183
|
+
label: () => "飞书通知",
|
|
184
|
+
inject: () => ({ scope })
|
|
185
|
+
}, FeishuSettingsSection));
|
|
186
|
+
}
|
|
187
|
+
//#endregion
|
|
188
|
+
exports.apply = apply;
|
|
189
|
+
exports.inject = inject;
|
|
190
|
+
return module.exports;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +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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"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
3
|
+
//#region src/index.ts
|
|
4
|
+
const name = "dsh-feishu-notifier";
|
|
5
|
+
const Config = z.object({
|
|
6
|
+
enabled: z.boolean().default(true),
|
|
7
|
+
webhook: z.string().role("secret").default("")
|
|
8
|
+
});
|
|
9
|
+
const SETTINGS_NAMESPACE = settingsNamespace("feishu-notifier");
|
|
10
|
+
const TEST_PATH = "/api/feishu-notifier/test";
|
|
11
|
+
function textFor(kind, detail) {
|
|
12
|
+
return kind === "approval" ? `DeepSeek Harness 需要你的操作\n${detail}` : `DeepSeek Harness 对话已结束\n${detail}`;
|
|
13
|
+
}
|
|
14
|
+
function recordOf(value) {
|
|
15
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
16
|
+
}
|
|
17
|
+
function turnReasonText(reason) {
|
|
18
|
+
const value = recordOf(reason);
|
|
19
|
+
switch (typeof value?.kind === "string" ? value.kind : void 0) {
|
|
20
|
+
case "completed": return "正常完成";
|
|
21
|
+
case "blocked": return "被策略阻止";
|
|
22
|
+
case "max-tokens": return "达到最大 token 限制";
|
|
23
|
+
case "interrupted": return "从中断状态恢复时结束";
|
|
24
|
+
case "error": {
|
|
25
|
+
const error = recordOf(value.error);
|
|
26
|
+
return typeof error?.message === "string" ? `发生错误:${error.message}` : "发生错误";
|
|
27
|
+
}
|
|
28
|
+
case "aborted": {
|
|
29
|
+
const cause = recordOf(value.reason);
|
|
30
|
+
if (typeof cause?.reason === "string") return `已中止:${cause.reason}`;
|
|
31
|
+
if (typeof cause?.kind === "string") return `已中止(${cause.kind})`;
|
|
32
|
+
return "已中止";
|
|
33
|
+
}
|
|
34
|
+
default: return typeof reason === "string" ? reason : "结束原因未知";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function sendText(current, text) {
|
|
38
|
+
const config = current();
|
|
39
|
+
const webhook = config.webhook.trim();
|
|
40
|
+
if (!config.enabled || webhook === "") return;
|
|
41
|
+
const response = await fetch(webhook, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers: { "content-type": "application/json" },
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
msg_type: "text",
|
|
46
|
+
content: { text }
|
|
47
|
+
}),
|
|
48
|
+
redirect: "error"
|
|
49
|
+
});
|
|
50
|
+
const body = await response.text();
|
|
51
|
+
if (!response.ok) throw new Error(`Feishu webhook returned HTTP ${String(response.status)}: ${body.slice(0, 200)}`);
|
|
52
|
+
try {
|
|
53
|
+
const result = JSON.parse(body);
|
|
54
|
+
if (result.code !== void 0 && result.code !== 0) throw new Error(`Feishu webhook rejected the message: ${result.msg ?? `code ${String(result.code)}`}`);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (error instanceof SyntaxError) return;
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
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);
|
|
64
|
+
}
|
|
65
|
+
async function handleTest(req, res, current) {
|
|
66
|
+
if (req.method !== "POST") {
|
|
67
|
+
res.writeHead(405, { allow: "POST" });
|
|
68
|
+
res.end();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
await sendText(current, "这是一条来自 DeepSeek Harness 的飞书通知测试消息。");
|
|
73
|
+
writeJson(res, 200, {
|
|
74
|
+
ok: true,
|
|
75
|
+
message: "测试消息已发送"
|
|
76
|
+
});
|
|
77
|
+
} catch (error) {
|
|
78
|
+
writeJson(res, 502, {
|
|
79
|
+
ok: false,
|
|
80
|
+
message: error instanceof Error ? error.message : String(error)
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function notify(current, kind, detail) {
|
|
85
|
+
sendText(current, textFor(kind, detail)).catch((error) => {
|
|
86
|
+
console.warn(`[feishu-notifier] notification failed: ${String(error)}`);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function apply(ctx, config) {
|
|
90
|
+
let current = () => config;
|
|
91
|
+
installSettingsSection(ctx, SETTINGS_NAMESPACE, Config, config, {
|
|
92
|
+
setSource: (source) => {
|
|
93
|
+
current = source;
|
|
94
|
+
},
|
|
95
|
+
onChange: () => {}
|
|
96
|
+
});
|
|
97
|
+
ctx.on("approval/request", (request, next) => {
|
|
98
|
+
notify(current, "approval", request.reason ?? `工具 ${request.toolName} 正在等待批准。`);
|
|
99
|
+
return next();
|
|
100
|
+
});
|
|
101
|
+
ctx.on("session/event", (_session, event) => {
|
|
102
|
+
if (event.type === "turn/end") notify(current, "turn-end", `第 ${String(event.data.turn)} 轮:${turnReasonText(event.data.reason)}`);
|
|
103
|
+
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
|
+
});
|
|
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
|
+
});
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
export { Config, apply, name, turnReasonText };
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-feishu-notifier",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Send DeepSeek Harness interaction and turn notifications to a Feishu bot",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"packageManager": "pnpm@11.7.0",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=22.19.0"
|
|
10
|
+
},
|
|
11
|
+
"main": "lib/index.js",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./lib/index.js",
|
|
14
|
+
"./client": "./lib/client.js",
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"lib",
|
|
19
|
+
"cordis.yml",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"package.json"
|
|
23
|
+
],
|
|
24
|
+
"dsh": {
|
|
25
|
+
"bundle": {
|
|
26
|
+
"patch": "./cordis.yml"
|
|
27
|
+
},
|
|
28
|
+
"client": {
|
|
29
|
+
"platform": "web",
|
|
30
|
+
"inject": [
|
|
31
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
32
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsdown --config tsdown.config.ts",
|
|
38
|
+
"prepare": "pnpm run build",
|
|
39
|
+
"prepublishOnly": "pnpm run build",
|
|
40
|
+
"pack:check": "pnpm pack --dry-run"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
44
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.5",
|
|
45
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
49
|
+
"@deepseek-ai/dsh-agent": "^0.1.0-rc.5",
|
|
50
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.5",
|
|
51
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.5",
|
|
52
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.5",
|
|
53
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.5",
|
|
54
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.5",
|
|
55
|
+
"@deepseek-ai/dsh-session": "^0.1.0-rc.5",
|
|
56
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.5",
|
|
57
|
+
"@deepseek-ai/dsh-user-approval": "^0.1.0-rc.5",
|
|
58
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
59
|
+
"@types/react": "^18.3.1",
|
|
60
|
+
"react": "^18.2.0",
|
|
61
|
+
"tsdown": "^0.22.2",
|
|
62
|
+
"typescript": "^6.0.3"
|
|
63
|
+
}
|
|
64
|
+
}
|