dsh-tiddlywiki 0.16.19 → 0.16.21

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.
@@ -1,61 +1,64 @@
1
- /**
2
- * Shared client-side read of the effective UI config (mirrors UiDefaultsPublic
3
- * on the host). Used by the input-dock quick-note button, the quick-note entry
4
- * points (which need `quickNoteMode` to decide native/card on click) and any
5
- * other client mount that needs a config value before building DOM.
6
- *
7
- * Cached with a short TTL so per-click reads (dock/FAB) don't hit /status on
8
- * every interaction, while a settings-page change still takes effect within
9
- * seconds without a reload.
10
- *
11
- * @module dsh-tiddlywiki/client/ui-config
12
- */
13
-
14
- import { STATUS_ENDPOINT } from './endpoints.ts'
15
-
16
- export interface UiConfig {
17
- /** 聊天输入框上方的「快速笔记」快捷按钮是否显示(默认 true)。 */
18
- showQuickNoteDock: boolean
19
- /** 点击「快速笔记」后的打开方式:native=直达 TW 原生编辑器;card=Markdown 卡片。 */
20
- quickNoteMode: 'native' | 'card'
21
- /** 左侧侧边栏 TW 入口的显示名称(默认「TiddlyWiki」)。 */
22
- sidebarLabel: string
23
- /** 会话顶部「知识库」Tab 的显示名称(默认「知识库」)。 */
24
- tabLabel: string
25
- /** 是否在会话顶部显示「知识库」Tab(会话相关 wiki 汇总,默认 true)。 */
26
- showSessionTab: boolean
27
- }
28
-
29
- const FALLBACK: UiConfig = { showQuickNoteDock: true, quickNoteMode: 'native', sidebarLabel: 'TiddlyWiki', tabLabel: '知识库', showSessionTab: true }
30
-
31
- const CACHE_TTL_MS = 15_000
32
- let cache: { at: number; value: UiConfig } | undefined
33
-
34
- /** Fetch /status once and read the ui.* fields (backward compatible: a host
35
- * that predates a field simply falls back to the default). Cached for
36
- * `CACHE_TTL_MS`; callers that need a fresh value (settings just saved) may
37
- * pass `{ force: true }`. */
38
- export async function fetchUiConfig(opts: { force?: boolean } = {}): Promise<UiConfig> {
39
- const now = Date.now()
40
- if (!opts.force && cache !== undefined && now - cache.at < CACHE_TTL_MS) return cache.value
41
- try {
42
- const res = await fetch(STATUS_ENDPOINT, { signal: AbortSignal.timeout(5_000) })
43
- if (!res.ok) return FALLBACK
44
- const p = (await res.json()) as { ui?: { showQuickNoteDock?: boolean; quickNoteMode?: 'native' | 'card'; sidebarLabel?: string; tabLabel?: string; showSessionTab?: boolean } }
45
- const value: UiConfig = {
46
- showQuickNoteDock: p.ui?.showQuickNoteDock !== false,
47
- quickNoteMode: p.ui?.quickNoteMode === 'card' ? 'card' : 'native',
48
- sidebarLabel: typeof p.ui?.sidebarLabel === 'string' && p.ui.sidebarLabel.trim().length > 0
49
- ? p.ui.sidebarLabel.trim()
50
- : 'TiddlyWiki',
51
- tabLabel: typeof p.ui?.tabLabel === 'string' && p.ui.tabLabel.trim().length > 0
52
- ? p.ui.tabLabel.trim()
53
- : '知识库',
54
- showSessionTab: p.ui?.showSessionTab !== false,
55
- }
56
- cache = { at: Date.now(), value }
57
- return value
58
- } catch {
59
- return FALLBACK
60
- }
61
- }
1
+ /**
2
+ * Shared client-side read of the effective UI config (mirrors UiDefaultsPublic
3
+ * on the host). Used by the input-dock quick-note button, the quick-note entry
4
+ * points (which need `quickNoteMode` to decide native/card on click) and any
5
+ * other client mount that needs a config value before building DOM.
6
+ *
7
+ * Cached with a short TTL so per-click reads (dock/FAB) don't hit /status on
8
+ * every interaction, while a settings-page change still takes effect within
9
+ * seconds without a reload.
10
+ *
11
+ * @module dsh-tiddlywiki/client/ui-config
12
+ */
13
+
14
+ import { STATUS_ENDPOINT } from './endpoints.ts'
15
+
16
+ export interface UiConfig {
17
+ /** 聊天输入框上方的「快速笔记」快捷按钮是否显示(默认 true)。 */
18
+ showQuickNoteDock: boolean
19
+ /** 点击「快速笔记」后的打开方式:native=直达 TW 原生编辑器;card=Markdown 卡片。 */
20
+ quickNoteMode: 'native' | 'card'
21
+ /** 左侧侧边栏 TW 入口的显示名称(默认「TiddlyWiki」)。 */
22
+ sidebarLabel: string
23
+ /** 会话顶部「知识库」Tab 的显示名称(默认「知识库」)。 */
24
+ tabLabel: string
25
+ /** 是否在会话顶部显示「知识库」Tab(会话相关 wiki 汇总,默认 true)。 */
26
+ showSessionTab: boolean
27
+ /** 是否在 DSH 右侧边栏提供 TiddlyWiki 入口/Tab(默认 true)。 */
28
+ showRightbarTab: boolean
29
+ }
30
+
31
+ const FALLBACK: UiConfig = { showQuickNoteDock: true, quickNoteMode: 'native', sidebarLabel: 'TiddlyWiki', tabLabel: '知识库', showSessionTab: true, showRightbarTab: true }
32
+
33
+ const CACHE_TTL_MS = 15_000
34
+ let cache: { at: number; value: UiConfig } | undefined
35
+
36
+ /** Fetch /status once and read the ui.* fields (backward compatible: a host
37
+ * that predates a field simply falls back to the default). Cached for
38
+ * `CACHE_TTL_MS`; callers that need a fresh value (settings just saved) may
39
+ * pass `{ force: true }`. */
40
+ export async function fetchUiConfig(opts: { force?: boolean } = {}): Promise<UiConfig> {
41
+ const now = Date.now()
42
+ if (!opts.force && cache !== undefined && now - cache.at < CACHE_TTL_MS) return cache.value
43
+ try {
44
+ const res = await fetch(STATUS_ENDPOINT, { signal: AbortSignal.timeout(5_000) })
45
+ if (!res.ok) return FALLBACK
46
+ const p = (await res.json()) as { ui?: { showQuickNoteDock?: boolean; quickNoteMode?: 'native' | 'card'; sidebarLabel?: string; tabLabel?: string; showSessionTab?: boolean; showRightbarTab?: boolean } }
47
+ const value: UiConfig = {
48
+ showQuickNoteDock: p.ui?.showQuickNoteDock !== false,
49
+ quickNoteMode: p.ui?.quickNoteMode === 'card' ? 'card' : 'native',
50
+ sidebarLabel: typeof p.ui?.sidebarLabel === 'string' && p.ui.sidebarLabel.trim().length > 0
51
+ ? p.ui.sidebarLabel.trim()
52
+ : 'TiddlyWiki',
53
+ tabLabel: typeof p.ui?.tabLabel === 'string' && p.ui.tabLabel.trim().length > 0
54
+ ? p.ui.tabLabel.trim()
55
+ : '知识库',
56
+ showSessionTab: p.ui?.showSessionTab !== false,
57
+ showRightbarTab: p.ui?.showRightbarTab !== false,
58
+ }
59
+ cache = { at: Date.now(), value }
60
+ return value
61
+ } catch {
62
+ return FALLBACK
63
+ }
64
+ }
@@ -81,6 +81,13 @@ export interface PluginConfigShape {
81
81
  tabLabel?: string
82
82
  /** 是否在会话顶部显示「知识库」Tab(会话相关 wiki 汇总,默认 true)。 */
83
83
  showSessionTab?: boolean
84
+ /**
85
+ * 是否在 DSH 右侧边栏(new rightbar)提供 TiddlyWiki 入口/Tab(默认
86
+ * true):在 rightbar 首页(guide 页)注册「TiddlyWiki 知识库」入口盒,
87
+ * 点击即在右侧栏以 tab 形式打开完整 TW 编辑器(与聊天并排)。老版本
88
+ * DSH 无 rightbar 时该配置不生效(客户端自动跳过)。
89
+ */
90
+ showRightbarTab?: boolean
84
91
  }
85
92
  uiLanguage?: string
86
93
  [key: string]: unknown
