dsh-tiddlywiki 0.7.3 → 0.8.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/README.md +25 -1
- package/lib/client.bundle.js +264 -2
- package/lib/client.js +264 -2
- package/lib/index.js +83 -26
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/editor-popup.ts +127 -121
- package/src/client/panel.ts +12 -1
- package/src/client/settings-page.ts +2 -0
- package/src/client/styles.ts +4 -2
- package/src/client/theme-sync.ts +312 -0
- package/src/host/config.ts +14 -0
- package/src/host/routes.ts +36 -6
- package/src/host/tools.ts +41 -12
- package/src/index.ts +22 -11
package/src/host/config.ts
CHANGED
|
@@ -16,6 +16,12 @@ import type { TiddlyWebClient } from './tw-api.ts'
|
|
|
16
16
|
/** Config tiddler (JSON string) where the settings page stores overrides. */
|
|
17
17
|
export const CONFIG_TIDDLER = '$:/plugins/dsh-tiddlywiki/config'
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Default dark palette the embedded TW switches to when DSH is dark
|
|
21
|
+
* (mirrored in src/client/theme-sync.ts — the two bundles cannot share code).
|
|
22
|
+
*/
|
|
23
|
+
export const DARK_PALETTE_DEFAULT = '$:/palettes/CupertinoDark'
|
|
24
|
+
|
|
19
25
|
/** Extensible, loose plugin config shape (future fields just appear here). */
|
|
20
26
|
export interface PluginConfigShape {
|
|
21
27
|
note?: { tag?: string }
|
|
@@ -27,6 +33,14 @@ export interface PluginConfigShape {
|
|
|
27
33
|
showPanelStatus?: boolean
|
|
28
34
|
/** 是否在界面右下角显示「同步」悬浮按钮(默认 true)。 */
|
|
29
35
|
showSyncButton?: boolean
|
|
36
|
+
/**
|
|
37
|
+
* 嵌入式 TW 是否跟随 DSH 深浅主题(默认 true):DSH 暗色时把 TW 活动
|
|
38
|
+
* palette 临时切到 `darkPalette`,浅色时恢复用户原 palette;仅内存生效,
|
|
39
|
+
* 不会写回 wiki。
|
|
40
|
+
*/
|
|
41
|
+
followDshTheme?: boolean
|
|
42
|
+
/** DSH 暗色时 TW 使用的 palette tiddler 标题(默认 CupertinoDark)。 */
|
|
43
|
+
darkPalette?: string
|
|
30
44
|
/**
|
|
31
45
|
* 一键发送给 Agent(TW 笔记 → DSH 会话注入)。
|
|
32
46
|
* `enabled: false` 时路由 403、TW 端按钮点击提示未启用;`token` 非空时
|
package/src/host/routes.ts
CHANGED
|
@@ -73,11 +73,25 @@ export interface SessionControllerFace {
|
|
|
73
73
|
create(request: { cwd?: string; workspaceId?: string }): Promise<{ sessionId: string }>
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Structural face over the DSH `workspaceRegistry` service (a subset of
|
|
78
|
+
* dsh-workspace). Only what the agent-create route needs is declared; the
|
|
79
|
+
* runtime instance is a real Service, never inspected data.
|
|
80
|
+
*/
|
|
81
|
+
export interface WorkspaceRegistryFace {
|
|
82
|
+
/** Resolve or create the workspace owning `path` — idempotent by canonical path. */
|
|
83
|
+
create(path: string, title?: string): Promise<{ id: string; path: string }>
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Effective UI flags returned by /status (mirror index.ts). */
|
|
77
87
|
export interface UiDefaultsPublic {
|
|
78
88
|
showQuickNote: boolean
|
|
79
89
|
showPanelStatus: boolean
|
|
80
90
|
showSyncButton: boolean
|
|
91
|
+
/** 嵌入式 TW 是否跟随 DSH 深浅主题(false 时客户端停止 palette 同步)。 */
|
|
92
|
+
followDshTheme: boolean
|
|
93
|
+
/** DSH 暗色时 TW 使用的 palette tiddler 标题。 */
|
|
94
|
+
darkPalette: string
|
|
81
95
|
}
|
|
82
96
|
|
|
83
97
|
export interface RouteDeps {
|
|
@@ -91,6 +105,10 @@ export interface RouteDeps {
|
|
|
91
105
|
/** Optional DSH sessionController service (agent-send routes only); resolved
|
|
92
106
|
* lazily per request because it may register after webServer appears. */
|
|
93
107
|
getSessionController: () => SessionControllerFace | undefined
|
|
108
|
+
/** Optional DSH workspaceRegistry service (agent-create route); resolves a
|
|
109
|
+
* cwd to a real Workspace so new sessions land inside it instead of the
|
|
110
|
+
* ungrouped bucket. */
|
|
111
|
+
getWorkspaceRegistry: () => WorkspaceRegistryFace | undefined
|
|
94
112
|
/** Whether the one-click send-to-agent feature is enabled (config switch). */
|
|
95
113
|
sendToAgentEnabled: () => boolean
|
|
96
114
|
/** Optional shared token that must match `x-send-to-agent-token` when set. */
|
|
@@ -334,10 +352,14 @@ export function registerRoutes(ctx: { webServer: WebServerFace }, deps: RouteDep
|
|
|
334
352
|
}
|
|
335
353
|
|
|
336
354
|
/**
|
|
337
|
-
* POST /dsh-tiddlywiki/agent/create — create (or adopt) one ordinary session
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
355
|
+
* POST /dsh-tiddlywiki/agent/create — create (or adopt) one ordinary session
|
|
356
|
+
* inside a real DSH workspace resolved from the requested path. The picker
|
|
357
|
+
* uses it for "new workspace / new session": the directory is materialised so
|
|
358
|
+
* a brand-new workspace actually exists on disk, the path is resolved to its
|
|
359
|
+
* (idempotent) Workspace, and the session is created with `workspaceId` so it
|
|
360
|
+
* lands under that workspace in the sidebar. Creating with bare `cwd` instead
|
|
361
|
+
* would leave the session in the ungrouped bucket even when its working
|
|
362
|
+
* directory matches an existing workspace path.
|
|
341
363
|
*/
|
|
342
364
|
const handleAgentCreate = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {
|
|
343
365
|
try {
|
|
@@ -361,10 +383,18 @@ export function registerRoutes(ctx: { webServer: WebServerFace }, deps: RouteDep
|
|
|
361
383
|
json(res, { ok: false, error: 'session service unavailable' }, 503)
|
|
362
384
|
return
|
|
363
385
|
}
|
|
386
|
+
const ws = deps.getWorkspaceRegistry()
|
|
364
387
|
if (cwd.length > 0) {
|
|
365
388
|
await mkdir(cwd, { recursive: true })
|
|
366
389
|
}
|
|
367
|
-
|
|
390
|
+
let created: { sessionId: string }
|
|
391
|
+
if (cwd.length > 0 && ws !== undefined) {
|
|
392
|
+
const workspace = await ws.create(cwd)
|
|
393
|
+
created = await sc.create({ workspaceId: workspace.id })
|
|
394
|
+
json(res, { ok: true, sessionId: created.sessionId, cwd, workspaceId: workspace.id })
|
|
395
|
+
return
|
|
396
|
+
}
|
|
397
|
+
created = await sc.create({ cwd: cwd.length > 0 ? cwd : undefined })
|
|
368
398
|
json(res, { ok: true, sessionId: created.sessionId, cwd: cwd || null })
|
|
369
399
|
} catch (err) {
|
|
370
400
|
json(res, { ok: false, error: err instanceof Error ? err.message : String(err) }, 500)
|
package/src/host/tools.ts
CHANGED
|
@@ -18,6 +18,19 @@ import { defineTool } from '../sdk.ts'
|
|
|
18
18
|
import type { TiddlyWebClient, Tiddler } from './tw-api.ts'
|
|
19
19
|
import type { GitFace } from './git.ts'
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* 约定标签:标记「由 Agent 撰写」的笔记。
|
|
23
|
+
* 新建(title 不存在)时由 tiddlywiki_put / tiddlywiki_batch_put 自动补打;
|
|
24
|
+
* 首页据此把这类笔记单独列在「Agent 区块」并从主标签列表排除。
|
|
25
|
+
*/
|
|
26
|
+
export const AGENT_WRITTEN_TAG = 'agent-written'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 约定标签:标记「Agent 撰写后又经人类编辑」的笔记(双标签分级第二档)。
|
|
30
|
+
* 人类在编辑某篇 Agent 笔记时手动补打,首页把这类笔记归入「Agent + 人工」档。
|
|
31
|
+
*/
|
|
32
|
+
export const HUMAN_EDITED_TAG = 'human-edited'
|
|
33
|
+
|
|
21
34
|
/** Structural tool-registry face (subset of the dsh tools service). */
|
|
22
35
|
export interface ToolsCtx {
|
|
23
36
|
tools: { register(tool: unknown): () => void }
|
|
@@ -83,6 +96,21 @@ function rewriteRefs(text: string, oldTitle: string, newTitle: string): { text:
|
|
|
83
96
|
return { text: out, count }
|
|
84
97
|
}
|
|
85
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Compute the final tags for a write:
|
|
101
|
+
* - NEW tiddler (created by this tool) → auto-append the agent-written tag,
|
|
102
|
+
* unless it is a `$:/` system tiddler (config/language/theme internals).
|
|
103
|
+
* - Existing tiddler → keep the caller's tags as-is (an agent maintaining a
|
|
104
|
+
* human note must NOT silently claim authorship; an already agent-written
|
|
105
|
+
* note keeps its tag because the caller provides the full tag list).
|
|
106
|
+
*/
|
|
107
|
+
function finalTagsForWrite(title: string, existing: Tiddler | undefined, tags: string[]): string[] {
|
|
108
|
+
if (existing !== undefined) return tags
|
|
109
|
+
if (title.startsWith('$:/')) return tags
|
|
110
|
+
if (tags.includes(AGENT_WRITTEN_TAG)) return tags
|
|
111
|
+
return [...tags, AGENT_WRITTEN_TAG]
|
|
112
|
+
}
|
|
113
|
+
|
|
86
114
|
export function registerTiddlywikiTools(ctx: ToolsCtx, deps: ToolsDeps): Array<() => void> {
|
|
87
115
|
const disposers: Array<() => void> = []
|
|
88
116
|
const register = (tool: unknown): void => { disposers.push(ctx.tools.register(tool)) }
|
|
@@ -227,7 +255,7 @@ export function registerTiddlywikiTools(ctx: ToolsCtx, deps: ToolsDeps): Array<(
|
|
|
227
255
|
// ── tiddlywiki_put ───────────────────────────────────────────────────────
|
|
228
256
|
register(defineTool({
|
|
229
257
|
name: 'tiddlywiki_put',
|
|
230
|
-
description: '写入(新建或覆盖)一个 TiddlyWiki tiddler。同名覆盖;tags 为标签数组,fields 为附加自定义字段(json 对象,会写入 tiddler 字段)。写入后触发自动 commit
|
|
258
|
+
description: '写入(新建或覆盖)一个 TiddlyWiki tiddler。同名覆盖;tags 为标签数组,fields 为附加自定义字段(json 对象,会写入 tiddler 字段)。写入后触发自动 commit。新建(title 不存在)时自动补打 agent-written 标签标记「由 Agent 撰写」,无需手动添加。',
|
|
231
259
|
parameters: {
|
|
232
260
|
title: { type: 'string', description: 'tiddler 标题(精确匹配,覆盖同名)', required: true },
|
|
233
261
|
text: { type: 'string', description: 'tiddler 全文(wiki 文本)', required: true },
|
|
@@ -249,19 +277,21 @@ export function registerTiddlywikiTools(ctx: ToolsCtx, deps: ToolsDeps): Array<(
|
|
|
249
277
|
execute: async (args: { title: string; text: string; tags?: string[]; fields?: Record<string, unknown> }): Promise<PutResult> => {
|
|
250
278
|
const wiki = deps.wiki()
|
|
251
279
|
if (wiki === undefined) throw new Error('TiddlyWiki 服务未运行(tiddlywiki_status 可查)')
|
|
280
|
+
const existing = await wiki.get(args.title).catch(() => undefined)
|
|
281
|
+
const tags = finalTagsForWrite(args.title, existing, Array.isArray(args.tags) ? args.tags.filter((t) => typeof t === 'string' && t.trim().length > 0) : [])
|
|
252
282
|
const tiddler: Tiddler = { title: args.title, text: args.text }
|
|
253
|
-
if (
|
|
283
|
+
if (tags.length > 0) tiddler.tags = tags
|
|
254
284
|
if (args.fields !== undefined && typeof args.fields === 'object' && args.fields !== null) Object.assign(tiddler, args.fields)
|
|
255
285
|
await wiki.put(tiddler)
|
|
256
286
|
deps.autoCommit()
|
|
257
|
-
return { ok: true, title: args.title, tags
|
|
287
|
+
return { ok: true, title: args.title, tags, fields: args.fields ?? null }
|
|
258
288
|
},
|
|
259
289
|
}))
|
|
260
290
|
|
|
261
291
|
// ── tiddlywiki_batch_put ─────────────────────────────────────────────────
|
|
262
292
|
register(defineTool({
|
|
263
293
|
name: 'tiddlywiki_batch_put',
|
|
264
|
-
description: '批量写入/覆盖多个 TiddlyWiki tiddler(一次工具调用)。overwrite=false 时跳过已存在的标题;返回逐条结果。写入后触发自动 commit
|
|
294
|
+
description: '批量写入/覆盖多个 TiddlyWiki tiddler(一次工具调用)。overwrite=false 时跳过已存在的标题;返回逐条结果。写入后触发自动 commit。新建(title 不存在)的条目会自动补打 agent-written 标签,无需手动添加。',
|
|
265
295
|
parameters: {
|
|
266
296
|
items: {
|
|
267
297
|
type: 'array',
|
|
@@ -301,16 +331,15 @@ export function registerTiddlywikiTools(ctx: ToolsCtx, deps: ToolsDeps): Array<(
|
|
|
301
331
|
for (const item of list) {
|
|
302
332
|
if (typeof item.title !== 'string' || item.title.length === 0) throw new Error('batch_put: 每条 items 都需要非空 title')
|
|
303
333
|
if (typeof item.text !== 'string') throw new Error(`batch_put: items「${item.title}」缺少 text`)
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
continue
|
|
310
|
-
}
|
|
334
|
+
const existing = await wiki.get(item.title).catch(() => undefined)
|
|
335
|
+
if (!overwrite && existing !== undefined) {
|
|
336
|
+
skipped++
|
|
337
|
+
results.push({ title: item.title, written: false, skipped: true })
|
|
338
|
+
continue
|
|
311
339
|
}
|
|
340
|
+
const tags = finalTagsForWrite(item.title, existing, Array.isArray(item.tags) ? item.tags.filter((t) => typeof t === 'string' && t.trim().length > 0) : [])
|
|
312
341
|
const tiddler: Tiddler = { title: item.title, text: item.text }
|
|
313
|
-
if (
|
|
342
|
+
if (tags.length > 0) tiddler.tags = tags
|
|
314
343
|
if (item.fields !== undefined && typeof item.fields === 'object' && item.fields !== null) Object.assign(tiddler, item.fields)
|
|
315
344
|
await wiki.put(tiddler)
|
|
316
345
|
written++
|
package/src/index.ts
CHANGED
|
@@ -22,8 +22,8 @@ import { watch, type FSWatcher } from 'node:fs'
|
|
|
22
22
|
import { writeFile } from 'node:fs/promises'
|
|
23
23
|
import { join } from 'node:path'
|
|
24
24
|
import { AutoCommitter, GitFace } from './host/git.ts'
|
|
25
|
-
import { registerRoutes, type SessionControllerFace, type WebServerFace } from './host/routes.ts'
|
|
26
|
-
import { ConfigStore, deepMerge, type PluginConfigShape } from './host/config.ts'
|
|
25
|
+
import { registerRoutes, type SessionControllerFace, type WebServerFace, type WorkspaceRegistryFace } from './host/routes.ts'
|
|
26
|
+
import { ConfigStore, deepMerge, DARK_PALETTE_DEFAULT, type PluginConfigShape } from './host/config.ts'
|
|
27
27
|
import { registerAdminRoutes, ensureLanguage, resolveTwRoot, type AdminDeps } from './host/admin.ts'
|
|
28
28
|
import { seedDocNote, DOC_NOTE_TITLE } from './host/seed-notes.ts'
|
|
29
29
|
import { TiddlyWebClient } from './host/tw-api.ts'
|
|
@@ -56,7 +56,7 @@ export interface TiddlywikiConfig {
|
|
|
56
56
|
port?: number
|
|
57
57
|
git?: { autoCommit?: boolean; debounceMs?: number; remote?: string; branch?: string }
|
|
58
58
|
note?: { tag?: string }
|
|
59
|
-
ui?: { showQuickNote?: boolean; showPanelStatus?: boolean; showSyncButton?: boolean }
|
|
59
|
+
ui?: { showQuickNote?: boolean; showPanelStatus?: boolean; showSyncButton?: boolean; followDshTheme?: boolean; darkPalette?: string }
|
|
60
60
|
auth?: { username?: string; password?: string }
|
|
61
61
|
}
|
|
62
62
|
|
|
@@ -77,7 +77,7 @@ interface ResolvedConfig {
|
|
|
77
77
|
port: number
|
|
78
78
|
git: { autoCommit: boolean; debounceMs: number; remote: string; branch: string }
|
|
79
79
|
note: { tag: string }
|
|
80
|
-
ui: { showQuickNote: boolean; showPanelStatus: boolean; showSyncButton: boolean; sendToAgent: { enabled: boolean } }
|
|
80
|
+
ui: { showQuickNote: boolean; showPanelStatus: boolean; showSyncButton: boolean; followDshTheme: boolean; darkPalette: string; sendToAgent: { enabled: boolean } }
|
|
81
81
|
auth: { username?: string; password?: string }
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -87,7 +87,7 @@ const DEFAULTS: ResolvedConfig = {
|
|
|
87
87
|
port: 0,
|
|
88
88
|
git: { autoCommit: true, debounceMs: 60_000, remote: '', branch: 'main' },
|
|
89
89
|
note: { tag: 'inbox' },
|
|
90
|
-
ui: { showQuickNote: true, showPanelStatus: true, showSyncButton: true, sendToAgent: { enabled: true } },
|
|
90
|
+
ui: { showQuickNote: true, showPanelStatus: true, showSyncButton: true, followDshTheme: true, darkPalette: DARK_PALETTE_DEFAULT, sendToAgent: { enabled: true } },
|
|
91
91
|
auth: { username: '', password: '' },
|
|
92
92
|
}
|
|
93
93
|
|
|
@@ -183,7 +183,9 @@ pull 冲突后:先 \`tiddlywiki_git_resolve files=[冲突文件] strategy=keep
|
|
|
183
183
|
|
|
184
184
|
把 wiki 当作长期记忆与知识沉淀的地方:会议纪要、决策记录、调研笔记、随手的想法都可存成独立 tiddler(tag 建议用 inbox/meeting/decision 等便于检索)。
|
|
185
185
|
|
|
186
|
-
用本插件自动创建笔记时,除了业务性 tag 外,请把「当前工作区(项目)的名字」也作为标签之一加上去(例如 \`tiddlywiki_put\` 的 tags 里带上当前 workspace
|
|
186
|
+
用本插件自动创建笔记时,除了业务性 tag 外,请把「当前工作区(项目)的名字」也作为标签之一加上去(例如 \`tiddlywiki_put\` 的 tags 里带上当前 workspace 名),这样笔记能按项目归集、检索。
|
|
187
|
+
|
|
188
|
+
**Agent 笔记标签约定**:\`tiddlywiki_put\` / \`tiddlywiki_batch_put\` 新建笔记时,插件会自动补打 \`agent-written\` 标签(标记「由 Agent 撰写」),无需手动添加,也不要手动移除它(除非用户明确要求)。首页会把 Agent 笔记单独列在「Agent 区块」,主标签列表只统计人类笔记。若某篇 Agent 笔记后续被人类编辑过,请在该笔记上补打 \`human-edited\` 标签,首页会把它归入「Agent + 人工」档。覆盖写入已有的(人类)笔记时不会自动加 agent-written,请保持笔记原本的归属。`
|
|
187
189
|
|
|
188
190
|
/**
|
|
189
191
|
* Mount the host half.
|
|
@@ -212,11 +214,17 @@ export function apply(ctx: HostCtx, rawConfig: TiddlywikiConfig = {}): void {
|
|
|
212
214
|
const tag = eff().note?.tag
|
|
213
215
|
return typeof tag === 'string' && tag.trim().length > 0 ? tag : config.note.tag
|
|
214
216
|
}
|
|
215
|
-
const effectiveUi = (): { showQuickNote: boolean; showPanelStatus: boolean; showSyncButton: boolean } =>
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
217
|
+
const effectiveUi = (): { showQuickNote: boolean; showPanelStatus: boolean; showSyncButton: boolean; followDshTheme: boolean; darkPalette: string } => {
|
|
218
|
+
const ui = eff().ui ?? {}
|
|
219
|
+
const palette = typeof ui.darkPalette === 'string' && ui.darkPalette.trim().length > 0 ? ui.darkPalette.trim() : DARK_PALETTE_DEFAULT
|
|
220
|
+
return {
|
|
221
|
+
showQuickNote: ui.showQuickNote !== false,
|
|
222
|
+
showPanelStatus: ui.showPanelStatus !== false,
|
|
223
|
+
showSyncButton: ui.showSyncButton !== false,
|
|
224
|
+
followDshTheme: ui.followDshTheme !== false,
|
|
225
|
+
darkPalette: palette,
|
|
226
|
+
}
|
|
227
|
+
}
|
|
220
228
|
|
|
221
229
|
const disposers: Array<() => void> = []
|
|
222
230
|
const disposeAll = (): void => {
|
|
@@ -359,6 +367,8 @@ export function apply(ctx: HostCtx, rawConfig: TiddlywikiConfig = {}): void {
|
|
|
359
367
|
// agent routes must work whenever a request actually arrives.
|
|
360
368
|
const getSessionController = (): SessionControllerFace | undefined =>
|
|
361
369
|
ctx.get('sessionController') as SessionControllerFace | undefined
|
|
370
|
+
const getWorkspaceRegistry = (): WorkspaceRegistryFace | undefined =>
|
|
371
|
+
ctx.get('workspaceRegistry') as WorkspaceRegistryFace | undefined
|
|
362
372
|
const disposeRoutes = registerRoutes({ webServer: ws }, {
|
|
363
373
|
server,
|
|
364
374
|
getClient: client,
|
|
@@ -368,6 +378,7 @@ export function apply(ctx: HostCtx, rawConfig: TiddlywikiConfig = {}): void {
|
|
|
368
378
|
uiDefaults: () => effectiveUi(),
|
|
369
379
|
getWikiPath: () => wikiPath,
|
|
370
380
|
getSessionController,
|
|
381
|
+
getWorkspaceRegistry,
|
|
371
382
|
sendToAgentEnabled: () => eff().ui?.sendToAgent?.enabled !== false,
|
|
372
383
|
sendToAgentToken: () => {
|
|
373
384
|
const token = eff().ui?.sendToAgent?.token
|