dsh-tiddlywiki 0.16.12 → 0.16.13

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/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 with filters/recent/tags/batch/rename + git sync & conflict resolve), a full TiddlyWiki editor embedded in the GUI center column (same-origin proxy, works over Tailscale/LAN/domain), a quick-note card with draft auto-save & recent-notes picker behind a single knowledge FAB, and git-based sync with auto-commit. Mounts via the official dsh plugin system — no DSH source changes.",
4
- "version": "0.16.12",
4
+ "version": "0.16.13",
5
5
  "type": "module",
6
6
  "keywords": [
7
7
  "dsh",
@@ -7,3 +7,4 @@
7
7
  * @module dsh-tiddlywiki/client/endpoints
8
8
  */
9
9
  export const STATUS_ENDPOINT = '/dsh-tiddlywiki/status'
10
+ export const GET_ENDPOINT = '/dsh-tiddlywiki/get'
@@ -18,6 +18,7 @@
18
18
  */
19
19
  import * as React from 'react'
20
20
  import { attachThemeSync } from './theme-sync.ts'
21
+ import { GET_ENDPOINT } from './endpoints.ts'
21
22
 
22
23
  export const SESSION_SUMMARY_VIEW_ID = 'dsh-tiddlywiki-summary'
23
24
  const SUMMARY_ENDPOINT = '/dsh-tiddlywiki/session/summary'
@@ -49,6 +50,7 @@ function SessionSummaryView(props: SessionSummaryViewProps): React.ReactElement
49
50
  const { sessionId, viewRequest, completeViewRequest } = props
50
51
  const [phase, setPhase] = React.useState<'loading' | 'ready' | 'error'>('loading')
51
52
  const [twUrl, setTwUrl] = React.useState<string | null>(null)
53
+ const [summaryTitle, setSummaryTitle] = React.useState<string | null>(null)
52
54
  const [error, setError] = React.useState('')
53
55
  const [frameNonce, setFrameNonce] = React.useState(0)
54
56
  const genRef = React.useRef(0)
@@ -77,6 +79,7 @@ function SessionSummaryView(props: SessionSummaryViewProps): React.ReactElement
77
79
  return
78
80
  }
79
81
  if (typeof data.title === 'string' && data.title.length > 0) {
82
+ setSummaryTitle(data.title)
80
83
  setTwUrl(new URL(TW_PROXY_BASE, window.location.origin).href + '#' + encodeURIComponent(data.title))
81
84
  // 新 nonce 强制 iframe 重挂载 → 用最新汇总重新加载(含刷新场景)。
82
85
  setFrameNonce((n) => n + 1)
@@ -94,6 +97,31 @@ function SessionSummaryView(props: SessionSummaryViewProps): React.ReactElement
94
97
  void generate()
95
98
  }, [generate])
96
99
 
100
+ // 自愈:汇总条目是 volatile 的 `$:/temp`,TW 重启即消失;若 iframe 已就绪但条目
101
+ // 被清掉(/get 404),自动重新生成,避免停在 TW 的「佚失条目」页。连续 3 次仍
102
+ // 缺失(例如服务端写不进去)就停止自动重试,交还手动「🔄 刷新」。
103
+ React.useEffect(() => {
104
+ if (phase !== 'ready' || summaryTitle === null) return
105
+ let misses = 0
106
+ const timer = window.setInterval(() => {
107
+ void (async () => {
108
+ try {
109
+ const res = await fetch(`${GET_ENDPOINT}?title=${encodeURIComponent(summaryTitle)}`, { signal: AbortSignal.timeout(8_000) })
110
+ const data = (await res.json().catch(() => null)) as { notFound?: boolean } | null
111
+ if (res.status === 404 || data?.notFound === true) {
112
+ misses++
113
+ if (misses < 3) void generate()
114
+ } else {
115
+ misses = 0
116
+ }
117
+ } catch {
118
+ /* 检查失败保持现状,下个周期再试 */
119
+ }
120
+ })()
121
+ }, 30_000)
122
+ return () => window.clearInterval(timer)
123
+ }, [phase, summaryTitle, generate])
124
+
97
125
  // 一次性 focus 请求直接确认(本视图无可聚焦子目标,避免 shell 挂起)。
98
126
  React.useEffect(() => {
99
127
  if (viewRequest !== null && viewRequest !== undefined) completeViewRequest?.()
@@ -105,9 +105,24 @@ function collectDescendantIds(nodes: SessionLineageNode[] | undefined, out: stri
105
105
  }
106
106
  }
107
107
 
108
+ /**
109
+ * 汇总条目会直接写进 `[[标题]]` wikitext——标题既要能安全渲染、又不能制造死链。
110
+ * 会话日志里常混入模型的示例/残缺写法(`#…`、`#标题`、带反引号的截断链接、
111
+ * 控制字符等),这类收进来会被 TW 渲染成「佚失条目」;一律过滤。
112
+ */
113
+ function isPlausibleTitle(title: string): boolean {
114
+ if (title.length === 0 || title.length > 300) return false
115
+ if (title.trim().length === 0) return false
116
+ if (/[\u0000-\u001f\u007f]/.test(title)) return false // 控制字符 / 换行
117
+ if (/[\]|`#]/.test(title)) return false // 会破坏 [[…]] 链接语法
118
+ if (title.startsWith('…') || title === '标题') return false // 占位 / 示例写法
119
+ return true
120
+ }
121
+
108
122
  /** 记录一篇笔记的触达;同标题合并(时间取最新、detail 取最新、subagent 取 AND)。 */
109
123
  function record(map: Map<string, NoteEntry>, entry: NoteEntry): void {
110
124
  if (entry.title.startsWith('$:/')) return
125
+ if (!isPlausibleTitle(entry.title)) return
111
126
  const prev = map.get(entry.title)
112
127
  if (prev !== undefined) {
113
128
  prev.time = Math.max(prev.time, entry.time)
@@ -120,7 +135,9 @@ function record(map: Map<string, NoteEntry>, entry: NoteEntry): void {
120
135
 
121
136
  /** 扫助手回复文本里的 `/dsh-tiddlywiki/tw/#标题` 引用链接 → 读取。 */
122
137
  function scanRefs(text: string, time: number, subagent: boolean, collected: Collected): void {
123
- const re = /\/dsh-tiddlywiki\/tw\/#([^)\s\]]+)/g
138
+ // 只认到下一个 `)` / 空白 / `]` / `#` 为止——`#` 会截断后续碎片(模型示例常写成
139
+ // `#…` 或 `#标题`,这些由 isPlausibleTitle 在 record 时过滤)。
140
+ const re = /\/dsh-tiddlywiki\/tw\/#([^#)\s\]]+)/g
124
141
  let m: RegExpExecArray | null
125
142
  while ((m = re.exec(text)) !== null) {
126
143
  const raw = m[1] ?? ''