@dsh-plus/actual 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -7,6 +7,110 @@ window.__ModuleLoader__.load({
7
7
  let react_jsx_runtime = require("react/jsx-runtime");
8
8
  let react = require("react");
9
9
  require("@deepseek-ai/dsh-client-ui-primitives");
10
+ //#region ../shared/src/client/fields.tsx
11
+ /** 卡片按钮(与 CardChrome 页脚同一套类名与视觉)。 */
12
+ function Button(props) {
13
+ const p = props.prefix ?? "dshp";
14
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
15
+ type: "button",
16
+ className: `${p}-btn ${props.variant === "primary" ? `${p}-btnPrimary` : `${p}-btnGhost`}`,
17
+ disabled: props.disabled === true,
18
+ onClick: props.onClick,
19
+ children: props.label
20
+ });
21
+ }
22
+ function TextField(props) {
23
+ const p = props.prefix ?? "dshp";
24
+ const invalid = props.invalid === true;
25
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
26
+ className: `${p}-field`,
27
+ children: [
28
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29
+ className: `${p}-head`,
30
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
31
+ className: `${p}-label`,
32
+ htmlFor: props.id,
33
+ children: props.label
34
+ }), props.badge !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35
+ className: `${p}-badge ${props.badge.set ? `${p}-badgeSet` : `${p}-badgeUnset`}`,
36
+ children: props.badge.text
37
+ }) : null]
38
+ }),
39
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
40
+ id: props.id,
41
+ className: `${p}-input${invalid ? ` ${p}-inputInvalid` : ""}`,
42
+ type: props.password === true ? "password" : "text",
43
+ inputMode: props.numeric === true ? "numeric" : void 0,
44
+ autoComplete: props.password === true ? "off" : void 0,
45
+ "aria-invalid": invalid || void 0,
46
+ value: props.value,
47
+ disabled: props.disabled === true,
48
+ onChange: (event) => props.onEdit(event.target.value)
49
+ }),
50
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
51
+ className: invalid ? `${p}-invalid` : `${p}-hint`,
52
+ children: invalid ? props.invalidLabel ?? "" : props.hint ?? ""
53
+ })
54
+ ]
55
+ });
56
+ }
57
+ /**
58
+ * 勾选行:自绘圆角扁平开关(switch),对齐官方 WebUI 无原生 checkbox 的
59
+ * 设计语言(官方选择类控件全走胶囊/菜单,见 cardCss 的 -switch 规则)。
60
+ * 语义保持 checkbox(无障碍树与表单语义不变),视觉自绘。
61
+ */
62
+ function CheckRow(props) {
63
+ const p = props.prefix ?? "dshp";
64
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
65
+ className: `${p}-checkRow`,
66
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
67
+ id: props.id,
68
+ type: "checkbox",
69
+ role: "switch",
70
+ "aria-checked": props.checked,
71
+ checked: props.checked,
72
+ disabled: props.disabled === true,
73
+ onChange: (event) => props.onEdit(event.target.checked)
74
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
75
+ htmlFor: props.id,
76
+ children: props.label
77
+ })]
78
+ });
79
+ }
80
+ function SelectField(props) {
81
+ const p = props.prefix ?? "dshp";
82
+ const invalid = props.invalid === true;
83
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
84
+ className: `${p}-field`,
85
+ children: [
86
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
87
+ className: `${p}-head`,
88
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
89
+ className: `${p}-label`,
90
+ htmlFor: props.id,
91
+ children: props.label
92
+ })
93
+ }),
94
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
95
+ id: props.id,
96
+ className: `${p}-select${invalid ? ` ${p}-inputInvalid` : ""}`,
97
+ "aria-invalid": invalid || void 0,
98
+ value: props.value,
99
+ disabled: props.disabled === true,
100
+ onChange: (event) => props.onEdit(event.target.value),
101
+ children: props.options.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
102
+ value: option.value,
103
+ children: option.label
104
+ }, option.value))
105
+ }),
106
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
107
+ className: invalid ? `${p}-invalid` : `${p}-hint`,
108
+ children: invalid ? props.invalidLabel ?? "" : props.hint ?? ""
109
+ })
110
+ ]
111
+ });
112
+ }
113
+ //#endregion
10
114
  //#region ../shared/src/client/card.tsx
