@zaofan/dsh-qqbot 1.10.45 → 1.10.46
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/client/qqbot-settings.js +33 -7
- package/docs/extension-contract.md +31 -0
- package/package.json +1 -1
- package/settings-host.js +67 -2
package/client/qqbot-settings.js
CHANGED
|
@@ -6476,13 +6476,39 @@ var QQS_THEME_CSS = [
|
|
|
6476
6476
|
onChange: function (e) { var v = e.target.value; upd(v === '' ? undefined : Number(v)) } })
|
|
6477
6477
|
} else if (f.type === 'select') {
|
|
6478
6478
|
var opts = Array.isArray(f.options) ? f.options : []
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
|
|
6479
|
+
// 统一成 [{v: 实际值, t: 显示文本}]
|
|
6480
|
+
var normOpts = opts.map(function (o) {
|
|
6481
|
+
var v = (o && typeof o === 'object') ? (o.value !== undefined ? o.value : o.label) : o
|
|
6482
|
+
var t = (o && typeof o === 'object') ? (o.label || o.value) : o
|
|
6483
|
+
return { v: String(v), t: String(t) }
|
|
6484
|
+
})
|
|
6485
|
+
if (f.searchable) {
|
|
6486
|
+
// ★ 可搜索下拉(2026-10-26):选项很多时(比如 294 个 TTS 音色)原生 select 没法找。
|
|
6487
|
+
// 用 <input list=datalist>:浏览器原生支持输入过滤,且允许手填列表外的值。
|
|
6488
|
+
// datalist 的 option.value 用「显示文本」,这样搜中文名、选中后存进去的也是那一整串;
|
|
6489
|
+
// 读取侧(如 lib/settings.mjs 的 resolveVoice)需要自己从「中文名|id」里剥出真值。
|
|
6490
|
+
var dlId = 'qqs-dl-' + String(f.key || 'f')
|
|
6491
|
+
control = h('div', null,
|
|
6492
|
+
h('input', {
|
|
6493
|
+
type: 'text',
|
|
6494
|
+
list: dlId,
|
|
6495
|
+
style: Object.assign({}, S.fieldInput, { width: '100%' }),
|
|
6496
|
+
value: val === undefined || val === null ? '' : String(val),
|
|
6497
|
+
placeholder: f.placeholder || ('输入关键字筛选(共 ' + normOpts.length + ' 项,也可直接手填值)'),
|
|
6498
|
+
onInput: function (e) { upd(e.target.value) },
|
|
6499
|
+
onChange: function (e) { upd(e.target.value) },
|
|
6500
|
+
}),
|
|
6501
|
+
h('datalist', { id: dlId },
|
|
6502
|
+
normOpts.map(function (o) {
|
|
6503
|
+
return h('option', { key: o.v, value: o.t })
|
|
6504
|
+
})))
|
|
6505
|
+
} else {
|
|
6506
|
+
control = h('select', { style: S.fieldInput, value: val === undefined ? '' : val,
|
|
6507
|
+
onChange: function (e) { upd(e.target.value) } },
|
|
6508
|
+
normOpts.map(function (o) {
|
|
6509
|
+
return h('option', { key: o.v, value: o.v, style: { background: '#2a2a2e', color: '#e8e8ea' } }, o.t)
|
|
6510
|
+
}))
|
|
6511
|
+
}
|
|
6486
6512
|
} else if (f.type === 'list') {
|
|
6487
6513
|
var arr = Array.isArray(val) ? val : []
|
|
6488
6514
|
control = h('div', null,
|
|
@@ -76,6 +76,37 @@
|
|
|
76
76
|
| 宿主自重启 | `await env.restart()` | 复用内置 `/bot-restart`(自动识别启动命令、强补 `--no-open`)。默认**仅主人**可触发;要放开写 `env.restart({ requireOwner: false })` |
|
|
77
77
|
| 回调按钮卡 | `env.registerInteractionCard({cardId, buttonIds, expireAt})` | 发**带回调按钮**的卡片必须先登记,否则点了没人认 |
|
|
78
78
|
|
|
79
|
+
### 扩展设置页的字段类型(schema)
|
|
80
|
+
|
|
81
|
+
扩展文件里 `export const qqbotSettingsPage = { title, desc, schema: [...] }`,面板就会给它生成设置页。
|
|
82
|
+
`schema` 每项支持:
|
|
83
|
+
|
|
84
|
+
| type | 控件 | 说明 |
|
|
85
|
+
| --- | --- | --- |
|
|
86
|
+
| `bool` | 开关 | 值 true/false |
|
|
87
|
+
| `number` | 数字框 | 值 number |
|
|
88
|
+
| `string`(默认) | 输入框 | 加 `secret: true` 变密码框 |
|
|
89
|
+
| `list` | 多行文本 | 一行一项,值 string[] |
|
|
90
|
+
| `select` | 下拉 | `options` 可写字符串数组,也可写 `{ value, label }` |
|
|
91
|
+
|
|
92
|
+
**`select` + `searchable: true`(1.10.46 起)**
|
|
93
|
+
|
|
94
|
+
选项很多(几十上百条)时,原生 `<select>` 根本没法找。加上这个标记会渲染成
|
|
95
|
+
`<input list="…"> + <datalist>`:浏览器原生**输入过滤**,并且**允许手填列表外的值**。
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
{ key: 'defaultSpeaker', type: 'select', searchable: true, label: '默认音色',
|
|
99
|
+
default: 'Vivi 2.0|zh_female_vv_uranus_bigtts',
|
|
100
|
+
options: VOICES.map(v => v.label) } // ['中文名|voice_type', ...]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
⚠️ 两个必须注意的点:
|
|
104
|
+
|
|
105
|
+
1. **`select` 取到的值永远是字符串**(`'24000'`,不是 `24000`)。
|
|
106
|
+
如果你的扩展要把它传给要求严格类型的第三方接口,**自己转数字**,否则会像 TTS 那样撞
|
|
107
|
+
`55000000 cannot unmarshal string into ... of type int`。
|
|
108
|
+
2. 用 `中文名|id` 这种复合值时,**读取侧要负责剥出真值**(本项目 TTS 包用 `pickSpeaker()` 统一处理)。
|
|
109
|
+
|
|
79
110
|
### 扩展包声明「最低插件版本」(1.10.44 起)
|
|
80
111
|
|
|
81
112
|
包的清单文件里可以写 `minPluginVersion`,面板会拿它跟当前装的插件版本对照,不满足就标红:
|
package/package.json
CHANGED
package/settings-host.js
CHANGED
|
@@ -121,13 +121,72 @@ const COPY_SKIP = /\.(rar|zip|7z|bak|tmp|log|diag)$/i;
|
|
|
121
121
|
const ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
122
122
|
|
|
123
123
|
/** 同源回环 fence: 仅本机 loopback + 同源/无 Origin 放行 */
|
|
124
|
+
/**
|
|
125
|
+
* 规范化 host:'1.2.3.4:8443' / 'http://a.b' / 'a.b' → 'a.b:8443' 或 'a.b'
|
|
126
|
+
* 解析失败就原样小写返回,让比较退化成字符串相等。
|
|
127
|
+
*/
|
|
128
|
+
function normHost(v) {
|
|
129
|
+
const t = String(v || '').trim().toLowerCase();
|
|
130
|
+
if (!t) return '';
|
|
131
|
+
try { return new URL(t.includes('://') ? t : `http://${t}`).host; } catch { return t; }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 宿主 dsh web 的 --trusted-host 白名单(issue #22)
|
|
136
|
+
*
|
|
137
|
+
* 为什么读 process.argv:插件与 dsh web **同进程**,宿主已经用它放行了远程访问,
|
|
138
|
+
* 插件这边再自己维护一套 loopback 判断就会两边策略打架 —— 反代场景下
|
|
139
|
+
* Caddy 保留原始 Host(如 example.com:8443),插件一律 403 而宿主放行。
|
|
140
|
+
*
|
|
141
|
+
* 支持两种写法:
|
|
142
|
+
* --trusted-host a.com:8443 b.com (一个参数跟多个值,遇到 - 开头结束)
|
|
143
|
+
* --trusted-host=a.com:8443
|
|
144
|
+
*/
|
|
145
|
+
let TRUSTED_HOST_CACHE;
|
|
146
|
+
function trustedHostList() {
|
|
147
|
+
if (TRUSTED_HOST_CACHE !== undefined) return TRUSTED_HOST_CACHE;
|
|
148
|
+
const set = new Set();
|
|
149
|
+
const argv = process.argv || [];
|
|
150
|
+
for (let i = 0; i < argv.length; i++) {
|
|
151
|
+
const a = argv[i];
|
|
152
|
+
if (typeof a !== 'string') continue;
|
|
153
|
+
if (a === '--trusted-host') {
|
|
154
|
+
for (let j = i + 1; j < argv.length; j++) {
|
|
155
|
+
const v = argv[j];
|
|
156
|
+
if (typeof v !== 'string' || !v || v.startsWith('-')) break;
|
|
157
|
+
const n = normHost(v);
|
|
158
|
+
if (n) set.add(n);
|
|
159
|
+
}
|
|
160
|
+
} else if (a.startsWith('--trusted-host=')) {
|
|
161
|
+
const n = normHost(a.slice('--trusted-host='.length));
|
|
162
|
+
if (n) set.add(n);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
TRUSTED_HOST_CACHE = set;
|
|
166
|
+
return set;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** 请求的 host 是否命中 --trusted-host 白名单(带端口精确比;不带端口则比 hostname) */
|
|
170
|
+
function hostAllowed(host, hostname) {
|
|
171
|
+
const list = trustedHostList();
|
|
172
|
+
if (!list.size) return false;
|
|
173
|
+
for (const t of list) {
|
|
174
|
+
if (t === host) return true;
|
|
175
|
+
if (!t.includes(':')) {
|
|
176
|
+
try { if (new URL(`http://${t}`).hostname === hostname) return true; } catch { /* 忽略 */ }
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
|
|
124
182
|
function isTrusted(req) {
|
|
125
183
|
const h = req.headers?.host;
|
|
126
184
|
if (typeof h !== 'string') return false;
|
|
127
185
|
let hostname;
|
|
128
186
|
try { hostname = new URL(`http://${h}`).hostname; } catch { return false; }
|
|
129
187
|
const loopback = /^(\[::1\]|localhost|127(\.\d{1,3}){3})$/.test(hostname);
|
|
130
|
-
|
|
188
|
+
// ★ 2026-10-26(issue #22):loopback ∪ 宿主 --trusted-host 白名单
|
|
189
|
+
if (!loopback && !hostAllowed(String(h).toLowerCase(), hostname)) return false;
|
|
131
190
|
if (req.headers?.['sec-fetch-site'] === 'cross-site') return false;
|
|
132
191
|
const o = req.headers?.origin;
|
|
133
192
|
if (o === undefined) return true;
|
|
@@ -620,7 +679,13 @@ export function apply(ctx) {
|
|
|
620
679
|
const wa = await import('./dist/session/workspace-attach.js');
|
|
621
680
|
workspaceAttach = typeof wa.workspaceAttachState === 'function' ? wa.workspaceAttachState() : null;
|
|
622
681
|
} catch { /* 模块未部署时忽略 */ }
|
|
623
|
-
writeJson(res, 200, {
|
|
682
|
+
writeJson(res, 200, {
|
|
683
|
+
hasSettings, nss, describeErr, workspaceAttach,
|
|
684
|
+
// ★ issue #22 诊断:远程访问被 403 时看这里 —— 空集合说明 dsh web 没带 --trusted-host
|
|
685
|
+
trustedHosts: [...trustedHostList()],
|
|
686
|
+
hostHeader: req.headers?.host || '(无)',
|
|
687
|
+
hostTrusted: isTrusted(req),
|
|
688
|
+
});
|
|
624
689
|
} catch (e) { writeJson(res, 500, { error: String(e?.message ?? e) }); }
|
|
625
690
|
});
|
|
626
691
|
|