dsh-tiddlywiki 0.1.2 → 0.2.1
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 +7 -4
- package/lib/client.bundle.js +276 -67
- package/lib/client.js +276 -67
- package/lib/index.js +2225 -2152
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/note-widget.ts +373 -210
- package/src/client/panel.ts +51 -25
- package/src/client/settings-page.ts +3 -0
- package/src/client/styles.ts +68 -25
- package/src/host/config.ts +6 -0
- package/src/host/routes.ts +49 -10
- package/src/host/seed-notes.ts +79 -62
- package/src/index.ts +16 -4
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-tiddlywiki",
|
|
3
3
|
"description": "TiddlyWiki 5 as the DSH persistent knowledge base: tiddlywiki_* agent tools (search/get/put/delete + git sync), a full TiddlyWiki editor embedded in the GUI center column, a floating quick-note widget in the chat area, and git-based sync with auto-commit. Mounts via the official dsh plugin system — no DSH source changes.",
|
|
4
|
-
"version": "0.1
|
|
4
|
+
"version": "0.2.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"dsh",
|
|
@@ -1,210 +1,373 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Floating quick-note widget (design doc §12, D6/D7) — bottom-right, fixed,
|
|
3
|
-
* collapsible, independent of any shell DOM. Lets the human jot drafts /
|
|
4
|
-
* scratch notes while waiting for the AI or drafting the next prompt.
|
|
5
|
-
*
|
|
6
|
-
* Save posts to /dsh-tiddlywiki/note → an independent tiddler (title &
|
|
7
|
-
* editable; defaults: timestamp title + config tag, usually "inbox").
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Floating quick-note widget (design doc §12, D6/D7) — bottom-right, fixed,
|
|
3
|
+
* collapsible, independent of any shell DOM. Lets the human jot drafts /
|
|
4
|
+
* scratch notes while waiting for the AI or drafting the next prompt.
|
|
5
|
+
*
|
|
6
|
+
* Save posts to /dsh-tiddlywiki/note → an independent tiddler (title & tags
|
|
7
|
+
* editable; defaults: timestamp title + config tag, usually "inbox").
|
|
8
|
+
*
|
|
9
|
+
* ui.showQuickNote config: the widget is mounted async and stays hidden (and
|
|
10
|
+
* never appended) when the option is off, so the toggle button can be disabled
|
|
11
|
+
* from the settings page without touching the shell.
|
|
12
|
+
*
|
|
13
|
+
* Tag editor: multi-select chips + autocomplete from the wiki's existing tags
|
|
14
|
+
* (GET /dsh-tiddlywiki/tags). Enter/`,` commits the typed value; Backspace on
|
|
15
|
+
* an empty field removes the last chip; the dropdown filters on typing.
|
|
16
|
+
*
|
|
17
|
+
* @module dsh-tiddlywiki/client/note-widget
|
|
18
|
+
*/
|
|
19
|
+
import { toast } from './toast.ts'
|
|
20
|
+
import { openEditorPopup } from './editor-popup.ts'
|
|
21
|
+
|
|
22
|
+
const NOTE_ENDPOINT = '/dsh-tiddlywiki/note'
|
|
23
|
+
const EDIT_ENDPOINT = '/dsh-tiddlywiki/edit'
|
|
24
|
+
const STATUS_ENDPOINT = '/dsh-tiddlywiki/status'
|
|
25
|
+
const TAGS_ENDPOINT = '/dsh-tiddlywiki/tags'
|
|
26
|
+
|
|
27
|
+
function pad(n: number): string {
|
|
28
|
+
return n < 10 ? `0${n}` : String(n)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Default note title: `YYYY-MM-DD HH:mm`. */
|
|
32
|
+
function timestampTitle(date = new Date()): string {
|
|
33
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface UiOptions {
|
|
37
|
+
showQuickNote: boolean
|
|
38
|
+
defaultTag: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function fetchUiOptions(): Promise<UiOptions> {
|
|
42
|
+
const fallback: UiOptions = { showQuickNote: true, defaultTag: 'inbox' }
|
|
43
|
+
try {
|
|
44
|
+
const res = await fetch(STATUS_ENDPOINT, { signal: AbortSignal.timeout(5_000) })
|
|
45
|
+
if (!res.ok) return fallback
|
|
46
|
+
const payload = (await res.json()) as { ui?: { showQuickNote?: boolean }; note?: { tag?: string } }
|
|
47
|
+
return {
|
|
48
|
+
showQuickNote: payload.ui?.showQuickNote !== false,
|
|
49
|
+
defaultTag: payload.note?.tag ?? 'inbox',
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
return fallback
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Multi-tag chip editor with autocomplete from the wiki's existing tags. */
|
|
57
|
+
function buildTagEditor(): {
|
|
58
|
+
el: HTMLDivElement
|
|
59
|
+
getTags: () => string[]
|
|
60
|
+
setDefault: (tag: string) => void
|
|
61
|
+
} {
|
|
62
|
+
const wrap = document.createElement('div')
|
|
63
|
+
wrap.className = 'dsh-tw-note-tags'
|
|
64
|
+
|
|
65
|
+
const chipWrap = document.createElement('div')
|
|
66
|
+
chipWrap.className = 'dsh-tw-note-chips'
|
|
67
|
+
const input = document.createElement('input')
|
|
68
|
+
input.className = 'dsh-tw-note-taginput'
|
|
69
|
+
input.placeholder = 'tag(可多选,自动补全)'
|
|
70
|
+
const suggest = document.createElement('div')
|
|
71
|
+
suggest.className = 'dsh-tw-note-tagsuggest'
|
|
72
|
+
suggest.hidden = true
|
|
73
|
+
wrap.append(chipWrap, input, suggest)
|
|
74
|
+
|
|
75
|
+
const chips: string[] = []
|
|
76
|
+
const hideSuggest = (): void => { suggest.hidden = true }
|
|
77
|
+
|
|
78
|
+
const renderChips = (): void => {
|
|
79
|
+
chipWrap.replaceChildren()
|
|
80
|
+
for (const tag of chips) {
|
|
81
|
+
const chip = document.createElement('span')
|
|
82
|
+
chip.className = 'dsh-tw-note-tagchip'
|
|
83
|
+
chip.textContent = tag
|
|
84
|
+
const x = document.createElement('span')
|
|
85
|
+
x.className = 'dsh-tw-note-tagchip-x'
|
|
86
|
+
x.textContent = '×'
|
|
87
|
+
x.title = `移除 tag「${tag}」`
|
|
88
|
+
x.addEventListener('click', (event) => {
|
|
89
|
+
event.stopPropagation()
|
|
90
|
+
const i = chips.indexOf(tag)
|
|
91
|
+
if (i >= 0) {
|
|
92
|
+
chips.splice(i, 1)
|
|
93
|
+
renderChips()
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
chip.append(x)
|
|
97
|
+
chipWrap.append(chip)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const addTag = (tag: string): void => {
|
|
102
|
+
const t = tag.trim()
|
|
103
|
+
if (t.length === 0 || chips.includes(t)) return
|
|
104
|
+
chips.push(t)
|
|
105
|
+
input.value = ''
|
|
106
|
+
renderChips()
|
|
107
|
+
hideSuggest()
|
|
108
|
+
input.focus()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const commitInput = (): void => {
|
|
112
|
+
for (const raw of input.value.split(/\s+/)) addTag(raw)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Existing tags, fetched lazily once per widget lifetime.
|
|
116
|
+
let knownTags: string[] = []
|
|
117
|
+
let tagsPromise: Promise<string[]> | undefined
|
|
118
|
+
const ensureTags = (): Promise<string[]> => {
|
|
119
|
+
tagsPromise ??= fetch(TAGS_ENDPOINT, { signal: AbortSignal.timeout(5_000) })
|
|
120
|
+
.then((r) => (r.ok ? (r.json() as Promise<{ tags?: string[] }>) : Promise.resolve<{ tags?: string[] }>({})))
|
|
121
|
+
.then((p) => [...(p.tags ?? [])].sort((a, b) => a.localeCompare(b, 'zh')))
|
|
122
|
+
.catch(() => [])
|
|
123
|
+
void tagsPromise.then((list) => { knownTags = list })
|
|
124
|
+
return tagsPromise
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const showSuggest = (): void => {
|
|
128
|
+
const q = input.value.trim().toLowerCase()
|
|
129
|
+
const matches = knownTags
|
|
130
|
+
.filter((t) => !chips.includes(t) && (q.length === 0 || t.toLowerCase().includes(q)))
|
|
131
|
+
.slice(0, 8)
|
|
132
|
+
suggest.replaceChildren()
|
|
133
|
+
for (const tag of matches) {
|
|
134
|
+
const item = document.createElement('div')
|
|
135
|
+
item.className = 'dsh-tw-note-tagsuggest-item'
|
|
136
|
+
item.textContent = tag
|
|
137
|
+
item.addEventListener('mousedown', (event) => {
|
|
138
|
+
event.preventDefault()
|
|
139
|
+
addTag(tag)
|
|
140
|
+
})
|
|
141
|
+
suggest.append(item)
|
|
142
|
+
}
|
|
143
|
+
suggest.hidden = matches.length === 0
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
input.addEventListener('focus', () => { void ensureTags().then(showSuggest) })
|
|
147
|
+
input.addEventListener('input', () => {
|
|
148
|
+
if (knownTags.length === 0) void ensureTags().then(showSuggest)
|
|
149
|
+
else showSuggest()
|
|
150
|
+
})
|
|
151
|
+
input.addEventListener('keydown', (event) => {
|
|
152
|
+
if (event.key === 'Enter' || event.key === ',') {
|
|
153
|
+
event.preventDefault()
|
|
154
|
+
commitInput()
|
|
155
|
+
} else if (event.key === 'Backspace' && input.value.length === 0 && chips.length > 0) {
|
|
156
|
+
chips.pop()
|
|
157
|
+
renderChips()
|
|
158
|
+
} else if (event.key === 'Escape') {
|
|
159
|
+
hideSuggest()
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
document.addEventListener('click', (event) => {
|
|
163
|
+
if (!wrap.contains(event.target as Node)) hideSuggest()
|
|
164
|
+
}, true)
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
el: wrap,
|
|
168
|
+
getTags: () => {
|
|
169
|
+
commitInput()
|
|
170
|
+
return [...chips]
|
|
171
|
+
},
|
|
172
|
+
setDefault: (tag: string) => {
|
|
173
|
+
// Only pre-fill when nothing is chosen yet; never steal focus.
|
|
174
|
+
if (chips.length === 0) {
|
|
175
|
+
const t = tag.trim()
|
|
176
|
+
if (t.length > 0) {
|
|
177
|
+
chips.push(t)
|
|
178
|
+
renderChips()
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Build the whole widget DOM and wire it up (returns the appended root). */
|
|
186
|
+
function buildWidget(): HTMLDivElement {
|
|
187
|
+
const root = document.createElement('div')
|
|
188
|
+
root.className = 'dsh-tw-note'
|
|
189
|
+
|
|
190
|
+
const card = document.createElement('div')
|
|
191
|
+
card.className = 'dsh-tw-note-card'
|
|
192
|
+
card.hidden = true
|
|
193
|
+
|
|
194
|
+
const head = document.createElement('div')
|
|
195
|
+
head.className = 'dsh-tw-note-head'
|
|
196
|
+
const label = document.createElement('span')
|
|
197
|
+
label.className = 'dsh-tw-note-label'
|
|
198
|
+
label.textContent = '📝 快速笔记'
|
|
199
|
+
const closeBtn = document.createElement('button')
|
|
200
|
+
closeBtn.type = 'button'
|
|
201
|
+
closeBtn.className = 'dsh-tw-note-close'
|
|
202
|
+
closeBtn.title = '收起'
|
|
203
|
+
closeBtn.textContent = '✕'
|
|
204
|
+
head.append(label, closeBtn)
|
|
205
|
+
|
|
206
|
+
const fields = document.createElement('div')
|
|
207
|
+
fields.className = 'dsh-tw-note-fields'
|
|
208
|
+
const titleInput = document.createElement('input')
|
|
209
|
+
titleInput.className = 'dsh-tw-note-title'
|
|
210
|
+
titleInput.placeholder = '标题(默认时间戳)'
|
|
211
|
+
const tagEditor = buildTagEditor()
|
|
212
|
+
fields.append(titleInput, tagEditor.el)
|
|
213
|
+
|
|
214
|
+
const textarea = document.createElement('textarea')
|
|
215
|
+
textarea.className = 'dsh-tw-note-text'
|
|
216
|
+
textarea.placeholder = '写点东西…(Ctrl+Enter 保存)'
|
|
217
|
+
|
|
218
|
+
const foot = document.createElement('div')
|
|
219
|
+
foot.className = 'dsh-tw-note-foot'
|
|
220
|
+
const hint = document.createElement('span')
|
|
221
|
+
hint.className = 'dsh-tw-note-hint'
|
|
222
|
+
hint.textContent = 'Ctrl+Enter'
|
|
223
|
+
const edit = document.createElement('button')
|
|
224
|
+
edit.type = 'button'
|
|
225
|
+
edit.className = 'dsh-tw-note-edit'
|
|
226
|
+
edit.title = '保存并在 TiddlyWiki 原生编辑器中打开'
|
|
227
|
+
edit.textContent = '✏️ 在 TW 中编辑'
|
|
228
|
+
const save = document.createElement('button')
|
|
229
|
+
save.type = 'button'
|
|
230
|
+
save.className = 'dsh-tw-note-save'
|
|
231
|
+
save.textContent = '保存'
|
|
232
|
+
foot.append(hint, edit, save)
|
|
233
|
+
|
|
234
|
+
card.append(head, fields, textarea, foot)
|
|
235
|
+
|
|
236
|
+
const toggle = document.createElement('button')
|
|
237
|
+
toggle.type = 'button'
|
|
238
|
+
toggle.className = 'dsh-tw-note-toggle'
|
|
239
|
+
toggle.textContent = '📝 快速笔记'
|
|
240
|
+
|
|
241
|
+
root.append(card, toggle)
|
|
242
|
+
document.body.append(root)
|
|
243
|
+
|
|
244
|
+
let defaultTag = 'inbox'
|
|
245
|
+
let opened = false
|
|
246
|
+
|
|
247
|
+
const resetTitle = (): void => {
|
|
248
|
+
titleInput.value = timestampTitle()
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const open = async (): Promise<void> => {
|
|
252
|
+
card.hidden = false
|
|
253
|
+
opened = true
|
|
254
|
+
resetTitle()
|
|
255
|
+
textarea.focus()
|
|
256
|
+
// Fetch the effective default tag once, then pre-fill if nothing is chosen.
|
|
257
|
+
defaultTag = (await fetchUiOptions()).defaultTag
|
|
258
|
+
tagEditor.setDefault(defaultTag)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const close = (): void => {
|
|
262
|
+
card.hidden = true
|
|
263
|
+
opened = false
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
toggle.addEventListener('click', () => { void (opened ? close() : open()) })
|
|
267
|
+
closeBtn.addEventListener('click', close)
|
|
268
|
+
|
|
269
|
+
const doSave = async (): Promise<void> => {
|
|
270
|
+
const text = textarea.value.trim()
|
|
271
|
+
if (text.length === 0) {
|
|
272
|
+
toast('内容为空,未保存')
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
save.disabled = true
|
|
276
|
+
save.textContent = '保存中…'
|
|
277
|
+
try {
|
|
278
|
+
const res = await fetch(NOTE_ENDPOINT, {
|
|
279
|
+
method: 'POST',
|
|
280
|
+
headers: { 'content-type': 'application/json' },
|
|
281
|
+
body: JSON.stringify({
|
|
282
|
+
title: titleInput.value.trim(),
|
|
283
|
+
tags: tagEditor.getTags(),
|
|
284
|
+
text,
|
|
285
|
+
}),
|
|
286
|
+
signal: AbortSignal.timeout(10_000),
|
|
287
|
+
})
|
|
288
|
+
const payload = (await res.json().catch(() => null)) as { ok?: boolean; title?: string; error?: string } | null
|
|
289
|
+
if (!res.ok || payload?.ok !== true) {
|
|
290
|
+
toast(`保存失败:${payload?.error ?? `HTTP ${res.status}`}`)
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
textarea.value = ''
|
|
294
|
+
resetTitle()
|
|
295
|
+
toast(`已保存「${payload.title ?? titleInput.value}」`)
|
|
296
|
+
close()
|
|
297
|
+
} catch (err) {
|
|
298
|
+
toast(`保存失败:${err instanceof Error ? err.message : String(err)}`)
|
|
299
|
+
} finally {
|
|
300
|
+
save.disabled = false
|
|
301
|
+
save.textContent = '保存'
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
save.addEventListener('click', () => { void doSave() })
|
|
306
|
+
textarea.addEventListener('keydown', (event) => {
|
|
307
|
+
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
|
|
308
|
+
event.preventDefault()
|
|
309
|
+
void doSave()
|
|
310
|
+
}
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
/** Save (if non-empty) and open the tiddler in TW's native editor. */
|
|
314
|
+
const doEdit = async (): Promise<void> => {
|
|
315
|
+
const title = titleInput.value.trim().length > 0 ? titleInput.value.trim() : timestampTitle()
|
|
316
|
+
const text = textarea.value
|
|
317
|
+
const tags = tagEditor.getTags()
|
|
318
|
+
edit.disabled = true
|
|
319
|
+
edit.textContent = '打开中…'
|
|
320
|
+
try {
|
|
321
|
+
const res = await fetch(EDIT_ENDPOINT, {
|
|
322
|
+
method: 'POST',
|
|
323
|
+
headers: { 'content-type': 'application/json' },
|
|
324
|
+
body: JSON.stringify({ title, tags, text }),
|
|
325
|
+
signal: AbortSignal.timeout(10_000),
|
|
326
|
+
})
|
|
327
|
+
const payload = (await res.json().catch(() => null)) as
|
|
328
|
+
| { ok?: boolean; title?: string; draftTitle?: string; twUrl?: string; error?: string }
|
|
329
|
+
| null
|
|
330
|
+
if (!res.ok || payload?.ok !== true) {
|
|
331
|
+
toast(`打开失败:${payload?.error ?? `HTTP ${res.status}`}`)
|
|
332
|
+
return
|
|
333
|
+
}
|
|
334
|
+
if (typeof payload.twUrl !== 'string' || typeof payload.draftTitle !== 'string') {
|
|
335
|
+
toast('打开失败:服务未返回编辑器地址')
|
|
336
|
+
return
|
|
337
|
+
}
|
|
338
|
+
openEditorPopup(`${payload.twUrl}#${encodeURIComponent(payload.draftTitle)}`, payload.title ?? title)
|
|
339
|
+
toast(`已在弹出窗口打开「${payload.title ?? title}」编辑器`)
|
|
340
|
+
} catch (err) {
|
|
341
|
+
toast(`打开失败:${err instanceof Error ? err.message : String(err)}`)
|
|
342
|
+
} finally {
|
|
343
|
+
edit.disabled = false
|
|
344
|
+
edit.textContent = '✏️ 在 TW 中编辑'
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
edit.addEventListener('click', () => { void doEdit() })
|
|
349
|
+
|
|
350
|
+
return root
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Mount the floating quick-note widget. Fetches /status first: when
|
|
355
|
+
* `ui.showQuickNote` is off the widget is never created (no DOM side effects).
|
|
356
|
+
* Returns a disposer that removes it.
|
|
357
|
+
*/
|
|
358
|
+
export function mountNoteWidget(): () => void {
|
|
359
|
+
let disposed = false
|
|
360
|
+
let root: HTMLDivElement | undefined
|
|
361
|
+
const disposer = (): void => {
|
|
362
|
+
disposed = true
|
|
363
|
+
root?.remove()
|
|
364
|
+
const toastEl = document.querySelector<HTMLElement>('.dsh-tw-toast')
|
|
365
|
+
toastEl?.remove()
|
|
366
|
+
}
|
|
367
|
+
void (async () => {
|
|
368
|
+
const ui = await fetchUiOptions()
|
|
369
|
+
if (disposed || !ui.showQuickNote) return
|
|
370
|
+
root = buildWidget()
|
|
371
|
+
})()
|
|
372
|
+
return disposer
|
|
373
|
+
}
|