11
115
  const IDLE_STATUS = {
12
116
  kind: "idle",
@@ -55,12 +159,12 @@ window.__ModuleLoader__.load({
55
159
  className: `${p}-status${props.status.kind === "error" ? ` ${p}-statusError` : ""}`,
56
160
  role: "status",
57
161
  children: props.status.text
58
- }) : null, props.actions.map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
59
- type: "button",
60
- className: `${p}-btn ${action.variant === "primary" ? `${p}-btnPrimary` : `${p}-btnGhost`}`,
61
- disabled: action.disabled === true,
62
- onClick: action.onClick,
63
- children: action.label
162
+ }) : null, props.actions.map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
163
+ prefix: p,
164
+ label: action.label,
165
+ variant: action.variant,
166
+ disabled: action.disabled,
167
+ onClick: action.onClick
64
168
  }, action.key))]
65
169
  })
66
170
  ]
@@ -181,99 +285,6 @@ window.__ModuleLoader__.load({
181
285
  };
182
286
  }
183
287
  //#endregion
184
- //#region ../shared/src/client/fields.tsx
185
- function TextField(props) {
186
- const p = props.prefix ?? "dshp";
187
- const invalid = props.invalid === true;
188
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
189
- className: `${p}-field`,
190
- children: [
191
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
192
- className: `${p}-head`,
193
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
194
- className: `${p}-label`,
195
- htmlFor: props.id,
196
- children: props.label
197
- }), props.badge !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
198
- className: `${p}-badge ${props.badge.set ? `${p}-badgeSet` : `${p}-badgeUnset`}`,
199
- children: props.badge.text
200
- }) : null]
201
- }),
202
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
203
- id: props.id,
204
- className: `${p}-input${invalid ? ` ${p}-inputInvalid` : ""}`,
205
- type: props.password === true ? "password" : "text",
206
- inputMode: props.numeric === true ? "numeric" : void 0,
207
- autoComplete: props.password === true ? "off" : void 0,
208
- "aria-invalid": invalid || void 0,
209
- value: props.value,
210
- disabled: props.disabled === true,
211
- onChange: (event) => props.onEdit(event.target.value)
212
- }),
213
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
214
- className: invalid ? `${p}-invalid` : `${p}-hint`,
215
- children: invalid ? props.invalidLabel ?? "" : props.hint ?? ""
216
- })
217
- ]
218
- });
219
- }
220
- /**
221
- * 勾选行:自绘圆角扁平开关(switch),对齐官方 WebUI 无原生 checkbox 的
222
- * 设计语言(官方选择类控件全走胶囊/菜单,见 cardCss 的 -switch 规则)。
223
- * 语义保持 checkbox(无障碍树与表单语义不变),视觉自绘。
224
- */
225
- function CheckRow(props) {
226
- const p = props.prefix ?? "dshp";
227
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
228
- className: `${p}-checkRow`,
229
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
230
- id: props.id,
231
- type: "checkbox",
232
- role: "switch",
233
- "aria-checked": props.checked,
234
- checked: props.checked,
235
- disabled: props.disabled === true,
236
- onChange: (event) => props.onEdit(event.target.checked)
237
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
238
- htmlFor: props.id,
239
- children: props.label
240
- })]
241
- });
242
- }
243
- function SelectField(props) {
244
- const p = props.prefix ?? "dshp";
245
- const invalid = props.invalid === true;
246
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
247
- className: `${p}-field`,
248
- children: [
249
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
250
- className: `${p}-head`,
251
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
252
- className: `${p}-label`,
253
- htmlFor: props.id,
254
- children: props.label
255
- })
256
- }),
257
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
258
- id: props.id,
259
- className: `${p}-select${invalid ? ` ${p}-inputInvalid` : ""}`,
260
- "aria-invalid": invalid || void 0,
261
- value: props.value,
262
- disabled: props.disabled === true,
263
- onChange: (event) => props.onEdit(event.target.value),
264
- children: props.options.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
265
- value: option.value,
266
- children: option.label
267
- }, option.value))
268
- }),
269
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
270
- className: invalid ? `${p}-invalid` : `${p}-hint`,
271
- children: invalid ? props.invalidLabel ?? "" : props.hint ?? ""
272
- })
273
- ]
274
- });
275
- }
276
- //#endregion
277
288
  //#region ../shared/src/client/i18n.ts
