@dingpenghui/dsh-obsidian-sync 3.4.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/README.md +131 -0
- package/cordis.patch.yml +23 -0
- package/lib/client.js +372 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.ts +41 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +810 -0
- package/lib/index.js.map +1 -0
- package/package.json +91 -0
- package/src/client/PluginDetailPage.module.css +81 -0
- package/src/client/css-module.d.ts +9 -0
- package/src/client/index.ts +109 -0
- package/src/client/plugin-detail-controller.ts +137 -0
- package/src/client/plugin-detail-page.tsx +106 -0
- package/src/index.ts +897 -0
- package/src/locales/index.ts +40 -0
- package/src/shared.ts +24 -0
- package/src/slot-contract.d.ts +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# DSH Obsidian Sync
|
|
2
|
+
|
|
3
|
+
> DSH Cordis 插件:按需搜索 Obsidian 知识库 + 幂等归档 DSH 会话(零 token 注入)
|
|
4
|
+
|
|
5
|
+
## 设计理念
|
|
6
|
+
|
|
7
|
+
纯模型 Tool 按需调用,不注入系统提示词。默认 token 成本 ≈ 两个 Tool 的 schema。
|
|
8
|
+
|
|
9
|
+
- 进程内 fs 服务(resolve/stat/listDir/readText/writeText),不起子进程
|
|
10
|
+
- 倒排索引 + 60s 增量重建(dirty 标记,跳过 .obsidian/.git)
|
|
11
|
+
- CJK bigram 分词 + 全文索引 + IDF 加权排序
|
|
12
|
+
- 按 `date + shortId` 幂等 upsert(标题变更不影响去重)
|
|
13
|
+
- 笔记写入 `vault/04-Archive/`,YAML frontmatter + Obsidian 原生标签
|
|
14
|
+
- 自动挂 `DSH-会话归档-索引.md` 的按日期段(双井号边界 + 同 shortId 去重)
|
|
15
|
+
|
|
16
|
+
## 提供的 Tool
|
|
17
|
+
|
|
18
|
+
### `obsidian.search`
|
|
19
|
+
|
|
20
|
+
在 Obsidian 知识库中按关键词搜索相关笔记(中英文均可,自动 CJK bigram 分词)。
|
|
21
|
+
|
|
22
|
+
```jsonc
|
|
23
|
+
// 输入
|
|
24
|
+
{ "topic": "npm 发布 2FA", "limit": 5 }
|
|
25
|
+
|
|
26
|
+
// 输出
|
|
27
|
+
{
|
|
28
|
+
"ok": true,
|
|
29
|
+
"matches": [
|
|
30
|
+
{
|
|
31
|
+
"file": "04-Archive/2026-09-12-3073121a-…",
|
|
32
|
+
"score": 3.72,
|
|
33
|
+
"snippet": "npm 2FA 查证:TOTP 已废弃…",
|
|
34
|
+
"size": 4200,
|
|
35
|
+
"matchReason": "npm pub lis 2fa"
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"count": 5,
|
|
39
|
+
"vaultPath": "E:/dsh-workspace/obsidian-vault"
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### `obsidian.sync_session`
|
|
44
|
+
|
|
45
|
+
把当前 DSH 会话幂等归档到 Obsidian vault。摘要未变则跳过写盘。
|
|
46
|
+
|
|
47
|
+
```jsonc
|
|
48
|
+
// 输入
|
|
49
|
+
{
|
|
50
|
+
"session_id": "session-76bb60db-bdde-…",
|
|
51
|
+
"title": "dsh-obsidian-sync 优化与发布",
|
|
52
|
+
"summary": "用户需求:… 过程要点:… 交付物:…",
|
|
53
|
+
"tags": ["dsh", "obsidian", "cordis"],
|
|
54
|
+
"related": ["2026-09-11-a9095879-dsh-tool-agnes工具升级与发布.md"],
|
|
55
|
+
"raw_log": "C:\\Users\\dph\\.dsh\\sessions\\--E-dsh-workspace--\\session-76bb60db\\session.v3.jsonl.zstd"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 输出(首次写入)
|
|
59
|
+
{ "ok": true, "skipped": false, "file": "04-Archive/2026-09-13-76bb60db-…", "index": "updated" }
|
|
60
|
+
|
|
61
|
+
// 输出(摘要未变)
|
|
62
|
+
{ "ok": true, "skipped": true, "file": "…", "reason": "summary unchanged", "index": "unchanged" }
|
|
63
|
+
|
|
64
|
+
// 输出(失败)
|
|
65
|
+
{ "ok": false, "error": "writeText failed: …", "code": "WRITE_FAILED" }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 安装
|
|
69
|
+
|
|
70
|
+
### 方式一:从 npm 安装(推荐)
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
dsh plugin --profile web add @dingpenghui/dsh-obsidian-sync
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 方式二:从本地路径安装
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
cd dsh-obsidian-sync
|
|
80
|
+
pnpm install
|
|
81
|
+
pnpm build
|
|
82
|
+
dsh plugin --profile web add .
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## 配置
|
|
86
|
+
|
|
87
|
+
在 `~/.dsh/settings.yaml` 中添加:
|
|
88
|
+
|
|
89
|
+
```yaml
|
|
90
|
+
obsidian-sync:
|
|
91
|
+
vaultPath: "E:/dsh-workspace/obsidian-vault"
|
|
92
|
+
searchEnabled: true
|
|
93
|
+
indexRefreshMs: 60000
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
所有字段均有默认值,可不配置直接挂载。
|
|
97
|
+
|
|
98
|
+
## 开发
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pnpm install
|
|
102
|
+
pnpm build # tsdown 构建 → lib/
|
|
103
|
+
pnpm typecheck # tsc 类型检查
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 测试
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
node scripts/test-tokenize.mjs # CJK bigram 分词
|
|
110
|
+
node scripts/test-idempotent.mjs # 幂等 upsert
|
|
111
|
+
node scripts/test-index-boundary.mjs # 索引边界插入
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## 发布
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pnpm version patch
|
|
118
|
+
git push origin main --tags
|
|
119
|
+
npm publish
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`.npmrc` 不入版本库(含 npm access token),发布前需本地配置。
|
|
123
|
+
|
|
124
|
+
## 许可证
|
|
125
|
+
|
|
126
|
+
MIT
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
**作者**: Dingpenghui-good
|
|
131
|
+
**仓库**: https://github.com/Dingpenghui-good/dsh-obsidian-sync
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# DSH Obsidian Sync plugin configuration
|
|
2
|
+
# Install: dsh plugin --profile web add <path-to-plugin>
|
|
3
|
+
#
|
|
4
|
+
# The row id IS the settings namespace: the settings framework keys every
|
|
5
|
+
# descriptor by the entry's id, and the client addresses the form through
|
|
6
|
+
# `configForms.get('dsh-obsidian-sync')`. The schema defaults
|
|
7
|
+
# (vaultPath: E:/dsh-workspace/obsidian-vault, searchEnabled: true,
|
|
8
|
+
# indexRefreshMs: 60000) materialize when the row ships no config.
|
|
9
|
+
#
|
|
10
|
+
# Prerequisites (editable on the plugin detail page, volatile HMR live apply):
|
|
11
|
+
# vaultPath - absolute path to an existing Obsidian vault (PARA layout)
|
|
12
|
+
# searchEnabled - whether to register the inverted-index search tools
|
|
13
|
+
# indexRefreshMs - index rebuild interval in ms (>= 5000)
|
|
14
|
+
#
|
|
15
|
+
# A bundle patch replaces the WHOLE config of its target row, so every key
|
|
16
|
+
# the row owns must be restated here.
|
|
17
|
+
- insert:
|
|
18
|
+
- id: dsh-obsidian-sync
|
|
19
|
+
name: '@dingpenghui/dsh-obsidian-sync'
|
|
20
|
+
config:
|
|
21
|
+
vaultPath: 'E:/dsh-workspace/obsidian-vault'
|
|
22
|
+
searchEnabled: true
|
|
23
|
+
indexRefreshMs: 60000
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@dingpenghui/dsh-obsidian-sync",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
//#region src/client/plugin-detail-controller.ts
|
|
10
|
+
/** vaultPath 字段的 format/parse 规范:非空绝对/相对路径,空串清除回默认。 */
|
|
11
|
+
function vaultPathFieldSpec() {
|
|
12
|
+
return {
|
|
13
|
+
field: "vaultPath",
|
|
14
|
+
format: (value) => typeof value === "string" ? value : "",
|
|
15
|
+
parse: (text) => {
|
|
16
|
+
const trimmed = text.trim();
|
|
17
|
+
if (trimmed === "") return { kind: "clear" };
|
|
18
|
+
return {
|
|
19
|
+
kind: "set",
|
|
20
|
+
value: trimmed
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** indexRefreshMs 字段的 format/parse 规范:≥5000 的正整数毫秒。 */
|
|
26
|
+
function indexRefreshMsFieldSpec() {
|
|
27
|
+
return {
|
|
28
|
+
field: "indexRefreshMs",
|
|
29
|
+
format: (value) => typeof value === "number" ? String(value) : "",
|
|
30
|
+
parse: (text) => {
|
|
31
|
+
const trimmed = text.trim();
|
|
32
|
+
if (trimmed === "") return { kind: "clear" };
|
|
33
|
+
const n = Number(trimmed);
|
|
34
|
+
if (!Number.isFinite(n) || n < 5e3 || !Number.isInteger(n)) return void 0;
|
|
35
|
+
return {
|
|
36
|
+
kind: "set",
|
|
37
|
+
value: n
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* searchEnabled 字段的 format/parse 规范:布尔开关。
|
|
44
|
+
* 详情页以 Switch 控件呈现,`format` 把布尔值映射为 'true'/'false' 文本
|
|
45
|
+
* (与页面 `searchEnabled.text === 'true'` 的读法保持一致),
|
|
46
|
+
* `parse` 把 'true'/'false' 文本解析回布尔值;空串清除回默认(true)。
|
|
47
|
+
* 必须注册进 SettingsFormModel 的 specs —— 否则 `field('searchEnabled')`
|
|
48
|
+
* 在 projection() 里抛 "plugin card has no field searchEnabled"。
|
|
49
|
+
*/
|
|
50
|
+
function searchEnabledFieldSpec() {
|
|
51
|
+
return {
|
|
52
|
+
field: "searchEnabled",
|
|
53
|
+
format: (value) => typeof value === "boolean" ? String(value) : "",
|
|
54
|
+
parse: (text) => {
|
|
55
|
+
const trimmed = text.trim().toLowerCase();
|
|
56
|
+
if (trimmed === "true") return {
|
|
57
|
+
kind: "set",
|
|
58
|
+
value: true
|
|
59
|
+
};
|
|
60
|
+
if (trimmed === "false") return {
|
|
61
|
+
kind: "set",
|
|
62
|
+
value: false
|
|
63
|
+
};
|
|
64
|
+
if (trimmed === "") return { kind: "clear" };
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** 把一个宿主 settings 命名空间桥接到详情页的 staged form。 */
|
|
69
|
+
function asFormScope(scope) {
|
|
70
|
+
return {
|
|
71
|
+
getSnapshot: () => {
|
|
72
|
+
const s = scope.getSnapshot();
|
|
73
|
+
return {
|
|
74
|
+
status: s.status,
|
|
75
|
+
value: s.value,
|
|
76
|
+
base: s.base,
|
|
77
|
+
user: s.user,
|
|
78
|
+
writable: s.writable,
|
|
79
|
+
revision: s.revision
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
subscribe: (listener) => scope.subscribe(listener),
|
|
83
|
+
mutate: (ops, expectedRevision) => scope.mutate(ops, expectedRevision)
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** 把命名空间的 `ConfigForm` 适配为详情页的 staged form。 */
|
|
87
|
+
var ObsidianSyncPageController = class {
|
|
88
|
+
form;
|
|
89
|
+
store;
|
|
90
|
+
/** @param scope - 共享配置表单(命名空间的 ConfigForm)。 */
|
|
91
|
+
constructor(scope) {
|
|
92
|
+
this.form = new _deepseek_ai_dsh_client_ui_primitives.SettingsFormModel(asFormScope(scope), [
|
|
93
|
+
vaultPathFieldSpec(),
|
|
94
|
+
searchEnabledFieldSpec(),
|
|
95
|
+
indexRefreshMsFieldSpec()
|
|
96
|
+
]);
|
|
97
|
+
this.store = this.form.bind(() => this.projection());
|
|
98
|
+
}
|
|
99
|
+
projection() {
|
|
100
|
+
return {
|
|
101
|
+
...this.form.shell(),
|
|
102
|
+
vaultPath: this.form.field("vaultPath"),
|
|
103
|
+
searchEnabled: this.form.field("searchEnabled"),
|
|
104
|
+
indexRefreshMs: this.form.field("indexRefreshMs")
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** 构建详情页 slot 注册注入的面。 */
|
|
108
|
+
inject() {
|
|
109
|
+
return {
|
|
110
|
+
hooks: { obsidianSyncPage: this.store },
|
|
111
|
+
...this.form.actions()
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** 释放表单订阅。 */
|
|
115
|
+
dispose() {
|
|
116
|
+
this.form.dispose();
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region \0dsh-css:E:\dsh-workspace\dsh-obsidian-sync\src\client\PluginDetailPage.module.css.mjs
|
|
121
|
+
const css = ".jWPSbW_page{flex-direction:column;gap:8px;padding-bottom:16px;display:flex}.jWPSbW_field{border-bottom:1px solid var(--dsw-alias-border-l2);flex-direction:column;gap:8px;padding:16px 0;display:flex}.jWPSbW_fieldText{flex-direction:column;gap:4px;display:flex}.jWPSbW_title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}.jWPSbW_subHint{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:400;line-height:18px}.jWPSbW_hint{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));padding:8px 0 0;font-size:12px;font-weight:400;line-height:18px}.jWPSbW_gitHint{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));border-top:1px dashed var(--dsw-alias-border-l2);margin-top:8px;padding:8px 0 0;font-size:12px;font-weight:400;line-height:18px}.jWPSbW_input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-l1);width:100%;color:var(--dsw-alias-label-primary);border-radius:6px;outline:none;padding:8px 12px;font-family:inherit;font-size:13px;transition:border-color .15s}.jWPSbW_input:focus{border-color:var(--dsw-alias-accent-l1)}.jWPSbW_input:disabled{opacity:.5;cursor:not-allowed}.jWPSbW_inputSmall{width:120px;}";
|
|
122
|
+
const tagId = "@dingpenghui/dsh-obsidian-sync/PluginDetailPage.module.css";
|
|
123
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
124
|
+
const tag = document.createElement("style");
|
|
125
|
+
tag.dataset.plugin = "@dingpenghui/dsh-obsidian-sync";
|
|
126
|
+
tag.dataset.pluginCss = tagId;
|
|
127
|
+
tag.textContent = css;
|
|
128
|
+
document.head.appendChild(tag);
|
|
129
|
+
}
|
|
130
|
+
var PluginDetailPage_module_css_default = {
|
|
131
|
+
"hint": "jWPSbW_hint",
|
|
132
|
+
"inputSmall": "jWPSbW_inputSmall",
|
|
133
|
+
"subHint": "jWPSbW_subHint",
|
|
134
|
+
"title": "jWPSbW_title",
|
|
135
|
+
"input": "jWPSbW_input",
|
|
136
|
+
"page": "jWPSbW_page",
|
|
137
|
+
"fieldText": "jWPSbW_fieldText",
|
|
138
|
+
"field": "jWPSbW_field",
|
|
139
|
+
"gitHint": "jWPSbW_gitHint"
|
|
140
|
+
};
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region src/client/plugin-detail-page.tsx
|
|
143
|
+
/** 从 locale 字典派生 SettingsForm 的标签文案。 */
|
|
144
|
+
function formLabels(t) {
|
|
145
|
+
return {
|
|
146
|
+
unavailable: t("form.unavailable"),
|
|
147
|
+
readOnly: t("form.readOnly"),
|
|
148
|
+
saveFailed: t("form.saveFailed"),
|
|
149
|
+
save: t("form.save"),
|
|
150
|
+
saving: t("form.saving")
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* 渲染详情页:一行摘要(`view: 'summary'`)或前提条件表单(`view: 'page'`)。
|
|
155
|
+
* @param props - 视图、locale 文案、表单快照与动作。
|
|
156
|
+
* @returns 摘要文字,或带保存控件的前提条件表单。
|
|
157
|
+
*/
|
|
158
|
+
function ObsidianSyncDetailPage(props) {
|
|
159
|
+
const { t } = props;
|
|
160
|
+
const state = props.useObsidianSyncPage((snapshot) => snapshot);
|
|
161
|
+
if (props.view === "summary") return t("description");
|
|
162
|
+
const disabled = !state.writable;
|
|
163
|
+
const vaultPath = state.vaultPath;
|
|
164
|
+
const searchEnabled = state.searchEnabled;
|
|
165
|
+
const indexRefreshMs = state.indexRefreshMs;
|
|
166
|
+
const searchOn = searchEnabled.text !== "" ? searchEnabled.text === "true" : true;
|
|
167
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SettingsForm, {
|
|
168
|
+
labels: formLabels(t),
|
|
169
|
+
state,
|
|
170
|
+
onSave: props.save,
|
|
171
|
+
onDiscard: props.discard,
|
|
172
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
173
|
+
className: PluginDetailPage_module_css_default.page,
|
|
174
|
+
children: [
|
|
175
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
176
|
+
className: PluginDetailPage_module_css_default.hint,
|
|
177
|
+
children: t("page.vaultPath.hint")
|
|
178
|
+
}),
|
|
179
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
180
|
+
className: PluginDetailPage_module_css_default.field,
|
|
181
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
182
|
+
className: PluginDetailPage_module_css_default.fieldText,
|
|
183
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
184
|
+
className: PluginDetailPage_module_css_default.title,
|
|
185
|
+
children: t("page.vaultPath.label")
|
|
186
|
+
})
|
|
187
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
188
|
+
type: "text",
|
|
189
|
+
className: PluginDetailPage_module_css_default.input,
|
|
190
|
+
value: vaultPath.text,
|
|
191
|
+
placeholder: "E:/dsh-workspace/obsidian-vault",
|
|
192
|
+
onChange: (e) => {
|
|
193
|
+
props.edit("vaultPath", e.target.value);
|
|
194
|
+
},
|
|
195
|
+
disabled
|
|
196
|
+
})]
|
|
197
|
+
}),
|
|
198
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
199
|
+
className: PluginDetailPage_module_css_default.field,
|
|
200
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
201
|
+
className: PluginDetailPage_module_css_default.fieldText,
|
|
202
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
203
|
+
className: PluginDetailPage_module_css_default.title,
|
|
204
|
+
children: t("page.searchEnabled.label")
|
|
205
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
206
|
+
className: PluginDetailPage_module_css_default.subHint,
|
|
207
|
+
children: t("page.searchEnabled.hint")
|
|
208
|
+
})]
|
|
209
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
|
|
210
|
+
checked: searchOn,
|
|
211
|
+
onChange: (next) => {
|
|
212
|
+
props.edit("searchEnabled", String(next));
|
|
213
|
+
},
|
|
214
|
+
label: t("page.searchEnabled.label"),
|
|
215
|
+
disabled
|
|
216
|
+
})]
|
|
217
|
+
}),
|
|
218
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
219
|
+
className: PluginDetailPage_module_css_default.field,
|
|
220
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
221
|
+
className: PluginDetailPage_module_css_default.fieldText,
|
|
222
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
223
|
+
className: PluginDetailPage_module_css_default.title,
|
|
224
|
+
children: t("page.indexRefreshMs.label")
|
|
225
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
226
|
+
className: PluginDetailPage_module_css_default.subHint,
|
|
227
|
+
children: t("page.indexRefreshMs.hint")
|
|
228
|
+
})]
|
|
229
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
230
|
+
type: "text",
|
|
231
|
+
inputMode: "numeric",
|
|
232
|
+
className: PluginDetailPage_module_css_default.inputSmall,
|
|
233
|
+
value: indexRefreshMs.text,
|
|
234
|
+
placeholder: "60000",
|
|
235
|
+
onChange: (e) => {
|
|
236
|
+
props.edit("indexRefreshMs", e.target.value);
|
|
237
|
+
},
|
|
238
|
+
disabled
|
|
239
|
+
})]
|
|
240
|
+
}),
|
|
241
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
242
|
+
className: PluginDetailPage_module_css_default.gitHint,
|
|
243
|
+
children: t("page.gitHint")
|
|
244
|
+
})
|
|
245
|
+
]
|
|
246
|
+
})
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
//#endregion
|
|
250
|
+
//#region src/locales/index.ts
|
|
251
|
+
/** Localization dictionaries (detail page prerequisites form) */
|
|
252
|
+
const zh = {
|
|
253
|
+
"title": "Obsidian Sync 前提条件",
|
|
254
|
+
"description": "配置 Obsidian vault 路径与索引参数,保存后插件按新值运行。",
|
|
255
|
+
"page.vaultPath.label": "Obsidian vault 路径",
|
|
256
|
+
"page.vaultPath.hint": "必须指向已存在的 Obsidian vault 目录(PARA 结构,含 02-Projects / 03-Areas / 04-Archive 与 DSH-会话归档-索引.md)。路径错误将导致搜索与归档全部失败。",
|
|
257
|
+
"page.searchEnabled.label": "启用按需搜索索引",
|
|
258
|
+
"page.searchEnabled.hint": "开启后注册 obsidian.search / obsidian.brief 倒排索引搜索;关闭后仅保留 read_note 与 sync_session。",
|
|
259
|
+
"page.indexRefreshMs.label": "索引重建间隔(毫秒)",
|
|
260
|
+
"page.indexRefreshMs.hint": "倒排索引增量重建 / vault 外部变更感知的间隔,下限 5000ms,默认 60000ms。",
|
|
261
|
+
"page.gitHint": "git 自动推送前提:vault 为 git 仓库且配置了 remote。不满足时归档笔记仍正常写入,git 步骤自动降级并在结果中提示。",
|
|
262
|
+
"form.unavailable": "该插件当前未加载,暂时无法配置。",
|
|
263
|
+
"form.readOnly": "本部署的设置为只读。",
|
|
264
|
+
"form.saveFailed": "保存未被接受,已保留供你修改。",
|
|
265
|
+
"form.save": "保存",
|
|
266
|
+
"form.saving": "保存中…",
|
|
267
|
+
"form.overridden": "已覆盖",
|
|
268
|
+
"form.reset": "恢复默认",
|
|
269
|
+
"form.invalid": "无效值"
|
|
270
|
+
};
|
|
271
|
+
const en = {
|
|
272
|
+
"title": "Obsidian Sync prerequisites",
|
|
273
|
+
"description": "Configure the Obsidian vault path and index settings. The plugin runs with the new values after saving.",
|
|
274
|
+
"page.vaultPath.label": "Obsidian vault path",
|
|
275
|
+
"page.vaultPath.hint": "Must point to an existing Obsidian vault (PARA layout with 02-Projects / 03-Areas / 04-Archive and DSH-会话归档-索引.md). A wrong path breaks all search and archiving.",
|
|
276
|
+
"page.searchEnabled.label": "Enable on-demand search index",
|
|
277
|
+
"page.searchEnabled.hint": "When on, obsidian.search / obsidian.brief inverted-index search is registered; when off only read_note and sync_session remain.",
|
|
278
|
+
"page.indexRefreshMs.label": "Index refresh interval (ms)",
|
|
279
|
+
"page.indexRefreshMs.hint": "Interval for incremental index rebuilds and external vault change detection. Minimum 5000ms, default 60000ms.",
|
|
280
|
+
"page.gitHint": "Auto git push requires the vault to be a git repo with a remote. Without it, archive notes are still written; the git step degrades gracefully and reports the failure in the result.",
|
|
281
|
+
"form.unavailable": "This plugin is not loaded, so it cannot be configured right now.",
|
|
282
|
+
"form.readOnly": "This deployment stores settings read-only.",
|
|
283
|
+
"form.saveFailed": "The deployment did not accept these values; they were left for you to correct.",
|
|
284
|
+
"form.save": "Save",
|
|
285
|
+
"form.saving": "Saving…",
|
|
286
|
+
"form.overridden": "Overridden",
|
|
287
|
+
"form.reset": "Reset to default",
|
|
288
|
+
"form.invalid": "Invalid value"
|
|
289
|
+
};
|
|
290
|
+
//#endregion
|
|
291
|
+
//#region src/shared.ts
|
|
292
|
+
/**
|
|
293
|
+
* Shared constants usable by both the Host bundle and the Client bundle.
|
|
294
|
+
* The client reads the settings namespace through this barrel so it never
|
|
295
|
+
* value-imports a Host-only package such as `@deepseek-ai/dsh-fs`.
|
|
296
|
+
*/
|
|
297
|
+
/** The settings namespace is the profile entry id of this plugin row (cordis.yml `id`). */
|
|
298
|
+
const OBSIDIAN_SYNC_NAMESPACE = "dsh-obsidian-sync";
|
|
299
|
+
/** npm 包名:`plugins.bundle.config` slot 的 key。 */
|
|
300
|
+
const PLUGIN_PACKAGE_NAME = "@dingpenghui/dsh-obsidian-sync";
|
|
301
|
+
/** `plugins.row.config` slot 的 key:`<package>#<row id>`。 */
|
|
302
|
+
const PLUGIN_ROW_CONFIG_KEY = `${PLUGIN_PACKAGE_NAME}#${OBSIDIAN_SYNC_NAMESPACE}`;
|
|
303
|
+
//#endregion
|
|
304
|
+
//#region src/client/index.ts
|
|
305
|
+
const DICT_NS = "settings.dsh-obsidian-sync";
|
|
306
|
+
/**
|
|
307
|
+
* 占位 ConfigForm:宿主未提供 settings 传输(configForms 服务缺失)时,
|
|
308
|
+
* 详情页以只读「不可用」快照呈现,保存控件随之禁用 —— 条目永不因缺失
|
|
309
|
+
* 而抛错。所有 getter 为惰性 thunk(与真实 scope 的快照模型一致)。
|
|
310
|
+
*/
|
|
311
|
+
function unavailableForm() {
|
|
312
|
+
const snapshot = () => ({
|
|
313
|
+
status: "unavailable",
|
|
314
|
+
value: void 0,
|
|
315
|
+
base: void 0,
|
|
316
|
+
user: void 0,
|
|
317
|
+
writable: false,
|
|
318
|
+
revision: 0,
|
|
319
|
+
mode: "host"
|
|
320
|
+
});
|
|
321
|
+
return {
|
|
322
|
+
getSnapshot: snapshot,
|
|
323
|
+
subscribe: (listener) => {
|
|
324
|
+
listener();
|
|
325
|
+
return () => {};
|
|
326
|
+
},
|
|
327
|
+
set: () => Promise.resolve(false),
|
|
328
|
+
unset: () => Promise.resolve(false),
|
|
329
|
+
mutate: () => Promise.resolve(false)
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
const inject = ["slots", "locale"];
|
|
333
|
+
function apply(ctx) {
|
|
334
|
+
const slots = ctx.get("slots");
|
|
335
|
+
const locale = ctx.get("locale");
|
|
336
|
+
ctx.effect(() => locale.register(DICT_NS, {
|
|
337
|
+
zh,
|
|
338
|
+
en
|
|
339
|
+
}), "obsidian-sync: dictionaries");
|
|
340
|
+
const configForms = ctx.get("configForms");
|
|
341
|
+
const detailPage = new ObsidianSyncPageController(configForms ? configForms.get(OBSIDIAN_SYNC_NAMESPACE) : unavailableForm());
|
|
342
|
+
ctx.effect(() => () => {
|
|
343
|
+
detailPage.dispose();
|
|
344
|
+
}, "obsidian-sync: detail form subscriptions");
|
|
345
|
+
ctx.effect(() => {
|
|
346
|
+
if (configForms === void 0) return () => {};
|
|
347
|
+
const bundleDisposer = slots.inject("plugins.bundle.config", () => slots.register({
|
|
348
|
+
name: "plugins.bundle.config",
|
|
349
|
+
key: PLUGIN_PACKAGE_NAME,
|
|
350
|
+
locale: DICT_NS,
|
|
351
|
+
inject: () => detailPage.inject()
|
|
352
|
+
}, ObsidianSyncDetailPage));
|
|
353
|
+
const rowDisposer = slots.inject("plugins.row.config", () => slots.register({
|
|
354
|
+
name: "plugins.row.config",
|
|
355
|
+
key: PLUGIN_ROW_CONFIG_KEY,
|
|
356
|
+
locale: DICT_NS,
|
|
357
|
+
inject: () => detailPage.inject()
|
|
358
|
+
}, ObsidianSyncDetailPage));
|
|
359
|
+
return () => {
|
|
360
|
+
rowDisposer();
|
|
361
|
+
bundleDisposer();
|
|
362
|
+
};
|
|
363
|
+
}, "obsidian-sync: detail page");
|
|
364
|
+
}
|
|
365
|
+
//#endregion
|
|
366
|
+
exports.apply = apply;
|
|
367
|
+
exports.inject = inject;
|
|
368
|
+
return module.exports;
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","names":["SettingsFormModel","SettingsForm","css","Switch","zhDict","enDict"],"sources":["../src/client/plugin-detail-controller.ts","../src/client/plugin-detail-page.tsx","../src/locales/index.ts","../src/shared.ts","../src/client/index.ts"],"sourcesContent":["/**\r\n * dsh-obsidian-sync 详情页的 staged form:把宿主 settings 命名空间桥接到\r\n * 插件详情页(plugins.bundle.config / plugins.row.config)的保存控件。\r\n * 参照 ui-settings-shell 的 ShellCardController 模式。\r\n */\r\nimport type { SnapshotStore } from '@deepseek-ai/dsh-client-store'\r\nimport {\r\n SettingsFormModel,\r\n type SettingsFieldState,\r\n type SettingsFormActions,\r\n type SettingsFormScope,\r\n type SettingsFormShell,\r\n} from '@deepseek-ai/dsh-client-ui-primitives'\r\nimport type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client'\r\nimport type { ObsidianSyncSettings } from '../shared.ts'\r\n\r\n/** 详情页渲染的表单状态。 */\r\nexport interface ObsidianSyncPageState extends SettingsFormShell {\r\n vaultPath: SettingsFieldState\r\n searchEnabled: SettingsFieldState\r\n indexRefreshMs: SettingsFieldState\r\n}\r\n\r\n/** 注册侧注入给详情页组件的面。 */\r\nexport interface ObsidianSyncPageFace extends SettingsFormActions {\r\n hooks: {\r\n /** 页面快照,渲染器绑定为 useObsidianSyncPage。 */\r\n obsidianSyncPage: SnapshotStore<ObsidianSyncPageState>\r\n }\r\n}\r\n\r\n/** vaultPath 字段的 format/parse 规范:非空绝对/相对路径,空串清除回默认。 */\r\nfunction vaultPathFieldSpec() {\r\n return {\r\n field: 'vaultPath',\r\n format: (value: unknown) => (typeof value === 'string' ? value : ''),\r\n parse: (text: string) => {\r\n const trimmed = text.trim()\r\n if (trimmed === '') return { kind: 'clear' as const }\r\n return { kind: 'set' as const, value: trimmed }\r\n },\r\n }\r\n}\r\n\r\n/** indexRefreshMs 字段的 format/parse 规范:≥5000 的正整数毫秒。 */\r\nfunction indexRefreshMsFieldSpec() {\r\n return {\r\n field: 'indexRefreshMs',\r\n format: (value: unknown) => (typeof value === 'number' ? String(value) : ''),\r\n parse: (text: string) => {\r\n const trimmed = text.trim()\r\n if (trimmed === '') return { kind: 'clear' as const }\r\n const n = Number(trimmed)\r\n if (!Number.isFinite(n) || n < 5000 || !Number.isInteger(n)) return undefined\r\n return { kind: 'set' as const, value: n }\r\n },\r\n }\r\n}\r\n\r\n/**\r\n * searchEnabled 字段的 format/parse 规范:布尔开关。\r\n * 详情页以 Switch 控件呈现,`format` 把布尔值映射为 'true'/'false' 文本\r\n * (与页面 `searchEnabled.text === 'true'` 的读法保持一致),\r\n * `parse` 把 'true'/'false' 文本解析回布尔值;空串清除回默认(true)。\r\n * 必须注册进 SettingsFormModel 的 specs —— 否则 `field('searchEnabled')`\r\n * 在 projection() 里抛 \"plugin card has no field searchEnabled\"。\r\n */\r\nfunction searchEnabledFieldSpec() {\r\n return {\r\n field: 'searchEnabled',\r\n format: (value: unknown) =>\r\n typeof value === 'boolean' ? String(value) : '',\r\n parse: (text: string) => {\r\n const trimmed = text.trim().toLowerCase()\r\n if (trimmed === 'true') return { kind: 'set' as const, value: true }\r\n if (trimmed === 'false') return { kind: 'set' as const, value: false }\r\n if (trimmed === '') return { kind: 'clear' as const }\r\n return undefined\r\n },\r\n }\r\n}\r\n\r\n/** 把一个宿主 settings 命名空间桥接到详情页的 staged form。 */\r\nfunction asFormScope(\r\n scope: ConfigForm<ObsidianSyncSettings>\r\n): SettingsFormScope<ObsidianSyncSettings> {\r\n return {\r\n getSnapshot: () => {\r\n const s = scope.getSnapshot()\r\n return {\r\n status: s.status,\r\n value: s.value,\r\n base: s.base,\r\n user: s.user,\r\n writable: s.writable,\r\n revision: s.revision,\r\n }\r\n },\r\n subscribe: (listener) => scope.subscribe(listener),\r\n mutate: (ops, expectedRevision) => scope.mutate(ops as never, expectedRevision),\r\n }\r\n}\r\n\r\n/** 把命名空间的 `ConfigForm` 适配为详情页的 staged form。 */\r\nexport class ObsidianSyncPageController {\r\n private readonly form: SettingsFormModel<ObsidianSyncSettings>\r\n private readonly store: SnapshotStore<ObsidianSyncPageState>\r\n\r\n /** @param scope - 共享配置表单(命名空间的 ConfigForm)。 */\r\n constructor(scope: ConfigForm<ObsidianSyncSettings>) {\r\n this.form = new SettingsFormModel(asFormScope(scope), [\r\n vaultPathFieldSpec(),\r\n searchEnabledFieldSpec(),\r\n indexRefreshMsFieldSpec(),\r\n ])\r\n this.store = this.form.bind(() => this.projection())\r\n }\r\n\r\n private projection(): ObsidianSyncPageState {\r\n return {\r\n ...this.form.shell(),\r\n vaultPath: this.form.field('vaultPath'),\r\n searchEnabled: this.form.field('searchEnabled'),\r\n indexRefreshMs: this.form.field('indexRefreshMs'),\r\n }\r\n }\r\n\r\n /** 构建详情页 slot 注册注入的面。 */\r\n inject(): ObsidianSyncPageFace {\r\n return { hooks: { obsidianSyncPage: this.store }, ...this.form.actions() }\r\n }\r\n\r\n /** 释放表单订阅。 */\r\n dispose(): void {\r\n this.form.dispose()\r\n }\r\n}\r\n","/**\r\n * dsh-obsidian-sync 详情页:点击插件列表中的插件名字后打开的画面。\r\n * 展示使用前提条件(vault 路径 / 搜索开关 / 索引间隔)并带保存控件。\r\n */\r\nimport type * as React from 'react'\r\nimport type { PropsLocale, PropsRuntime, InjectFace, TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'\r\nimport { SettingsForm, Switch } from '@deepseek-ai/dsh-client-ui-primitives'\r\nimport type { ObsidianSyncPageFace } from './plugin-detail-controller.ts'\r\nimport css from './PluginDetailPage.module.css'\r\n\r\n/** 渲染器为详情页绑定的 props。 */\r\nexport type ObsidianSyncDetailPageProps =\r\n & PropsRuntime<'plugins.bundle.config'>\r\n & PropsLocale<'settings.dsh-obsidian-sync'>\r\n & InjectFace<ObsidianSyncPageFace>\r\n\r\n/** 从 locale 字典派生 SettingsForm 的标签文案。 */\r\nfunction formLabels(t: TranslateNS<'settings.dsh-obsidian-sync'>) {\r\n return {\r\n unavailable: t('form.unavailable'),\r\n readOnly: t('form.readOnly'),\r\n saveFailed: t('form.saveFailed'),\r\n save: t('form.save'),\r\n saving: t('form.saving'),\r\n }\r\n}\r\n\r\n/**\r\n * 渲染详情页:一行摘要(`view: 'summary'`)或前提条件表单(`view: 'page'`)。\r\n * @param props - 视图、locale 文案、表单快照与动作。\r\n * @returns 摘要文字,或带保存控件的前提条件表单。\r\n */\r\nexport function ObsidianSyncDetailPage(props: ObsidianSyncDetailPageProps) {\r\n const { t } = props\r\n const state = props.useObsidianSyncPage((snapshot) => snapshot)\r\n\r\n if (props.view === 'summary') {\r\n return t('description')\r\n }\r\n\r\n const disabled = !state.writable\r\n const vaultPath = state.vaultPath\r\n const searchEnabled = state.searchEnabled\r\n const indexRefreshMs = state.indexRefreshMs\r\n\r\n // 布尔字段:以宿主当前值决定开/关,空草稿视为无用户覆盖 → 回落默认(true)。\r\n const searchOn = searchEnabled.text !== '' ? searchEnabled.text === 'true' : true\r\n\r\n return (\r\n <SettingsForm labels={formLabels(t)} state={state} onSave={props.save} onDiscard={props.discard}>\r\n <div className={css.page}>\r\n <div className={css.hint}>{t('page.vaultPath.hint')}</div>\r\n <div className={css.field}>\r\n <div className={css.fieldText}>\r\n <div className={css.title}>{t('page.vaultPath.label')}</div>\r\n </div>\r\n <input\r\n type=\"text\"\r\n className={css.input}\r\n value={vaultPath.text}\r\n placeholder=\"E:/dsh-workspace/obsidian-vault\"\r\n onChange={(e: React.ChangeEvent<HTMLInputElement>) => {\r\n props.edit('vaultPath', e.target.value)\r\n }}\r\n disabled={disabled}\r\n />\r\n </div>\r\n\r\n <div className={css.field}>\r\n <div className={css.fieldText}>\r\n <div className={css.title}>{t('page.searchEnabled.label')}</div>\r\n <div className={css.subHint}>{t('page.searchEnabled.hint')}</div>\r\n </div>\r\n <Switch\r\n checked={searchOn}\r\n onChange={(next: boolean) => {\r\n props.edit('searchEnabled', String(next))\r\n }}\r\n label={t('page.searchEnabled.label')}\r\n disabled={disabled}\r\n />\r\n </div>\r\n\r\n <div className={css.field}>\r\n <div className={css.fieldText}>\r\n <div className={css.title}>{t('page.indexRefreshMs.label')}</div>\r\n <div className={css.subHint}>{t('page.indexRefreshMs.hint')}</div>\r\n </div>\r\n <input\r\n type=\"text\"\r\n inputMode=\"numeric\"\r\n className={css.inputSmall}\r\n value={indexRefreshMs.text}\r\n placeholder=\"60000\"\r\n onChange={(e: React.ChangeEvent<HTMLInputElement>) => {\r\n props.edit('indexRefreshMs', e.target.value)\r\n }}\r\n disabled={disabled}\r\n />\r\n </div>\r\n\r\n <div className={css.gitHint}>{t('page.gitHint')}</div>\r\n </div>\r\n </SettingsForm>\r\n )\r\n}\r\n","/** Localization dictionaries (detail page prerequisites form) */\r\nexport const zh = {\r\n \"title\": \"Obsidian Sync 前提条件\",\r\n \"description\": \"配置 Obsidian vault 路径与索引参数,保存后插件按新值运行。\",\r\n \"page.vaultPath.label\": \"Obsidian vault 路径\",\r\n \"page.vaultPath.hint\": \"必须指向已存在的 Obsidian vault 目录(PARA 结构,含 02-Projects / 03-Areas / 04-Archive 与 DSH-会话归档-索引.md)。路径错误将导致搜索与归档全部失败。\",\r\n \"page.searchEnabled.label\": \"启用按需搜索索引\",\r\n \"page.searchEnabled.hint\": \"开启后注册 obsidian.search / obsidian.brief 倒排索引搜索;关闭后仅保留 read_note 与 sync_session。\",\r\n \"page.indexRefreshMs.label\": \"索引重建间隔(毫秒)\",\r\n \"page.indexRefreshMs.hint\": \"倒排索引增量重建 / vault 外部变更感知的间隔,下限 5000ms,默认 60000ms。\",\r\n \"page.gitHint\": \"git 自动推送前提:vault 为 git 仓库且配置了 remote。不满足时归档笔记仍正常写入,git 步骤自动降级并在结果中提示。\",\r\n \"form.unavailable\": \"该插件当前未加载,暂时无法配置。\",\r\n \"form.readOnly\": \"本部署的设置为只读。\",\r\n \"form.saveFailed\": \"保存未被接受,已保留供你修改。\",\r\n \"form.save\": \"保存\",\r\n \"form.saving\": \"保存中…\",\r\n \"form.overridden\": \"已覆盖\",\r\n \"form.reset\": \"恢复默认\",\r\n \"form.invalid\": \"无效值\",\r\n} as const\r\n\r\nexport const en = {\r\n \"title\": \"Obsidian Sync prerequisites\",\r\n \"description\": \"Configure the Obsidian vault path and index settings. The plugin runs with the new values after saving.\",\r\n \"page.vaultPath.label\": \"Obsidian vault path\",\r\n \"page.vaultPath.hint\": \"Must point to an existing Obsidian vault (PARA layout with 02-Projects / 03-Areas / 04-Archive and DSH-会话归档-索引.md). A wrong path breaks all search and archiving.\",\r\n \"page.searchEnabled.label\": \"Enable on-demand search index\",\r\n \"page.searchEnabled.hint\": \"When on, obsidian.search / obsidian.brief inverted-index search is registered; when off only read_note and sync_session remain.\",\r\n \"page.indexRefreshMs.label\": \"Index refresh interval (ms)\",\r\n \"page.indexRefreshMs.hint\": \"Interval for incremental index rebuilds and external vault change detection. Minimum 5000ms, default 60000ms.\",\r\n \"page.gitHint\": \"Auto git push requires the vault to be a git repo with a remote. Without it, archive notes are still written; the git step degrades gracefully and reports the failure in the result.\",\r\n \"form.unavailable\": \"This plugin is not loaded, so it cannot be configured right now.\",\r\n \"form.readOnly\": \"This deployment stores settings read-only.\",\r\n \"form.saveFailed\": \"The deployment did not accept these values; they were left for you to correct.\",\r\n \"form.save\": \"Save\",\r\n \"form.saving\": \"Saving…\",\r\n \"form.overridden\": \"Overridden\",\r\n \"form.reset\": \"Reset to default\",\r\n \"form.invalid\": \"Invalid value\",\r\n} as const\r\n","/**\r\n * Shared constants usable by both the Host bundle and the Client bundle.\r\n * The client reads the settings namespace through this barrel so it never\r\n * value-imports a Host-only package such as `@deepseek-ai/dsh-fs`.\r\n */\r\n\r\n/** The settings namespace is the profile entry id of this plugin row (cordis.yml `id`). */\r\nexport const OBSIDIAN_SYNC_NAMESPACE = 'dsh-obsidian-sync'\r\n\r\n/** npm 包名:`plugins.bundle.config` slot 的 key。 */\r\nexport const PLUGIN_PACKAGE_NAME = '@dingpenghui/dsh-obsidian-sync'\r\n\r\n/** `plugins.row.config` slot 的 key:`<package>#<row id>`。 */\r\nexport const PLUGIN_ROW_CONFIG_KEY = `${PLUGIN_PACKAGE_NAME}#${OBSIDIAN_SYNC_NAMESPACE}`\r\n\r\n/** 详情页编辑的字段 —— 命名空间 schema 的子集(前提条件)。 */\r\nexport interface ObsidianSyncSettings {\r\n /** Obsidian vault 绝对路径(前提条件 1:已存在的 vault 目录)。 */\r\n vaultPath?: string\r\n /** 是否启用按需搜索索引。 */\r\n searchEnabled?: boolean\r\n /** 索引增量重建间隔(毫秒,下限 5000)。 */\r\n indexRefreshMs?: number\r\n}\r\n","/**\r\n * Client-side entry for the dsh-obsidian-sync plugin.\r\n * Registers a plugin detail page (the screen opened when clicking the plugin's\r\n * name in the Plugins list) through the Plugins page's `plugins.bundle.config`\r\n * and `plugins.row.config` slots. The page hosts the prerequisite form:\r\n * vaultPath / searchEnabled / indexRefreshMs, saved through the Host's\r\n * settings form.\r\n */\r\nimport type { Context as ClientContext } from '@deepseek-ai/cordis'\r\n// Type-only: pulls the SlotRegistry service merge (ctx.slots).\r\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client'\r\n// Type-only: the Plugins page slot contract (plugins.bundle.config /\r\n// plugins.row.config); the owner's SlotMap merge, never a runtime import.\r\nimport type {} from '@deepseek-ai/dsh-client-ui-plugin-manager/client'\r\n// Type-only: pulls the ctx.locale merge.\r\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\r\n// Type-only: the ctx.configForms Context merge and the settings slot types.\r\nimport type {} from '@deepseek-ai/dsh-client-ui-settings/client'\r\nimport type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client'\r\n\r\nimport { ObsidianSyncPageController } from './plugin-detail-controller.ts'\r\nimport type { ObsidianSyncSettings } from '../shared.ts'\r\nimport { ObsidianSyncDetailPage } from './plugin-detail-page.tsx'\r\nimport { zh as zhDict, en as enDict } from '../locales/index.ts'\r\n// Shared barrel: keeps this bundle free of Host-only package value imports.\r\nimport {\r\n OBSIDIAN_SYNC_NAMESPACE,\r\n PLUGIN_PACKAGE_NAME,\r\n PLUGIN_ROW_CONFIG_KEY,\r\n} from '../shared.ts'\r\n\r\nconst DICT_NS = 'settings.dsh-obsidian-sync'\r\n\r\n/**\r\n * 占位 ConfigForm:宿主未提供 settings 传输(configForms 服务缺失)时,\r\n * 详情页以只读「不可用」快照呈现,保存控件随之禁用 —— 条目永不因缺失\r\n * 而抛错。所有 getter 为惰性 thunk(与真实 scope 的快照模型一致)。\r\n */\r\nfunction unavailableForm(): ConfigForm<ObsidianSyncSettings> {\r\n const snapshot = () => ({\r\n status: 'unavailable' as const,\r\n value: undefined as ObsidianSyncSettings | undefined,\r\n base: undefined,\r\n user: undefined,\r\n writable: false,\r\n revision: 0,\r\n mode: 'host' as const,\r\n })\r\n return {\r\n getSnapshot: snapshot,\r\n subscribe: (listener) => {\r\n // 只读占位永不提交;返回一个真实的空 disposer(与真实订阅同形)。\r\n listener()\r\n return () => {}\r\n },\r\n set: () => Promise.resolve(false),\r\n unset: () => Promise.resolve(false),\r\n mutate: () => Promise.resolve(false),\r\n } as ConfigForm<ObsidianSyncSettings>\r\n}\r\n\r\nexport const inject = ['slots', 'locale'] as const\r\n\r\nexport function apply(ctx: ClientContext): void {\r\n const slots = ctx.get('slots')!\r\n const locale = ctx.get('locale')!\r\n\r\n // Register locale dictionaries\r\n ctx.effect(() => locale.register(DICT_NS, { zh: zhDict, en: enDict }), 'obsidian-sync: dictionaries')\r\n\r\n // 详情页:把前提条件表单(vault 路径 / 搜索开关 / 索引间隔)挂到插件详情页。\r\n // 激活健壮性(0.2.0-rc.2):configForms 不是硬依赖,缺失时降级为只读占位\r\n // 表单(status: unavailable),条目永不因服务缺失而抛错;slot 注册本身\r\n // 不读 configForms,随页面打开才惰性拉取快照。\r\n const configForms = ctx.get('configForms')\r\n\r\n const scope: ConfigForm<ObsidianSyncSettings> = configForms\r\n ? configForms.get(OBSIDIAN_SYNC_NAMESPACE)\r\n : unavailableForm()\r\n\r\n const detailPage = new ObsidianSyncPageController(scope)\r\n ctx.effect(() => () => {\r\n detailPage.dispose()\r\n }, 'obsidian-sync: detail form subscriptions')\r\n\r\n ctx.effect(() => {\r\n // configForms 缺失(宿主未提供 settings 传输)时不注册详情页 slot,\r\n // 但仍返回一个空 cleanup,保持 effect 回调返回类型一致(Disposable)。\r\n if (configForms === undefined) return () => {}\r\n // Bundle 详情页(点击插件名字打开的画面):描述与行之间的配置区块。\r\n const bundleDisposer = slots.inject('plugins.bundle.config', () => slots.register({\r\n name: 'plugins.bundle.config',\r\n key: PLUGIN_PACKAGE_NAME,\r\n locale: DICT_NS,\r\n inject: () => detailPage.inject(),\r\n }, ObsidianSyncDetailPage))\r\n // 行详情页(行上\"配置\"控件打开的画面):`<package>#<row id>` key。\r\n const rowDisposer = slots.inject('plugins.row.config', () => slots.register({\r\n name: 'plugins.row.config',\r\n key: PLUGIN_ROW_CONFIG_KEY,\r\n locale: DICT_NS,\r\n inject: () => detailPage.inject(),\r\n }, ObsidianSyncDetailPage))\r\n return () => {\r\n rowDisposer()\r\n bundleDisposer()\r\n }\r\n }, 'obsidian-sync: detail page')\r\n}\r\n"],"mappings":"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"}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
3
|
+
//#region src/index.d.ts
|
|
4
|
+
/** Cordis 插件名。 */
|
|
5
|
+
declare const name = "obsidian-sync";
|
|
6
|
+
/** 本插件需要的 Service(`tools`/`fs` 为硬依赖,缺失时等待 Cordis 重激活)。
|
|
7
|
+
* 仅保留真实消费的 Service:settings 是可选增强(未组合时降级为默认值)。 */
|
|
8
|
+
declare const inject: string[];
|
|
9
|
+
/** 插件配置。schema 字段标记 `.volatile()`:宿主 settings 框架投影到浏览器
|
|
10
|
+
* 设置页,详情页保存后经 profile Cordis patch + volatile HMR 即时生效,无需重启。
|
|
11
|
+
* 接口本身保留普通类型(与已验证的 conversation-language 插件一致),
|
|
12
|
+
* volatile 语义由 schema 与 Loader 承担。 */
|
|
13
|
+
interface Config {
|
|
14
|
+
/** Obsidian vault 绝对路径;缺省 `E:/dsh-workspace/obsidian-vault`。 */
|
|
15
|
+
vaultPath: string;
|
|
16
|
+
/** 是否注册 obsidian.search(倒排索引 + 定时增量重建)。 */
|
|
17
|
+
searchEnabled: boolean;
|
|
18
|
+
/** 索引增量重建间隔(毫秒,下限 5000)。 */
|
|
19
|
+
indexRefreshMs: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Schemastery 配置 schema:加载器用它解析行 `config` 并补默认值。
|
|
23
|
+
*
|
|
24
|
+
* 注意 Schemastery 的 API 与 zod 不同:没有 `.optional()` / `.int()`。
|
|
25
|
+
* 对象属性默认即可选(不调用 `.required()` 时,缺失键不会写入解析结果),
|
|
26
|
+
* 整数约束通过 `.step(1)` 表达。
|
|
27
|
+
*/
|
|
28
|
+
/** 运行时 schema:volatile 标记 + 默认值。Loader 据此解析行 config 并补默认。 */
|
|
29
|
+
declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
30
|
+
vaultPath: z<string, string, "volatile-defined">;
|
|
31
|
+
searchEnabled: z<boolean, boolean, "volatile-defined">;
|
|
32
|
+
indexRefreshMs: z<number, number, "volatile-defined">;
|
|
33
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
34
|
+
vaultPath: z<string, string, "volatile-defined">;
|
|
35
|
+
searchEnabled: z<boolean, boolean, "volatile-defined">;
|
|
36
|
+
indexRefreshMs: z<number, number, "volatile-defined">;
|
|
37
|
+
}>>, "plain">;
|
|
38
|
+
declare function apply(ctx: Context): void;
|
|
39
|
+
//#endregion
|
|
40
|
+
export { Config, apply, inject, name };
|
|
41
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;;cAyCa;;;cAIA;;;;;UAMI;;EAEf;;EAEA;;EAEA;;;;;;;;;;cAWW,QAAM,EAAA,YAAA,QAAA;;;;;;;;;iBAMH,MAAM,KAAK"}
|