package/src/host/http.ts CHANGED
@@ -1,57 +1,57 @@
1
- /**
2
- * Shared node:http helpers for the DSH webserver routes (routes.ts, admin.ts).
3
- *
4
- * One implementation of body reading + JSON response writing so the route
5
- * layers cannot drift (they previously carried two/three copies).
6
- *
7
- * @module dsh-tiddlywiki/host/http
8
- */
9
- import type { IncomingMessage, ServerResponse } from 'node:http'
10
-
11
- /** Default cap for small JSON bodies (note/restart/config…). */
12
- export const MAX_JSON_BODY_BYTES = 2 * 1024 * 1024
13
-
14
- /** Cap for the /api passthrough body (tiddler content can be large). */
15
- export const MAX_PROXY_BODY_BYTES = 16 * 1024 * 1024
16
-
17
- /** Cap for uploaded file bodies (/tw proxy, /upload). */
18
- export const MAX_UPLOAD_BYTES = 64 * 1024 * 1024
19
-
20
- function readBodyStream(req: IncomingMessage, limit: number, encoding: true): Promise<string>
21
- function readBodyStream(req: IncomingMessage, limit: number, encoding: false): Promise<Buffer>
22
- function readBodyStream(req: IncomingMessage, limit: number, encoding: boolean): Promise<string | Buffer> {
23
- return new Promise((resolveP, rejectP) => {
24
- let size = 0
25
- const chunks: Buffer[] = []
26
- req.on('data', (chunk: Buffer) => {
27
- size += chunk.length
28
- if (size > limit) {
29
- rejectP(new Error('body too large'))
30
- req.destroy()
31
- return
32
- }
33
- chunks.push(chunk)
34
- })
35
- req.on('end', () => {
36
- const buf = Buffer.concat(chunks)
37
- resolveP(encoding ? buf.toString('utf8') : buf)
38
- })
39
- req.on('error', rejectP)
40
- })
41
- }
42
-
43
- /** Read a JSON (utf8) request body up to `limit` bytes (default MAX_JSON_BODY_BYTES). */
44
- export function readBody(req: IncomingMessage, limit = MAX_JSON_BODY_BYTES): Promise<string> {
45
- return readBodyStream(req, limit, true)
46
- }
47
-
48
- /** Read a raw (binary-safe) request body up to `limit` bytes (default MAX_UPLOAD_BYTES). */
49
- export function readBodyBuffer(req: IncomingMessage, limit = MAX_UPLOAD_BYTES): Promise<Buffer> {
50
- return readBodyStream(req, limit, false)
51
- }
52
-
53
- /** Write a JSON response (no-store, utf-8). */
54
- export function json(res: ServerResponse, payload: unknown, status = 200): void {
55
- res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
56
- res.end(JSON.stringify(payload))
57
- }
1
+ /**
2
+ * Shared node:http helpers for the DSH webserver routes (routes.ts, admin.ts).
3
+ *
4
+ * One implementation of body reading + JSON response writing so the route
5
+ * layers cannot drift (they previously carried two/three copies).
6
+ *
7
+ * @module dsh-tiddlywiki/host/http
8
+ */
9
+ import type { IncomingMessage, ServerResponse } from 'node:http'
10
+
11
+ /** Default cap for small JSON bodies (note/restart/config…). */
12
+ export const MAX_JSON_BODY_BYTES = 2 * 1024 * 1024
13
+
14
+ /** Cap for the /api passthrough body (tiddler content can be large). */
15
+ export const MAX_PROXY_BODY_BYTES = 16 * 1024 * 1024
16
+
17
+ /** Cap for uploaded file bodies (/tw proxy, /upload). */
18
+ export const MAX_UPLOAD_BYTES = 64 * 1024 * 1024
19
+
20
+ function readBodyStream(req: IncomingMessage, limit: number, encoding: true): Promise<string>
21
+ function readBodyStream(req: IncomingMessage, limit: number, encoding: false): Promise<Buffer>
22
+ function readBodyStream(req: IncomingMessage, limit: number, encoding: boolean): Promise<string | Buffer> {
23
+ return new Promise((resolveP, rejectP) => {
24
+ let size = 0
25
+ const chunks: Buffer[] = []
26
+ req.on('data', (chunk: Buffer) => {
27
+ size += chunk.length
28
+ if (size > limit) {
29
+ rejectP(new Error('body too large'))
30
+ req.destroy()
31
+ return
32
+ }
33
+ chunks.push(chunk)
34
+ })
35
+ req.on('end', () => {
36
+ const buf = Buffer.concat(chunks)
37
+ resolveP(encoding ? buf.toString('utf8') : buf)
38
+ })
39
+ req.on('error', rejectP)
40
+ })
41
+ }
42
+
43
+ /** Read a JSON (utf8) request body up to `limit` bytes (default MAX_JSON_BODY_BYTES). */
44
+ export function readBody(req: IncomingMessage, limit = MAX_JSON_BODY_BYTES): Promise<string> {
45
+ return readBodyStream(req, limit, true)
46
+ }
47
+
48
+ /** Read a raw (binary-safe) request body up to `limit` bytes (default MAX_UPLOAD_BYTES). */
49
+ export function readBodyBuffer(req: IncomingMessage, limit = MAX_UPLOAD_BYTES): Promise<Buffer> {
50
+ return readBodyStream(req, limit, false)
51
+ }
52
+
53
+ /** Write a JSON response (no-store, utf-8). */
54
+ export function json(res: ServerResponse, payload: unknown, status = 200): void {
55
+ res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
56
+ res.end(JSON.stringify(payload))
57
+ }
@@ -151,6 +151,8 @@ export interface UiDefaultsPublic {
151
151
  tabLabel: string
152
152
  /** 是否在会话顶部显示「知识库」Tab(会话相关 wiki 汇总,默认 true)。 */
153
153
  showSessionTab: boolean
154
+ /** 是否在 DSH 右侧边栏提供 TiddlyWiki 入口/Tab(默认 true)。 */
155
+ showRightbarTab: boolean
154
156
  }
155
157
 