278
289
  /** 卡片通用文案(save/discard/unsaved/loading 等四插件共有键)。 */
279
290
  const commonZh = {
@@ -567,16 +578,235 @@ ${extra}
567
578
  };
568
579
  }
569
580
  //#endregion
581
+ //#region src/refs.ts
582
+ /**
583
+ * 凭据引用名字面量(纯常量,零依赖)。
584
+ *
585
+ * 宿主半(credentials.ts 的 seam 解析)与浏览器半(配置卡片的
586
+ * `remote.credentials` 读写)共用同一份取值;浏览器包不得经此路径拖入
587
+ * `@dsh-plus/actual-mcp` 的 node 侧实现(child_process 等),故字面量独立成模块。
588
+ *
589
+ * **必须与 `@dsh-plus/actual-mcp` 的 `SECRET_ENV` 逐字一致**:引用名与官方 CLI
590
+ * 原生读取的环境变量名同形(`ACTUAL_` 前缀亦符合《插件存储规范》的
591
+ * `<PLUGIN_ID>_` 大写约定),凭据文件里的一处配置才能同时被 seam 解析、
592
+ * 又被 CLI 自身继承环境读到。该一致性由 `tests/refs.test.ts` 钉住。
593
+ * @module @dsh-plus/actual/refs
594
+ */
595
+ /** 服务器口令。 */
596
+ const REF_PASSWORD = "ACTUAL_PASSWORD";
597
+ /** 会话令牌(官方 CLI 优先于口令)。 */
598
+ const REF_SESSION_TOKEN = "ACTUAL_SESSION_TOKEN";
599
+ /** 端到端加密口令(仅加密预算需要)。 */
600
+ const REF_ENCRYPTION_PASSWORD = "ACTUAL_ENCRYPTION_PASSWORD";
601
+ /** 卡片展示与批量 describe 使用的引用清单(顺序即展示顺序)。 */
602
+ const CREDENTIAL_REFS = [
603
+ REF_PASSWORD,
604
+ REF_SESSION_TOKEN,
605
+ REF_ENCRYPTION_PASSWORD
606
+ ];
607
+ //#endregion
608
+ //#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
+ /**
627
+ * 订阅宿主凭据变更:只认本插件关心的引用,其余(别的插件写自己的密钥)
628
+ * 不触发重读。
629
+ */
630
+ function useCredentialChanges(face, read) {
631
+ (0, react.useEffect)(() => face.onChange((ref) => {
632
+ if (CREDENTIAL_REFS.includes(ref)) read();
633
+ }), [face, read]);
634
+ }
635
+ /**
636
+ * 凭据行状态:挂载即 describe 一次,写入后重新 describe;
637
+ * 外部改动经 `credentials/reference-updated` 触发同一路径刷新。
638
+ */
639
+ function useCredentials(face) {
640
+ const [views, setViews] = (0, react.useState)({});
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
+ };
681
+ }
682
+ //#endregion
683
+ //#region src/client/secrets-section.tsx
684
+ /**
685
+ * 「凭据」小节:三行密钥(服务器口令 / 会话令牌 / 端到端加密口令),
686
+ * 每行可写可清,右侧徽标显示「已配置 / 未配置」。
687
+ *
688
+ * 行内只做呈现与暂存:值一经保存即从输入框清空,不回显、不驻留 DOM;
689
+ * 引用被进程环境遮蔽(`writable:false`)时禁用编辑并说明原因——这正是
690
+ * 部署方把口令放进 systemd/shell 环境的场景,必须让用户看懂为什么改不了。
691
+ * @module @dsh-plus/actual/client/secrets-section
692
+ */
693
+ /** 三行密钥的固定顺序(口令 → 令牌 → 加密口令)。 */
694
+ const SECRET_ROWS = [
695
+ {
696
+ ref: REF_PASSWORD,
697
+ labelKey: "secretPassword",
698
+ hintKey: "secretPasswordHint"
699
+ },
700
+ {
701
+ ref: REF_SESSION_TOKEN,
702
+ labelKey: "secretToken",
703
+ hintKey: "secretTokenHint"
704
+ },
705
+ {
706
+ ref: REF_ENCRYPTION_PASSWORD,
707
+ labelKey: "secretEncryption",
708
+ hintKey: "secretEncryptionHint"
709
+ }
710
+ ];
711
+ /** 单行密钥:输入框 + 保存/清除,保存成功后清空输入。 */
712
+ function SecretRow(props) {
713
+ const { t, row, view, busy } = props;
714
+ const [text, setText] = (0, react.useState)("");
715
+ const locked = busy || !view.writable;
716
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
717
+ className: "act-secret",
718
+ children: [
719
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
720
+ prefix: "act",
721
+ id: `act-secret-${row.ref}`,
722
+ label: t(row.labelKey),
723
+ hint: t(row.hintKey),
724
+ password: true,
725
+ value: text,
726
+ disabled: locked,
727
+ badge: {
728
+ text: t(view.configured ? "secretSet" : "secretUnset"),
729
+ set: view.configured
730
+ },
731
+ onEdit: setText
732
+ }),
733
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
734
+ className: "act-secretActions",
735
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
736
+ prefix: "act",
737
+ variant: "primary",
738
+ label: t("save"),
739
+ disabled: locked || text === "",
740
+ onClick: () => {
741
+ props.onSave(row.ref, text).then((ok) => {
742
+ if (ok) setText("");
743
+ });
744
+ }
745
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
746
+ prefix: "act",
747
+ variant: "ghost",
748
+ label: t("secretClear"),
749
+ disabled: locked || !view.configured,
750
+ onClick: () => {
751
+ props.onClear(row.ref);
752
+ }
753
+ })]
754
+ }),
755
+ view.writable ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
756
+ className: "act-warn",
757
+ children: t("secretShadowed")
758
+ })
759
+ ]
760
+ });
761
+ }
762
+ /** 凭据小节(读写经官方 `remote.credentials`)。 */
763
+ function SecretsSection(props) {
764
+ const { t, face } = props;
765
+ const { views, busy, error, save, clear } = useCredentials(face);
766
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
767
+ className: "act-section",
768
+ children: [
769
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
770
+ className: "act-sectionHead",
771
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
772
+ className: "act-sectionTitle",
773
+ children: t("secretsTitle")
774
+ })
775
+ }),
776
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
777
+ className: "act-sectionDesc",
778
+ children: t("secretsBody")
779
+ }),
780
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
781
+ className: "act-sectionBody",
782
+ children: SECRET_ROWS.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SecretRow, {
783
+ t,
784
+ row,
785
+ view: viewOf(views, row.ref),
786
+ busy,
787
+ onSave: save,
788
+ onClear: clear
789
+ }, row.ref))
790
+ }),
791
+ error === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
792
+ className: "act-warn",
793
+ children: [t("secretFailed"), error]
794
+ })
795
+ ]
796
+ });
797
+ }
798
+ //#endregion
570
799
  //#region src/client/card.tsx
