@dsh-plus/secret-env 0.1.29 → 0.1.30
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/lib/client.js +119 -87
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/package.json +2 -2
- package/src/client/menu.tsx +146 -77
- package/src/config.ts +4 -4
- package/src/service.ts +2 -2
package/lib/client.js
CHANGED
|
@@ -108,7 +108,7 @@ window.__ModuleLoader__.load({
|
|
|
108
108
|
unsaved: "未保存",
|
|
109
109
|
loading: "加载中…",
|
|
110
110
|
unavailable: "配置服务不可用。",
|
|
111
|
-
readOnly: "当前部署无 settings provider
|
|
111
|
+
readOnly: "当前部署无 settings provider,配置为只读;请在 profile 的 cordis.patch.yml 行级配置。",
|
|
112
112
|
invalidNumber: "请输入正整数。",
|
|
113
113
|
collapse: "收起",
|
|
114
114
|
expand: "展开",
|
|
@@ -125,7 +125,7 @@ window.__ModuleLoader__.load({
|
|
|
125
125
|
unsaved: "Unsaved",
|
|
126
126
|
loading: "Loading…",
|
|
127
127
|
unavailable: "Settings service unavailable.",
|
|
128
|
-
readOnly: "No settings provider in this deployment; config is read-only.
|
|
128
|
+
readOnly: "No settings provider in this deployment; config is read-only. Configure it at the profile row in cordis.patch.yml.",
|
|
129
129
|
invalidNumber: "Enter a positive integer.",
|
|
130
130
|
collapse: "Collapse",
|
|
131
131
|
expand: "Expand",
|
|
@@ -754,6 +754,105 @@ ${extra}
|
|
|
754
754
|
}))
|
|
755
755
|
];
|
|
756
756
|
}
|
|
757
|
+
/** 菜单展开期的按键处理(捕获阶段先於 Lexical/官方管线截获,已消费则阻止冒泡)。 */
|
|
758
|
+
function handleMenuKey(event, ctx) {
|
|
759
|
+
const list = ctx.candidates;
|
|
760
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
761
|
+
event.preventDefault();
|
|
762
|
+
event.stopPropagation();
|
|
763
|
+
const delta = event.key === "ArrowDown" ? 1 : -1;
|
|
764
|
+
ctx.setHighlight((prev) => (prev + delta + list.length) % list.length);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (event.key === "Enter" || event.key === "Tab") {
|
|
768
|
+
const entry = list[ctx.highlight] ?? list[0];
|
|
769
|
+
if (entry !== void 0) {
|
|
770
|
+
event.preventDefault();
|
|
771
|
+
event.stopPropagation();
|
|
772
|
+
ctx.pick(entry);
|
|
773
|
+
}
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
if (event.key === "Escape") {
|
|
777
|
+
event.preventDefault();
|
|
778
|
+
event.stopPropagation();
|
|
779
|
+
ctx.suppressed.current = ctx.hit;
|
|
780
|
+
ctx.setHit(null);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
/** IME 组合期间不介入:在编辑器根上挂组合态监听,返回解绑函数。 */
|
|
784
|
+
function bindCompositionGuards(root, composing) {
|
|
785
|
+
const onStart = () => {
|
|
786
|
+
composing.current = true;
|
|
787
|
+
};
|
|
788
|
+
const onEnd = () => {
|
|
789
|
+
composing.current = false;
|
|
790
|
+
};
|
|
791
|
+
root.addEventListener("compositionstart", onStart);
|
|
792
|
+
root.addEventListener("compositionend", onEnd);
|
|
793
|
+
return () => {
|
|
794
|
+
root.removeEventListener("compositionstart", onStart);
|
|
795
|
+
root.removeEventListener("compositionend", onEnd);
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
/** composer 卡片之外按下即关闭(对齐官方菜单的外部 dismiss 语义)。 */
|
|
799
|
+
function bindOutsideDismiss(wrap, dismiss) {
|
|
800
|
+
const onDown = (event) => {
|
|
801
|
+
if (!(event.target instanceof Node)) return;
|
|
802
|
+
if ((wrap?.closest("[data-composer-card]"))?.contains(event.target) === true) return;
|
|
803
|
+
dismiss();
|
|
804
|
+
};
|
|
805
|
+
document.addEventListener("pointerdown", onDown, true);
|
|
806
|
+
return () => document.removeEventListener("pointerdown", onDown, true);
|
|
807
|
+
}
|
|
808
|
+
/** 展开态候选面板(闭合态的常驻锚点容器由调用方渲染)。 */
|
|
809
|
+
function SecretPanel(props) {
|
|
810
|
+
const { candidates, highlight, wrapRef, t, onHighlight, onPick } = props;
|
|
811
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
812
|
+
className: "dse-menuWrap",
|
|
813
|
+
ref: wrapRef,
|
|
814
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
815
|
+
className: "dse-menu",
|
|
816
|
+
role: "listbox",
|
|
817
|
+
"aria-label": t("menu.aria"),
|
|
818
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
819
|
+
className: "dse-menuTitle",
|
|
820
|
+
children: t("menu.title")
|
|
821
|
+
}), candidates.map((entry, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
822
|
+
type: "button",
|
|
823
|
+
role: "option",
|
|
824
|
+
"aria-selected": index === highlight,
|
|
825
|
+
title: `$${entry.envName}`,
|
|
826
|
+
className: `dse-menuItem${index === highlight ? " dse-menuItemActive" : ""}`,
|
|
827
|
+
onMouseEnter: () => onHighlight(index),
|
|
828
|
+
onMouseDown: (event) => {
|
|
829
|
+
event.preventDefault();
|
|
830
|
+
onPick(entry);
|
|
831
|
+
},
|
|
832
|
+
children: [
|
|
833
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
834
|
+
className: "dse-menuName",
|
|
835
|
+
children: entry.name
|
|
836
|
+
}),
|
|
837
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
838
|
+
className: "dse-menuDesc",
|
|
839
|
+
children: entry.description
|
|
840
|
+
}),
|
|
841
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
842
|
+
className: "dse-menuBadges",
|
|
843
|
+
children: [entry.once === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
844
|
+
className: "dse-badge",
|
|
845
|
+
children: t("scopeOnce")
|
|
846
|
+
}) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
847
|
+
className: "dse-badge dse-badgeDim",
|
|
848
|
+
children: entry.scope === "session" ? t("scopeSession") : entry.scope === "inherited" ? t("scopeInherited") : t("scopeGlobal")
|
|
849
|
+
})]
|
|
850
|
+
})
|
|
851
|
+
]
|
|
852
|
+
}, `${entry.scope}:${entry.name}`))]
|
|
853
|
+
})
|
|
854
|
+
});
|
|
855
|
+
}
|
|
757
856
|
function SecretMenu(props) {
|
|
758
857
|
const { sessionId, t, insertToken } = props;
|
|
759
858
|
const input = (props.useInput ?? ((_selector) => FALLBACK_INPUT))((state) => state);
|
|
@@ -799,18 +898,7 @@ ${extra}
|
|
|
799
898
|
(0, react.useEffect)(() => {
|
|
800
899
|
const root = editorRootOf(wrapRef.current);
|
|
801
900
|
if (root === null) return;
|
|
802
|
-
|
|
803
|
-
composingRef.current = true;
|
|
804
|
-
};
|
|
805
|
-
const onEnd = () => {
|
|
806
|
-
composingRef.current = false;
|
|
807
|
-
};
|
|
808
|
-
root.addEventListener("compositionstart", onStart);
|
|
809
|
-
root.addEventListener("compositionend", onEnd);
|
|
810
|
-
return () => {
|
|
811
|
-
root.removeEventListener("compositionstart", onStart);
|
|
812
|
-
root.removeEventListener("compositionend", onEnd);
|
|
813
|
-
};
|
|
901
|
+
return bindCompositionGuards(root, composingRef);
|
|
814
902
|
}, []);
|
|
815
903
|
const pick = (entry) => {
|
|
816
904
|
if (hit === null) return;
|
|
@@ -834,42 +922,22 @@ ${extra}
|
|
|
834
922
|
const root = editorRootOf(wrapRef.current);
|
|
835
923
|
if (root === null) return;
|
|
836
924
|
const onKey = (event) => {
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
const entry = list[highlightRef.current] ?? list[0];
|
|
847
|
-
if (entry !== void 0) {
|
|
848
|
-
event.preventDefault();
|
|
849
|
-
event.stopPropagation();
|
|
850
|
-
pickRef.current(entry);
|
|
851
|
-
}
|
|
852
|
-
return;
|
|
853
|
-
}
|
|
854
|
-
if (event.key === "Escape") {
|
|
855
|
-
event.preventDefault();
|
|
856
|
-
event.stopPropagation();
|
|
857
|
-
suppressedRef.current = hit;
|
|
858
|
-
setHit(null);
|
|
859
|
-
}
|
|
925
|
+
handleMenuKey(event, {
|
|
926
|
+
candidates: candidatesRef.current,
|
|
927
|
+
highlight: highlightRef.current,
|
|
928
|
+
hit,
|
|
929
|
+
suppressed: suppressedRef,
|
|
930
|
+
pick: (entry) => pickRef.current(entry),
|
|
931
|
+
setHighlight,
|
|
932
|
+
setHit
|
|
933
|
+
});
|
|
860
934
|
};
|
|
861
935
|
root.addEventListener("keydown", onKey, true);
|
|
862
936
|
return () => root.removeEventListener("keydown", onKey, true);
|
|
863
937
|
}, [open, hit]);
|
|
864
938
|
(0, react.useEffect)(() => {
|
|
865
939
|
if (!open) return;
|
|
866
|
-
|
|
867
|
-
if (!(event.target instanceof Node)) return;
|
|
868
|
-
if ((wrapRef.current?.closest("[data-composer-card]"))?.contains(event.target) === true) return;
|
|
869
|
-
setHit(null);
|
|
870
|
-
};
|
|
871
|
-
document.addEventListener("pointerdown", onDown, true);
|
|
872
|
-
return () => document.removeEventListener("pointerdown", onDown, true);
|
|
940
|
+
return bindOutsideDismiss(wrapRef.current, () => setHit(null));
|
|
873
941
|
}, [open]);
|
|
874
942
|
(0, react.useEffect)(() => {
|
|
875
943
|
setHighlight((prev) => candidates.length === 0 ? 0 : Math.min(prev, candidates.length - 1));
|
|
@@ -878,49 +946,13 @@ ${extra}
|
|
|
878
946
|
className: "dse-menuWrap",
|
|
879
947
|
ref: wrapRef
|
|
880
948
|
});
|
|
881
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
889
|
-
className: "dse-menuTitle",
|
|
890
|
-
children: t("menu.title")
|
|
891
|
-
}), candidates.map((entry, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
892
|
-
type: "button",
|
|
893
|
-
role: "option",
|
|
894
|
-
"aria-selected": index === highlight,
|
|
895
|
-
title: `$${entry.envName}`,
|
|
896
|
-
className: `dse-menuItem${index === highlight ? " dse-menuItemActive" : ""}`,
|
|
897
|
-
onMouseEnter: () => setHighlight(index),
|
|
898
|
-
onMouseDown: (event) => {
|
|
899
|
-
event.preventDefault();
|
|
900
|
-
pick(entry);
|
|
901
|
-
},
|
|
902
|
-
children: [
|
|
903
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
904
|
-
className: "dse-menuName",
|
|
905
|
-
children: entry.name
|
|
906
|
-
}),
|
|
907
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
908
|
-
className: "dse-menuDesc",
|
|
909
|
-
children: entry.description
|
|
910
|
-
}),
|
|
911
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
912
|
-
className: "dse-menuBadges",
|
|
913
|
-
children: [entry.once === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
914
|
-
className: "dse-badge",
|
|
915
|
-
children: t("scopeOnce")
|
|
916
|
-
}) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
917
|
-
className: "dse-badge dse-badgeDim",
|
|
918
|
-
children: entry.scope === "session" ? t("scopeSession") : entry.scope === "inherited" ? t("scopeInherited") : t("scopeGlobal")
|
|
919
|
-
})]
|
|
920
|
-
})
|
|
921
|
-
]
|
|
922
|
-
}, `${entry.scope}:${entry.name}`))]
|
|
923
|
-
})
|
|
949
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SecretPanel, {
|
|
950
|
+
candidates,
|
|
951
|
+
highlight,
|
|
952
|
+
wrapRef,
|
|
953
|
+
t,
|
|
954
|
+
onHighlight: setHighlight,
|
|
955
|
+
onPick: pick
|
|
924
956
|
});
|
|
925
957
|
}
|
|
926
958
|
//#endregion
|
package/lib/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
21
21
|
secrets: z<NoInfer<SecretMetaInput[]>, NoInfer<SecretMeta[]>, "volatile-defined">;
|
|
22
22
|
masked: z<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
|
|
23
23
|
}>>, "plain">;
|
|
24
|
-
/** 活动字段形态(
|
|
24
|
+
/** 活动字段形态(loader 解析产物:volatile 字段为活动引用)。 */
|
|
25
25
|
type SecretEnvConfigFields = Schemastery.TypeT<typeof Config>;
|
|
26
26
|
/** 平面配置形态(消费面的读取形态,由活动引用解包得到)。 */
|
|
27
27
|
type SecretEnvConfig = UnwrapVolatile<SecretEnvConfigFields>;
|
package/lib/index.js
CHANGED
|
@@ -13,7 +13,7 @@ const SETTINGS_NS = "dsh-plus-secret-env";
|
|
|
13
13
|
//#region src/config.ts
|
|
14
14
|
/**
|
|
15
15
|
* 配置单一事实源:cordis 行级 Config(组合默认值)与 settings namespace
|
|
16
|
-
*
|
|
16
|
+
* (用户层=profile 行级覆盖层 cordis.patch.yml,热生效)共用同一 schemastery schema。
|
|
17
17
|
* `secrets` 只是全局密钥的**元数据索引**(后缀/描述/创建时间),
|
|
18
18
|
* 值永远走 dsh-credentials seam($DSH_HOME/.credentials.yaml),不进本文件。
|
|
19
19
|
* @module secret-env/config
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-plus/secret-env",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.30",
|
|
4
4
|
"description": "dsh-plus service+ui plugin: 密钥环境变量——以 $DSH_VAR_* 变量名向 agent 暴露密钥(全局持久/会话级/一次性),执行期经 dsh-shell-env 原生注入,值不进消息流,不影响缓存率",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"src"
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@dsh-plus/shared": "0.1.
|
|
22
|
+
"@dsh-plus/shared": "0.1.36"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
25
|
"@deepseek-ai/cordis": "^4.0.5-alpha.1",
|
package/src/client/menu.tsx
CHANGED
|
@@ -7,7 +7,15 @@
|
|
|
7
7
|
* bail 通道 'slash/input-insert-text'(span + draftRev CAS,编辑器内应用)。
|
|
8
8
|
* @module secret-env/client/menu
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
type Dispatch,
|
|
12
|
+
type MutableRefObject,
|
|
13
|
+
type ReactElement,
|
|
14
|
+
type SetStateAction,
|
|
15
|
+
useEffect,
|
|
16
|
+
useRef,
|
|
17
|
+
useState,
|
|
18
|
+
} from 'react'
|
|
11
19
|
|
|
12
20
|
import { fetchSecrets, type SecretList } from './api.ts'
|
|
13
21
|
import type { Translate } from './i18n.ts'
|
|
@@ -116,6 +124,123 @@ function toCandidates(list: SecretList): SecretCandidate[] {
|
|
|
116
124
|
]
|
|
117
125
|
}
|
|
118
126
|
|
|
127
|
+
interface MenuKeyContext {
|
|
128
|
+
candidates: SecretCandidate[]
|
|
129
|
+
highlight: number
|
|
130
|
+
hit: SecretHit | null
|
|
131
|
+
suppressed: MutableRefObject<SecretHit | null>
|
|
132
|
+
pick: (entry: SecretCandidate) => void
|
|
133
|
+
setHighlight: Dispatch<SetStateAction<number>>
|
|
134
|
+
setHit: Dispatch<SetStateAction<SecretHit | null>>
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** 菜单展开期的按键处理(捕获阶段先於 Lexical/官方管线截获,已消费则阻止冒泡)。 */
|
|
138
|
+
function handleMenuKey(event: KeyboardEvent, ctx: MenuKeyContext): void {
|
|
139
|
+
const list = ctx.candidates
|
|
140
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
141
|
+
event.preventDefault()
|
|
142
|
+
event.stopPropagation()
|
|
143
|
+
const delta = event.key === 'ArrowDown' ? 1 : -1
|
|
144
|
+
ctx.setHighlight((prev) => (prev + delta + list.length) % list.length)
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
if (event.key === 'Enter' || event.key === 'Tab') {
|
|
148
|
+
const entry = list[ctx.highlight] ?? list[0]
|
|
149
|
+
if (entry !== undefined) {
|
|
150
|
+
event.preventDefault()
|
|
151
|
+
event.stopPropagation()
|
|
152
|
+
ctx.pick(entry)
|
|
153
|
+
}
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
if (event.key === 'Escape') {
|
|
157
|
+
event.preventDefault()
|
|
158
|
+
event.stopPropagation()
|
|
159
|
+
ctx.suppressed.current = ctx.hit
|
|
160
|
+
ctx.setHit(null)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** IME 组合期间不介入:在编辑器根上挂组合态监听,返回解绑函数。 */
|
|
165
|
+
function bindCompositionGuards(
|
|
166
|
+
root: HTMLElement,
|
|
167
|
+
composing: MutableRefObject<boolean>,
|
|
168
|
+
): () => void {
|
|
169
|
+
const onStart = (): void => {
|
|
170
|
+
composing.current = true
|
|
171
|
+
}
|
|
172
|
+
const onEnd = (): void => {
|
|
173
|
+
composing.current = false
|
|
174
|
+
}
|
|
175
|
+
root.addEventListener('compositionstart', onStart)
|
|
176
|
+
root.addEventListener('compositionend', onEnd)
|
|
177
|
+
return () => {
|
|
178
|
+
root.removeEventListener('compositionstart', onStart)
|
|
179
|
+
root.removeEventListener('compositionend', onEnd)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** composer 卡片之外按下即关闭(对齐官方菜单的外部 dismiss 语义)。 */
|
|
184
|
+
function bindOutsideDismiss(wrap: HTMLElement | null, dismiss: () => void): () => void {
|
|
185
|
+
const onDown = (event: PointerEvent): void => {
|
|
186
|
+
if (!(event.target instanceof Node)) return
|
|
187
|
+
const card = wrap?.closest('[data-composer-card]')
|
|
188
|
+
if (card?.contains(event.target) === true) return
|
|
189
|
+
dismiss()
|
|
190
|
+
}
|
|
191
|
+
document.addEventListener('pointerdown', onDown, true)
|
|
192
|
+
return () => document.removeEventListener('pointerdown', onDown, true)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
interface SecretPanelProps {
|
|
196
|
+
candidates: SecretCandidate[]
|
|
197
|
+
highlight: number
|
|
198
|
+
wrapRef: MutableRefObject<HTMLDivElement | null>
|
|
199
|
+
t: Translate
|
|
200
|
+
onHighlight: (index: number) => void
|
|
201
|
+
onPick: (entry: SecretCandidate) => void
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** 展开态候选面板(闭合态的常驻锚点容器由调用方渲染)。 */
|
|
205
|
+
function SecretPanel(props: SecretPanelProps): ReactElement {
|
|
206
|
+
const { candidates, highlight, wrapRef, t, onHighlight, onPick } = props
|
|
207
|
+
return (
|
|
208
|
+
<div className="dse-menuWrap" ref={wrapRef}>
|
|
209
|
+
<div className="dse-menu" role="listbox" aria-label={t('menu.aria')}>
|
|
210
|
+
<div className="dse-menuTitle">{t('menu.title')}</div>
|
|
211
|
+
{candidates.map((entry, index) => (
|
|
212
|
+
<button
|
|
213
|
+
key={`${entry.scope}:${entry.name}`}
|
|
214
|
+
type="button"
|
|
215
|
+
role="option"
|
|
216
|
+
aria-selected={index === highlight}
|
|
217
|
+
title={`$${entry.envName}`}
|
|
218
|
+
className={`dse-menuItem${index === highlight ? ' dse-menuItemActive' : ''}`}
|
|
219
|
+
onMouseEnter={() => onHighlight(index)}
|
|
220
|
+
onMouseDown={(event) => {
|
|
221
|
+
event.preventDefault()
|
|
222
|
+
onPick(entry)
|
|
223
|
+
}}
|
|
224
|
+
>
|
|
225
|
+
<span className="dse-menuName">{entry.name}</span>
|
|
226
|
+
<span className="dse-menuDesc">{entry.description}</span>
|
|
227
|
+
<span className="dse-menuBadges">
|
|
228
|
+
{entry.once === true ? <span className="dse-badge">{t('scopeOnce')}</span> : null}
|
|
229
|
+
<span className="dse-badge dse-badgeDim">
|
|
230
|
+
{entry.scope === 'session'
|
|
231
|
+
? t('scopeSession')
|
|
232
|
+
: entry.scope === 'inherited'
|
|
233
|
+
? t('scopeInherited')
|
|
234
|
+
: t('scopeGlobal')}
|
|
235
|
+
</span>
|
|
236
|
+
</span>
|
|
237
|
+
</button>
|
|
238
|
+
))}
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
119
244
|
export function SecretMenu(props: SecretMenuProps): ReactElement | null {
|
|
120
245
|
const { sessionId, t, insertToken } = props
|
|
121
246
|
// useInput 是标准钩子 prop,必须无条件调用;旧壳缺席时回落常量(菜单永不展开)。
|
|
@@ -182,18 +307,7 @@ export function SecretMenu(props: SecretMenuProps): ReactElement | null {
|
|
|
182
307
|
useEffect(() => {
|
|
183
308
|
const root = editorRootOf(wrapRef.current)
|
|
184
309
|
if (root === null) return
|
|
185
|
-
|
|
186
|
-
composingRef.current = true
|
|
187
|
-
}
|
|
188
|
-
const onEnd = (): void => {
|
|
189
|
-
composingRef.current = false
|
|
190
|
-
}
|
|
191
|
-
root.addEventListener('compositionstart', onStart)
|
|
192
|
-
root.addEventListener('compositionend', onEnd)
|
|
193
|
-
return () => {
|
|
194
|
-
root.removeEventListener('compositionstart', onStart)
|
|
195
|
-
root.removeEventListener('compositionend', onEnd)
|
|
196
|
-
}
|
|
310
|
+
return bindCompositionGuards(root, composingRef)
|
|
197
311
|
}, [])
|
|
198
312
|
|
|
199
313
|
const pick = (entry: SecretCandidate): void => {
|
|
@@ -217,29 +331,15 @@ export function SecretMenu(props: SecretMenuProps): ReactElement | null {
|
|
|
217
331
|
const root = editorRootOf(wrapRef.current)
|
|
218
332
|
if (root === null) return
|
|
219
333
|
const onKey = (event: KeyboardEvent): void => {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const entry = list[highlightRef.current] ?? list[0]
|
|
230
|
-
if (entry !== undefined) {
|
|
231
|
-
event.preventDefault()
|
|
232
|
-
event.stopPropagation()
|
|
233
|
-
pickRef.current(entry)
|
|
234
|
-
}
|
|
235
|
-
return
|
|
236
|
-
}
|
|
237
|
-
if (event.key === 'Escape') {
|
|
238
|
-
event.preventDefault()
|
|
239
|
-
event.stopPropagation()
|
|
240
|
-
suppressedRef.current = hit
|
|
241
|
-
setHit(null)
|
|
242
|
-
}
|
|
334
|
+
handleMenuKey(event, {
|
|
335
|
+
candidates: candidatesRef.current,
|
|
336
|
+
highlight: highlightRef.current,
|
|
337
|
+
hit,
|
|
338
|
+
suppressed: suppressedRef,
|
|
339
|
+
pick: (entry) => pickRef.current(entry),
|
|
340
|
+
setHighlight,
|
|
341
|
+
setHit,
|
|
342
|
+
})
|
|
243
343
|
}
|
|
244
344
|
root.addEventListener('keydown', onKey, true)
|
|
245
345
|
return () => root.removeEventListener('keydown', onKey, true)
|
|
@@ -248,14 +348,7 @@ export function SecretMenu(props: SecretMenuProps): ReactElement | null {
|
|
|
248
348
|
// 点 composer 卡片之外处关闭(对齐官方菜单的外部 dismiss 语义)。
|
|
249
349
|
useEffect(() => {
|
|
250
350
|
if (!open) return
|
|
251
|
-
|
|
252
|
-
if (!(event.target instanceof Node)) return
|
|
253
|
-
const card = wrapRef.current?.closest('[data-composer-card]')
|
|
254
|
-
if (card?.contains(event.target) === true) return
|
|
255
|
-
setHit(null)
|
|
256
|
-
}
|
|
257
|
-
document.addEventListener('pointerdown', onDown, true)
|
|
258
|
-
return () => document.removeEventListener('pointerdown', onDown, true)
|
|
351
|
+
return bindOutsideDismiss(wrapRef.current, () => setHit(null))
|
|
259
352
|
}, [open])
|
|
260
353
|
|
|
261
354
|
// 高亮随候选集变化收敛到合法范围。
|
|
@@ -267,39 +360,15 @@ export function SecretMenu(props: SecretMenuProps): ReactElement | null {
|
|
|
267
360
|
// 锚点常驻:闭合态也要挂载容器,检测效果依赖它定位 composer 编辑器根。
|
|
268
361
|
return <div className="dse-menuWrap" ref={wrapRef} />
|
|
269
362
|
}
|
|
363
|
+
|
|
270
364
|
return (
|
|
271
|
-
<
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
aria-selected={index === highlight}
|
|
280
|
-
title={`$${entry.envName}`}
|
|
281
|
-
className={`dse-menuItem${index === highlight ? ' dse-menuItemActive' : ''}`}
|
|
282
|
-
onMouseEnter={() => setHighlight(index)}
|
|
283
|
-
onMouseDown={(event) => {
|
|
284
|
-
event.preventDefault()
|
|
285
|
-
pick(entry)
|
|
286
|
-
}}
|
|
287
|
-
>
|
|
288
|
-
<span className="dse-menuName">{entry.name}</span>
|
|
289
|
-
<span className="dse-menuDesc">{entry.description}</span>
|
|
290
|
-
<span className="dse-menuBadges">
|
|
291
|
-
{entry.once === true ? <span className="dse-badge">{t('scopeOnce')}</span> : null}
|
|
292
|
-
<span className="dse-badge dse-badgeDim">
|
|
293
|
-
{entry.scope === 'session'
|
|
294
|
-
? t('scopeSession')
|
|
295
|
-
: entry.scope === 'inherited'
|
|
296
|
-
? t('scopeInherited')
|
|
297
|
-
: t('scopeGlobal')}
|
|
298
|
-
</span>
|
|
299
|
-
</span>
|
|
300
|
-
</button>
|
|
301
|
-
))}
|
|
302
|
-
</div>
|
|
303
|
-
</div>
|
|
365
|
+
<SecretPanel
|
|
366
|
+
candidates={candidates}
|
|
367
|
+
highlight={highlight}
|
|
368
|
+
wrapRef={wrapRef}
|
|
369
|
+
t={t}
|
|
370
|
+
onHighlight={setHighlight}
|
|
371
|
+
onPick={pick}
|
|
372
|
+
/>
|
|
304
373
|
)
|
|
305
374
|
}
|
package/src/config.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 配置单一事实源:cordis 行级 Config(组合默认值)与 settings namespace
|
|
3
|
-
*
|
|
3
|
+
* (用户层=profile 行级覆盖层 cordis.patch.yml,热生效)共用同一 schemastery schema。
|
|
4
4
|
* `secrets` 只是全局密钥的**元数据索引**(后缀/描述/创建时间),
|
|
5
5
|
* 值永远走 dsh-credentials seam($DSH_HOME/.credentials.yaml),不进本文件。
|
|
6
6
|
* @module secret-env/config
|
|
@@ -10,7 +10,7 @@ import type { UnwrapVolatile } from '@dsh-plus/shared'
|
|
|
10
10
|
|
|
11
11
|
import { SETTINGS_NS as NS_LITERAL } from './ns.ts'
|
|
12
12
|
|
|
13
|
-
/** settings
|
|
13
|
+
/** settings 命名空间(字面量即合法命名空间,编译期校验)。 */
|
|
14
14
|
export const SETTINGS_NS = NS_LITERAL
|
|
15
15
|
|
|
16
16
|
/** 一条全局密钥的元数据(值不在此处)。 */
|
|
@@ -36,7 +36,7 @@ const SecretMetaSchema: z<SecretMetaInput, SecretMeta> = z.object({
|
|
|
36
36
|
createdAt: z.string().description('创建时间 ISO').default(''),
|
|
37
37
|
})
|
|
38
38
|
|
|
39
|
-
//
|
|
39
|
+
// 全字段 `.volatile()`——条目进入 settings describe 视图(卡片可读写),
|
|
40
40
|
// loader 原位提交活动引用,索引读取现取即热。
|
|
41
41
|
export const Config = z.object({
|
|
42
42
|
secrets: z
|
|
@@ -51,7 +51,7 @@ export const Config = z.object({
|
|
|
51
51
|
.volatile(),
|
|
52
52
|
})
|
|
53
53
|
|
|
54
|
-
/** 活动字段形态(
|
|
54
|
+
/** 活动字段形态(loader 解析产物:volatile 字段为活动引用)。 */
|
|
55
55
|
export type SecretEnvConfigFields = Schemastery.TypeT<typeof Config>
|
|
56
56
|
/** 平面配置形态(消费面的读取形态,由活动引用解包得到)。 */
|
|
57
57
|
export type SecretEnvConfig = UnwrapVolatile<SecretEnvConfigFields>
|
package/src/service.ts
CHANGED
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
import { envNameOf, suffixOf } from './names.ts'
|
|
40
40
|
import { SETTINGS_NS } from './ns.ts'
|
|
41
41
|
|
|
42
|
-
/** settings
|
|
42
|
+
/** settings 服务窄面(只需 update 写入用户层;describe 读由 loader 并入 config)。 */
|
|
43
43
|
interface SettingsLike {
|
|
44
44
|
update(ns: string, patch: Record<string, unknown>): Promise<void>
|
|
45
45
|
}
|
|
@@ -69,7 +69,7 @@ export class SecretEnvService extends Service {
|
|
|
69
69
|
|
|
70
70
|
constructor(ctx: Context, config: SecretEnvConfig | SecretEnvConfigFields | undefined) {
|
|
71
71
|
super(ctx, 'secretEnv')
|
|
72
|
-
//
|
|
72
|
+
// 替代 installSection/setSource:活动引用原位提交——索引读取经
|
|
73
73
|
// readIndex 现取平面快照,用户层写入走 loader 落盘 + 原位提交热生效。
|
|
74
74
|
this.base = config !== undefined ? unwrapVolatile(config) : { secrets: [], masked: [] }
|
|
75
75
|
this.readIndex = config !== undefined ? () => unwrapVolatile(config) : undefined
|