@mrweicodes/dsh-permgate 1.5.1 → 1.5.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/client.js +44 -20
- package/index.js +67 -17
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -7,6 +7,9 @@ window.__ModuleLoader__.load({
|
|
|
7
7
|
let React = require("react");
|
|
8
8
|
|
|
9
9
|
const CSS = ".pg-modal { position: fixed; top: 16px; right: 16px; z-index: 9999; width: 420px; max-width: 92vw; max-height: 82vh; overflow-y: auto; background: var(--dsw-alias-bg-overlay, #ffffff); color: var(--dsw-alias-label-primary, #24292f); border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.4)); border-radius: 10px; box-shadow: 0 10px 36px rgba(0,0,0,0.32); padding: 14px 16px; font-family: system-ui, -apple-system, \"Segoe UI\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif; } .pg-modal-title { font-size: 14px; font-weight: 600; margin-bottom: 6px; } .pg-modal-req { font-size: 12px; margin-bottom: 4px; } .pg-modal-body { font-size: 12px; color: var(--dsw-alias-label-secondary, #57606a); margin-bottom: 8px; } .pg-intent { font-size: 12px; background: var(--dsw-alias-bg-layer-1, #f6f8fa); border: 1px solid var(--dsw-alias-border-l1, #d0d7de); border-left: 3px solid var(--dsw-alias-brand-primary, #1f6feb); border-radius: 6px; padding: 6px 8px; color: var(--dsw-alias-label-secondary, #57606a); margin-bottom: 6px; line-height: 1.5; word-break: break-word; } .pg-intent-tag { display: inline-block; font-weight: 700; color: var(--dsw-alias-brand-primary, #1f6feb); margin-right: 6px; font-size: 11px; } .pg-args { font-size: 11px; background: var(--dsw-alias-bg-layer-1, #f6f8fa); border: 1px solid var(--dsw-alias-border-l1, #d0d7de); border-left: 3px solid rgba(128,128,128,0.65); border-radius: 6px; padding: 5px 8px; color: var(--dsw-alias-label-secondary, #57606a); margin-bottom: 8px; line-height: 1.5; word-break: break-all; font-family: ui-monospace, Consolas, monospace; } .pg-args-tag { display: inline-block; font-weight: 700; color: rgba(128,128,128,0.95); margin-right: 6px; font-size: 11px; font-family: system-ui, -apple-system, \"Segoe UI\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif; } .pg-args-row { display: flex; gap: 6px; padding: 1px 0; } .pg-args-label { font-weight: 600; flex: 0 0 auto; } .pg-args-val { word-break: break-all; flex: 1; } .pg-cand-head { font-size: 12px; font-weight: 600; margin: 6px 0 4px; } .pg-cand-hint { font-size: 11px; color: var(--dsw-alias-label-secondary, #8c959f); margin-bottom: 4px; } .pg-cand { display: flex; align-items: center; gap: 6px; padding: 4px 0; border-bottom: 1px solid var(--dsw-alias-border-l1, rgba(128,128,128,0.2)); } .pg-cand-label { flex: 1; font-family: ui-monospace, Consolas, monospace; font-size: 11px; word-break: break-all; } .pg-radio { display: inline-flex; align-items: center; gap: 3px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.4)); background: transparent; color: var(--dsw-alias-label-secondary, #57606a); font-size: 11px; font-family: inherit; cursor: pointer; transition: all 0.15s ease; } .pg-radio:hover { border-color: var(--dsw-alias-brand-primary, #1f6feb); } .pg-radio-allow { border-color: var(--dsw-alias-state-success-primary, #1a7f37); color: var(--dsw-alias-state-success-primary, #1a7f37); background: rgba(26, 127, 55, 0.12); font-weight: 600; } .pg-radio-deny { border-color: var(--dsw-alias-state-error-primary, #cf222e); color: var(--dsw-alias-state-error-primary, #cf222e); background: rgba(207, 34, 46, 0.12); font-weight: 600; } .pg-footer { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; } .pg-action { display: inline-flex; align-items: center; padding: 5px 16px; border-radius: 6px; border: 1px solid; font-size: 12px; font-family: inherit; font-weight: 600; cursor: pointer; transition: filter 0.15s ease, box-shadow 0.15s ease, transform 0.05s ease; } .pg-action:hover { filter: brightness(1.07); box-shadow: 0 2px 8px rgba(0,0,0,0.18); } .pg-action:active { transform: translateY(1px); box-shadow: none; } .pg-action:disabled { opacity: 0.5; cursor: not-allowed; filter: none; box-shadow: none; } .pg-action-allow { border-color: rgba(26, 127, 55, 0.45); color: #1a7f37; background: rgba(26, 127, 55, 0.12); } @supports (color: color-mix(in srgb, red 10%, blue)) { .pg-action-allow { border-color: color-mix(in srgb, var(--dsw-alias-state-success-primary, #1a7f37) 45%, transparent); color: var(--dsw-alias-state-success-primary, #1a7f37); background: color-mix(in srgb, var(--dsw-alias-state-success-primary, #1a7f37) 12%, transparent); } } .pg-action-deny { border-color: rgba(207, 34, 46, 0.45); color: #cf222e; background: rgba(207, 34, 46, 0.12); } @supports (color: color-mix(in srgb, red 10%, blue)) { .pg-action-deny { border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary, #cf222e) 45%, transparent); color: var(--dsw-alias-state-error-primary, #cf222e); background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #cf222e) 12%, transparent); } } .pg-btn { padding: 4px 10px; border-radius: 6px; border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.4)); background: transparent; color: var(--dsw-alias-label-primary, #24292f); cursor: pointer; font-size: 13px; font-family: inherit; transition: border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease, transform 0.05s ease; } .pg-btn:hover { border-color: var(--dsw-alias-brand-primary, #1f6feb); background: rgba(31, 111, 235, 0.08); } .pg-btn:active { transform: translateY(1px); } .pg-btn:disabled { opacity: 0.5; cursor: not-allowed; } .pg-btn-danger:hover { border-color: var(--dsw-alias-state-error-primary, #cf222e); color: var(--dsw-alias-state-error-primary, #cf222e); background: rgba(207, 34, 46, 0.08); } .pg-btn-on { border-color: var(--dsw-alias-state-success-primary, #1a7f37); color: var(--dsw-alias-state-success-primary, #1a7f37); background: rgba(26, 127, 55, 0.12); font-weight: 600; } .pg-btn-on:hover { border-color: var(--dsw-alias-state-success-primary, #1a7f37); color: var(--dsw-alias-state-success-primary, #1a7f37); background: rgba(26, 127, 55, 0.2); } .pg-btn-off { border-color: var(--dsw-alias-state-error-primary, #cf222e); color: var(--dsw-alias-state-error-primary, #cf222e); background: rgba(207, 34, 46, 0.1); font-weight: 600; } .pg-btn-off:hover { border-color: var(--dsw-alias-state-error-primary, #cf222e); color: var(--dsw-alias-state-error-primary, #cf222e); background: rgba(207, 34, 46, 0.16); } .pg-btn-confirm { border-color: var(--dsw-alias-state-error-primary, #cf222e); color: #ffffff; background: var(--dsw-alias-state-error-primary, #cf222e); } .pg-btn-confirm:hover { border-color: var(--dsw-alias-state-error-primary, #cf222e); color: #ffffff; background: var(--dsw-alias-state-error-primary, #cf222e); filter: brightness(1.12); } .pg-field { padding: 4px 8px; border-radius: 6px; border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.4)); background: var(--dsw-alias-bg-layer-1, #f6f8fa); color: var(--dsw-alias-label-primary, #24292f); color-scheme: light dark; font-size: 13px; font-family: inherit; transition: border-color 0.15s ease, box-shadow 0.15s ease; } .pg-field:hover { border-color: var(--dsw-alias-brand-primary, #1f6feb); } .pg-field:focus { outline: none; border-color: var(--dsw-alias-brand-primary, #1f6feb); box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.22); } .pg-field:disabled { opacity: 0.5; cursor: not-allowed; } .pg-field option { color: var(--dsw-alias-label-primary, #24292f); background: var(--dsw-alias-bg-overlay, #ffffff); } .pg-tab { padding: 6px 16px; border-radius: 6px 6px 0 0; border: none; border-bottom: 2px solid transparent; background: transparent; color: var(--dsw-alias-label-primary, #24292f); cursor: pointer; font-size: 13px; font-family: inherit; margin-right: 4px; transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease; } .pg-tab:hover { background: rgba(128, 128, 128, 0.12); } .pg-tab-on { border-bottom-color: var(--dsw-alias-brand-primary, #1f6feb); color: var(--dsw-alias-brand-primary, #1f6feb); font-weight: 600; }";
|
|
10
|
+
// radio 禁用态的视觉提示:主 CSS 里 .pg-radio 没有 :disabled 规则,
|
|
11
|
+
// 原先靠内联 style 兜底,去掉后禁用按钮看起来仍可点。
|
|
12
|
+
const RADIO_DISABLED_CSS = '.pg-radio:disabled { opacity: 0.5; cursor: not-allowed; }';
|
|
10
13
|
|
|
11
14
|
const DIFF_CSS = '.pg-link { display: inline-flex; align-items: center; gap: 4px; background: var(--dsw-alias-bg-layer-1, #f0f2f5); color: var(--dsw-alias-label-primary, #24292f); border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.45)); border-radius: 6px; padding: 3px 12px; cursor: pointer; font-size: 12px; font-weight: 500; margin-bottom: 6px; } .pg-link:hover { border-color: #1f6feb; color: #1f6feb; }';
|
|
12
15
|
|
|
@@ -695,7 +698,7 @@ window.__ModuleLoader__.load({
|
|
|
695
698
|
'app.intent': '意图说明',
|
|
696
699
|
'app.args': '参数',
|
|
697
700
|
'app.cand.head': '添加项目规则(当前项目)',
|
|
698
|
-
'app.cand.hint': '
|
|
701
|
+
'app.cand.hint': '点亮条目后点「允许 / 拒绝」即写入当前项目例外。',
|
|
699
702
|
'app.allow': '允许',
|
|
700
703
|
'app.deny': '拒绝',
|
|
701
704
|
'app.allowOne': '√ 允许',
|
|
@@ -873,7 +876,7 @@ window.__ModuleLoader__.load({
|
|
|
873
876
|
'app.intent': 'Intent',
|
|
874
877
|
'app.args': 'Arguments',
|
|
875
878
|
'app.cand.head': 'Add project rule (current project)',
|
|
876
|
-
'app.cand.hint': 'Toggle items
|
|
879
|
+
'app.cand.hint': 'Toggle items then click Allow / Deny to write them into the current project exceptions.',
|
|
877
880
|
'app.allow': 'Allow',
|
|
878
881
|
'app.deny': 'Deny',
|
|
879
882
|
'app.allowOne': '√ Allow',
|
|
@@ -1086,6 +1089,8 @@ window.__ModuleLoader__.load({
|
|
|
1086
1089
|
const [openArgs, setOpenArgs] = React.useState({});
|
|
1087
1090
|
const [denyText, setDenyText] = React.useState({});
|
|
1088
1091
|
const [denyMode, setDenyMode] = React.useState({});
|
|
1092
|
+
// 进入拒绝态时被临时收起的「允许此项」勾选(按审批 id 存),点「取消」时原样恢复
|
|
1093
|
+
const [denySaved, setDenySaved] = React.useState({});
|
|
1089
1094
|
const [, setTick] = React.useState(0);
|
|
1090
1095
|
useLocaleTick();
|
|
1091
1096
|
const poll = () => {
|
|
@@ -1107,11 +1112,9 @@ window.__ModuleLoader__.load({
|
|
|
1107
1112
|
for (const key of diffFetching) if (!aliveIds.has(key)) diffFetching.delete(key);
|
|
1108
1113
|
for (const p of pending) {
|
|
1109
1114
|
if (!p.hasDiff || diffCache.has(p.id) || diffFetching.has(p.id)) continue
|
|
1110
|
-
|
|
1111
|
-
const open = openDetail[p.id] === undefined ? true :
|
|
1115
|
+
// 详情默认展开(含图片:读图时用户最想看的就是那张图),展开即预取
|
|
1116
|
+
const open = openDetail[p.id] === undefined ? true : openDetail[p.id] === true
|
|
1112
1117
|
if (!open) continue
|
|
1113
|
-
// 图片详情是整图 data URL(体积大):不做自动预取,只有用户主动展开该条时才拉取
|
|
1114
|
-
if (p.imagePreview && !userOpened) continue
|
|
1115
1118
|
diffFetching.add(p.id)
|
|
1116
1119
|
schedulePrismIdle()
|
|
1117
1120
|
call('permgate:file-diff', { id: p.id }).then((r) => {
|
|
@@ -1146,26 +1149,43 @@ window.__ModuleLoader__.load({
|
|
|
1146
1149
|
const confirmDeny = (p) => {
|
|
1147
1150
|
const reason = (denyText[p.id] || '').trim();
|
|
1148
1151
|
setDenyMode(Object.assign({}, denyMode, { [p.id]: false }));
|
|
1152
|
+
const rest = Object.assign({}, denySaved);
|
|
1153
|
+
delete rest[p.id];
|
|
1154
|
+
setDenySaved(rest);
|
|
1149
1155
|
decide(p.id, 'deny', pickedDenyRules(p), reason || undefined);
|
|
1150
1156
|
};
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1157
|
+
// 取消拒绝:把进入前收起的「允许此项」勾选原样放回去
|
|
1158
|
+
const cancelDeny = (p) => {
|
|
1159
|
+
const snap = denySaved[p.id];
|
|
1160
|
+
if (snap && Object.keys(snap).length) setSel(Object.assign({}, sel, snap));
|
|
1161
|
+
const rest = Object.assign({}, denySaved);
|
|
1162
|
+
delete rest[p.id];
|
|
1163
|
+
setDenySaved(rest);
|
|
1164
|
+
setDenyMode(Object.assign({}, denyMode, { [p.id]: false }));
|
|
1165
|
+
};
|
|
1166
|
+
// 进入拒绝态:立刻把「允许此项」的勾选收起来(视觉上就是取消了),并记下原选择供「取消」恢复。
|
|
1167
|
+
// 之前是「保留勾选 + 置灰变浅」,看起来仍像选中了允许,容易误读成「一边选允许一边提交拒绝」。
|
|
1168
|
+
const enterDeny = (p) => {
|
|
1169
|
+
const snap = {};
|
|
1170
|
+
for (const c of (p.candidates || [])) if (sel[c.id] === 'allow') snap[c.id] = 'allow';
|
|
1171
|
+
const next = Object.assign({}, sel);
|
|
1172
|
+
for (const id of Object.keys(snap)) delete next[id];
|
|
1173
|
+
setDenySaved(Object.assign({}, denySaved, { [p.id]: snap }));
|
|
1174
|
+
setSel(next);
|
|
1175
|
+
setDenyMode(Object.assign({}, denyMode, { [p.id]: true }));
|
|
1176
|
+
};
|
|
1155
1177
|
const radio = (p, c, v, label, cls) => {
|
|
1156
|
-
//
|
|
1157
|
-
|
|
1158
|
-
const muted = v === 'allow' && !!denyMode[p.id];
|
|
1178
|
+
// 拒绝态下「允许此项」已被收起,这里只做禁止再点(避免又勾回来和拒绝态自相矛盾)
|
|
1179
|
+
const blocked = v === 'allow' && !!denyMode[p.id];
|
|
1159
1180
|
return React.createElement('button', {
|
|
1160
1181
|
className: 'pg-radio' + (sel[c.id] === v ? ' ' + cls : ''),
|
|
1161
|
-
disabled: busyId === p.id ||
|
|
1162
|
-
style: muted ? { opacity: 0.45, cursor: 'not-allowed' } : undefined,
|
|
1182
|
+
disabled: busyId === p.id || blocked,
|
|
1163
1183
|
onClick: () => pick(c.id, v),
|
|
1164
1184
|
}, label);
|
|
1165
1185
|
};
|
|
1166
1186
|
// 编辑/写入审批(有 diff):详情默认展开、参数默认收起;无 diff 时参数照常显示
|
|
1167
|
-
//
|
|
1168
|
-
const detailOpen = (p) => (openDetail[p.id] === undefined ?
|
|
1187
|
+
// 有 diff 的审批(含图片)详情默认展开:读图时那张缩略图就是用户最想看的东西
|
|
1188
|
+
const detailOpen = (p) => (openDetail[p.id] === undefined ? !!p.hasDiff : openDetail[p.id]);
|
|
1169
1189
|
const argsOpen = (p) => (openArgs[p.id] === undefined ? !p.hasDiff : openArgs[p.id]);
|
|
1170
1190
|
const toggle = (map, setMap, p, v) => setMap(Object.assign({}, map, { [p.id]: v }));
|
|
1171
1191
|
// 点击文件名 → 打开右侧对比抽屉(父组件 OverlayRoot 持有 pin 状态)
|
|
@@ -1213,8 +1233,12 @@ window.__ModuleLoader__.load({
|
|
|
1213
1233
|
(p.candidates || []).length ? React.createElement('div', null,
|
|
1214
1234
|
React.createElement('div', { className: 'pg-cand-head' }, T('app.cand.head')),
|
|
1215
1235
|
React.createElement('div', { className: 'pg-cand-hint' }, T('app.cand.hint')),
|
|
1216
|
-
(p.candidates || []).map((c) => React.createElement('div', { key: c.id, className: 'pg-cand' },
|
|
1217
|
-
|
|
1236
|
+
(p.candidates || []).map((c) => React.createElement('div', { key: c.id, className: 'pg-cand', style: { alignItems: 'flex-start' } },
|
|
1237
|
+
// 主行是路径本身,范围说明降级成下面的灰色小字(按钮与主行对齐)
|
|
1238
|
+
React.createElement('div', { style: { flex: 1, minWidth: 0 } },
|
|
1239
|
+
React.createElement('div', { className: 'pg-cand-label' }, c.label),
|
|
1240
|
+
c.hint ? React.createElement('div', { style: { fontSize: 11, color: 'rgba(128,128,128,0.85)', marginTop: 2, lineHeight: 1.4 } }, c.hint) : null,
|
|
1241
|
+
),
|
|
1218
1242
|
radio(p, c, 'allow', T('app.allowOne'), 'pg-radio-allow'),
|
|
1219
1243
|
radio(p, c, 'deny', T('app.denyOne'), 'pg-radio-deny'),
|
|
1220
1244
|
)),
|
|
@@ -2034,7 +2058,7 @@ window.__ModuleLoader__.load({
|
|
|
2034
2058
|
const tag = document.createElement('style');
|
|
2035
2059
|
tag.dataset.plugin = 'dsh-permgate';
|
|
2036
2060
|
tag.dataset.pluginCss = 'dsh-permgate';
|
|
2037
|
-
tag.textContent = CSS;
|
|
2061
|
+
tag.textContent = CSS + RADIO_DISABLED_CSS;
|
|
2038
2062
|
document.head.appendChild(tag);
|
|
2039
2063
|
ctx.effect(() => () => { try { tag.remove(); } catch (e) {} });
|
|
2040
2064
|
}
|
package/index.js
CHANGED
|
@@ -244,6 +244,30 @@ export default {
|
|
|
244
244
|
return String(p).replace(/\\/g, '/').replace(/\/+$/, '')
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
// 路径规范化单点:相对路径先按 root 绝对化,再折叠 .. 与重复分隔符。
|
|
248
|
+
// glob/norm/globToRegExp 都不折叠 ..,仅规范化写入侧会让例外永不命中参数原文。
|
|
249
|
+
function normAbsPath(p) {
|
|
250
|
+
const s = norm(p)
|
|
251
|
+
if (!s) return s
|
|
252
|
+
// glob 不是文件路径:绝对化会改变匹配范围(`**/*.env` 会被拼成 `G:/MCP/**/*.env`,
|
|
253
|
+
// 从「任意目录」缩成「仅工作区内」),故只做斜杠归一,不绝对化也不折叠 ..。
|
|
254
|
+
// 与 hasGlobMeta 同口径:只认 * 与 ?([ ] 在 globToRegExp 里是字面量,不是通配符)。
|
|
255
|
+
if (/[*?]/.test(s)) return s
|
|
256
|
+
// file:// 等 URL 形态不是文件系统路径,原样返回(与 resolveArgPath 同口径)
|
|
257
|
+
if (s.indexOf('://') !== -1) return s
|
|
258
|
+
const isAbs = s.indexOf('/') === 0 || /^[a-zA-Z]:/.test(s)
|
|
259
|
+
const abs = isAbs ? s : (root ? norm(root + '/' + s) : '')
|
|
260
|
+
if (!abs) return s
|
|
261
|
+
return norm(pathResolve(/^[a-zA-Z]:$/.test(abs) ? abs + '/' : abs))
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 路径同一性键:判重、写入去重、匹配三处共用同一口径,保证「同一条例外」在三处同答案。
|
|
265
|
+
// 必须基于 normAbsPath(绝对化 + 折叠 ..)而非裸 normPathKey,否则候选写入的规范值
|
|
266
|
+
// 与面板/工具入口写入的相对路径或含 .. 原文会被当成两条不同例外。
|
|
267
|
+
function pathKey(p) {
|
|
268
|
+
return normPathKey(normAbsPath(p))
|
|
269
|
+
}
|
|
270
|
+
|
|
247
271
|
function safeJson(v) {
|
|
248
272
|
try { return JSON.stringify(v) } catch (e) { return '' }
|
|
249
273
|
}
|
|
@@ -847,7 +871,9 @@ export default {
|
|
|
847
871
|
}
|
|
848
872
|
|
|
849
873
|
function matchException(r, value, kind) {
|
|
850
|
-
|
|
874
|
+
// 路径两侧统一走 normAbsPath:写入值可能来自候选(规范绝对路径),而 value 是
|
|
875
|
+
// 工具参数原文(可能是相对路径或含 ..)——只规范化写入侧会让例外永不命中。
|
|
876
|
+
if (kind === 'path') return matchGlob(normAbsPath(r.path), normAbsPath(value))
|
|
851
877
|
return matchCommand(r.match, value)
|
|
852
878
|
}
|
|
853
879
|
|
|
@@ -2271,7 +2297,8 @@ export default {
|
|
|
2271
2297
|
}
|
|
2272
2298
|
if (kind === 'path' && catKey && EXC_CATS.indexOf(catKey) !== -1) {
|
|
2273
2299
|
const cat = proj[catKey]
|
|
2274
|
-
|
|
2300
|
+
// 与 pathKey 同口径:相对路径、含 .. 、斜杠与大小写写法都归一到同一条
|
|
2301
|
+
return !!(cat && Array.isArray(cat.exceptions) && cat.exceptions.some((r) => pathKey(r.path) === pathKey(value)))
|
|
2275
2302
|
}
|
|
2276
2303
|
if (kind === 'tool') {
|
|
2277
2304
|
return !!(Array.isArray(proj.custom) && proj.custom.some((r) => r.tool === value))
|
|
@@ -2393,7 +2420,9 @@ export default {
|
|
|
2393
2420
|
|
|
2394
2421
|
function buildCandidates(entry) {
|
|
2395
2422
|
const out = []
|
|
2396
|
-
|
|
2423
|
+
// hint:候选行下方的灰色小字(说明这条会放开什么),与 label(主文案,通常就是路径)分开下发,
|
|
2424
|
+
// 由客户端排版成「路径 + 小字 + 按钮」,避免把说明塞进 label 里挤成一大段
|
|
2425
|
+
const push = (label, value, kind, writes, hint) => out.push({ id: 'c' + Math.random().toString(36).slice(2, 8), label, value, kind, writes: Array.isArray(writes) ? writes : [{ cat: entry.cat, kind, value }], ...(hint ? { hint } : {}) })
|
|
2397
2426
|
const t = (zh, en) => (uiLang === 'en' ? en : zh)
|
|
2398
2427
|
if (entry.kind === 'command' && entry.value) {
|
|
2399
2428
|
const parts = splitCommandSegments(String(entry.value))
|
|
@@ -2420,25 +2449,44 @@ export default {
|
|
|
2420
2449
|
const catKey = entry.toolCat && EXC_CATS.indexOf(entry.toolCat) !== -1 ? entry.toolCat : entry.cat
|
|
2421
2450
|
const outsideHere = !!(catKey && catKey !== 'directory' && isOutside(entry.value, root))
|
|
2422
2451
|
// 候选文案用的分类名:说明这条候选会放开「哪一类操作」,避免文案与落盘的例外分类不符
|
|
2423
|
-
|
|
2452
|
+
// 标签与设置面板的分类名保持一致(edit 就叫「编辑文件」,不再写「写入/编辑」)
|
|
2453
|
+
const KIND_LABEL = { read: ['读取文件', 'file reads'], image: ['读取图片', 'image reads'], edit: ['编辑文件', 'file edits'], undo: ['撤销操作', 'undo actions'] }
|
|
2424
2454
|
const kindLabel = KIND_LABEL[catKey] || ['此类操作', 'this kind of operation']
|
|
2425
2455
|
if (outsideHere) {
|
|
2426
2456
|
// 工作区外路径:两道闸各给一条候选。「整个目录」写 directory 与该分类的目录 glob ——
|
|
2427
2457
|
// 点一次后该类操作在该目录下都不再询问;「仅此文件」写自身分类例外 + directory 的精确路径例外。
|
|
2428
|
-
//
|
|
2429
|
-
//
|
|
2458
|
+
// 路径先规范化为绝对路径(折叠 .. 与重复分隔符):判重与落盘值统一基于它。
|
|
2459
|
+
// 含通配符的原文不做折叠——pathResolve 会把 `*` 当普通目录名、被其后的 .. 吃掉
|
|
2460
|
+
// (实测 C:/x/*/../y.txt → C:/x/y.txt),使通配路径被误当成精确路径生成候选。
|
|
2461
|
+
const absVal = hasGlobMeta(entry.value) ? norm(entry.value) : normAbsPath(entry.value)
|
|
2462
|
+
// 守卫按**原始值**判定:absVal 已折叠 ..,对它判 hasParentSeg 恒为 false,等于没有守卫。
|
|
2463
|
+
// 含 .. 的原文不给目录 glob——dirGlob 取父目录,`..` 可拼出覆盖整个盘根的 `G:/*`。
|
|
2430
2464
|
const globSafe = !hasGlobMeta(entry.value) && !hasParentSeg(entry.value)
|
|
2431
|
-
const glob = globSafe ? dirGlob(
|
|
2465
|
+
const glob = globSafe ? dirGlob(absVal) : ''
|
|
2432
2466
|
const hasDirGlob = globSafe && alreadyInProject(glob, 'path', 'directory')
|
|
2433
2467
|
const hasKindGlob = globSafe && alreadyInProject(glob, 'path', catKey)
|
|
2434
2468
|
if (globSafe && (!hasDirGlob || !hasKindGlob)) {
|
|
2435
|
-
|
|
2469
|
+
// label 就是路径本身(弹窗里最该被看清的东西),范围说明走 hint 小字
|
|
2470
|
+
push(glob, glob, 'path', [{ cat: 'directory', kind: 'path', value: glob }, { cat: catKey, kind: 'path', value: glob }], t('工作区外访问目录 + ' + kindLabel[0] + '权限', 'Outside workspace · directory + ' + kindLabel[1]))
|
|
2436
2471
|
}
|
|
2437
|
-
|
|
2438
|
-
|
|
2472
|
+
const fileVal = absVal
|
|
2473
|
+
// 判重按该候选实际要写的**全部分类**判定:它同时写 catKey 与 directory 两条例外,
|
|
2474
|
+
// 只要其中一道闸还没有等价例外就得给出候选——否则用户补不上那道闸,只能改选
|
|
2475
|
+
// 「整个目录」,精确授权被迫放大为目录级授权。
|
|
2476
|
+
const fileWrites = [{ cat: catKey, kind: 'path', value: fileVal }, { cat: 'directory', kind: 'path', value: fileVal }]
|
|
2477
|
+
// 按候选实际要写的两个分类(catKey + directory)各自判重:任一道闸缺等价例外即给候选。
|
|
2478
|
+
// 注:能走到这里就说明至少一道闸没命中等价例外(两道都命中时 resolveCategory 直接
|
|
2479
|
+
// 返回 allow/deny、不会弹窗),因此 covered 当前恒为 false——保留它是防御性冗余,
|
|
2480
|
+
// 用来表达「两条写入目标各自判重」的语义,避免未来弹窗流程变更后候选被误隐藏。
|
|
2481
|
+
const covered = fileWrites.every((w) => alreadyInProject(w.value, 'path', w.cat))
|
|
2482
|
+
// 文件候选只要求原文无通配符:含 .. 的原文已被规范化成精确绝对路径,可以安全给候选
|
|
2483
|
+
if (!hasGlobMeta(entry.value) && !covered) {
|
|
2484
|
+
push(fileVal, fileVal, 'path', fileWrites, t('工作区外访问文件 + ' + kindLabel[0] + '权限', 'Outside workspace · file + ' + kindLabel[1]))
|
|
2439
2485
|
}
|
|
2440
2486
|
} else if (!hasGlobMeta(entry.value)) {
|
|
2441
|
-
|
|
2487
|
+
// 工作区内同样走规范化单点,避免同一份配置里两种路径写法并存
|
|
2488
|
+
const absVal = normAbsPath(entry.value)
|
|
2489
|
+
if (!alreadyInProject(absVal, 'path', entry.cat)) push(absVal, absVal, 'path')
|
|
2442
2490
|
}
|
|
2443
2491
|
}
|
|
2444
2492
|
// 其余分类无「例外」候选:快捷工具(web_search/skill 等)走 quickTools 设置;
|
|
@@ -2468,10 +2516,9 @@ export default {
|
|
|
2468
2516
|
// 审批发起时的项目根:root 是跨会话共享的闭包变量,随后可能被其他会话覆盖,
|
|
2469
2517
|
// 打相对路径/对比/打开文件必须用发起会话自己的根
|
|
2470
2518
|
projRoot: root || null,
|
|
2471
|
-
// 编辑/写入,或带文件路径的读取 →
|
|
2472
|
-
// (写类=diff,读类=窗口化内容;图片是整图 data URL
|
|
2519
|
+
// 编辑/写入,或带文件路径的读取 → 弹窗「详情」默认展开并自动取数据
|
|
2520
|
+
// (写类=diff,读类=窗口化内容;图片是整图 data URL,受 IMAGE_MAX_BYTES/像素上限约束)
|
|
2473
2521
|
hasDiff: isPreviewableFileTool(exec.name, exec.arguments) && !!pathArg(exec.arguments),
|
|
2474
|
-
imagePreview: isFileImage(exec.name) === true,
|
|
2475
2522
|
toolCat: pathToolCat(exec.name, exec.arguments),
|
|
2476
2523
|
// str_replace_editor 的内核在审批发起时定下(insert 的 insert_line 语义随内核相反),
|
|
2477
2524
|
// 详情预览按发起时的实际内核解释,避免中途判别漂移
|
|
@@ -2518,7 +2565,9 @@ export default {
|
|
|
2518
2565
|
if (kind === 'path' && cat && cat !== 'command' && EXC_CATS.indexOf(cat) !== -1) {
|
|
2519
2566
|
const c = block[cat] || freshCategory(cat, target === 'project')
|
|
2520
2567
|
if (!c.exceptions) c.exceptions = []
|
|
2521
|
-
|
|
2568
|
+
// 去重与 alreadyInProject / matchException 同口径(pathKey:绝对化 + 折叠 .. + 大小写/斜杠归一):
|
|
2569
|
+
// 否则候选写规范绝对路径、面板写相对或含 .. 原文时会各存一条指向同一路径的例外。
|
|
2570
|
+
const idx = c.exceptions.findIndex((r) => pathKey(r.path) === pathKey(value) && r.action === decision)
|
|
2522
2571
|
if (idx !== -1) {
|
|
2523
2572
|
// 命中既有同向条目:提到数组头部,否则它会被前面的反向旧条目遮蔽(resolveCategory 只取首个匹配),
|
|
2524
2573
|
// 用户的决定等于被静默丢弃;带新理由时一并回写(reason 已在入口按 deny + trim + 200 规范化)。
|
|
@@ -2528,7 +2577,8 @@ export default {
|
|
|
2528
2577
|
block[cat] = c
|
|
2529
2578
|
return hit
|
|
2530
2579
|
}
|
|
2531
|
-
|
|
2580
|
+
// 落盘统一存规范化路径,与判重/匹配口径一致
|
|
2581
|
+
const item = build({ path: normAbsPath(value) })
|
|
2532
2582
|
c.exceptions.unshift(item)
|
|
2533
2583
|
block[cat] = c
|
|
2534
2584
|
return item
|
|
@@ -2993,7 +3043,7 @@ export default {
|
|
|
2993
3043
|
const argsPreview = e.argsJson && e.argsJson.length > 160 ? e.argsJson.slice(0, 160) + '…' : (e.argsJson || '')
|
|
2994
3044
|
const reason = typeof e.reason === 'string' ? e.reason : L(e.reason, lang)
|
|
2995
3045
|
const intent = typeof e.intent === 'string' ? e.intent : L(e.intent, lang)
|
|
2996
|
-
out.push({ id: e.id, tool: e.tool, reason, ts: e.ts, args: argsPreview, intent, candidates: e.candidates || [], argLines: e.argLines || [], hasDiff: e.hasDiff === true
|
|
3046
|
+
out.push({ id: e.id, tool: e.tool, reason, ts: e.ts, args: argsPreview, intent, candidates: e.candidates || [], argLines: e.argLines || [], hasDiff: e.hasDiff === true })
|
|
2997
3047
|
}
|
|
2998
3048
|
return json(res, out)
|
|
2999
3049
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrweicodes/dsh-permgate",
|
|
3
3
|
"description": "Permission gateway for DSH (DeepSeek Harness): reviews tool calls per category (directory/command/read/image/edit/undo/subagent/doom loop) with global/project config, exceptions, quick-tool defaults, custom rules, a fallback policy for unmatched calls, bilingual approval modal and sandbox-upgrade flow. Config persisted at $DSH_HOME/dsh-permgate/config.json.",
|
|
4
|
-
"version": "1.5.
|
|
4
|
+
"version": "1.5.2",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/MrWeiCodes/dsh-permgate.git"
|