@t4r71/dsh-dual-axis-ui 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 +78 -0
- package/lib/client.js +3401 -0
- package/lib/index.js +13 -0
- package/lib/types/client/index.d.ts +17 -0
- package/lib/types/client/permission/PermissionCustomEditor.d.ts +65 -0
- package/lib/types/client/permission/PermissionRow.d.ts +39 -0
- package/lib/types/client/permission/PermissionSelect.d.ts +89 -0
- package/lib/types/client/permission/axis-editor.d.ts +263 -0
- package/lib/types/client/permission/catalog.d.ts +62 -0
- package/lib/types/client/permission/index.d.ts +54 -0
- package/lib/types/client/permission/locales.d.ts +161 -0
- package/lib/types/client/permission/presentation.d.ts +167 -0
- package/lib/types/client/permission/session-axes-data.d.ts +108 -0
- package/lib/types/client/permission/session-axes-seed.d.ts +60 -0
- package/lib/types/client/permission/session-axes.d.ts +68 -0
- package/lib/types/client/permission/settings-store.d.ts +77 -0
- package/lib/types/client/row/axes.d.ts +112 -0
- package/lib/types/client/row/groups.d.ts +150 -0
- package/lib/types/client/row/index.d.ts +104 -0
- package/lib/types/client/row/locales.d.ts +59 -0
- package/lib/types/index.d.ts +10 -0
- package/package.json +111 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,3401 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@t4r71/dsh-dual-axis-ui",
|
|
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_store = require("@deepseek-ai/dsh-client-store");
|
|
9
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
10
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
11
|
+
//#region lib/types/client/permission/catalog.js
|
|
12
|
+
/** Identity-stable process permission catalog shared by both selection surfaces. */
|
|
13
|
+
/** One latest-result-wins catalog reader for the whole browser process. */
|
|
14
|
+
var PermissionCatalogDirectory = class {
|
|
15
|
+
ctx;
|
|
16
|
+
/** Complete snapshot consumed by both the slash popup and composer seat. */
|
|
17
|
+
store = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)({ value: null });
|
|
18
|
+
/**
|
|
19
|
+
* One tick per invalidation (a catalog notification or a connection-generation
|
|
20
|
+
* change), published before the replacement read starts. Consumers that must
|
|
21
|
+
* drop displayed options subscribe here instead of to {@link store}, whose
|
|
22
|
+
* publications also settle a read a displayed surface is waiting for.
|
|
23
|
+
*/
|
|
24
|
+
invalidations = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)({ count: 0 });
|
|
25
|
+
connection;
|
|
26
|
+
stopCatalog;
|
|
27
|
+
stopGeneration;
|
|
28
|
+
generationId;
|
|
29
|
+
initialized = false;
|
|
30
|
+
epoch = 0;
|
|
31
|
+
pending;
|
|
32
|
+
failure = /* @__PURE__ */ new Error("permission catalog has no complete value");
|
|
33
|
+
disposed = false;
|
|
34
|
+
/**
|
|
35
|
+
* Subscribe to both invalidation sources before the first read, closing the
|
|
36
|
+
* install/read race.
|
|
37
|
+
* @param ctx - root Client context carrying Remote and Connection.
|
|
38
|
+
*/
|
|
39
|
+
constructor(ctx) {
|
|
40
|
+
this.ctx = ctx;
|
|
41
|
+
this.connection = ctx.get("connection");
|
|
42
|
+
this.stopCatalog = ctx.remote.$on("permission-presets/catalog-changed", () => {
|
|
43
|
+
this.invalidate();
|
|
44
|
+
this.refresh();
|
|
45
|
+
});
|
|
46
|
+
this.stopGeneration = this.connection.generation.subscribe(() => {
|
|
47
|
+
this.syncGeneration();
|
|
48
|
+
});
|
|
49
|
+
this.syncGeneration();
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Publish one invalidation tick for consumers holding displayed options.
|
|
53
|
+
* Neither caller can run after disposal: `dispose()` unsubscribes the
|
|
54
|
+
* catalog-changed listener, and `syncGeneration()` returns early when the
|
|
55
|
+
* directory is disposed.
|
|
56
|
+
*/
|
|
57
|
+
invalidate() {
|
|
58
|
+
this.invalidations.set({ count: this.invalidations.getSnapshot().count + 1 });
|
|
59
|
+
}
|
|
60
|
+
/** Force a fresh complete read for the active connection generation. */
|
|
61
|
+
refresh() {
|
|
62
|
+
if (this.disposed) return;
|
|
63
|
+
const generationId = this.connection.generation.getSnapshot()?.id;
|
|
64
|
+
if (generationId === void 0) return;
|
|
65
|
+
if (generationId !== this.generationId) {
|
|
66
|
+
this.syncGeneration();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
this.startRead(generationId);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Resolve a complete current-generation catalog for an imperative popup
|
|
73
|
+
* open. An active refresh settles before a retained value can be reused.
|
|
74
|
+
* @returns The active Host generation's complete permission catalog.
|
|
75
|
+
*/
|
|
76
|
+
async load() {
|
|
77
|
+
if (this.pending === void 0 && this.store.getSnapshot().value === null) this.refresh();
|
|
78
|
+
while (!this.disposed) {
|
|
79
|
+
const generationId = this.connection.generation.getSnapshot()?.id;
|
|
80
|
+
if (generationId === void 0) throw new Error("permission catalog has no active Host connection");
|
|
81
|
+
if (generationId !== this.generationId) this.syncGeneration();
|
|
82
|
+
const pending = this.pending;
|
|
83
|
+
if (pending !== void 0) {
|
|
84
|
+
await pending;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const state = this.store.getSnapshot();
|
|
88
|
+
if (state.value !== null) return state.value;
|
|
89
|
+
throw this.failure;
|
|
90
|
+
}
|
|
91
|
+
throw new Error("permission catalog directory is disposed");
|
|
92
|
+
}
|
|
93
|
+
/** Stop subscriptions and revoke every late settlement's write access. */
|
|
94
|
+
dispose() {
|
|
95
|
+
if (this.disposed) return;
|
|
96
|
+
this.disposed = true;
|
|
97
|
+
++this.epoch;
|
|
98
|
+
this.pending = void 0;
|
|
99
|
+
this.stopGeneration();
|
|
100
|
+
this.stopCatalog();
|
|
101
|
+
}
|
|
102
|
+
/** Observe generation loss/replacement and hard-clear the old Host value. */
|
|
103
|
+
syncGeneration() {
|
|
104
|
+
if (this.disposed) return;
|
|
105
|
+
const generationId = this.connection.generation.getSnapshot()?.id;
|
|
106
|
+
if (this.initialized && generationId === this.generationId) return;
|
|
107
|
+
if (this.initialized) this.invalidate();
|
|
108
|
+
this.initialized = true;
|
|
109
|
+
this.generationId = generationId;
|
|
110
|
+
++this.epoch;
|
|
111
|
+
this.pending = void 0;
|
|
112
|
+
this.failure = /* @__PURE__ */ new Error("permission catalog has no complete value");
|
|
113
|
+
this.store.set({ value: null });
|
|
114
|
+
if (generationId !== void 0) this.startRead(generationId);
|
|
115
|
+
}
|
|
116
|
+
/** Start one independent read; the newest epoch in the same generation wins. */
|
|
117
|
+
startRead(generationId) {
|
|
118
|
+
const epoch = ++this.epoch;
|
|
119
|
+
this.failure = /* @__PURE__ */ new Error("permission catalog has no complete value");
|
|
120
|
+
const operation = this.ctx.remote.permissionPresets.catalog().then((result) => {
|
|
121
|
+
if (!result.ok) throw new Error(`${result.error.code}: ${result.error.message}`);
|
|
122
|
+
if (!this.accepts(epoch, generationId)) return;
|
|
123
|
+
this.store.set({ value: result.value });
|
|
124
|
+
}).catch((error) => {
|
|
125
|
+
if (!this.accepts(epoch, generationId)) return;
|
|
126
|
+
this.failure = error instanceof Error ? error : new Error(String(error));
|
|
127
|
+
this.store.set({ value: null });
|
|
128
|
+
}).finally(() => {
|
|
129
|
+
if (this.pending === operation) this.pending = void 0;
|
|
130
|
+
});
|
|
131
|
+
this.pending = operation;
|
|
132
|
+
}
|
|
133
|
+
/** Fence by disposal, refresh epoch, and the actual Connection generation. */
|
|
134
|
+
accepts(epoch, generationId) {
|
|
135
|
+
return !this.disposed && epoch === this.epoch && generationId === this.generationId && this.connection.generation.getSnapshot()?.id === generationId;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region ../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
|
|
140
|
+
function r(e) {
|
|
141
|
+
var t, f, n = "";
|
|
142
|
+
if ("string" == typeof e || "number" == typeof e) n += e;
|
|
143
|
+
else if ("object" == typeof e) if (Array.isArray(e)) {
|
|
144
|
+
var o = e.length;
|
|
145
|
+
for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
|
|
146
|
+
} else for (f in e) e[f] && (n && (n += " "), n += f);
|
|
147
|
+
return n;
|
|
148
|
+
}
|
|
149
|
+
function clsx() {
|
|
150
|
+
for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
|
|
151
|
+
return n;
|
|
152
|
+
}
|
|
153
|
+
//#endregion
|
|
154
|
+
//#region lib/types/client/permission/locales.js
|
|
155
|
+
/** `settings.permission` namespace dictionaries (the Permission row's copy). */
|
|
156
|
+
/** Locale namespace shared by both current-session permission pickers. */
|
|
157
|
+
const PERMISSION_ACCESS_NS = "permission.access";
|
|
158
|
+
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
159
|
+
const zh$1 = {
|
|
160
|
+
"title": "权限",
|
|
161
|
+
"description": "选择新会话的默认权限模式",
|
|
162
|
+
"loading": "加载中",
|
|
163
|
+
"unavailable": "不可用",
|
|
164
|
+
"preset.readOnly": "仅可查看",
|
|
165
|
+
"preset.workspaceWrite": "工作区内修改",
|
|
166
|
+
"preset.fullAccess": "完全权限"
|
|
167
|
+
};
|
|
168
|
+
/** English dictionary, checked complete against the zh key set. */
|
|
169
|
+
const en$1 = {
|
|
170
|
+
"title": "Permission",
|
|
171
|
+
"description": "Choose the default permission mode for new sessions",
|
|
172
|
+
"loading": "Loading",
|
|
173
|
+
"unavailable": "Unavailable",
|
|
174
|
+
"preset.readOnly": "Read Only",
|
|
175
|
+
"preset.workspaceWrite": "Workspace Write",
|
|
176
|
+
"preset.fullAccess": "Full access"
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* Simplified Chinese dictionary for the two current-session axis pickers. Each
|
|
180
|
+
* axis is a four-value dropdown (`deny / workspace / all / custom`), and the two
|
|
181
|
+
* dropdowns must stay unambiguous for assistive technology, for role-based
|
|
182
|
+
* locators, and for the eye: the two pickers sit side by side with no visible
|
|
183
|
+
* caption, so every value states its own axis in the copy itself. `custom` is the
|
|
184
|
+
* one shared value — "自定义" opens a path editor whose contents say which axis it
|
|
185
|
+
* belongs to, and the two pickers are already told apart by the other three.
|
|
186
|
+
*/
|
|
187
|
+
const accessZh = {
|
|
188
|
+
"axis.read.deny": "禁止读",
|
|
189
|
+
"axis.read.workspace": "工作区读",
|
|
190
|
+
"axis.read.all": "全盘读",
|
|
191
|
+
"axis.write.deny": "禁止写",
|
|
192
|
+
"axis.write.workspace": "工作区写",
|
|
193
|
+
"axis.write.all": "全盘写",
|
|
194
|
+
"axis.custom": "自定义",
|
|
195
|
+
"axis.unknown": "未设置",
|
|
196
|
+
"editor.readTitle": "读取权限 · 自定义路径",
|
|
197
|
+
"editor.writeTitle": "写入权限 · 自定义路径",
|
|
198
|
+
"editor.base": "底座",
|
|
199
|
+
"editor.readBase": "读取权限的底座",
|
|
200
|
+
"editor.writeBase": "写入权限的底座",
|
|
201
|
+
"editor.baseDeny": "概不允许",
|
|
202
|
+
"editor.baseWorkspace": "会话工作区",
|
|
203
|
+
"editor.baseAll": "整台主机",
|
|
204
|
+
"editor.allow": "额外放行的绝对路径(每行一条)",
|
|
205
|
+
"editor.readAllow": "读取权限额外放行的绝对路径",
|
|
206
|
+
"editor.writeAllow": "写入权限额外放行的绝对路径",
|
|
207
|
+
"editor.deny": "排除的绝对路径(每行一条,优先于放行)",
|
|
208
|
+
"editor.readDeny": "读取权限排除的绝对路径",
|
|
209
|
+
"editor.writeDeny": "写入权限排除的绝对路径",
|
|
210
|
+
"editor.hint": "允许的范围 = 底座 + 放行 − 排除;一条路径同时出现在两边时,排除优先。",
|
|
211
|
+
"editor.submit": "应用",
|
|
212
|
+
"editor.sending": "提交中…",
|
|
213
|
+
"editor.cancel": "取消",
|
|
214
|
+
"editor.failed": "宿主没有接受这次改动:自定义路径需要审批,未获批准时不写入。",
|
|
215
|
+
"editor.notStored": "宿主执行了这条命令,但设置文档里没有出现这次改动:它被拒绝了。下拉仍显示当刻生效的取值。",
|
|
216
|
+
"editor.problemBase": "未知的底座。",
|
|
217
|
+
"editor.problemPath": "路径必须是绝对路径(以斜杠、盘符或双反斜杠开头)。",
|
|
218
|
+
"editor.problemComma": "路径里不能含逗号:宿主用逗号分隔参数。",
|
|
219
|
+
"editor.groups": "规则组(可多选,也可以一个都不选)",
|
|
220
|
+
"editor.groupBoth": "读 + 写",
|
|
221
|
+
"editor.groupRead": "仅读侧",
|
|
222
|
+
"editor.groupWrite": "仅写侧",
|
|
223
|
+
"editor.groupNone": "两侧都没有规则",
|
|
224
|
+
"editor.groupMissing": "设置页没有定义这个 id",
|
|
225
|
+
"editor.groupsEmpty": "设置页还没有定义规则组。",
|
|
226
|
+
"narrowing.title": "写轴被读轴收窄",
|
|
227
|
+
"narrowing.body": "实际生效的写范围 = 写轴 ∩ 读轴。下面这些根本来由写轴放行,读轴不覆盖它们,因此写不进去。",
|
|
228
|
+
"narrowing.removed": "读轴从写范围里削掉了:{roots}",
|
|
229
|
+
"narrowing.none": "(没有)",
|
|
230
|
+
"narrowing.lostUnbounded": "写轴本来无界(整台主机),现在被收成读轴的范围。",
|
|
231
|
+
"narrowing.marker": "{title}:{removed}",
|
|
232
|
+
"editor.problemGroupId": "选中的规则组 id 里含分隔符(竖线、逗号或等号),宿主读不出这个 id。请到设置页改掉这个 id。",
|
|
233
|
+
"readLabel": "读取权限,当前:{name}",
|
|
234
|
+
"writeLabel": "写入权限,当前:{name}",
|
|
235
|
+
"customSummary": "自定义路径,基于{base}",
|
|
236
|
+
"customAllow": "额外允许:{paths}",
|
|
237
|
+
"customDeny": "额外禁止:{paths}",
|
|
238
|
+
"customNone": "无",
|
|
239
|
+
"close": "关闭",
|
|
240
|
+
"preset.readOnly": "仅可查看",
|
|
241
|
+
"preset.workspaceWrite": "工作区内修改",
|
|
242
|
+
"preset.fullAccess": "完全权限",
|
|
243
|
+
"confirm.title": "确认放开权限?",
|
|
244
|
+
"confirm.description": "这次改动会把该条访问轴放到「整台主机」或额外的绝对路径上,后续操作将减少确认步骤,并且可以直接执行更多操作,包括敏感操作、文件修改或外部命令。仅建议在你信任后续任务时使用。",
|
|
245
|
+
"confirm.acknowledge": "我已了解风险,并愿意继续",
|
|
246
|
+
"confirm.cancel": "取消",
|
|
247
|
+
"confirm.enable": "确认放开"
|
|
248
|
+
};
|
|
249
|
+
/** English dictionary for the two current-session axis pickers. */
|
|
250
|
+
const accessEn = {
|
|
251
|
+
"axis.read.deny": "Deny read",
|
|
252
|
+
"axis.read.workspace": "Read workspace",
|
|
253
|
+
"axis.read.all": "Read all",
|
|
254
|
+
"axis.write.deny": "Deny write",
|
|
255
|
+
"axis.write.workspace": "Write workspace",
|
|
256
|
+
"axis.write.all": "Write all",
|
|
257
|
+
"axis.custom": "Custom",
|
|
258
|
+
"axis.unknown": "Not set",
|
|
259
|
+
"editor.readTitle": "Read access · custom paths",
|
|
260
|
+
"editor.writeTitle": "Write access · custom paths",
|
|
261
|
+
"editor.base": "Base",
|
|
262
|
+
"editor.readBase": "Read access base",
|
|
263
|
+
"editor.writeBase": "Write access base",
|
|
264
|
+
"editor.baseDeny": "Deny everything",
|
|
265
|
+
"editor.baseWorkspace": "Session workspace",
|
|
266
|
+
"editor.baseAll": "The whole host",
|
|
267
|
+
"editor.allow": "Additional allowed absolute paths (one per line)",
|
|
268
|
+
"editor.readAllow": "Absolute paths read access also allows",
|
|
269
|
+
"editor.writeAllow": "Absolute paths write access also allows",
|
|
270
|
+
"editor.deny": "Excluded absolute paths (one per line, they win)",
|
|
271
|
+
"editor.readDeny": "Absolute paths read access excludes",
|
|
272
|
+
"editor.writeDeny": "Absolute paths write access excludes",
|
|
273
|
+
"editor.hint": "Allowed = base + allowed − excluded; when a path is on both lists, the exclusion wins.",
|
|
274
|
+
"editor.submit": "Apply",
|
|
275
|
+
"editor.sending": "Applying…",
|
|
276
|
+
"editor.cancel": "Cancel",
|
|
277
|
+
"editor.failed": "The host refused this change: custom paths need approval, and nothing is written without it.",
|
|
278
|
+
"editor.notStored": "The host ran the command but the settings document never showed this change, so it was refused. The dropdown still shows the value in force.",
|
|
279
|
+
"editor.problemBase": "Unknown base.",
|
|
280
|
+
"editor.problemPath": "A path must be absolute (start with a slash, a drive letter, or a double backslash).",
|
|
281
|
+
"editor.problemComma": "A path cannot contain a comma: the host separates its arguments with commas.",
|
|
282
|
+
"editor.groups": "Rule groups (pick any number, or none at all)",
|
|
283
|
+
"editor.groupBoth": "read + write",
|
|
284
|
+
"editor.groupRead": "read side only",
|
|
285
|
+
"editor.groupWrite": "write side only",
|
|
286
|
+
"editor.groupNone": "no rules on either side",
|
|
287
|
+
"editor.groupMissing": "the settings page does not define this id",
|
|
288
|
+
"editor.groupsEmpty": "The settings page defines no rule groups yet.",
|
|
289
|
+
"narrowing.title": "The read axis narrowed the write axis",
|
|
290
|
+
"narrowing.body": "The write range in force is the write axis INTERSECTED with the read axis. These roots the write axis granted are not covered by the read axis, so nothing can be written there.",
|
|
291
|
+
"narrowing.removed": "The read axis removed these from the write range: {roots}",
|
|
292
|
+
"narrowing.none": "(none)",
|
|
293
|
+
"narrowing.lostUnbounded": "The write axis was unbounded (the whole host) and is now held to the read range.",
|
|
294
|
+
"narrowing.marker": "{title}: {removed}",
|
|
295
|
+
"editor.problemGroupId": "A selected rule-group id carries a separator (a bar, a comma, or an equals sign), so the host cannot read it back. Rename that id on the settings page.",
|
|
296
|
+
"readLabel": "Read access, current: {name}",
|
|
297
|
+
"writeLabel": "Write access, current: {name}",
|
|
298
|
+
"customSummary": "Custom paths over {base}",
|
|
299
|
+
"customAllow": "Also allowed: {paths}",
|
|
300
|
+
"customDeny": "Also denied: {paths}",
|
|
301
|
+
"customNone": "none",
|
|
302
|
+
"close": "Close",
|
|
303
|
+
"preset.readOnly": "Read Only",
|
|
304
|
+
"preset.workspaceWrite": "Workspace Write",
|
|
305
|
+
"preset.fullAccess": "Full access",
|
|
306
|
+
"confirm.title": "Widen access?",
|
|
307
|
+
"confirm.description": "This change puts that access axis on the whole host, or on additional absolute paths. Later actions reduce confirmation steps and can run more things directly, including sensitive operations, file changes, or external commands. Only use it when you trust subsequent tasks.",
|
|
308
|
+
"confirm.acknowledge": "I understand the risks and want to continue",
|
|
309
|
+
"confirm.cancel": "Cancel",
|
|
310
|
+
"confirm.enable": "Widen access"
|
|
311
|
+
};
|
|
312
|
+
//#endregion
|
|
313
|
+
//#region lib/types/client/permission/presentation.js
|
|
314
|
+
/** Every value the axes offer, in the dropdowns' display order. */
|
|
315
|
+
const AXIS_VALUES = [
|
|
316
|
+
"deny",
|
|
317
|
+
"workspace",
|
|
318
|
+
"all",
|
|
319
|
+
"custom"
|
|
320
|
+
];
|
|
321
|
+
/**
|
|
322
|
+
* Narrow one axis's wire kind to a value the dropdowns can show.
|
|
323
|
+
* @param kind - the kind field as it arrives on the wire.
|
|
324
|
+
* @returns one of the four values, or undefined when the kind is unrecognized.
|
|
325
|
+
*/
|
|
326
|
+
function axisValueOf$2(kind) {
|
|
327
|
+
return AXIS_VALUES.includes(kind) ? kind : void 0;
|
|
328
|
+
}
|
|
329
|
+
const AXIS_LABEL_KEYS = {
|
|
330
|
+
read: {
|
|
331
|
+
"deny": "axis.read.deny",
|
|
332
|
+
"workspace": "axis.read.workspace",
|
|
333
|
+
"all": "axis.read.all",
|
|
334
|
+
"custom": "axis.custom"
|
|
335
|
+
},
|
|
336
|
+
write: {
|
|
337
|
+
"deny": "axis.write.deny",
|
|
338
|
+
"workspace": "axis.write.workspace",
|
|
339
|
+
"all": "axis.write.all",
|
|
340
|
+
"custom": "axis.custom"
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
/** English fallback per axis label, read from the dictionary that owns the copy. */
|
|
344
|
+
const DEFAULT_AXIS_LABELS = {
|
|
345
|
+
"axis.read.deny": accessEn["axis.read.deny"],
|
|
346
|
+
"axis.read.workspace": accessEn["axis.read.workspace"],
|
|
347
|
+
"axis.read.all": accessEn["axis.read.all"],
|
|
348
|
+
"axis.write.deny": accessEn["axis.write.deny"],
|
|
349
|
+
"axis.write.workspace": accessEn["axis.write.workspace"],
|
|
350
|
+
"axis.write.all": accessEn["axis.write.all"],
|
|
351
|
+
"axis.custom": accessEn["axis.custom"],
|
|
352
|
+
"axis.unknown": accessEn["axis.unknown"]
|
|
353
|
+
};
|
|
354
|
+
const PRESET_LABEL_KEYS = /* @__PURE__ */ new Map([
|
|
355
|
+
["read-only", "preset.readOnly"],
|
|
356
|
+
["workspace-write", "preset.workspaceWrite"],
|
|
357
|
+
["danger-full-access", "preset.fullAccess"]
|
|
358
|
+
]);
|
|
359
|
+
/**
|
|
360
|
+
* English built-in preset labels, read from the dictionary that owns the copy
|
|
361
|
+
* so a host-supplied machine value still renders as product copy on a surface
|
|
362
|
+
* that has no locale seat.
|
|
363
|
+
*/
|
|
364
|
+
const DEFAULT_PRESET_LABELS = {
|
|
365
|
+
"preset.readOnly": en$1["preset.readOnly"],
|
|
366
|
+
"preset.workspaceWrite": en$1["preset.workspaceWrite"],
|
|
367
|
+
"preset.fullAccess": en$1["preset.fullAccess"]
|
|
368
|
+
};
|
|
369
|
+
/**
|
|
370
|
+
* Render one axis value under its product label. The axis is required because
|
|
371
|
+
* the two pickers share no visible caption, so the label itself says which axis
|
|
372
|
+
* it belongs to ("工作区读" under the read picker, "工作区写" under the write one).
|
|
373
|
+
* @param value - the axis value.
|
|
374
|
+
* @param axis - the axis the value belongs to, which selects the copy.
|
|
375
|
+
* @param t - optional locale dictionary lookup for the built-in labels.
|
|
376
|
+
* @returns the localized label, or the English label without a locale seat.
|
|
377
|
+
*/
|
|
378
|
+
function axisLabel(value, axis, t) {
|
|
379
|
+
const key = AXIS_LABEL_KEYS[axis][value];
|
|
380
|
+
return t?.(key) ?? DEFAULT_AXIS_LABELS[key];
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* The NAME half of one dropdown's trigger label: one word per value.
|
|
384
|
+
*
|
|
385
|
+
* A `custom` axis renders as the bare word and nothing else. It used to append
|
|
386
|
+
* its base and group count, which is how a group pick on an already-custom axis
|
|
387
|
+
* became visible; the trigger does not have room for that text, and the editor a
|
|
388
|
+
* `custom` pick opens states the base, the groups and both path lists in full,
|
|
389
|
+
* so the detail lives there instead of on the trigger. Nothing else observes the
|
|
390
|
+
* difference: a submission is judged by the stored record the read-back
|
|
391
|
+
* compares against, never by the trigger's text.
|
|
392
|
+
*
|
|
393
|
+
* An absent value (no stored record, or a kind this build does not know) says so
|
|
394
|
+
* instead of borrowing a value: the two dropdowns must render even when nothing
|
|
395
|
+
* can be claimed about the axis, and "未设置" is the honest label for that.
|
|
396
|
+
* @param value - the axis value to name, or `undefined` when there is none.
|
|
397
|
+
* @param axis - which axis this name belongs to.
|
|
398
|
+
* @param t - locale lookup for the label.
|
|
399
|
+
* @returns the name to interpolate into the trigger's `readLabel`/`writeLabel`.
|
|
400
|
+
*/
|
|
401
|
+
function axisValueName(value, axis, t) {
|
|
402
|
+
if (value === void 0) return t("axis.unknown");
|
|
403
|
+
return axisLabel(value, axis, (key) => t(key));
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Build the write axis's narrowing notice.
|
|
407
|
+
* @param narrowing - the host's report, or `undefined` when there is none.
|
|
408
|
+
* @param t - locale lookup for every sentence.
|
|
409
|
+
* @returns the notice to render, or `undefined` when nothing was removed.
|
|
410
|
+
*/
|
|
411
|
+
function narrowingNotice(narrowing, t) {
|
|
412
|
+
if (narrowing === void 0 || !narrowing.narrowed) return void 0;
|
|
413
|
+
const roots = narrowing.droppedRoots.length === 0 ? t("narrowing.none") : narrowing.droppedRoots.join("\n");
|
|
414
|
+
const title = t("narrowing.title");
|
|
415
|
+
const removed = t("narrowing.removed", { roots });
|
|
416
|
+
return {
|
|
417
|
+
title,
|
|
418
|
+
removed,
|
|
419
|
+
lostUnbounded: narrowing.lostUnbounded ? t("narrowing.lostUnbounded") : "",
|
|
420
|
+
marked: true,
|
|
421
|
+
marker: t("narrowing.marker", {
|
|
422
|
+
title,
|
|
423
|
+
removed
|
|
424
|
+
})
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Render a permission preset under its product label.
|
|
429
|
+
* @param value - preset machine value.
|
|
430
|
+
* @param name - host-supplied preset name.
|
|
431
|
+
* @param t - optional locale dictionary lookup for built-in product labels.
|
|
432
|
+
* @returns the localized built-in label, or the host's own name VERBATIM.
|
|
433
|
+
*/
|
|
434
|
+
function displayPermissionPreset(value, name, t) {
|
|
435
|
+
const key = PRESET_LABEL_KEYS.get(value);
|
|
436
|
+
if (key !== void 0 && (name === value || name === DEFAULT_PRESET_LABELS[key])) return t?.(key) ?? DEFAULT_PRESET_LABELS[key];
|
|
437
|
+
return name;
|
|
438
|
+
}
|
|
439
|
+
//#endregion
|
|
440
|
+
//#region lib/types/client/permission/axis-editor.js
|
|
441
|
+
/**
|
|
442
|
+
* 输入框上方两个访问轴下拉的 `custom` 路径编辑器:把一条轴收成可编辑的草稿、
|
|
443
|
+
* 把草稿收成宿主 `/permission` 命令接受的参数串,并给出全局初值与兜底默认值。
|
|
444
|
+
*
|
|
445
|
+
* 这里不碰 React、不碰 DOM、也不碰宿主 —— 界面半边每次展开编辑器与每次提交都走
|
|
446
|
+
* 这几个函数,因此"界面上能编的形状"与"宿主解析器接受的形状"只有这一份定义。
|
|
447
|
+
* 宿主的语法在 `dsh-dual-axis` 的 `parseAxisEntry` / `parseCustomFragment`:
|
|
448
|
+
* `<axis>:<kind>[:base=<deny|workspace|all>,groups=<id>|<id>,allow=<绝对路径>,deny=<绝对路径>]`,
|
|
449
|
+
* `allow` / `deny` 重复 key 表示多条、逗号分隔,路径必须是绝对路径;`groups` 只写
|
|
450
|
+
* **组 id**,组体由宿主在判定时按 id 现取。
|
|
451
|
+
*
|
|
452
|
+
* @module @deepseek-ai/dsh-client-ui-permission-presets/client/axis-editor
|
|
453
|
+
*/
|
|
454
|
+
/** `custom` 可叠加的三个底座,与宿主 `AxisBase` 同一套字面量。 */
|
|
455
|
+
const AXIS_BASES$2 = [
|
|
456
|
+
"deny",
|
|
457
|
+
"workspace",
|
|
458
|
+
"all"
|
|
459
|
+
];
|
|
460
|
+
/** 两个下拉里那个「整台主机」的取值;它就是要过二次确认闸的那一个。 */
|
|
461
|
+
const ALL_AXIS = "all";
|
|
462
|
+
/**
|
|
463
|
+
* 把设置节的 `groups` 读成多选用的组清单。
|
|
464
|
+
*
|
|
465
|
+
* 读取是宽容的:读不懂的成员跳过,不抛 —— 这个清单只是给人选的东西,坏取值由宿主
|
|
466
|
+
* 的 `parseLibrary` 在判定时响亮失败,界面不该因为一个坏成员就整块打不开。
|
|
467
|
+
* @param value - 设置节里的 `groups`,未信任。
|
|
468
|
+
* @returns 每个可用的组一项,顺序与文档一致,id 去重。
|
|
469
|
+
*/
|
|
470
|
+
function groupOptionsOf(value) {
|
|
471
|
+
if (!Array.isArray(value)) return [];
|
|
472
|
+
const options = [];
|
|
473
|
+
const seen = /* @__PURE__ */ new Set();
|
|
474
|
+
for (const entry of value) {
|
|
475
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
476
|
+
const candidate = entry;
|
|
477
|
+
if (typeof candidate.id !== "string" || candidate.id === "" || seen.has(candidate.id)) continue;
|
|
478
|
+
seen.add(candidate.id);
|
|
479
|
+
options.push({
|
|
480
|
+
id: candidate.id,
|
|
481
|
+
name: typeof candidate.name === "string" && candidate.name !== "" ? candidate.name : candidate.id,
|
|
482
|
+
read: candidate.read !== void 0 && candidate.read !== null,
|
|
483
|
+
write: candidate.write !== void 0 && candidate.write !== null
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
return options;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* 多选列表的完整内容:设置页定义的组,加上这条对话已经引用、但设置页不再定义的 id。
|
|
490
|
+
*
|
|
491
|
+
* 后一类必须留在列表里并保持勾选:它们缺席就等于「打开编辑器再应用」会**静默**把
|
|
492
|
+
* 引用删掉,而宿主正是靠这些引用响亮失败的(规格 §「引用不到的组 id → 响亮失败」)。
|
|
493
|
+
* 取消勾选仍然允许 —— 移除一个引用属于收窄,永远允许。
|
|
494
|
+
* @param library - 设置页当前定义的组。
|
|
495
|
+
* @param selected - 这条对话预设里引用的 id。
|
|
496
|
+
* @returns 列表的每一行。
|
|
497
|
+
*/
|
|
498
|
+
function groupChoicesOf(library, selected) {
|
|
499
|
+
const choices = library.map((option) => ({
|
|
500
|
+
id: option.id,
|
|
501
|
+
name: option.name,
|
|
502
|
+
coverage: option.read && option.write ? "both" : option.read ? "read" : option.write ? "write" : "none",
|
|
503
|
+
missing: false,
|
|
504
|
+
checked: selected.includes(option.id)
|
|
505
|
+
}));
|
|
506
|
+
for (const id of selected) {
|
|
507
|
+
if (library.some((option) => option.id === id)) continue;
|
|
508
|
+
choices.push({
|
|
509
|
+
id,
|
|
510
|
+
name: id,
|
|
511
|
+
coverage: "none",
|
|
512
|
+
missing: true,
|
|
513
|
+
checked: true
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
return choices;
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* 某一轴拿不到任何配置时使用的兜底轴:读轴 `all`、写轴 `workspace`,
|
|
520
|
+
* 即 `dsh-sandbox` 的 `DEFAULT_READ_SCOPE` / `DEFAULT_WRITE_SCOPE`。
|
|
521
|
+
*
|
|
522
|
+
* 这两条是**部署默认值**,不是"兜底给全盘权限":读轴 `all`、写轴 `workspace`
|
|
523
|
+
* 正是 `dsh-sandbox` 里那两条默认轴,所以拿不到全局配置时也不会把写权限放大。
|
|
524
|
+
* @param axis - 轴名。
|
|
525
|
+
* @returns 该轴的兜底取值。
|
|
526
|
+
*/
|
|
527
|
+
function fallbackAxis(axis) {
|
|
528
|
+
return axis === "read" ? "all" : "workspace";
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* 一条空白草稿:以给定取值起步,并以该取值本身为底座。
|
|
532
|
+
*
|
|
533
|
+
* 底座取"这条轴当前的取值"而不是写死的 `workspace`:拿不到任何全局配置时,读轴的
|
|
534
|
+
* 兜底是 `all`,于是编辑器的底座也必须是 `all` —— 若写死 `workspace`,兜底打开的
|
|
535
|
+
* 编辑器会静默把读轴收窄成会话工作区,与那条兜底值本身矛盾。
|
|
536
|
+
* @param kind - 起步取值,同时用作底座。
|
|
537
|
+
* @returns 一份可编辑的草稿。
|
|
538
|
+
*/
|
|
539
|
+
function emptyDraft(kind) {
|
|
540
|
+
return {
|
|
541
|
+
kind,
|
|
542
|
+
base: AXIS_BASES$2.includes(kind) ? kind : "workspace",
|
|
543
|
+
groups: [],
|
|
544
|
+
allow: "",
|
|
545
|
+
deny: ""
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* 一条轴的起始草稿:**这条会话自己的轴**优先,它缺席时才退到部署设置值,再没有
|
|
550
|
+
* 才用该轴的部署默认值。它永远不会空着打开,也永远不抛 —— 兜底就是这个函数的
|
|
551
|
+
* 全部意义。
|
|
552
|
+
*
|
|
553
|
+
* 「会话优先」不是顺序偏好而是这个界面的定义:编辑器表达的就是这条会话,打开时
|
|
554
|
+
* 显示别的值会与它旁边那个下拉自相矛盾。会话读轴是 `deny` 时初值必须是 `deny`,
|
|
555
|
+
* 不是部署默认的 `all`。
|
|
556
|
+
* @param axis - 正在编辑的轴。
|
|
557
|
+
* @param current - 这条会话在该轴上的取值,宿主投影还没有这条轴时为 undefined。
|
|
558
|
+
* @param global - 部署设置在该轴上的取值,只在会话没有取值时兜底。
|
|
559
|
+
* @returns 一份不用重打任何东西就能提交的草稿。
|
|
560
|
+
*/
|
|
561
|
+
function initialDraft(axis, current, global) {
|
|
562
|
+
const source = current ?? global;
|
|
563
|
+
if (source === void 0) return emptyDraft(fallbackAxis(axis));
|
|
564
|
+
if (source.kind !== "custom") return {
|
|
565
|
+
kind: "custom",
|
|
566
|
+
base: AXIS_BASES$2.includes(source.kind) ? source.kind : "workspace",
|
|
567
|
+
groups: [],
|
|
568
|
+
allow: "",
|
|
569
|
+
deny: ""
|
|
570
|
+
};
|
|
571
|
+
return {
|
|
572
|
+
kind: "custom",
|
|
573
|
+
base: AXIS_BASES$2.includes(source.base ?? "") ? source.base : "workspace",
|
|
574
|
+
groups: [...source.groups ?? []],
|
|
575
|
+
allow: (source.allow ?? []).join("\n"),
|
|
576
|
+
deny: (source.deny ?? []).join("\n")
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
/** 一条绝对路径写法(POSIX 或 Windows)。与宿主的 `isAbsoluteSpelling` 同一条规则。 */
|
|
580
|
+
const POSIX_ABSOLUTE = /^\//u;
|
|
581
|
+
/** Windows 盘符或 UNC 写法。 */
|
|
582
|
+
const WINDOWS_ABSOLUTE = /^(?:[A-Za-z]:[\\/]|\\\\)/u;
|
|
583
|
+
/**
|
|
584
|
+
* 一个写法是否为绝对路径。相对路径只能相对宿主进程的 cwd 解析,永远不是写这份配置
|
|
585
|
+
* 的人想说的位置,所以与宿主一样在这里就拒绝,而不是留给宿主报一句难读的错。
|
|
586
|
+
* @param value - 待检查的路径写法。
|
|
587
|
+
* @returns 该写法是否为绝对路径。
|
|
588
|
+
*/
|
|
589
|
+
function isAbsolutePath$1(value) {
|
|
590
|
+
return POSIX_ABSOLUTE.test(value) || WINDOWS_ABSOLUTE.test(value);
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* 把一个多行文本框读成路径列表:去掉空行与首尾空白。
|
|
594
|
+
* @param text - 文本框里的原文。
|
|
595
|
+
* @returns 其中每一行作为一条路径。
|
|
596
|
+
*/
|
|
597
|
+
function pathLines$1(text) {
|
|
598
|
+
return text.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line !== "");
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* 把一份 `custom` 草稿收成宿主解析器接受的参数片段
|
|
602
|
+
* (`base=<…>,groups=<id>|<id>,allow=<…>,deny=<…>`,`allow` / `deny` 每条重复一次 key)。
|
|
603
|
+
*
|
|
604
|
+
* 路径里的逗号、以及组 id 里的三类分隔符都会被宿主的解析器当成别的东西,因此它们
|
|
605
|
+
* 在这里就拒绝,而不是拼出一条宿主读成另一个意思的命令行。
|
|
606
|
+
* @param draft - 编辑器里的草稿。
|
|
607
|
+
* @param problemOf - 生成本地化说明的回调。
|
|
608
|
+
* @returns 参数片段,或一句说明这份草稿为什么还不能提交的话。
|
|
609
|
+
*/
|
|
610
|
+
function customFragment(draft, problemOf) {
|
|
611
|
+
if (!AXIS_BASES$2.includes(draft.base)) return {
|
|
612
|
+
ok: false,
|
|
613
|
+
problem: problemOf("baseUnknown")
|
|
614
|
+
};
|
|
615
|
+
for (const id of draft.groups) if (id === "" || id.includes("|") || id.includes(",") || id.includes("=")) return {
|
|
616
|
+
ok: false,
|
|
617
|
+
problem: problemOf("groupUnsupported")
|
|
618
|
+
};
|
|
619
|
+
const entries = [];
|
|
620
|
+
if (draft.groups.length > 0) entries.push("groups=" + draft.groups.join("|"));
|
|
621
|
+
const lists = [["allow", draft.allow], ["deny", draft.deny]];
|
|
622
|
+
for (const [key, text] of lists) for (const path of pathLines$1(text)) {
|
|
623
|
+
if (!isAbsolutePath$1(path)) return {
|
|
624
|
+
ok: false,
|
|
625
|
+
problem: problemOf("notAbsolute")
|
|
626
|
+
};
|
|
627
|
+
if (path.includes(",")) return {
|
|
628
|
+
ok: false,
|
|
629
|
+
problem: problemOf("commaUnsupported")
|
|
630
|
+
};
|
|
631
|
+
entries.push(`${key}=${path}`);
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
ok: true,
|
|
635
|
+
fragment: ["base=" + draft.base, ...entries].join(",")
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* 一份 `custom` 草稿提交出去之后,宿主文档里应当出现的那条轴值。
|
|
640
|
+
*
|
|
641
|
+
* 与 {@link customFragment} 同一份草稿、同一套取值:片段是发给宿主的那句话,这里是
|
|
642
|
+
* 那句话落成记录之后的形状。两者一起用才有意义 —— 宿主对自己没写的改动同样会回一句
|
|
643
|
+
* 「命令执行了」(`Session.command` 的 `matched` 只说明命令被解析到,见
|
|
644
|
+
* `packages/api/session-controller/src/client/sessions/session.ts:387-391`),所以界面
|
|
645
|
+
* 只能拿写入之后的记录来核对这次提交到底有没有生效。
|
|
646
|
+
*
|
|
647
|
+
* 组 id 去重,因为宿主 `normalizeScope` 的 `groupIds` 也去重;路径列表保持顺序,
|
|
648
|
+
* 因为宿主的 `pathList` 保序。
|
|
649
|
+
* @param draft - 编辑器里的草稿。
|
|
650
|
+
* @returns 宿主文档里那条轴的形状。
|
|
651
|
+
*/
|
|
652
|
+
function draftScope(draft) {
|
|
653
|
+
return {
|
|
654
|
+
kind: "custom",
|
|
655
|
+
base: draft.base,
|
|
656
|
+
groups: [...new Set(draft.groups)],
|
|
657
|
+
allow: pathLines$1(draft.allow),
|
|
658
|
+
deny: pathLines$1(draft.deny)
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* 两条轴值是不是同一件事 —— 按值比,不按引用比。
|
|
663
|
+
*
|
|
664
|
+
* 比较的是宿主写进文档之后的那份形状:`groups` / `allow` / `deny` 只有一条轴真的是
|
|
665
|
+
* `custom` 时才参与比较(`deny` / `workspace` / `all` 都不带这些成员),列表按顺序比,
|
|
666
|
+
* 因为顺序是取值的一部分(提交顺序就是用户勾选的顺序)。
|
|
667
|
+
* @param left - 一条轴值。
|
|
668
|
+
* @param right - 另一条轴值。
|
|
669
|
+
* @returns 两者是否描述同一条轴。
|
|
670
|
+
*/
|
|
671
|
+
function sameStoredAxis(left, right) {
|
|
672
|
+
if (left === void 0 || right === void 0) return left === right;
|
|
673
|
+
if (left.kind !== right.kind) return false;
|
|
674
|
+
if (left.kind !== "custom" || right.kind !== "custom") return true;
|
|
675
|
+
const sameList = (a, b) => {
|
|
676
|
+
const one = a ?? [];
|
|
677
|
+
const other = b ?? [];
|
|
678
|
+
return one.length === other.length && one.every((value, index) => value === other[index]);
|
|
679
|
+
};
|
|
680
|
+
return left.base === right.base && sameList(left.groups, right.groups) && sameList(left.allow, right.allow) && sameList(left.deny, right.deny);
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* 一条轴取值是否会**放开**权限:只有 `all`(整台主机)与带额外放行路径的 `custom`
|
|
684
|
+
* 会,`deny` / `workspace` 和空清单的 `custom` 不会。
|
|
685
|
+
*
|
|
686
|
+
* 这个判定就是发布版二次确认闸的开关。它按**取值本身**算,不按界面动作算:同一条
|
|
687
|
+
* `write:all`,无论从下拉选出还是从编辑器拼出,都要过一次同样的确认。
|
|
688
|
+
* @param value - 轴取值。
|
|
689
|
+
* @returns 该取值是否会放大访问范围。
|
|
690
|
+
*/
|
|
691
|
+
function widensAccess(value) {
|
|
692
|
+
return value === ALL_AXIS;
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* 一条 `custom` 提交片段是否会放开权限:底座是 `all`,或带了任何额外放行路径。
|
|
696
|
+
* 排除清单不收窄判定 —— `base=all,deny=/x` 仍然是整台主机减去一条路径。
|
|
697
|
+
* @param fragment - `customFragment` 产出的片段。
|
|
698
|
+
* @returns 该片段是否会放大访问范围。
|
|
699
|
+
*/
|
|
700
|
+
function fragmentWidensAccess(fragment) {
|
|
701
|
+
const entries = fragment.split(",");
|
|
702
|
+
if (entries[0] === `base=${ALL_AXIS}`) return true;
|
|
703
|
+
if (entries.some((entry) => entry.startsWith("groups="))) return true;
|
|
704
|
+
return entries.some((entry) => entry.startsWith("allow="));
|
|
705
|
+
}
|
|
706
|
+
//#endregion
|
|
707
|
+
//#region \0t4-css:M:\T4\out\release\dsh-dual-axis\packages\dual-axis-ui\src\client\permission\PermissionSelect.module.css.mjs
|
|
708
|
+
const css$1 = "._9vJ8OG_axes{background:var(--dsw-alias-bg-module-platform);border-radius:24px;align-items:center;gap:2px;min-width:0;padding:0 4px 0 6px;display:inline-flex}._9vJ8OG_trigger{min-width:0;max-width:200px;height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:20px;outline:none;align-items:center;gap:4px;padding:0 4px 0 6px;font-size:13px;font-weight:500;line-height:20px;display:inline-flex}._9vJ8OG_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}._9vJ8OG_trigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3)}._9vJ8OG_trigger:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}._9vJ8OG_triggerIcon{flex:none;display:inline-flex}._9vJ8OG_triggerIcon svg{width:14px;height:14px}._9vJ8OG_triggerLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}._9vJ8OG_optionLabel{align-items:baseline;gap:4px;min-width:0;max-width:100%;display:inline-flex}._9vJ8OG_optionLabelText{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}._9vJ8OG_badge{color:var(--dsw-alias-label-tertiary);letter-spacing:.2px;flex:none;align-self:flex-start;margin-top:-1px;font-size:8px;font-weight:600;line-height:10px}._9vJ8OG_chevron{color:var(--dsw-alias-label-caption);flex:none;transition:transform .12s;display:inline-flex}@container (width<=460px){._9vJ8OG_triggerLabel{display:none}}._9vJ8OG_chevronOpen{transform:rotate(180deg)}._9vJ8OG_seat{align-items:center;min-width:0;display:inline-flex;position:relative}._9vJ8OG_editor{z-index:30;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-module-platform);width:320px;box-shadow:var(--dsw-shadow-popover,0 8px 24px #0000002e);color:var(--dsw-alias-label-primary);text-align:left;border-radius:12px;gap:8px;padding:12px;font-size:12px;display:grid;position:absolute;bottom:calc(100% + 6px);left:0}._9vJ8OG_editorTitle{font-size:13px;font-weight:600}._9vJ8OG_editorRow{align-items:center;gap:8px;display:flex}._9vJ8OG_editorField{gap:3px;display:grid}._9vJ8OG_editorLabel{min-width:3rem;color:var(--dsw-alias-label-secondary)}._9vJ8OG_editorSelect{border:1px solid var(--dsw-alias-border-l2);min-width:0;height:26px;color:inherit;font:inherit;background:0 0;border-radius:6px;flex:auto}._9vJ8OG_editorArea{resize:vertical;border:1px solid var(--dsw-alias-border-l2);width:100%;color:inherit;background:0 0;border-radius:6px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:18px}._9vJ8OG_editorHint{color:var(--dsw-alias-label-tertiary);line-height:16px}._9vJ8OG_editorProblem{color:var(--dsw-alias-label-error,#d4380d);line-height:16px}._9vJ8OG_editorActions{align-items:center;gap:8px;display:flex}._9vJ8OG_editorSubmit,._9vJ8OG_editorCancel{border:1px solid var(--dsw-alias-border-l2);height:26px;color:inherit;font:inherit;cursor:pointer;background:0 0;border-radius:6px;padding:0 10px}._9vJ8OG_editorSubmit{background:var(--dsw-alias-interactive-bg-hover);font-weight:600}._9vJ8OG_editorSubmit:disabled,._9vJ8OG_editorCancel:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}._9vJ8OG_groupList{gap:2px;max-height:132px;display:grid;overflow-y:auto}._9vJ8OG_groupRow{cursor:pointer;align-items:center;gap:6px;min-width:0;display:flex}._9vJ8OG_groupName{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}._9vJ8OG_groupId{color:var(--dsw-alias-label-tertiary);flex:none;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px}._9vJ8OG_groupCoverage{color:var(--dsw-alias-label-caption);flex:none;font-size:11px}._9vJ8OG_groupMissing{color:var(--dsw-alias-label-error,#d4380d);flex:none;font-size:11px}._9vJ8OG_narrowingMark{background:var(--dsw-alias-label-error,#d4380d);border-radius:50%;flex:none;width:5px;height:5px;margin-right:-2px}._9vJ8OG_narrowing{border-left:2px solid var(--dsw-alias-label-error,#d4380d);background:var(--dsw-alias-interactive-bg-hover);border-radius:4px;gap:3px;padding:6px 8px;display:grid}._9vJ8OG_narrowingTitle{font-weight:600}._9vJ8OG_narrowingBody{color:var(--dsw-alias-label-secondary);line-height:15px}._9vJ8OG_narrowingRoots{color:var(--dsw-alias-label-primary);white-space:pre-wrap;overflow-wrap:anywhere;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;line-height:16px}";
|
|
709
|
+
const tagId$1 = "@t4r71/dsh-dual-axis-ui/PermissionSelect.module.css";
|
|
710
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
|
|
711
|
+
const tag = document.createElement("style");
|
|
712
|
+
tag.dataset.plugin = "@t4r71/dsh-dual-axis-ui";
|
|
713
|
+
tag.dataset.pluginCss = tagId$1;
|
|
714
|
+
tag.textContent = css$1;
|
|
715
|
+
document.head.appendChild(tag);
|
|
716
|
+
}
|
|
717
|
+
var PermissionSelect_module_css_default = {
|
|
718
|
+
"axes": "_9vJ8OG_axes",
|
|
719
|
+
"badge": "_9vJ8OG_badge",
|
|
720
|
+
"chevron": "_9vJ8OG_chevron",
|
|
721
|
+
"chevronOpen": "_9vJ8OG_chevronOpen",
|
|
722
|
+
"editor": "_9vJ8OG_editor",
|
|
723
|
+
"editorActions": "_9vJ8OG_editorActions",
|
|
724
|
+
"editorArea": "_9vJ8OG_editorArea",
|
|
725
|
+
"editorCancel": "_9vJ8OG_editorCancel",
|
|
726
|
+
"editorField": "_9vJ8OG_editorField",
|
|
727
|
+
"editorHint": "_9vJ8OG_editorHint",
|
|
728
|
+
"editorLabel": "_9vJ8OG_editorLabel",
|
|
729
|
+
"editorProblem": "_9vJ8OG_editorProblem",
|
|
730
|
+
"editorRow": "_9vJ8OG_editorRow",
|
|
731
|
+
"editorSelect": "_9vJ8OG_editorSelect",
|
|
732
|
+
"editorSubmit": "_9vJ8OG_editorSubmit",
|
|
733
|
+
"editorTitle": "_9vJ8OG_editorTitle",
|
|
734
|
+
"groupCoverage": "_9vJ8OG_groupCoverage",
|
|
735
|
+
"groupId": "_9vJ8OG_groupId",
|
|
736
|
+
"groupList": "_9vJ8OG_groupList",
|
|
737
|
+
"groupMissing": "_9vJ8OG_groupMissing",
|
|
738
|
+
"groupName": "_9vJ8OG_groupName",
|
|
739
|
+
"groupRow": "_9vJ8OG_groupRow",
|
|
740
|
+
"narrowing": "_9vJ8OG_narrowing",
|
|
741
|
+
"narrowingBody": "_9vJ8OG_narrowingBody",
|
|
742
|
+
"narrowingMark": "_9vJ8OG_narrowingMark",
|
|
743
|
+
"narrowingRoots": "_9vJ8OG_narrowingRoots",
|
|
744
|
+
"narrowingTitle": "_9vJ8OG_narrowingTitle",
|
|
745
|
+
"optionLabel": "_9vJ8OG_optionLabel",
|
|
746
|
+
"optionLabelText": "_9vJ8OG_optionLabelText",
|
|
747
|
+
"seat": "_9vJ8OG_seat",
|
|
748
|
+
"trigger": "_9vJ8OG_trigger",
|
|
749
|
+
"triggerIcon": "_9vJ8OG_triggerIcon",
|
|
750
|
+
"triggerLabel": "_9vJ8OG_triggerLabel"
|
|
751
|
+
};
|
|
752
|
+
//#endregion
|
|
753
|
+
//#region lib/types/client/permission/PermissionCustomEditor.js
|
|
754
|
+
/**
|
|
755
|
+
* 一条轴的 `custom` 路径编辑器:底座下拉加两个绝对路径列表。
|
|
756
|
+
*
|
|
757
|
+
* 这个编辑器是让 `custom` 成为一条完整策略语句的东西。另外三个取值本身各自就是
|
|
758
|
+
* 完整语句 —— 选中即提交 —— 而 `custom` 必须带底座,宿主的解析器宁可拒绝一条裸的
|
|
759
|
+
* `write:custom` 也不肯替用户猜一个。所以下拉里选中 `custom` 时不直接提交,而是展开
|
|
760
|
+
* 这个编辑器,由它提交完整的 `axis:custom:base=…,allow=…,deny=…` 参数。
|
|
761
|
+
*
|
|
762
|
+
* 两个路径框都是一行一条绝对路径。全局设置文档里存的是列表,而这个界面没有路径
|
|
763
|
+
* 选择器,所以文本是诚实的载体;一条相对路径会挡住提交并说明原因,而不是被解析到
|
|
764
|
+
* 某个用户从没指定过的目录上去。
|
|
765
|
+
*
|
|
766
|
+
* 提交要过一次风险确认:`base=all` 或带额外放行路径的 `custom` 都会放大该轴的
|
|
767
|
+
* 访问范围,所以「应用」先落到确认框,只有勾选并确认才真的写下去。不放大权限的
|
|
768
|
+
* 草稿(例如在 `workspace` 底座上只加排除路径)不过闸,免得确认变成噪音。
|
|
769
|
+
*
|
|
770
|
+
* @module @deepseek-ai/dsh-client-ui-permission-presets/client/PermissionCustomEditor
|
|
771
|
+
*/
|
|
772
|
+
/** 该轴在该取值下的词典键前缀(读轴写轴各一套文案)。 */
|
|
773
|
+
function axisKeys(axis) {
|
|
774
|
+
return axis === "read" ? {
|
|
775
|
+
title: "editor.readTitle",
|
|
776
|
+
base: "editor.readBase",
|
|
777
|
+
allow: "editor.readAllow",
|
|
778
|
+
deny: "editor.readDeny"
|
|
779
|
+
} : {
|
|
780
|
+
title: "editor.writeTitle",
|
|
781
|
+
base: "editor.writeBase",
|
|
782
|
+
allow: "editor.writeAllow",
|
|
783
|
+
deny: "editor.writeDeny"
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
/** 一个底座取值对应的词典键。 */
|
|
787
|
+
function baseOptionKey(base) {
|
|
788
|
+
return base === "deny" ? "editor.baseDeny" : base === "workspace" ? "editor.baseWorkspace" : "editor.baseAll";
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* 渲染一条轴的 `custom` 路径编辑器。
|
|
792
|
+
*
|
|
793
|
+
* 草稿从会话当前值起步,**任何取值都算**:这条会话的轴是这个界面表达的对象,
|
|
794
|
+
* 编辑器打开时显示别的值(例如部署默认)就会与它旁边那个下拉自相矛盾。只有
|
|
795
|
+
* 会话值缺席时才退到该轴的部署默认值。一次失败的提交保留草稿并显示一句可读的话,
|
|
796
|
+
* 而不是抛出去,所以被拒的审批留下的是一个还能用的编辑器。
|
|
797
|
+
* @param props - 轴、起始值、写入口与词典座位。
|
|
798
|
+
* @returns 底座下拉、两个路径框与提交行。
|
|
799
|
+
*/
|
|
800
|
+
function PermissionCustomEditor({ axis, current, global, notice, groups, locked, busy, onSubmit, onDismiss, t }) {
|
|
801
|
+
const keys = axisKeys(axis);
|
|
802
|
+
const [draft, setDraft] = (0, react.useState)(() => initialDraft(axis, current, global));
|
|
803
|
+
const [failure, setFailure] = (0, react.useState)("none");
|
|
804
|
+
const [sending, setSending] = (0, react.useState)(false);
|
|
805
|
+
const [gate, setGate] = (0, react.useState)(null);
|
|
806
|
+
const [acknowledged, setAcknowledged] = (0, react.useState)(false);
|
|
807
|
+
const parsed = customFragment(draft, (key) => t(problemFor(key)));
|
|
808
|
+
const problem = parsed.ok ? "" : parsed.problem;
|
|
809
|
+
const choices = groupChoicesOf(groups(), draft.groups);
|
|
810
|
+
const edit = (patch) => {
|
|
811
|
+
setDraft((state) => ({
|
|
812
|
+
...state,
|
|
813
|
+
...patch
|
|
814
|
+
}));
|
|
815
|
+
};
|
|
816
|
+
const toggleGroup = (id, checked) => {
|
|
817
|
+
edit({ groups: checked ? [...draft.groups, id] : draft.groups.filter((current) => current !== id) });
|
|
818
|
+
};
|
|
819
|
+
const disabled = locked || busy || sending || gate !== null;
|
|
820
|
+
/**
|
|
821
|
+
* 真正提交一份已经完整的参数串。
|
|
822
|
+
*
|
|
823
|
+
* 只有文档里真的出现了这次取值才关掉编辑器:宿主对自己**没有**写入的命令同样会回
|
|
824
|
+
* 一句「命令执行了」(它只回答命令有没有被解析到,见 `PermissionSelectInjected.select`),
|
|
825
|
+
* 所以一次被拒的写入在旧实现里与成功一模一样 —— 编辑器关掉、下拉回到原值、设置文档
|
|
826
|
+
* 没变、界面上一个字都没有。
|
|
827
|
+
*/
|
|
828
|
+
const send = (args, expected) => {
|
|
829
|
+
setSending(true);
|
|
830
|
+
setFailure("none");
|
|
831
|
+
onSubmit(args, expected).then((landed) => {
|
|
832
|
+
setSending(false);
|
|
833
|
+
if (landed) onDismiss();
|
|
834
|
+
else setFailure("refused");
|
|
835
|
+
}).catch(() => {
|
|
836
|
+
setSending(false);
|
|
837
|
+
setFailure("failed");
|
|
838
|
+
});
|
|
839
|
+
};
|
|
840
|
+
const submit = () => {
|
|
841
|
+
if (!parsed.ok) return;
|
|
842
|
+
setFailure("none");
|
|
843
|
+
const entry = `${axis}:custom:${parsed.fragment}`;
|
|
844
|
+
const expected = draftScope(draft);
|
|
845
|
+
if (fragmentWidensAccess(parsed.fragment)) {
|
|
846
|
+
setAcknowledged(false);
|
|
847
|
+
setGate({
|
|
848
|
+
args: entry,
|
|
849
|
+
expected
|
|
850
|
+
});
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
send(entry, expected);
|
|
854
|
+
};
|
|
855
|
+
return (0, react_jsx_runtime.jsxs)("div", {
|
|
856
|
+
className: PermissionSelect_module_css_default.editor,
|
|
857
|
+
"data-dsh-axis-editor": axis,
|
|
858
|
+
children: [
|
|
859
|
+
(0, react_jsx_runtime.jsx)("div", {
|
|
860
|
+
className: PermissionSelect_module_css_default.editorTitle,
|
|
861
|
+
children: t(keys.title)
|
|
862
|
+
}),
|
|
863
|
+
notice === void 0 ? null : (0, react_jsx_runtime.jsxs)("div", {
|
|
864
|
+
className: PermissionSelect_module_css_default.narrowing,
|
|
865
|
+
"data-dsh-axis-narrowing": axis,
|
|
866
|
+
role: "status",
|
|
867
|
+
children: [
|
|
868
|
+
(0, react_jsx_runtime.jsx)("div", {
|
|
869
|
+
className: PermissionSelect_module_css_default.narrowingTitle,
|
|
870
|
+
"data-dsh-narrowing-title": true,
|
|
871
|
+
children: notice.title
|
|
872
|
+
}),
|
|
873
|
+
(0, react_jsx_runtime.jsx)("div", {
|
|
874
|
+
className: PermissionSelect_module_css_default.narrowingBody,
|
|
875
|
+
children: t("narrowing.body")
|
|
876
|
+
}),
|
|
877
|
+
(0, react_jsx_runtime.jsx)("div", {
|
|
878
|
+
className: PermissionSelect_module_css_default.narrowingRoots,
|
|
879
|
+
"data-dsh-narrowing-roots": true,
|
|
880
|
+
children: notice.removed
|
|
881
|
+
}),
|
|
882
|
+
notice.lostUnbounded === "" ? null : (0, react_jsx_runtime.jsx)("div", {
|
|
883
|
+
className: PermissionSelect_module_css_default.narrowingBody,
|
|
884
|
+
children: notice.lostUnbounded
|
|
885
|
+
})
|
|
886
|
+
]
|
|
887
|
+
}),
|
|
888
|
+
(0, react_jsx_runtime.jsxs)("label", {
|
|
889
|
+
className: PermissionSelect_module_css_default.editorRow,
|
|
890
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
891
|
+
className: PermissionSelect_module_css_default.editorLabel,
|
|
892
|
+
children: t("editor.base")
|
|
893
|
+
}), (0, react_jsx_runtime.jsx)("select", {
|
|
894
|
+
className: PermissionSelect_module_css_default.editorSelect,
|
|
895
|
+
"data-dsh-axis-base": axis,
|
|
896
|
+
"aria-label": t(keys.base),
|
|
897
|
+
value: draft.base,
|
|
898
|
+
disabled,
|
|
899
|
+
onChange: (event) => {
|
|
900
|
+
edit({ base: event.target.value });
|
|
901
|
+
},
|
|
902
|
+
children: AXIS_BASES$2.map((base) => (0, react_jsx_runtime.jsx)("option", {
|
|
903
|
+
value: base,
|
|
904
|
+
children: t(baseOptionKey(base))
|
|
905
|
+
}, base))
|
|
906
|
+
})]
|
|
907
|
+
}),
|
|
908
|
+
(0, react_jsx_runtime.jsxs)("label", {
|
|
909
|
+
className: PermissionSelect_module_css_default.editorField,
|
|
910
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
911
|
+
className: PermissionSelect_module_css_default.editorLabel,
|
|
912
|
+
children: t("editor.allow")
|
|
913
|
+
}), (0, react_jsx_runtime.jsx)("textarea", {
|
|
914
|
+
className: PermissionSelect_module_css_default.editorArea,
|
|
915
|
+
"data-dsh-axis-allow": axis,
|
|
916
|
+
"aria-label": t(keys.allow),
|
|
917
|
+
rows: 3,
|
|
918
|
+
spellCheck: false,
|
|
919
|
+
value: draft.allow,
|
|
920
|
+
disabled,
|
|
921
|
+
onChange: (event) => {
|
|
922
|
+
edit({ allow: event.target.value });
|
|
923
|
+
}
|
|
924
|
+
})]
|
|
925
|
+
}),
|
|
926
|
+
(0, react_jsx_runtime.jsxs)("label", {
|
|
927
|
+
className: PermissionSelect_module_css_default.editorField,
|
|
928
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
929
|
+
className: PermissionSelect_module_css_default.editorLabel,
|
|
930
|
+
children: t("editor.deny")
|
|
931
|
+
}), (0, react_jsx_runtime.jsx)("textarea", {
|
|
932
|
+
className: PermissionSelect_module_css_default.editorArea,
|
|
933
|
+
"data-dsh-axis-deny": axis,
|
|
934
|
+
"aria-label": t(keys.deny),
|
|
935
|
+
rows: 3,
|
|
936
|
+
spellCheck: false,
|
|
937
|
+
value: draft.deny,
|
|
938
|
+
disabled,
|
|
939
|
+
onChange: (event) => {
|
|
940
|
+
edit({ deny: event.target.value });
|
|
941
|
+
}
|
|
942
|
+
})]
|
|
943
|
+
}),
|
|
944
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
945
|
+
className: PermissionSelect_module_css_default.editorField,
|
|
946
|
+
"data-dsh-axis-groups": axis,
|
|
947
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
948
|
+
className: PermissionSelect_module_css_default.editorLabel,
|
|
949
|
+
children: t("editor.groups")
|
|
950
|
+
}), choices.length === 0 ? (0, react_jsx_runtime.jsx)("div", {
|
|
951
|
+
className: PermissionSelect_module_css_default.editorHint,
|
|
952
|
+
children: t("editor.groupsEmpty")
|
|
953
|
+
}) : (0, react_jsx_runtime.jsx)("div", {
|
|
954
|
+
className: PermissionSelect_module_css_default.groupList,
|
|
955
|
+
children: choices.map((choice) => (0, react_jsx_runtime.jsxs)("label", {
|
|
956
|
+
className: PermissionSelect_module_css_default.groupRow,
|
|
957
|
+
"data-dsh-group-choice": choice.id,
|
|
958
|
+
children: [
|
|
959
|
+
(0, react_jsx_runtime.jsx)("input", {
|
|
960
|
+
type: "checkbox",
|
|
961
|
+
"data-dsh-group-check": choice.id,
|
|
962
|
+
checked: choice.checked,
|
|
963
|
+
disabled,
|
|
964
|
+
onChange: (event) => {
|
|
965
|
+
toggleGroup(choice.id, event.target.checked);
|
|
966
|
+
}
|
|
967
|
+
}),
|
|
968
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
969
|
+
className: PermissionSelect_module_css_default.groupName,
|
|
970
|
+
children: choice.name
|
|
971
|
+
}),
|
|
972
|
+
choice.name === choice.id ? null : (0, react_jsx_runtime.jsx)("span", {
|
|
973
|
+
className: PermissionSelect_module_css_default.groupId,
|
|
974
|
+
children: choice.id
|
|
975
|
+
}),
|
|
976
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
977
|
+
className: PermissionSelect_module_css_default.groupCoverage,
|
|
978
|
+
children: coverageLabel(choice, t)
|
|
979
|
+
}),
|
|
980
|
+
!choice.missing ? null : (0, react_jsx_runtime.jsx)("span", {
|
|
981
|
+
className: PermissionSelect_module_css_default.groupMissing,
|
|
982
|
+
children: t("editor.groupMissing")
|
|
983
|
+
})
|
|
984
|
+
]
|
|
985
|
+
}, choice.id))
|
|
986
|
+
})]
|
|
987
|
+
}),
|
|
988
|
+
(0, react_jsx_runtime.jsx)("div", {
|
|
989
|
+
className: PermissionSelect_module_css_default.editorHint,
|
|
990
|
+
children: t("editor.hint")
|
|
991
|
+
}),
|
|
992
|
+
problem === "" ? null : (0, react_jsx_runtime.jsx)("div", {
|
|
993
|
+
className: PermissionSelect_module_css_default.editorProblem,
|
|
994
|
+
role: "alert",
|
|
995
|
+
children: problem
|
|
996
|
+
}),
|
|
997
|
+
failure === "none" ? null : (0, react_jsx_runtime.jsx)("div", {
|
|
998
|
+
className: PermissionSelect_module_css_default.editorProblem,
|
|
999
|
+
role: "alert",
|
|
1000
|
+
children: t(failure === "refused" ? "editor.notStored" : "editor.failed")
|
|
1001
|
+
}),
|
|
1002
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1003
|
+
className: PermissionSelect_module_css_default.editorActions,
|
|
1004
|
+
children: [(0, react_jsx_runtime.jsx)("button", {
|
|
1005
|
+
type: "button",
|
|
1006
|
+
className: PermissionSelect_module_css_default.editorSubmit,
|
|
1007
|
+
"data-dsh-axis-submit": axis,
|
|
1008
|
+
disabled: disabled || problem !== "",
|
|
1009
|
+
onClick: submit,
|
|
1010
|
+
children: t(sending ? "editor.sending" : "editor.submit")
|
|
1011
|
+
}), (0, react_jsx_runtime.jsx)("button", {
|
|
1012
|
+
type: "button",
|
|
1013
|
+
className: PermissionSelect_module_css_default.editorCancel,
|
|
1014
|
+
disabled: sending,
|
|
1015
|
+
onClick: onDismiss,
|
|
1016
|
+
children: t("editor.cancel")
|
|
1017
|
+
})]
|
|
1018
|
+
}),
|
|
1019
|
+
gate === null ? null : (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.RiskConfirmation, {
|
|
1020
|
+
open: true,
|
|
1021
|
+
title: t("confirm.title"),
|
|
1022
|
+
description: t("confirm.description"),
|
|
1023
|
+
acknowledgeLabel: t("confirm.acknowledge"),
|
|
1024
|
+
cancelLabel: t("confirm.cancel"),
|
|
1025
|
+
closeLabel: t("close"),
|
|
1026
|
+
confirmLabel: t("confirm.enable"),
|
|
1027
|
+
acknowledged,
|
|
1028
|
+
disabled: locked,
|
|
1029
|
+
onAcknowledgedChange: setAcknowledged,
|
|
1030
|
+
onCancel: () => {
|
|
1031
|
+
setAcknowledged(false);
|
|
1032
|
+
setGate(null);
|
|
1033
|
+
},
|
|
1034
|
+
onConfirm: () => {
|
|
1035
|
+
const pending = gate;
|
|
1036
|
+
setGate(null);
|
|
1037
|
+
setAcknowledged(false);
|
|
1038
|
+
if (pending !== null) send(pending.args, pending.expected);
|
|
1039
|
+
}
|
|
1040
|
+
})
|
|
1041
|
+
]
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* 一个组覆盖哪些轴的说明文案。
|
|
1046
|
+
*
|
|
1047
|
+
* 选择器两个并排、没有可见标题,所以「这个组对这根轴什么也不做」必须明说:写侧组
|
|
1048
|
+
* 出现在读轴的选择器里不报错,但它对读轴一条规则也不贡献。
|
|
1049
|
+
* @param choice - 多选列表里的一行。
|
|
1050
|
+
* @param t - 词典座位。
|
|
1051
|
+
* @returns 一句话说明。
|
|
1052
|
+
*/
|
|
1053
|
+
function coverageLabel(choice, t) {
|
|
1054
|
+
if (choice.missing) return t("editor.groupMissing");
|
|
1055
|
+
if (choice.coverage === "both") return t("editor.groupBoth");
|
|
1056
|
+
if (choice.coverage === "read") return t("editor.groupRead");
|
|
1057
|
+
if (choice.coverage === "write") return t("editor.groupWrite");
|
|
1058
|
+
return t("editor.groupNone");
|
|
1059
|
+
}
|
|
1060
|
+
/** 一句拒绝说明对应的词典键。 */
|
|
1061
|
+
function problemFor(key) {
|
|
1062
|
+
if (key === "baseUnknown") return "editor.problemBase";
|
|
1063
|
+
if (key === "notAbsolute") return "editor.problemPath";
|
|
1064
|
+
if (key === "commaUnsupported") return "editor.problemComma";
|
|
1065
|
+
return "editor.problemGroupId";
|
|
1066
|
+
}
|
|
1067
|
+
//#endregion
|
|
1068
|
+
//#region lib/types/client/permission/PermissionSelect.js
|
|
1069
|
+
/** Every value an axis offers when the Host catalog advertises none of the four. */
|
|
1070
|
+
const AXIS_FALLBACK_VALUES = [
|
|
1071
|
+
"deny",
|
|
1072
|
+
"workspace",
|
|
1073
|
+
"all",
|
|
1074
|
+
"custom"
|
|
1075
|
+
];
|
|
1076
|
+
const permissionGlyphs = /* @__PURE__ */ new Map([
|
|
1077
|
+
["deny", (0, react_jsx_runtime.jsxs)("svg", {
|
|
1078
|
+
width: "16",
|
|
1079
|
+
height: "16",
|
|
1080
|
+
viewBox: "0 0 16 16",
|
|
1081
|
+
fill: "none",
|
|
1082
|
+
"aria-hidden": true,
|
|
1083
|
+
children: [(0, react_jsx_runtime.jsx)("path", {
|
|
1084
|
+
d: _deepseek_ai_dsh_client_ui_primitives.SHIELD_OUTLINE_PATH,
|
|
1085
|
+
stroke: "currentColor",
|
|
1086
|
+
strokeWidth: "1",
|
|
1087
|
+
strokeLinejoin: "round"
|
|
1088
|
+
}), (0, react_jsx_runtime.jsx)("path", {
|
|
1089
|
+
d: "M5.4 5.4L10.6 10.6M10.6 5.4L5.4 10.6",
|
|
1090
|
+
stroke: "currentColor",
|
|
1091
|
+
strokeWidth: "1.4",
|
|
1092
|
+
strokeLinecap: "round"
|
|
1093
|
+
})]
|
|
1094
|
+
})],
|
|
1095
|
+
["workspace", (0, react_jsx_runtime.jsxs)("svg", {
|
|
1096
|
+
width: "16",
|
|
1097
|
+
height: "16",
|
|
1098
|
+
viewBox: "0 0 16 16",
|
|
1099
|
+
fill: "none",
|
|
1100
|
+
"aria-hidden": true,
|
|
1101
|
+
children: [(0, react_jsx_runtime.jsx)("path", {
|
|
1102
|
+
d: _deepseek_ai_dsh_client_ui_primitives.SHIELD_OUTLINE_PATH,
|
|
1103
|
+
stroke: "currentColor",
|
|
1104
|
+
strokeWidth: "1",
|
|
1105
|
+
strokeLinejoin: "round"
|
|
1106
|
+
}), (0, react_jsx_runtime.jsx)("path", {
|
|
1107
|
+
d: "M12.1654 5.7552L8.9447 9.41475C8.73044 9.65816 8.53628 9.8804 8.35774 10.0423C8.1713 10.2114 7.94235 10.3717 7.64016 10.4254C7.48207 10.4535 7.32 10.4552 7.16151 10.4294C6.85843 10.3801 6.62728 10.2223 6.43836 10.0559C6.25752 9.89653 6.06037 9.67732 5.84264 9.43705L4.72925 8.20897L5.63557 7.38707L6.74897 8.61594C6.98603 8.87755 7.12974 9.03533 7.24673 9.13839C7.31033 9.19443 7.34485 9.21476 7.35823 9.22122C7.38068 9.22484 7.40352 9.22515 7.42593 9.22122C7.40522 9.22502 7.42893 9.23294 7.53583 9.136C7.65132 9.03126 7.79316 8.87139 8.02643 8.60638L11.2479 4.94763L12.1654 5.7552Z",
|
|
1108
|
+
fill: "currentColor"
|
|
1109
|
+
})]
|
|
1110
|
+
})],
|
|
1111
|
+
["all", (0, react_jsx_runtime.jsxs)("svg", {
|
|
1112
|
+
width: "16",
|
|
1113
|
+
height: "16",
|
|
1114
|
+
viewBox: "0 0 16 16",
|
|
1115
|
+
fill: "none",
|
|
1116
|
+
"aria-hidden": true,
|
|
1117
|
+
children: [
|
|
1118
|
+
(0, react_jsx_runtime.jsx)("path", {
|
|
1119
|
+
d: _deepseek_ai_dsh_client_ui_primitives.SHIELD_OUTLINE_PATH,
|
|
1120
|
+
stroke: "currentColor",
|
|
1121
|
+
strokeWidth: "1",
|
|
1122
|
+
strokeLinejoin: "round"
|
|
1123
|
+
}),
|
|
1124
|
+
(0, react_jsx_runtime.jsx)("path", {
|
|
1125
|
+
d: "M9.10094 4.5V8.75939H7.59888V4.5H9.10094Z",
|
|
1126
|
+
fill: "currentColor"
|
|
1127
|
+
}),
|
|
1128
|
+
(0, react_jsx_runtime.jsx)("path", {
|
|
1129
|
+
d: "M9.10094 9.8114V11.5H7.59888V9.8114H9.10094Z",
|
|
1130
|
+
fill: "currentColor"
|
|
1131
|
+
})
|
|
1132
|
+
]
|
|
1133
|
+
})],
|
|
1134
|
+
["custom", (0, react_jsx_runtime.jsxs)("svg", {
|
|
1135
|
+
width: "16",
|
|
1136
|
+
height: "16",
|
|
1137
|
+
viewBox: "0 0 16 16",
|
|
1138
|
+
fill: "none",
|
|
1139
|
+
"aria-hidden": true,
|
|
1140
|
+
children: [(0, react_jsx_runtime.jsx)("path", {
|
|
1141
|
+
d: _deepseek_ai_dsh_client_ui_primitives.SHIELD_OUTLINE_PATH,
|
|
1142
|
+
stroke: "currentColor",
|
|
1143
|
+
strokeWidth: "1",
|
|
1144
|
+
strokeLinejoin: "round"
|
|
1145
|
+
}), (0, react_jsx_runtime.jsx)("path", {
|
|
1146
|
+
d: "M6.4 5.2H11.6M6.4 7.6H11.6M6.4 10H9.8",
|
|
1147
|
+
stroke: "currentColor",
|
|
1148
|
+
strokeWidth: "1.3",
|
|
1149
|
+
strokeLinecap: "round"
|
|
1150
|
+
})]
|
|
1151
|
+
})],
|
|
1152
|
+
["unknown", (0, react_jsx_runtime.jsxs)("svg", {
|
|
1153
|
+
width: "16",
|
|
1154
|
+
height: "16",
|
|
1155
|
+
viewBox: "0 0 16 16",
|
|
1156
|
+
fill: "none",
|
|
1157
|
+
"aria-hidden": true,
|
|
1158
|
+
children: [
|
|
1159
|
+
(0, react_jsx_runtime.jsx)("path", {
|
|
1160
|
+
d: _deepseek_ai_dsh_client_ui_primitives.SHIELD_OUTLINE_PATH,
|
|
1161
|
+
stroke: "currentColor",
|
|
1162
|
+
strokeWidth: "1",
|
|
1163
|
+
strokeLinejoin: "round"
|
|
1164
|
+
}),
|
|
1165
|
+
(0, react_jsx_runtime.jsx)("path", {
|
|
1166
|
+
d: "M5.6 6.6C5.6 5.6 6.15 5 7.05 5C7.95 5 8.5 5.5 8.5 6.3C8.5 7.4 7.05 7.4 7.05 8.6",
|
|
1167
|
+
stroke: "currentColor",
|
|
1168
|
+
strokeWidth: "1.2",
|
|
1169
|
+
strokeLinecap: "round",
|
|
1170
|
+
fill: "none"
|
|
1171
|
+
}),
|
|
1172
|
+
(0, react_jsx_runtime.jsx)("path", {
|
|
1173
|
+
d: "M7.05 10.3V10.9",
|
|
1174
|
+
stroke: "currentColor",
|
|
1175
|
+
strokeWidth: "1.4",
|
|
1176
|
+
strokeLinecap: "round"
|
|
1177
|
+
})
|
|
1178
|
+
]
|
|
1179
|
+
})]
|
|
1180
|
+
]);
|
|
1181
|
+
/**
|
|
1182
|
+
* Glyph for one axis value. An absent value gets the `unknown` glyph rather
|
|
1183
|
+
* than nothing: the seat still has to read as an axis control.
|
|
1184
|
+
* @param value - the axis value, or `undefined` when none can be named.
|
|
1185
|
+
* @returns the glyph node.
|
|
1186
|
+
*/
|
|
1187
|
+
function permissionGlyph(value) {
|
|
1188
|
+
return permissionGlyphs.get(value ?? "unknown");
|
|
1189
|
+
}
|
|
1190
|
+
/** Locale key naming one axis's dropdown. */
|
|
1191
|
+
const AXIS_LABEL_KEY = {
|
|
1192
|
+
read: "readLabel",
|
|
1193
|
+
write: "writeLabel"
|
|
1194
|
+
};
|
|
1195
|
+
/**
|
|
1196
|
+
* Render one axis dropdown. The two dropdowns keep independent open, busy and
|
|
1197
|
+
* confirmation state, so a switch on one axis never disturbs the other's
|
|
1198
|
+
* displayed value.
|
|
1199
|
+
*
|
|
1200
|
+
* Picking `deny` or `workspace` applies directly: neither one widens access.
|
|
1201
|
+
* Picking `all` opens the risk confirmation first — it hands the whole host to
|
|
1202
|
+
* that axis — and only an acknowledged confirmation submits. Picking `custom`
|
|
1203
|
+
* opens {@link PermissionCustomEditor} instead of submitting: `custom` alone is
|
|
1204
|
+
* not a policy statement, and the host refuses it rather than guessing a base.
|
|
1205
|
+
* The editor runs the same gate on its own submission when that submission
|
|
1206
|
+
* widens access, so both routes to a widened axis pass one acknowledgement.
|
|
1207
|
+
*
|
|
1208
|
+
* The gate is per axis on purpose: the confirmation names the axis it is about,
|
|
1209
|
+
* which is the only thing that tells two side-by-side dropdowns apart.
|
|
1210
|
+
* @param props - the axis, its values, the locale seat, and the write path.
|
|
1211
|
+
* @returns the dropdown trigger and its menu, plus the editor while it is open.
|
|
1212
|
+
*/
|
|
1213
|
+
function AxisPicker({ axis, current, currentState, narrowing, values, locked, t, select, global, groups, pending, onPending }) {
|
|
1214
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
1215
|
+
const [editing, setEditing] = (0, react.useState)(false);
|
|
1216
|
+
const [gate, setGate] = (0, react.useState)(null);
|
|
1217
|
+
const [acknowledged, setAcknowledged] = (0, react.useState)(false);
|
|
1218
|
+
const shown = pending ?? current;
|
|
1219
|
+
const shownName = axisValueName(shown, axis, (key) => t(key));
|
|
1220
|
+
const notice = axis === "write" ? narrowingNotice(narrowing, t) : void 0;
|
|
1221
|
+
const label = t(AXIS_LABEL_KEY[axis], { name: shownName });
|
|
1222
|
+
const marker = notice?.marker ?? null;
|
|
1223
|
+
const items = values.map((value) => ({
|
|
1224
|
+
id: value,
|
|
1225
|
+
label: axisLabel(value, axis, (key) => t(key)),
|
|
1226
|
+
icon: permissionGlyph(value)
|
|
1227
|
+
}));
|
|
1228
|
+
/**
|
|
1229
|
+
* 真正写下去:乐观取值 → 提交 → 无论成败都收回乐观取值。
|
|
1230
|
+
*
|
|
1231
|
+
* 乐观取值是给人看的即时反馈,不是结论:`landed` 为假时它被收回,触发器于是回到
|
|
1232
|
+
* 当刻真的生效的那个取值,而不是停在一个宿主并没有接受的取值上。
|
|
1233
|
+
*/
|
|
1234
|
+
const settle = (value, args) => {
|
|
1235
|
+
onPending(value);
|
|
1236
|
+
return select(axis, args, { kind: value }).catch(() => false).then((landed) => {
|
|
1237
|
+
onPending(null);
|
|
1238
|
+
return landed;
|
|
1239
|
+
});
|
|
1240
|
+
};
|
|
1241
|
+
/**
|
|
1242
|
+
* 一次取值改动。放开权限的取值先挂起等确认,其余直接写。
|
|
1243
|
+
* @param value - 轴取值。
|
|
1244
|
+
* @param args - 交给宿主 `/permission` 的完整参数串。
|
|
1245
|
+
* @returns 宿主结清后的结果;挂起期间返回的是确认后的结果。
|
|
1246
|
+
*/
|
|
1247
|
+
const request = (value, args) => {
|
|
1248
|
+
if (!widensAccess(value)) return settle(value, args);
|
|
1249
|
+
setAcknowledged(false);
|
|
1250
|
+
setGate(value);
|
|
1251
|
+
return Promise.resolve(false);
|
|
1252
|
+
};
|
|
1253
|
+
/** 确认框点了「启用」:提交这次已经过用户明示同意的改动。 */
|
|
1254
|
+
const confirm = () => {
|
|
1255
|
+
const value = gate;
|
|
1256
|
+
if (value === null) return;
|
|
1257
|
+
setGate(null);
|
|
1258
|
+
setAcknowledged(false);
|
|
1259
|
+
settle(value, `${axis}:${value}`);
|
|
1260
|
+
};
|
|
1261
|
+
const choose = (id) => {
|
|
1262
|
+
setOpen(false);
|
|
1263
|
+
const picked = values.find((value) => value === id);
|
|
1264
|
+
if (picked === void 0) return;
|
|
1265
|
+
if (picked === "custom") {
|
|
1266
|
+
setEditing(true);
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
if (picked === shown) return;
|
|
1270
|
+
request(picked, `${axis}:${picked}`);
|
|
1271
|
+
};
|
|
1272
|
+
return (0, react_jsx_runtime.jsxs)("span", {
|
|
1273
|
+
className: PermissionSelect_module_css_default.seat,
|
|
1274
|
+
children: [
|
|
1275
|
+
(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
|
|
1276
|
+
open,
|
|
1277
|
+
items,
|
|
1278
|
+
selectedId: shown,
|
|
1279
|
+
onSelect: choose,
|
|
1280
|
+
onClose: () => {
|
|
1281
|
+
setOpen(false);
|
|
1282
|
+
},
|
|
1283
|
+
side: "top",
|
|
1284
|
+
portal: true,
|
|
1285
|
+
anchor: (0, react_jsx_runtime.jsxs)("button", {
|
|
1286
|
+
type: "button",
|
|
1287
|
+
className: PermissionSelect_module_css_default.trigger,
|
|
1288
|
+
"aria-label": label,
|
|
1289
|
+
"aria-expanded": editing,
|
|
1290
|
+
disabled: locked || pending !== null,
|
|
1291
|
+
onClick: () => {
|
|
1292
|
+
setOpen(!open);
|
|
1293
|
+
},
|
|
1294
|
+
children: [
|
|
1295
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
1296
|
+
className: PermissionSelect_module_css_default.triggerIcon,
|
|
1297
|
+
"aria-hidden": true,
|
|
1298
|
+
children: permissionGlyph(shown)
|
|
1299
|
+
}),
|
|
1300
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
1301
|
+
className: PermissionSelect_module_css_default.triggerLabel,
|
|
1302
|
+
children: shownName
|
|
1303
|
+
}),
|
|
1304
|
+
marker === null ? null : (0, react_jsx_runtime.jsx)("span", {
|
|
1305
|
+
className: PermissionSelect_module_css_default.narrowingMark,
|
|
1306
|
+
"data-dsh-narrowing-mark": axis,
|
|
1307
|
+
title: marker,
|
|
1308
|
+
"aria-label": marker,
|
|
1309
|
+
role: "img"
|
|
1310
|
+
}),
|
|
1311
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
1312
|
+
className: clsx(PermissionSelect_module_css_default.chevron, open && PermissionSelect_module_css_default.chevronOpen),
|
|
1313
|
+
"aria-hidden": true,
|
|
1314
|
+
children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineMedium, {})
|
|
1315
|
+
})
|
|
1316
|
+
]
|
|
1317
|
+
})
|
|
1318
|
+
}),
|
|
1319
|
+
editing ? (0, react_jsx_runtime.jsx)(PermissionCustomEditor, {
|
|
1320
|
+
axis,
|
|
1321
|
+
current: currentState,
|
|
1322
|
+
global: global(axis),
|
|
1323
|
+
notice,
|
|
1324
|
+
groups,
|
|
1325
|
+
locked,
|
|
1326
|
+
busy: pending !== null,
|
|
1327
|
+
onSubmit: (args, expected) => {
|
|
1328
|
+
onPending("custom");
|
|
1329
|
+
return select(axis, args, expected).finally(() => {
|
|
1330
|
+
onPending(null);
|
|
1331
|
+
});
|
|
1332
|
+
},
|
|
1333
|
+
onDismiss: () => {
|
|
1334
|
+
setEditing(false);
|
|
1335
|
+
},
|
|
1336
|
+
t
|
|
1337
|
+
}) : null,
|
|
1338
|
+
gate === null ? null : (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.RiskConfirmation, {
|
|
1339
|
+
open: true,
|
|
1340
|
+
title: t("confirm.title"),
|
|
1341
|
+
description: t("confirm.description"),
|
|
1342
|
+
acknowledgeLabel: t("confirm.acknowledge"),
|
|
1343
|
+
cancelLabel: t("confirm.cancel"),
|
|
1344
|
+
closeLabel: t("close"),
|
|
1345
|
+
confirmLabel: t("confirm.enable"),
|
|
1346
|
+
acknowledged,
|
|
1347
|
+
disabled: locked,
|
|
1348
|
+
onAcknowledgedChange: setAcknowledged,
|
|
1349
|
+
onCancel: () => {
|
|
1350
|
+
setAcknowledged(false);
|
|
1351
|
+
setGate(null);
|
|
1352
|
+
},
|
|
1353
|
+
onConfirm: confirm
|
|
1354
|
+
})
|
|
1355
|
+
]
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
function PermissionSelect({ locked, select, global, groupLibrary, sessionAxes, usePermissionCatalog, t, sessionId }) {
|
|
1359
|
+
const catalog = usePermissionCatalog((state) => state.value);
|
|
1360
|
+
const view = sessionAxes.useAxes(sessionId);
|
|
1361
|
+
const [pending, setPending] = (0, react.useState)({
|
|
1362
|
+
read: null,
|
|
1363
|
+
write: null
|
|
1364
|
+
});
|
|
1365
|
+
if (catalog === null) return null;
|
|
1366
|
+
const axisValues = catalog.options.map((option) => option.value).filter((value) => value === "deny" || value === "workspace" || value === "all" || value === "custom");
|
|
1367
|
+
/**
|
|
1368
|
+
* One seat's props. Never returns nothing: an axis this build cannot name is a
|
|
1369
|
+
* seat showing "未设置", because a permission control that disappears reads as
|
|
1370
|
+
* a removed feature, and the person looking at it has no way to tell the two
|
|
1371
|
+
* apart.
|
|
1372
|
+
* @param axis - which axis this seat edits.
|
|
1373
|
+
* @returns that seat's props.
|
|
1374
|
+
*/
|
|
1375
|
+
const seat = (axis) => {
|
|
1376
|
+
const scope = view.state === "unknown" ? void 0 : view.axes[axis];
|
|
1377
|
+
return {
|
|
1378
|
+
axis,
|
|
1379
|
+
current: scope === void 0 ? void 0 : axisValueOf$2(scope.kind),
|
|
1380
|
+
narrowing: view.state === "unknown" ? void 0 : view.axes.narrowing,
|
|
1381
|
+
currentState: scope,
|
|
1382
|
+
values: axisValues.length > 0 ? axisValues : AXIS_FALLBACK_VALUES,
|
|
1383
|
+
locked,
|
|
1384
|
+
t,
|
|
1385
|
+
select,
|
|
1386
|
+
global,
|
|
1387
|
+
groups: groupLibrary,
|
|
1388
|
+
pending: pending[axis],
|
|
1389
|
+
onPending: (value) => {
|
|
1390
|
+
setPending((state) => ({
|
|
1391
|
+
...state,
|
|
1392
|
+
[axis]: value
|
|
1393
|
+
}));
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
};
|
|
1397
|
+
return (0, react_jsx_runtime.jsxs)("div", {
|
|
1398
|
+
className: PermissionSelect_module_css_default.axes,
|
|
1399
|
+
children: [(0, react_jsx_runtime.jsx)(AxisPicker, { ...seat("read") }), (0, react_jsx_runtime.jsx)(AxisPicker, { ...seat("write") })]
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
//#endregion
|
|
1403
|
+
//#region \0t4-css:M:\T4\out\release\dsh-dual-axis\packages\dual-axis-ui\src\client\permission\PermissionRow.module.css.mjs
|
|
1404
|
+
const css = ".jUZ4eG_row{border-bottom:.5px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:16px 0;display:flex}.jUZ4eG_rowText{flex-direction:column;flex:1;gap:4px;min-width:0;padding-right:48px;display:flex}.jUZ4eG_title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}.jUZ4eG_desc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}.jUZ4eG_selector{background:var(--dsw-alias-bg-module-platform);height:36px;font:inherit;color:var(--dsw-alias-label-primary);cursor:pointer;border:none;border-radius:18px;align-items:center;gap:12px;padding:0 14px;font-size:14px;line-height:22px;display:inline-flex}.jUZ4eG_selector:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.jUZ4eG_selector:disabled{cursor:default}.jUZ4eG_chevron{flex:none}";
|
|
1405
|
+
const tagId = "@t4r71/dsh-dual-axis-ui/PermissionRow.module.css";
|
|
1406
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
1407
|
+
const tag = document.createElement("style");
|
|
1408
|
+
tag.dataset.plugin = "@t4r71/dsh-dual-axis-ui";
|
|
1409
|
+
tag.dataset.pluginCss = tagId;
|
|
1410
|
+
tag.textContent = css;
|
|
1411
|
+
document.head.appendChild(tag);
|
|
1412
|
+
}
|
|
1413
|
+
var PermissionRow_module_css_default = {
|
|
1414
|
+
"chevron": "jUZ4eG_chevron",
|
|
1415
|
+
"desc": "jUZ4eG_desc",
|
|
1416
|
+
"row": "jUZ4eG_row",
|
|
1417
|
+
"rowText": "jUZ4eG_rowText",
|
|
1418
|
+
"selector": "jUZ4eG_selector",
|
|
1419
|
+
"title": "jUZ4eG_title"
|
|
1420
|
+
};
|
|
1421
|
+
//#endregion
|
|
1422
|
+
//#region lib/types/client/permission/PermissionRow.js
|
|
1423
|
+
/**
|
|
1424
|
+
* Permission preference row: the default preset for subsequently created
|
|
1425
|
+
* sessions. Current-session switches remain on the composer's read/write
|
|
1426
|
+
* axis dropdowns.
|
|
1427
|
+
*/
|
|
1428
|
+
/**
|
|
1429
|
+
* Render the new-session Permission default selector. The row keeps the host's
|
|
1430
|
+
* single preset enum on purpose: its options come from the Host settings
|
|
1431
|
+
* schema (which still publishes one bundled preset per mode), not from the
|
|
1432
|
+
* current-session two-axis catalog, so splitting it here would invent a second
|
|
1433
|
+
* source of truth for the same durable value. A visible pick applies directly —
|
|
1434
|
+
* the full-access acknowledgement step was removed with the composer's.
|
|
1435
|
+
* @param props - composed slot props.
|
|
1436
|
+
* @returns the row, or null when the host does not expose permission settings.
|
|
1437
|
+
*/
|
|
1438
|
+
function PermissionRow({ load, select, usePermission, t }) {
|
|
1439
|
+
const state = usePermission((snapshot) => snapshot);
|
|
1440
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
1441
|
+
(0, react.useEffect)(() => {
|
|
1442
|
+
load();
|
|
1443
|
+
}, [load]);
|
|
1444
|
+
(0, react.useEffect)(() => {
|
|
1445
|
+
if (state.writable && state.status !== "unavailable") return;
|
|
1446
|
+
setOpen(false);
|
|
1447
|
+
}, [state.status, state.writable]);
|
|
1448
|
+
if (state.status === "unavailable") return null;
|
|
1449
|
+
const selected = state.options.find((option) => option.id === state.currentValue);
|
|
1450
|
+
const busy = state.status === "loading" || state.status === "saving";
|
|
1451
|
+
const optionLabel = (option) => displayPermissionPreset(option.id, option.label, t);
|
|
1452
|
+
const label = selected !== void 0 ? optionLabel(selected) : busy ? t("loading") : t("unavailable");
|
|
1453
|
+
const description = state.error ?? t("description");
|
|
1454
|
+
return (0, react_jsx_runtime.jsxs)("div", {
|
|
1455
|
+
className: PermissionRow_module_css_default.row,
|
|
1456
|
+
children: [(0, react_jsx_runtime.jsxs)("div", {
|
|
1457
|
+
className: PermissionRow_module_css_default.rowText,
|
|
1458
|
+
children: [(0, react_jsx_runtime.jsx)("div", {
|
|
1459
|
+
className: PermissionRow_module_css_default.title,
|
|
1460
|
+
children: t("title")
|
|
1461
|
+
}), (0, react_jsx_runtime.jsx)("div", {
|
|
1462
|
+
className: PermissionRow_module_css_default.desc,
|
|
1463
|
+
role: state.error === null ? void 0 : "alert",
|
|
1464
|
+
children: description
|
|
1465
|
+
})]
|
|
1466
|
+
}), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
|
|
1467
|
+
open,
|
|
1468
|
+
onClose: () => {
|
|
1469
|
+
setOpen(false);
|
|
1470
|
+
},
|
|
1471
|
+
items: state.options.map((option) => ({
|
|
1472
|
+
id: option.id,
|
|
1473
|
+
label: optionLabel(option)
|
|
1474
|
+
})),
|
|
1475
|
+
selectedId: state.currentValue,
|
|
1476
|
+
onSelect: (id) => {
|
|
1477
|
+
setOpen(false);
|
|
1478
|
+
if (id === state.currentValue) return;
|
|
1479
|
+
select(id);
|
|
1480
|
+
},
|
|
1481
|
+
align: "end",
|
|
1482
|
+
portal: true,
|
|
1483
|
+
anchor: (0, react_jsx_runtime.jsxs)("button", {
|
|
1484
|
+
type: "button",
|
|
1485
|
+
className: PermissionRow_module_css_default.selector,
|
|
1486
|
+
"aria-haspopup": "menu",
|
|
1487
|
+
"aria-expanded": open,
|
|
1488
|
+
disabled: busy || !state.writable || state.options.length === 0,
|
|
1489
|
+
onClick: () => {
|
|
1490
|
+
setOpen((value) => !value);
|
|
1491
|
+
},
|
|
1492
|
+
children: [label, (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineMedium, { className: PermissionRow_module_css_default.chevron })]
|
|
1493
|
+
})
|
|
1494
|
+
})]
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
//#endregion
|
|
1498
|
+
//#region lib/types/client/permission/settings-store.js
|
|
1499
|
+
/**
|
|
1500
|
+
* Permission default-settings controller. 权限描述符来自共享的 describe 镜像,可选
|
|
1501
|
+
* 取值来自进程级 {@link PermissionCatalogDirectory}(0.1.7 起动态预设枚举不再从
|
|
1502
|
+
* namespace schema 里挖,改由权限目录的 `defaultOptions` 提供);写入只针对
|
|
1503
|
+
* `defaultPreset`,带上描述符 revision,并把宿主接受的答案折回镜像。
|
|
1504
|
+
*/
|
|
1505
|
+
/** Permission's settings namespace on the host wire. */
|
|
1506
|
+
const PERMISSION_SETTINGS_NS = "permission";
|
|
1507
|
+
/**
|
|
1508
|
+
* Resolve the new-session choices from the permission domain's catalog.
|
|
1509
|
+
* @param view - 当前配置描述符。
|
|
1510
|
+
* @param catalog - 权限目录:可选预设与生效默认值。
|
|
1511
|
+
* @returns 当前取值,以及它允许的那些取值的标签。
|
|
1512
|
+
*/
|
|
1513
|
+
function permissionDefaultOf(view, catalog) {
|
|
1514
|
+
const currentValue = view.value?.defaultPreset ?? catalog.defaultPreset;
|
|
1515
|
+
const options = catalog.defaultOptions.map((option) => ({
|
|
1516
|
+
id: option.value,
|
|
1517
|
+
label: displayPermissionPreset(option.value, option.name)
|
|
1518
|
+
}));
|
|
1519
|
+
if (!options.some((option) => option.id === currentValue)) throw new Error("permission catalog does not advertise its current default");
|
|
1520
|
+
return {
|
|
1521
|
+
currentValue,
|
|
1522
|
+
options
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
/** Controller deriving the row from the shared mirror and writing the default through it. */
|
|
1526
|
+
var PermissionPresetSettingsController = class {
|
|
1527
|
+
describeFace;
|
|
1528
|
+
ctx;
|
|
1529
|
+
catalog;
|
|
1530
|
+
/** Row snapshot consumed through a bound selector hook. */
|
|
1531
|
+
store = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)({
|
|
1532
|
+
status: "idle",
|
|
1533
|
+
error: null,
|
|
1534
|
+
writable: false,
|
|
1535
|
+
currentValue: "",
|
|
1536
|
+
options: [],
|
|
1537
|
+
revision: 0
|
|
1538
|
+
});
|
|
1539
|
+
following;
|
|
1540
|
+
followingCatalog;
|
|
1541
|
+
saving = false;
|
|
1542
|
+
disposed = false;
|
|
1543
|
+
/**
|
|
1544
|
+
* @param describeFace - the shared mirror's read/fold face (descriptor and schema source).
|
|
1545
|
+
* @param ctx - the row plugin's context, whose `remote.settings` namespace
|
|
1546
|
+
* carries the `defaultPreset` write.
|
|
1547
|
+
* @param catalog - 已配置的权限可选值与生效默认值(进程级目录,与斜杠弹窗共用一份)。
|
|
1548
|
+
*/
|
|
1549
|
+
constructor(describeFace, ctx, catalog) {
|
|
1550
|
+
this.describeFace = describeFace;
|
|
1551
|
+
this.ctx = ctx;
|
|
1552
|
+
this.catalog = catalog;
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Begin following the mirror (idempotent) and reflect its current answer.
|
|
1556
|
+
* @returns settlement once the snapshot reflects the mirror.
|
|
1557
|
+
*/
|
|
1558
|
+
async load() {
|
|
1559
|
+
if (this.disposed) return;
|
|
1560
|
+
this.following ??= this.describeFace.subscribe(() => {
|
|
1561
|
+
this.derive();
|
|
1562
|
+
});
|
|
1563
|
+
this.followingCatalog ??= this.catalog.store.subscribe(() => {
|
|
1564
|
+
this.derive();
|
|
1565
|
+
});
|
|
1566
|
+
this.store.update((state) => {
|
|
1567
|
+
state.status = "loading";
|
|
1568
|
+
state.error = null;
|
|
1569
|
+
});
|
|
1570
|
+
await this.describeFace.ensure();
|
|
1571
|
+
if (this.describeFace.getSnapshot().status !== "unavailable") try {
|
|
1572
|
+
await this.catalog.load();
|
|
1573
|
+
} catch (error) {
|
|
1574
|
+
this.fail(error);
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
this.derive();
|
|
1578
|
+
}
|
|
1579
|
+
/**
|
|
1580
|
+
* Persist one preset as the default for subsequently created sessions.
|
|
1581
|
+
* A selection made while one is already saving is ignored — the row's
|
|
1582
|
+
* control is disabled during the save, so this only drops programmatic
|
|
1583
|
+
* double-submits rather than user intent.
|
|
1584
|
+
* @param preset - advertised preset key.
|
|
1585
|
+
* @returns nothing; {@link store} carries success or failure.
|
|
1586
|
+
*/
|
|
1587
|
+
async select(preset) {
|
|
1588
|
+
const state = this.store.getSnapshot();
|
|
1589
|
+
const view = this.describeFace.getSnapshot().view?.namespaces.find((entry) => entry.ns === PERMISSION_SETTINGS_NS);
|
|
1590
|
+
if (view === void 0 || !state.writable || this.saving) return;
|
|
1591
|
+
this.saving = true;
|
|
1592
|
+
this.store.update((draft) => {
|
|
1593
|
+
draft.status = "saving";
|
|
1594
|
+
draft.error = null;
|
|
1595
|
+
});
|
|
1596
|
+
let response;
|
|
1597
|
+
try {
|
|
1598
|
+
response = await this.ctx.remote.settings.mutate(PERMISSION_SETTINGS_NS, [{
|
|
1599
|
+
op: "set",
|
|
1600
|
+
path: ["defaultPreset"],
|
|
1601
|
+
value: preset
|
|
1602
|
+
}], view.revision);
|
|
1603
|
+
} finally {
|
|
1604
|
+
this.saving = false;
|
|
1605
|
+
}
|
|
1606
|
+
if (this.disposed) return;
|
|
1607
|
+
if (!response.ok) {
|
|
1608
|
+
this.fail(response.error);
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
this.describeFace.acceptView(response.value);
|
|
1612
|
+
}
|
|
1613
|
+
/** Stop following the mirror; later publishes leave the snapshot alone. */
|
|
1614
|
+
dispose() {
|
|
1615
|
+
this.disposed = true;
|
|
1616
|
+
this.following?.();
|
|
1617
|
+
this.following = void 0;
|
|
1618
|
+
this.followingCatalog?.();
|
|
1619
|
+
this.followingCatalog = void 0;
|
|
1620
|
+
}
|
|
1621
|
+
derive() {
|
|
1622
|
+
if (this.disposed || this.saving) return;
|
|
1623
|
+
const mirrored = this.describeFace.getSnapshot();
|
|
1624
|
+
if (mirrored.status === "unavailable") {
|
|
1625
|
+
this.store.update((state) => {
|
|
1626
|
+
state.status = "unavailable";
|
|
1627
|
+
state.writable = false;
|
|
1628
|
+
state.currentValue = "";
|
|
1629
|
+
state.options = [];
|
|
1630
|
+
});
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
if (mirrored.view === void 0) {
|
|
1634
|
+
if (mirrored.error !== null) this.fail(new Error(mirrored.error));
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
const view = mirrored.view.namespaces.find((entry) => entry.ns === PERMISSION_SETTINGS_NS);
|
|
1638
|
+
if (view === void 0) {
|
|
1639
|
+
this.store.update((state) => {
|
|
1640
|
+
state.status = "unavailable";
|
|
1641
|
+
state.writable = false;
|
|
1642
|
+
state.currentValue = "";
|
|
1643
|
+
state.options = [];
|
|
1644
|
+
});
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
try {
|
|
1648
|
+
const catalog = this.catalog.store.getSnapshot().value;
|
|
1649
|
+
if (catalog === null) return;
|
|
1650
|
+
const resolved = permissionDefaultOf(view, catalog);
|
|
1651
|
+
const { writable } = mirrored.view;
|
|
1652
|
+
this.store.update((state) => {
|
|
1653
|
+
state.status = "ready";
|
|
1654
|
+
state.error = null;
|
|
1655
|
+
state.writable = writable;
|
|
1656
|
+
state.currentValue = resolved.currentValue;
|
|
1657
|
+
state.options = resolved.options;
|
|
1658
|
+
state.revision = view.revision;
|
|
1659
|
+
});
|
|
1660
|
+
} catch (error) {
|
|
1661
|
+
this.fail(error);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
fail(error) {
|
|
1665
|
+
this.store.update((state) => {
|
|
1666
|
+
state.status = "error";
|
|
1667
|
+
state.error = error instanceof Error ? error.message : String(error);
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
};
|
|
1671
|
+
//#endregion
|
|
1672
|
+
//#region lib/types/client/permission/session-axes-seed.js
|
|
1673
|
+
/**
|
|
1674
|
+
* 记录缺席时两个下拉该显示的那一对轴:宿主 `seedPair` 的逐字镜像。
|
|
1675
|
+
*
|
|
1676
|
+
* 宿主那一份在 `@t4r71/dsh-dual-axis` 的 `src/session-store.ts`,是
|
|
1677
|
+
* `seedAxesFor` 的纯核:设置页那一行(`read` / `write` / `defaultGroups`),
|
|
1678
|
+
* 加上子代理子会话从**父会话当刻的记录**继承来的那一对,折成「这条会话在新存储里
|
|
1679
|
+
* 还没有记录时被钉下、也被判定路径执行的那一对」。记录落地之前,活读路径(模型提示词、
|
|
1680
|
+
* 读围栏、`/axis` 的兜底)交回的就是它,所以界面必须显示同一个值 —— 显示别的就是
|
|
1681
|
+
* 「界面显示 A、宿主执行 B」:设置页默认若是「禁止读」,那一轮仍然被当成「全盘读」。
|
|
1682
|
+
*
|
|
1683
|
+
* 为什么另起一个模块:它必须能在**零依赖**的程序里被直接加载,好让宿主包的
|
|
1684
|
+
* `tests/seed-parity.spec.ts` 把两侧喂同一组输入、断言输出逐字相等(这是防止两份
|
|
1685
|
+
* 实现以后各自漂移的唯一手段)。因此本文件不 import 任何东西,下面每一步都与宿主那
|
|
1686
|
+
* 一份一一对应:取值集合、校验顺序、键的插入顺序、连报错条件都一致。
|
|
1687
|
+
*
|
|
1688
|
+
* 与 `./session-axes-data.ts` 的分工:那边负责「文档里有没有这条会话的记录」,
|
|
1689
|
+
* 这边只负责「没有记录时那一对该是什么」。
|
|
1690
|
+
*
|
|
1691
|
+
* @module @deepseek-ai/dsh-client-ui-permission-presets/client/session-axes-seed
|
|
1692
|
+
*/
|
|
1693
|
+
/** 四种取值,校验顺序与宿主的 `AXIS_KINDS` 相同。 */
|
|
1694
|
+
const AXIS_KINDS$1 = [
|
|
1695
|
+
"deny",
|
|
1696
|
+
"workspace",
|
|
1697
|
+
"all",
|
|
1698
|
+
"custom"
|
|
1699
|
+
];
|
|
1700
|
+
/** 三个底座,校验顺序与宿主的 `AXIS_BASES` 相同。 */
|
|
1701
|
+
const AXIS_BASES$1 = [
|
|
1702
|
+
"deny",
|
|
1703
|
+
"workspace",
|
|
1704
|
+
"all"
|
|
1705
|
+
];
|
|
1706
|
+
/**
|
|
1707
|
+
* 读不懂任何输入时的那一对:宿主 `DEFAULT_AXES` 的同一对取值
|
|
1708
|
+
* (`packages/bundle/dual-axis/src/axis.ts` 的 `DEFAULT_READ_SCOPE` /
|
|
1709
|
+
* `DEFAULT_WRITE_SCOPE`)。
|
|
1710
|
+
*
|
|
1711
|
+
* 它**不再**是「会话没有记录」时的答案 —— 那是 {@link sessionAxesSeed} 算出来的
|
|
1712
|
+
* 种子。这一对只在设置页那一行整个读不懂时出现,而宿主在那同一种情形下交回的也正是
|
|
1713
|
+
* 它(`seedPair` 的 catch 与 `ensure` 的 catch 都答 `DEFAULT_AXES`)。
|
|
1714
|
+
*/
|
|
1715
|
+
const SESSION_AXES_FALLBACK = Object.freeze({
|
|
1716
|
+
read: Object.freeze({ kind: "all" }),
|
|
1717
|
+
write: Object.freeze({ kind: "workspace" })
|
|
1718
|
+
});
|
|
1719
|
+
/**
|
|
1720
|
+
* 一条配置字段是「普通对象」还是别的东西。数组也算对象,与宿主一致:数组随后会在
|
|
1721
|
+
* `kind` 那一关被拒,判定因此仍然落在同一处。
|
|
1722
|
+
* @param value - 待判定的取值。
|
|
1723
|
+
* @returns 是否是非 null 的 object。
|
|
1724
|
+
*/
|
|
1725
|
+
function isObjectLike(value) {
|
|
1726
|
+
return typeof value === "object" && value !== null;
|
|
1727
|
+
}
|
|
1728
|
+
/**
|
|
1729
|
+
* 解开一个 `.volatile()` 字段的 accessor。文档走的是 JSON,正常只会拿到普通值;
|
|
1730
|
+
* 保留这一步是为了与宿主逐字同形 —— 包括宿主在 `null` 上取属性会抛这一点,两侧因此
|
|
1731
|
+
* 在同一个输入上落到同一条兜底。
|
|
1732
|
+
* @param value - 字段取值。
|
|
1733
|
+
* @returns 底层取值。
|
|
1734
|
+
*/
|
|
1735
|
+
function unwrapVolatile(value) {
|
|
1736
|
+
if (value === void 0) return void 0;
|
|
1737
|
+
const accessor = value;
|
|
1738
|
+
return typeof accessor.get === "function" ? accessor.get() : value;
|
|
1739
|
+
}
|
|
1740
|
+
/**
|
|
1741
|
+
* 一条轴的路径表(`allow` / `deny`)。宿主 `pathList` 的镜像:必须是绝对路径,
|
|
1742
|
+
* 顺序即取值的一部分。
|
|
1743
|
+
* @param label - 报错里的轴名。
|
|
1744
|
+
* @param entry - 报错里的表名(`allow` 或 `deny`)。
|
|
1745
|
+
* @param value - 未信任的列表取值。
|
|
1746
|
+
* @returns 校验过的列表。
|
|
1747
|
+
* @throws 不是字符串数组,或含非绝对路径条目时。
|
|
1748
|
+
*/
|
|
1749
|
+
function pathList(label, entry, value) {
|
|
1750
|
+
if (value === void 0) return [];
|
|
1751
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) throw new Error(`dual-axis: ${label} ${entry} must be an array of absolute path strings`);
|
|
1752
|
+
for (const item of value) if (item.length === 0 || !isAbsoluteSpelling(item)) throw new Error(`dual-axis: ${label} ${entry} entry ${JSON.stringify(item)} must be an absolute path`);
|
|
1753
|
+
return [...value];
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1756
|
+
* 一条轴引用的组 id。宿主 `groupIds` 的镜像:只查「是非空字符串的名字」,
|
|
1757
|
+
* 按首次出现去重;这个名字在库里存不存在是判定时的事。
|
|
1758
|
+
* @param label - 报错里的轴名。
|
|
1759
|
+
* @param value - 未信任的 `groups` 取值。
|
|
1760
|
+
* @returns 去重后的 id 列表。
|
|
1761
|
+
* @throws 不是非空字符串数组时。
|
|
1762
|
+
*/
|
|
1763
|
+
function groupIds(label, value) {
|
|
1764
|
+
if (value === void 0) return [];
|
|
1765
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.length === 0)) throw new Error(`dual-axis: ${label} groups must be an array of non-empty rule-group ids`);
|
|
1766
|
+
return [...new Set(value)];
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1769
|
+
* 一个配置路径在拼写上是否绝对。宿主 `isAbsoluteSpelling` 的镜像:POSIX 与
|
|
1770
|
+
* Windows 两种拼法都收,真正绑定到本机的是随后的规范化。
|
|
1771
|
+
* @param path - 配置里的路径拼写。
|
|
1772
|
+
* @returns 是否绝对。
|
|
1773
|
+
*/
|
|
1774
|
+
function isAbsoluteSpelling(path) {
|
|
1775
|
+
return path.startsWith("/") || /^[A-Za-z]:[\\/]/.test(path) || path.startsWith("\\\\");
|
|
1776
|
+
}
|
|
1777
|
+
/**
|
|
1778
|
+
* 一条轴取值从文档(人手可能改过)收窄成合法的轴。宿主 `normalizeScope` 的镜像:
|
|
1779
|
+
* 读不懂就抛,绝不猜一个更宽或更窄的边界。
|
|
1780
|
+
* @param label - 报错里的轴名(`read` 或 `write`)。
|
|
1781
|
+
* @param value - 未信任的轴取值。
|
|
1782
|
+
* @returns 合法的轴取值。
|
|
1783
|
+
* @throws 不是四种取值之一,或 custom 的底座/路径不合法时。
|
|
1784
|
+
*/
|
|
1785
|
+
function normalizeAxis(label, value) {
|
|
1786
|
+
if (typeof value !== "object" || value === null) throw new Error(`dual-axis: ${label} must be an access-axis object, received ${JSON.stringify(value)}`);
|
|
1787
|
+
const candidate = value;
|
|
1788
|
+
if (typeof candidate.kind !== "string" || !AXIS_KINDS$1.includes(candidate.kind)) throw new Error(`dual-axis: ${label} carries unknown kind ${JSON.stringify(candidate.kind)} (expected: ${AXIS_KINDS$1.join(", ")})`);
|
|
1789
|
+
if (candidate.kind !== "custom") return { kind: candidate.kind };
|
|
1790
|
+
if (typeof candidate.base !== "string" || !AXIS_BASES$1.includes(candidate.base)) throw new Error(`dual-axis: ${label} custom base must be ${AXIS_BASES$1.join(", ")}, received ${JSON.stringify(candidate.base)}`);
|
|
1791
|
+
return {
|
|
1792
|
+
kind: "custom",
|
|
1793
|
+
base: candidate.base,
|
|
1794
|
+
groups: groupIds(label, candidate.groups),
|
|
1795
|
+
allow: pathList(label, "allow", candidate.allow),
|
|
1796
|
+
deny: pathList(label, "deny", candidate.deny)
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
/**
|
|
1800
|
+
* 设置页那一行折成一对轴。宿主 `axesOf` 的镜像:两侧缺席时各退回内置默认轴。
|
|
1801
|
+
* @param row - 那一行的取值,未信任。
|
|
1802
|
+
* @returns 那一行声明的两条轴。
|
|
1803
|
+
* @throws 任一条轴读不懂时。
|
|
1804
|
+
*/
|
|
1805
|
+
function axesOfRow(row) {
|
|
1806
|
+
const declared = isObjectLike(row) ? row : {};
|
|
1807
|
+
return {
|
|
1808
|
+
read: normalizeAxis("read", unwrapVolatile(declared.read) ?? { kind: "all" }),
|
|
1809
|
+
write: normalizeAxis("write", unwrapVolatile(declared.write) ?? { kind: "workspace" })
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
/**
|
|
1813
|
+
* 重新收窄另一处读出来的一对轴。宿主 `pairOf` 的镜像:宿主写下的那一对本就是收窄过的,
|
|
1814
|
+
* 而客户端从文档里读到的父会话记录是原始字节,两侧必须过同一道校验才谈得上逐字相等。
|
|
1815
|
+
* @param pair - 未信任的一对轴。
|
|
1816
|
+
* @returns 收窄后的一对轴。
|
|
1817
|
+
* @throws 任一成员不是合法轴取值时。
|
|
1818
|
+
*/
|
|
1819
|
+
function pairOf(pair) {
|
|
1820
|
+
return {
|
|
1821
|
+
read: normalizeAxis("read", pair.read),
|
|
1822
|
+
write: normalizeAxis("write", pair.write)
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
/**
|
|
1826
|
+
* 设置页那一行声明的 `defaultGroups`。宿主 `defaultGroupIds` 的镜像。
|
|
1827
|
+
* @param row - 那一行的取值,未信任。
|
|
1828
|
+
* @returns 去重后的组 id;没声明时为空。
|
|
1829
|
+
* @throws 声明了非法的组 id 时。
|
|
1830
|
+
*/
|
|
1831
|
+
function defaultGroupIds(row) {
|
|
1832
|
+
return groupIds("defaultGroups", isObjectLike(row) ? unwrapVolatile(row.defaultGroups) : void 0);
|
|
1833
|
+
}
|
|
1834
|
+
/**
|
|
1835
|
+
* 把新会话默认加载的组 id 挂到**自己就是 `custom` 的那条轴**上,其余轴按声明原样留下。
|
|
1836
|
+
* 宿主 `seedGroupReferences` 的镜像。
|
|
1837
|
+
*
|
|
1838
|
+
* 逐轴独立判断:只有 kind 本身就是 `custom` 的轴才带得住引用。判在设置页那一行上
|
|
1839
|
+
* (一个并不绑定到轴的勾选框)会把 `read: all` 提升成 `read: { kind: 'custom', base: 'all' }`,
|
|
1840
|
+
* 而两条轴谁都没被选成自定义。闭合成取值没有地方放 id,原样播种 —— 提升会报出一条
|
|
1841
|
+
* 设置页从没显示过的自定义范围。两条轴因此互不影响:一条挂组,另一条保持闭合取值。
|
|
1842
|
+
* @param axes - 被播种的那一对轴。
|
|
1843
|
+
* @param ids - 设置页那一行的 `defaultGroups`。
|
|
1844
|
+
* @returns 带上这些引用的那一对轴。
|
|
1845
|
+
*/
|
|
1846
|
+
function seedGroupReferences(axes, ids) {
|
|
1847
|
+
if (ids.length === 0) return axes;
|
|
1848
|
+
const attach = (axis) => axis.kind === "custom" ? {
|
|
1849
|
+
...axis,
|
|
1850
|
+
groups: [.../* @__PURE__ */ new Set([...axis.groups ?? [], ...ids])]
|
|
1851
|
+
} : axis;
|
|
1852
|
+
return {
|
|
1853
|
+
read: attach(axes.read),
|
|
1854
|
+
write: attach(axes.write)
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
/**
|
|
1858
|
+
* 记录缺席时该显示的那一对轴 —— 与宿主 `seedPair` 同一份计算、同一个兜底。
|
|
1859
|
+
*
|
|
1860
|
+
* 永不抛:设置页那一行读不懂、父会话记录读不懂,一律答
|
|
1861
|
+
* {@link SESSION_AXES_FALLBACK},与宿主在同一种输入上的答案相同。
|
|
1862
|
+
* @param row - 设置页那一行(`dual-axis` 命名空间)的取值,未信任。
|
|
1863
|
+
* @param inherited - 子代理子会话从父会话记录继承来的那一对,或 `undefined`。
|
|
1864
|
+
* @returns 这条会话此刻被钉下、也被被执行的那一对轴。
|
|
1865
|
+
*/
|
|
1866
|
+
function sessionAxesSeed(row, inherited) {
|
|
1867
|
+
try {
|
|
1868
|
+
return seedGroupReferences(inherited === void 0 ? axesOfRow(row) : pairOf(inherited), defaultGroupIds(row));
|
|
1869
|
+
} catch {
|
|
1870
|
+
return SESSION_AXES_FALLBACK;
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
//#endregion
|
|
1874
|
+
//#region lib/types/client/permission/session-axes-data.js
|
|
1875
|
+
/**
|
|
1876
|
+
* 会话轴在配置文档里的形状,以及按会话 id 取值的纯读取。
|
|
1877
|
+
*
|
|
1878
|
+
* 单独一个模块,唯一的理由是它必须能在**不被 store 依赖污染**的程序里被直接测到:
|
|
1879
|
+
* 本包的宿主半边把它存进自有的设置命名空间 `dual-axis-sessions`,字段名与结构是
|
|
1880
|
+
* 两边之间的接口,改一处就要改另一处。订阅与快照那一层在 `./session-axes.ts`。
|
|
1881
|
+
*
|
|
1882
|
+
* 这里只管**文档里有没有这条会话的记录**。「没有记录时该显示哪一对」是另一件事,
|
|
1883
|
+
* 由 `./session-axes-seed.ts` 按与宿主同一份计算给出 —— 那一对是设置页那一行
|
|
1884
|
+
* (加上子代理继承),不是内置默认对。
|
|
1885
|
+
*
|
|
1886
|
+
* @module @deepseek-ai/dsh-client-ui-permission-presets/client/session-axes-data
|
|
1887
|
+
*/
|
|
1888
|
+
/** 宿主存轴用的设置命名空间(Loader entry id)。 */
|
|
1889
|
+
const SESSION_AXES_NAMESPACE = "dual-axis-sessions";
|
|
1890
|
+
/** 设置页那一行的设置命名空间(Loader entry id):新会话的**种子**来源。 */
|
|
1891
|
+
const DUAL_AXIS_ROW_NAMESPACE = "dual-axis";
|
|
1892
|
+
/**
|
|
1893
|
+
* 从一条记录里读出宿主发布的收窄结论。
|
|
1894
|
+
*
|
|
1895
|
+
* 未信任输入:文档可能被人手改过,也可能带着旧版本写下的、没有这个成员的记录。
|
|
1896
|
+
* 前者读不懂就**丢掉**(当作没有收窄可显示),而不是把一份读不出来的报告渲染成
|
|
1897
|
+
* 「什么也没被削」—— 那正是最坏的那一种错。后者(成员整个缺席)本来就是「没有收窄」,
|
|
1898
|
+
* 与宿主围栏在那种记录下执行的范围一致。
|
|
1899
|
+
* @param value - 记录里的 `narrowing` 成员,未信任。
|
|
1900
|
+
* @returns 收窄结论,或 undefined(缺席 / 读不懂 / 两侧都说没有)。
|
|
1901
|
+
*/
|
|
1902
|
+
function narrowingOf(value) {
|
|
1903
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
1904
|
+
const candidate = value;
|
|
1905
|
+
if (typeof candidate.narrowed !== "boolean" || typeof candidate.lostUnbounded !== "boolean") return void 0;
|
|
1906
|
+
if (!candidate.narrowed) return void 0;
|
|
1907
|
+
const dropped = candidate.droppedRoots;
|
|
1908
|
+
if (!Array.isArray(dropped) || dropped.some((root) => typeof root !== "string")) return void 0;
|
|
1909
|
+
return {
|
|
1910
|
+
narrowed: true,
|
|
1911
|
+
droppedRoots: [...dropped],
|
|
1912
|
+
lostUnbounded: candidate.lostUnbounded
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
/**
|
|
1916
|
+
* 一份 describe 快照里,本命名空间那一行的取值。
|
|
1917
|
+
* @param namespaces - 快照里的全部命名空间行,或 undefined(还没有读到答案)。
|
|
1918
|
+
* @returns 那一行的取值,或 undefined(宿主没服务这一节)。
|
|
1919
|
+
*/
|
|
1920
|
+
function sessionAxesSectionValue(namespaces) {
|
|
1921
|
+
return namespaces?.find((row) => row.ns === SESSION_AXES_NAMESPACE)?.value;
|
|
1922
|
+
}
|
|
1923
|
+
/**
|
|
1924
|
+
* 一份 describe 快照里,设置页那一行(`dual-axis`)的取值 —— 新会话的种子。
|
|
1925
|
+
*
|
|
1926
|
+
* 它与 {@link sessionAxesSectionValue} 是**两节**,不能混:那一行的 read/write/defaultGroups
|
|
1927
|
+
* 描述的是**将要建立**的会话,判定路径不读它;而「这条会话没有记录」时判定路径执行的
|
|
1928
|
+
* 是种子算出来的那一对,所以界面必须能读到同一行才能显示同一个值。
|
|
1929
|
+
* @param namespaces - 快照里的全部命名空间行,或 undefined。
|
|
1930
|
+
* @returns 那一行的取值,或 undefined(宿主没服务这一节)。
|
|
1931
|
+
*/
|
|
1932
|
+
function sessionAxesRowValue(namespaces) {
|
|
1933
|
+
return namespaces?.find((row) => row.ns === DUAL_AXIS_ROW_NAMESPACE)?.value;
|
|
1934
|
+
}
|
|
1935
|
+
/**
|
|
1936
|
+
* 按会话 id 从一份文档快照里取出那一对轴。
|
|
1937
|
+
*
|
|
1938
|
+
* 未信任输入:文档可能被人手改过、也可能由别的写入方改过,所以只在形状真的读得懂时才
|
|
1939
|
+
* 返回取值;读不懂时返回 `undefined`,由 {@link sessionAxesView} 决定那是「照种子
|
|
1940
|
+
* 显示」还是「无从得知」。这里**不抛**:一个读不懂的会话轴不该让整个输入条崩掉。
|
|
1941
|
+
* @param value - 本命名空间那一行的取值。
|
|
1942
|
+
* @param sessionId - 要读的那条会话。
|
|
1943
|
+
* @returns 那条会话的轴对,或 undefined。
|
|
1944
|
+
*/
|
|
1945
|
+
function sessionAxesOf(value, sessionId) {
|
|
1946
|
+
if (sessionId === void 0) return void 0;
|
|
1947
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
1948
|
+
const field = value.axes;
|
|
1949
|
+
if (typeof field !== "object" || field === null || Array.isArray(field)) return void 0;
|
|
1950
|
+
const record = field[sessionId];
|
|
1951
|
+
if (typeof record !== "object" || record === null || Array.isArray(record)) return void 0;
|
|
1952
|
+
const pair = record;
|
|
1953
|
+
if (typeof pair.read !== "object" || pair.read === null) return void 0;
|
|
1954
|
+
if (typeof pair.write !== "object" || pair.write === null) return void 0;
|
|
1955
|
+
const narrowing = narrowingOf(pair.narrowing);
|
|
1956
|
+
return {
|
|
1957
|
+
read: pair.read,
|
|
1958
|
+
write: pair.write,
|
|
1959
|
+
...narrowing === void 0 ? {} : { narrowing }
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
/**
|
|
1963
|
+
* Resolve what the two dropdowns show for one session.
|
|
1964
|
+
*
|
|
1965
|
+
* Never returns nothing and never throws: an unreadable document, an unknown
|
|
1966
|
+
* session id, and a missing record each have a state of their own.
|
|
1967
|
+
* @param namespaces - every namespace row of one describe snapshot, or `undefined`
|
|
1968
|
+
* when the mirror has not delivered a document (which is NOT the same as a
|
|
1969
|
+
* document that carries no record for this session).
|
|
1970
|
+
* @param sessionId - the session to read; `undefined` when none is selected.
|
|
1971
|
+
* @param parentSessionId - the session's parent when it is a subagent child, which
|
|
1972
|
+
* changes the seed: a child inherits its parent's STORED pair rather than the
|
|
1973
|
+
* settings row, exactly as `inheritedAxes` does on the host. `undefined` for a
|
|
1974
|
+
* top-level session (and for a child whose lineage the client has not learned).
|
|
1975
|
+
* @returns the view the dropdowns render from.
|
|
1976
|
+
*/
|
|
1977
|
+
function sessionAxesView(namespaces, sessionId, parentSessionId) {
|
|
1978
|
+
if (sessionId === void 0) return { state: "unknown" };
|
|
1979
|
+
if (namespaces === void 0) return { state: "unknown" };
|
|
1980
|
+
const stored = sessionAxesSectionValue(namespaces);
|
|
1981
|
+
const axes = sessionAxesOf(stored, sessionId);
|
|
1982
|
+
if (axes !== void 0) return {
|
|
1983
|
+
state: "record",
|
|
1984
|
+
axes
|
|
1985
|
+
};
|
|
1986
|
+
const inherited = parentSessionId === void 0 ? void 0 : sessionAxesOf(stored, parentSessionId);
|
|
1987
|
+
return {
|
|
1988
|
+
state: "default",
|
|
1989
|
+
axes: sessionAxesSeed(sessionAxesRowValue(namespaces), inherited)
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
//#endregion
|
|
1993
|
+
//#region lib/types/client/permission/session-axes.js
|
|
1994
|
+
/**
|
|
1995
|
+
* 会话轴在本包浏览器半边的取值面:从共享的 settings describe 镜像里,按**会话 id**
|
|
1996
|
+
* 读出这条会话的那一对轴。
|
|
1997
|
+
*
|
|
1998
|
+
* 轴不再随会话日志下发(宿主半边把它存进自有的设置命名空间 `dual-axis-sessions`,
|
|
1999
|
+
* 见 `@t4r71/dsh-dual-axis/session-store`),于是客户端唯一的取数通道就是那份
|
|
2000
|
+
* 配置文档的镜像。变更通知因此是免费的:宿主写文档后发 `settings/document-updated`,
|
|
2001
|
+
* 该事件在转发白名单上,共享镜像已订阅它并重读(`ui-settings/src/client/index.ts:43`),
|
|
2002
|
+
* 本目录只是跟着镜像的快照走。
|
|
2003
|
+
*
|
|
2004
|
+
* 读的是**另一个命名空间**里的按会话分区字段,不是设置页那一行
|
|
2005
|
+
* (`dual-axis`):那一行的 read/write/defaultGroups 是新会话的种子,已经在跑的
|
|
2006
|
+
* 会话只认自己的记录。但**记录缺席**时两者交汇:那时宿主执行的正是那一行算出来的
|
|
2007
|
+
* 种子(见 `./session-axes-seed.ts`),所以本目录在没有记录时也要读那一行,才能显示
|
|
2008
|
+
* 与宿主同一个值。分组库仍由那一行的表单拥有,见 `./axis-editor.ts`。
|
|
2009
|
+
*
|
|
2010
|
+
* @module @deepseek-ai/dsh-client-ui-permission-presets/client/session-axes
|
|
2011
|
+
*/
|
|
2012
|
+
/**
|
|
2013
|
+
* 会话轴目录:把共享镜像的快照换成一个能直接喂给 `useSyncExternalStore` 的稳定值。
|
|
2014
|
+
*
|
|
2015
|
+
* 快照在这里重新发布,而不是把镜像的快照原样透出去:镜像每次重读都换一个对象,
|
|
2016
|
+
* 而重渲染的代价只有在这一层做一次比较才能省掉。比较是序列化比较 —— 镜像只在这份
|
|
2017
|
+
* 文档真的变了之后才发布(宿主只在 entry 的 raw 变化时发 `settings/document-updated`,
|
|
2018
|
+
* 见 `settings/src/index.ts:311-317`),所以那次序列化每次文档变更只做一次。
|
|
2019
|
+
*/
|
|
2020
|
+
var SessionAxesDirectory = class {
|
|
2021
|
+
describeFace;
|
|
2022
|
+
parentOf;
|
|
2023
|
+
local;
|
|
2024
|
+
following;
|
|
2025
|
+
/**
|
|
2026
|
+
* @param describeFace - 共享的 describe 镜像(`ctx.configForms.describe()`)。
|
|
2027
|
+
* @param parentOf - 这条会话的父会话 id;默认「不是子会话」,于是种子只按设置页那一行算。
|
|
2028
|
+
*/
|
|
2029
|
+
constructor(describeFace, parentOf = () => void 0) {
|
|
2030
|
+
this.describeFace = describeFace;
|
|
2031
|
+
this.parentOf = parentOf;
|
|
2032
|
+
this.local = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)(describeFace.getSnapshot());
|
|
2033
|
+
this.following = describeFace.subscribe(() => {
|
|
2034
|
+
this.fold();
|
|
2035
|
+
});
|
|
2036
|
+
this.fold();
|
|
2037
|
+
}
|
|
2038
|
+
/** 稳定快照,直接交给 `useSyncExternalStore`。 */
|
|
2039
|
+
getSnapshot = () => this.local.getSnapshot();
|
|
2040
|
+
/** 订阅本地快照的替换。 */
|
|
2041
|
+
subscribe = (listener) => this.local.subscribe(listener);
|
|
2042
|
+
/**
|
|
2043
|
+
* 一条会话当刻该显示的轴。同步,供编辑器取初值与提交后的核对使用。
|
|
2044
|
+
* @param sessionId - 当前会话 id;没有选中会话时是 undefined。
|
|
2045
|
+
* @returns 三态取值:有记录 / 照这条会话的种子显示 / 无从得知。
|
|
2046
|
+
*/
|
|
2047
|
+
axesOf = (sessionId) => sessionAxesView(this.local.getSnapshot().view?.namespaces, sessionId, sessionId === void 0 ? void 0 : this.parentOf(sessionId));
|
|
2048
|
+
/** 停掉对镜像的跟随;之后的发布不再进入本目录。 */
|
|
2049
|
+
dispose() {
|
|
2050
|
+
this.following();
|
|
2051
|
+
}
|
|
2052
|
+
fold() {
|
|
2053
|
+
const held = this.local.getSnapshot();
|
|
2054
|
+
const next = this.describeFace.getSnapshot();
|
|
2055
|
+
if (JSON.stringify(next.view?.namespaces ?? null) === JSON.stringify(held.view?.namespaces ?? null) && next.status === held.status && next.error === held.error) return;
|
|
2056
|
+
this.local.set(next);
|
|
2057
|
+
}
|
|
2058
|
+
};
|
|
2059
|
+
//#endregion
|
|
2060
|
+
//#region lib/types/client/permission/index.js
|
|
2061
|
+
/**
|
|
2062
|
+
* Permission plugin, browser half. The `/permission` slash popup stays the
|
|
2063
|
+
* PRESET picker (a full bundle switch), while the composer control is two
|
|
2064
|
+
* independent axis dropdowns — read and write — each carrying one of
|
|
2065
|
+
* `deny / workspace / all / custom`, and each displaying its own current
|
|
2066
|
+
* value (no preset name is shown). Options read the process catalog (preset
|
|
2067
|
+
* contract: id, label, optional detail/badge). The composer's axes read the
|
|
2068
|
+
* session `permissions` projection for their CURRENT kinds and the same
|
|
2069
|
+
* catalog for the values each axis offers, and submit `/permission
|
|
2070
|
+
* <axis>:<value>`. A pick is applied directly: the
|
|
2071
|
+
* full-access/full-permission acknowledgement step is gone, because a visible
|
|
2072
|
+
* selection is already an explicit one. The General-settings row separately
|
|
2073
|
+
* writes the default preset for sessions created later through the host
|
|
2074
|
+
* Settings API.
|
|
2075
|
+
*/
|
|
2076
|
+
/**
|
|
2077
|
+
* 双轴组合包在宿主上注册的设置节名(`@t4r71/dsh-dual-axis`)。
|
|
2078
|
+
*
|
|
2079
|
+
* 0.1.7 的设置命名空间就是 Loader entry id(`packages/settings/settings/src/index.ts`
|
|
2080
|
+
* 的 `describe()` 按已装载 entry 投影),而宿主那一行在 `cordis.patch.yml` 里就叫
|
|
2081
|
+
* `dual-axis`(`packages/bundle/dual-axis/src/config.ts` 的 `DUAL_AXIS_ROW_ID`)。
|
|
2082
|
+
* 0.1.6 的 `sandbox-axis` 已经退役:写它取不到任何值,于是这张表单永远返回
|
|
2083
|
+
* undefined,编辑器的部署兜底静默退化成编译期默认。
|
|
2084
|
+
*
|
|
2085
|
+
* 这一节持有「新会话从哪两条轴开始」。两个下拉的路径编辑器只在**会话还没有这条轴**
|
|
2086
|
+
* 时以它为初值;对话级的选择永远优先。
|
|
2087
|
+
*/
|
|
2088
|
+
const DUAL_AXIS_SETTINGS_NAMESPACE = "dual-axis";
|
|
2089
|
+
/** 提交之后最多等多久,等这次取值出现在设置文档里。 */
|
|
2090
|
+
const AXIS_CONFIRM_TIMEOUT_MS = 3e3;
|
|
2091
|
+
/** Required services (cordis fiber inject). */
|
|
2092
|
+
const inject$2 = [
|
|
2093
|
+
"commandUi",
|
|
2094
|
+
"connection",
|
|
2095
|
+
"sessions",
|
|
2096
|
+
"slots",
|
|
2097
|
+
"locale",
|
|
2098
|
+
"remote",
|
|
2099
|
+
"remote.permissionPresets",
|
|
2100
|
+
"remote.settings",
|
|
2101
|
+
"configForms"
|
|
2102
|
+
];
|
|
2103
|
+
/** Read one session's current permissions projection value (undefined = capability absent). */
|
|
2104
|
+
function selectionOf(session) {
|
|
2105
|
+
return session?.projections.faceOf("permissions").getSnapshot();
|
|
2106
|
+
}
|
|
2107
|
+
/**
|
|
2108
|
+
* Join the process catalog with one Session's current value. Options carry no
|
|
2109
|
+
* acknowledgement gate, so a pick is applied directly. A built-in preset's
|
|
2110
|
+
* label is localized from this package's dictionary: the host catalog names
|
|
2111
|
+
* those entries by their machine value (`read-only`), and showing that kebab
|
|
2112
|
+
* key to a person is a regression, not a design. A deployment's own preset name
|
|
2113
|
+
* is not recognized by the dictionary and is displayed verbatim.
|
|
2114
|
+
* @param catalog - the current Host's complete preset catalog.
|
|
2115
|
+
* @param currentValue - the session's current preset identity.
|
|
2116
|
+
* @param t - this package's locale seat.
|
|
2117
|
+
* @returns the option rows the shared popup shell renders.
|
|
2118
|
+
*/
|
|
2119
|
+
function optionsOf(catalog, currentValue, t) {
|
|
2120
|
+
return catalog.options.map((option) => ({
|
|
2121
|
+
id: option.value,
|
|
2122
|
+
label: displayPermissionPreset(option.value, option.value === option.name ? option.value : option.name, (key) => t(key)),
|
|
2123
|
+
...option.description !== void 0 ? { detail: option.description } : {},
|
|
2124
|
+
...option.value === currentValue ? { active: true } : {}
|
|
2125
|
+
}));
|
|
2126
|
+
}
|
|
2127
|
+
/**
|
|
2128
|
+
* Client plugin body: register the /permission popup picker over the
|
|
2129
|
+
* permissions projection.
|
|
2130
|
+
* @param ctx - client root context.
|
|
2131
|
+
*/
|
|
2132
|
+
function apply$2(ctx) {
|
|
2133
|
+
const command = ctx.get("commandUi");
|
|
2134
|
+
const sessions = ctx.sessions;
|
|
2135
|
+
ctx.effect(() => ctx.locale.register(PERMISSION_ACCESS_NS, {
|
|
2136
|
+
zh: accessZh,
|
|
2137
|
+
en: accessEn
|
|
2138
|
+
}), "ui-permission: current-session dictionaries");
|
|
2139
|
+
const t = ctx.locale.bind(PERMISSION_ACCESS_NS);
|
|
2140
|
+
const sessionFor = (session) => sessions.binding(session.sessionId)?.session;
|
|
2141
|
+
/**
|
|
2142
|
+
* 一次 `/axis` 调用被宿主**接受**(命令解析得到、handler 跑完)。
|
|
2143
|
+
*
|
|
2144
|
+
* 注意它不说明这次改动有没有写进去:`Session.command` 把结果收成 `matched`
|
|
2145
|
+
* (`packages/api/session-controller/src/client/sessions/session.ts:387-391`,`matched: result.value !== undefined`),丢掉了 handler 自己的 `kind`/`text`。
|
|
2146
|
+
* handler 返回 `{ kind: 'error' }` 的命令同样得到 `matched: true`。写入是否生效因此只能
|
|
2147
|
+
* 由 {@link select} 回读文档来判断。
|
|
2148
|
+
*/
|
|
2149
|
+
const submit = async (sessionId, args) => {
|
|
2150
|
+
const live = sessions.binding(sessionId)?.session;
|
|
2151
|
+
if (live === void 0) throw new Error("this session is not materialized yet");
|
|
2152
|
+
const result = await live.command("/axis " + args);
|
|
2153
|
+
if (!result.ok) throw new Error(`permission switch failed: ${result.error.code}: ${result.error.message}`);
|
|
2154
|
+
if (!result.value.matched) throw new Error("the host offers no /axis command");
|
|
2155
|
+
};
|
|
2156
|
+
/**
|
|
2157
|
+
* 等这条会话在设置文档里真的变成 `expected`。
|
|
2158
|
+
*
|
|
2159
|
+
* 宿主写完轴是整份文档的异步写,写完才发 `settings/document-updated`,镜像再重读,
|
|
2160
|
+
* 所以这里给的是一个有上界的等待而不是一次即时读。等到就是成功;等不到回答 `false`
|
|
2161
|
+
* —— 那正是「命令执行了但没写进去」,界面据此把失败说出来,而不是关掉编辑器假装成功。
|
|
2162
|
+
* @param sessionId - 提交所属的会话。
|
|
2163
|
+
* @param axis - 这次写的哪条轴。
|
|
2164
|
+
* @param expected - 这次提交应当落成的取值。
|
|
2165
|
+
* @returns 文档里是否出现了这次取值。
|
|
2166
|
+
*/
|
|
2167
|
+
const stored = async (sessionId, axis, expected) => {
|
|
2168
|
+
const deadline = Date.now() + AXIS_CONFIRM_TIMEOUT_MS;
|
|
2169
|
+
for (;;) {
|
|
2170
|
+
const view = sessionAxes.axesOf(sessionId);
|
|
2171
|
+
if (view.state === "record" && sameStoredAxis(view.axes[axis], expected)) return true;
|
|
2172
|
+
if (Date.now() >= deadline) return false;
|
|
2173
|
+
await new Promise((resolve) => {
|
|
2174
|
+
setTimeout(resolve, 100);
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
};
|
|
2178
|
+
const catalog = new PermissionCatalogDirectory(ctx);
|
|
2179
|
+
ctx.effect(() => () => {
|
|
2180
|
+
catalog.dispose();
|
|
2181
|
+
}, "ui-permission: process catalog directory");
|
|
2182
|
+
ctx.effect(() => catalog.invalidations.subscribe(() => {
|
|
2183
|
+
command.dismiss("permission");
|
|
2184
|
+
}), "ui-permission: dismiss stale slash choices");
|
|
2185
|
+
ctx.effect(() => ctx.locale.register("settings.permission", {
|
|
2186
|
+
zh: zh$1,
|
|
2187
|
+
en: en$1
|
|
2188
|
+
}), "ui-permission: settings row dictionaries");
|
|
2189
|
+
const controller = new PermissionPresetSettingsController(ctx.configForms.describe(), ctx, catalog);
|
|
2190
|
+
const load = () => controller.load();
|
|
2191
|
+
const select = (preset) => controller.select(preset);
|
|
2192
|
+
const injected = () => ({
|
|
2193
|
+
hooks: { permission: controller.store },
|
|
2194
|
+
load,
|
|
2195
|
+
select
|
|
2196
|
+
});
|
|
2197
|
+
ctx.effect(() => () => {
|
|
2198
|
+
controller.dispose();
|
|
2199
|
+
}, "ui-permission: settings row directory");
|
|
2200
|
+
const parentOf = (sessionId) => sessions.subagentAddress(sessionId)?.parentSessionId;
|
|
2201
|
+
const sessionAxes = new SessionAxesDirectory(ctx.configForms.describe(), parentOf);
|
|
2202
|
+
ctx.effect(() => () => {
|
|
2203
|
+
sessionAxes.dispose();
|
|
2204
|
+
}, "ui-permission: session axes directory");
|
|
2205
|
+
ctx.slots.inject("settings.general.item", () => ctx.slots.register({
|
|
2206
|
+
name: "settings.general.item",
|
|
2207
|
+
id: "permission",
|
|
2208
|
+
order: -20,
|
|
2209
|
+
locale: "settings.permission",
|
|
2210
|
+
inject: injected
|
|
2211
|
+
}, PermissionRow));
|
|
2212
|
+
const globalForm = ctx.configForms.get(DUAL_AXIS_SETTINGS_NAMESPACE);
|
|
2213
|
+
const globalAxes = () => globalForm.getSnapshot().value ?? {};
|
|
2214
|
+
const globalOf = (axis) => {
|
|
2215
|
+
const value = globalAxes()[axis];
|
|
2216
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
2217
|
+
const candidate = value;
|
|
2218
|
+
return typeof candidate.kind === "string" ? candidate : void 0;
|
|
2219
|
+
};
|
|
2220
|
+
ctx.slots.inject("conversation.input.permission", () => ctx.slots.register({
|
|
2221
|
+
name: "conversation.input.permission",
|
|
2222
|
+
locale: PERMISSION_ACCESS_NS,
|
|
2223
|
+
inject: (sessionId) => ({
|
|
2224
|
+
hooks: { permissionCatalog: catalog.store },
|
|
2225
|
+
select: async (axis, args, expected) => {
|
|
2226
|
+
await submit(sessionId, args);
|
|
2227
|
+
return await stored(sessionId, axis, expected);
|
|
2228
|
+
},
|
|
2229
|
+
global: (axis) => globalOf(axis),
|
|
2230
|
+
groupLibrary: () => groupOptionsOf(globalAxes().groups),
|
|
2231
|
+
sessionAxes: {
|
|
2232
|
+
useAxes: (sessionId) => {
|
|
2233
|
+
return sessionAxesView((0, react.useSyncExternalStore)(sessionAxes.subscribe, sessionAxes.getSnapshot).view?.namespaces, sessionId, sessionId === void 0 ? void 0 : parentOf(sessionId));
|
|
2234
|
+
},
|
|
2235
|
+
axesOf: (sessionId) => sessionAxes.axesOf(sessionId)
|
|
2236
|
+
}
|
|
2237
|
+
})
|
|
2238
|
+
}, PermissionSelect));
|
|
2239
|
+
ctx.effect(() => command.decorate({
|
|
2240
|
+
name: "permission",
|
|
2241
|
+
available: (session) => selectionOf(sessionFor(session)) !== void 0,
|
|
2242
|
+
ui: {
|
|
2243
|
+
kind: "popupSelect",
|
|
2244
|
+
options: async (session) => {
|
|
2245
|
+
const selection = selectionOf(sessionFor(session));
|
|
2246
|
+
if (selection === void 0) throw new Error("permission presets are not available on this host");
|
|
2247
|
+
return optionsOf(await catalog.load(), selection.currentValue, t);
|
|
2248
|
+
},
|
|
2249
|
+
onSelect: (option, session) => submit(session.sessionId, option.id).then(() => void 0)
|
|
2250
|
+
}
|
|
2251
|
+
}), "ui-permission: /permission decoration");
|
|
2252
|
+
}
|
|
2253
|
+
//#endregion
|
|
2254
|
+
//#region lib/types/client/row/axes.js
|
|
2255
|
+
/**
|
|
2256
|
+
* 双轴配置页的纯函数层:把设置节里的一条轴收成可编辑的草稿、把草稿收成一条
|
|
2257
|
+
* 轴值、把一条轴渲染成一句话。这里不碰 React、不碰 DOM,也不碰宿主 —— 界面
|
|
2258
|
+
* 半边每次挂载与每次保存都走这几个函数,因此"页面上能改的形状"与"宿主接受
|
|
2259
|
+
* 的形状"只有这一份定义。
|
|
2260
|
+
*
|
|
2261
|
+
* @module @t4r71/dsh-dual-axis-ui/client/axes
|
|
2262
|
+
*/
|
|
2263
|
+
/** 界面上的四条轴取值,顺序即下拉顺序:从最窄到最宽,custom 排在最后。 */
|
|
2264
|
+
const AXIS_KINDS = [
|
|
2265
|
+
"deny",
|
|
2266
|
+
"workspace",
|
|
2267
|
+
"all",
|
|
2268
|
+
"custom"
|
|
2269
|
+
];
|
|
2270
|
+
/** 界面上的三个底座,顺序即下拉顺序。 */
|
|
2271
|
+
const AXIS_BASES = [
|
|
2272
|
+
"deny",
|
|
2273
|
+
"workspace",
|
|
2274
|
+
"all"
|
|
2275
|
+
];
|
|
2276
|
+
/**
|
|
2277
|
+
* 一条绝对路径写法(POSIX 或 Windows)。与宿主半边的同名校验同一条规则:相对
|
|
2278
|
+
* 路径只能相对宿主进程的 cwd 解析,不可能是写配置的人想说的位置。
|
|
2279
|
+
* @param value - 待检查的路径写法。
|
|
2280
|
+
* @returns 该写法是否为绝对路径。
|
|
2281
|
+
*/
|
|
2282
|
+
function isAbsolutePath(value) {
|
|
2283
|
+
return value.startsWith("/") || /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\");
|
|
2284
|
+
}
|
|
2285
|
+
/**
|
|
2286
|
+
* 把一个未信任的值收成一条轴值。
|
|
2287
|
+
* @param value - 设置节里的取值,或 `undefined`。
|
|
2288
|
+
* @returns 一条轴值;形状不对时返回 undefined,由调用方决定用什么兜底。
|
|
2289
|
+
*/
|
|
2290
|
+
function axisValueOf$1(value) {
|
|
2291
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
2292
|
+
const candidate = value;
|
|
2293
|
+
if (typeof candidate.kind !== "string" || !AXIS_KINDS.includes(candidate.kind)) return void 0;
|
|
2294
|
+
if (candidate.kind !== "custom") return { kind: candidate.kind };
|
|
2295
|
+
if (typeof candidate.base !== "string" || !AXIS_BASES.includes(candidate.base)) return void 0;
|
|
2296
|
+
return {
|
|
2297
|
+
kind: "custom",
|
|
2298
|
+
base: candidate.base,
|
|
2299
|
+
allow: stringList(candidate.allow),
|
|
2300
|
+
deny: stringList(candidate.deny)
|
|
2301
|
+
};
|
|
2302
|
+
}
|
|
2303
|
+
/**
|
|
2304
|
+
* 把一份字符串列表读出来;不是数组的取值读成空列表。
|
|
2305
|
+
* @param value - 待读取的值。
|
|
2306
|
+
* @returns 其中的字符串成员。
|
|
2307
|
+
*/
|
|
2308
|
+
function stringList(value) {
|
|
2309
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
2310
|
+
}
|
|
2311
|
+
/**
|
|
2312
|
+
* 把一条轴值收成界面草稿。
|
|
2313
|
+
* @param value - 设置节里的取值。
|
|
2314
|
+
* @param fallback - 取值缺失或读不懂时使用的兜底轴(组合层声明的默认值)。
|
|
2315
|
+
* @returns 一份可编辑的草稿。
|
|
2316
|
+
*/
|
|
2317
|
+
function draftOf(value, fallback) {
|
|
2318
|
+
return draftOfAxis(axisValueOf$1(value) ?? fallback, axisValueOf$1(value) === void 0 && value !== void 0 ? "宿主给出的这条轴读不懂,已退回组合默认值" : "");
|
|
2319
|
+
}
|
|
2320
|
+
/**
|
|
2321
|
+
* 把一条已经收好的轴值转成草稿。
|
|
2322
|
+
* @param axis - 一条轴值。
|
|
2323
|
+
* @param problem - 直接带上的说明;不传即没有内容可说明。
|
|
2324
|
+
* @returns 一份可编辑的草稿。
|
|
2325
|
+
*/
|
|
2326
|
+
function draftOfAxis(axis, problem = "") {
|
|
2327
|
+
const kind = axis.kind;
|
|
2328
|
+
return {
|
|
2329
|
+
kind,
|
|
2330
|
+
base: kind === "custom" ? axis.base : "workspace",
|
|
2331
|
+
allow: kind === "custom" ? axis.allow.join("\n") : "",
|
|
2332
|
+
deny: kind === "custom" ? axis.deny.join("\n") : "",
|
|
2333
|
+
problem
|
|
2334
|
+
};
|
|
2335
|
+
}
|
|
2336
|
+
/**
|
|
2337
|
+
* 把草稿收成一条可以写回设置的轴值。非 custom 的草稿只带取值本身 —— 界面上
|
|
2338
|
+
* 留着的历史路径不会被写进另一条取值里,否则切一次取值就等于做了一次没人要求
|
|
2339
|
+
* 的授权。
|
|
2340
|
+
* @param draft - 界面上的草稿。
|
|
2341
|
+
* @returns 一条轴值,或一条说明这份草稿为什么还不能保存的话。
|
|
2342
|
+
*/
|
|
2343
|
+
function scopeOf(draft) {
|
|
2344
|
+
if (!AXIS_KINDS.includes(draft.kind)) return {
|
|
2345
|
+
ok: false,
|
|
2346
|
+
problem: `未知的取值 ${draft.kind}`
|
|
2347
|
+
};
|
|
2348
|
+
if (draft.kind !== "custom") return {
|
|
2349
|
+
ok: true,
|
|
2350
|
+
value: { kind: draft.kind }
|
|
2351
|
+
};
|
|
2352
|
+
if (!AXIS_BASES.includes(draft.base)) return {
|
|
2353
|
+
ok: false,
|
|
2354
|
+
problem: `未知的底座 ${draft.base}`
|
|
2355
|
+
};
|
|
2356
|
+
const allow = pathLines(draft.allow);
|
|
2357
|
+
const deny = pathLines(draft.deny);
|
|
2358
|
+
for (const [entry, paths] of [["allow", allow], ["deny", deny]]) {
|
|
2359
|
+
const bad = paths.find((path) => !isAbsolutePath(path));
|
|
2360
|
+
if (bad !== void 0) return {
|
|
2361
|
+
ok: false,
|
|
2362
|
+
problem: `${entry} 里的 ${JSON.stringify(bad)} 不是绝对路径`
|
|
2363
|
+
};
|
|
2364
|
+
}
|
|
2365
|
+
return {
|
|
2366
|
+
ok: true,
|
|
2367
|
+
value: {
|
|
2368
|
+
kind: "custom",
|
|
2369
|
+
base: draft.base,
|
|
2370
|
+
allow,
|
|
2371
|
+
deny
|
|
2372
|
+
}
|
|
2373
|
+
};
|
|
2374
|
+
}
|
|
2375
|
+
/**
|
|
2376
|
+
* 把一个多行文本框读成路径列表:忽略空行与首尾空白。
|
|
2377
|
+
* @param text - 文本框里的原文。
|
|
2378
|
+
* @returns 其中每一行作为一条路径。
|
|
2379
|
+
*/
|
|
2380
|
+
function pathLines(text) {
|
|
2381
|
+
return text.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line !== "");
|
|
2382
|
+
}
|
|
2383
|
+
/**
|
|
2384
|
+
* 两条轴是否描述同一个边界。逐成员比较:custom 的两个路径列表连顺序都是它取值
|
|
2385
|
+
* 的一部分,而每次重解析都会新建轴对象。
|
|
2386
|
+
* @param left - 一条轴。
|
|
2387
|
+
* @param right - 另一条轴。
|
|
2388
|
+
* @returns 两者是否描述同一条轴。
|
|
2389
|
+
*/
|
|
2390
|
+
function sameAxis(left, right) {
|
|
2391
|
+
if (left.kind !== right.kind) return false;
|
|
2392
|
+
if (left.kind !== "custom" || right.kind !== "custom") return true;
|
|
2393
|
+
const sameList = (a, b) => a.length === b.length && a.every((value, index) => value === b[index]);
|
|
2394
|
+
return left.base === right.base && sameList(left.allow, right.allow) && sameList(left.deny, right.deny);
|
|
2395
|
+
}
|
|
2396
|
+
/**
|
|
2397
|
+
* 把一条轴渲染成一句话,供配置页的摘要行与草稿预览使用。
|
|
2398
|
+
* @param axis - 一条轴值。
|
|
2399
|
+
* @param text - 当前语言的文案表。
|
|
2400
|
+
* @returns 一句话说明。
|
|
2401
|
+
*/
|
|
2402
|
+
function describeAxis(axis, text) {
|
|
2403
|
+
if (axis.kind !== "custom") return text.kind(axis.kind);
|
|
2404
|
+
const paths = [];
|
|
2405
|
+
if (axis.allow.length > 0) paths.push(`+${axis.allow.join(", ")}`);
|
|
2406
|
+
if (axis.deny.length > 0) paths.push(`-${axis.deny.join(", ")}`);
|
|
2407
|
+
return text.custom(text.base(axis.base), paths.length === 0 ? "—" : paths.join(" "));
|
|
2408
|
+
}
|
|
2409
|
+
//#endregion
|
|
2410
|
+
//#region lib/types/client/row/groups.js
|
|
2411
|
+
/**
|
|
2412
|
+
* 双轴配置页的规则组纯函数层:把设置节里的 `groups` / `defaultGroups` 收成可编辑
|
|
2413
|
+
* 草稿、把草稿收成宿主解析器接受的组库。这里不碰 React、不碰 DOM、也不碰宿主 ——
|
|
2414
|
+
* 界面每次挂载与每次保存都走这几个函数,因此「页面上能编的形状」与「宿主
|
|
2415
|
+
* `parseLibrary` 接受的形状」只有这一份定义。
|
|
2416
|
+
*
|
|
2417
|
+
* 组体只在设置页这一份:会话按 id 引用它,判定时现取(规格 §「为什么对话里绝不能
|
|
2418
|
+
* 存组体」)。所以这里的编辑结果写回的是**定义**,不是任何一条会话。
|
|
2419
|
+
*
|
|
2420
|
+
* @module @t4r71/dsh-dual-axis-ui/client/row/groups
|
|
2421
|
+
*/
|
|
2422
|
+
/**
|
|
2423
|
+
* 把一个未信任的取值读成一份文本草稿:不是数组时读成空串(界面上不显示任何路径)。
|
|
2424
|
+
* @param value - 设置文档里那条轴的 `allow` 或 `deny`。
|
|
2425
|
+
* @returns 每行一条的文本。
|
|
2426
|
+
*/
|
|
2427
|
+
function textOfPaths(value) {
|
|
2428
|
+
return (Array.isArray(value) ? value : []).filter((item) => typeof item === "string").join("\n");
|
|
2429
|
+
}
|
|
2430
|
+
/**
|
|
2431
|
+
* 把一份未信任的组取值读成界面草稿。
|
|
2432
|
+
* @param value - 组库里的一项。
|
|
2433
|
+
* @param isDefault - 这个 id 是否在 `defaultGroups` 里。
|
|
2434
|
+
* @returns 该组的草稿;id 不是一个字符串时用空串,让用户在界面上看见并改它。
|
|
2435
|
+
*/
|
|
2436
|
+
function draftOfGroup(value, isDefault) {
|
|
2437
|
+
const candidate = typeof value === "object" && value !== null ? value : {};
|
|
2438
|
+
const side = (rules) => {
|
|
2439
|
+
const rules_ = typeof rules === "object" && rules !== null ? rules : {};
|
|
2440
|
+
return {
|
|
2441
|
+
allow: textOfPaths(rules_.allow),
|
|
2442
|
+
deny: textOfPaths(rules_.deny)
|
|
2443
|
+
};
|
|
2444
|
+
};
|
|
2445
|
+
return {
|
|
2446
|
+
id: typeof candidate.id === "string" ? candidate.id : "",
|
|
2447
|
+
name: typeof candidate.name === "string" ? candidate.name : "",
|
|
2448
|
+
read: side(candidate.read),
|
|
2449
|
+
write: side(candidate.write),
|
|
2450
|
+
default: isDefault
|
|
2451
|
+
};
|
|
2452
|
+
}
|
|
2453
|
+
/**
|
|
2454
|
+
* 把设置文档里的组库与默认加载清单读成界面草稿。
|
|
2455
|
+
*
|
|
2456
|
+
* 读取是**宽容**的:这个函数的产物要能渲染出来让人修,所以读不懂的成员按空值处理,
|
|
2457
|
+
* 而不是抛出去让整页打不开。挡住保存的是 {@link groupLibraryOf},那里才拒绝坏取值。
|
|
2458
|
+
* @param groups - 设置文档里的 `groups`,未信任。
|
|
2459
|
+
* @param defaultGroups - 设置文档里的 `defaultGroups`,未信任。
|
|
2460
|
+
* @returns 每个组一份草稿,顺序与文档一致。
|
|
2461
|
+
*/
|
|
2462
|
+
function groupDraftsOf(groups, defaultGroups) {
|
|
2463
|
+
if (!Array.isArray(groups)) return [];
|
|
2464
|
+
const defaults = Array.isArray(defaultGroups) ? defaultGroups.filter((item) => typeof item === "string") : [];
|
|
2465
|
+
return groups.map((entry) => {
|
|
2466
|
+
const id = typeof entry === "object" && entry !== null ? entry.id : void 0;
|
|
2467
|
+
return draftOfGroup(entry, typeof id === "string" && defaults.includes(id));
|
|
2468
|
+
});
|
|
2469
|
+
}
|
|
2470
|
+
/**
|
|
2471
|
+
* 一条草稿的某一侧:两个列表都空即这一侧缺席。
|
|
2472
|
+
* @param side - 该侧的草稿。
|
|
2473
|
+
* @returns 该侧的取值,或 undefined。
|
|
2474
|
+
*/
|
|
2475
|
+
function axisValueOf(side) {
|
|
2476
|
+
const allow = pathLines(side.allow);
|
|
2477
|
+
const deny = pathLines(side.deny);
|
|
2478
|
+
if (allow.length === 0 && deny.length === 0) return void 0;
|
|
2479
|
+
return {
|
|
2480
|
+
allow,
|
|
2481
|
+
deny
|
|
2482
|
+
};
|
|
2483
|
+
}
|
|
2484
|
+
/**
|
|
2485
|
+
* 把一份组草稿收成可以写回设置的组库。
|
|
2486
|
+
*
|
|
2487
|
+
* 校验与宿主 `parseLibrary` / `groupAt` 逐条对齐:id 非空且唯一,每条路径必须是
|
|
2488
|
+
* 绝对路径。宿主在库读不懂时会让**每一次判定**都响亮失败,所以这里必须在写入之前
|
|
2489
|
+
* 就挡住坏取值,而不是把一份宿主读不懂的库保存下去。
|
|
2490
|
+
* @param drafts - 界面上的草稿。
|
|
2491
|
+
* @param problemOf - 把说明键转成当前语言文案的回调。
|
|
2492
|
+
* @returns 组库取值,或一句说明这份草稿为什么还不能保存的话。
|
|
2493
|
+
*/
|
|
2494
|
+
function groupLibraryOf(drafts, problemOf) {
|
|
2495
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2496
|
+
const value = [];
|
|
2497
|
+
for (const draft of drafts) {
|
|
2498
|
+
const id = draft.id.trim();
|
|
2499
|
+
if (id === "") return {
|
|
2500
|
+
ok: false,
|
|
2501
|
+
problem: problemOf("idEmpty", {})
|
|
2502
|
+
};
|
|
2503
|
+
if (seen.has(id)) return {
|
|
2504
|
+
ok: false,
|
|
2505
|
+
problem: problemOf("idDuplicate", { id })
|
|
2506
|
+
};
|
|
2507
|
+
seen.add(id);
|
|
2508
|
+
const sides = [["read", draft.read], ["write", draft.write]];
|
|
2509
|
+
for (const [side, rules] of sides) for (const [entry, text] of [["allow", rules.allow], ["deny", rules.deny]]) {
|
|
2510
|
+
const bad = pathLines(text).find((path) => !isAbsolutePath(path));
|
|
2511
|
+
if (bad !== void 0) return {
|
|
2512
|
+
ok: false,
|
|
2513
|
+
problem: problemOf("notAbsolute", {
|
|
2514
|
+
id,
|
|
2515
|
+
side,
|
|
2516
|
+
entry,
|
|
2517
|
+
path: bad
|
|
2518
|
+
})
|
|
2519
|
+
};
|
|
2520
|
+
}
|
|
2521
|
+
const read = axisValueOf(draft.read);
|
|
2522
|
+
const write = axisValueOf(draft.write);
|
|
2523
|
+
value.push({
|
|
2524
|
+
id,
|
|
2525
|
+
name: draft.name.trim() === "" ? id : draft.name,
|
|
2526
|
+
...read === void 0 ? {} : { read },
|
|
2527
|
+
...write === void 0 ? {} : { write }
|
|
2528
|
+
});
|
|
2529
|
+
}
|
|
2530
|
+
return {
|
|
2531
|
+
ok: true,
|
|
2532
|
+
value
|
|
2533
|
+
};
|
|
2534
|
+
}
|
|
2535
|
+
/**
|
|
2536
|
+
* 草稿里被勾上「新对话默认加载」的组 id。
|
|
2537
|
+
*
|
|
2538
|
+
* 只在 {@link groupLibraryOf} 通过之后取用:那时 id 非空且唯一,所以这份清单本身
|
|
2539
|
+
* 也是合法的 `defaultGroups`。
|
|
2540
|
+
* @param drafts - 界面上的草稿。
|
|
2541
|
+
* @returns 勾选顺序即文档顺序的 id 清单。
|
|
2542
|
+
*/
|
|
2543
|
+
function defaultGroupIdsOf(drafts) {
|
|
2544
|
+
return drafts.filter((draft) => draft.default).map((draft) => draft.id.trim());
|
|
2545
|
+
}
|
|
2546
|
+
/**
|
|
2547
|
+
* 给新加的一个组起一个还没有被占用的 id。
|
|
2548
|
+
*
|
|
2549
|
+
* 默认名 `g1`、`g2`…:id 是会话引用的东西,先给出一个能用的,再由人改。
|
|
2550
|
+
* @param drafts - 当前已有的草稿。
|
|
2551
|
+
* @returns 一个没有被占用的 id。
|
|
2552
|
+
*/
|
|
2553
|
+
function nextGroupId(drafts) {
|
|
2554
|
+
const taken = new Set(drafts.map((draft) => draft.id.trim()));
|
|
2555
|
+
for (let index = 1;; index += 1) {
|
|
2556
|
+
const candidate = `g${String(index)}`;
|
|
2557
|
+
if (!taken.has(candidate)) return candidate;
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
/**
|
|
2561
|
+
* 一个空组的草稿:id 未被占用、名字留空(保存时用 id 顶上)、两侧规则都空、
|
|
2562
|
+
* 默认不加载。空组是合法的 —— 组的内容随时可改,先建出来再填是正常用法。
|
|
2563
|
+
* @param drafts - 当前已有的草稿。
|
|
2564
|
+
* @returns 一份可以加进列表的草稿。
|
|
2565
|
+
*/
|
|
2566
|
+
function emptyGroupDraft(drafts) {
|
|
2567
|
+
return {
|
|
2568
|
+
id: nextGroupId(drafts),
|
|
2569
|
+
name: "",
|
|
2570
|
+
read: {
|
|
2571
|
+
allow: "",
|
|
2572
|
+
deny: ""
|
|
2573
|
+
},
|
|
2574
|
+
write: {
|
|
2575
|
+
allow: "",
|
|
2576
|
+
deny: ""
|
|
2577
|
+
},
|
|
2578
|
+
default: false
|
|
2579
|
+
};
|
|
2580
|
+
}
|
|
2581
|
+
/**
|
|
2582
|
+
* 两份组库是否描述同一份定义。
|
|
2583
|
+
*
|
|
2584
|
+
* 与轴的 `sameAxis` 同一个用法:保存时比对「现在的草稿」与「文档里已生效的值」,
|
|
2585
|
+
* 相同就不发写入。逐成员比较而不是引用比较,因为每次重解析都会新建对象。
|
|
2586
|
+
* @param left - 一份组库取值。
|
|
2587
|
+
* @param right - 另一份组库取值。
|
|
2588
|
+
* @returns 两者是否描述同一份组库。
|
|
2589
|
+
*/
|
|
2590
|
+
function sameGroupLibrary(left, right) {
|
|
2591
|
+
const list = (value) => (value ?? []).join("\0");
|
|
2592
|
+
const side = (value) => value === void 0 ? "" : list(value.allow) + "" + list(value.deny);
|
|
2593
|
+
if (left.length !== right.length) return false;
|
|
2594
|
+
return left.every((group, index) => {
|
|
2595
|
+
const other = right[index];
|
|
2596
|
+
if (other === void 0) return false;
|
|
2597
|
+
return group.id === other.id && group.name === other.name && side(group.read) === side(other.read) && side(group.write) === side(other.write);
|
|
2598
|
+
});
|
|
2599
|
+
}
|
|
2600
|
+
/**
|
|
2601
|
+
* 把一份已经收好的组库摊成设置文档接受的 JSON。
|
|
2602
|
+
*
|
|
2603
|
+
* 取值里的列表是 `readonly string[]`,而文档取值是可变的 JSON 数组:传只读数组会被
|
|
2604
|
+
* 线上格式挡住,所以写入前摊平一层(与轴的 `jsonAxis` 同因)。
|
|
2605
|
+
* @param library - 一份校验过的组库。
|
|
2606
|
+
* @returns 同一份取值的可变副本。
|
|
2607
|
+
*/
|
|
2608
|
+
function jsonGroupLibrary(library) {
|
|
2609
|
+
const side = (value) => value === void 0 ? void 0 : {
|
|
2610
|
+
allow: [...value.allow],
|
|
2611
|
+
deny: [...value.deny]
|
|
2612
|
+
};
|
|
2613
|
+
return library.map((group) => {
|
|
2614
|
+
const read = side(group.read);
|
|
2615
|
+
const write = side(group.write);
|
|
2616
|
+
return {
|
|
2617
|
+
id: group.id,
|
|
2618
|
+
name: group.name,
|
|
2619
|
+
...read === void 0 ? {} : { read },
|
|
2620
|
+
...write === void 0 ? {} : { write }
|
|
2621
|
+
};
|
|
2622
|
+
});
|
|
2623
|
+
}
|
|
2624
|
+
/**
|
|
2625
|
+
* 默认加载清单的 JSON 形式(可变数组)。
|
|
2626
|
+
* @param ids - 被勾选的组 id。
|
|
2627
|
+
* @returns 同一份清单的可变副本。
|
|
2628
|
+
*/
|
|
2629
|
+
function jsonDefaultGroupIds(ids) {
|
|
2630
|
+
return [...ids];
|
|
2631
|
+
}
|
|
2632
|
+
//#endregion
|
|
2633
|
+
//#region lib/types/client/row/locales.js
|
|
2634
|
+
/**
|
|
2635
|
+
* 双轴配置页自己的词典。界面文案只在这里出现一份,配置页按当前语言取用。
|
|
2636
|
+
*
|
|
2637
|
+
* @module @t4r71/dsh-dual-axis-ui/client/locales
|
|
2638
|
+
*/
|
|
2639
|
+
/** 词典命名空间:与配置页注册到 slot 上的 `locale` 一致。 */
|
|
2640
|
+
const DUAL_AXIS_LOCALE_NS = "sandbox-axis";
|
|
2641
|
+
/** 中文文案。 */
|
|
2642
|
+
const zh = {
|
|
2643
|
+
rowSummary: "文件沙箱的两条访问轴:新会话按这里的读轴与写轴执行。",
|
|
2644
|
+
titleRead: "读轴",
|
|
2645
|
+
titleWrite: "写轴",
|
|
2646
|
+
hintRead: "这条轴界定的是一条读操作能碰到的路径范围;它压在所有读取之上。",
|
|
2647
|
+
hintWrite: "这条轴界定的是一条写操作能碰到的路径范围;它比读轴更严:读轴放行、写轴不放行,仍然写不了。",
|
|
2648
|
+
kindLabel: "取值",
|
|
2649
|
+
baseLabel: "底座",
|
|
2650
|
+
allowLabel: "额外放行的绝对路径(每行一条)",
|
|
2651
|
+
denyLabel: "排除的绝对路径(每行一条,优先于放行)",
|
|
2652
|
+
kindDeny: "概不允许",
|
|
2653
|
+
kindWorkspace: "仅会话工作区",
|
|
2654
|
+
kindAll: "整台主机",
|
|
2655
|
+
kindCustom: "自定义(底座 + 放行 - 排除)",
|
|
2656
|
+
baseDeny: "概不允许",
|
|
2657
|
+
baseWorkspace: "会话工作区",
|
|
2658
|
+
baseAll: "整台主机",
|
|
2659
|
+
customTemplate: "在「{base}」之上放行 {paths}",
|
|
2660
|
+
save: "保存",
|
|
2661
|
+
saving: "保存中…",
|
|
2662
|
+
discard: "放弃改动",
|
|
2663
|
+
reset: "恢复默认",
|
|
2664
|
+
overridden: "已自定义",
|
|
2665
|
+
failed: "保存失败:宿主拒绝了这次写入,草稿已保留",
|
|
2666
|
+
readOnly: "当前设置文档不可写,改动无法保存",
|
|
2667
|
+
loading: "正在读取双轴设置…",
|
|
2668
|
+
unavailable: "宿主没有提供 {ns} 这个设置命名空间,因此这里没有可改的值。",
|
|
2669
|
+
groupsTitle: "规则组库",
|
|
2670
|
+
groupsHint: "规则组是具名的规则片段:对话按 id 引用它,判定时现取定义。改这里的内容会立刻作用于所有引用它的对话。",
|
|
2671
|
+
groupsEmpty: "还没有定义规则组。",
|
|
2672
|
+
groupsAdd: "新增规则组",
|
|
2673
|
+
groupsRemove: "删除这个组",
|
|
2674
|
+
groupIdLabel: "id",
|
|
2675
|
+
groupNameLabel: "名称",
|
|
2676
|
+
groupDefaultLoad: "新对话默认加载",
|
|
2677
|
+
groupSideRead: "读侧规则",
|
|
2678
|
+
groupSideWrite: "写侧规则",
|
|
2679
|
+
entryAllow: "放行",
|
|
2680
|
+
entryDeny: "排除",
|
|
2681
|
+
groupAxesHint: "两侧都可以留空:留空表示这个组不覆盖那一根轴。",
|
|
2682
|
+
groupsDefaultHint: "勾上的组是新对话的初始组;已经在跑的对话按它自己的预设走,不受这里影响。",
|
|
2683
|
+
groupsDefaultHintCustom: "只挂在本节取值选为「自定义」的那条轴上;两条轴都是闭合取值时,这个勾选不起作用。",
|
|
2684
|
+
groupsProblemIdEmpty: "规则组的 id 不能为空。",
|
|
2685
|
+
groupsProblemIdDuplicate: "规则组的 id 必须唯一:{id} 出现了多次。",
|
|
2686
|
+
groupsProblemNotAbsolute: "规则组 {id} 的{side}{entry}列表里,{path} 不是绝对路径。",
|
|
2687
|
+
live: "保存后对之后创建的会话生效;当前会话仍由输入框上方的两个下拉控制。"
|
|
2688
|
+
};
|
|
2689
|
+
/** 英文文案:键集合与中文一致。 */
|
|
2690
|
+
const en = {
|
|
2691
|
+
rowSummary: "The file sandbox's two access axes: new sessions run under the read and write axes set here.",
|
|
2692
|
+
titleRead: "Read axis",
|
|
2693
|
+
titleWrite: "Write axis",
|
|
2694
|
+
hintRead: "This axis bounds the paths a read operation can reach; it sits above every read.",
|
|
2695
|
+
hintWrite: "This axis bounds the paths a write operation can reach. It is stricter than the read axis: permitted to read but not to write still means no write.",
|
|
2696
|
+
kindLabel: "Kind",
|
|
2697
|
+
baseLabel: "Base",
|
|
2698
|
+
allowLabel: "Additional allowed absolute paths (one per line)",
|
|
2699
|
+
denyLabel: "Excluded absolute paths (one per line, they win)",
|
|
2700
|
+
kindDeny: "Deny everything",
|
|
2701
|
+
kindWorkspace: "Session workspace only",
|
|
2702
|
+
kindAll: "The whole host",
|
|
2703
|
+
kindCustom: "Custom (base + allow − deny)",
|
|
2704
|
+
baseDeny: "Deny everything",
|
|
2705
|
+
baseWorkspace: "Session workspace",
|
|
2706
|
+
baseAll: "The whole host",
|
|
2707
|
+
customTemplate: "Over \"{base}\", allow {paths}",
|
|
2708
|
+
save: "Save",
|
|
2709
|
+
saving: "Saving…",
|
|
2710
|
+
discard: "Discard",
|
|
2711
|
+
reset: "Restore default",
|
|
2712
|
+
overridden: "Customized",
|
|
2713
|
+
failed: "Save failed: the host refused the write and the draft is kept",
|
|
2714
|
+
readOnly: "The settings document is not writable, so changes cannot be saved",
|
|
2715
|
+
loading: "Reading the dual-axis settings…",
|
|
2716
|
+
unavailable: "The host serves no {ns} settings namespace, so there is nothing to edit here.",
|
|
2717
|
+
groupsTitle: "Rule groups",
|
|
2718
|
+
groupsHint: "A rule group is a named rule fragment: a session references it by id and the definition is read at decision time. Editing one here applies at once to every session that references it.",
|
|
2719
|
+
groupsEmpty: "No rule groups are defined yet.",
|
|
2720
|
+
groupsAdd: "Add a rule group",
|
|
2721
|
+
groupsRemove: "Remove this group",
|
|
2722
|
+
groupIdLabel: "id",
|
|
2723
|
+
groupNameLabel: "Name",
|
|
2724
|
+
groupDefaultLoad: "New sessions load it",
|
|
2725
|
+
groupSideRead: "Read-side rules",
|
|
2726
|
+
groupSideWrite: "Write-side rules",
|
|
2727
|
+
entryAllow: "allowed",
|
|
2728
|
+
entryDeny: "excluded",
|
|
2729
|
+
groupAxesHint: "Either side may stay empty: an empty side means this group does not cover that axis.",
|
|
2730
|
+
groupsDefaultHint: "The checked groups are what a new session starts out referencing; a session already running keeps its own preset and is unaffected here.",
|
|
2731
|
+
groupsDefaultHintCustom: "They are attached only to the axis whose kind is set to Custom here; while both axes keep a closed kind this checkbox does nothing.",
|
|
2732
|
+
groupsProblemIdEmpty: "A rule group needs a non-empty id.",
|
|
2733
|
+
groupsProblemIdDuplicate: "Rule-group ids must be unique: {id} appears more than once.",
|
|
2734
|
+
groupsProblemNotAbsolute: "{path} in the {side} {entry} list of rule group {id} is not an absolute path.",
|
|
2735
|
+
live: "A save applies to sessions created afterwards; the current session stays with the two dropdowns above the composer."
|
|
2736
|
+
};
|
|
2737
|
+
//#endregion
|
|
2738
|
+
//#region lib/types/client/row/index.js
|
|
2739
|
+
/**
|
|
2740
|
+
* 双轴访问模式组合包的浏览器半边:General 设置页上这一行自己的配置。
|
|
2741
|
+
* 宿主半边在 `@t4r71/dsh-dual-axis`(注册设置节、新会话钉轴),这一半只负责改那份设置。
|
|
2742
|
+
*
|
|
2743
|
+
* 页面只表达一件事 —— 新会话从哪两条轴开始:读轴与写轴,每条都能选
|
|
2744
|
+
* deny / workspace / all / custom,选 custom 时再给底座与两个绝对路径列表
|
|
2745
|
+
* (放行、排除,排除优先),再加上组库与「新对话默认加载哪些组」。取值只写进
|
|
2746
|
+
* 宿主注册的 `dual-axis` 设置节(节名就是这一行在条目树里的行 id),页面上不另存一份。
|
|
2747
|
+
*
|
|
2748
|
+
* 0.1.7 起,设置页的表单由**页面下发**:这一行不再自持 `ctx.settingsScope`
|
|
2749
|
+
* (该服务已整块删除),而是消费 `props.form` —— owner 渲染时算好的
|
|
2750
|
+
* `{ state: ConfigFormSnapshot, mutate }`。state 里的 `value` / `user` / `writable`
|
|
2751
|
+
* 与 0.1.6 的 scope 快照同义,因此草稿层原样保留;写入从逐字段的
|
|
2752
|
+
* `set` / `unset` 改成一次 `mutate`,两条轴共用同一个 revision fence。
|
|
2753
|
+
*
|
|
2754
|
+
* @module @t4r71/dsh-dual-axis-ui/client
|
|
2755
|
+
*/
|
|
2756
|
+
/** 两条轴的字段名。逐个处理而不是复制两段代码:加一条轴不会漏掉另一条。 */
|
|
2757
|
+
const AXES = ["read", "write"];
|
|
2758
|
+
/**
|
|
2759
|
+
* 这一行在组合包 `cordis.patch.yml` 里的行 id。
|
|
2760
|
+
*
|
|
2761
|
+
* 宿主半边 `@t4r71/dsh-dual-axis` 的补丁里那一行就叫 `dual-axis`
|
|
2762
|
+
* (`packages/bundle/dual-axis/cordis.patch.yml:11`)。**这个 id 不能自造**:
|
|
2763
|
+
* 插件页是拿它去条目树里查那一条 entry 的。
|
|
2764
|
+
*/
|
|
2765
|
+
const DUAL_AXIS_ROW_ID = "dual-axis";
|
|
2766
|
+
/** 这一行的配置页在插件页上注册的键:`<组合包名>#<行 id>`。 */
|
|
2767
|
+
const DUAL_AXIS_ROW_KEY = `@t4r71/dsh-dual-axis#${DUAL_AXIS_ROW_ID}`;
|
|
2768
|
+
/** 这份配置行需要的服务:page 由 ui-settings-general 的 General 分栏声明。 */
|
|
2769
|
+
const inject$1 = ["slots", "locale"];
|
|
2770
|
+
/** 一条规则草稿读不懂时用的词典键。 */
|
|
2771
|
+
const GROUP_PROBLEM_KEYS = {
|
|
2772
|
+
idEmpty: "groupsProblemIdEmpty",
|
|
2773
|
+
idDuplicate: "groupsProblemIdDuplicate",
|
|
2774
|
+
notAbsolute: "groupsProblemNotAbsolute"
|
|
2775
|
+
};
|
|
2776
|
+
/**
|
|
2777
|
+
* 把规则组的说明键转成当前语言的文案。
|
|
2778
|
+
*
|
|
2779
|
+
* 说明里的「哪一侧、哪个列表」也是界面文案,所以在这里一并换成本地化词,而不是把
|
|
2780
|
+
* `read` / `allow` 这类字段名直接塞进句子给人看。
|
|
2781
|
+
* @param t - 这一行的翻译座位。
|
|
2782
|
+
* @returns 说明键与参数到文案的回调。
|
|
2783
|
+
*/
|
|
2784
|
+
function problemText(t) {
|
|
2785
|
+
return (key, params) => {
|
|
2786
|
+
const side = params["side"];
|
|
2787
|
+
const entry = params["entry"];
|
|
2788
|
+
return t(GROUP_PROBLEM_KEYS[key], {
|
|
2789
|
+
...params,
|
|
2790
|
+
...side === void 0 ? {} : { side: t(side === "read" ? "groupSideRead" : "groupSideWrite") },
|
|
2791
|
+
...entry === void 0 ? {} : { entry: t(entry === "allow" ? "entryAllow" : "entryDeny") }
|
|
2792
|
+
});
|
|
2793
|
+
};
|
|
2794
|
+
}
|
|
2795
|
+
/** 词典交给 {@link describeAxis} 的文案表。 */
|
|
2796
|
+
function textOf(t) {
|
|
2797
|
+
const kindLabels = {
|
|
2798
|
+
deny: "kindDeny",
|
|
2799
|
+
workspace: "kindWorkspace",
|
|
2800
|
+
all: "kindAll",
|
|
2801
|
+
custom: "kindCustom"
|
|
2802
|
+
};
|
|
2803
|
+
const baseLabels = {
|
|
2804
|
+
deny: "baseDeny",
|
|
2805
|
+
workspace: "baseWorkspace",
|
|
2806
|
+
all: "baseAll"
|
|
2807
|
+
};
|
|
2808
|
+
return {
|
|
2809
|
+
kind: (kind) => t(kindLabels[kind]),
|
|
2810
|
+
base: (base) => t(baseLabels[base]),
|
|
2811
|
+
custom: (base, paths) => t("customTemplate").replace("{base}", base).replace("{paths}", paths)
|
|
2812
|
+
};
|
|
2813
|
+
}
|
|
2814
|
+
/** 一条轴的中文/英文标题键。 */
|
|
2815
|
+
function titleKey(field) {
|
|
2816
|
+
return field === "read" ? "titleRead" : "titleWrite";
|
|
2817
|
+
}
|
|
2818
|
+
/** 一条轴的说明键。 */
|
|
2819
|
+
function hintKey(field) {
|
|
2820
|
+
return field === "read" ? "hintRead" : "hintWrite";
|
|
2821
|
+
}
|
|
2822
|
+
/**
|
|
2823
|
+
* 组合层声明的兜底轴:设置节里缺一条轴时,页面按这一条显示。
|
|
2824
|
+
* @param field - 轴字段名。
|
|
2825
|
+
* @returns 该字段的兜底轴值。
|
|
2826
|
+
*/
|
|
2827
|
+
function fallbackOf(field) {
|
|
2828
|
+
return field === "read" ? { kind: "all" } : { kind: "workspace" };
|
|
2829
|
+
}
|
|
2830
|
+
/**
|
|
2831
|
+
* 去掉一份草稿表里的某个字段。用展开重写而不是 `delete`:后者在 TS 里是对动态
|
|
2832
|
+
* 键的删除,lint 明确禁止,而这里本来就只是要一份少了该字段的新表。
|
|
2833
|
+
* @param drafts - 当前草稿表。
|
|
2834
|
+
* @param field - 要去掉的轴字段名。
|
|
2835
|
+
* @returns 少了该字段的新表。
|
|
2836
|
+
*/
|
|
2837
|
+
function omitKey(drafts, field) {
|
|
2838
|
+
const { [field]: _dropped, ...rest } = drafts;
|
|
2839
|
+
return rest;
|
|
2840
|
+
}
|
|
2841
|
+
/**
|
|
2842
|
+
* 把一条轴值摊成设置文档接受的那份 JSON。
|
|
2843
|
+
*
|
|
2844
|
+
* `AxisValue` 的两个路径列表是 `readonly string[]`,而设置文档的取值是
|
|
2845
|
+
* `JsonValue`(可变数组):传只读数组会被线上格式挡住,所以写入前摊平一层。
|
|
2846
|
+
* @param axis - 一条轴值。
|
|
2847
|
+
* @returns 同一份取值的可变副本。
|
|
2848
|
+
*/
|
|
2849
|
+
function jsonAxis(axis) {
|
|
2850
|
+
if (axis.kind !== "custom") return { kind: axis.kind };
|
|
2851
|
+
return {
|
|
2852
|
+
kind: "custom",
|
|
2853
|
+
base: axis.base,
|
|
2854
|
+
allow: [...axis.allow],
|
|
2855
|
+
deny: [...axis.deny]
|
|
2856
|
+
};
|
|
2857
|
+
}
|
|
2858
|
+
/** 用户层里是否写着这个字段;presence 就是「已自定义」的判据。 */
|
|
2859
|
+
function overriddenIn(user, field) {
|
|
2860
|
+
return typeof user === "object" && user !== null && field in user;
|
|
2861
|
+
}
|
|
2862
|
+
/**
|
|
2863
|
+
* 两份 id 清单是否相同。默认加载清单的顺序就是文档顺序,所以逐位比较。
|
|
2864
|
+
* @param left - 一份 id 清单。
|
|
2865
|
+
* @param right - 另一份 id 清单。
|
|
2866
|
+
* @returns 两者是否逐位相同。
|
|
2867
|
+
*/
|
|
2868
|
+
function sameIdList(left, right) {
|
|
2869
|
+
return left.length === right.length && left.every((id, index) => id === right[index]);
|
|
2870
|
+
}
|
|
2871
|
+
/**
|
|
2872
|
+
* 跟踪两条轴与规则组库的草稿、把草稿写回设置节。
|
|
2873
|
+
*
|
|
2874
|
+
* 保存是一次全有或全无的提交:任何一份草稿还不是合法取值,这次保存就不发出任何
|
|
2875
|
+
* 写入 —— 一次只落一半的保存会留下"读轴改了、写轴没改"这种没人要求的中间状态,
|
|
2876
|
+
* 而它恰恰是权限配置里最不该出现的状态。规则组库与默认加载清单写在同一节里,
|
|
2877
|
+
* 于是它们也共用同一个 revision fence,不会出现「组建好了但没人默认加载」这种半落。
|
|
2878
|
+
* 0.1.7 的 `form.mutate` 收一串 op 并共用一个 fence,因此这一条契约由一次调用
|
|
2879
|
+
* 本身保证,不再靠调用方连发两次写入去凑。
|
|
2880
|
+
* @param form - 页面下发的表单面:当前快照与一次原子提交。
|
|
2881
|
+
* @param t - 这一行的翻译座位,规则组的说明文案按当前语言取。
|
|
2882
|
+
* @returns 两条轴与组库的状态,以及编辑、保存、放弃动作。
|
|
2883
|
+
*/
|
|
2884
|
+
function useDrafts(form, t) {
|
|
2885
|
+
const [drafts, setDrafts] = (0, react.useState)({});
|
|
2886
|
+
const [resetting, setResetting] = (0, react.useState)([]);
|
|
2887
|
+
const [groupDraft, setGroupDraft] = (0, react.useState)(null);
|
|
2888
|
+
const [saving, setSaving] = (0, react.useState)(false);
|
|
2889
|
+
const [failed, setFailed] = (0, react.useState)(false);
|
|
2890
|
+
const value = form.state.value ?? {};
|
|
2891
|
+
const user = form.state.user;
|
|
2892
|
+
const rows = {};
|
|
2893
|
+
for (const field of AXES) rows[field] = {
|
|
2894
|
+
draft: drafts[field] ?? draftOf(value[field], fallbackOf(field)),
|
|
2895
|
+
overridden: overriddenIn(user, field) || resetting.includes(field)
|
|
2896
|
+
};
|
|
2897
|
+
const storedGroups = groupDraftsOf(value.groups, value.defaultGroups);
|
|
2898
|
+
const groups = groupDraft ?? storedGroups;
|
|
2899
|
+
const groupValidation = groupDraft === null ? void 0 : groupLibraryOf(groupDraft, problemText(t));
|
|
2900
|
+
/** 这次保存要执行的写动作,或第一条挡住保存的说明。 */
|
|
2901
|
+
const plan = () => {
|
|
2902
|
+
const writes = [];
|
|
2903
|
+
for (const field of AXES) {
|
|
2904
|
+
if (resetting.includes(field)) {
|
|
2905
|
+
if (overriddenIn(user, field)) writes.push({
|
|
2906
|
+
kind: "clear",
|
|
2907
|
+
path: [field]
|
|
2908
|
+
});
|
|
2909
|
+
continue;
|
|
2910
|
+
}
|
|
2911
|
+
const parsed = scopeOf(rows[field].draft);
|
|
2912
|
+
if (!parsed.ok) return { problem: parsed.problem };
|
|
2913
|
+
const saved = scopeOf(draftOf(value[field], fallbackOf(field)));
|
|
2914
|
+
if (saved.ok && sameAxis(saved.value, parsed.value)) continue;
|
|
2915
|
+
writes.push({
|
|
2916
|
+
kind: "set",
|
|
2917
|
+
path: [field],
|
|
2918
|
+
value: jsonAxis(parsed.value)
|
|
2919
|
+
});
|
|
2920
|
+
}
|
|
2921
|
+
if (groupDraft === null) return { writes };
|
|
2922
|
+
const library = groupLibraryOf(groupDraft, problemText(t));
|
|
2923
|
+
if (!library.ok) return { problem: library.problem };
|
|
2924
|
+
const stored = groupLibraryOf(storedGroups, problemText(t));
|
|
2925
|
+
const libraryChanged = !(stored.ok && sameGroupLibrary(stored.value, library.value));
|
|
2926
|
+
const defaultsChanged = !sameIdList(defaultGroupIdsOf(storedGroups), defaultGroupIdsOf(groupDraft));
|
|
2927
|
+
if (libraryChanged) writes.push({
|
|
2928
|
+
kind: "set",
|
|
2929
|
+
path: ["groups"],
|
|
2930
|
+
value: jsonGroupLibrary(library.value)
|
|
2931
|
+
});
|
|
2932
|
+
if (libraryChanged || defaultsChanged) writes.push({
|
|
2933
|
+
kind: "set",
|
|
2934
|
+
path: ["defaultGroups"],
|
|
2935
|
+
value: jsonDefaultGroupIds(defaultGroupIdsOf(groupDraft))
|
|
2936
|
+
});
|
|
2937
|
+
return { writes };
|
|
2938
|
+
};
|
|
2939
|
+
const validation = plan();
|
|
2940
|
+
const invalid = "problem" in validation ? validation.problem : "";
|
|
2941
|
+
const save = () => {
|
|
2942
|
+
const planned = plan();
|
|
2943
|
+
if ("problem" in planned || planned.writes.length === 0) return;
|
|
2944
|
+
const ops = planned.writes.map((write) => write.kind === "clear" ? {
|
|
2945
|
+
op: "unset",
|
|
2946
|
+
path: [...write.path]
|
|
2947
|
+
} : {
|
|
2948
|
+
op: "set",
|
|
2949
|
+
path: [...write.path],
|
|
2950
|
+
value: write.value
|
|
2951
|
+
});
|
|
2952
|
+
setSaving(true);
|
|
2953
|
+
setFailed(false);
|
|
2954
|
+
(async () => {
|
|
2955
|
+
let landed = false;
|
|
2956
|
+
try {
|
|
2957
|
+
landed = await form.mutate(ops);
|
|
2958
|
+
} catch {
|
|
2959
|
+
landed = false;
|
|
2960
|
+
}
|
|
2961
|
+
setSaving(false);
|
|
2962
|
+
setFailed(!landed);
|
|
2963
|
+
if (landed) {
|
|
2964
|
+
setDrafts({});
|
|
2965
|
+
setResetting([]);
|
|
2966
|
+
setGroupDraft(null);
|
|
2967
|
+
}
|
|
2968
|
+
})();
|
|
2969
|
+
};
|
|
2970
|
+
return {
|
|
2971
|
+
rows,
|
|
2972
|
+
groups,
|
|
2973
|
+
groupProblem: groupValidation !== void 0 && !groupValidation.ok ? groupValidation.problem : "",
|
|
2974
|
+
edit: (field, draft) => {
|
|
2975
|
+
setDrafts((current) => ({
|
|
2976
|
+
...current,
|
|
2977
|
+
[field]: draft
|
|
2978
|
+
}));
|
|
2979
|
+
},
|
|
2980
|
+
editGroups: (next) => {
|
|
2981
|
+
setGroupDraft(next);
|
|
2982
|
+
},
|
|
2983
|
+
reset: (field) => {
|
|
2984
|
+
setResetting((current) => current.includes(field) ? current : [...current, field]);
|
|
2985
|
+
setDrafts((current) => omitKey(current, field));
|
|
2986
|
+
},
|
|
2987
|
+
save,
|
|
2988
|
+
discard: () => {
|
|
2989
|
+
setDrafts({});
|
|
2990
|
+
setResetting([]);
|
|
2991
|
+
setGroupDraft(null);
|
|
2992
|
+
setFailed(false);
|
|
2993
|
+
},
|
|
2994
|
+
dirty: Object.keys(drafts).length > 0 || resetting.length > 0 || groupDraft !== null,
|
|
2995
|
+
saving,
|
|
2996
|
+
failed,
|
|
2997
|
+
invalid
|
|
2998
|
+
};
|
|
2999
|
+
}
|
|
3000
|
+
/**
|
|
3001
|
+
* 一条轴的一组控件:取值下拉,custom 时再加底座与两个路径文本框。
|
|
3002
|
+
* @param props - 这一条轴的字段名、草稿、禁用状态、翻译与编辑回调。
|
|
3003
|
+
* @returns 这一条轴的控件。
|
|
3004
|
+
*/
|
|
3005
|
+
function AxisFields(props) {
|
|
3006
|
+
const { draft } = props.row;
|
|
3007
|
+
const t = props.t;
|
|
3008
|
+
const title = t(titleKey(props.field));
|
|
3009
|
+
const edit = (patch) => {
|
|
3010
|
+
props.onEdit({
|
|
3011
|
+
...draft,
|
|
3012
|
+
...patch
|
|
3013
|
+
});
|
|
3014
|
+
};
|
|
3015
|
+
return (0, react.createElement)("div", {
|
|
3016
|
+
"data-dsh-axis": props.field,
|
|
3017
|
+
style: {
|
|
3018
|
+
display: "grid",
|
|
3019
|
+
gap: "6px"
|
|
3020
|
+
}
|
|
3021
|
+
}, (0, react.createElement)("div", { style: {
|
|
3022
|
+
display: "flex",
|
|
3023
|
+
gap: "8px",
|
|
3024
|
+
alignItems: "center"
|
|
3025
|
+
} }, (0, react.createElement)("span", { style: {
|
|
3026
|
+
fontWeight: 600,
|
|
3027
|
+
minWidth: "5rem"
|
|
3028
|
+
} }, title), (0, react.createElement)("label", { style: {
|
|
3029
|
+
display: "flex",
|
|
3030
|
+
gap: "8px",
|
|
3031
|
+
alignItems: "center"
|
|
3032
|
+
} }, (0, react.createElement)("span", { style: { fontSize: "12px" } }, t("kindLabel")), (0, react.createElement)("select", {
|
|
3033
|
+
"data-dsh-axis-kind": props.field,
|
|
3034
|
+
"aria-label": title + " " + t("kindLabel"),
|
|
3035
|
+
value: draft.kind,
|
|
3036
|
+
disabled: props.disabled,
|
|
3037
|
+
onChange: (event) => {
|
|
3038
|
+
edit({ kind: event.target.value });
|
|
3039
|
+
}
|
|
3040
|
+
}, AXIS_KINDS.map((kind) => (0, react.createElement)("option", {
|
|
3041
|
+
key: kind,
|
|
3042
|
+
value: kind
|
|
3043
|
+
}, t(kind === "deny" ? "kindDeny" : kind === "workspace" ? "kindWorkspace" : kind === "all" ? "kindAll" : "kindCustom"))))), props.row.overridden ? (0, react.createElement)("span", { style: {
|
|
3044
|
+
display: "inline-flex",
|
|
3045
|
+
gap: "6px",
|
|
3046
|
+
alignItems: "center",
|
|
3047
|
+
fontSize: "12px",
|
|
3048
|
+
opacity: .7
|
|
3049
|
+
} }, t("overridden"), (0, react.createElement)("button", {
|
|
3050
|
+
type: "button",
|
|
3051
|
+
"data-dsh-axis-reset": props.field,
|
|
3052
|
+
disabled: props.disabled,
|
|
3053
|
+
style: {
|
|
3054
|
+
fontSize: "12px",
|
|
3055
|
+
cursor: "pointer"
|
|
3056
|
+
},
|
|
3057
|
+
onClick: props.onReset
|
|
3058
|
+
}, t("reset"))) : null), (0, react.createElement)("div", { style: {
|
|
3059
|
+
opacity: .7,
|
|
3060
|
+
fontSize: "12px"
|
|
3061
|
+
} }, t(hintKey(props.field))), draft.kind !== "custom" ? null : (0, react.createElement)("div", { style: {
|
|
3062
|
+
display: "grid",
|
|
3063
|
+
gap: "6px",
|
|
3064
|
+
paddingLeft: "1rem"
|
|
3065
|
+
} }, (0, react.createElement)("label", { style: {
|
|
3066
|
+
display: "flex",
|
|
3067
|
+
gap: "8px",
|
|
3068
|
+
alignItems: "center"
|
|
3069
|
+
} }, (0, react.createElement)("span", { style: {
|
|
3070
|
+
fontSize: "12px",
|
|
3071
|
+
minWidth: "3rem"
|
|
3072
|
+
} }, t("baseLabel")), (0, react.createElement)("select", {
|
|
3073
|
+
"data-dsh-axis-base": props.field,
|
|
3074
|
+
"aria-label": title + " " + t("baseLabel"),
|
|
3075
|
+
value: draft.base,
|
|
3076
|
+
disabled: props.disabled,
|
|
3077
|
+
onChange: (event) => {
|
|
3078
|
+
edit({ base: event.target.value });
|
|
3079
|
+
}
|
|
3080
|
+
}, AXIS_BASES.map((base) => (0, react.createElement)("option", {
|
|
3081
|
+
key: base,
|
|
3082
|
+
value: base
|
|
3083
|
+
}, t(base === "deny" ? "baseDeny" : base === "workspace" ? "baseWorkspace" : "baseAll"))))), (0, react.createElement)("label", { style: {
|
|
3084
|
+
display: "grid",
|
|
3085
|
+
gap: "2px"
|
|
3086
|
+
} }, (0, react.createElement)("span", { style: { fontSize: "12px" } }, t("allowLabel")), (0, react.createElement)("textarea", {
|
|
3087
|
+
"data-dsh-axis-allow": props.field,
|
|
3088
|
+
"aria-label": title + " " + t("allowLabel"),
|
|
3089
|
+
rows: 3,
|
|
3090
|
+
spellCheck: false,
|
|
3091
|
+
value: draft.allow,
|
|
3092
|
+
disabled: props.disabled,
|
|
3093
|
+
style: {
|
|
3094
|
+
fontFamily: "monospace",
|
|
3095
|
+
width: "100%"
|
|
3096
|
+
},
|
|
3097
|
+
onChange: (event) => {
|
|
3098
|
+
edit({ allow: event.target.value });
|
|
3099
|
+
}
|
|
3100
|
+
})), (0, react.createElement)("label", { style: {
|
|
3101
|
+
display: "grid",
|
|
3102
|
+
gap: "2px"
|
|
3103
|
+
} }, (0, react.createElement)("span", { style: { fontSize: "12px" } }, t("denyLabel")), (0, react.createElement)("textarea", {
|
|
3104
|
+
"data-dsh-axis-deny": props.field,
|
|
3105
|
+
"aria-label": title + " " + t("denyLabel"),
|
|
3106
|
+
rows: 3,
|
|
3107
|
+
spellCheck: false,
|
|
3108
|
+
value: draft.deny,
|
|
3109
|
+
disabled: props.disabled,
|
|
3110
|
+
style: {
|
|
3111
|
+
fontFamily: "monospace",
|
|
3112
|
+
width: "100%"
|
|
3113
|
+
},
|
|
3114
|
+
onChange: (event) => {
|
|
3115
|
+
edit({ deny: event.target.value });
|
|
3116
|
+
}
|
|
3117
|
+
}))), draft.problem === "" ? null : (0, react.createElement)("div", { style: {
|
|
3118
|
+
fontSize: "12px",
|
|
3119
|
+
opacity: .85
|
|
3120
|
+
} }, draft.problem));
|
|
3121
|
+
}
|
|
3122
|
+
/**
|
|
3123
|
+
* 配置行本体:两条轴各一组控件,加一个保存、一个放弃、以及每条轴自己的
|
|
3124
|
+
* 「恢复默认」。
|
|
3125
|
+
* @param props - 页面在 \`plugins.row.config\` 渲染点下发的 \`view\` 与表单面,加当前语言的翻译。
|
|
3126
|
+
* @returns 摘要行,或带控件的表单。
|
|
3127
|
+
*/
|
|
3128
|
+
function DualAxisRow(props) {
|
|
3129
|
+
const t = props.t;
|
|
3130
|
+
const form = props.form;
|
|
3131
|
+
if (form === void 0) return (0, react.createElement)("div", null, t("unavailable", { ns: DUAL_AXIS_ROW_ID }));
|
|
3132
|
+
return (0, react.createElement)(DualAxisForm, {
|
|
3133
|
+
view: props.view,
|
|
3134
|
+
form,
|
|
3135
|
+
t
|
|
3136
|
+
});
|
|
3137
|
+
}
|
|
3138
|
+
/**
|
|
3139
|
+
* 一个规则组的控件:id、名字、默认加载勾选、删除,以及读侧与写侧各两个路径框。
|
|
3140
|
+
*
|
|
3141
|
+
* 两侧各自可空 —— 一个组可以只给读侧、只给写侧或两者都给(规格 §「组的规则可只
|
|
3142
|
+
* 覆盖一根轴」)。两侧都空的组是合法的:内容随时可改,先建出来再填是正常用法。
|
|
3143
|
+
* 四个路径框用 \`data-dsh-group-side\`、\`data-dsh-group-entry\` 与 \`data-dsh-group-index\`
|
|
3144
|
+
* 三件套定位,而不是四组写死的属性名:两侧的结构本来就是同一份。
|
|
3145
|
+
* @param props - 组在这份草稿里的位置、草稿本身、禁用状态、翻译与编辑回调。
|
|
3146
|
+
* @returns 这一个组的控件。
|
|
3147
|
+
*/
|
|
3148
|
+
function GroupFields(props) {
|
|
3149
|
+
const draft = props.draft;
|
|
3150
|
+
const t = props.t;
|
|
3151
|
+
const index = props.index;
|
|
3152
|
+
const ordinal = String(index + 1);
|
|
3153
|
+
/** 改这个组的某一侧规则。 */
|
|
3154
|
+
const editSide = (which, patch) => {
|
|
3155
|
+
props.onEdit(which === "read" ? { read: {
|
|
3156
|
+
...draft.read,
|
|
3157
|
+
...patch
|
|
3158
|
+
} } : { write: {
|
|
3159
|
+
...draft.write,
|
|
3160
|
+
...patch
|
|
3161
|
+
} });
|
|
3162
|
+
};
|
|
3163
|
+
/** 这个组的一条轴的两个路径框。 */
|
|
3164
|
+
const side = (which) => {
|
|
3165
|
+
const rules = which === "read" ? draft.read : draft.write;
|
|
3166
|
+
const sideName = t(which === "read" ? "groupSideRead" : "groupSideWrite");
|
|
3167
|
+
const area = (entry) => (0, react.createElement)("label", { style: {
|
|
3168
|
+
display: "grid",
|
|
3169
|
+
gap: "2px"
|
|
3170
|
+
} }, (0, react.createElement)("span", { style: { fontSize: "12px" } }, t(entry === "allow" ? "allowLabel" : "denyLabel")), (0, react.createElement)("textarea", {
|
|
3171
|
+
"data-dsh-group-side": which,
|
|
3172
|
+
"data-dsh-group-entry": entry,
|
|
3173
|
+
"data-dsh-group-index": index,
|
|
3174
|
+
"aria-label": sideName + " " + t(entry === "allow" ? "allowLabel" : "denyLabel"),
|
|
3175
|
+
rows: 2,
|
|
3176
|
+
spellCheck: false,
|
|
3177
|
+
value: rules[entry],
|
|
3178
|
+
disabled: props.disabled,
|
|
3179
|
+
style: {
|
|
3180
|
+
fontFamily: "monospace",
|
|
3181
|
+
width: "100%"
|
|
3182
|
+
},
|
|
3183
|
+
onChange: (event) => {
|
|
3184
|
+
editSide(which, { [entry]: event.target.value });
|
|
3185
|
+
}
|
|
3186
|
+
}));
|
|
3187
|
+
return (0, react.createElement)("div", { style: {
|
|
3188
|
+
display: "grid",
|
|
3189
|
+
gap: "4px",
|
|
3190
|
+
paddingLeft: "0.5rem"
|
|
3191
|
+
} }, (0, react.createElement)("div", { style: {
|
|
3192
|
+
fontSize: "12px",
|
|
3193
|
+
fontWeight: 600
|
|
3194
|
+
} }, sideName), area("allow"), area("deny"));
|
|
3195
|
+
};
|
|
3196
|
+
return (0, react.createElement)("div", {
|
|
3197
|
+
"data-dsh-group": index,
|
|
3198
|
+
style: {
|
|
3199
|
+
display: "grid",
|
|
3200
|
+
gap: "6px",
|
|
3201
|
+
padding: "8px",
|
|
3202
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
3203
|
+
borderRadius: "8px"
|
|
3204
|
+
}
|
|
3205
|
+
}, (0, react.createElement)("div", { style: {
|
|
3206
|
+
display: "flex",
|
|
3207
|
+
gap: "10px",
|
|
3208
|
+
alignItems: "center",
|
|
3209
|
+
flexWrap: "wrap"
|
|
3210
|
+
} }, (0, react.createElement)("label", { style: {
|
|
3211
|
+
display: "flex",
|
|
3212
|
+
gap: "6px",
|
|
3213
|
+
alignItems: "center"
|
|
3214
|
+
} }, (0, react.createElement)("span", { style: { fontSize: "12px" } }, t("groupIdLabel")), (0, react.createElement)("input", {
|
|
3215
|
+
type: "text",
|
|
3216
|
+
"data-dsh-group-id": index,
|
|
3217
|
+
"aria-label": t("groupIdLabel") + " " + ordinal,
|
|
3218
|
+
value: draft.id,
|
|
3219
|
+
disabled: props.disabled,
|
|
3220
|
+
spellCheck: false,
|
|
3221
|
+
style: {
|
|
3222
|
+
fontFamily: "monospace",
|
|
3223
|
+
width: "10rem"
|
|
3224
|
+
},
|
|
3225
|
+
onChange: (event) => {
|
|
3226
|
+
props.onEdit({ id: event.target.value });
|
|
3227
|
+
}
|
|
3228
|
+
})), (0, react.createElement)("label", { style: {
|
|
3229
|
+
display: "flex",
|
|
3230
|
+
gap: "6px",
|
|
3231
|
+
alignItems: "center"
|
|
3232
|
+
} }, (0, react.createElement)("span", { style: { fontSize: "12px" } }, t("groupNameLabel")), (0, react.createElement)("input", {
|
|
3233
|
+
type: "text",
|
|
3234
|
+
"data-dsh-group-name": index,
|
|
3235
|
+
"aria-label": t("groupNameLabel") + " " + ordinal,
|
|
3236
|
+
value: draft.name,
|
|
3237
|
+
disabled: props.disabled,
|
|
3238
|
+
onChange: (event) => {
|
|
3239
|
+
props.onEdit({ name: event.target.value });
|
|
3240
|
+
}
|
|
3241
|
+
})), (0, react.createElement)("label", { style: {
|
|
3242
|
+
display: "flex",
|
|
3243
|
+
gap: "6px",
|
|
3244
|
+
alignItems: "center",
|
|
3245
|
+
fontSize: "12px"
|
|
3246
|
+
} }, (0, react.createElement)("input", {
|
|
3247
|
+
type: "checkbox",
|
|
3248
|
+
"data-dsh-group-default": index,
|
|
3249
|
+
checked: draft.default,
|
|
3250
|
+
disabled: props.disabled,
|
|
3251
|
+
onChange: (event) => {
|
|
3252
|
+
props.onEdit({ default: event.target.checked });
|
|
3253
|
+
}
|
|
3254
|
+
}), t("groupDefaultLoad")), (0, react.createElement)("button", {
|
|
3255
|
+
type: "button",
|
|
3256
|
+
"data-dsh-group-remove": index,
|
|
3257
|
+
disabled: props.disabled,
|
|
3258
|
+
style: {
|
|
3259
|
+
fontSize: "12px",
|
|
3260
|
+
cursor: "pointer"
|
|
3261
|
+
},
|
|
3262
|
+
onClick: props.onRemove
|
|
3263
|
+
}, t("groupsRemove"))), (0, react.createElement)("div", { style: {
|
|
3264
|
+
fontSize: "12px",
|
|
3265
|
+
opacity: .7
|
|
3266
|
+
} }, t("groupAxesHint")), side("read"), side("write"));
|
|
3267
|
+
}
|
|
3268
|
+
/**
|
|
3269
|
+
* 拿到表单面之后的表单本体。页面只在 \`view === 'page'\` 时给出 form,因此
|
|
3270
|
+
* \`summary\` 分支读的是设置文档里已生效的轴值,与 \`page\` 分支同一份快照。
|
|
3271
|
+
* @param props - 视图、表单面与翻译。
|
|
3272
|
+
* @returns 一句话摘要,或带控件的表单。
|
|
3273
|
+
*/
|
|
3274
|
+
function DualAxisForm(props) {
|
|
3275
|
+
const form = props.form;
|
|
3276
|
+
const t = props.t;
|
|
3277
|
+
const drafts = useDrafts(form, t);
|
|
3278
|
+
const text = textOf(t);
|
|
3279
|
+
const stored = form.state.value ?? {};
|
|
3280
|
+
const summary = AXES.map((field) => `${t(titleKey(field))}: ${describeAxis(axisValueOf$1(stored[field]) ?? fallbackOf(field), text)}`).join(" · ");
|
|
3281
|
+
if (props.view === "summary") return summary;
|
|
3282
|
+
if (form.state.status === "loading") return (0, react.createElement)("div", null, t("loading"));
|
|
3283
|
+
const disabled = !form.state.writable || drafts.saving;
|
|
3284
|
+
return (0, react.createElement)("div", { style: {
|
|
3285
|
+
display: "grid",
|
|
3286
|
+
gap: "12px",
|
|
3287
|
+
maxWidth: "760px"
|
|
3288
|
+
} }, (0, react.createElement)("div", null, summary), (0, react.createElement)("div", { style: {
|
|
3289
|
+
fontSize: "12px",
|
|
3290
|
+
opacity: .7
|
|
3291
|
+
} }, t("live")), ...AXES.map((field) => (0, react.createElement)(AxisFields, {
|
|
3292
|
+
key: field,
|
|
3293
|
+
field,
|
|
3294
|
+
row: drafts.rows[field],
|
|
3295
|
+
disabled,
|
|
3296
|
+
t,
|
|
3297
|
+
onEdit: (draft) => {
|
|
3298
|
+
drafts.edit(field, draft);
|
|
3299
|
+
},
|
|
3300
|
+
onReset: () => {
|
|
3301
|
+
drafts.reset(field);
|
|
3302
|
+
}
|
|
3303
|
+
})), (0, react.createElement)("div", {
|
|
3304
|
+
"data-dsh-groups": "",
|
|
3305
|
+
style: {
|
|
3306
|
+
display: "grid",
|
|
3307
|
+
gap: "8px"
|
|
3308
|
+
}
|
|
3309
|
+
}, (0, react.createElement)("div", { style: { fontWeight: 600 } }, t("groupsTitle")), (0, react.createElement)("div", { style: {
|
|
3310
|
+
fontSize: "12px",
|
|
3311
|
+
opacity: .7
|
|
3312
|
+
} }, t("groupsHint")), drafts.groups.length === 0 ? (0, react.createElement)("div", { style: {
|
|
3313
|
+
fontSize: "12px",
|
|
3314
|
+
opacity: .7
|
|
3315
|
+
} }, t("groupsEmpty")) : null, ...drafts.groups.map((group, index) => (0, react.createElement)(GroupFields, {
|
|
3316
|
+
key: String(index),
|
|
3317
|
+
index,
|
|
3318
|
+
draft: group,
|
|
3319
|
+
disabled,
|
|
3320
|
+
t,
|
|
3321
|
+
onEdit: (patch) => {
|
|
3322
|
+
drafts.editGroups(drafts.groups.map((current, at) => at === index ? {
|
|
3323
|
+
...current,
|
|
3324
|
+
...patch
|
|
3325
|
+
} : current));
|
|
3326
|
+
},
|
|
3327
|
+
onRemove: () => {
|
|
3328
|
+
drafts.editGroups(drafts.groups.filter((_current, at) => at !== index));
|
|
3329
|
+
}
|
|
3330
|
+
})), (0, react.createElement)("button", {
|
|
3331
|
+
type: "button",
|
|
3332
|
+
"data-dsh-group-add": "",
|
|
3333
|
+
disabled,
|
|
3334
|
+
style: {
|
|
3335
|
+
justifySelf: "start",
|
|
3336
|
+
fontSize: "12px",
|
|
3337
|
+
cursor: "pointer"
|
|
3338
|
+
},
|
|
3339
|
+
onClick: () => {
|
|
3340
|
+
drafts.editGroups([...drafts.groups, emptyGroupDraft(drafts.groups)]);
|
|
3341
|
+
}
|
|
3342
|
+
}, t("groupsAdd")), drafts.groupProblem === "" ? null : (0, react.createElement)("div", {
|
|
3343
|
+
role: "alert",
|
|
3344
|
+
style: { fontSize: "12px" }
|
|
3345
|
+
}, drafts.groupProblem), (0, react.createElement)("div", { style: {
|
|
3346
|
+
fontSize: "12px",
|
|
3347
|
+
opacity: .7
|
|
3348
|
+
} }, t("groupsDefaultHint")), (0, react.createElement)("div", { style: {
|
|
3349
|
+
fontSize: "12px",
|
|
3350
|
+
opacity: .7
|
|
3351
|
+
} }, t("groupsDefaultHintCustom"))), (0, react.createElement)("div", { style: {
|
|
3352
|
+
display: "flex",
|
|
3353
|
+
gap: "8px",
|
|
3354
|
+
alignItems: "center",
|
|
3355
|
+
flexWrap: "wrap"
|
|
3356
|
+
} }, (0, react.createElement)("button", {
|
|
3357
|
+
type: "button",
|
|
3358
|
+
"data-dsh-axis-save": "",
|
|
3359
|
+
disabled: disabled || !drafts.dirty || drafts.invalid !== "",
|
|
3360
|
+
onClick: drafts.save
|
|
3361
|
+
}, t(drafts.saving ? "saving" : "save")), (0, react.createElement)("button", {
|
|
3362
|
+
type: "button",
|
|
3363
|
+
disabled: !drafts.dirty || drafts.saving,
|
|
3364
|
+
onClick: drafts.discard
|
|
3365
|
+
}, t("discard")), drafts.failed ? (0, react.createElement)("span", { style: { fontSize: "12px" } }, t("failed")) : null, drafts.invalid !== "" ? (0, react.createElement)("span", { style: { fontSize: "12px" } }, drafts.invalid) : null, !form.state.writable ? (0, react.createElement)("span", { style: { fontSize: "12px" } }, t("readOnly")) : null));
|
|
3366
|
+
}
|
|
3367
|
+
/**
|
|
3368
|
+
* 挂载配置行:注册词典,再把这一行注册到插件页的 \`plugins.row.config\`。
|
|
3369
|
+
* 表单由页面下发,这里不再自持任何 scope。
|
|
3370
|
+
* @param ctx - 浏览器插件上下文。
|
|
3371
|
+
*/
|
|
3372
|
+
function apply$1(ctx) {
|
|
3373
|
+
ctx.effect(() => ctx.locale.register(DUAL_AXIS_LOCALE_NS, {
|
|
3374
|
+
zh,
|
|
3375
|
+
en
|
|
3376
|
+
}), "dual-axis: dictionaries");
|
|
3377
|
+
ctx.slots.inject("plugins.row.config", () => ctx.slots.register({
|
|
3378
|
+
name: "plugins.row.config",
|
|
3379
|
+
key: DUAL_AXIS_ROW_KEY,
|
|
3380
|
+
locale: DUAL_AXIS_LOCALE_NS
|
|
3381
|
+
}, DualAxisRow));
|
|
3382
|
+
}
|
|
3383
|
+
//#endregion
|
|
3384
|
+
//#region lib/types/client/index.js
|
|
3385
|
+
const inject = [.../* @__PURE__ */ new Set([...inject$2, ...inject$1])];
|
|
3386
|
+
/**
|
|
3387
|
+
* 挂载两个界面面:下拉那面先挂(它注册会话输入区的 slot 与词典),配置行那面随后。
|
|
3388
|
+
* @param ctx - 浏览器插件上下文。
|
|
3389
|
+
*/
|
|
3390
|
+
function apply(ctx) {
|
|
3391
|
+
apply$2(ctx);
|
|
3392
|
+
apply$1(ctx);
|
|
3393
|
+
}
|
|
3394
|
+
//#endregion
|
|
3395
|
+
exports.apply = apply;
|
|
3396
|
+
exports.inject = inject;
|
|
3397
|
+
return module.exports;
|
|
3398
|
+
}
|
|
3399
|
+
});
|
|
3400
|
+
|
|
3401
|
+
//# sourceMappingURL=client.js.map
|