dsh-isolation-gate 0.3.2 → 1.0.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/CHANGELOG.md +38 -5
- package/README.en.md +21 -6
- package/README.md +34 -15
- package/docs/release-0.4.0.md +56 -0
- package/docs/screenshots/02-group.png +0 -0
- package/docs/screenshots/04-audit.png +0 -0
- package/docs/screenshots/05-special-profile.png +0 -0
- package/docs/screenshots/README.md +11 -3
- package/lib/client.js +771 -33
- package/lib/index.js +87 -5
- package/lib/profiles.js +60 -16
- package/lib/store.js +81 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -1086,7 +1086,80 @@ window.__ModuleLoader__.load({
|
|
|
1086
1086
|
}
|
|
1087
1087
|
|
|
1088
1088
|
/** 有状态的一层:只负责取数与动作,渲染全交给 GateView。 */
|
|
1089
|
-
// ==================== 页 2
|
|
1089
|
+
// ==================== 页 2:隔离强度 ====================
|
|
1090
|
+
//
|
|
1091
|
+
// 2026-10-22 户主原话「策略档名字太抽象,不知道干嘛的」⇒ 改叫「隔离强度」,
|
|
1092
|
+
// 三档一律用白话名(宽松 / 严格 / 全封)+ 一句"这档会发生什么"。
|
|
1093
|
+
const PROFILE_PLAIN = {
|
|
1094
|
+
normal: { cn: '宽松', line: '只拦你点名的目录(黑名单);没点名的都能读能写。日常用这档。' },
|
|
1095
|
+
strict: { cn: '严格', line: '反过来:只放你点名的目录(白名单),别处一律拒。' },
|
|
1096
|
+
sealed: { cn: '全封', line: '任何目录都不许碰,连没登记的工具调用也拒。最保险,也最干不了活。' },
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function profilePlain(name) {
|
|
1100
|
+
return PROFILE_PLAIN[name] || { cn: name, line: '自定义档' }
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
/**
|
|
1104
|
+
* 组类型的人话解释。这是整个插件最容易看错的一处语义:
|
|
1105
|
+
* 严格组 = 白名单:清单里的放行,**清单外的全拒**
|
|
1106
|
+
* 宽松组 = 黑名单:清单里的受限,**清单外的照常**
|
|
1107
|
+
* 2026-10-23 户主实测踩坑(把 E 盘加进严格组当"只禁 E",结果全盘封死)⇒ 必须显眼。
|
|
1108
|
+
*/
|
|
1109
|
+
const PROFILE_MODE = {
|
|
1110
|
+
allowlist: {
|
|
1111
|
+
tag: '白名单模式',
|
|
1112
|
+
name: '严格组',
|
|
1113
|
+
rule: '只放清单里的;没列进组的,一律拒',
|
|
1114
|
+
color: C.warn,
|
|
1115
|
+
bg: 'rgba(251,191,36,0.14)',
|
|
1116
|
+
},
|
|
1117
|
+
blocklist: {
|
|
1118
|
+
tag: '黑名单模式',
|
|
1119
|
+
name: '宽松组',
|
|
1120
|
+
rule: '只限清单里的;没列进组的,照常放行',
|
|
1121
|
+
color: C.ok,
|
|
1122
|
+
bg: 'rgba(74,222,128,0.14)',
|
|
1123
|
+
},
|
|
1124
|
+
sealed: {
|
|
1125
|
+
tag: '全封模式',
|
|
1126
|
+
name: '全封',
|
|
1127
|
+
rule: '什么都不放行',
|
|
1128
|
+
color: C.bad,
|
|
1129
|
+
bg: 'rgba(248,113,113,0.14)',
|
|
1130
|
+
},
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function profileMode(mode) {
|
|
1134
|
+
return (
|
|
1135
|
+
PROFILE_MODE[mode] || { tag: '自定义模式', name: mode || '自定义', rule: '', color: C.dim, bg: 'transparent' }
|
|
1136
|
+
)
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/** 一条醒目的「这个组是白名单还是黑名单」横幅。 */
|
|
1140
|
+
function ModeStrip(props) {
|
|
1141
|
+
const info = profileMode(props.mode)
|
|
1142
|
+
return h(
|
|
1143
|
+
'div',
|
|
1144
|
+
{
|
|
1145
|
+
'data-gate-mode': props.mode,
|
|
1146
|
+
style: {
|
|
1147
|
+
display: 'flex',
|
|
1148
|
+
alignItems: 'center',
|
|
1149
|
+
gap: '6px',
|
|
1150
|
+
flexWrap: 'wrap',
|
|
1151
|
+
marginTop: '6px',
|
|
1152
|
+
padding: '5px 9px',
|
|
1153
|
+
borderRadius: '8px',
|
|
1154
|
+
border: `1px solid ${info.color}55`,
|
|
1155
|
+
background: info.bg,
|
|
1156
|
+
},
|
|
1157
|
+
},
|
|
1158
|
+
h('span', { style: { fontSize: '11px', fontWeight: 700, color: info.color } }, info.tag),
|
|
1159
|
+
h('span', { style: { fontSize: '11px', color: C.text } }, info.rule),
|
|
1160
|
+
)
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1090
1163
|
function ProfilesPage() {
|
|
1091
1164
|
const [data, setData] = useState(null)
|
|
1092
1165
|
const [error, setError] = useState(null)
|
|
@@ -1120,8 +1193,8 @@ window.__ModuleLoader__.load({
|
|
|
1120
1193
|
h(
|
|
1121
1194
|
'div',
|
|
1122
1195
|
{ style: { display: 'flex', alignItems: 'baseline', gap: '10px', flexWrap: 'wrap' } },
|
|
1123
|
-
h('div', { style: { fontSize: '20px', fontWeight: 750 } }, '门禁 ·
|
|
1124
|
-
h('div', { style: { fontSize: '12px', color: C.dim } }, '
|
|
1196
|
+
h('div', { style: { fontSize: '20px', fontWeight: 750 } }, '门禁 · 隔离强度'),
|
|
1197
|
+
h('div', { style: { fontSize: '12px', color: C.dim } }, '一句话:这档决定 dsh 默认能碰什么、碰多狠'),
|
|
1125
1198
|
h('span', { style: { flex: '1 1 auto' } }),
|
|
1126
1199
|
h(Button, { label: busy ? '刷新中…' : '刷新', onClick: load, disabled: busy }),
|
|
1127
1200
|
),
|
|
@@ -1130,43 +1203,410 @@ window.__ModuleLoader__.load({
|
|
|
1130
1203
|
profiles.length
|
|
1131
1204
|
? h(
|
|
1132
1205
|
'div',
|
|
1133
|
-
{ style: { display: 'grid', gap: '10px', marginTop: '14px', gridTemplateColumns: 'repeat(auto-fill, minmax(
|
|
1134
|
-
profiles.map((profile) =>
|
|
1135
|
-
|
|
1206
|
+
{ style: { display: 'grid', gap: '10px', marginTop: '14px', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))' } },
|
|
1207
|
+
profiles.map((profile) => {
|
|
1208
|
+
const plain = profilePlain(profile.name)
|
|
1209
|
+
return h(
|
|
1136
1210
|
'div',
|
|
1137
|
-
{
|
|
1211
|
+
{
|
|
1212
|
+
key: profile.name,
|
|
1213
|
+
'data-gate-profile': profile.name,
|
|
1214
|
+
style: {
|
|
1215
|
+
border: `1px solid ${profile.isDefault ? C.accent : C.border2}`,
|
|
1216
|
+
borderRadius: '12px',
|
|
1217
|
+
padding: '12px 14px',
|
|
1218
|
+
background: C.card,
|
|
1219
|
+
},
|
|
1220
|
+
},
|
|
1138
1221
|
h(
|
|
1139
1222
|
'div',
|
|
1140
|
-
{ style: { display: 'flex', alignItems: 'baseline', gap: '8px' } },
|
|
1141
|
-
h('div', { style: { fontSize: '
|
|
1223
|
+
{ style: { display: 'flex', alignItems: 'baseline', gap: '8px', flexWrap: 'wrap' } },
|
|
1224
|
+
h('div', { style: { fontSize: '16px', fontWeight: 700 } }, plain.cn),
|
|
1225
|
+
h('span', { style: { fontSize: '11px', color: C.dim } }, profile.name),
|
|
1142
1226
|
profile.isDefault
|
|
1143
|
-
? h('span', { style: { fontSize: '11px', color: C.ok, border: `1px solid ${C.ok}`, borderRadius: '999px', padding: '0 6px' } }, '
|
|
1227
|
+
? h('span', { style: { fontSize: '11px', color: C.ok, border: `1px solid ${C.ok}`, borderRadius: '999px', padding: '0 6px' } }, '当前默认')
|
|
1144
1228
|
: null,
|
|
1145
|
-
h('span', { style: { flex: '1 1 auto' } }),
|
|
1146
|
-
h('span', { style: { fontSize: '11px', color: C.dim } }, profile.mode),
|
|
1147
1229
|
),
|
|
1148
|
-
h(
|
|
1230
|
+
h('div', { style: { fontSize: '13px', lineHeight: '20px', marginTop: '6px' } }, plain.line),
|
|
1231
|
+
h(
|
|
1232
|
+
Line,
|
|
1233
|
+
null,
|
|
1234
|
+
`没登记的目录:${profile.mode === 'allowlist' || profile.mode === 'sealed' ? '拒' : '放行'} · ` +
|
|
1235
|
+
`联网:${profile.network === 'deny' ? '禁' : '允许'} · ` +
|
|
1236
|
+
`没登记的工具:${profile.unknownTool === 'deny' ? '拒' : '放行'}`,
|
|
1237
|
+
),
|
|
1149
1238
|
profile.allow.length
|
|
1150
1239
|
? profile.allow.map((entry) =>
|
|
1151
1240
|
h('div', { key: entry.path, style: { fontSize: '12px', color: C.dim, marginTop: '4px', wordBreak: 'break-all' } }, `${entry.level} · ${entry.path}`),
|
|
1152
1241
|
)
|
|
1153
|
-
: h('div', { style: { fontSize: '12px', color: C.
|
|
1154
|
-
)
|
|
1155
|
-
),
|
|
1242
|
+
: h('div', { style: { fontSize: '12px', color: C.dim, marginTop: '4px' } }, profile.mode === 'blocklist' ? '不点名 ⇒ 不拦' : '没点名的路径 ⇒ 全拒'),
|
|
1243
|
+
)
|
|
1244
|
+
}),
|
|
1156
1245
|
)
|
|
1157
1246
|
: null,
|
|
1158
|
-
h(SectionTitle, { title: '
|
|
1247
|
+
h(SectionTitle, { title: '按目录加严', hint: '某个目录里放 .dsh-gate.json,只影响这棵子树;只能更严,不能解锁别处' }),
|
|
1159
1248
|
dirBindings.length
|
|
1160
1249
|
? dirBindings.map((item) =>
|
|
1161
1250
|
h(
|
|
1162
1251
|
'div',
|
|
1163
1252
|
{ key: item.dir, style: { border: `1px solid ${C.border2}`, borderRadius: '10px', padding: '8px 10px', marginTop: '6px', background: C.card } },
|
|
1164
|
-
h('div', { style: { fontSize: '13px' } }, item.profile
|
|
1253
|
+
h('div', { style: { fontSize: '13px' } }, `${profilePlain(item.profile).cn}${item.profile ? `(${item.profile})` : ''}`),
|
|
1165
1254
|
h('div', { style: { fontSize: '11px', color: C.dim, wordBreak: 'break-all' } }, item.dir),
|
|
1166
1255
|
),
|
|
1167
1256
|
)
|
|
1168
|
-
: h(Line, null, '
|
|
1257
|
+
: h(Line, null, '还没有任何 .dsh-gate.json(在目标目录建一个即生效)'),
|
|
1169
1258
|
h(Line, null, '示例:{"profile":"strict","allow":[{"path":"D:\\\\某项目","level":"read"}]}'),
|
|
1259
|
+
h(Line, null, '注意:强度档管的是 dsh 自己的工具调用;别的程序、已在跑的进程、绕过工具直接读写,都不在这个范围内。'),
|
|
1260
|
+
)
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// ==================== 页 2b:隔离强度 · 模板编辑器 ====================
|
|
1264
|
+
const ALL_LEVELS = [
|
|
1265
|
+
['deny', '禁'],
|
|
1266
|
+
['read', '只读'],
|
|
1267
|
+
['full', '可读写删'],
|
|
1268
|
+
]
|
|
1269
|
+
const EDIT_LEVELS = {
|
|
1270
|
+
blocklist: ALL_LEVELS,
|
|
1271
|
+
allowlist: ALL_LEVELS,
|
|
1272
|
+
sealed: ALL_LEVELS,
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
class Boundary extends React.Component {
|
|
1276
|
+
constructor(props) {
|
|
1277
|
+
super(props)
|
|
1278
|
+
this.state = { err: null }
|
|
1279
|
+
}
|
|
1280
|
+
static getDerivedStateFromError(err) {
|
|
1281
|
+
return { err }
|
|
1282
|
+
}
|
|
1283
|
+
componentDidCatch(err) {
|
|
1284
|
+
console.error('[门禁] 渲染出错:', err)
|
|
1285
|
+
}
|
|
1286
|
+
render() {
|
|
1287
|
+
if (this.state.err) {
|
|
1288
|
+
return h(
|
|
1289
|
+
'div',
|
|
1290
|
+
{ style: { padding: '16px', borderRadius: '10px', border: `1px solid ${C.bad}`, color: C.bad, fontSize: '13px', lineHeight: 1.6 } },
|
|
1291
|
+
`这一块渲染出错:${this.state.err.message}`,
|
|
1292
|
+
)
|
|
1293
|
+
}
|
|
1294
|
+
return this.props.children
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
function ProfilesEditor() {
|
|
1299
|
+
const [data, setData] = useState(null)
|
|
1300
|
+
const [error, setError] = useState(null)
|
|
1301
|
+
const [notice, setNotice] = useState(null)
|
|
1302
|
+
const [busy, setBusy] = useState(false)
|
|
1303
|
+
const [draft, setDraft] = useState(null)
|
|
1304
|
+
const [pathInput, setPathInput] = useState('')
|
|
1305
|
+
|
|
1306
|
+
const load = useCallback(async () => {
|
|
1307
|
+
setBusy(true)
|
|
1308
|
+
setError(null)
|
|
1309
|
+
try {
|
|
1310
|
+
setData(await call(`${BASE}/profiles`))
|
|
1311
|
+
} catch (err) {
|
|
1312
|
+
setError(err.message)
|
|
1313
|
+
} finally {
|
|
1314
|
+
setBusy(false)
|
|
1315
|
+
}
|
|
1316
|
+
}, [])
|
|
1317
|
+
|
|
1318
|
+
useEffect(() => {
|
|
1319
|
+
load()
|
|
1320
|
+
}, [load])
|
|
1321
|
+
|
|
1322
|
+
const profiles = ((data && data.profiles) || []).filter((row) => !row.builtin)
|
|
1323
|
+
|
|
1324
|
+
const startFrom = (mode) => {
|
|
1325
|
+
setDraft({
|
|
1326
|
+
name: mode === 'allowlist' ? '严格组' : '宽松组',
|
|
1327
|
+
label: '',
|
|
1328
|
+
mode,
|
|
1329
|
+
allow: [],
|
|
1330
|
+
})
|
|
1331
|
+
setPathInput('')
|
|
1332
|
+
setNotice(null)
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
const editExisting = (row) => {
|
|
1336
|
+
setDraft({
|
|
1337
|
+
name: row.name,
|
|
1338
|
+
label: row.label === row.name ? '' : row.label,
|
|
1339
|
+
mode: row.mode,
|
|
1340
|
+
allow: (row.allow || []).map((e) => ({ path: e.path, level: e.level })),
|
|
1341
|
+
})
|
|
1342
|
+
setPathInput('')
|
|
1343
|
+
setNotice(null)
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
const addEntry = () => {
|
|
1347
|
+
const value = pathInput.trim()
|
|
1348
|
+
if (!value) return
|
|
1349
|
+
const list = Array.isArray(draft.allow) ? draft.allow : []
|
|
1350
|
+
const fallback = draft.mode === 'blocklist' ? 'deny' : 'read'
|
|
1351
|
+
setDraft({ ...draft, allow: [...list, { path: value, level: fallback }] })
|
|
1352
|
+
setPathInput('')
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
const pickFolder = async () => {
|
|
1356
|
+
setBusy(true)
|
|
1357
|
+
setError(null)
|
|
1358
|
+
try {
|
|
1359
|
+
const result = await post(`${BASE}/pick`, {})
|
|
1360
|
+
const picked = result && (result.path || result.dir || result.selected)
|
|
1361
|
+
if (picked) setPathInput(String(picked))
|
|
1362
|
+
} catch (err) {
|
|
1363
|
+
setError(err.message)
|
|
1364
|
+
} finally {
|
|
1365
|
+
setBusy(false)
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
const save = async () => {
|
|
1370
|
+
setBusy(true)
|
|
1371
|
+
setError(null)
|
|
1372
|
+
setNotice(null)
|
|
1373
|
+
try {
|
|
1374
|
+
await post(`${BASE}/profile/save`, {
|
|
1375
|
+
name: draft.name,
|
|
1376
|
+
label: draft.label,
|
|
1377
|
+
mode: draft.mode,
|
|
1378
|
+
allow: Array.isArray(draft.allow) ? draft.allow : [],
|
|
1379
|
+
})
|
|
1380
|
+
setDraft(null)
|
|
1381
|
+
await load()
|
|
1382
|
+
setNotice('已保存')
|
|
1383
|
+
} catch (err) {
|
|
1384
|
+
setError(err.message)
|
|
1385
|
+
} finally {
|
|
1386
|
+
setBusy(false)
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
const removeProfile = async (name) => {
|
|
1391
|
+
setBusy(true)
|
|
1392
|
+
setError(null)
|
|
1393
|
+
setNotice(null)
|
|
1394
|
+
try {
|
|
1395
|
+
await post(`${BASE}/profile/delete`, { name })
|
|
1396
|
+
await load()
|
|
1397
|
+
setNotice(`已删除「${name}」`)
|
|
1398
|
+
} catch (err) {
|
|
1399
|
+
setError(err.message)
|
|
1400
|
+
} finally {
|
|
1401
|
+
setBusy(false)
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
const makeDefault = async (row) => {
|
|
1406
|
+
setBusy(true)
|
|
1407
|
+
setError(null)
|
|
1408
|
+
setNotice(null)
|
|
1409
|
+
try {
|
|
1410
|
+
await post(`${BASE}/profile/save`, {
|
|
1411
|
+
name: row.name,
|
|
1412
|
+
label: row.label === row.name ? '' : row.label,
|
|
1413
|
+
mode: row.mode,
|
|
1414
|
+
allow: row.allow,
|
|
1415
|
+
asDefault: true,
|
|
1416
|
+
})
|
|
1417
|
+
await load()
|
|
1418
|
+
setNotice(`「${row.label || row.name}」已设为全局默认`)
|
|
1419
|
+
} catch (err) {
|
|
1420
|
+
setError(err.message)
|
|
1421
|
+
} finally {
|
|
1422
|
+
setBusy(false)
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
const inputStyle = {
|
|
1427
|
+
fontSize: '13px',
|
|
1428
|
+
padding: '5px 9px',
|
|
1429
|
+
borderRadius: '8px',
|
|
1430
|
+
border: `1px solid ${C.border}`,
|
|
1431
|
+
background: 'transparent',
|
|
1432
|
+
color: C.text,
|
|
1433
|
+
}
|
|
1434
|
+
const levelOptions = draft && draft.mode === 'allowlist'
|
|
1435
|
+
? EDIT_LEVELS.allowlist
|
|
1436
|
+
: EDIT_LEVELS.blocklist
|
|
1437
|
+
|
|
1438
|
+
return h(
|
|
1439
|
+
'div',
|
|
1440
|
+
{ style: { padding: '18px 22px 32px', height: '100%', overflow: 'auto', boxSizing: 'border-box', background: C.card2, color: C.text } },
|
|
1441
|
+
h(
|
|
1442
|
+
'div',
|
|
1443
|
+
{ style: { display: 'flex', alignItems: 'baseline', gap: '10px', flexWrap: 'wrap' } },
|
|
1444
|
+
h('div', { style: { fontSize: '20px', fontWeight: 750 } }, '门禁 · 特别门禁'),
|
|
1445
|
+
h('div', { style: { fontSize: '12px', color: C.dim } }, '这里编"某个会话专用"的规则;没给会话挂,就等于没有特别门禁'),
|
|
1446
|
+
h('span', { style: { flex: '1 1 auto' } }),
|
|
1447
|
+
h(Button, { label: busy ? '刷新中…' : '刷新', onClick: load, disabled: busy }),
|
|
1448
|
+
),
|
|
1449
|
+
error ? h('div', { style: { color: C.bad, fontSize: '12px', marginTop: '10px' } }, `✗ ${error}`) : null,
|
|
1450
|
+
notice ? h('div', { style: { color: C.ok, fontSize: '12px', marginTop: '10px' } }, `✓ ${notice}`) : null,
|
|
1451
|
+
|
|
1452
|
+
h(SectionTitle, { title: '新建特别组', hint: '先选严格还是宽松,再往里加目录' }),
|
|
1453
|
+
h(
|
|
1454
|
+
'div',
|
|
1455
|
+
{ style: { display: 'flex', gap: '8px', flexWrap: 'wrap' } },
|
|
1456
|
+
h(Button, { label: '+ 严格组(只放我选的,其余一律拒)', primary: true, onClick: () => startFrom('allowlist') }),
|
|
1457
|
+
h(Button, { label: '+ 宽松组(只禁我选的,其余照常)', onClick: () => startFrom('blocklist') }),
|
|
1458
|
+
),
|
|
1459
|
+
|
|
1460
|
+
draft
|
|
1461
|
+
? h(
|
|
1462
|
+
'div',
|
|
1463
|
+
{ 'data-gate-editor': '1', style: { marginTop: '12px', border: `1px solid ${C.accent}`, borderRadius: '12px', padding: '14px', background: C.card } },
|
|
1464
|
+
h(
|
|
1465
|
+
'div',
|
|
1466
|
+
{ style: { display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap' } },
|
|
1467
|
+
h('span', { style: { fontSize: '13px', fontWeight: 700 } }, `编辑模板「${draft.name}」`),
|
|
1468
|
+
h('span', { style: { flex: '1 1 auto' } }),
|
|
1469
|
+
h(Button, { label: busy ? '保存中…' : '保存', primary: true, onClick: save, disabled: busy }),
|
|
1470
|
+
h(Button, { label: '取消', onClick: () => setDraft(null), disabled: busy }),
|
|
1471
|
+
),
|
|
1472
|
+
h(
|
|
1473
|
+
'div',
|
|
1474
|
+
{ style: { display: 'flex', gap: '8px', marginTop: '10px', flexWrap: 'wrap', alignItems: 'center' } },
|
|
1475
|
+
h('span', { style: { fontSize: '12px', color: C.dim } }, '模板名'),
|
|
1476
|
+
h('input', { value: draft.name, onChange: (e) => setDraft({ ...draft, name: e.target.value }), style: { ...inputStyle, minWidth: '200px' } }),
|
|
1477
|
+
h('span', { style: { fontSize: '12px', color: C.dim } }, '显示名'),
|
|
1478
|
+
h('input', { value: draft.label, placeholder: '可空', onChange: (e) => setDraft({ ...draft, label: e.target.value }), style: { ...inputStyle, minWidth: '150px' } }),
|
|
1479
|
+
),
|
|
1480
|
+
h(ModeStrip, { mode: draft.mode }),
|
|
1481
|
+
h(
|
|
1482
|
+
Line,
|
|
1483
|
+
{ style: { marginTop: '6px' } },
|
|
1484
|
+
draft.mode === 'allowlist'
|
|
1485
|
+
? '清单里的条目:只读 = 只能读、可读写删 = 放行、禁 = 连这条也拒。**清单外的路径一律拒。**'
|
|
1486
|
+
: '清单里的条目:禁 = 完全不许碰、只读 = 只能读、可读写删 = 放行。**清单外的路径照常。**',
|
|
1487
|
+
),
|
|
1488
|
+
h(
|
|
1489
|
+
'div',
|
|
1490
|
+
{ style: { display: 'flex', gap: '8px', marginTop: '8px', flexWrap: 'wrap', alignItems: 'center' } },
|
|
1491
|
+
h(Button, {
|
|
1492
|
+
label:
|
|
1493
|
+
draft.mode === 'allowlist'
|
|
1494
|
+
? '改成宽松组(只禁我选的,其余照常)'
|
|
1495
|
+
: '改成严格组(只放我选的,其余一律拒)',
|
|
1496
|
+
disabled: busy,
|
|
1497
|
+
onClick: () =>
|
|
1498
|
+
setDraft({ ...draft, mode: draft.mode === 'allowlist' ? 'blocklist' : 'allowlist' }),
|
|
1499
|
+
}),
|
|
1500
|
+
h('span', { style: { fontSize: '11px', color: C.dim } }, '切类型不动清单,只换「清单外怎么算」'),
|
|
1501
|
+
),
|
|
1502
|
+
h(
|
|
1503
|
+
'div',
|
|
1504
|
+
{ style: { display: 'flex', gap: '8px', marginTop: '12px', flexWrap: 'wrap', alignItems: 'center' } },
|
|
1505
|
+
h('input', {
|
|
1506
|
+
value: pathInput,
|
|
1507
|
+
placeholder: 'D:\\某个目录',
|
|
1508
|
+
onChange: (e) => setPathInput(e.target.value),
|
|
1509
|
+
onKeyDown: (e) => {
|
|
1510
|
+
if (e.key === 'Enter') addEntry()
|
|
1511
|
+
},
|
|
1512
|
+
style: { ...inputStyle, minWidth: '320px' },
|
|
1513
|
+
}),
|
|
1514
|
+
h(Button, { label: '选择文件夹…', onClick: pickFolder, disabled: busy }),
|
|
1515
|
+
h(Button, { label: '加进清单', onClick: addEntry, disabled: !pathInput.trim() }),
|
|
1516
|
+
),
|
|
1517
|
+
(Array.isArray(draft.allow) ? draft.allow : []).length === 0
|
|
1518
|
+
? h(Line, { style: { marginTop: '10px' } }, draft.mode === 'blocklist' ? '清单为空 ⇒ 这档什么也不拦' : '清单为空 ⇒ 这档什么都拒(等同全封)')
|
|
1519
|
+
: h(
|
|
1520
|
+
'div',
|
|
1521
|
+
{ style: { display: 'grid', gap: '6px', marginTop: '10px' } },
|
|
1522
|
+
(Array.isArray(draft.allow) ? draft.allow : []).map((entry, index) =>
|
|
1523
|
+
h(
|
|
1524
|
+
'div',
|
|
1525
|
+
{ key: `${entry.path}-${index}`, style: { display: 'flex', gap: '8px', alignItems: 'center', padding: '6px 9px', borderRadius: '9px', border: `1px solid ${C.border}`, background: C.card2 } },
|
|
1526
|
+
h('span', { style: { fontSize: '12px', wordBreak: 'break-all', flex: '1 1 auto' } }, entry.path),
|
|
1527
|
+
h(
|
|
1528
|
+
'div',
|
|
1529
|
+
{ style: { display: 'flex', gap: '4px' } },
|
|
1530
|
+
levelOptions.map(([level, label]) =>
|
|
1531
|
+
h(Button, {
|
|
1532
|
+
key: level,
|
|
1533
|
+
label,
|
|
1534
|
+
primary: entry.level === level,
|
|
1535
|
+
disabled: busy,
|
|
1536
|
+
onClick: () =>
|
|
1537
|
+
setDraft({
|
|
1538
|
+
...draft,
|
|
1539
|
+
allow: draft.allow.map((row, i) => (i === index ? { ...row, level } : row)),
|
|
1540
|
+
}),
|
|
1541
|
+
}),
|
|
1542
|
+
),
|
|
1543
|
+
),
|
|
1544
|
+
h(Button, {
|
|
1545
|
+
label: '删',
|
|
1546
|
+
disabled: busy,
|
|
1547
|
+
onClick: () => setDraft({ ...draft, allow: draft.allow.filter((_, i) => i !== index) }),
|
|
1548
|
+
}),
|
|
1549
|
+
),
|
|
1550
|
+
),
|
|
1551
|
+
),
|
|
1552
|
+
)
|
|
1553
|
+
: null,
|
|
1554
|
+
|
|
1555
|
+
h(SectionTitle, { title: '现有模板', hint: '内置三档不可删;自定义的可随时改' }),
|
|
1556
|
+
!data && !error ? h(Line, null, '读取中…') : null,
|
|
1557
|
+
h(
|
|
1558
|
+
'div',
|
|
1559
|
+
{ style: { display: 'grid', gap: '10px', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))' } },
|
|
1560
|
+
profiles.map((row) =>
|
|
1561
|
+
h(
|
|
1562
|
+
'div',
|
|
1563
|
+
{ key: row.name, 'data-gate-profile': row.name, style: { border: `1px solid ${row.isDefault ? C.accent : C.border2}`, borderRadius: '12px', padding: '12px 14px', background: C.card } },
|
|
1564
|
+
h(
|
|
1565
|
+
'div',
|
|
1566
|
+
{ style: { display: 'flex', alignItems: 'baseline', gap: '8px', flexWrap: 'wrap' } },
|
|
1567
|
+
h('div', { style: { fontSize: '16px', fontWeight: 700 } }, row.label || row.name),
|
|
1568
|
+
h('span', { style: { fontSize: '11px', color: C.dim } }, row.name),
|
|
1569
|
+
row.builtin
|
|
1570
|
+
? h('span', { style: { fontSize: '11px', color: C.dim, border: `1px solid ${C.border}`, borderRadius: '999px', padding: '0 6px' } }, '内置')
|
|
1571
|
+
: null,
|
|
1572
|
+
),
|
|
1573
|
+
h(ModeStrip, { mode: row.mode }),
|
|
1574
|
+
(() => {
|
|
1575
|
+
const overrides = (data && data.sessionOverrides) || {}
|
|
1576
|
+
const users = Object.keys(overrides).filter((sid) => overrides[sid] === row.name)
|
|
1577
|
+
return h(
|
|
1578
|
+
Line,
|
|
1579
|
+
{ style: { marginTop: '4px' } },
|
|
1580
|
+
users.length === 0
|
|
1581
|
+
? `成员 ${row.allow.length} 个 · 还没有会话挂它`
|
|
1582
|
+
: `成员 ${row.allow.length} 个 · 挂在 ${users.length} 个会话:${users
|
|
1583
|
+
.map((sid) => String(sid).slice(0, 8))
|
|
1584
|
+
.join('、')}`,
|
|
1585
|
+
)
|
|
1586
|
+
})(),
|
|
1587
|
+
row.allow.length
|
|
1588
|
+
? h(
|
|
1589
|
+
'div',
|
|
1590
|
+
{ style: { marginTop: '6px', display: 'grid', gap: '3px' } },
|
|
1591
|
+
row.allow.map((entry) =>
|
|
1592
|
+
h(
|
|
1593
|
+
'div',
|
|
1594
|
+
{ key: entry.path, style: { fontSize: '12px', color: C.dim, wordBreak: 'break-all' } },
|
|
1595
|
+
`${entry.level === 'deny' ? '禁' : entry.level === 'read' ? '只读' : '可读写删'} · ${entry.path}`,
|
|
1596
|
+
),
|
|
1597
|
+
),
|
|
1598
|
+
)
|
|
1599
|
+
: h(Line, { style: { marginTop: '6px' } }, '清单为空'),
|
|
1600
|
+
h(
|
|
1601
|
+
'div',
|
|
1602
|
+
{ style: { display: 'flex', gap: '8px', marginTop: '10px' } },
|
|
1603
|
+
h(Button, { label: '编辑', onClick: () => editExisting(row), disabled: busy }),
|
|
1604
|
+
row.builtin ? null : h(Button, { label: '删除', onClick: () => removeProfile(row.name), disabled: busy }),
|
|
1605
|
+
),
|
|
1606
|
+
),
|
|
1607
|
+
),
|
|
1608
|
+
),
|
|
1609
|
+
h(Line, { style: { marginTop: '16px' } }, '模板只管 dsh 自己的工具调用;别的程序、已在跑的进程、绕过工具直接读写,都不在范围内。'),
|
|
1170
1610
|
)
|
|
1171
1611
|
}
|
|
1172
1612
|
|
|
@@ -1261,27 +1701,79 @@ window.__ModuleLoader__.load({
|
|
|
1261
1701
|
|
|
1262
1702
|
|
|
1263
1703
|
// ==================== 外壳:单页 + 标签(照「技能」面板的样子) ====================
|
|
1704
|
+
//
|
|
1705
|
+
// 2026-10-22 修「点了不切换」:标签用**原生 button**(不再包一层 Button),
|
|
1706
|
+
// 并把当前页记在**模块级变量**里 —— 万一宿主重建了组件实例,回来还是刚才那页。
|
|
1707
|
+
const SHELL_TABS = [
|
|
1708
|
+
['list', '全局门禁', '所有会话都要守:哪些目录能被 dsh 碰,各给什么级别'],
|
|
1709
|
+
['profiles', '特别门禁', '给某个会话单独加一套规则(黑名单 / 白名单 / 全封)'],
|
|
1710
|
+
['audit', '审计与诊断', '谁动过什么 + 一键清理'],
|
|
1711
|
+
]
|
|
1712
|
+
let lastTab = 'list'
|
|
1713
|
+
|
|
1714
|
+
function TabBar(props) {
|
|
1715
|
+
return h(
|
|
1716
|
+
'div',
|
|
1717
|
+
{ role: 'tablist', style: { display: 'flex', gap: '6px', flexWrap: 'wrap' } },
|
|
1718
|
+
SHELL_TABS.map(([id, label, hint]) =>
|
|
1719
|
+
h(
|
|
1720
|
+
'button',
|
|
1721
|
+
{
|
|
1722
|
+
key: id,
|
|
1723
|
+
type: 'button',
|
|
1724
|
+
role: 'tab',
|
|
1725
|
+
'data-gate-tab': id,
|
|
1726
|
+
'aria-selected': props.value === id ? 'true' : 'false',
|
|
1727
|
+
title: hint,
|
|
1728
|
+
onClick: () => props.onPick(id),
|
|
1729
|
+
style: {
|
|
1730
|
+
cursor: 'pointer',
|
|
1731
|
+
fontSize: '13px',
|
|
1732
|
+
fontWeight: props.value === id ? 600 : 500,
|
|
1733
|
+
padding: '6px 14px',
|
|
1734
|
+
borderRadius: '8px',
|
|
1735
|
+
border: props.value === id ? `1px solid ${C.accent}` : `1px solid ${C.border}`,
|
|
1736
|
+
background: props.value === id ? C.card : 'transparent',
|
|
1737
|
+
color: props.value === id ? C.accent : C.dim,
|
|
1738
|
+
boxShadow: props.value === id ? `inset 0 -2px 0 0 ${C.accent}` : 'none',
|
|
1739
|
+
},
|
|
1740
|
+
},
|
|
1741
|
+
label,
|
|
1742
|
+
),
|
|
1743
|
+
),
|
|
1744
|
+
)
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1264
1747
|
function GateShell() {
|
|
1265
|
-
const [tab,
|
|
1266
|
-
const
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
]
|
|
1748
|
+
const [tab, setTabState] = useState(lastTab)
|
|
1749
|
+
const setTab = useCallback((id) => {
|
|
1750
|
+
lastTab = id
|
|
1751
|
+
setTabState(id)
|
|
1752
|
+
}, [])
|
|
1753
|
+
const current = SHELL_TABS.find((row) => row[0] === tab) || SHELL_TABS[0]
|
|
1271
1754
|
return h(
|
|
1272
1755
|
'div',
|
|
1273
1756
|
{ style: { height: '100%', display: 'flex', flexDirection: 'column', background: C.card2, color: C.text } },
|
|
1274
1757
|
h(
|
|
1275
1758
|
'div',
|
|
1276
|
-
{
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1759
|
+
{
|
|
1760
|
+
style: {
|
|
1761
|
+
padding: '12px 22px 10px',
|
|
1762
|
+
borderBottom: `1px solid ${C.border2}`,
|
|
1763
|
+
display: 'flex',
|
|
1764
|
+
alignItems: 'center',
|
|
1765
|
+
gap: '10px',
|
|
1766
|
+
flexWrap: 'wrap',
|
|
1767
|
+
},
|
|
1768
|
+
},
|
|
1769
|
+
h(TabBar, { value: tab, onPick: setTab }),
|
|
1770
|
+
h('span', { style: { flex: '1 1 auto' } }),
|
|
1771
|
+
h('span', { 'data-gate-tab-current': tab, style: { fontSize: '11px', color: C.dim } }, current[2]),
|
|
1280
1772
|
),
|
|
1281
1773
|
h(
|
|
1282
1774
|
'div',
|
|
1283
|
-
{ style: { flex: '1 1 auto', minHeight: 0 } },
|
|
1284
|
-
tab === 'profiles' ? h(
|
|
1775
|
+
{ style: { flex: '1 1 auto', minHeight: 0, overflow: 'hidden' } },
|
|
1776
|
+
tab === 'profiles' ? h(Boundary, null, h(ProfilesEditor)) : tab === 'audit' ? h(AuditPage) : h(GatePage),
|
|
1285
1777
|
),
|
|
1286
1778
|
)
|
|
1287
1779
|
}
|
|
@@ -1493,6 +1985,226 @@ window.__ModuleLoader__.load({
|
|
|
1493
1985
|
})
|
|
1494
1986
|
}
|
|
1495
1987
|
|
|
1988
|
+
const SESSION_SLOT = 'conversation.session.header.utilities'
|
|
1989
|
+
const SESSION_BUTTON_ID = 'isolation-gate:session'
|
|
1990
|
+
|
|
1991
|
+
/** 组类型 → 中文(会话头部那一小块只留最短的说法,但必须带上"清单外怎么算")。 */
|
|
1992
|
+
function sessionModeText(profile) {
|
|
1993
|
+
if (!profile) return ''
|
|
1994
|
+
if (profile.mode === 'allowlist') return '严格组 · 没点名的全拒'
|
|
1995
|
+
if (profile.mode === 'sealed') return '全封 · 什么都不放'
|
|
1996
|
+
return '宽松组 · 没点名的照常'
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
/**
|
|
2000
|
+
* 会话头部小按钮(垃圾桶旁边):给**当前会话**挂一个特别组。
|
|
2001
|
+
*
|
|
2002
|
+
* 槽位契约(`conversation.session.header.utilities`,list,session 作用域)会给组件
|
|
2003
|
+
* 标准会话 props,其中就有 `sessionId` —— 所以这里不需要再问「是哪个会话」。
|
|
2004
|
+
*
|
|
2005
|
+
* 单选:一个会话最多挂一个组;「不额外限制」= 清掉绑定,只走全局名单。
|
|
2006
|
+
* 数据源与「特别门禁」页完全同一份(`GET /profiles?sessionId=` / `POST /session-profile`),
|
|
2007
|
+
* 不在这里留副本 —— 免得头部显示的和工具侧读到的不是一回事。
|
|
2008
|
+
*/
|
|
2009
|
+
function SessionGateButton(props) {
|
|
2010
|
+
const sessionId = props && props.sessionId ? String(props.sessionId) : ''
|
|
2011
|
+
const [open, setOpen] = useState(false)
|
|
2012
|
+
const [model, setModel] = useState(null)
|
|
2013
|
+
const [busy, setBusy] = useState(false)
|
|
2014
|
+
const [error, setError] = useState('')
|
|
2015
|
+
|
|
2016
|
+
const load = useCallback(() => {
|
|
2017
|
+
if (sessionId === '') return
|
|
2018
|
+
call(`${BASE}/profiles?sessionId=${encodeURIComponent(sessionId)}`)
|
|
2019
|
+
.then((next) => {
|
|
2020
|
+
setModel(next)
|
|
2021
|
+
setError('')
|
|
2022
|
+
})
|
|
2023
|
+
.catch((err) => setError(err.message))
|
|
2024
|
+
}, [sessionId])
|
|
2025
|
+
|
|
2026
|
+
useEffect(() => {
|
|
2027
|
+
load()
|
|
2028
|
+
}, [load])
|
|
2029
|
+
|
|
2030
|
+
const pick = (name) => {
|
|
2031
|
+
setBusy(true)
|
|
2032
|
+
post(`${BASE}/session-profile`, { sessionId, profile: name })
|
|
2033
|
+
.then(() => {
|
|
2034
|
+
setBusy(false)
|
|
2035
|
+
setOpen(false)
|
|
2036
|
+
load()
|
|
2037
|
+
})
|
|
2038
|
+
.catch((err) => {
|
|
2039
|
+
setBusy(false)
|
|
2040
|
+
setError(err.message)
|
|
2041
|
+
})
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
const profiles = model && Array.isArray(model.profiles) ? model.profiles : []
|
|
2045
|
+
const groups = profiles.filter((item) => item.builtin !== true)
|
|
2046
|
+
const current = model && typeof model.currentSessionProfile === 'string' ? model.currentSessionProfile : ''
|
|
2047
|
+
const currentItem = profiles.filter((item) => item.name === current)[0] || null
|
|
2048
|
+
const currentLabel = current === '' ? '' : (currentItem && currentItem.label) || current
|
|
2049
|
+
|
|
2050
|
+
const trigger = h(
|
|
2051
|
+
'button',
|
|
2052
|
+
{
|
|
2053
|
+
type: 'button',
|
|
2054
|
+
'data-gate-session-button': SESSION_BUTTON_ID,
|
|
2055
|
+
title:
|
|
2056
|
+
current === ''
|
|
2057
|
+
? '本会话没挂特别组,只走全局名单 —— 点这里挂一个'
|
|
2058
|
+
: `本会话挂的特别组:${currentLabel}(点这里换一个)`,
|
|
2059
|
+
onClick: () => setOpen((value) => !value),
|
|
2060
|
+
style: {
|
|
2061
|
+
cursor: 'pointer',
|
|
2062
|
+
display: 'inline-flex',
|
|
2063
|
+
alignItems: 'center',
|
|
2064
|
+
gap: '5px',
|
|
2065
|
+
fontSize: '12px',
|
|
2066
|
+
lineHeight: '22px',
|
|
2067
|
+
padding: '0 8px',
|
|
2068
|
+
borderRadius: '8px',
|
|
2069
|
+
border: `1px solid ${current === '' ? C.border : C.accent}`,
|
|
2070
|
+
background: current === '' ? 'transparent' : 'rgba(77,107,254,0.16)',
|
|
2071
|
+
color: current === '' ? C.dim : C.text,
|
|
2072
|
+
maxWidth: '180px',
|
|
2073
|
+
},
|
|
2074
|
+
},
|
|
2075
|
+
h(GateIcon, { size: 13 }),
|
|
2076
|
+
h(
|
|
2077
|
+
'span',
|
|
2078
|
+
{ style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } },
|
|
2079
|
+
current === '' ? '门禁' : currentLabel,
|
|
2080
|
+
),
|
|
2081
|
+
)
|
|
2082
|
+
|
|
2083
|
+
if (!open) return h('span', { style: { display: 'inline-flex' } }, trigger)
|
|
2084
|
+
|
|
2085
|
+
const row = (key, label, hint, active) =>
|
|
2086
|
+
h(
|
|
2087
|
+
'button',
|
|
2088
|
+
{
|
|
2089
|
+
key,
|
|
2090
|
+
type: 'button',
|
|
2091
|
+
disabled: busy,
|
|
2092
|
+
'data-gate-session-pick': key === '' ? '(none)' : key,
|
|
2093
|
+
onClick: () => pick(key),
|
|
2094
|
+
style: {
|
|
2095
|
+
cursor: busy ? 'default' : 'pointer',
|
|
2096
|
+
display: 'block',
|
|
2097
|
+
width: '100%',
|
|
2098
|
+
textAlign: 'left',
|
|
2099
|
+
padding: '6px 8px',
|
|
2100
|
+
borderRadius: '8px',
|
|
2101
|
+
border: 'none',
|
|
2102
|
+
background: active ? 'rgba(77,107,254,0.22)' : 'transparent',
|
|
2103
|
+
color: C.text,
|
|
2104
|
+
opacity: busy ? 0.6 : 1,
|
|
2105
|
+
},
|
|
2106
|
+
},
|
|
2107
|
+
h(
|
|
2108
|
+
'div',
|
|
2109
|
+
{ style: { display: 'flex', alignItems: 'center', gap: '6px', fontSize: '12px' } },
|
|
2110
|
+
h('span', { style: { color: active ? C.accent : C.idle } }, active ? '●' : '○'),
|
|
2111
|
+
h('span', { style: { flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, label),
|
|
2112
|
+
),
|
|
2113
|
+
hint
|
|
2114
|
+
? h('div', { style: { fontSize: '11px', color: C.dim, marginLeft: '16px' } }, hint)
|
|
2115
|
+
: null,
|
|
2116
|
+
)
|
|
2117
|
+
|
|
2118
|
+
const body = h(
|
|
2119
|
+
'div',
|
|
2120
|
+
null,
|
|
2121
|
+
h(
|
|
2122
|
+
'div',
|
|
2123
|
+
{
|
|
2124
|
+
style: {
|
|
2125
|
+
fontSize: '11px',
|
|
2126
|
+
fontWeight: 600,
|
|
2127
|
+
letterSpacing: '0.04em',
|
|
2128
|
+
color: C.dim,
|
|
2129
|
+
padding: '4px 8px 6px',
|
|
2130
|
+
},
|
|
2131
|
+
},
|
|
2132
|
+
'本会话的门禁',
|
|
2133
|
+
),
|
|
2134
|
+
row('', '不额外限制(只走全局名单)', current === '' ? '当前' : '', current === ''),
|
|
2135
|
+
groups.length === 0
|
|
2136
|
+
? h(
|
|
2137
|
+
'div',
|
|
2138
|
+
{ style: { padding: '8px 9px 6px', lineHeight: '17px' } },
|
|
2139
|
+
h('div', { style: { fontSize: '12px', color: C.text, marginBottom: '3px' } }, '还没有特别组'),
|
|
2140
|
+
h(
|
|
2141
|
+
'div',
|
|
2142
|
+
{ style: { fontSize: '11px', color: C.dim } },
|
|
2143
|
+
'左边「门禁 → 特别门禁」里点「+ 严格组 / + 宽松组」建一个,再回来挂。',
|
|
2144
|
+
),
|
|
2145
|
+
)
|
|
2146
|
+
: groups.map((item) =>
|
|
2147
|
+
row(
|
|
2148
|
+
item.name,
|
|
2149
|
+
item.label,
|
|
2150
|
+
`${sessionModeText(item)} · ${Array.isArray(item.allow) ? item.allow.length : 0} 条${
|
|
2151
|
+
current === item.name ? ' · 当前' : ''
|
|
2152
|
+
}`,
|
|
2153
|
+
current === item.name,
|
|
2154
|
+
),
|
|
2155
|
+
),
|
|
2156
|
+
error
|
|
2157
|
+
? h('div', { style: { fontSize: '11px', color: C.bad, padding: '6px 8px', wordBreak: 'break-all' } }, error)
|
|
2158
|
+
: null,
|
|
2159
|
+
h(
|
|
2160
|
+
'div',
|
|
2161
|
+
{
|
|
2162
|
+
style: {
|
|
2163
|
+
marginTop: '6px',
|
|
2164
|
+
paddingTop: '6px',
|
|
2165
|
+
borderTop: `1px solid ${C.border}`,
|
|
2166
|
+
fontSize: '11px',
|
|
2167
|
+
color: C.dim,
|
|
2168
|
+
padding: '6px 8px 2px',
|
|
2169
|
+
lineHeight: '16px',
|
|
2170
|
+
},
|
|
2171
|
+
},
|
|
2172
|
+
'组只会更严,不会放宽全局名单。',
|
|
2173
|
+
),
|
|
2174
|
+
)
|
|
2175
|
+
|
|
2176
|
+
return h(
|
|
2177
|
+
'span',
|
|
2178
|
+
{ style: { position: 'relative', display: 'inline-flex' } },
|
|
2179
|
+
trigger,
|
|
2180
|
+
h('div', {
|
|
2181
|
+
key: 'backdrop',
|
|
2182
|
+
onClick: () => setOpen(false),
|
|
2183
|
+
style: { position: 'fixed', inset: 0, zIndex: 50 },
|
|
2184
|
+
}),
|
|
2185
|
+
h(
|
|
2186
|
+
'div',
|
|
2187
|
+
{
|
|
2188
|
+
key: 'popover',
|
|
2189
|
+
style: {
|
|
2190
|
+
position: 'absolute',
|
|
2191
|
+
right: 0,
|
|
2192
|
+
top: 'calc(100% + 6px)',
|
|
2193
|
+
width: '252px',
|
|
2194
|
+
zIndex: 51,
|
|
2195
|
+
padding: '6px',
|
|
2196
|
+
borderRadius: '10px',
|
|
2197
|
+
border: `1px solid ${C.border2}`,
|
|
2198
|
+
/* 抬升面用 bg-layer-1。bg-overlay 是「浮层遮罩色」,铺在深色底上会发脏灰 */
|
|
2199
|
+
background: 'var(--dsw-alias-bg-layer-1, #1e2026)',
|
|
2200
|
+
boxShadow: '0 18px 44px rgba(0,0,0,0.55), inset 0 1px 0 rgba(255,255,255,0.04)',
|
|
2201
|
+
},
|
|
2202
|
+
},
|
|
2203
|
+
h(Boundary, null, body),
|
|
2204
|
+
),
|
|
2205
|
+
)
|
|
2206
|
+
}
|
|
2207
|
+
|
|
1496
2208
|
function apply(ctx) {
|
|
1497
2209
|
try {
|
|
1498
2210
|
const disposeRow = ctx.slots.inject('sidebar.panellist', () =>
|
|
@@ -1504,12 +2216,16 @@ window.__ModuleLoader__.load({
|
|
|
1504
2216
|
const disposePage = ctx.slots.inject('main', () =>
|
|
1505
2217
|
ctx.slots.register({ name: 'main', key: PANEL_ID }, GateShell),
|
|
1506
2218
|
)
|
|
2219
|
+
const disposeSession = ctx.slots.inject(SESSION_SLOT, () =>
|
|
2220
|
+
ctx.slots.register({ name: SESSION_SLOT, id: SESSION_BUTTON_ID, order: 1 }, SessionGateButton),
|
|
2221
|
+
)
|
|
1507
2222
|
ctx.effect(() => () => {
|
|
2223
|
+
disposeSession()
|
|
1508
2224
|
disposePage()
|
|
1509
2225
|
disposeRow()
|
|
1510
2226
|
})
|
|
1511
2227
|
console.log(
|
|
1512
|
-
`[dsh-isolation-gate] client: 「门禁」行 +
|
|
2228
|
+
`[dsh-isolation-gate] client: 「门禁」行 + 整页 + 会话头部小按钮已挂到 slots(order ${PANEL_ORDER},紧贴「技能」5 下方)`,
|
|
1513
2229
|
)
|
|
1514
2230
|
} catch (error) {
|
|
1515
2231
|
console.error('[dsh-isolation-gate] client: 槽位注册失败', error)
|
|
@@ -1519,7 +2235,29 @@ window.__ModuleLoader__.load({
|
|
|
1519
2235
|
return {
|
|
1520
2236
|
apply,
|
|
1521
2237
|
inject: ['slots'],
|
|
1522
|
-
__selftest: {
|
|
2238
|
+
__selftest: {
|
|
2239
|
+
GateShell,
|
|
2240
|
+
GateView,
|
|
2241
|
+
TabBar,
|
|
2242
|
+
SHELL_TABS,
|
|
2243
|
+
PROFILE_PLAIN,
|
|
2244
|
+
ProfilesPage,
|
|
2245
|
+
ProfilesEditor,
|
|
2246
|
+
AuditPage,
|
|
2247
|
+
GateIcon,
|
|
2248
|
+
Segmented,
|
|
2249
|
+
ObjectRow,
|
|
2250
|
+
GroupCard,
|
|
2251
|
+
PANEL_ID,
|
|
2252
|
+
PANEL_ORDER,
|
|
2253
|
+
SessionGateButton,
|
|
2254
|
+
sessionModeText,
|
|
2255
|
+
profileMode,
|
|
2256
|
+
ModeStrip,
|
|
2257
|
+
PROFILE_MODE,
|
|
2258
|
+
SESSION_BUTTON_ID,
|
|
2259
|
+
SESSION_SLOT,
|
|
2260
|
+
},
|
|
1523
2261
|
}
|
|
1524
2262
|
},
|
|
1525
2263
|
})
|