@mzzsfy/dsh-turn-notify 0.8.1 → 0.9.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 +3 -3
- package/package.json +1 -1
- package/src/client.js +14 -14
- package/src/core.mjs +3 -3
- package/test/core.test.mjs +6 -5
- package/test/route.test.mjs +25 -7
package/README.md
CHANGED
|
@@ -78,7 +78,7 @@ turn-notify:
|
|
|
78
78
|
|
|
79
79
|
## webhook 推送
|
|
80
80
|
|
|
81
|
-
在面板填入 webhook URL 即启用。host 直发,标签页全关也送达;payload 为 Slack 兼容 JSON(text 字段承载通知文本,summary 字段承载通知小结),另附 event/category/status/session/workspace/durationMs/tokens/ts 结构化字段;超时 10
|
|
81
|
+
在面板填入 webhook URL 即启用。host 直发,标签页全关也送达;payload 为 Slack 兼容 JSON(text 字段承载通知文本,summary 字段承载通知小结),另附 event/category/status/session/workspace/durationMs/tokens/ts 结构化字段;超时 10 秒,不重试。URL 原文随配置响应回显、保存即提交输入框内容(清空并保存即禁用),测试按钮返回真实投递结果。
|
|
82
82
|
|
|
83
83
|
## IM 推送
|
|
84
84
|
|
|
@@ -106,7 +106,7 @@ turn-notify:
|
|
|
106
106
|
|
|
107
107
|
## settings.yaml 参考
|
|
108
108
|
|
|
109
|
-
面板与 settings.yaml 读写同一命名空间,两边改动互通、保存即生效。webhookUrl
|
|
109
|
+
面板与 settings.yaml 读写同一命名空间,两边改动互通、保存即生效。webhookUrl 随面板配置响应回显,yaml 直改同样可见。
|
|
110
110
|
|
|
111
111
|
```yaml
|
|
112
112
|
turn-notify:
|
|
@@ -155,7 +155,7 @@ turn-notify:
|
|
|
155
155
|
## 安全
|
|
156
156
|
|
|
157
157
|
- 配置写入类接口(config / mapping / upload / 音效改名与删除 / 测试)带同源守卫:Origin 与 Host 不符即 403,JSON 写入另校验 content-type,阻断跨站页面 drive-by 改写配置。
|
|
158
|
-
- webhookUrl
|
|
158
|
+
- webhookUrl 在 settings 层标记为 secret(导出与日志脱敏);面板配置接口原文回显,本机同源可读写。
|
|
159
159
|
- 音效上传双重校验扩展名与真实音频内容;音效读取接口拒绝音频扩展名以外的文件。
|
|
160
160
|
- 已知边界:同源守卫不防 DNS rebinding(Origin 与 Host 相等即放行)。该暴露面属 host webserver 全部 /api 路由的存量问题,应在 host 层统一解决而非逐插件补丁。
|
|
161
161
|
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -1009,10 +1009,10 @@ window.__ModuleLoader__.load({
|
|
|
1009
1009
|
|
|
1010
1010
|
const PERMISSION_LABELS = { granted: '已授权', denied: '已拒绝', default: '未授权' }
|
|
1011
1011
|
|
|
1012
|
-
// 面板表单占位:GET config 返回前展示;webhookUrl
|
|
1012
|
+
// 面板表单占位:GET config 返回前展示;webhookUrl 原文随响应回显,表单所见即所存;
|
|
1013
1013
|
// imAvailable 缺省为假,加载响应后 dsh-im 在场才渲染 IM 投递卡
|
|
1014
1014
|
const DEFAULT_CONFIG = {
|
|
1015
|
-
|
|
1015
|
+
webhookUrl: '',
|
|
1016
1016
|
minTurnDurationMs: 5 * 1000,
|
|
1017
1017
|
rootsOnly: true,
|
|
1018
1018
|
suppressSubagentWake: true,
|
|
@@ -1196,6 +1196,7 @@ window.__ModuleLoader__.load({
|
|
|
1196
1196
|
.then((res) => {
|
|
1197
1197
|
setConfig({ ...DEFAULT_CONFIG, ...res })
|
|
1198
1198
|
setMappingState(res.soundMapping || {})
|
|
1199
|
+
setUrlDraft(typeof res.webhookUrl === 'string' ? res.webhookUrl : '')
|
|
1199
1200
|
setConfigLoaded(true)
|
|
1200
1201
|
})
|
|
1201
1202
|
.catch(() => {})
|
|
@@ -1366,14 +1367,12 @@ window.__ModuleLoader__.load({
|
|
|
1366
1367
|
: String(config.minTurnDurationMs)
|
|
1367
1368
|
if (raw.length === 0) throw new Error('最短回合时长不能为空')
|
|
1368
1369
|
const trimmedUrl = urlDraft.trim()
|
|
1369
|
-
//
|
|
1370
|
-
const patchBody = { minTurnDurationMs: Number(raw), rootsOnly: config.rootsOnly, suppressSubagentWake: config.suppressSubagentWake }
|
|
1371
|
-
// 只写语义:输入留空即保持现有 webhook 不变
|
|
1372
|
-
if (trimmedUrl.length > 0) patchBody.webhookUrl = trimmedUrl
|
|
1370
|
+
// 所见即所存:输入框即配置值,清空并保存即禁用 webhook 通道
|
|
1371
|
+
const patchBody = { webhookUrl: trimmedUrl, minTurnDurationMs: Number(raw), rootsOnly: config.rootsOnly, suppressSubagentWake: config.suppressSubagentWake }
|
|
1373
1372
|
const res = await api('/api/turn-notify/config', { method: 'POST', body: JSON.stringify(patchBody) })
|
|
1374
1373
|
setConfig({ ...DEFAULT_CONFIG, ...res })
|
|
1375
1374
|
if (res.soundMapping) setMappingState(res.soundMapping)
|
|
1376
|
-
setUrlDraft('')
|
|
1375
|
+
setUrlDraft(typeof res.webhookUrl === 'string' ? res.webhookUrl : '')
|
|
1377
1376
|
patch('配置已保存,立即生效')
|
|
1378
1377
|
} catch (error) {
|
|
1379
1378
|
patch('保存失败:' + (error && error.message ? error.message : String(error)), 'error')
|
|
@@ -1386,7 +1385,7 @@ window.__ModuleLoader__.load({
|
|
|
1386
1385
|
const res = await api('/api/turn-notify/config', { method: 'POST', body: JSON.stringify({ webhookUrl: '' }) })
|
|
1387
1386
|
setConfig({ ...DEFAULT_CONFIG, ...res })
|
|
1388
1387
|
if (res.soundMapping) setMappingState(res.soundMapping)
|
|
1389
|
-
setUrlDraft('')
|
|
1388
|
+
setUrlDraft(typeof res.webhookUrl === 'string' ? res.webhookUrl : '')
|
|
1390
1389
|
patch('webhook 已清除')
|
|
1391
1390
|
} catch (error) {
|
|
1392
1391
|
patch('清除失败:' + (error && error.message ? error.message : String(error)), 'error')
|
|
@@ -1467,8 +1466,9 @@ window.__ModuleLoader__.load({
|
|
|
1467
1466
|
}
|
|
1468
1467
|
|
|
1469
1468
|
async function testWebhook() {
|
|
1470
|
-
|
|
1471
|
-
|
|
1469
|
+
// 草稿与已保存值不同即存在未保存改动,测试的只能是已保存配置
|
|
1470
|
+
if (urlDraft.trim() !== String(config.webhookUrl || '').trim()) {
|
|
1471
|
+
patch('表单中的 webhook URL 尚未保存,本次测试的是已保存配置;请先保存再测试', 'error')
|
|
1472
1472
|
return
|
|
1473
1473
|
}
|
|
1474
1474
|
try {
|
|
@@ -1632,15 +1632,15 @@ window.__ModuleLoader__.load({
|
|
|
1632
1632
|
field('webhook', [
|
|
1633
1633
|
h('input', {
|
|
1634
1634
|
className: 'tn-input tn-fill', type: 'text',
|
|
1635
|
-
title: '通知由 host 直接 POST 到该地址,标签页全关也送达;Slack 兼容 JSON 格式,超时 10
|
|
1636
|
-
placeholder:
|
|
1635
|
+
title: '通知由 host 直接 POST 到该地址,标签页全关也送达;Slack 兼容 JSON 格式,超时 10 秒不重试',
|
|
1636
|
+
placeholder: 'Slack-compatible URL,留空禁用',
|
|
1637
1637
|
value: urlDraft,
|
|
1638
1638
|
onChange: (e) => setUrlDraft(e.target.value),
|
|
1639
1639
|
}),
|
|
1640
|
-
config.
|
|
1640
|
+
String(config.webhookUrl || '').trim().length > 0
|
|
1641
1641
|
? h('button', { className: 'tn-btn tn-btn--danger', disabled: busy, title: '清除已配置的 webhook,清除后该通道禁用', onClick: () => void clearWebhook() }, '清除')
|
|
1642
1642
|
: null,
|
|
1643
|
-
], '
|
|
1643
|
+
], '保存即提交输入框内容,清空并保存即禁用;改动后先保存,测试按钮只测已保存配置'),
|
|
1644
1644
|
field('最短回合时长', [
|
|
1645
1645
|
h('input', {
|
|
1646
1646
|
className: 'tn-input', type: 'number', min: 0, step: 500, style: { width: '90px' },
|
package/src/core.mjs
CHANGED
|
@@ -664,7 +664,7 @@ export function resolvedConfig(settings) {
|
|
|
664
664
|
}
|
|
665
665
|
}
|
|
666
666
|
|
|
667
|
-
// 面板可见配置:webhookUrl
|
|
667
|
+
// 面板可见配置:webhookUrl 原文随响应回显,面板所见即所存。
|
|
668
668
|
export function publicConfig(settings) {
|
|
669
669
|
const resolved = resolvedConfig(settings)
|
|
670
670
|
return {
|
|
@@ -675,6 +675,6 @@ export function publicConfig(settings) {
|
|
|
675
675
|
soundMapping: resolved.soundMapping,
|
|
676
676
|
imTargets: resolved.imTargets,
|
|
677
677
|
kindRoutes: resolved.kindRoutes,
|
|
678
|
-
|
|
678
|
+
webhookUrl: resolved.webhookUrl,
|
|
679
679
|
}
|
|
680
|
-
}
|
|
680
|
+
}
|
package/test/core.test.mjs
CHANGED
|
@@ -679,7 +679,7 @@ test('配置解析:enabled 缺省键按开补全,字段类型回退默认', () =
|
|
|
679
679
|
assert.equal(resolvedConfig({ suppressSubagentWake: false }).suppressSubagentWake, false)
|
|
680
680
|
})
|
|
681
681
|
|
|
682
|
-
test('面板可见配置:webhookUrl
|
|
682
|
+
test('面板可见配置:webhookUrl 原文回显', () => {
|
|
683
683
|
assert.deepEqual(
|
|
684
684
|
publicConfig({ webhookUrl: 'https://hook.example/service/xxx', enabled: { completed: false }, soundMapping: { completed: 'snd-1' }, kindRoutes: { ask: ['im'] } }),
|
|
685
685
|
{
|
|
@@ -690,12 +690,13 @@ test('面板可见配置:webhookUrl 不出主机,仅回是否已配置', () => {
|
|
|
690
690
|
soundMapping: { completed: 'snd-1' },
|
|
691
691
|
imTargets: [],
|
|
692
692
|
kindRoutes: { ask: ['im'] },
|
|
693
|
-
|
|
693
|
+
webhookUrl: 'https://hook.example/service/xxx',
|
|
694
694
|
},
|
|
695
695
|
)
|
|
696
|
-
assert.equal(publicConfig({}).
|
|
697
|
-
|
|
698
|
-
assert.equal(
|
|
696
|
+
assert.equal(publicConfig({}).webhookUrl, '')
|
|
697
|
+
// 空白串原样回显,不静默归一
|
|
698
|
+
assert.equal(publicConfig({ webhookUrl: ' ' }).webhookUrl, ' ')
|
|
699
|
+
assert.equal('webhookConfigured' in publicConfig({ webhookUrl: 'https://hook.example' }), false)
|
|
699
700
|
})
|
|
700
701
|
|
|
701
702
|
test('会话事件有界累积:标题提取后封账为字符串,内存不再增长', () => {
|
package/test/route.test.mjs
CHANGED
|
@@ -79,7 +79,7 @@ function makeCtx(extraServices) {
|
|
|
79
79
|
return { ctx, routes }
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
test('config GET 返回解析后的默认配置,webhookUrl
|
|
82
|
+
test('config GET 返回解析后的默认配置,webhookUrl 原文回显', async () => {
|
|
83
83
|
const { ctx, routes } = makeCtx()
|
|
84
84
|
apply(ctx)
|
|
85
85
|
const handler = routes.get('/api/turn-notify/config')
|
|
@@ -87,8 +87,8 @@ test('config GET 返回解析后的默认配置,webhookUrl 凭据不出主机',
|
|
|
87
87
|
const res = makeRes()
|
|
88
88
|
await handler(makeReq('GET'), res)
|
|
89
89
|
assert.equal(res.status, 200)
|
|
90
|
-
assert.equal(res.body.
|
|
91
|
-
assert.equal('
|
|
90
|
+
assert.equal(res.body.webhookUrl, '')
|
|
91
|
+
assert.equal('webhookConfigured' in res.body, false)
|
|
92
92
|
assert.equal(res.body.minTurnDurationMs, MIN_TURN_MS)
|
|
93
93
|
assert.equal(res.body.rootsOnly, true)
|
|
94
94
|
assert.equal(res.body.enabled.completed, true)
|
|
@@ -105,12 +105,30 @@ test('config POST 非法补丁 400,合法补丁持久化且部分开关不覆盖
|
|
|
105
105
|
const good = makeRes()
|
|
106
106
|
await handler(makeReq('POST', { webhookUrl: 'https://hook.example', enabled: { completed: false } }, JSON_HEADERS), good)
|
|
107
107
|
assert.equal(good.status, 200)
|
|
108
|
-
assert.equal(good.body.
|
|
108
|
+
assert.equal(good.body.webhookUrl, 'https://hook.example')
|
|
109
109
|
assert.equal(good.body.enabled.completed, false)
|
|
110
110
|
assert.equal(good.body.enabled.error, true)
|
|
111
111
|
const readback = makeRes()
|
|
112
112
|
await handler(makeReq('GET'), readback)
|
|
113
|
-
assert.equal(readback.body.
|
|
113
|
+
assert.equal(readback.body.webhookUrl, 'https://hook.example')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('config POST 空串清空 webhookUrl:先存后清双向回环,清后通道禁用', async () => {
|
|
117
|
+
const { ctx, routes } = makeCtx()
|
|
118
|
+
apply(ctx)
|
|
119
|
+
const handler = routes.get('/api/turn-notify/config')
|
|
120
|
+
const save = makeRes()
|
|
121
|
+
await handler(makeReq('POST', { webhookUrl: 'https://hook.example/x' }, JSON_HEADERS), save)
|
|
122
|
+
assert.equal(save.status, 200)
|
|
123
|
+
assert.equal(save.body.webhookUrl, 'https://hook.example/x')
|
|
124
|
+
// 清空并保存=禁用:空串经校验落库,回读为空串而非回退旧值
|
|
125
|
+
const clear = makeRes()
|
|
126
|
+
await handler(makeReq('POST', { webhookUrl: '' }, JSON_HEADERS), clear)
|
|
127
|
+
assert.equal(clear.status, 200)
|
|
128
|
+
assert.equal(clear.body.webhookUrl, '')
|
|
129
|
+
const readback = makeRes()
|
|
130
|
+
await handler(makeReq('GET'), readback)
|
|
131
|
+
assert.equal(readback.body.webhookUrl, '')
|
|
114
132
|
})
|
|
115
133
|
|
|
116
134
|
test('config POST 负路径:跨源 403,非 JSON 400,畸形体 400,空补丁 200', async () => {
|
|
@@ -228,7 +246,7 @@ test('config POST 仅 imTargets 的部分补丁:不触碰其他配置项,响应
|
|
|
228
246
|
const res = makeRes()
|
|
229
247
|
await handler(makeReq('POST', { imTargets: [{ botId: 'wx_a', targetId: 'owner' }, { botId: 'wx_b', targetId: 'owner' }] }, JSON_HEADERS), res)
|
|
230
248
|
assert.equal(res.status, 200)
|
|
231
|
-
assert.equal(res.body.
|
|
249
|
+
assert.equal(res.body.webhookUrl, 'https://hooks.example.com/x')
|
|
232
250
|
assert.equal(res.body.enabled.completed, false)
|
|
233
251
|
assert.deepEqual(res.body.imTargets, [{ botId: 'wx_a', targetId: 'owner' }, { botId: 'wx_b', targetId: 'owner' }])
|
|
234
252
|
// 取消注册:整列表替换为空,其余配置仍原样
|
|
@@ -236,7 +254,7 @@ test('config POST 仅 imTargets 的部分补丁:不触碰其他配置项,响应
|
|
|
236
254
|
await handler(makeReq('POST', { imTargets: [] }, JSON_HEADERS), clear)
|
|
237
255
|
assert.equal(clear.status, 200)
|
|
238
256
|
assert.deepEqual(clear.body.imTargets, [])
|
|
239
|
-
assert.equal(clear.body.
|
|
257
|
+
assert.equal(clear.body.webhookUrl, 'https://hooks.example.com/x')
|
|
240
258
|
assert.equal(clear.body.enabled.completed, false)
|
|
241
259
|
})
|
|
242
260
|
|