571
800
  /**
572
801
  * 「Actual Budget 连接」配置卡片:经 injectPluginConfigCard 注册(插件页
573
802
  * plugins.row.config / plugins.bundle.config,view 分发 summary/page)。
574
803
  *
575
- * 编辑范围只覆盖**非密钥**字段。密钥与高级字段没有编辑器,卡片内给出明确落点
576
- * (见 i18n 的 secretsBody / advancedBody)——按《插件开发指南》,可配置项要么
577
- * 有编辑器、要么写明为什么没有与改在哪里配。
804
+ * 编辑范围覆盖全部可公开字段与三行密钥:非密钥字段走 settings 草稿(脏判定 +
805
+ * 保存按钮),密钥走官方 credentials seam(各自即时保存,与草稿互不干扰)。
806
+ * 剩余无编辑器的字段在卡片内给出明确落点(见 i18n 的 advancedBody)——按
807
+ * 《插件开发指南》,可配置项要么有编辑器、要么写明为什么没有与改在哪里配。
578
808
  *
579
- * 字段按主题拆成两个小节组件,卡片本体只负责外壳、状态与保存动作。
809
+ * 字段按主题拆成小节组件,卡片本体只负责外壳、状态与保存动作。
580
810
  * @module @dsh-plus/actual/client/card
581
811
  */
582
812
  /** 模块级稳定函数:参与 useNamespaceDraft 的依赖比较。 */