156
158
  export interface RouteDeps {
@@ -25,7 +25,7 @@ export interface HomeIndexItem {
25
25
  }
26
26
 
27
27
  /** The home tiddlers, exactly as seeded (user-owned afterwards). */
28
- export const HOME_INDEX_ITEMS: HomeIndexItem[] = [{"title":"🏠 主页","tags":["索引","TableOfContents"],"type":"text/vnd.tiddlywiki","text":"\\whitespace trim\n\n\\define quadrant-board()\n<div style=\"display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-top:8px;\">\n<div style=\"border:1px solid rgba(128,128,128,0.22); border-radius:10px; padding:8px 12px; background:linear-gradient(135deg,#e74c3c1a,transparent);\">\n<div style=\"font-weight:700; margin-bottom:6px;\">🔴 重要 · 紧急 <span style=\"color:#888; font-weight:400; font-size:0.82em;\">{{{[tag[todo]!tag[done]field:q[q1]count[]]}}} 件</span></div>\n<$list filter=\"[tag[todo]!tag[done]field:q[q1]sort[modified]]\">\n<div style=\"display:flex; gap:6px; align-items:baseline; padding:3px 0; border-bottom:1px dashed rgba(128,128,128,0.15);\">\n<$checkbox tiddler=<<currentTiddler>> tag=\"done\"/>\n<$link to=<<currentTiddler>> style=\"flex:1;\"><$view field=\"title\"/></$link>\n<$list filter=\"[<currentTiddler>get[due]compare:date:lt<today>]\"><span style=\"color:#c0392b; font-size:0.78em;\">逾期</span></$list>\n<$list filter=\"[<currentTiddler>get[due]!is[blank]]\"><span style=\"color:#888; font-size:0.8em;\"><$view field=\"due\" format=\"date\" template=\"MM-DD\"/></span></$list>\n</div>\n</$list>\n<$list filter=\"[tag[todo]!tag[done]field:q[q1]!count[]]\"><div style=\"color:#aaa; font-size:0.85em;\">(暂无)</div></$list>\n</div>\n<div style=\"border:1px solid rgba(128,128,128,0.22); border-radius:10px; padding:8px 12px; background:linear-gradient(135deg,#3498db1a,transparent);\">\n<div style=\"font-weight:700; margin-bottom:6px;\">🔵 重要 · 不紧急 <span style=\"color:#888; font-weight:400; font-size:0.82em;\">{{{[tag[todo]!tag[done]field:q[q2]count[]]}}} 件</span></div>\n<$list filter=\"[tag[todo]!tag[done]field:q[q2]sort[modified]]\">\n<div style=\"display:flex; gap:6px; align-items:baseline; padding:3px 0; border-bottom:1px dashed rgba(128,128,128,0.15);\">\n<$checkbox tiddler=<<currentTiddler>> tag=\"done\"/>\n<$link to=<<currentTiddler>> style=\"flex:1;\"><$view field=\"title\"/></$link>\n<$list filter=\"[<currentTiddler>get[due]compare:date:lt<today>]\"><span style=\"color:#c0392b; font-size:0.78em;\">逾期</span></$list>\n<$list filter=\"[<currentTiddler>get[due]!is[blank]]\"><span style=\"color:#888; font-size:0.8em;\"><$view field=\"due\" format=\"date\" template=\"MM-DD\"/></span></$list>\n</div>\n</$list>\n<$list filter=\"[tag[todo]!tag[done]field:q[q2]!count[]]\"><div style=\"color:#aaa; font-size:0.85em;\">(暂无)</div></$list>\n</div>\n<div style=\"border:1px solid rgba(128,128,128,0.22); border-radius:10px; padding:8px 12px; background:linear-gradient(135deg,#f39c121a,transparent);\">\n<div style=\"font-weight:700; margin-bottom:6px;\">🟠 紧急 · 不重要 <span style=\"color:#888; font-weight:400; font-size:0.82em;\">{{{[tag[todo]!tag[done]field:q[q3]count[]]}}} 件</span></div>\n<$list filter=\"[tag[todo]!tag[done]field:q[q3]sort[modified]]\">\n<div style=\"display:flex; gap:6px; align-items:baseline; padding:3px 0; border-bottom:1px dashed rgba(128,128,128,0.15);\">\n<$checkbox tiddler=<<currentTiddler>> tag=\"done\"/>\n<$link to=<<currentTiddler>> style=\"flex:1;\"><$view field=\"title\"/></$link>\n<$list filter=\"[<currentTiddler>get[due]compare:date:lt<today>]\"><span style=\"color:#c0392b; font-size:0.78em;\">逾期</span></$list>\n<$list filter=\"[<currentTiddler>get[due]!is[blank]]\"><span style=\"color:#888; font-size:0.8em;\"><$view field=\"due\" format=\"date\" template=\"MM-DD\"/></span></$list>\n</div>\n</$list>\n<$list filter=\"[tag[todo]!tag[done]field:q[q3]!count[]]\"><div style=\"color:#aaa; font-size:0.85em;\">(暂无)</div></$list>\n</div>\n<div style=\"border:1px solid rgba(128,128,128,0.22); border-radius:10px; padding:8px 12px; background:linear-gradient(135deg,#95a5a61a,transparent);\">\n<div style=\"font-weight:700; margin-bottom:6px;\">⚪ 不重要 · 不紧急 <span style=\"color:#888; font-weight:400; font-size:0.82em;\">{{{[tag[todo]!tag[done]field:q[q4]count[]]}}} 件</span></div>\n<$list filter=\"[tag[todo]!tag[done]field:q[q4]sort[modified]]\">\n<div style=\"display:flex; gap:6px; align-items:baseline; padding:3px 0; border-bottom:1px dashed rgba(128,128,128,0.15);\">\n<$checkbox tiddler=<<currentTiddler>> tag=\"done\"/>\n<$link to=<<currentTiddler>> style=\"flex:1;\"><$view field=\"title\"/></$link>\n<$list filter=\"[<currentTiddler>get[due]compare:date:lt<today>]\"><span style=\"color:#c0392b; font-size:0.78em;\">逾期</span></$list>\n<$list filter=\"[<currentTiddler>get[due]!is[blank]]\"><span style=\"color:#888; font-size:0.8em;\"><$view field=\"due\" format=\"date\" template=\"MM-DD\"/></span></$list>\n</div>\n</$list>\n<$list filter=\"[tag[todo]!tag[done]field:q[q4]!count[]]\"><div style=\"color:#aaa; font-size:0.85em;\">(暂无)</div></$list>\n</div>\n</div>\n\\end\n\n<$set name=\"today\" value=<<now \"YYYY0MM0DD\">>>\n\n!! 🏠 主页\n\n<div class=\"tc-message-box\">待办看板与知识库入口。</div>\n\n<div style=\"display:flex; gap:8px; flex-wrap:wrap; margin:8px 0;\">\n<$button class=\"tc-btn-invisible tc-tiddlylink\" style=\"padding:6px 16px; border:1px solid rgba(128,128,128,0.35); border-radius:8px; font-weight:600;\">\n<$action-navigate $to=\"所有标签\"/>\n🏷 所有标签\n</$button>\n<$button class=\"tc-btn-invisible tc-tiddlylink\" style=\"padding:6px 16px; border:1px solid rgba(128,128,128,0.35); border-radius:8px; font-weight:600;\">\n<$action-navigate $to=\"所有文章\"/>\n📚 所有文章\n</$button>\n<$button class=\"tc-btn-invisible tc-tiddlylink\" style=\"padding:6px 16px; border:1px solid rgba(128,128,128,0.35); border-radius:8px; font-weight:600;\">\n<$action-navigate $to=\"主题汇总·示例\"/>\n🗂 主题汇总\n</$button>\n</div>\n\n!! ✅ 待办 · 四象限\n\n<div class=\"tc-message-box\">输入一行要事、选好象限,点「➕ 添加」即建任务(默认 Q2 重要·不紧急)。你输入的这句话会直接成为任务标题,首页一目了然。点任务左侧方框即完成(自动加 <code>done</code> 标签并从看板消失)。给任意笔记打上 <code>todo</code> 标签也会被收集到下方「未分类」,补填 <code>q</code> 字段即进入象限。</div>\n\n<div style=\"display:flex; gap:6px; flex-wrap:wrap; align-items:center; margin:10px 0;\">\n<$edit-text tiddler=\"$:/state/todo/title\" tag=\"input\" placeholder=\"快速添加:写一句要做的事,选象限后点 ➕…\" style=\"flex:1; min-width:220px; padding:6px 10px; border-radius:6px; border:1px solid rgba(128,128,128,0.3);\"/>\n<$select tiddler=\"$:/state/todo/quadrant\" default=\"q2\" style=\"padding:6px 10px; border-radius:6px; border:1px solid rgba(128,128,128,0.3);\">\n<option value=\"q1\">Q1 重要·紧急</option>\n<option value=\"q2\">Q2 重要·不紧急</option>\n<option value=\"q3\">Q3 紧急·不重要</option>\n<option value=\"q4\">Q4 不重要·不紧急</option>\n</$select>\n<$button class=\"tc-btn-invisible\" style=\"padding:6px 14px; border-radius:6px; border:1px solid rgba(128,128,128,0.35); font-weight:600;\">\n<$list filter=\"[{$:/state/todo/title}!is[blank]]\">\n<$action-createtiddler $basetitle={{{[{$:/state/todo/title}]}}} tags=\"todo\" text={{{[{$:/state/todo/title}]}}} q={{{[{$:/state/todo/quadrant}!is[blank]else[q2]]}}}/>\n<$action-setfield $tiddler=\"$:/state/todo/title\" text=\"\"/>\n</$list>\n➕ 添加\n</$button>\n</div>\n\n<div style=\"margin:6px 0; color:#555;\">📅 今日到期 <b>{{{[tag[todo]!tag[done]field:due<today>count[]]}}}</b> 件 · ⏰ 已逾期 <b>{{{[tag[todo]!tag[done]get[due]compare:date:lt<today>count[]]}}}</b> 件</div>\n\n<<quadrant-board>>\n\n<$list filter=\"[tag[todo]!tag[done]!has[q]count[]!match[0]]\">\n<div style=\"margin-top:10px; border:1px dashed rgba(128,128,128,0.35); border-radius:10px; padding:8px 12px;\">\n<div style=\"font-weight:700; margin-bottom:6px;\">📥 未分类待办 <span style=\"color:#888; font-weight:400; font-size:0.82em;\">{{{[tag[todo]!tag[done]!has[q]count[]]}}} 件</span></div>\n<$list filter=\"[tag[todo]!tag[done]!has[q]sort[modified]]\">\n<div style=\"display:flex; gap:6px; align-items:baseline; padding:3px 0; border-bottom:1px dashed rgba(128,128,128,0.15);\">\n<$checkbox tiddler=<<currentTiddler>> tag=\"done\"/>\n<$link to=<<currentTiddler>> style=\"flex:1;\"><$view field=\"title\"/></$link>\n<$list filter=\"[<currentTiddler>get[due]compare:date:lt<today>]\"><span style=\"color:#c0392b; font-size:0.78em;\">逾期</span></$list>\n</div>\n</$list>\n</div>\n</$list>\n\n</$set>\n"},{"title":"所有标签","tags":["索引","TableOfContents"],"type":"text/vnd.tiddlywiki","text":"\\whitespace trim\n\n\\define tag-count() [all[tiddlers]!is[system]!tag[agent-written]tag<currentTiddler>count[]]\n\\define tag-list() [all[tiddlers]!is[system]!tag[agent-written]tags[]!prefix[$:/tags/]!match[索引]]\n\n\\define agent-notes-pure() [all[tiddlers]!is[system]tag[agent-written]!tag[human-edited]count[]]\n\\define agent-notes-mixed() [all[tiddlers]!is[system]tag[agent-written]tag[human-edited]count[]]\n\\define agent-tags-pure() [all[tiddlers]!is[system]tag[agent-written]!tag[human-edited]tags[]!prefix[$:/tags/]!match[索引]!match[agent-written]!match[human-edited]]\n\\define agent-tags-mixed() [all[tiddlers]!is[system]tag[agent-written]tag[human-edited]tags[]!prefix[$:/tags/]!match[索引]!match[agent-written]!match[human-edited]]\n\\define agent-count-pure() [all[tiddlers]!is[system]tag[agent-written]!tag[human-edited]tag<currentTiddler>count[]]\n\\define agent-count-mixed() [all[tiddlers]!is[system]tag[agent-written]tag[human-edited]tag<currentTiddler>count[]]\n\n<$button class=\"tc-btn-invisible tc-tiddlylink\" style=\"margin:6px 0; padding:4px 10px; border:1px solid rgba(128,128,128,0.3); border-radius:6px; font-size:0.9em;\">\n<$action-navigate $to=\"🏠 主页\"/>\n← 回主页\n</$button>\n\n!! 🏷 所有标签\n\n<div class=\"tc-message-box\">按笔记数量从多到少排序,仅统计人类笔记(Agent 撰写的笔记已排除,见下方「🤖 Agent 撰写的标签」)。点击标签,查看包含该标签的所有笔记。</div>\n\n<div style=\"margin-top:12px;\">\n<$list filter=\"[subfilter<tag-list>] +[!sortsub:number<tag-count>]\">\n<$set name=\"count\" value={{{ [subfilter<tag-count>] }}}>\n<div style=\"padding:7px 2px; border-bottom:1px solid rgba(128,128,128,0.15);\">\n<$button set=\"$:/state/tag\" setTo=<<currentTiddler>> class=\"tc-btn-invisible tc-tiddlylink\" style=\"width:100%; text-align:left;\">\n<$action-navigate $to=\"标签笔记\"/>\n<span style=\"font-weight:600; font-size:1.05em;\"><<currentTiddler>></span>\n<span style=\"color:#888; font-size:0.85em; margin-left:8px;\"><<count>> 篇</span>\n</$button>\n</div>\n</$set>\n</$list>\n</div>\n\n!! 🤖 Agent 撰写的标签\n\n<div class=\"tc-message-box\">以下标签来自带有 <code>agent-written</code> 标签(由 Agent 撰写)的笔记,与上方主列表分开统计。若某篇 Agent 笔记又被人类编辑过,请给它补打 <code>human-edited</code> 标签,即归入下方「Agent + 人工」档。</div>\n\n<div style=\"font-weight:700; margin:8px 0 4px;\">🦾 纯 Agent <span style=\"color:#888; font-weight:400; font-size:0.82em;\">{{{[subfilter<agent-notes-pure>]}}} 篇</span></div>\n<div style=\"margin-left:10px;\">\n<$list filter=\"[subfilter<agent-tags-pure>] +[!sortsub:number<agent-count-pure>]\">\n<$set name=\"count\" value={{{ [subfilter<agent-count-pure>] }}}>\n<div style=\"padding:7px 2px; border-bottom:1px solid rgba(128,128,128,0.15);\">\n<$button set=\"$:/state/tag\" setTo=<<currentTiddler>> class=\"tc-btn-invisible tc-tiddlylink\" style=\"width:100%; text-align:left;\">\n<$action-navigate $to=\"标签笔记\"/>\n<span style=\"font-weight:600; font-size:1.05em;\"><<currentTiddler>></span>\n<span style=\"color:#888; font-size:0.85em; margin-left:8px;\"><<count>> 篇</span>\n</$button>\n</div>\n</$set>\n</$list>\n<$list filter=\"[subfilter<agent-tags-pure>!count[]]\"><div style=\"color:#aaa; font-size:0.85em;\">(暂无纯 Agent 笔记)</div></$list>\n</div>\n\n<div style=\"font-weight:700; margin:10px 0 4px;\">🤝 Agent + 人工 <span style=\"color:#888; font-weight:400; font-size:0.82em;\">{{{[subfilter<agent-notes-mixed>]}}} 篇</span></div>\n<div style=\"margin-left:10px;\">\n<$list filter=\"[subfilter<agent-tags-mixed>] +[!sortsub:number<agent-count-mixed>]\">\n<$set name=\"count\" value={{{ [subfilter<agent-count-mixed>] }}}>\n<div style=\"padding:7px 2px; border-bottom:1px solid rgba(128,128,128,0.15);\">\n<$button set=\"$:/state/tag\" setTo=<<currentTiddler>> class=\"tc-btn-invisible tc-tiddlylink\" style=\"width:100%; text-align:left;\">\n<$action-navigate $to=\"标签笔记\"/>\n<span style=\"font-weight:600; font-size:1.05em;\"><<currentTiddler>></span>\n<span style=\"color:#888; font-size:0.85em; margin-left:8px;\"><<count>> 篇</span>\n</$button>\n</div>\n</$set>\n</$list>\n<$list filter=\"[subfilter<agent-tags-mixed>!count[]]\"><div style=\"color:#aaa; font-size:0.85em;\">(暂无「Agent + 人工」笔记)</div></$list>\n</div>\n"},{"title":"标签笔记","tags":["索引"],"type":"text/vnd.tiddlywiki","text":"\\whitespace trim\n\n<$set name=\"sel\" value={{{ [{$:/state/tag}] }}}>\n\n!! 标签:<<sel>>\n\n<div class=\"tc-message-box\">包含标签 <strong><<sel>></strong> 的所有笔记(自动收集,按最近修改排序)。</div>\n\n<ul>\n<$list filter=\"[all[tiddlers]!is[system]!has[draft.of]tag<sel>] +[!sort[modified]]\">\n<li><$link to=<<currentTiddler>>><$view field=\"title\"/></$link><span style=\"color:#aaa; font-size:0.85em;\"> · <$view field=\"modified\" format=\"relativedate\"/></span></li>\n</$list>\n</ul>\n\n</$set>\n"}]
28
+ export const HOME_INDEX_ITEMS: HomeIndexItem[] = [{"title":"🏠 主页","tags":["索引","TableOfContents"],"type":"text/vnd.tiddlywiki","text":"\\whitespace trim\n\n\\define quadrant-board()\n<div style=\"display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-top:8px;\">\n<div style=\"border:1px solid rgba(128,128,128,0.22); border-radius:10px; padding:8px 12px; background:linear-gradient(135deg,#e74c3c1a,transparent);\">\n<div style=\"font-weight:700; margin-bottom:6px;\">🔴 重要 · 紧急 <span style=\"color:#888; font-weight:400; font-size:0.82em;\">{{{[tag[todo]!tag[done]field:q[q1]count[]]}}} 件</span></div>\n<$list filter=\"[tag[todo]!tag[done]field:q[q1]sort[modified]]\">\n<div style=\"display:flex; gap:6px; align-items:baseline; padding:3px 0; border-bottom:1px dashed rgba(128,128,128,0.15);\">\n<$checkbox tiddler=<<currentTiddler>> tag=\"done\"/>\n<$link to=<<currentTiddler>> style=\"flex:1;\"><$view field=\"title\"/></$link>\n<$list filter=\"[<currentTiddler>get[due]compare:date:lt<today>]\"><span style=\"color:#c0392b; font-size:0.78em;\">逾期</span></$list>\n<$list filter=\"[<currentTiddler>get[due]!is[blank]]\"><span style=\"color:#888; font-size:0.8em;\"><$view field=\"due\" format=\"date\" template=\"MM-DD\"/></span></$list>\n</div>\n</$list>\n<$list filter=\"[tag[todo]!tag[done]field:q[q1]!count[]]\"><div style=\"color:#aaa; font-size:0.85em;\">(暂无)</div></$list>\n</div>\n<div style=\"border:1px solid rgba(128,128,128,0.22); border-radius:10px; padding:8px 12px; background:linear-gradient(135deg,#3498db1a,transparent);\">\n<div style=\"font-weight:700; margin-bottom:6px;\">🔵 重要 · 不紧急 <span style=\"color:#888; font-weight:400; font-size:0.82em;\">{{{[tag[todo]!tag[done]field:q[q2]count[]]}}} 件</span></div>\n<$list filter=\"[tag[todo]!tag[done]field:q[q2]sort[modified]]\">\n<div style=\"display:flex; gap:6px; align-items:baseline; padding:3px 0; border-bottom:1px dashed rgba(128,128,128,0.15);\">\n<$checkbox tiddler=<<currentTiddler>> tag=\"done\"/>\n<$link to=<<currentTiddler>> style=\"flex:1;\"><$view field=\"title\"/></$link>\n<$list filter=\"[<currentTiddler>get[due]compare:date:lt<today>]\"><span style=\"color:#c0392b; font-size:0.78em;\">逾期</span></$list>\n<$list filter=\"[<currentTiddler>get[due]!is[blank]]\"><span style=\"color:#888; font-size:0.8em;\"><$view field=\"due\" format=\"date\" template=\"MM-DD\"/></span></$list>\n</div>\n</$list>\n<$list filter=\"[tag[todo]!tag[done]field:q[q2]!count[]]\"><div style=\"color:#aaa; font-size:0.85em;\">(暂无)</div></$list>\n</div>\n<div style=\"border:1px solid rgba(128,128,128,0.22); border-radius:10px; padding:8px 12px; background:linear-gradient(135deg,#f39c121a,transparent);\">\n<div style=\"font-weight:700; margin-bottom:6px;\">🟠 紧急 · 不重要 <span style=\"color:#888; font-weight:400; font-size:0.82em;\">{{{[tag[todo]!tag[done]field:q[q3]count[]]}}} 件</span></div>\n<$list filter=\"[tag[todo]!tag[done]field:q[q3]sort[modified]]\">\n<div style=\"display:flex; gap:6px; align-items:baseline; padding:3px 0; border-bottom:1px dashed rgba(128,128,128,0.15);\">\n<$checkbox tiddler=<<currentTiddler>> tag=\"done\"/>\n<$link to=<<currentTiddler>> style=\"flex:1;\"><$view field=\"title\"/></$link>\n<$list filter=\"[<currentTiddler>get[due]compare:date:lt<today>]\"><span style=\"color:#c0392b; font-size:0.78em;\">逾期</span></$list>\n<$list filter=\"[<currentTiddler>get[due]!is[blank]]\"><span style=\"color:#888; font-size:0.8em;\"><$view field=\"due\" format=\"date\" template=\"MM-DD\"/></span></$list>\n</div>\n</$list>\n<$list filter=\"[tag[todo]!tag[done]field:q[q3]!count[]]\"><div style=\"color:#aaa; font-size:0.85em;\">(暂无)</div></$list>\n</div>\n<div style=\"border:1px solid rgba(128,128,128,0.22); border-radius:10px; padding:8px 12px; background:linear-gradient(135deg,#95a5a61a,transparent);\">\n<div style=\"font-weight:700; margin-bottom:6px;\">⚪ 不重要 · 不紧急 <span style=\"color:#888; font-weight:400; font-size:0.82em;\">{{{[tag[todo]!tag[done]field:q[q4]count[]]}}} 件</span></div>\n<$list filter=\"[tag[todo]!tag[done]field:q[q4]sort[modified]]\">\n<div style=\"display:flex; gap:6px; align-items:baseline; padding:3px 0; border-bottom:1px dashed rgba(128,128,128,0.15);\">\n<$checkbox tiddler=<<currentTiddler>> tag=\"done\"/>\n<$link to=<<currentTiddler>> style=\"flex:1;\"><$view field=\"title\"/></$link>\n<$list filter=\"[<currentTiddler>get[due]compare:date:lt<today>]\"><span style=\"color:#c0392b; font-size:0.78em;\">逾期</span></$list>\n<$list filter=\"[<currentTiddler>get[due]!is[blank]]\"><span style=\"color:#888; font-size:0.8em;\"><$view field=\"due\" format=\"date\" template=\"MM-DD\"/></span></$list>\n</div>\n</$list>\n<$list filter=\"[tag[todo]!tag[done]field:q[q4]!count[]]\"><div style=\"color:#aaa; font-size:0.85em;\">(暂无)</div></$list>\n</div>\n</div>\n\\end\n\n<$set name=\"today\" value=<<now \"YYYY0MM0DD\">>>\n\n!! 🏠 主页\n\n<div class=\"tc-message-box\">待办看板与知识库入口。</div>\n\n<div style=\"display:flex; gap:8px; flex-wrap:wrap; margin:8px 0;\">\n<$button class=\"tc-btn-invisible tc-tiddlylink\" style=\"padding:6px 16px; border:1px solid rgba(128,128,128,0.35); border-radius:8px; font-weight:600;\">\n<$action-navigate $to=\"所有标签\"/>\n🏷 所有标签\n</$button>\n<$button class=\"tc-btn-invisible tc-tiddlylink\" style=\"padding:6px 16px; border:1px solid rgba(128,128,128,0.35); border-radius:8px; font-weight:600;\">\n<$action-navigate $to=\"所有文章\"/>\n📚 所有文章\n</$button>\n<$button class=\"tc-btn-invisible tc-tiddlylink\" style=\"padding:6px 16px; border:1px solid rgba(128,128,128,0.35); border-radius:8px; font-weight:600;\">\n<$action-navigate $to=\"主题汇总·示例\"/>\n🗂 主题汇总\n</$button>\n</div>\n\n!! ✍️ 快速记笔记(完整编辑器)\n\n<div class=\"tc-message-box\">直接在首页写笔记:填标题、用工具栏写正文(支持 Markdown:**加粗**、*斜体*、# 标题、- 列表、> 引用 等)、加标签,点「💾 保存」即创建新笔记(默认 Markdown,标题留空用当前时间)。勾选「同时加入待办」并选象限,可一并进入下方四象限看板。</div>\n\n<div style=\"border:1px solid rgba(128,128,128,0.22); border-radius:10px; padding:10px 12px; margin:10px 0;\">\n\n<div style=\"display:flex; gap:6px; align-items:center; margin-bottom:6px;\">\n<$edit-text tiddler=\"$:/state/home-note/title\" tag=\"input\" placeholder=\"标题(留空用当前时间)\" style=\"flex:1; min-width:200px; padding:6px 10px; border-radius:6px; border:1px solid rgba(128,128,128,0.3);\"/>\n</div>\n\n<$edit-text tiddler=\"$:/state/home-note/text\" tag=\"textarea\" class=\"tc-edit-texteditor\" autoHeight=\"yes\" minHeight=\"150px\" placeholder=\"正文:支持 **加粗**、*斜体*、# 标题、- 列表、> 引用 等 Markdown 语法\" style=\"width:100%; box-sizing:border-box; padding:6px 10px; border-radius:6px; border:1px solid rgba(128,128,128,0.3);\">\n<$button class=\"tc-btn-invisible\" tooltip=\"加粗\" style=\"padding:2px 8px; font-weight:700;\"><$action-sendmessage $message=\"tm-edit-text-operation\" $param=\"wrap-selection\" prefix=\"**\" suffix=\"**\" trimSelection=\"yes\"/>B</$button>\n<$button class=\"tc-btn-invisible\" tooltip=\"斜体\" style=\"padding:2px 8px; font-style:italic;\"><$action-sendmessage $message=\"tm-edit-text-operation\" $param=\"wrap-selection\" prefix=\"*\" suffix=\"*\" trimSelection=\"yes\"/>I</$button>\n<$button class=\"tc-btn-invisible\" tooltip=\"删除线\" style=\"padding:2px 8px;\"><$action-sendmessage $message=\"tm-edit-text-operation\" $param=\"wrap-selection\" prefix=\"~~\" suffix=\"~~\" trimSelection=\"yes\"/>S</$button>\n<$button class=\"tc-btn-invisible\" tooltip=\"标题\" style=\"padding:2px 8px;\"><$action-sendmessage $message=\"tm-edit-text-operation\" $param=\"prefix-lines\" character=\"#\" count=\"1\"/>H</$button>\n<$button class=\"tc-btn-invisible\" tooltip=\"无序列表\" style=\"padding:2px 8px;\"><$action-sendmessage $message=\"tm-edit-text-operation\" $param=\"prefix-lines\" character=\"-\" count=\"1\"/>•</$button>\n<$button class=\"tc-btn-invisible\" tooltip=\"有序列表\" style=\"padding:2px 8px;\"><$action-sendmessage $message=\"tm-edit-text-operation\" $param=\"prefix-lines\" character=\"1.\" count=\"1\"/>1.</$button>\n<$button class=\"tc-btn-invisible\" tooltip=\"引用\" style=\"padding:2px 8px;\"><$action-sendmessage $message=\"tm-edit-text-operation\" $param=\"prefix-lines\" character=\">\" count=\"1\"/>❝</$button>\n<$button class=\"tc-btn-invisible\" tooltip=\"行内代码\" style=\"padding:2px 8px;\"><$action-sendmessage $message=\"tm-edit-text-operation\" $param=\"wrap-selection\" prefix=\"`\" suffix=\"`\" trimSelection=\"yes\"/>&lt;/&gt;</$button>\n</$edit-text>\n\n<div style=\"display:flex; gap:6px; flex-wrap:wrap; align-items:center; margin-top:6px;\">\n<$edit-text tiddler=\"$:/state/home-note/tags\" tag=\"input\" placeholder=\"标签(逗号分隔,可选)\" style=\"flex:1; min-width:160px; padding:6px 10px; border-radius:6px; border:1px solid rgba(128,128,128,0.3);\"/>\n<$checkbox tiddler=\"$:/state/home-note/todo\" field=\"text\" checked=\"yes\" unchecked=\"no\"> 同时加入待办</$checkbox>\n<$select tiddler=\"$:/state/home-note/quadrant\" default=\"q2\" style=\"padding:6px 10px; border-radius:6px; border:1px solid rgba(128,128,128,0.3);\">\n<option value=\"q1\">Q1 重要·紧急</option>\n<option value=\"q2\">Q2 重要·不紧急</option>\n<option value=\"q3\">Q3 紧急·不重要</option>\n<option value=\"q4\">Q4 不重要·不紧急</option>\n</$select>\n</div>\n\n<div style=\"display:flex; gap:6px; align-items:center; margin-top:8px; flex-wrap:wrap;\">\n<$button class=\"tc-btn-invisible\" style=\"padding:6px 16px; border-radius:6px; border:1px solid rgba(128,128,128,0.35); font-weight:600; background:rgba(128,128,128,0.06);\">\n<$list filter=\"[{$:/state/home-note/title}!is[blank]] [{$:/state/home-note/text}!is[blank]]\">\n<$action-createtiddler\n$basetitle={{{ [{$:/state/home-note/title}!is[blank]then{$:/state/home-note/title}] ~[<now \"YYYY-MM-DD-HHmm\">] }}}\ntext={{{ [{$:/state/home-note/text}] }}}\ntags={{{ [{$:/state/home-note/tags}split[,]split[,]trim[]] [{$:/state/home-note/todo}match[yes]then[[todo]]] +[join[ ]] }}}\nq={{{ [{$:/state/home-note/todo}match[yes]then{$:/state/home-note/quadrant}!is[blank]] ~[{$:/state/home-note/todo}match[yes]then[q2]] }}}\ntype=\"text/markdown\"\n$savetitle=\"$:/state/home-note/last-created\"/>\n<$action-setfield $tiddler=\"$:/state/home-note/title\" text=\"\"/>\n<$action-setfield $tiddler=\"$:/state/home-note/text\" text=\"\"/>\n<$action-setfield $tiddler=\"$:/state/home-note/tags\" text=\"\"/>\n</$list>\n💾 保存\n</$button>\n<$button class=\"tc-btn-invisible\" tooltip=\"清空草稿\" style=\"padding:6px 12px; border-radius:6px; border:1px solid rgba(128,128,128,0.3);\">\n<$action-setfield $tiddler=\"$:/state/home-note/title\" text=\"\"/>\n<$action-setfield $tiddler=\"$:/state/home-note/text\" text=\"\"/>\n<$action-setfield $tiddler=\"$:/state/home-note/tags\" text=\"\"/>\n🗑 清空\n</$button>\n<$list filter=\"[{$:/state/home-note/last-created}!is[blank]]\">\n<span style=\"color:#2e7d32; font-size:0.9em;\">✅ 已保存:<$link to={{{ [{$:/state/home-note/last-created}] }}}>{{{$:/state/home-note/last-created}}}</$link></span>\n</$list>\n</div>\n\n</div>\n\n!! ✅ 待办 · 四象限\n\n<div class=\"tc-message-box\">在上方「快速记笔记」勾选「同时加入待办」即建任务(默认 Q2 重要·不紧急);也可以给任意笔记打上 <code>todo</code> 标签收集到下方。点任务左侧方框即完成(自动加 <code>done</code> 标签并从看板消失)。给任意笔记打上 <code>todo</code> 标签也会被收集到下方「未分类」,补填 <code>q</code> 字段即进入象限。</div>\n\n<div style=\"margin:6px 0; color:#555;\">📅 今日到期 <b>{{{[tag[todo]!tag[done]field:due<today>count[]]}}}</b> 件 · ⏰ 已逾期 <b>{{{[tag[todo]!tag[done]get[due]compare:date:lt<today>count[]]}}}</b> 件</div>\n\n<<quadrant-board>>\n\n<$list filter=\"[tag[todo]!tag[done]!has[q]count[]!match[0]]\">\n<div style=\"margin-top:10px; border:1px dashed rgba(128,128,128,0.35); border-radius:10px; padding:8px 12px;\">\n<div style=\"font-weight:700; margin-bottom:6px;\">📥 未分类待办 <span style=\"color:#888; font-weight:400; font-size:0.82em;\">{{{[tag[todo]!tag[done]!has[q]count[]]}}} 件</span></div>\n<$list filter=\"[tag[todo]!tag[done]!has[q]sort[modified]]\">\n<div style=\"display:flex; gap:6px; align-items:baseline; padding:3px 0; border-bottom:1px dashed rgba(128,128,128,0.15);\">\n<$checkbox tiddler=<<currentTiddler>> tag=\"done\"/>\n<$link to=<<currentTiddler>> style=\"flex:1;\"><$view field=\"title\"/></$link>\n<$list filter=\"[<currentTiddler>get[due]compare:date:lt<today>]\"><span style=\"color:#c0392b; font-size:0.78em;\">逾期</span></$list>\n</div>\n</$list>\n</div>\n</$list>\n\n</$set>"},{"title":"所有标签","tags":["索引","TableOfContents"],"type":"text/vnd.tiddlywiki","text":"\\whitespace trim\n\n\\define tag-count() [all[tiddlers]!is[system]!tag[agent-written]tag<currentTiddler>count[]]\n\\define tag-list() [all[tiddlers]!is[system]!tag[agent-written]tags[]!prefix[$:/tags/]!match[索引]]\n\n\\define agent-notes-pure() [all[tiddlers]!is[system]tag[agent-written]!tag[human-edited]count[]]\n\\define agent-notes-mixed() [all[tiddlers]!is[system]tag[agent-written]tag[human-edited]count[]]\n\\define agent-tags-pure() [all[tiddlers]!is[system]tag[agent-written]!tag[human-edited]tags[]!prefix[$:/tags/]!match[索引]!match[agent-written]!match[human-edited]]\n\\define agent-tags-mixed() [all[tiddlers]!is[system]tag[agent-written]tag[human-edited]tags[]!prefix[$:/tags/]!match[索引]!match[agent-written]!match[human-edited]]\n\\define agent-count-pure() [all[tiddlers]!is[system]tag[agent-written]!tag[human-edited]tag<currentTiddler>count[]]\n\\define agent-count-mixed() [all[tiddlers]!is[system]tag[agent-written]tag[human-edited]tag<currentTiddler>count[]]\n\n<$button class=\"tc-btn-invisible tc-tiddlylink\" style=\"margin:6px 0; padding:4px 10px; border:1px solid rgba(128,128,128,0.3); border-radius:6px; font-size:0.9em;\">\n<$action-navigate $to=\"🏠 主页\"/>\n← 回主页\n</$button>\n\n!! 🏷 所有标签\n\n<div class=\"tc-message-box\">按笔记数量从多到少排序,仅统计人类笔记(Agent 撰写的笔记已排除,见下方「🤖 Agent 撰写的标签」)。点击标签,查看包含该标签的所有笔记。</div>\n\n<div style=\"margin-top:12px;\">\n<$list filter=\"[subfilter<tag-list>] +[!sortsub:number<tag-count>]\">\n<$set name=\"count\" value={{{ [subfilter<tag-count>] }}}>\n<div style=\"padding:7px 2px; border-bottom:1px solid rgba(128,128,128,0.15);\">\n<$button set=\"$:/state/tag\" setTo=<<currentTiddler>> class=\"tc-btn-invisible tc-tiddlylink\" style=\"width:100%; text-align:left;\">\n<$action-navigate $to=\"标签笔记\"/>\n<span style=\"font-weight:600; font-size:1.05em;\"><<currentTiddler>></span>\n<span style=\"color:#888; font-size:0.85em; margin-left:8px;\"><<count>> 篇</span>\n</$button>\n</div>\n</$set>\n</$list>\n</div>\n\n!! 🤖 Agent 撰写的标签\n\n<div class=\"tc-message-box\">以下标签来自带有 <code>agent-written</code> 标签(由 Agent 撰写)的笔记,与上方主列表分开统计。若某篇 Agent 笔记又被人类编辑过,请给它补打 <code>human-edited</code> 标签,即归入下方「Agent + 人工」档。</div>\n\n<div style=\"font-weight:700; margin:8px 0 4px;\">🦾 纯 Agent <span style=\"color:#888; font-weight:400; font-size:0.82em;\">{{{[subfilter<agent-notes-pure>]}}} 篇</span></div>\n<div style=\"margin-left:10px;\">\n<$list filter=\"[subfilter<agent-tags-pure>] +[!sortsub:number<agent-count-pure>]\">\n<$set name=\"count\" value={{{ [subfilter<agent-count-pure>] }}}>\n<div style=\"padding:7px 2px; border-bottom:1px solid rgba(128,128,128,0.15);\">\n<$button set=\"$:/state/tag\" setTo=<<currentTiddler>> class=\"tc-btn-invisible tc-tiddlylink\" style=\"width:100%; text-align:left;\">\n<$action-navigate $to=\"标签笔记\"/>\n<span style=\"font-weight:600; font-size:1.05em;\"><<currentTiddler>></span>\n<span style=\"color:#888; font-size:0.85em; margin-left:8px;\"><<count>> 篇</span>\n</$button>\n</div>\n</$set>\n</$list>\n<$list filter=\"[subfilter<agent-tags-pure>!count[]]\"><div style=\"color:#aaa; font-size:0.85em;\">(暂无纯 Agent 笔记)</div></$list>\n</div>\n\n<div style=\"font-weight:700; margin:10px 0 4px;\">🤝 Agent + 人工 <span style=\"color:#888; font-weight:400; font-size:0.82em;\">{{{[subfilter<agent-notes-mixed>]}}} 篇</span></div>\n<div style=\"margin-left:10px;\">\n<$list filter=\"[subfilter<agent-tags-mixed>] +[!sortsub:number<agent-count-mixed>]\">\n<$set name=\"count\" value={{{ [subfilter<agent-count-mixed>] }}}>\n<div style=\"padding:7px 2px; border-bottom:1px solid rgba(128,128,128,0.15);\">\n<$button set=\"$:/state/tag\" setTo=<<currentTiddler>> class=\"tc-btn-invisible tc-tiddlylink\" style=\"width:100%; text-align:left;\">\n<$action-navigate $to=\"标签笔记\"/>\n<span style=\"font-weight:600; font-size:1.05em;\"><<currentTiddler>></span>\n<span style=\"color:#888; font-size:0.85em; margin-left:8px;\"><<count>> 篇</span>\n</$button>\n</div>\n</$set>\n</$list>\n<$list filter=\"[subfilter<agent-tags-mixed>!count[]]\"><div style=\"color:#aaa; font-size:0.85em;\">(暂无「Agent + 人工」笔记)</div></$list>\n</div>\n"},{"title":"标签笔记","tags":["索引"],"type":"text/vnd.tiddlywiki","text":"\\whitespace trim\n\n<$set name=\"sel\" value={{{ [{$:/state/tag}] }}}>\n\n!! 标签:<<sel>>\n\n<div class=\"tc-message-box\">包含标签 <strong><<sel>></strong> 的所有笔记(自动收集,按最近修改排序)。</div>\n\n<ul>\n<$list filter=\"[all[tiddlers]!is[system]!has[draft.of]tag<sel>] +[!sort[modified]]\">\n<li><$link to=<<currentTiddler>>><$view field=\"title\"/></$link><span style=\"color:#aaa; font-size:0.85em;\"> · <$view field=\"modified\" format=\"relativedate\"/></span></li>\n</$list>\n</ul>\n\n</$set>\n"}]
29
29
 
30
30
  /**
31
31
  * Seed the home/index tiddlers exactly once per wiki (mirrors the doc-note
@@ -1,66 +1,66 @@
1
- /**
2
- * Generated from the wiki bundle (do not hand-edit the constant).
3
- * Source: render.bundle.json
4
- *
5
- * The "原生渲染路由" TW plugin, packaged as a TiddlyWiki plugin bundle
6
- * (`{"tiddlers": {...}}`), seeded into fresh wikis by seedRenderRoute. It
7
- * registers a server-side route module (`server-routes/render.js`,
8
- * module-type: route) that renders wiki text into a pure HTML fragment with
9
- * internal links rewritten to the same-origin DSH proxy hash.
10
- *
11
- * @module dsh-tiddlywiki/host/seed-render
12
- */
13
- import type { TiddlyWebClient } from './tw-api.ts'
14
-
15
- /** The packaged plugin tiddler title (a TW system tiddler, type application/json). */
16
- export const RENDER_PLUGIN_TITLE = '$:/plugins/dsh/render'
17
-
18
- /** One-time marker: presence means "the route was offered once — hands off". */
19
- export const RENDER_MARKER_TITLE = '$:/plugins/dsh-tiddlywiki/seed-render'
20
-
21
- /** The bundle's JSON text (`{"tiddlers": {...}}`), exactly as TW stores it. */
22
- export const RENDER_BUNDLE_TEXT = "{\n \"tiddlers\": {\n \"$:/plugins/dsh/render/plugin.info\": {\n \"title\": \"$:/plugins/dsh/render/plugin.info\",\n \"type\": \"application/json\",\n \"text\": \"{\\\"title\\\":\\\"$:/plugins/dsh/render\\\",\\\"name\\\":\\\"DSH Wiki Render\\\",\\\"description\\\":\\\"把 wiki 文本原生渲染成 HTML 片段(POST /render 服务端路由),供 DSH 回复流工具卡与 wiki 链接跳转使用\\\",\\\"author\\\":\\\"dsh-tiddlywiki\\\",\\\"version\\\":\\\"0.1.0\\\",\\\"plugin-type\\\":\\\"plugin\\\"}\"\n },\n \"$:/plugins/dsh/render/server-routes/render.js\": {\n \"title\": \"$:/plugins/dsh/render/server-routes/render.js\",\n \"type\": \"application/javascript\",\n \"module-type\": \"route\",\n \"text\": \"/*\\\\\\ntitle: $:/plugins/dsh/render/server-routes/render.js\\ntype: application/javascript\\nmodule-type: route\\n\\nPOST /render — native TiddlyWiki → HTML fragment for the DSH reply stream\\n\\nBody (JSON in state.data):\\n { title } render an existing tiddler's wikified body\\n { text, contextTitle?, parseAsInline? }\\n render arbitrary wiki text as a fragment\\n\\\\*/\\n\\\"use strict\\\";\\n\\n/*\\nThe fragment's internal wiki links are rewritten to the SAME-ORIGIN DSH proxy\\nhash (`/dsh-tiddlywiki/tw/#<title>`) by overriding the `tv-wikilink-template`\\nvariable while rendering. Verified against tiddlywiki 5.4.1's link widget:\\n`link.js` expands `$uri_encoded$` (encodeURIComponentExtended) into the href,\\nso the default `#Title` becomes `/dsh-tiddlywiki/tw/#Title`. The DSH page's\\ndocument-level click interceptor + the embedded TW's hash navigation\\n(story.js reads the hash via decodeURIComponentSafe) both handle that href —\\nno post-hoc string rewriting needed.\\n*/\\nvar WIKILINK_TEMPLATE = \\\"/dsh-tiddlywiki/tw/#$uri_encoded$\\\";\\n\\nexports.methods = [\\\"POST\\\"];\\n\\nexports.path = /^\\\\/render$/;\\n\\nexports.info = {\\n\\tpriority: 100\\n};\\n\\nfunction sendJson(response,status,payload) {\\n\\tresponse.writeHead(status,{\\\"Content-Type\\\":\\\"application/json; charset=utf-8\\\",\\\"Cache-Control\\\":\\\"no-store\\\"});\\n\\tresponse.end(JSON.stringify(payload));\\n}\\n\\nexports.handler = function(request,response,state) {\\n\\tvar body;\\n\\ttry {\\n\\t\\tbody = JSON.parse(state.data || \\\"{}\\\");\\n\\t} catch(e) {\\n\\t\\tsendJson(response,400,{ok:false,error:\\\"invalid JSON body\\\"});\\n\\t\\treturn;\\n\\t}\\n\\ttry {\\n\\t\\tvar variables = {\\n\\t\\t\\t\\\"tv-wikilink-template\\\": WIKILINK_TEMPLATE\\n\\t\\t};\\n\\t\\tif(typeof body.title === \\\"string\\\" && body.title.length > 0) {\\n\\t\\t\\t// Render an existing tiddler's wikified body (block parse, so\\n\\t\\t\\t// headings / tables / lists render natively).\\n\\t\\t\\tvar tiddler = state.wiki.getTiddler(body.title);\\n\\t\\t\\tif(!tiddler) {\\n\\t\\t\\t\\tsendJson(response,404,{ok:false,notFound:true,title:body.title});\\n\\t\\t\\t\\treturn;\\n\\t\\t\\t}\\n\\t\\t\\tvariables.currentTiddler = body.title;\\n\\t\\t\\tvar html = state.wiki.renderText(\\\"text/html\\\",\\\"text/vnd.tiddlywiki\\\",tiddler.fields.text,{\\n\\t\\t\\t\\tparseAsInline: false,\\n\\t\\t\\t\\tvariables: variables\\n\\t\\t\\t});\\n\\t\\t\\tresponse.writeHead(200,{\\\"Content-Type\\\":\\\"text/html; charset=utf-8\\\",\\\"Cache-Control\\\":\\\"no-store\\\"});\\n\\t\\t\\tresponse.end(html);\\n\\t\\t\\treturn;\\n\\t\\t}\\n\\t\\tif(typeof body.text === \\\"string\\\") {\\n\\t\\t\\t// Render arbitrary wiki text (contextTitle gives currentTiddler so\\n\\t\\t\\t// links/transclusions resolve in that tiddler's context).\\n\\t\\t\\tif(typeof body.contextTitle === \\\"string\\\" && body.contextTitle.length > 0) {\\n\\t\\t\\t\\tvariables.currentTiddler = body.contextTitle;\\n\\t\\t\\t}\\n\\t\\t\\tvar inline = body.parseAsInline === true;\\n\\t\\t\\tvar textHtml = state.wiki.renderText(\\\"text/html\\\",\\\"text/vnd.tiddlywiki\\\",body.text,{\\n\\t\\t\\t\\tparseAsInline: inline,\\n\\t\\t\\t\\tvariables: variables\\n\\t\\t\\t});\\n\\t\\t\\tresponse.writeHead(200,{\\\"Content-Type\\\":\\\"text/html; charset=utf-8\\\",\\\"Cache-Control\\\":\\\"no-store\\\"});\\n\\t\\t\\tresponse.end(textHtml);\\n\\t\\t\\treturn;\\n\\t\\t}\\n\\t\\tsendJson(response,400,{ok:false,error:\\\"body must provide \\\\\\\"title\\\\\\\" or \\\\\\\"text\\\\\\\"\\\"});\\n\\t} catch(e) {\\n\\t\\tsendJson(response,500,{ok:false,error:String((e && e.message) || e)});\\n\\t}\\n};\\n\"\n }\n }\n}"
23
-
24
- /**
25
- * Seed the "原生渲染路由" TW plugin exactly once per wiki (mirrors the
26
- * send-to-agent one-shot policy). The marker records the offer; afterwards the
27
- * bundle is user-owned — deleting it and restarting dsh web does NOT recreate
28
- * it, and edits are never overwritten. With `opts.force` the bundle is
29
- * (re)written even when it already exists and the marker is (re)written — the
30
- * settings page uses this for "重新初始化". Returns whether a bundle was
31
- * written this call. Never throws.
32
- */
33
- export async function seedRenderRoute(client: TiddlyWebClient, opts?: { force?: boolean }): Promise<boolean> {
34
- const force = opts?.force === true
35
- if (!force) {
36
- const marker = await client.get(RENDER_MARKER_TITLE).catch(() => undefined)
37
- if (marker !== undefined) return false
38
- }
39
- const existing = await client.get(RENDER_PLUGIN_TITLE).catch(() => undefined)
40
- let wrote = false
41
- if (force || existing === undefined) {
42
- await client.put({
43
- title: RENDER_PLUGIN_TITLE,
44
- text: RENDER_BUNDLE_TEXT,
45
- type: 'application/json',
46
- tags: [],
47
- // TW only REGISTERS a wiki tiddler as a plugin (boot.js
48
- // registerPluginTiddlers) when the OUTER tiddler carries a
49
- // `plugin-type` field — without it the bundle is never unpacked and its
50
- // server-routes module is never defined, so /render 404s. The metadata
51
- // mirrors what the live send-to-agent bundle's tiddler carries.
52
- 'plugin-type': 'plugin',
53
- name: 'DSH Wiki Render',
54
- author: 'dsh-tiddlywiki',
55
- version: '0.1.0',
56
- description: '把 wiki 文本原生渲染成 HTML 片段(POST /render 服务端路由),供 DSH 回复流工具卡与 wiki 链接跳转使用',
57
- })
58
- wrote = true
59
- }
60
- // Record the offer regardless, so an existing bundle (upgrade from a
61
- // pre-seed wiki) also becomes user-owned from here on.
62
- await client
63
- .put({ title: RENDER_MARKER_TITLE, text: 'seeded-once', type: 'text/plain', tags: [] })
64
- .catch(() => undefined)
65
- return wrote
66
- }
1
+ /**
2
+ * Generated from the wiki bundle (do not hand-edit the constant).
3
+ * Source: render.bundle.json
4
+ *
5
+ * The "原生渲染路由" TW plugin, packaged as a TiddlyWiki plugin bundle
6
+ * (`{"tiddlers": {...}}`), seeded into fresh wikis by seedRenderRoute. It
7
+ * registers a server-side route module (`server-routes/render.js`,
8
+ * module-type: route) that renders wiki text into a pure HTML fragment with
9
+ * internal links rewritten to the same-origin DSH proxy hash.
10
+ *
11
+ * @module dsh-tiddlywiki/host/seed-render
12
+ */
13
+ import type { TiddlyWebClient } from './tw-api.ts'
14
+
15
+ /** The packaged plugin tiddler title (a TW system tiddler, type application/json). */
16
+ export const RENDER_PLUGIN_TITLE = '$:/plugins/dsh/render'
17
+
18
+ /** One-time marker: presence means "the route was offered once — hands off". */
19
+ export const RENDER_MARKER_TITLE = '$:/plugins/dsh-tiddlywiki/seed-render'
20
+
21
+ /** The bundle's JSON text (`{"tiddlers": {...}}`), exactly as TW stores it. */
22
+ export const RENDER_BUNDLE_TEXT = "{\n \"tiddlers\": {\n \"$:/plugins/dsh/render/plugin.info\": {\n \"title\": \"$:/plugins/dsh/render/plugin.info\",\n \"type\": \"application/json\",\n \"text\": \"{\\\"title\\\":\\\"$:/plugins/dsh/render\\\",\\\"name\\\":\\\"DSH Wiki Render\\\",\\\"description\\\":\\\"把 wiki 文本原生渲染成 HTML 片段(POST /render 服务端路由),供 DSH 回复流工具卡与 wiki 链接跳转使用\\\",\\\"author\\\":\\\"dsh-tiddlywiki\\\",\\\"version\\\":\\\"0.1.0\\\",\\\"plugin-type\\\":\\\"plugin\\\"}\"\n },\n \"$:/plugins/dsh/render/server-routes/render.js\": {\n \"title\": \"$:/plugins/dsh/render/server-routes/render.js\",\n \"type\": \"application/javascript\",\n \"module-type\": \"route\",\n \"text\": \"/*\\\\\\ntitle: $:/plugins/dsh/render/server-routes/render.js\\ntype: application/javascript\\nmodule-type: route\\n\\nPOST /render — native TiddlyWiki → HTML fragment for the DSH reply stream\\n\\nBody (JSON in state.data):\\n { title } render an existing tiddler's wikified body\\n { text, contextTitle?, parseAsInline? }\\n render arbitrary wiki text as a fragment\\n\\\\*/\\n\\\"use strict\\\";\\n\\n/*\\nThe fragment's internal wiki links are rewritten to the SAME-ORIGIN DSH proxy\\nhash (`/dsh-tiddlywiki/tw/#<title>`) by overriding the `tv-wikilink-template`\\nvariable while rendering. Verified against tiddlywiki 5.4.1's link widget:\\n`link.js` expands `$uri_encoded$` (encodeURIComponentExtended) into the href,\\nso the default `#Title` becomes `/dsh-tiddlywiki/tw/#Title`. The DSH page's\\ndocument-level click interceptor + the embedded TW's hash navigation\\n(story.js reads the hash via decodeURIComponentSafe) both handle that href —\\nno post-hoc string rewriting needed.\\n*/\\nvar WIKILINK_TEMPLATE = \\\"/dsh-tiddlywiki/tw/#$uri_encoded$\\\";\\n\\nexports.methods = [\\\"POST\\\"];\\n\\nexports.path = /^\\\\/render$/;\\n\\nexports.info = {\\n\\tpriority: 100\\n};\\n\\nfunction sendJson(response,status,payload) {\\n\\tresponse.writeHead(status,{\\\"Content-Type\\\":\\\"application/json; charset=utf-8\\\",\\\"Cache-Control\\\":\\\"no-store\\\"});\\n\\tresponse.end(JSON.stringify(payload));\\n}\\n\\nexports.handler = function(request,response,state) {\\n\\tvar body;\\n\\ttry {\\n\\t\\tbody = JSON.parse(state.data || \\\"{}\\\");\\n\\t} catch(e) {\\n\\t\\tsendJson(response,400,{ok:false,error:\\\"invalid JSON body\\\"});\\n\\t\\treturn;\\n\\t}\\n\\ttry {\\n\\t\\tvar variables = {\\n\\t\\t\\t\\\"tv-wikilink-template\\\": WIKILINK_TEMPLATE\\n\\t\\t};\\n\\t\\tif(typeof body.title === \\\"string\\\" && body.title.length > 0) {\\n\\t\\t\\t// Render an existing tiddler's wikified body (block parse, so\\n\\t\\t\\t// headings / tables / lists render natively).\\n\\t\\t\\tvar tiddler = state.wiki.getTiddler(body.title);\\n\\t\\t\\tif(!tiddler) {\\n\\t\\t\\t\\tsendJson(response,404,{ok:false,notFound:true,title:body.title});\\n\\t\\t\\t\\treturn;\\n\\t\\t\\t}\\n\\t\\t\\tvariables.currentTiddler = body.title;\\n\\t\\t\\tvar html = state.wiki.renderText(\\\"text/html\\\",\\\"text/vnd.tiddlywiki\\\",tiddler.fields.text,{\\n\\t\\t\\t\\tparseAsInline: false,\\n\\t\\t\\t\\tvariables: variables\\n\\t\\t\\t});\\n\\t\\t\\tresponse.writeHead(200,{\\\"Content-Type\\\":\\\"text/html; charset=utf-8\\\",\\\"Cache-Control\\\":\\\"no-store\\\"});\\n\\t\\t\\tresponse.end(html);\\n\\t\\t\\treturn;\\n\\t\\t}\\n\\t\\tif(typeof body.text === \\\"string\\\") {\\n\\t\\t\\t// Render arbitrary wiki text (contextTitle gives currentTiddler so\\n\\t\\t\\t// links/transclusions resolve in that tiddler's context).\\n\\t\\t\\tif(typeof body.contextTitle === \\\"string\\\" && body.contextTitle.length > 0) {\\n\\t\\t\\t\\tvariables.currentTiddler = body.contextTitle;\\n\\t\\t\\t}\\n\\t\\t\\tvar inline = body.parseAsInline === true;\\n\\t\\t\\tvar textHtml = state.wiki.renderText(\\\"text/html\\\",\\\"text/vnd.tiddlywiki\\\",body.text,{\\n\\t\\t\\t\\tparseAsInline: inline,\\n\\t\\t\\t\\tvariables: variables\\n\\t\\t\\t});\\n\\t\\t\\tresponse.writeHead(200,{\\\"Content-Type\\\":\\\"text/html; charset=utf-8\\\",\\\"Cache-Control\\\":\\\"no-store\\\"});\\n\\t\\t\\tresponse.end(textHtml);\\n\\t\\t\\treturn;\\n\\t\\t}\\n\\t\\tsendJson(response,400,{ok:false,error:\\\"body must provide \\\\\\\"title\\\\\\\" or \\\\\\\"text\\\\\\\"\\\"});\\n\\t} catch(e) {\\n\\t\\tsendJson(response,500,{ok:false,error:String((e && e.message) || e)});\\n\\t}\\n};\\n\"\n }\n }\n}"
23
+
24
+ /**
25
+ * Seed the "原生渲染路由" TW plugin exactly once per wiki (mirrors the
26
+ * send-to-agent one-shot policy). The marker records the offer; afterwards the
27
+ * bundle is user-owned — deleting it and restarting dsh web does NOT recreate
28
+ * it, and edits are never overwritten. With `opts.force` the bundle is
29
+ * (re)written even when it already exists and the marker is (re)written — the
30
+ * settings page uses this for "重新初始化". Returns whether a bundle was
31
+ * written this call. Never throws.
32
+ */
33
+ export async function seedRenderRoute(client: TiddlyWebClient, opts?: { force?: boolean }): Promise<boolean> {
34
+ const force = opts?.force === true
35
+ if (!force) {
36
+ const marker = await client.get(RENDER_MARKER_TITLE).catch(() => undefined)
37
+ if (marker !== undefined) return false
38
+ }
39
+ const existing = await client.get(RENDER_PLUGIN_TITLE).catch(() => undefined)
40
+ let wrote = false
41
+ if (force || existing === undefined) {
42
+ await client.put({
43
+ title: RENDER_PLUGIN_TITLE,
44
+ text: RENDER_BUNDLE_TEXT,
45
+ type: 'application/json',
46
+ tags: [],
47
+ // TW only REGISTERS a wiki tiddler as a plugin (boot.js
48
+ // registerPluginTiddlers) when the OUTER tiddler carries a
49
+ // `plugin-type` field — without it the bundle is never unpacked and its
50
+ // server-routes module is never defined, so /render 404s. The metadata
51
+ // mirrors what the live send-to-agent bundle's tiddler carries.
52
+ 'plugin-type': 'plugin',
53
+ name: 'DSH Wiki Render',
54
+ author: 'dsh-tiddlywiki',
55
+ version: '0.1.0',
56
+ description: '把 wiki 文本原生渲染成 HTML 片段(POST /render 服务端路由),供 DSH 回复流工具卡与 wiki 链接跳转使用',
57
+ })
58
+ wrote = true
59
+ }
60
+ // Record the offer regardless, so an existing bundle (upgrade from a
61
+ // pre-seed wiki) also becomes user-owned from here on.
62
+ await client
63
+ .put({ title: RENDER_MARKER_TITLE, text: 'seeded-once', type: 'text/plain', tags: [] })
64
+ .catch(() => undefined)
65
+ return wrote
66
+ }