@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/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dsh-plus/actual",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "dsh-plus service + UI plugin: Actual Budget 能力发现(@actual-app/cli help 树运行时派生 × 进程内 MCP tools/list)、actual agent 预设注册与配置卡片——零手工工具清单",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./client": {
|
|
14
|
+
"default": "./lib/client.js"
|
|
15
|
+
},
|
|
16
|
+
"./src/*": "./src/*",
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"lib",
|
|
21
|
+
"src"
|
|
22
|
+
],
|
|
23
|
+
"dsh": {
|
|
24
|
+
"client": {
|
|
25
|
+
"inject": [
|
|
26
|
+
"@deepseek-ai/dsh-client-locale",
|
|
27
|
+
"@deepseek-ai/dsh-client-ui-renderer",
|
|
28
|
+
"@deepseek-ai/dsh-api-remotes"
|
|
29
|
+
],
|
|
30
|
+
"platform": "web"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@modelcontextprotocol/client": "2.3.0",
|
|
35
|
+
"@dsh-plus/shared": "0.1.34",
|
|
36
|
+
"@dsh-plus/actual-mcp": "0.1.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@deepseek-ai/cordis": "^4.0.5-alpha.1",
|
|
40
|
+
"@deepseek-ai/dsh-agent-preset-registry": "^0.2.1-alpha.1",
|
|
41
|
+
"@deepseek-ai/schemastery": "^3.18.5-alpha.1"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@deepseek-ai/cordis": "4.0.5-alpha.1",
|
|
45
|
+
"@deepseek-ai/dsh-agent-preset-registry": "0.2.1-alpha.1",
|
|
46
|
+
"@deepseek-ai/schemastery": "3.18.5-alpha.1",
|
|
47
|
+
"@types/react": "~18.3.31",
|
|
48
|
+
"react": "^18.2.0"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
},
|
|
53
|
+
"license": "MIT",
|
|
54
|
+
"repository": {
|
|
55
|
+
"type": "git",
|
|
56
|
+
"url": "git+https://github.com/A-G-guy/dsh-plus.git",
|
|
57
|
+
"directory": "packages/actual"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "tsdown",
|
|
61
|
+
"watch": "tsdown --watch"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「Actual Budget 连接」配置卡片:经 injectPluginConfigCard 注册(插件页
|
|
3
|
+
* plugins.row.config / plugins.bundle.config,view 分发 summary/page)。
|
|
4
|
+
*
|
|
5
|
+
* 编辑范围只覆盖**非密钥**字段。密钥与高级字段没有编辑器,卡片内给出明确落点
|
|
6
|
+
* (见 i18n 的 secretsBody / advancedBody)——按《插件开发指南》,可配置项要么
|
|
7
|
+
* 有编辑器、要么写明为什么没有与改在哪里配。
|
|
8
|
+
*
|
|
9
|
+
* 字段按主题拆成两个小节组件,卡片本体只负责外壳、状态与保存动作。
|
|
10
|
+
* @module @dsh-plus/actual/client/card
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
CardChrome,
|
|
15
|
+
CardLoading,
|
|
16
|
+
CheckRow,
|
|
17
|
+
type NamespaceSettingsApi,
|
|
18
|
+
type PluginConfigViewProps,
|
|
19
|
+
type Scope,
|
|
20
|
+
SelectField,
|
|
21
|
+
TextField,
|
|
22
|
+
useNamespaceDraft,
|
|
23
|
+
} from '@dsh-plus/shared/client'
|
|
24
|
+
import type { ReactElement } from 'react'
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
type ConfigValue,
|
|
28
|
+
type Draft,
|
|
29
|
+
type DraftViolation,
|
|
30
|
+
draftFromValue,
|
|
31
|
+
patchFromDraft,
|
|
32
|
+
validateDraft,
|
|
33
|
+
} from './draft.ts'
|
|
34
|
+
import type { Translate } from './i18n.ts'
|
|
35
|
+
|
|
36
|
+
export interface CardProps extends PluginConfigViewProps {
|
|
37
|
+
t: Translate
|
|
38
|
+
scope: Scope
|
|
39
|
+
api: NamespaceSettingsApi
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 小节共用的 props。 */
|
|
43
|
+
interface SectionProps {
|
|
44
|
+
t: Translate
|
|
45
|
+
draft: Draft
|
|
46
|
+
disabled: boolean
|
|
47
|
+
violation: DraftViolation | undefined
|
|
48
|
+
onEdit(patch: Partial<Draft>): void
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 模块级稳定函数:参与 useNamespaceDraft 的依赖比较。 */
|
|
52
|
+
function fromValue(value: ConfigValue): Draft {
|
|
53
|
+
return draftFromValue(value)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function toPatch(draft: Draft): Record<string, unknown> {
|
|
57
|
+
return patchFromDraft(draft)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 条件式 invalid 属性:`exactOptionalPropertyTypes` 下不可显式传 `invalidLabel: undefined`,
|
|
62
|
+
* 只能按「有文案才带该键」构造。
|
|
63
|
+
*/
|
|
64
|
+
function violationProps(
|
|
65
|
+
active: boolean,
|
|
66
|
+
label: string,
|
|
67
|
+
): { invalid: boolean; invalidLabel?: string } {
|
|
68
|
+
return active ? { invalid: true, invalidLabel: label } : { invalid: false }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 说明段落(无编辑器的字段在此交代落点)。 */
|
|
72
|
+
function Note(props: {
|
|
73
|
+
title: string
|
|
74
|
+
body: string
|
|
75
|
+
snippet?: string
|
|
76
|
+
tail?: string
|
|
77
|
+
}): ReactElement {
|
|
78
|
+
return (
|
|
79
|
+
<div className="act-note">
|
|
80
|
+
<p className="act-noteTitle">{props.title}</p>
|
|
81
|
+
<p className="act-noteBody">{props.body}</p>
|
|
82
|
+
{props.snippet !== undefined ? <code className="act-snippet">{props.snippet}</code> : null}
|
|
83
|
+
{props.tail !== undefined ? <p className="act-noteBody">{props.tail}</p> : null}
|
|
84
|
+
</div>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 连接小节:服务器地址、预算、CLI 命令与版本策略。 */
|
|
89
|
+
function ConnectionSection(props: SectionProps): ReactElement {
|
|
90
|
+
const { t, draft, disabled, violation, onEdit } = props
|
|
91
|
+
return (
|
|
92
|
+
<>
|
|
93
|
+
<TextField
|
|
94
|
+
prefix="act"
|
|
95
|
+
id="act-server-url"
|
|
96
|
+
label={t('serverUrl')}
|
|
97
|
+
hint={t('serverUrlHint')}
|
|
98
|
+
value={draft.serverUrl}
|
|
99
|
+
disabled={disabled}
|
|
100
|
+
{...violationProps(violation?.field === 'serverUrl', t('serverUrlInvalid'))}
|
|
101
|
+
onEdit={(value) => onEdit({ serverUrl: value })}
|
|
102
|
+
/>
|
|
103
|
+
<TextField
|
|
104
|
+
prefix="act"
|
|
105
|
+
id="act-sync-id"
|
|
106
|
+
label={t('syncId')}
|
|
107
|
+
hint={t('syncIdHint')}
|
|
108
|
+
value={draft.syncId}
|
|
109
|
+
disabled={disabled}
|
|
110
|
+
onEdit={(value) => onEdit({ syncId: value })}
|
|
111
|
+
/>
|
|
112
|
+
<TextField
|
|
113
|
+
prefix="act"
|
|
114
|
+
id="act-cli-command"
|
|
115
|
+
label={t('cliCommand')}
|
|
116
|
+
hint={t('cliCommandHint')}
|
|
117
|
+
value={draft.cliCommand}
|
|
118
|
+
disabled={disabled}
|
|
119
|
+
{...violationProps(violation?.field === 'cliCommand', t('cliCommandInvalid'))}
|
|
120
|
+
onEdit={(value) => onEdit({ cliCommand: value })}
|
|
121
|
+
/>
|
|
122
|
+
<SelectField
|
|
123
|
+
prefix="act"
|
|
124
|
+
id="act-version-policy"
|
|
125
|
+
label={t('cliVersionPolicy')}
|
|
126
|
+
hint={t('cliVersionPolicyHint')}
|
|
127
|
+
value={draft.cliVersionPolicy}
|
|
128
|
+
options={[
|
|
129
|
+
{ value: 'warn', label: t('policyWarn') },
|
|
130
|
+
{ value: 'strict', label: t('policyStrict') },
|
|
131
|
+
]}
|
|
132
|
+
disabled={disabled}
|
|
133
|
+
onEdit={(value) => onEdit({ cliVersionPolicy: value === 'strict' ? 'strict' : 'warn' })}
|
|
134
|
+
/>
|
|
135
|
+
</>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** 行为小节:写确认、辅助工具与工具名前缀。 */
|
|
140
|
+
function PolicySection(props: SectionProps): ReactElement {
|
|
141
|
+
const { t, draft, disabled, violation, onEdit } = props
|
|
142
|
+
return (
|
|
143
|
+
<>
|
|
144
|
+
<CheckRow
|
|
145
|
+
prefix="act"
|
|
146
|
+
id="act-confirm-writes"
|
|
147
|
+
label={t('confirmWrites')}
|
|
148
|
+
checked={draft.confirmWrites}
|
|
149
|
+
disabled={disabled}
|
|
150
|
+
onEdit={(value) => onEdit({ confirmWrites: value })}
|
|
151
|
+
/>
|
|
152
|
+
<p className="act-noteBody">{t('confirmWritesHint')}</p>
|
|
153
|
+
<CheckRow
|
|
154
|
+
prefix="act"
|
|
155
|
+
id="act-aux-tools"
|
|
156
|
+
label={t('auxTools')}
|
|
157
|
+
checked={draft.auxTools}
|
|
158
|
+
disabled={disabled}
|
|
159
|
+
onEdit={(value) => onEdit({ auxTools: value })}
|
|
160
|
+
/>
|
|
161
|
+
<p className="act-noteBody">{t('auxToolsHint')}</p>
|
|
162
|
+
<TextField
|
|
163
|
+
prefix="act"
|
|
164
|
+
id="act-name-prefix"
|
|
165
|
+
label={t('namePrefix')}
|
|
166
|
+
hint={t('namePrefixHint')}
|
|
167
|
+
value={draft.namePrefix}
|
|
168
|
+
disabled={disabled}
|
|
169
|
+
{...violationProps(violation?.field === 'namePrefix', t('namePrefixInvalid'))}
|
|
170
|
+
onEdit={(value) => onEdit({ namePrefix: value })}
|
|
171
|
+
/>
|
|
172
|
+
</>
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function ActualCard(props: CardProps): ReactElement | string | null {
|
|
177
|
+
const { t, scope, api } = props
|
|
178
|
+
const { draft, dirty, saving, status, disabled, edit, reset, save } = useNamespaceDraft<
|
|
179
|
+
ConfigValue,
|
|
180
|
+
Draft
|
|
181
|
+
>({ scope, api, from: fromValue, patch: toPatch, saveFailedLabel: t('saveFailed') })
|
|
182
|
+
|
|
183
|
+
if (props.view === 'summary') return t('summaryLine')
|
|
184
|
+
if (draft === null) return <CardLoading prefix="act" text={t('loading')} />
|
|
185
|
+
|
|
186
|
+
const violation = validateDraft(draft)
|
|
187
|
+
const section: SectionProps = { t, draft, disabled, violation, onEdit: edit }
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<CardChrome
|
|
191
|
+
prefix="act"
|
|
192
|
+
title={t('title')}
|
|
193
|
+
description={t('description')}
|
|
194
|
+
statusBadge={{ text: t(draft.enabled ? 'enabledOn' : 'enabledOff'), on: draft.enabled }}
|
|
195
|
+
dirty={dirty}
|
|
196
|
+
dirtyLabel={t('unsaved')}
|
|
197
|
+
readOnlyNotice={disabled ? t('readOnly') : undefined}
|
|
198
|
+
status={status}
|
|
199
|
+
actions={[
|
|
200
|
+
{ key: 'discard', label: t('discard'), disabled: !dirty || saving, onClick: reset },
|
|
201
|
+
{
|
|
202
|
+
key: 'save',
|
|
203
|
+
label: t(saving ? 'saving' : 'save'),
|
|
204
|
+
variant: 'primary',
|
|
205
|
+
disabled: !dirty || saving || violation !== undefined || disabled,
|
|
206
|
+
onClick: save,
|
|
207
|
+
},
|
|
208
|
+
]}
|
|
209
|
+
>
|
|
210
|
+
<CheckRow
|
|
211
|
+
prefix="act"
|
|
212
|
+
id="act-enabled"
|
|
213
|
+
label={t('enabled')}
|
|
214
|
+
checked={draft.enabled}
|
|
215
|
+
disabled={disabled}
|
|
216
|
+
onEdit={(value) => edit({ enabled: value })}
|
|
217
|
+
/>
|
|
218
|
+
<p className="act-noteBody">{t('enabledHint')}</p>
|
|
219
|
+
<ConnectionSection {...section} />
|
|
220
|
+
<PolicySection {...section} />
|
|
221
|
+
<Note
|
|
222
|
+
title={t('secretsTitle')}
|
|
223
|
+
body={t('secretsBody')}
|
|
224
|
+
snippet={t('secretsSnippet')}
|
|
225
|
+
tail={t('secretsTail')}
|
|
226
|
+
/>
|
|
227
|
+
<Note title={t('advancedTitle')} body={t('advancedBody')} />
|
|
228
|
+
</CardChrome>
|
|
229
|
+
)
|
|
230
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 浏览器半入口:注册 locale 字典 + 注册「Actual Budget 连接」卡片
|
|
3
|
+
* (injectPluginConfigCard 两槽位:插件页 plugins.row.config /
|
|
4
|
+
* plugins.bundle.config)。
|
|
5
|
+
* 构建产物为 window.__ModuleLoader__.load({id, factory}) 形式的 CJS factory
|
|
6
|
+
* (包装见 tsdown.config.ts);样式沿用官方 data-plugin-css 约定,HMR 据此卸载。
|
|
7
|
+
* 配置读写经 ctx.remote.settings 直连(scope/api 来自 @dsh-plus/shared/client)。
|
|
8
|
+
* @module @dsh-plus/actual/client
|
|
9
|
+
*/
|
|
10
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
11
|
+
import {
|
|
12
|
+
createNamespaceApi,
|
|
13
|
+
createSettingsScope,
|
|
14
|
+
injectPluginConfigCard,
|
|
15
|
+
type PluginClientContext,
|
|
16
|
+
} from '@dsh-plus/shared/client'
|
|
17
|
+
|
|
18
|
+
import { SETTINGS_NS } from '../ns.ts'
|
|
19
|
+
import { ActualCard } from './card.tsx'
|
|
20
|
+
import { type DictKey, en, NS, zh } from './i18n.ts'
|
|
21
|
+
import { injectStyle } from './styles.ts'
|
|
22
|
+
|
|
23
|
+
export const name = 'dsh-plus-actual'
|
|
24
|
+
|
|
25
|
+
/** 浏览器半需要的 cordis 服务 key(loader 据此注入;package.json 的 dsh.client.inject 管包加载顺序)。 */
|
|
26
|
+
export const inject = ['slots', 'locale', 'remote', 'remote.settings'] as const
|
|
27
|
+
|
|
28
|
+
/** 宿主窄面收编在 @dsh-plus/shared/client;TKey 传入本包 DictKey 使键名受检。 */
|
|
29
|
+
type ClientContext = PluginClientContext<DictKey>
|
|
30
|
+
|
|
31
|
+
export function apply(ctx: Context): void {
|
|
32
|
+
const c = ctx as unknown as ClientContext
|
|
33
|
+
const tag = injectStyle()
|
|
34
|
+
c.effect(
|
|
35
|
+
() => () => {
|
|
36
|
+
tag?.remove()
|
|
37
|
+
},
|
|
38
|
+
'actual: style',
|
|
39
|
+
)
|
|
40
|
+
c.effect(() => c.locale.register(NS, { zh, en }), 'actual: locale')
|
|
41
|
+
const scope = createSettingsScope(c, SETTINGS_NS, 'actual: settings scope')
|
|
42
|
+
const api = createNamespaceApi(c.get('remote').settings, SETTINGS_NS)
|
|
43
|
+
injectPluginConfigCard(c.slots, {
|
|
44
|
+
ns: SETTINGS_NS,
|
|
45
|
+
rowId: 'dsh-plus-actual',
|
|
46
|
+
pkg: '@dsh-plus/actual',
|
|
47
|
+
component: ActualCard,
|
|
48
|
+
inject: () => ({ t: c.locale.bind(NS), scope, api }),
|
|
49
|
+
})
|
|
50
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 卡片草稿的纯转换与校验(无 JSX,`node --test` 可直接测)。
|
|
3
|
+
*
|
|
4
|
+
* `cliCommand` 在配置里是 `string[]`(argv 前缀),在卡片里是空格分隔的文本——
|
|
5
|
+
* 这层转换是卡片唯一容易出错的地方,因此单独成模块并逐例钉住:**不做 shell
|
|
6
|
+
* 解析**,引号按字面字符处理并直接判非法,避免"看起来支持引号、实际把引号当
|
|
7
|
+
* 参数一部分"的静默错配。
|
|
8
|
+
* @module @dsh-plus/actual/client/draft
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** CLI 与服务端版本不一致时的处置(与宿主 schema 的 union 一致)。 */
|
|
12
|
+
export type CliVersionPolicy = 'warn' | 'strict'
|
|
13
|
+
|
|
14
|
+
/** settings 命名空间里本卡片负责的字段(其余字段不出现在卡片中)。 */
|
|
15
|
+
export interface ConfigValue {
|
|
16
|
+
enabled: boolean
|
|
17
|
+
serverUrl: string
|
|
18
|
+
syncId: string
|
|
19
|
+
cliCommand: string[]
|
|
20
|
+
cliVersionPolicy: CliVersionPolicy
|
|
21
|
+
confirmWrites: boolean
|
|
22
|
+
auxTools: boolean
|
|
23
|
+
namePrefix: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 编辑草稿:`cliCommand` 以文本承载(空格分隔的 argv)。 */
|
|
27
|
+
export interface Draft {
|
|
28
|
+
enabled: boolean
|
|
29
|
+
serverUrl: string
|
|
30
|
+
syncId: string
|
|
31
|
+
cliCommand: string
|
|
32
|
+
cliVersionPolicy: CliVersionPolicy
|
|
33
|
+
confirmWrites: boolean
|
|
34
|
+
auxTools: boolean
|
|
35
|
+
namePrefix: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 校验失败的字段与文案键。 */
|
|
39
|
+
export interface DraftViolation {
|
|
40
|
+
field: 'serverUrl' | 'cliCommand' | 'namePrefix'
|
|
41
|
+
messageKey: 'serverUrlInvalid' | 'cliCommandInvalid' | 'namePrefixInvalid'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** argv → 卡片文本(空格分隔)。 */
|
|
45
|
+
export function argvToText(argv: readonly string[]): string {
|
|
46
|
+
return argv.join(' ')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 卡片文本 → argv;纯空白视为「自动探测」(空数组)。 */
|
|
50
|
+
export function textToArgv(text: string): string[] {
|
|
51
|
+
const trimmed = text.trim()
|
|
52
|
+
return trimmed === '' ? [] : trimmed.split(/\s+/)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 命名空间解析值 → 草稿。 */
|
|
56
|
+
export function draftFromValue(value: ConfigValue): Draft {
|
|
57
|
+
return {
|
|
58
|
+
enabled: value.enabled,
|
|
59
|
+
serverUrl: value.serverUrl,
|
|
60
|
+
syncId: value.syncId,
|
|
61
|
+
cliCommand: argvToText(value.cliCommand),
|
|
62
|
+
cliVersionPolicy: value.cliVersionPolicy,
|
|
63
|
+
confirmWrites: value.confirmWrites,
|
|
64
|
+
auxTools: value.auxTools,
|
|
65
|
+
namePrefix: value.namePrefix,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 草稿 → 命名空间补丁(仅本卡片负责的字段)。 */
|
|
70
|
+
export function patchFromDraft(draft: Draft): Record<string, unknown> {
|
|
71
|
+
return {
|
|
72
|
+
enabled: draft.enabled,
|
|
73
|
+
serverUrl: draft.serverUrl.trim(),
|
|
74
|
+
syncId: draft.syncId.trim(),
|
|
75
|
+
cliCommand: textToArgv(draft.cliCommand),
|
|
76
|
+
cliVersionPolicy: draft.cliVersionPolicy,
|
|
77
|
+
confirmWrites: draft.confirmWrites,
|
|
78
|
+
auxTools: draft.auxTools,
|
|
79
|
+
namePrefix: draft.namePrefix.trim(),
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 首个校验问题;全部合法返回 undefined(调用方据此禁用保存)。 */
|
|
84
|
+
export function validateDraft(draft: Draft): DraftViolation | undefined {
|
|
85
|
+
if (!/^https?:\/\/\S+$/.test(draft.serverUrl.trim())) {
|
|
86
|
+
return { field: 'serverUrl', messageKey: 'serverUrlInvalid' }
|
|
87
|
+
}
|
|
88
|
+
if (/["']/.test(draft.cliCommand)) {
|
|
89
|
+
return { field: 'cliCommand', messageKey: 'cliCommandInvalid' }
|
|
90
|
+
}
|
|
91
|
+
if (!/^[A-Za-z0-9_-]*$/.test(draft.namePrefix.trim())) {
|
|
92
|
+
return { field: 'namePrefix', messageKey: 'namePrefixInvalid' }
|
|
93
|
+
}
|
|
94
|
+
return undefined
|
|
95
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置卡片文案(zh/en)。经 ctx.locale.register 注册、bind 取用。
|
|
3
|
+
* 公共键(save/discard/unsaved 等)来自 shared 的 common 字典,本文件只维护业务键。
|
|
4
|
+
*
|
|
5
|
+
* 两处说明文案是有意为之的「落点」:密钥与高级字段都没有编辑器,必须在卡片内
|
|
6
|
+
* 写清「为什么没有、该去哪里配」,否则就是留给用户的空承诺。
|
|
7
|
+
* @module @dsh-plus/actual/client/i18n
|
|
8
|
+
*/
|
|
9
|
+
import { commonEn, commonZh, mergeDict } from '@dsh-plus/shared/client'
|
|
10
|
+
|
|
11
|
+
export const NS = 'dsh-plus-actual'
|
|
12
|
+
|
|
13
|
+
const ownZh = {
|
|
14
|
+
title: 'Actual Budget 连接',
|
|
15
|
+
description:
|
|
16
|
+
'配置本机 Actual 同步服务器与 CLI 行为。改动热生效,无需重载:连接相关字段会触发能力重新发现。',
|
|
17
|
+
summaryLine: 'Actual 服务器地址、预算 Sync ID、CLI 与工具策略。',
|
|
18
|
+
|
|
19
|
+
enabled: '启用 actual 预设',
|
|
20
|
+
enabledHint: '关闭即不注册该预设(等价插件未安装);已开的会话保持原组合,新会话起不再出现。',
|
|
21
|
+
|
|
22
|
+
serverUrl: '同步服务器地址',
|
|
23
|
+
serverUrlHint:
|
|
24
|
+
'自托管默认 http://127.0.0.1:5006。服务端版本从 <地址>/info 读取,用于版本一致性告警。',
|
|
25
|
+
serverUrlInvalid: '必须是 http(s):// 开头的完整地址',
|
|
26
|
+
|
|
27
|
+
syncId: '预算 Sync ID',
|
|
28
|
+
syncIdHint:
|
|
29
|
+
'Actual 客户端 → 设置 → 显示高级设置 → Sync ID。留空则读环境变量 ACTUAL_SYNC_ID。多数命令都需要它。',
|
|
30
|
+
|
|
31
|
+
cliCommand: 'CLI 命令',
|
|
32
|
+
cliCommandHint:
|
|
33
|
+
'留空 = 自动探测(ACTUAL_CLI 环境变量 → PATH 上的 actual → 包内 @actual-app/cli)。需要指定具体副本时填完整 argv,如 node /opt/actual/dist/cli.js(空格分隔,不支持引号)。',
|
|
34
|
+
cliCommandInvalid: '不支持引号:请直接以空格分隔各个参数',
|
|
35
|
+
|
|
36
|
+
cliVersionPolicy: '版本不一致处置',
|
|
37
|
+
cliVersionPolicyHint:
|
|
38
|
+
'CLI 与服务端 major.minor 不一致时:warn 告警继续(Actual 服务端只做同步中继,通常仍可用);strict 直接让发现失败。',
|
|
39
|
+
policyWarn: 'warn —— 告警并继续',
|
|
40
|
+
policyStrict: 'strict —— 直接失败',
|
|
41
|
+
|
|
42
|
+
confirmWrites: '写操作需确认',
|
|
43
|
+
confirmWritesHint:
|
|
44
|
+
'经 DSH 审批策略生效:会话为「每次询问」时弹审批窗;「完全权限」下自动通过(绝不静默拒绝)。',
|
|
45
|
+
auxTools: '挂载辅助工具',
|
|
46
|
+
auxToolsHint: '额外提供 web 检索、结构化提问与待办跟踪三件套;它们不改动预算数据。',
|
|
47
|
+
|
|
48
|
+
namePrefix: '工具名前缀',
|
|
49
|
+
namePrefixHint:
|
|
50
|
+
'模型可见的工具名 = 前缀 + 命令族名(如 actual_accounts)。留空则直接用裸族名——query、server 等过于通用,不建议。',
|
|
51
|
+
namePrefixInvalid: '只允许字母、数字、下划线与连字符',
|
|
52
|
+
|
|
53
|
+
secretsTitle: '服务器口令',
|
|
54
|
+
secretsBody:
|
|
55
|
+
'口令属密钥:它不进用户层配置,卡片也不提供输入框(写进来会被脱敏遮蔽,等于白填)。请在 profile 的 cordis.patch.yml 里做行级注入,口令本身放环境变量:',
|
|
56
|
+
secretsSnippet:
|
|
57
|
+
'- id: dsh-plus-actual\n config:\n syncId: !!js process.env.ACTUAL_SYNC_ID\n password: !!js process.env.ACTUAL_PASSWORD',
|
|
58
|
+
secretsTail:
|
|
59
|
+
'把 ACTUAL_PASSWORD 放进 dsh 服务进程的环境(systemd 的 EnvironmentFile)。官方 CLI 还支持 ACTUAL_PASSWORD_FILE——更适合密钥文件;encrypted 预算另需 ACTUAL_ENCRYPTION_PASSWORD。改动环境变量后需重启 dsh 服务。',
|
|
60
|
+
|
|
61
|
+
advancedTitle: '其余字段',
|
|
62
|
+
advancedBody:
|
|
63
|
+
'以下字段只在 profile 的 cordis.patch.yml 里配置:预算目录与超时(dataDir / cacheTtl / lockTimeout / toolCallTimeoutMs)、工具暴露与读写判定(allow / deny / readActions / alwaysAsk / readTools)、预设显示与提示词(name / description / order / personaPrefix / includeRuntimeContext)。',
|
|
64
|
+
} as const
|
|
65
|
+
|
|
66
|
+
const ownEn = {
|
|
67
|
+
title: 'Actual Budget connection',
|
|
68
|
+
description:
|
|
69
|
+
'Configure the local Actual sync server and CLI behaviour. Changes apply hot — connection fields trigger a fresh capability discovery.',
|
|
70
|
+
summaryLine: 'Actual server address, budget sync ID, CLI and tool policy.',
|
|
71
|
+
|
|
72
|
+
enabled: 'Enable the actual preset',
|
|
73
|
+
enabledHint:
|
|
74
|
+
'Off stops registering the preset (equivalent to not installed). Live sessions keep their composition; new ones will not see it.',
|
|
75
|
+
|
|
76
|
+
serverUrl: 'Sync server URL',
|
|
77
|
+
serverUrlHint:
|
|
78
|
+
'Self-hosted default is http://127.0.0.1:5006. The server version is read from <url>/info for the version-mismatch warning.',
|
|
79
|
+
serverUrlInvalid: 'Must be a full http(s):// URL',
|
|
80
|
+
|
|
81
|
+
syncId: 'Budget sync ID',
|
|
82
|
+
syncIdHint:
|
|
83
|
+
'Actual client → Settings → Show advanced settings → Sync ID. Empty falls back to the ACTUAL_SYNC_ID environment variable. Most commands need it.',
|
|
84
|
+
|
|
85
|
+
cliCommand: 'CLI command',
|
|
86
|
+
cliCommandHint:
|
|
87
|
+
'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).',
|
|
88
|
+
cliCommandInvalid: 'Quotes are not supported: separate arguments with spaces',
|
|
89
|
+
|
|
90
|
+
cliVersionPolicy: 'On version mismatch',
|
|
91
|
+
cliVersionPolicyHint:
|
|
92
|
+
'When CLI and server major.minor differ: warn keeps going (the Actual server is a sync relay, usually still fine); strict fails discovery outright.',
|
|
93
|
+
policyWarn: 'warn — alert and continue',
|
|
94
|
+
policyStrict: 'strict — fail outright',
|
|
95
|
+
|
|
96
|
+
confirmWrites: 'Confirm write operations',
|
|
97
|
+
confirmWritesHint:
|
|
98
|
+
'Routed through the DSH approval policy: prompts when the session asks every time, passes automatically under full permission (never silently denies).',
|
|
99
|
+
auxTools: 'Mount auxiliary tools',
|
|
100
|
+
auxToolsHint:
|
|
101
|
+
'Adds web search, structured questions and todo tracking; none of them touch budget data.',
|
|
102
|
+
|
|
103
|
+
namePrefix: 'Tool name prefix',
|
|
104
|
+
namePrefixHint:
|
|
105
|
+
'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.',
|
|
106
|
+
namePrefixInvalid: 'Letters, digits, underscore and hyphen only',
|
|
107
|
+
|
|
108
|
+
secretsTitle: 'Server password',
|
|
109
|
+
secretsBody:
|
|
110
|
+
'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:',
|
|
111
|
+
secretsSnippet:
|
|
112
|
+
'- id: dsh-plus-actual\n config:\n syncId: !!js process.env.ACTUAL_SYNC_ID\n password: !!js process.env.ACTUAL_PASSWORD',
|
|
113
|
+
secretsTail:
|
|
114
|
+
'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.',
|
|
115
|
+
|
|
116
|
+
advancedTitle: 'Remaining fields',
|
|
117
|
+
advancedBody:
|
|
118
|
+
'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).',
|
|
119
|
+
} as const
|
|
120
|
+
|
|
121
|
+
export type DictKey = keyof typeof commonZh | keyof typeof ownZh
|
|
122
|
+
|
|
123
|
+
/** 卡片共用的翻译函数类型(slot 注入的 bind 结果按此消费)。 */
|
|
124
|
+
export type Translate = (key: DictKey) => string
|
|
125
|
+
|
|
126
|
+
// 标注为 Record<DictKey, string>:en 缺任一键即编译期报错(中英强制对齐)。
|
|
127
|
+
export const zh: Record<DictKey, string> = mergeDict(commonZh, ownZh)
|
|
128
|
+
export const en: Record<DictKey, string> = mergeDict(commonEn, ownEn)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置卡片样式:基础规则走 @dsh-plus/shared/client 的 cardCss('act'),
|
|
3
|
+
* 本文件只补插件特有规则(说明段落与代码片段)。
|
|
4
|
+
* 沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载)。
|
|
5
|
+
* @module @dsh-plus/actual/client/styles
|
|
6
|
+
*/
|
|
7
|
+
import { cardCss, injectCardStyle } from '@dsh-plus/shared/client'
|
|
8
|
+
|
|
9
|
+
export const PLUGIN_ID = '@dsh-plus/actual'
|
|
10
|
+
|
|
11
|
+
const extraCss = `
|
|
12
|
+
.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)}
|
|
13
|
+
.act-noteTitle{margin:0 0 6px;font-size:13px;font-weight:600}
|
|
14
|
+
.act-noteBody{margin:0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.6}
|
|
15
|
+
.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}
|
|
16
|
+
`
|
|
17
|
+
|
|
18
|
+
export const cardCssAll = cardCss('act', extraCss)
|
|
19
|
+
|
|
20
|
+
/** 幂等注入样式标签;返回标签(已存在或环境无 document 时为 null)。 */
|
|
21
|
+
export function injectStyle(): HTMLStyleElement | null {
|
|
22
|
+
return injectCardStyle(PLUGIN_ID, cardCssAll)
|
|
23
|
+
}
|