@@ -600,24 +830,13 @@ ${extra}
600
830
  function Note(props) {
601
831
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
602
832
  className: "act-note",
603
- children: [
604
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
605
- className: "act-noteTitle",
606
- children: props.title
607
- }),
608
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
609
- className: "act-noteBody",
610
- children: props.body
611
- }),
612
- props.snippet !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
613
- className: "act-snippet",
614
- children: props.snippet
615
- }) : null,
616
- props.tail !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
617
- className: "act-noteBody",
618
- children: props.tail
619
- }) : null
620
- ]
833
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
834
+ className: "act-noteTitle",
835
+ children: props.title
836
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
837
+ className: "act-noteBody",
838
+ children: props.body
839
+ })]
621
840
  });
622
841
  }
623
842
  /** 连接小节:服务器地址、预算、CLI 命令与版本策略。 */
@@ -712,7 +931,7 @@ ${extra}
712
931
  ] });
713
932
  }
714
933
  function ActualCard(props) {
715
- const { t, scope, api } = props;
934
+ const { t, scope, api, credentials } = props;
716
935
  const { draft, dirty, saving, status, disabled, edit, reset, save } = useNamespaceDraft({
717
936
  scope,
718
937
  api,
@@ -771,13 +990,11 @@ ${extra}
771
990
  children: t("enabledHint")
772
991
  }),
773
992
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConnectionSection, { ...section }),
774
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PolicySection, { ...section }),
775
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Note, {
776
- title: t("secretsTitle"),
777
- body: t("secretsBody"),
778
- snippet: t("secretsSnippet"),
779
- tail: t("secretsTail")
993
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SecretsSection, {
994
+ t,
995
+ face: credentials
780
996
  }),
997
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PolicySection, { ...section }),
781
998
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Note, {
782
999
  title: t("advancedTitle"),
783
1000
  body: t("advancedBody")
@@ -806,7 +1023,7 @@ ${extra}
806
1023
  serverUrlHint: "自托管默认 http://127.0.0.1:5006。服务端版本从 <地址>/info 读取,用于版本一致性告警。",
807
1024
  serverUrlInvalid: "必须是 http(s):// 开头的完整地址",
808
1025
  syncId: "预算 Sync ID",
809
- syncIdHint: "Actual 客户端 → 设置 → 显示高级设置 → Sync ID。留空则读环境变量 ACTUAL_SYNC_ID。多数命令都需要它。",
1026
+ syncIdHint: "Actual 客户端 → 设置 → 显示高级设置 → Sync ID,即 budgets list 里的 groupId;不要填同一条目里的 cloudFileId(填错会报 Budget not found)。留空则读环境变量 ACTUAL_SYNC_ID。",
810
1027
  cliCommand: "CLI 命令",
811
1028
  cliCommandHint: "留空 = 自动探测(ACTUAL_CLI 环境变量 → PATH 上的 actual → 包内 @actual-app/cli)。需要指定具体副本时填完整 argv,如 node /opt/actual/dist/cli.js(空格分隔,不支持引号)。",
812
1029
  cliCommandInvalid: "不支持引号:请直接以空格分隔各个参数",
@@ -821,10 +1038,19 @@ ${extra}
821
1038
  namePrefix: "工具名前缀",
822
1039
  namePrefixHint: "模型可见的工具名 = 前缀 + 命令族名(如 actual_accounts)。留空则直接用裸族名——query、server 等过于通用,不建议。",
823
1040
  namePrefixInvalid: "只允许字母、数字、下划线与连字符",
824
- secretsTitle: "服务器口令",
825
- secretsBody: "口令属密钥:它不进用户层配置,卡片也不提供输入框(写进来会被脱敏遮蔽,等于白填)。请在 profile 的 cordis.patch.yml 里做行级注入,口令本身放环境变量:",
826
- secretsSnippet: "- id: dsh-plus-actual\n config:\n syncId: !!js process.env.ACTUAL_SYNC_ID\n password: !!js process.env.ACTUAL_PASSWORD",
827
- secretsTail: "把 ACTUAL_PASSWORD 放进 dsh 服务进程的环境(systemd 的 EnvironmentFile)。官方 CLI 还支持 ACTUAL_PASSWORD_FILE——更适合密钥文件;encrypted 预算另需 ACTUAL_ENCRYPTION_PASSWORD。改动环境变量后需重启 dsh 服务。",
1041
+ secretsTitle: "凭据",
1042
+ secretsBody: "密钥单独存放,不进入会话、提示词与配置流,保存后立即生效(下一次工具调用即用新值)。三行都可留空:口令与会话令牌二选一即可,加密口令仅加密预算需要。",
1043
+ secretPassword: "服务器口令",
1044
+ secretPasswordHint: "自托管部署的登录口令。ACTUAL_LOGIN_METHOD=password 时用它换会话令牌。",
1045
+ secretToken: "会话令牌",
1046
+ secretTokenHint: "已有的会话令牌(优先于口令)。从浏览器开发者工具的 X-Actual-Token 头可取。",
1047
+ secretEncryption: "端到端加密口令",
1048
+ secretEncryptionHint: "仅当预算启用了端到端加密时需要;普通预算留空。",
1049
+ secretSet: "已配置",
1050
+ secretUnset: "未配置",
1051
+ secretClear: "清除",
1052
+ secretShadowed: "该引用由 dsh 进程环境提供(只读层优先于凭据文件),卡片改不了:请改环境来源并重启 dsh。",
1053
+ secretFailed: "凭据操作失败:",
828
1054
  advancedTitle: "其余字段",
829
1055
  advancedBody: "以下字段只在 profile 的 cordis.patch.yml 里配置:预算目录与超时(dataDir / cacheTtl / lockTimeout / toolCallTimeoutMs)、工具暴露与读写判定(allow / deny / readActions / alwaysAsk / readTools)、预设显示与提示词(name / description / order / personaPrefix / includeRuntimeContext)。"
830
1056
  };
