@dsh-plus/actual 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/lib/client.js +924 -0
- package/lib/index.d.ts +187 -0
- package/lib/index.js +753 -0
- package/package.json +63 -0
- package/src/client/card.tsx +230 -0
- package/src/client/client.ts +50 -0
- package/src/client/draft.ts +95 -0
- package/src/client/i18n.ts +128 -0
- package/src/client/styles.ts +23 -0
- package/src/config.ts +166 -0
- package/src/definition.ts +75 -0
- package/src/index.ts +104 -0
- package/src/mcp-session.ts +171 -0
- package/src/ns.ts +9 -0
- package/src/prompt.ts +45 -0
- package/src/reconnect.ts +114 -0
- package/src/runtime.ts +332 -0
- package/src/service.ts +166 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,924 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@dsh-plus/actual",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
8
|
+
let react = require("react");
|
|
9
|
+
require("@deepseek-ai/dsh-client-ui-primitives");
|
|
10
|
+
//#region ../shared/src/client/card.tsx
|
|
11
|
+
const IDLE_STATUS = {
|
|
12
|
+
kind: "idle",
|
|
13
|
+
text: ""
|
|
14
|
+
};
|
|
15
|
+
function CardChrome(props) {
|
|
16
|
+
const p = props.prefix;
|
|
17
|
+
const badge = props.statusBadge;
|
|
18
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
19
|
+
className: `${p}-section`,
|
|
20
|
+
"aria-label": props.title,
|
|
21
|
+
children: [
|
|
22
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
23
|
+
className: `${p}-sectionHead`,
|
|
24
|
+
children: [
|
|
25
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
26
|
+
className: `${p}-sectionTitle`,
|
|
27
|
+
children: props.title
|
|
28
|
+
}),
|
|
29
|
+
badge !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
30
|
+
className: `${p}-sectionBadge ${badge.on ? `${p}-statusOn` : `${p}-statusOff`}`,
|
|
31
|
+
children: badge.text
|
|
32
|
+
}) : null,
|
|
33
|
+
props.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
34
|
+
className: `${p}-pending`,
|
|
35
|
+
children: props.dirtyLabel
|
|
36
|
+
}) : null
|
|
37
|
+
]
|
|
38
|
+
}),
|
|
39
|
+
props.description === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
40
|
+
className: `${p}-sectionDesc`,
|
|
41
|
+
children: props.description
|
|
42
|
+
}),
|
|
43
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
44
|
+
className: `${p}-sectionBody`,
|
|
45
|
+
children: [
|
|
46
|
+
props.readOnlyNotice !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
47
|
+
className: `${p}-readOnly`,
|
|
48
|
+
role: "status",
|
|
49
|
+
children: props.readOnlyNotice
|
|
50
|
+
}) : null,
|
|
51
|
+
props.children,
|
|
52
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
53
|
+
className: `${p}-sectionFooter`,
|
|
54
|
+
children: [props.status !== void 0 && props.status.kind !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
55
|
+
className: `${p}-status${props.status.kind === "error" ? ` ${p}-statusError` : ""}`,
|
|
56
|
+
role: "status",
|
|
57
|
+
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
|
|
64
|
+
}, action.key))]
|
|
65
|
+
})
|
|
66
|
+
]
|
|
67
|
+
})
|
|
68
|
+
]
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/** 取值未到达时的占位:与 CardChrome 同形态,避免宿主页里闪一次卡片外壳。 */
|
|
72
|
+
function CardLoading(props) {
|
|
73
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
74
|
+
className: `${props.prefix}-section`,
|
|
75
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
76
|
+
className: `${props.prefix}-readOnly`,
|
|
77
|
+
role: "status",
|
|
78
|
+
children: props.text
|
|
79
|
+
})
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region ../shared/src/client/config-slots.ts
|
|
84
|
+
/** dsh-plus 聚合 bundle 的包名(各插件 row 均经其补丁层插入 profile)。 */
|
|
85
|
+
const DSH_PLUS_BUNDLE = "@dsh-plus/bundle-main";
|
|
86
|
+
/**
|
|
87
|
+
* 把一张配置卡片注册进插件页的两个配置槽位。
|
|
88
|
+
* 两个槽位都由 0.2.1-alpha.1 的插件页声明,任一缺席即报错——不做版本探测。
|
|
89
|
+
*/
|
|
90
|
+
function injectPluginConfigCard(slots, reg) {
|
|
91
|
+
const base = {
|
|
92
|
+
locale: reg.ns,
|
|
93
|
+
inject: reg.inject
|
|
94
|
+
};
|
|
95
|
+
slots.inject("plugins.row.config", () => slots.register({
|
|
96
|
+
name: "plugins.row.config",
|
|
97
|
+
key: `${DSH_PLUS_BUNDLE}#${reg.rowId}`,
|
|
98
|
+
...base
|
|
99
|
+
}, reg.component));
|
|
100
|
+
slots.inject("plugins.bundle.config", () => slots.register({
|
|
101
|
+
name: "plugins.bundle.config",
|
|
102
|
+
key: reg.pkg,
|
|
103
|
+
...base
|
|
104
|
+
}, reg.component));
|
|
105
|
+
}
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region ../shared/src/client/draft.ts
|
|
108
|
+
/**
|
|
109
|
+
* 单命名空间 staged 草稿(简单配置卡片共用):读 scope 快照、播种草稿、
|
|
110
|
+
* 脏判定、保存(revision fencing + 重新装载)与状态行。
|
|
111
|
+
*
|
|
112
|
+
* 语义与官方 staged 表单一致:Host 更新不覆盖在途编辑,只有保存才写回;
|
|
113
|
+
* 保存成功后按 Host 解析值重置草稿。`from`/`patch` 参与 effect 与 useMemo
|
|
114
|
+
* 依赖,必须是模块级稳定函数(不要内联箭头函数)。
|
|
115
|
+
* @module @dsh-plus/shared/client/draft
|
|
116
|
+
*/
|
|
117
|
+
function useNamespaceDraft(options) {
|
|
118
|
+
const { scope, api, from, patch, saveFailedLabel } = options;
|
|
119
|
+
const snapshot = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
|
|
120
|
+
const value = snapshot.value;
|
|
121
|
+
const [draft, setDraftState] = (0, react.useState)(null);
|
|
122
|
+
const [saving, setSaving] = (0, react.useState)(false);
|
|
123
|
+
const [status, setStatus] = (0, react.useState)(IDLE_STATUS);
|
|
124
|
+
(0, react.useEffect)(() => {
|
|
125
|
+
if (value === void 0 || draft !== null) return;
|
|
126
|
+
setDraftState(from(value));
|
|
127
|
+
}, [
|
|
128
|
+
value,
|
|
129
|
+
draft,
|
|
130
|
+
from
|
|
131
|
+
]);
|
|
132
|
+
const dirty = (0, react.useMemo)(() => value !== void 0 && draft !== null && JSON.stringify(patch(draft)) !== JSON.stringify(patch(from(value))), [
|
|
133
|
+
value,
|
|
134
|
+
draft,
|
|
135
|
+
from,
|
|
136
|
+
patch
|
|
137
|
+
]);
|
|
138
|
+
const setDraft = (next) => {
|
|
139
|
+
setDraftState(next);
|
|
140
|
+
setStatus(IDLE_STATUS);
|
|
141
|
+
};
|
|
142
|
+
const edit = (part) => {
|
|
143
|
+
if (draft === null) return;
|
|
144
|
+
setDraft({
|
|
145
|
+
...draft,
|
|
146
|
+
...part
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
const reset = () => {
|
|
150
|
+
if (value === void 0) return;
|
|
151
|
+
setDraft(from(value));
|
|
152
|
+
};
|
|
153
|
+
const save = () => {
|
|
154
|
+
if (draft === null) return;
|
|
155
|
+
setSaving(true);
|
|
156
|
+
const revision = scope.getSnapshot().revision;
|
|
157
|
+
api.update(patch(draft), revision).then(async () => {
|
|
158
|
+
await scope.load();
|
|
159
|
+
const next = scope.getSnapshot().value;
|
|
160
|
+
if (next !== void 0) setDraftState(from(next));
|
|
161
|
+
setStatus(IDLE_STATUS);
|
|
162
|
+
}).catch((error) => {
|
|
163
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
164
|
+
setStatus({
|
|
165
|
+
kind: "error",
|
|
166
|
+
text: `${saveFailedLabel}${message}`
|
|
167
|
+
});
|
|
168
|
+
}).finally(() => setSaving(false));
|
|
169
|
+
};
|
|
170
|
+
return {
|
|
171
|
+
value,
|
|
172
|
+
draft,
|
|
173
|
+
dirty,
|
|
174
|
+
saving,
|
|
175
|
+
status,
|
|
176
|
+
disabled: !snapshot.writable,
|
|
177
|
+
setDraft,
|
|
178
|
+
edit,
|
|
179
|
+
reset,
|
|
180
|
+
save
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
//#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
|
+
//#region ../shared/src/client/i18n.ts
|
|
278
|
+
/** 卡片通用文案(save/discard/unsaved/loading 等四插件共有键)。 */
|
|
279
|
+
const commonZh = {
|
|
280
|
+
save: "保存",
|
|
281
|
+
saving: "保存中…",
|
|
282
|
+
discard: "放弃",
|
|
283
|
+
unsaved: "未保存",
|
|
284
|
+
loading: "加载中…",
|
|
285
|
+
unavailable: "配置服务不可用。",
|
|
286
|
+
readOnly: "当前部署无 settings provider,配置为只读;请编辑 settings.yaml。",
|
|
287
|
+
invalidNumber: "请输入正整数。",
|
|
288
|
+
collapse: "收起",
|
|
289
|
+
expand: "展开",
|
|
290
|
+
enabledOn: "已启用",
|
|
291
|
+
enabledOff: "未启用",
|
|
292
|
+
saveFailed: "保存失败,请检查填写内容。",
|
|
293
|
+
confirm: "确认",
|
|
294
|
+
cancel: "取消"
|
|
295
|
+
};
|
|
296
|
+
const commonEn = {
|
|
297
|
+
save: "Save",
|
|
298
|
+
saving: "Saving…",
|
|
299
|
+
discard: "Discard",
|
|
300
|
+
unsaved: "Unsaved",
|
|
301
|
+
loading: "Loading…",
|
|
302
|
+
unavailable: "Settings service unavailable.",
|
|
303
|
+
readOnly: "No settings provider in this deployment; config is read-only. Edit settings.yaml.",
|
|
304
|
+
invalidNumber: "Enter a positive integer.",
|
|
305
|
+
collapse: "Collapse",
|
|
306
|
+
expand: "Expand",
|
|
307
|
+
enabledOn: "On",
|
|
308
|
+
enabledOff: "Off",
|
|
309
|
+
saveFailed: "Save failed; check the fields.",
|
|
310
|
+
confirm: "Confirm",
|
|
311
|
+
cancel: "Cancel"
|
|
312
|
+
};
|
|
313
|
+
/**
|
|
314
|
+
* own 覆盖同键合并(返回新对象,入参不可变)。
|
|
315
|
+
* 返回类型保留两侧键集合,调用方据此派生 DictKey。
|
|
316
|
+
*/
|
|
317
|
+
function mergeDict(base, own) {
|
|
318
|
+
return {
|
|
319
|
+
...base,
|
|
320
|
+
...own
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
//#endregion
|
|
324
|
+
//#region ../shared/src/client/scope.ts
|
|
325
|
+
/**
|
|
326
|
+
* remote.settings 直连实现的命名空间 scope(官方配置页 tab 同款模式)。
|
|
327
|
+
* 读:describe 取命名空间视图(脱敏 value/revision + 顶层 writable);
|
|
328
|
+
* 刷新:remote `settings/document-updated`(按命名空间过滤)与
|
|
329
|
+
* `connection/reset`;generation 防旧读覆盖新发布。任何页面 origin
|
|
330
|
+
* (loopback 或 tailnet 信任域名)下行为一致。
|
|
331
|
+
* effect 归属调用方 fiber(label 需含插件名以便诊断)。
|
|
332
|
+
*/
|
|
333
|
+
function createSettingsScope(c, ns, label) {
|
|
334
|
+
const remote = c.get("remote");
|
|
335
|
+
let state = {
|
|
336
|
+
status: "loading",
|
|
337
|
+
value: void 0,
|
|
338
|
+
revision: void 0,
|
|
339
|
+
writable: false
|
|
340
|
+
};
|
|
341
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
342
|
+
let generation = 0;
|
|
343
|
+
const publish = (next) => {
|
|
344
|
+
state = next;
|
|
345
|
+
for (const listener of [...listeners]) listener();
|
|
346
|
+
};
|
|
347
|
+
const load = async () => {
|
|
348
|
+
const gen = ++generation;
|
|
349
|
+
let view;
|
|
350
|
+
try {
|
|
351
|
+
const response = await remote.settings.describe();
|
|
352
|
+
if (!response.ok) return;
|
|
353
|
+
view = response.value;
|
|
354
|
+
} catch {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (gen !== generation) return;
|
|
358
|
+
const namespace = view.namespaces.find((candidate) => candidate.ns === ns);
|
|
359
|
+
if (namespace === void 0) {
|
|
360
|
+
publish({
|
|
361
|
+
status: "unavailable",
|
|
362
|
+
value: void 0,
|
|
363
|
+
revision: void 0,
|
|
364
|
+
writable: view.writable
|
|
365
|
+
});
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
publish({
|
|
369
|
+
status: "ready",
|
|
370
|
+
value: namespace.value,
|
|
371
|
+
revision: namespace.revision,
|
|
372
|
+
writable: view.writable
|
|
373
|
+
});
|
|
374
|
+
};
|
|
375
|
+
const refresh = (namespace) => {
|
|
376
|
+
if (namespace !== void 0 && namespace !== ns) return;
|
|
377
|
+
load();
|
|
378
|
+
};
|
|
379
|
+
const disposers = [remote.$on("settings/document-updated", refresh), c.on("connection/reset", () => {
|
|
380
|
+
load();
|
|
381
|
+
})];
|
|
382
|
+
c.effect(() => () => {
|
|
383
|
+
for (const dispose of disposers) dispose();
|
|
384
|
+
}, label);
|
|
385
|
+
load();
|
|
386
|
+
return {
|
|
387
|
+
getSnapshot: () => state,
|
|
388
|
+
subscribe: (listener) => {
|
|
389
|
+
listeners.add(listener);
|
|
390
|
+
return () => {
|
|
391
|
+
listeners.delete(listener);
|
|
392
|
+
};
|
|
393
|
+
},
|
|
394
|
+
load
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
/** 以 remote.settings 构造命名空间绑定的 api 面。 */
|
|
398
|
+
function createNamespaceApi(remote, ns) {
|
|
399
|
+
return {
|
|
400
|
+
update: async (patch, expectedRevision) => {
|
|
401
|
+
const response = await remote.update(ns, patch, expectedRevision);
|
|
402
|
+
if (!response.ok) throw new Error(response.error.message ?? "settings update failed");
|
|
403
|
+
},
|
|
404
|
+
describe: async () => {
|
|
405
|
+
const response = await remote.describe();
|
|
406
|
+
if (!response.ok) throw new Error(response.error.message ?? "settings describe failed");
|
|
407
|
+
return response.value;
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
//#endregion
|
|
412
|
+
//#region ../shared/src/client/styles.ts
|
|
413
|
+
/**
|
|
414
|
+
* 配置分节样式工厂(各插件 client 的公共收编版)。
|
|
415
|
+
*
|
|
416
|
+
* 目标宿主 dsh 0.2.1-alpha.1:插件页把配置视图放进已有页面级内边距的容器里
|
|
417
|
+
* (`div.detailSections[data-plugin-config]` 或 `section.detailSection[data-plugin-config]`),
|
|
418
|
+
* 外层无边框——故这里输出的是**无边框分节**(小节标题 13px/600、字段 12px 0、
|
|
419
|
+
* 横向零内边距、页脚非 sticky),度量对齐官方设置页
|
|
420
|
+
* (`@deepseek-ai/dsh-client-ui-primitives` 的 settings-form)。
|
|
421
|
+
*
|
|
422
|
+
* 移动端与可达性(767px 断点 + pointer:coarse,对齐 ui-mobile-fit 约定):
|
|
423
|
+
* - 窄屏 input/textarea 16px(防 iOS 聚焦缩放)、可点目标 ≥44px;
|
|
424
|
+
* - 页脚按钮等宽换行、状态文字独占一行。
|
|
425
|
+
* 沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载),不覆盖上游选择器。
|
|
426
|
+
* @module @dsh-plus/shared/client/styles
|
|
427
|
+
*/
|
|
428
|
+
/** 生成全套分节 CSS;prefix 为插件类名前缀(如 'dne'),extra 为插件特有规则。 */
|
|
429
|
+
function cardCss(prefix, extra = "") {
|
|
430
|
+
return `
|
|
431
|
+
.${prefix}-section{box-sizing:border-box;flex-direction:column;width:100%;min-width:0;padding:16px 0;display:flex}
|
|
432
|
+
.${prefix}-sectionHead{align-items:center;gap:8px;min-width:0;display:flex}
|
|
433
|
+
.${prefix}-sectionTitle{min-width:0;color:var(--dsw-alias-label-primary);flex:1;margin:0;font-size:13px;font-weight:600;line-height:1.5}
|
|
434
|
+
.${prefix}-sectionBadge{white-space:nowrap;border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
|
|
435
|
+
.${prefix}-sectionDesc{color:var(--dsw-alias-label-tertiary);margin:4px 0 0;font-size:12px;line-height:1.5}
|
|
436
|
+
.${prefix}-sectionBody{flex-direction:column;min-width:0;display:flex}
|
|
437
|
+
.${prefix}-sectionFooter{align-items:center;gap:8px;padding-top:16px;display:flex;flex-wrap:wrap}
|
|
438
|
+
.${prefix}-pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
|
|
439
|
+
.${prefix}-statusOn{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
|
|
440
|
+
.${prefix}-statusOff{color:var(--dsw-alias-label-tertiary)}
|
|
441
|
+
.${prefix}-field{flex-direction:column;gap:6px;padding:12px 0;display:flex}
|
|
442
|
+
.${prefix}-field+.${prefix}-field{border-top:1px solid var(--dsw-alias-border-l2)}
|
|
443
|
+
.${prefix}-head{align-items:center;gap:8px;display:flex}
|
|
444
|
+
.${prefix}-label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}
|
|
445
|
+
.${prefix}-badge{white-space:nowrap;border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
|
|
446
|
+
.${prefix}-badgeSet{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
|
|
447
|
+
.${prefix}-badgeUnset{color:var(--dsw-alias-label-tertiary)}
|
|
448
|
+
.${prefix}-input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;transition:border-color .18s,background .18s}
|
|
449
|
+
.${prefix}-input:hover:not(:disabled):not(:focus-visible){border-color:var(--dsw-alias-label-dimmed)}
|
|
450
|
+
.${prefix}-input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}
|
|
451
|
+
.${prefix}-input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
|
|
452
|
+
.${prefix}-inputInvalid{border-color:var(--dsw-alias-label-error)}
|
|
453
|
+
.${prefix}-select{appearance:none;background:var(--dsw-alias-bg-module-platform);border:none;cursor:pointer;background-image:linear-gradient(45deg,transparent 50%,var(--dsw-alias-label-secondary) 50%),linear-gradient(135deg,var(--dsw-alias-label-secondary) 50%,transparent 50%);background-position:calc(100% - 16px) 50%,calc(100% - 11px) 50%;background-size:5px 5px;background-repeat:no-repeat;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;height:34px;box-sizing:border-box;border-radius:8px;padding:0 30px 0 12px;transition:background-color .18s}
|
|
454
|
+
.${prefix}-select:hover:not(:disabled){background-color:var(--dsw-alias-interactive-bg-hover)}
|
|
455
|
+
.${prefix}-hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}
|
|
456
|
+
.${prefix}-invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}
|
|
457
|
+
.${prefix}-checkRow{align-items:center;gap:10px;display:flex;padding:6px 0}
|
|
458
|
+
.${prefix}-checkRow label{color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5;cursor:pointer;flex:1;min-width:0}
|
|
459
|
+
/* 自绘圆角扁平开关(官方 UI 无可见原生 checkbox,选择类全走胶囊/菜单) */
|
|
460
|
+
.${prefix}-checkRow input{appearance:none;-webkit-appearance:none;position:relative;box-sizing:border-box;background:var(--dsw-alias-bg-module-platform);border:none;border-radius:999px;width:34px;height:20px;flex:none;cursor:pointer;transition:background .18s;margin:0}
|
|
461
|
+
.${prefix}-checkRow input::after{content:"";position:absolute;top:2px;left:2px;background:var(--dsw-alias-label-primary);border-radius:999px;width:16px;height:16px;transition:left .18s cubic-bezier(.4,0,.2,1)}
|
|
462
|
+
.${prefix}-checkRow input:checked{background:var(--dsw-alias-brand-primary)}
|
|
463
|
+
.${prefix}-checkRow input:checked::after{left:16px;background:var(--dsw-alias-bg-base)}
|
|
464
|
+
.${prefix}-checkRow input:disabled{opacity:.4;cursor:default}
|
|
465
|
+
.${prefix}-checkRow input:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
|
|
466
|
+
.${prefix}-groupLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;padding:12px 0 2px;margin:0}
|
|
467
|
+
.${prefix}-readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}
|
|
468
|
+
.${prefix}-warn{color:var(--dsw-alias-label-error);margin:12px 0 0;font-size:12px;line-height:1.5}
|
|
469
|
+
.${prefix}-status{min-width:0;color:var(--dsw-alias-label-secondary);flex:1;margin:0;font-size:12px;line-height:1.5}
|
|
470
|
+
.${prefix}-statusError{color:var(--dsw-alias-label-error)}
|
|
471
|
+
.${prefix}-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}
|
|
472
|
+
.${prefix}-btnGhost{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}
|
|
473
|
+
.${prefix}-btnGhost:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}
|
|
474
|
+
.${prefix}-btnPrimary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}
|
|
475
|
+
.${prefix}-btn:disabled{opacity:.4;cursor:default}
|
|
476
|
+
.${prefix}-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
477
|
+
@media (max-width:767px){
|
|
478
|
+
.${prefix}-input,.${prefix}-select{font-size:16px;height:40px}
|
|
479
|
+
.${prefix}-checkRow{min-height:44px;padding:8px 0}
|
|
480
|
+
.${prefix}-status{flex-basis:100%;flex:none}
|
|
481
|
+
.${prefix}-btn{min-height:44px;flex:1;min-width:96px}
|
|
482
|
+
}
|
|
483
|
+
@media (pointer:coarse){
|
|
484
|
+
.${prefix}-btn{min-height:44px}
|
|
485
|
+
.${prefix}-checkRow{min-height:44px}
|
|
486
|
+
.${prefix}-input,.${prefix}-select{height:40px}
|
|
487
|
+
}
|
|
488
|
+
${extra}
|
|
489
|
+
`;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* 幂等注入样式标签(官方 data-plugin-css 约定);返回新标签,
|
|
493
|
+
* 已存在或环境无 document 时返回 null(调用方 effect 卸载用)。
|
|
494
|
+
*/
|
|
495
|
+
function injectCardStyle(pluginId, css) {
|
|
496
|
+
if (typeof document === "undefined") return null;
|
|
497
|
+
const tagId = `${pluginId}/card.css`;
|
|
498
|
+
if (document.querySelector(`style[data-plugin-css=${JSON.stringify(tagId)}]`) !== null) return null;
|
|
499
|
+
const tag = document.createElement("style");
|
|
500
|
+
tag.dataset.plugin = pluginId;
|
|
501
|
+
tag.dataset.pluginCss = tagId;
|
|
502
|
+
tag.textContent = css;
|
|
503
|
+
document.head.appendChild(tag);
|
|
504
|
+
return tag;
|
|
505
|
+
}
|
|
506
|
+
//#endregion
|
|
507
|
+
//#region src/ns.ts
|
|
508
|
+
/**
|
|
509
|
+
* settings 命名空间字面量(纯常量,零依赖)。
|
|
510
|
+
*
|
|
511
|
+
* 宿主以字面量注册 settings section(用户层配置经 dsh-settings-file 持久化,
|
|
512
|
+
* 热生效);与行级 cordis Config 共用同一份 schemastery schema(config.ts)。
|
|
513
|
+
* 浏览器半的配置卡片按同一字面量读写。
|
|
514
|
+
* @module @dsh-plus/actual/ns
|
|
515
|
+
*/
|
|
516
|
+
const SETTINGS_NS = "dsh-plus-actual";
|
|
517
|
+
//#endregion
|
|
518
|
+
//#region src/client/draft.ts
|
|
519
|
+
/** argv → 卡片文本(空格分隔)。 */
|
|
520
|
+
function argvToText(argv) {
|
|
521
|
+
return argv.join(" ");
|
|
522
|
+
}
|
|
523
|
+
/** 卡片文本 → argv;纯空白视为「自动探测」(空数组)。 */
|
|
524
|
+
function textToArgv(text) {
|
|
525
|
+
const trimmed = text.trim();
|
|
526
|
+
return trimmed === "" ? [] : trimmed.split(/\s+/);
|
|
527
|
+
}
|
|
528
|
+
/** 命名空间解析值 → 草稿。 */
|
|
529
|
+
function draftFromValue(value) {
|
|
530
|
+
return {
|
|
531
|
+
enabled: value.enabled,
|
|
532
|
+
serverUrl: value.serverUrl,
|
|
533
|
+
syncId: value.syncId,
|
|
534
|
+
cliCommand: argvToText(value.cliCommand),
|
|
535
|
+
cliVersionPolicy: value.cliVersionPolicy,
|
|
536
|
+
confirmWrites: value.confirmWrites,
|
|
537
|
+
auxTools: value.auxTools,
|
|
538
|
+
namePrefix: value.namePrefix
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
/** 草稿 → 命名空间补丁(仅本卡片负责的字段)。 */
|
|
542
|
+
function patchFromDraft(draft) {
|
|
543
|
+
return {
|
|
544
|
+
enabled: draft.enabled,
|
|
545
|
+
serverUrl: draft.serverUrl.trim(),
|
|
546
|
+
syncId: draft.syncId.trim(),
|
|
547
|
+
cliCommand: textToArgv(draft.cliCommand),
|
|
548
|
+
cliVersionPolicy: draft.cliVersionPolicy,
|
|
549
|
+
confirmWrites: draft.confirmWrites,
|
|
550
|
+
auxTools: draft.auxTools,
|
|
551
|
+
namePrefix: draft.namePrefix.trim()
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
/** 首个校验问题;全部合法返回 undefined(调用方据此禁用保存)。 */
|
|
555
|
+
function validateDraft(draft) {
|
|
556
|
+
if (!/^https?:\/\/\S+$/.test(draft.serverUrl.trim())) return {
|
|
557
|
+
field: "serverUrl",
|
|
558
|
+
messageKey: "serverUrlInvalid"
|
|
559
|
+
};
|
|
560
|
+
if (/["']/.test(draft.cliCommand)) return {
|
|
561
|
+
field: "cliCommand",
|
|
562
|
+
messageKey: "cliCommandInvalid"
|
|
563
|
+
};
|
|
564
|
+
if (!/^[A-Za-z0-9_-]*$/.test(draft.namePrefix.trim())) return {
|
|
565
|
+
field: "namePrefix",
|
|
566
|
+
messageKey: "namePrefixInvalid"
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
//#endregion
|
|
570
|
+
//#region src/client/card.tsx
|
|
571
|
+
/**
|
|
572
|
+
* 「Actual Budget 连接」配置卡片:经 injectPluginConfigCard 注册(插件页
|
|
573
|
+
* plugins.row.config / plugins.bundle.config,view 分发 summary/page)。
|
|
574
|
+
*
|
|
575
|
+
* 编辑范围只覆盖**非密钥**字段。密钥与高级字段没有编辑器,卡片内给出明确落点
|
|
576
|
+
* (见 i18n 的 secretsBody / advancedBody)——按《插件开发指南》,可配置项要么
|
|
577
|
+
* 有编辑器、要么写明为什么没有与改在哪里配。
|
|
578
|
+
*
|
|
579
|
+
* 字段按主题拆成两个小节组件,卡片本体只负责外壳、状态与保存动作。
|
|
580
|
+
* @module @dsh-plus/actual/client/card
|
|
581
|
+
*/
|
|
582
|
+
/** 模块级稳定函数:参与 useNamespaceDraft 的依赖比较。 */
|
|
583
|
+
function fromValue(value) {
|
|
584
|
+
return draftFromValue(value);
|
|
585
|
+
}
|
|
586
|
+
function toPatch(draft) {
|
|
587
|
+
return patchFromDraft(draft);
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* 条件式 invalid 属性:`exactOptionalPropertyTypes` 下不可显式传 `invalidLabel: undefined`,
|
|
591
|
+
* 只能按「有文案才带该键」构造。
|
|
592
|
+
*/
|
|
593
|
+
function violationProps(active, label) {
|
|
594
|
+
return active ? {
|
|
595
|
+
invalid: true,
|
|
596
|
+
invalidLabel: label
|
|
597
|
+
} : { invalid: false };
|
|
598
|
+
}
|
|
599
|
+
/** 说明段落(无编辑器的字段在此交代落点)。 */
|
|
600
|
+
function Note(props) {
|
|
601
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
602
|
+
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
|
+
]
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
/** 连接小节:服务器地址、预算、CLI 命令与版本策略。 */
|
|
624
|
+
function ConnectionSection(props) {
|
|
625
|
+
const { t, draft, disabled, violation, onEdit } = props;
|
|
626
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
627
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
628
|
+
prefix: "act",
|
|
629
|
+
id: "act-server-url",
|
|
630
|
+
label: t("serverUrl"),
|
|
631
|
+
hint: t("serverUrlHint"),
|
|
632
|
+
value: draft.serverUrl,
|
|
633
|
+
disabled,
|
|
634
|
+
...violationProps(violation?.field === "serverUrl", t("serverUrlInvalid")),
|
|
635
|
+
onEdit: (value) => onEdit({ serverUrl: value })
|
|
636
|
+
}),
|
|
637
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
638
|
+
prefix: "act",
|
|
639
|
+
id: "act-sync-id",
|
|
640
|
+
label: t("syncId"),
|
|
641
|
+
hint: t("syncIdHint"),
|
|
642
|
+
value: draft.syncId,
|
|
643
|
+
disabled,
|
|
644
|
+
onEdit: (value) => onEdit({ syncId: value })
|
|
645
|
+
}),
|
|
646
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
647
|
+
prefix: "act",
|
|
648
|
+
id: "act-cli-command",
|
|
649
|
+
label: t("cliCommand"),
|
|
650
|
+
hint: t("cliCommandHint"),
|
|
651
|
+
value: draft.cliCommand,
|
|
652
|
+
disabled,
|
|
653
|
+
...violationProps(violation?.field === "cliCommand", t("cliCommandInvalid")),
|
|
654
|
+
onEdit: (value) => onEdit({ cliCommand: value })
|
|
655
|
+
}),
|
|
656
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectField, {
|
|
657
|
+
prefix: "act",
|
|
658
|
+
id: "act-version-policy",
|
|
659
|
+
label: t("cliVersionPolicy"),
|
|
660
|
+
hint: t("cliVersionPolicyHint"),
|
|
661
|
+
value: draft.cliVersionPolicy,
|
|
662
|
+
options: [{
|
|
663
|
+
value: "warn",
|
|
664
|
+
label: t("policyWarn")
|
|
665
|
+
}, {
|
|
666
|
+
value: "strict",
|
|
667
|
+
label: t("policyStrict")
|
|
668
|
+
}],
|
|
669
|
+
disabled,
|
|
670
|
+
onEdit: (value) => onEdit({ cliVersionPolicy: value === "strict" ? "strict" : "warn" })
|
|
671
|
+
})
|
|
672
|
+
] });
|
|
673
|
+
}
|
|
674
|
+
/** 行为小节:写确认、辅助工具与工具名前缀。 */
|
|
675
|
+
function PolicySection(props) {
|
|
676
|
+
const { t, draft, disabled, violation, onEdit } = props;
|
|
677
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
678
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
|
|
679
|
+
prefix: "act",
|
|
680
|
+
id: "act-confirm-writes",
|
|
681
|
+
label: t("confirmWrites"),
|
|
682
|
+
checked: draft.confirmWrites,
|
|
683
|
+
disabled,
|
|
684
|
+
onEdit: (value) => onEdit({ confirmWrites: value })
|
|
685
|
+
}),
|
|
686
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
687
|
+
className: "act-noteBody",
|
|
688
|
+
children: t("confirmWritesHint")
|
|
689
|
+
}),
|
|
690
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
|
|
691
|
+
prefix: "act",
|
|
692
|
+
id: "act-aux-tools",
|
|
693
|
+
label: t("auxTools"),
|
|
694
|
+
checked: draft.auxTools,
|
|
695
|
+
disabled,
|
|
696
|
+
onEdit: (value) => onEdit({ auxTools: value })
|
|
697
|
+
}),
|
|
698
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
699
|
+
className: "act-noteBody",
|
|
700
|
+
children: t("auxToolsHint")
|
|
701
|
+
}),
|
|
702
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
703
|
+
prefix: "act",
|
|
704
|
+
id: "act-name-prefix",
|
|
705
|
+
label: t("namePrefix"),
|
|
706
|
+
hint: t("namePrefixHint"),
|
|
707
|
+
value: draft.namePrefix,
|
|
708
|
+
disabled,
|
|
709
|
+
...violationProps(violation?.field === "namePrefix", t("namePrefixInvalid")),
|
|
710
|
+
onEdit: (value) => onEdit({ namePrefix: value })
|
|
711
|
+
})
|
|
712
|
+
] });
|
|
713
|
+
}
|
|
714
|
+
function ActualCard(props) {
|
|
715
|
+
const { t, scope, api } = props;
|
|
716
|
+
const { draft, dirty, saving, status, disabled, edit, reset, save } = useNamespaceDraft({
|
|
717
|
+
scope,
|
|
718
|
+
api,
|
|
719
|
+
from: fromValue,
|
|
720
|
+
patch: toPatch,
|
|
721
|
+
saveFailedLabel: t("saveFailed")
|
|
722
|
+
});
|
|
723
|
+
if (props.view === "summary") return t("summaryLine");
|
|
724
|
+
if (draft === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardLoading, {
|
|
725
|
+
prefix: "act",
|
|
726
|
+
text: t("loading")
|
|
727
|
+
});
|
|
728
|
+
const violation = validateDraft(draft);
|
|
729
|
+
const section = {
|
|
730
|
+
t,
|
|
731
|
+
draft,
|
|
732
|
+
disabled,
|
|
733
|
+
violation,
|
|
734
|
+
onEdit: edit
|
|
735
|
+
};
|
|
736
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CardChrome, {
|
|
737
|
+
prefix: "act",
|
|
738
|
+
title: t("title"),
|
|
739
|
+
description: t("description"),
|
|
740
|
+
statusBadge: {
|
|
741
|
+
text: t(draft.enabled ? "enabledOn" : "enabledOff"),
|
|
742
|
+
on: draft.enabled
|
|
743
|
+
},
|
|
744
|
+
dirty,
|
|
745
|
+
dirtyLabel: t("unsaved"),
|
|
746
|
+
readOnlyNotice: disabled ? t("readOnly") : void 0,
|
|
747
|
+
status,
|
|
748
|
+
actions: [{
|
|
749
|
+
key: "discard",
|
|
750
|
+
label: t("discard"),
|
|
751
|
+
disabled: !dirty || saving,
|
|
752
|
+
onClick: reset
|
|
753
|
+
}, {
|
|
754
|
+
key: "save",
|
|
755
|
+
label: t(saving ? "saving" : "save"),
|
|
756
|
+
variant: "primary",
|
|
757
|
+
disabled: !dirty || saving || violation !== void 0 || disabled,
|
|
758
|
+
onClick: save
|
|
759
|
+
}],
|
|
760
|
+
children: [
|
|
761
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
|
|
762
|
+
prefix: "act",
|
|
763
|
+
id: "act-enabled",
|
|
764
|
+
label: t("enabled"),
|
|
765
|
+
checked: draft.enabled,
|
|
766
|
+
disabled,
|
|
767
|
+
onEdit: (value) => edit({ enabled: value })
|
|
768
|
+
}),
|
|
769
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
770
|
+
className: "act-noteBody",
|
|
771
|
+
children: t("enabledHint")
|
|
772
|
+
}),
|
|
773
|
+
/* @__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")
|
|
780
|
+
}),
|
|
781
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Note, {
|
|
782
|
+
title: t("advancedTitle"),
|
|
783
|
+
body: t("advancedBody")
|
|
784
|
+
})
|
|
785
|
+
]
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
//#endregion
|
|
789
|
+
//#region src/client/i18n.ts
|
|
790
|
+
/**
|
|
791
|
+
* 配置卡片文案(zh/en)。经 ctx.locale.register 注册、bind 取用。
|
|
792
|
+
* 公共键(save/discard/unsaved 等)来自 shared 的 common 字典,本文件只维护业务键。
|
|
793
|
+
*
|
|
794
|
+
* 两处说明文案是有意为之的「落点」:密钥与高级字段都没有编辑器,必须在卡片内
|
|
795
|
+
* 写清「为什么没有、该去哪里配」,否则就是留给用户的空承诺。
|
|
796
|
+
* @module @dsh-plus/actual/client/i18n
|
|
797
|
+
*/
|
|
798
|
+
const NS = "dsh-plus-actual";
|
|
799
|
+
const ownZh = {
|
|
800
|
+
title: "Actual Budget 连接",
|
|
801
|
+
description: "配置本机 Actual 同步服务器与 CLI 行为。改动热生效,无需重载:连接相关字段会触发能力重新发现。",
|
|
802
|
+
summaryLine: "Actual 服务器地址、预算 Sync ID、CLI 与工具策略。",
|
|
803
|
+
enabled: "启用 actual 预设",
|
|
804
|
+
enabledHint: "关闭即不注册该预设(等价插件未安装);已开的会话保持原组合,新会话起不再出现。",
|
|
805
|
+
serverUrl: "同步服务器地址",
|
|
806
|
+
serverUrlHint: "自托管默认 http://127.0.0.1:5006。服务端版本从 <地址>/info 读取,用于版本一致性告警。",
|
|
807
|
+
serverUrlInvalid: "必须是 http(s):// 开头的完整地址",
|
|
808
|
+
syncId: "预算 Sync ID",
|
|
809
|
+
syncIdHint: "Actual 客户端 → 设置 → 显示高级设置 → Sync ID。留空则读环境变量 ACTUAL_SYNC_ID。多数命令都需要它。",
|
|
810
|
+
cliCommand: "CLI 命令",
|
|
811
|
+
cliCommandHint: "留空 = 自动探测(ACTUAL_CLI 环境变量 → PATH 上的 actual → 包内 @actual-app/cli)。需要指定具体副本时填完整 argv,如 node /opt/actual/dist/cli.js(空格分隔,不支持引号)。",
|
|
812
|
+
cliCommandInvalid: "不支持引号:请直接以空格分隔各个参数",
|
|
813
|
+
cliVersionPolicy: "版本不一致处置",
|
|
814
|
+
cliVersionPolicyHint: "CLI 与服务端 major.minor 不一致时:warn 告警继续(Actual 服务端只做同步中继,通常仍可用);strict 直接让发现失败。",
|
|
815
|
+
policyWarn: "warn —— 告警并继续",
|
|
816
|
+
policyStrict: "strict —— 直接失败",
|
|
817
|
+
confirmWrites: "写操作需确认",
|
|
818
|
+
confirmWritesHint: "经 DSH 审批策略生效:会话为「每次询问」时弹审批窗;「完全权限」下自动通过(绝不静默拒绝)。",
|
|
819
|
+
auxTools: "挂载辅助工具",
|
|
820
|
+
auxToolsHint: "额外提供 web 检索、结构化提问与待办跟踪三件套;它们不改动预算数据。",
|
|
821
|
+
namePrefix: "工具名前缀",
|
|
822
|
+
namePrefixHint: "模型可见的工具名 = 前缀 + 命令族名(如 actual_accounts)。留空则直接用裸族名——query、server 等过于通用,不建议。",
|
|
823
|
+
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 服务。",
|
|
828
|
+
advancedTitle: "其余字段",
|
|
829
|
+
advancedBody: "以下字段只在 profile 的 cordis.patch.yml 里配置:预算目录与超时(dataDir / cacheTtl / lockTimeout / toolCallTimeoutMs)、工具暴露与读写判定(allow / deny / readActions / alwaysAsk / readTools)、预设显示与提示词(name / description / order / personaPrefix / includeRuntimeContext)。"
|
|
830
|
+
};
|
|
831
|
+
const ownEn = {
|
|
832
|
+
title: "Actual Budget connection",
|
|
833
|
+
description: "Configure the local Actual sync server and CLI behaviour. Changes apply hot — connection fields trigger a fresh capability discovery.",
|
|
834
|
+
summaryLine: "Actual server address, budget sync ID, CLI and tool policy.",
|
|
835
|
+
enabled: "Enable the actual preset",
|
|
836
|
+
enabledHint: "Off stops registering the preset (equivalent to not installed). Live sessions keep their composition; new ones will not see it.",
|
|
837
|
+
serverUrl: "Sync server URL",
|
|
838
|
+
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
|
+
serverUrlInvalid: "Must be a full http(s):// URL",
|
|
840
|
+
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.",
|
|
842
|
+
cliCommand: "CLI command",
|
|
843
|
+
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
|
+
cliCommandInvalid: "Quotes are not supported: separate arguments with spaces",
|
|
845
|
+
cliVersionPolicy: "On version mismatch",
|
|
846
|
+
cliVersionPolicyHint: "When CLI and server major.minor differ: warn keeps going (the Actual server is a sync relay, usually still fine); strict fails discovery outright.",
|
|
847
|
+
policyWarn: "warn — alert and continue",
|
|
848
|
+
policyStrict: "strict — fail outright",
|
|
849
|
+
confirmWrites: "Confirm write operations",
|
|
850
|
+
confirmWritesHint: "Routed through the DSH approval policy: prompts when the session asks every time, passes automatically under full permission (never silently denies).",
|
|
851
|
+
auxTools: "Mount auxiliary tools",
|
|
852
|
+
auxToolsHint: "Adds web search, structured questions and todo tracking; none of them touch budget data.",
|
|
853
|
+
namePrefix: "Tool name prefix",
|
|
854
|
+
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
|
+
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.",
|
|
860
|
+
advancedTitle: "Remaining fields",
|
|
861
|
+
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
|
+
};
|
|
863
|
+
const zh = mergeDict(commonZh, ownZh);
|
|
864
|
+
const en = mergeDict(commonEn, ownEn);
|
|
865
|
+
//#endregion
|
|
866
|
+
//#region src/client/styles.ts
|
|
867
|
+
/**
|
|
868
|
+
* 配置卡片样式:基础规则走 @dsh-plus/shared/client 的 cardCss('act'),
|
|
869
|
+
* 本文件只补插件特有规则(说明段落与代码片段)。
|
|
870
|
+
* 沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载)。
|
|
871
|
+
* @module @dsh-plus/actual/client/styles
|
|
872
|
+
*/
|
|
873
|
+
const PLUGIN_ID = "@dsh-plus/actual";
|
|
874
|
+
const cardCssAll = cardCss("act", `
|
|
875
|
+
.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
|
+
.act-noteTitle{margin:0 0 6px;font-size:13px;font-weight:600}
|
|
877
|
+
.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}
|
|
879
|
+
`);
|
|
880
|
+
/** 幂等注入样式标签;返回标签(已存在或环境无 document 时为 null)。 */
|
|
881
|
+
function injectStyle() {
|
|
882
|
+
return injectCardStyle(PLUGIN_ID, cardCssAll);
|
|
883
|
+
}
|
|
884
|
+
//#endregion
|
|
885
|
+
//#region src/client/client.ts
|
|
886
|
+
const name = "dsh-plus-actual";
|
|
887
|
+
/** 浏览器半需要的 cordis 服务 key(loader 据此注入;package.json 的 dsh.client.inject 管包加载顺序)。 */
|
|
888
|
+
const inject = [
|
|
889
|
+
"slots",
|
|
890
|
+
"locale",
|
|
891
|
+
"remote",
|
|
892
|
+
"remote.settings"
|
|
893
|
+
];
|
|
894
|
+
function apply(ctx) {
|
|
895
|
+
const c = ctx;
|
|
896
|
+
const tag = injectStyle();
|
|
897
|
+
c.effect(() => () => {
|
|
898
|
+
tag?.remove();
|
|
899
|
+
}, "actual: style");
|
|
900
|
+
c.effect(() => c.locale.register(NS, {
|
|
901
|
+
zh,
|
|
902
|
+
en
|
|
903
|
+
}), "actual: locale");
|
|
904
|
+
const scope = createSettingsScope(c, SETTINGS_NS, "actual: settings scope");
|
|
905
|
+
const api = createNamespaceApi(c.get("remote").settings, SETTINGS_NS);
|
|
906
|
+
injectPluginConfigCard(c.slots, {
|
|
907
|
+
ns: SETTINGS_NS,
|
|
908
|
+
rowId: "dsh-plus-actual",
|
|
909
|
+
pkg: "@dsh-plus/actual",
|
|
910
|
+
component: ActualCard,
|
|
911
|
+
inject: () => ({
|
|
912
|
+
t: c.locale.bind(NS),
|
|
913
|
+
scope,
|
|
914
|
+
api
|
|
915
|
+
})
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
//#endregion
|
|
919
|
+
exports.apply = apply;
|
|
920
|
+
exports.inject = inject;
|
|
921
|
+
exports.name = name;
|
|
922
|
+
return module.exports;
|
|
923
|
+
}
|
|
924
|
+
});
|