@dsh-plus/actual 0.2.6 → 0.2.8
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 +110 -86
- package/package.json +8 -8
- package/src/client/client.ts +3 -26
- package/src/client/secrets.ts +17 -121
package/lib/client.js
CHANGED
|
@@ -208,6 +208,106 @@ window.__ModuleLoader__.load({
|
|
|
208
208
|
}, reg.component));
|
|
209
209
|
}
|
|
210
210
|
//#endregion
|
|
211
|
+
//#region ../shared/src/client/credentials.tsx
|
|
212
|
+
/**
|
|
213
|
+
* 凭据面(共享件):官方 settings-controller 的 `remote.credentials` 命名空间
|
|
214
|
+
* (describe/set/unset/onChange),复用官方 credentials seam,不另开 HTTP 端点。
|
|
215
|
+
* **值只写不读**——describe 只回「已配置 / 来源 / 可写」,没有任何方法回传值。
|
|
216
|
+
*
|
|
217
|
+
* 凭据与 settings 草稿相互独立:不进命名空间、不参与脏判定,保存即落
|
|
218
|
+
* `$DSH_HOME/.credentials.yaml` 并热生效(宿主每次工具调用重新 resolve),
|
|
219
|
+
* 因此改口令不需要保存卡片、也不需要 /reload 或重启。
|
|
220
|
+
* @module @dsh-plus/shared/client/credentials
|
|
221
|
+
*/
|
|
222
|
+
/**
|
|
223
|
+
* 把官方 `remote.credentials` 收成窄面:只补一个把宿主事件包成订阅的方法,
|
|
224
|
+
* 让状态层不必认识 `$on` 的事件名与载荷形状。
|
|
225
|
+
*/
|
|
226
|
+
function credentialsFace(remote) {
|
|
227
|
+
const { credentials } = remote;
|
|
228
|
+
return {
|
|
229
|
+
describe: (refs) => credentials.describe(refs),
|
|
230
|
+
set: (ref, value) => credentials.set(ref, value),
|
|
231
|
+
unset: (ref) => credentials.unset(ref),
|
|
232
|
+
onChange: (listener) => remote.$on("credentials/reference-updated", (ref) => {
|
|
233
|
+
if (typeof ref === "string") listener(ref);
|
|
234
|
+
})
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
/** 未答(或应答缺项)时的视图:按「未配置、可写」呈现。 */
|
|
238
|
+
const ABSENT_VIEW = {
|
|
239
|
+
configured: false,
|
|
240
|
+
writable: true
|
|
241
|
+
};
|
|
242
|
+
/** 取某引用的视图;应答缺项回落为 {@link ABSENT_VIEW}。 */
|
|
243
|
+
function viewOf(views, ref) {
|
|
244
|
+
return views[ref] ?? ABSENT_VIEW;
|
|
245
|
+
}
|
|
246
|
+
/** 应答信封的错误文案(成功为空串)。 */
|
|
247
|
+
function failureOf(result) {
|
|
248
|
+
return result.ok ? "" : result.error.message ?? "";
|
|
249
|
+
}
|
|
250
|
+
function messageOf(error) {
|
|
251
|
+
return error instanceof Error ? error.message : String(error);
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* 凭据行状态:挂载即 describe 一次,写入后重新 describe;
|
|
255
|
+
* 外部改动经 `credentials/reference-updated` 触发同一路径刷新。
|
|
256
|
+
* @param face - 官方 credentials 远程面。
|
|
257
|
+
* @param refs - 本卡片关心的引用名(以换行拼接作为稳定依赖,避免数组身份抖动)。
|
|
258
|
+
*/
|
|
259
|
+
function useCredentials$1(face, refs) {
|
|
260
|
+
const key = refs.join("\n");
|
|
261
|
+
const [views, setViews] = (0, react.useState)({});
|
|
262
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
263
|
+
const [error, setError] = (0, react.useState)("");
|
|
264
|
+
const read = (0, react.useCallback)(async () => {
|
|
265
|
+
const list = key === "" ? [] : key.split("\n");
|
|
266
|
+
const result = await face.describe(list);
|
|
267
|
+
if (!result.ok) {
|
|
268
|
+
setError(failureOf(result));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
setError("");
|
|
272
|
+
setViews(result.value);
|
|
273
|
+
}, [face, key]);
|
|
274
|
+
(0, react.useEffect)(() => {
|
|
275
|
+
read();
|
|
276
|
+
}, [read]);
|
|
277
|
+
(0, react.useEffect)(() => face.onChange((ref) => {
|
|
278
|
+
if ((key === "" ? [] : key.split("\n")).includes(ref)) read();
|
|
279
|
+
}), [
|
|
280
|
+
face,
|
|
281
|
+
key,
|
|
282
|
+
read
|
|
283
|
+
]);
|
|
284
|
+
const write = (0, react.useCallback)(async (run) => {
|
|
285
|
+
setBusy(true);
|
|
286
|
+
try {
|
|
287
|
+
const result = await run();
|
|
288
|
+
if (!result.ok) {
|
|
289
|
+
setError(failureOf(result));
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
setError("");
|
|
293
|
+
await read();
|
|
294
|
+
return true;
|
|
295
|
+
} catch (thrown) {
|
|
296
|
+
setError(messageOf(thrown));
|
|
297
|
+
return false;
|
|
298
|
+
} finally {
|
|
299
|
+
setBusy(false);
|
|
300
|
+
}
|
|
301
|
+
}, [read]);
|
|
302
|
+
return {
|
|
303
|
+
views,
|
|
304
|
+
busy,
|
|
305
|
+
error,
|
|
306
|
+
save: (0, react.useCallback)((ref, value) => write(() => face.set(ref, value)), [face, write]),
|
|
307
|
+
clear: (0, react.useCallback)((ref) => write(() => face.unset(ref)), [face, write])
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
//#endregion
|
|
211
311
|
//#region ../shared/src/client/draft.ts
|
|
212
312
|
/**
|
|
213
313
|
* 单命名空间 staged 草稿(简单配置卡片共用):读 scope 快照、播种草稿、
|
|
@@ -606,78 +706,18 @@ ${extra}
|
|
|
606
706
|
];
|
|
607
707
|
//#endregion
|
|
608
708
|
//#region src/client/secrets.ts
|
|
609
|
-
/** 未答(或应答缺项)时的视图:按「未配置、可写」呈现。 */
|
|
610
|
-
const ABSENT_VIEW = {
|
|
611
|
-
configured: false,
|
|
612
|
-
writable: true
|
|
613
|
-
};
|
|
614
|
-
/** 取某引用的视图;应答缺项回落为 {@link ABSENT_VIEW}。 */
|
|
615
|
-
function viewOf(views, ref) {
|
|
616
|
-
return views[ref] ?? ABSENT_VIEW;
|
|
617
|
-
}
|
|
618
|
-
/** 应答信封的错误文案(成功为空串)。 */
|
|
619
|
-
function failureOf(result) {
|
|
620
|
-
return result.ok ? "" : result.error.message ?? "";
|
|
621
|
-
}
|
|
622
|
-
/** 提取异常文本。 */
|
|
623
|
-
function messageOf(error) {
|
|
624
|
-
return error instanceof Error ? error.message : String(error);
|
|
625
|
-
}
|
|
626
709
|
/**
|
|
627
|
-
*
|
|
628
|
-
*
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
}
|
|
635
|
-
/**
|
|
636
|
-
* 凭据行状态:挂载即 describe 一次,写入后重新 describe;
|
|
637
|
-
* 外部改动经 `credentials/reference-updated` 触发同一路径刷新。
|
|
710
|
+
* 浏览器半的凭据面:复用 `@dsh-plus/shared/client` 的凭据 hook(describe/set/unset,
|
|
711
|
+
* 支持多引用),此处只绑定本插件关心的引用清单。
|
|
712
|
+
*
|
|
713
|
+
* **值只写不读**——describe 只回「已配置 / 来源 / 可写」,没有任何方法回传值。
|
|
714
|
+
* 凭据与 settings 草稿相互独立:它不进命名空间、不参与脏判定,保存即落
|
|
715
|
+
* `$DSH_HOME/.credentials.yaml` 并热生效(宿主每次工具调用重新 resolve)。
|
|
716
|
+
* @module @dsh-plus/actual/client/secrets
|
|
638
717
|
*/
|
|
718
|
+
/** 本插件的凭据行状态(引用清单固定为 {@link CREDENTIAL_REFS})。 */
|
|
639
719
|
function useCredentials(face) {
|
|
640
|
-
|
|
641
|
-
const [busy, setBusy] = (0, react.useState)(false);
|
|
642
|
-
const [error, setError] = (0, react.useState)("");
|
|
643
|
-
const read = (0, react.useCallback)(async () => {
|
|
644
|
-
const result = await face.describe([...CREDENTIAL_REFS]);
|
|
645
|
-
if (!result.ok) {
|
|
646
|
-
setError(failureOf(result));
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
|
-
setError("");
|
|
650
|
-
setViews(result.value);
|
|
651
|
-
}, [face]);
|
|
652
|
-
(0, react.useEffect)(() => {
|
|
653
|
-
read();
|
|
654
|
-
}, [read]);
|
|
655
|
-
useCredentialChanges(face, read);
|
|
656
|
-
const write = (0, react.useCallback)(async (run) => {
|
|
657
|
-
setBusy(true);
|
|
658
|
-
try {
|
|
659
|
-
const result = await run();
|
|
660
|
-
if (!result.ok) {
|
|
661
|
-
setError(failureOf(result));
|
|
662
|
-
return false;
|
|
663
|
-
}
|
|
664
|
-
setError("");
|
|
665
|
-
await read();
|
|
666
|
-
return true;
|
|
667
|
-
} catch (thrown) {
|
|
668
|
-
setError(messageOf(thrown));
|
|
669
|
-
return false;
|
|
670
|
-
} finally {
|
|
671
|
-
setBusy(false);
|
|
672
|
-
}
|
|
673
|
-
}, [read]);
|
|
674
|
-
return {
|
|
675
|
-
views,
|
|
676
|
-
busy,
|
|
677
|
-
error,
|
|
678
|
-
save: (0, react.useCallback)((ref, value) => write(() => face.set(ref, value)), [face, write]),
|
|
679
|
-
clear: (0, react.useCallback)((ref) => write(() => face.unset(ref)), [face, write])
|
|
680
|
-
};
|
|
720
|
+
return useCredentials$1(face, CREDENTIAL_REFS);
|
|
681
721
|
}
|
|
682
722
|
//#endregion
|
|
683
723
|
//#region src/client/secrets-section.tsx
|
|
@@ -1132,22 +1172,6 @@ ${extra}
|
|
|
1132
1172
|
"remote.settings",
|
|
1133
1173
|
"remote.credentials"
|
|
1134
1174
|
];
|
|
1135
|
-
/**
|
|
1136
|
-
* 把官方 `remote.credentials` 收成窄面:只补一个把宿主事件包成订阅的方法,
|
|
1137
|
-
* 让状态层不必认识 `$on` 的事件名与载荷形状。
|
|
1138
|
-
*/
|
|
1139
|
-
function credentialsFace(c) {
|
|
1140
|
-
const remote = c.get("remote");
|
|
1141
|
-
const { credentials } = remote;
|
|
1142
|
-
return {
|
|
1143
|
-
describe: (refs) => credentials.describe(refs),
|
|
1144
|
-
set: (ref, value) => credentials.set(ref, value),
|
|
1145
|
-
unset: (ref) => credentials.unset(ref),
|
|
1146
|
-
onChange: (listener) => remote.$on("credentials/reference-updated", (ref) => {
|
|
1147
|
-
if (typeof ref === "string") listener(ref);
|
|
1148
|
-
})
|
|
1149
|
-
};
|
|
1150
|
-
}
|
|
1151
1175
|
function apply(ctx) {
|
|
1152
1176
|
const c = ctx;
|
|
1153
1177
|
const tag = injectStyle();
|
|
@@ -1160,7 +1184,7 @@ ${extra}
|
|
|
1160
1184
|
}), "actual: locale");
|
|
1161
1185
|
const scope = createSettingsScope(c, SETTINGS_NS, "actual: settings scope");
|
|
1162
1186
|
const api = createNamespaceApi(c.get("remote").settings, SETTINGS_NS);
|
|
1163
|
-
const credentials = credentialsFace(c);
|
|
1187
|
+
const credentials = credentialsFace(c.get("remote"));
|
|
1164
1188
|
injectPluginConfigCard(c.slots, {
|
|
1165
1189
|
ns: SETTINGS_NS,
|
|
1166
1190
|
rowId: "dsh-plus-actual",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-plus/actual",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "dsh-plus service + UI plugin: Actual Budget 能力发现(@actual-app/cli help 树运行时派生 × 进程内 MCP tools/list)、actual agent 预设注册与配置卡片——零手工工具清单",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -33,17 +33,17 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@modelcontextprotocol/client": "2.3.0",
|
|
35
35
|
"@dsh-plus/actual-mcp": "0.2.0",
|
|
36
|
-
"@dsh-plus/shared": "0.1.
|
|
36
|
+
"@dsh-plus/shared": "0.1.43"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
|
-
"@deepseek-ai/cordis": "^4.0.5
|
|
40
|
-
"@deepseek-ai/dsh-agent-preset-registry": "^0.2.1-
|
|
41
|
-
"@deepseek-ai/schemastery": "^3.18.5
|
|
39
|
+
"@deepseek-ai/cordis": "^4.0.5",
|
|
40
|
+
"@deepseek-ai/dsh-agent-preset-registry": "^0.2.1-rc.2",
|
|
41
|
+
"@deepseek-ai/schemastery": "^3.18.5"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@deepseek-ai/cordis": "4.0.5
|
|
45
|
-
"@deepseek-ai/dsh-agent-preset-registry": "0.2.1-
|
|
46
|
-
"@deepseek-ai/schemastery": "3.18.5
|
|
44
|
+
"@deepseek-ai/cordis": "4.0.5",
|
|
45
|
+
"@deepseek-ai/dsh-agent-preset-registry": "0.2.1-rc.2",
|
|
46
|
+
"@deepseek-ai/schemastery": "3.18.5",
|
|
47
47
|
"@types/react": "~18.3.31",
|
|
48
48
|
"react": "^18.2.0"
|
|
49
49
|
},
|
package/src/client/client.ts
CHANGED
|
@@ -12,15 +12,15 @@ import type { Context } from '@deepseek-ai/cordis'
|
|
|
12
12
|
import {
|
|
13
13
|
createNamespaceApi,
|
|
14
14
|
createSettingsScope,
|
|
15
|
+
credentialsFace,
|
|
15
16
|
injectPluginConfigCard,
|
|
16
17
|
type PluginClientContext,
|
|
17
|
-
type
|
|
18
|
+
type RemoteWithCredentials,
|
|
18
19
|
} from '@dsh-plus/shared/client'
|
|
19
20
|
|
|
20
21
|
import { SETTINGS_NS } from '../ns.ts'
|
|
21
22
|
import { ActualCard } from './card.tsx'
|
|
22
23
|
import { type DictKey, en, NS, zh } from './i18n.ts'
|
|
23
|
-
import type { CredentialsRemoteFace } from './secrets.ts'
|
|
24
24
|
import { injectStyle } from './styles.ts'
|
|
25
25
|
|
|
26
26
|
export const name = 'dsh-plus-actual'
|
|
@@ -41,29 +41,6 @@ export const inject = [
|
|
|
41
41
|
/** 宿主窄面收编在 @dsh-plus/shared/client;TKey 传入本包 DictKey 使键名受检。 */
|
|
42
42
|
type ClientContext = PluginClientContext<DictKey>
|
|
43
43
|
|
|
44
|
-
/** `remote` 服务加上凭据命名空间(inject 已声明,故按存在处理)。 */
|
|
45
|
-
type RemoteWithCredentials = RemoteLike & {
|
|
46
|
-
credentials: Omit<CredentialsRemoteFace, 'onChange'>
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* 把官方 `remote.credentials` 收成窄面:只补一个把宿主事件包成订阅的方法,
|
|
51
|
-
* 让状态层不必认识 `$on` 的事件名与载荷形状。
|
|
52
|
-
*/
|
|
53
|
-
function credentialsFace(c: ClientContext): CredentialsRemoteFace {
|
|
54
|
-
const remote = c.get('remote') as RemoteWithCredentials
|
|
55
|
-
const { credentials } = remote
|
|
56
|
-
return {
|
|
57
|
-
describe: (refs) => credentials.describe(refs),
|
|
58
|
-
set: (ref, value) => credentials.set(ref, value),
|
|
59
|
-
unset: (ref) => credentials.unset(ref),
|
|
60
|
-
onChange: (listener) =>
|
|
61
|
-
remote.$on('credentials/reference-updated', (ref) => {
|
|
62
|
-
if (typeof ref === 'string') listener(ref)
|
|
63
|
-
}),
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
44
|
export function apply(ctx: Context): void {
|
|
68
45
|
const c = ctx as unknown as ClientContext
|
|
69
46
|
const tag = injectStyle()
|
|
@@ -76,7 +53,7 @@ export function apply(ctx: Context): void {
|
|
|
76
53
|
c.effect(() => c.locale.register(NS, { zh, en }), 'actual: locale')
|
|
77
54
|
const scope = createSettingsScope(c, SETTINGS_NS, 'actual: settings scope')
|
|
78
55
|
const api = createNamespaceApi(c.get('remote').settings, SETTINGS_NS)
|
|
79
|
-
const credentials = credentialsFace(c)
|
|
56
|
+
const credentials = credentialsFace(c.get('remote') as RemoteWithCredentials)
|
|
80
57
|
injectPluginConfigCard(c.slots, {
|
|
81
58
|
ns: SETTINGS_NS,
|
|
82
59
|
rowId: 'dsh-plus-actual',
|
package/src/client/secrets.ts
CHANGED
|
@@ -1,132 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* **值只写不读**——describe 只回「已配置 / 来源 / 可写」,没有任何方法回传值。
|
|
2
|
+
* 浏览器半的凭据面:复用 `@dsh-plus/shared/client` 的凭据 hook(describe/set/unset,
|
|
3
|
+
* 支持多引用),此处只绑定本插件关心的引用清单。
|
|
5
4
|
*
|
|
5
|
+
* **值只写不读**——describe 只回「已配置 / 来源 / 可写」,没有任何方法回传值。
|
|
6
6
|
* 凭据与 settings 草稿相互独立:它不进命名空间、不参与脏判定,保存即落
|
|
7
|
-
* `$DSH_HOME/.credentials.yaml` 并热生效(宿主每次工具调用重新 resolve
|
|
8
|
-
* 因此改口令不需要保存卡片、也不需要 /reload 或重启。
|
|
7
|
+
* `$DSH_HOME/.credentials.yaml` 并热生效(宿主每次工具调用重新 resolve)。
|
|
9
8
|
* @module @dsh-plus/actual/client/secrets
|
|
10
9
|
*/
|
|
11
|
-
import
|
|
12
|
-
|
|
10
|
+
import {
|
|
11
|
+
type CredentialsRemoteFace,
|
|
12
|
+
type CredentialsState,
|
|
13
|
+
useCredentials as useSharedCredentials,
|
|
14
|
+
} from '@dsh-plus/shared/client'
|
|
13
15
|
|
|
14
16
|
import { CREDENTIAL_REFS } from '../refs.ts'
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
18
|
+
export type {
|
|
19
|
+
CredentialsRemoteFace,
|
|
20
|
+
CredentialsState,
|
|
21
|
+
CredentialView,
|
|
22
|
+
} from '@dsh-plus/shared/client'
|
|
23
|
+
export { failureOf, viewOf } from '@dsh-plus/shared/client'
|
|
22
24
|
|
|
23
|
-
/**
|
|
24
|
-
export interface CredentialsRemoteFace {
|
|
25
|
-
describe(refs: readonly string[]): Promise<RemoteResult<Record<string, CredentialView>>>
|
|
26
|
-
set(ref: string, value: string): Promise<RemoteResult<unknown>>
|
|
27
|
-
unset(ref: string): Promise<RemoteResult<unknown>>
|
|
28
|
-
/** 订阅引用变更(宿主提交写入或观察到凭据文件被外部编辑时触发)。 */
|
|
29
|
-
onChange(listener: (ref: string) => void): () => void
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** 未答(或应答缺项)时的视图:按「未配置、可写」呈现。 */
|
|
33
|
-
export const ABSENT_VIEW: CredentialView = { configured: false, writable: true }
|
|
34
|
-
|
|
35
|
-
/** 取某引用的视图;应答缺项回落为 {@link ABSENT_VIEW}。 */
|
|
36
|
-
export function viewOf(views: Record<string, CredentialView>, ref: string): CredentialView {
|
|
37
|
-
return views[ref] ?? ABSENT_VIEW
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** 应答信封的错误文案(成功为空串)。 */
|
|
41
|
-
export function failureOf(result: RemoteResult<unknown>): string {
|
|
42
|
-
return result.ok ? '' : (result.error.message ?? '')
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** 提取异常文本。 */
|
|
46
|
-
function messageOf(error: unknown): string {
|
|
47
|
-
return error instanceof Error ? error.message : String(error)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** 凭据读写的卡片状态。 */
|
|
51
|
-
export interface CredentialsState {
|
|
52
|
-
views: Record<string, CredentialView>
|
|
53
|
-
/** 有写入在途(按钮禁用用)。 */
|
|
54
|
-
busy: boolean
|
|
55
|
-
/** 最近一次失败文案(空串 = 无)。 */
|
|
56
|
-
error: string
|
|
57
|
-
/** 写入成功返回 true(调用方据此决定是否清空输入框)。 */
|
|
58
|
-
save(ref: string, value: string): Promise<boolean>
|
|
59
|
-
clear(ref: string): Promise<boolean>
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* 订阅宿主凭据变更:只认本插件关心的引用,其余(别的插件写自己的密钥)
|
|
64
|
-
* 不触发重读。
|
|
65
|
-
*/
|
|
66
|
-
function useCredentialChanges(face: CredentialsRemoteFace, read: () => void): void {
|
|
67
|
-
useEffect(
|
|
68
|
-
() =>
|
|
69
|
-
face.onChange((ref) => {
|
|
70
|
-
if ((CREDENTIAL_REFS as readonly string[]).includes(ref)) read()
|
|
71
|
-
}),
|
|
72
|
-
[face, read],
|
|
73
|
-
)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* 凭据行状态:挂载即 describe 一次,写入后重新 describe;
|
|
78
|
-
* 外部改动经 `credentials/reference-updated` 触发同一路径刷新。
|
|
79
|
-
*/
|
|
25
|
+
/** 本插件的凭据行状态(引用清单固定为 {@link CREDENTIAL_REFS})。 */
|
|
80
26
|
export function useCredentials(face: CredentialsRemoteFace): CredentialsState {
|
|
81
|
-
|
|
82
|
-
const [busy, setBusy] = useState(false)
|
|
83
|
-
const [error, setError] = useState('')
|
|
84
|
-
|
|
85
|
-
const read = useCallback(async (): Promise<void> => {
|
|
86
|
-
const result = await face.describe([...CREDENTIAL_REFS])
|
|
87
|
-
if (!result.ok) {
|
|
88
|
-
setError(failureOf(result))
|
|
89
|
-
return
|
|
90
|
-
}
|
|
91
|
-
setError('')
|
|
92
|
-
setViews(result.value)
|
|
93
|
-
}, [face])
|
|
94
|
-
|
|
95
|
-
useEffect(() => {
|
|
96
|
-
void read()
|
|
97
|
-
}, [read])
|
|
98
|
-
useCredentialChanges(face, read)
|
|
99
|
-
|
|
100
|
-
const write = useCallback(
|
|
101
|
-
async (run: () => Promise<RemoteResult<unknown>>): Promise<boolean> => {
|
|
102
|
-
setBusy(true)
|
|
103
|
-
try {
|
|
104
|
-
const result = await run()
|
|
105
|
-
if (!result.ok) {
|
|
106
|
-
setError(failureOf(result))
|
|
107
|
-
return false
|
|
108
|
-
}
|
|
109
|
-
setError('')
|
|
110
|
-
await read()
|
|
111
|
-
return true
|
|
112
|
-
} catch (thrown: unknown) {
|
|
113
|
-
setError(messageOf(thrown))
|
|
114
|
-
return false
|
|
115
|
-
} finally {
|
|
116
|
-
setBusy(false)
|
|
117
|
-
}
|
|
118
|
-
},
|
|
119
|
-
[read],
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
const save = useCallback(
|
|
123
|
-
(ref: string, value: string): Promise<boolean> => write(() => face.set(ref, value)),
|
|
124
|
-
[face, write],
|
|
125
|
-
)
|
|
126
|
-
const clear = useCallback(
|
|
127
|
-
(ref: string): Promise<boolean> => write(() => face.unset(ref)),
|
|
128
|
-
[face, write],
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
return { views, busy, error, save, clear }
|
|
27
|
+
return useSharedCredentials(face, CREDENTIAL_REFS)
|
|
132
28
|
}
|