@@ -838,7 +1064,7 @@ ${extra}
838
1064
  serverUrlHint: "Self-hosted default is http://127.0.0.1:5006. The server version is read from <url>/info for the version-mismatch warning.",
839
1065
  serverUrlInvalid: "Must be a full http(s):// URL",
840
1066
  syncId: "Budget sync ID",
841
- syncIdHint: "Actual client → Settings → Show advanced settings → Sync ID. Empty falls back to the ACTUAL_SYNC_ID environment variable. Most commands need it.",
1067
+ syncIdHint: "Actual client → Settings → Show advanced settings → Sync ID, i.e. the groupId in budgets list; not the cloudFileId of the same entry (a wrong value reports Budget not found). Empty falls back to ACTUAL_SYNC_ID.",
842
1068
  cliCommand: "CLI command",
843
1069
  cliCommandHint: "Empty = auto-detect (ACTUAL_CLI env → actual on PATH → bundled @actual-app/cli). To pin a specific copy, give the full argv, e.g. node /opt/actual/dist/cli.js (space-separated, no quotes).",
844
1070
  cliCommandInvalid: "Quotes are not supported: separate arguments with spaces",
@@ -853,10 +1079,19 @@ ${extra}
853
1079
  namePrefix: "Tool name prefix",
854
1080
  namePrefixHint: "Model-visible tool name = prefix + command family (e.g. actual_accounts). Empty uses bare family names — query, server and tags are too generic, so a prefix is advised.",
855
1081
  namePrefixInvalid: "Letters, digits, underscore and hyphen only",
856
- secretsTitle: "Server password",
857
- secretsBody: "The password is a secret: it never enters the user-layer configuration, and this card offers no input for it (a value written here is masked away — filling it in would do nothing). Inject it at the profile row in cordis.patch.yml and keep the value in the environment:",
858
- secretsSnippet: "- id: dsh-plus-actual\n config:\n syncId: !!js process.env.ACTUAL_SYNC_ID\n password: !!js process.env.ACTUAL_PASSWORD",
859
- secretsTail: "Put ACTUAL_PASSWORD into the dsh service process environment (systemd EnvironmentFile). The official CLI also supports ACTUAL_PASSWORD_FILE, which suits secret files better; an encrypted budget additionally needs ACTUAL_ENCRYPTION_PASSWORD. Restart the dsh service after changing environment variables.",
1082
+ secretsTitle: "Credentials",
1083
+ secretsBody: "Secrets live separately — they never enter the conversation, the prompt or the settings stream, and a save applies immediately (the next tool call uses it). All three may stay empty: password and session token are alternatives, and the encryption password is only needed for encrypted budgets.",
1084
+ secretPassword: "Server password",
1085
+ secretPasswordHint: "The login password of your self-hosted deployment. Exchanged for a session token when ACTUAL_LOGIN_METHOD=password.",
1086
+ secretToken: "Session token",
1087
+ secretTokenHint: "An existing session token; it takes precedence over the password. Read it from the X-Actual-Token header in devtools.",
1088
+ secretEncryption: "End-to-end encryption password",
1089
+ secretEncryptionHint: "Only needed when the budget has end-to-end encryption enabled; leave empty otherwise.",
1090
+ secretSet: "Configured",
1091
+ secretUnset: "Not configured",
1092
+ secretClear: "Clear",
1093
+ secretShadowed: "This reference comes from the dsh process environment, which shadows the credentials file and cannot be edited here. Change the environment source and restart dsh.",
1094
+ secretFailed: "Credential operation failed: ",
860
1095
  advancedTitle: "Remaining fields",
861
1096
  advancedBody: "These are configured only in the profile cordis.patch.yml: budget directory and timeouts (dataDir / cacheTtl / lockTimeout / toolCallTimeoutMs), tool exposure and read/write classification (allow / deny / readActions / alwaysAsk / readTools), and preset display and prompt (name / description / order / personaPrefix / includeRuntimeContext)."
862
1097
  };
@@ -866,7 +1101,7 @@ ${extra}
866
1101
  //#region src/client/styles.ts
867
1102
  /**
868
1103
  * 配置卡片样式:基础规则走 @dsh-plus/shared/client 的 cardCss('act'),
869
- * 本文件只补插件特有规则(说明段落与代码片段)。
1104
+ * 本文件只补插件特有规则(说明段落与凭据行)。
870
1105
  * 沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载)。
871
1106
  * @module @dsh-plus/actual/client/styles
872
1107
  */
@@ -875,7 +1110,8 @@ ${extra}
875
1110
  .act-note{margin:14px 0 0;padding:10px 12px;border-radius:8px;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2)}
876
1111
  .act-noteTitle{margin:0 0 6px;font-size:13px;font-weight:600}
877
1112
  .act-noteBody{margin:0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.6}
878
- .act-snippet{display:block;margin:8px 0 0;padding:8px 10px;border-radius:6px;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family-mono,monospace);font-size:11px;line-height:1.5;white-space:pre;overflow-x:auto}
1113
+ .act-secret{border-top:1px solid var(--dsw-alias-border-l2)}
1114
+ .act-secretActions{display:flex;gap:8px;padding:0 0 12px}
879
1115
  `);
880
1116
  /** 幂等注入样式标签;返回标签(已存在或环境无 document 时为 null)。 */
881
1117
  function injectStyle() {
@@ -884,13 +1120,34 @@ ${extra}
884
1120
  //#endregion
885
1121
  //#region src/client/client.ts
886
1122
  const name = "dsh-plus-actual";
887
- /** 浏览器半需要的 cordis 服务 key(loader 据此注入;package.json 的 dsh.client.inject 管包加载顺序)。 */
1123
+ /**
1124
+ * 浏览器半需要的 cordis 服务 key(loader 据此注入;package.json 的 dsh.client.inject 管包加载顺序)。
1125
+ * `remote.credentials` 为硬依赖:卡片是密钥的唯一配置面,命名空间缺席时整张卡片
1126
+ * 都不该挂(官方设置页同样如此声明)。
1127
+ */
888
1128
  const inject = [
889
1129
  "slots",
890
1130
  "locale",
891
1131
  "remote",
892
- "remote.settings"
1132
+ "remote.settings",
1133
+ "remote.credentials"
893
1134
  ];
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
+ }
894
1151
  function apply(ctx) {
895
1152
  const c = ctx;
896
1153
  const tag = injectStyle();
@@ -903,6 +1160,7 @@ ${extra}
903
1160
  }), "actual: locale");
904
1161
  const scope = createSettingsScope(c, SETTINGS_NS, "actual: settings scope");
905
1162
  const api = createNamespaceApi(c.get("remote").settings, SETTINGS_NS);
1163
+ const credentials = credentialsFace(c);
906
1164
  injectPluginConfigCard(c.slots, {
907
1165
  ns: SETTINGS_NS,
908
1166
  rowId: "dsh-plus-actual",
@@ -911,7 +1169,8 @@ ${extra}
911
1169
  inject: () => ({
912
1170
  t: c.locale.bind(NS),
913
1171
  scope,
914
- api
1172
+ api,
1173
+ credentials
915
1174
  })
916
1175
  });
917
1176
  }