dsh-tiddlywiki 0.33.0 → 0.33.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/docs/CHANGELOG.md CHANGED
@@ -4,6 +4,15 @@
4
4
 
5
5
  > 最近几个主要版本的一句话记录(完整变更见 [Releases](https://github.com/bbqisbbq/dsh-tiddlywiki/releases) / git log)。
6
6
 
7
+ - **v0.33.1**(2026-10-26):**`.tid` 预览改为真正生效**(v0.32.0 那一版静默失效:打开 `.tid` 仍显示纯文本)。
8
+ 根因是宿主契约里我读错的一处:DSH 的文件预览分「元数据 + keyed 正文」两半,**正文按 `key` 配对** ——
9
+ 宿主自己写的是 `ctx.slots.register({ name: sidebar.right.tab.document, key: BODY_ID }, Body)`,而 v0.32.0 写成了 `id:`。
10
+ **它不报错、不告警**,宿主找不到渲染器就安静地用内置纯文本兜底,于是症状看起来像「功能没做」。
11
+ 同一轮照抄宿主补齐另外三个形状:`priority` 是**字符串**(宿主只判 `=== builtin ? 0 : 1`,非 builtin 才压得过内置纯文本)、`title` 传**函数**(语言切换可重取)、`wrap: false`(我们产出 HTML 而不是按行折行的纯文本);
12
+ 正文取文本也从「只认 `content.text`」改成读 `content.pages`(`loading: text-pages` 时宿主给 `{ kind: text, pages }`,每页 `{ offset, lines, text }`,按 offset 排序拼接)。
13
+ 守门同步加严(`verify-tid-preview` 12 条):**正向**钉 `slots.register({ name, key: TID_PREVIEW_ID })`、**反向**钉「不许出现 `slots.register({ … id: TID_PREVIEW_ID })`」、外加 priority/title/wrap/loading 与 pages 读取四条。
14
+ 另:`i18n` 键数自述 467 → 475 对齐;`profiles/desktop/package-lock.json`(npm 事故遗留、含 `link: true` 软链记账)已删除。
15
+
7
16
  - **v0.33.0**(2026-10-26):**客户端文案目录接进宿主 locale 服务(⑥b 的落地形态:注册,但不搬)**。原计划把 470+ 条键整体迁进 `ctx.locale.register(ns, {zh,en})`,但官方要求命名空间先并入 `LocaleNamespaceMap` 由**编译器**按类型化键并集校验 —— 本插件客户端是 esbuild 产出的单文件、无 `.d.ts`,参与不了那个类型合并,迁过去等于**放弃 `scripts/verify-client-i18n.mjs` 现有的键名校验**,去换一个「改语言即时生效」。现在两条都不放弃:**目录仍只有一份**(`i18n-catalog.ts` 的 7 个分片,宿主与本地共用同一个对象),启动时注册进宿主 locale 服务,并把宿主的 `bind(ns)` 接到 `t()` 前面 —— **宿主优先、本地表兜底**:服务缺失(老版本 DSH)、`register` 抛错、`bind` 不存在、宿主回空串 / 回键名本身 / 回非字符串,一律退回本地表,所以最差行为等于 v0.32.0。⚠️ 每次注册尝试都会先清掉上一次的绑定:留着旧解析器会让 `t()` 继续问一个已经不存在的命名空间(这一点是写守门时被自己的用例抓出来的)。**守门**:新增 `scripts/verify-locale-host.mjs`(进 `verify:unit`,7 条)—— 命名空间常量、无服务时静默跳过、注册的必须是**同一份** `MESSAGES`(另抄一份会慢慢分叉而界面看着正常)、`register` 的 disposer 必须回收、宿主优先、四类宿主异常必须回落、`t()` 必须委托给 `translate` 且保留本地兜底、接线必须在 `safeMount` 里。
8
17
 
9
18
  - **v0.32.0**(2026-10-26):**新增 `.tid` / `.tiddler` 文件的侧栏预览**(⑦ 的第三项)。wiki 文件夹本身就是一堆 `.tid`,而 DSH 的文件预览把未知扩展名当纯文本 —— 打开一个 `.tid` 看到的是 `title: …` 字段行加原始 wikitext,`[[链接]]`/`<<宏>>` 几乎不可读。现在注册一个预览实现:元数据用 `ctx.documentPreviews.register({ id, extensions: ['tid','tiddler'], priority, title, loading: 'text-pages' })`,正文以**同一个 id** 注入 keyed、Session 作用域的 `sidebar.right.tab.document`;正文把 wikitext POST 到本插件自己的 `/dsh-tiddlywiki/render`(铁律 #7 的净化管道)换成真 TW 渲染结果,字段行单独排一行摘要。**绝不白屏**是这个功能的硬约束:拿不到内容 → "等待文件内容…";只有字段没有正文 → 明确说明;渲染失败 → `<pre>` 原文 + 一句"渲染失败,下面是原文"。宿主没有 `documentPreviews` 服务时**什么都不做**(老版本 DSH 不受影响),且元数据与正文必须**成对**(拿不到 slots 就把已注册的元数据撤回,单边注册会让面板空白)。契约是从宿主 `app.asar` 的 `dsh-client-ui-sidebar-documentpreview/lib/client.js` 逐行核实后写的(含 `TextPreview` 的完整 props 签名与 `content` 三态),全部记在 `docs/development.md`。**守门**:新增 `scripts/verify-tid-preview.mjs`(进 `verify:unit`,10 条)——纯解析用例(字段/CRLF/没有字段必须整份当正文/只有字段/空文本)+ **成对注册**(两处必须用同一个 `TID_PREVIEW_ID` 常量、正文必须先 `slots.inject` 再 `register`)+ **绝不白屏**(`<pre>` 兜底、三种空态各自的 `t()` 文案)+ 渲染必须走 `RENDER_ENDPOINT` 且失败回 `null` 不抛 + 可选依赖的静默跳过与回滚。纯逻辑拆在 `client/tid-parse.ts`(宿主 barrel 也要导它,而宿主包里不能出现 `react`),`client/tid-preview.ts` 只放 UI。客户端 i18n 473 → 477 键。
package/lib/client.js CHANGED
@@ -962,7 +962,7 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
962
962
  `),n=t.indexOf(`
963
963
 
964
964
  `),r=n<0?t:t.slice(0,n),i=n<0?``:t.slice(n+2),a=[],o,s=r.trim().length>0;for(let e of r.split(`
965
- `)){if(e.trim().length===0)continue;let t=/^([^\s:][^:]*):\s?(.*)$/.exec(e);if(t===null){s=!1;continue}let n=(t[1]??``).trim(),r=(t[2]??``).trim();n.length!==0&&(n===`title`?o=r:a.push([n,r]))}return n<0&&(!s||o===void 0&&a.length===0)?{fields:[],text:t}:{...o===void 0?{}:{title:o},fields:a,text:i}}function FS(e){let t=e?.content;if(typeof t?.text==`string`)return t.text;let n=e?.content;return typeof n==`string`?n:``}async function IS(e,t){try{let n=await fetch(De(ne,t),{method:`POST`,headers:{"content-type":`application/json`,"x-requested-with":`TiddlyWiki`},body:JSON.stringify({text:e}),signal:AbortSignal.timeout(15e3)});if(!n.ok)return null;let r=await n.text();return r.trim().length>0?r:null}catch{return null}}function LS(){return function(e){let t=FS(e),n=c.useMemo(()=>PS(t),[t]),[r,i]=c.useState(null),[a,o]=c.useState(!1);c.useEffect(()=>{let e=!0;return o(!1),n.text.trim().length===0?(i(null),()=>{e=!1}):(IS(n.text).then(t=>{e&&(t===null?(o(!0),i(null)):i(t))}),()=>{e=!1})},[n.text]);let s=[];n.title!==void 0&&n.title.length>0&&s.push(c.createElement(`div`,{key:`title`,className:`dsh-tw-tid-title`},`📄 ${n.title}`)),n.fields.length>0&&s.push(c.createElement(`div`,{key:`fields`,className:`dsh-tw-tid-fields`},n.fields.map(([e,t])=>c.createElement(`span`,{key:e,className:`dsh-tw-tid-field`},`${e}: ${t.length>60?`${t.slice(0,60)}…`:t}`))));let l;return l=t.trim().length===0?c.createElement(`div`,{className:`dsh-tw-tid-empty`},S(`frame.tidPreviewWaiting`)):n.text.trim().length===0?c.createElement(`div`,{className:`dsh-tw-tid-empty`},S(`frame.tidPreviewNoBody`)):r===null?c.createElement(`div`,{className:`dsh-tw-tid-raw`},a?c.createElement(`div`,{className:`dsh-tw-tid-note`},S(`frame.tidPreviewFallback`)):null,c.createElement(`pre`,null,n.text)):c.createElement(`div`,{className:`dsh-tw-tid-body`,dangerouslySetInnerHTML:{__html:r}}),c.createElement(`div`,{className:`dsh-tw-tid-preview`},...s,l)}}function RS(e){let t=[],n=e.get?.(`documentPreviews`);if(n===void 0)return t;try{t.push(n.register({id:jS,extensions:[...MS],priority:100,title:S(`frame.tidPreviewTitle`),loading:`text-pages`}))}catch(e){return console.warn(`[dsh-tiddlywiki] tid preview registration failed:`,e),t}let r=e.slots;if(r===void 0){for(let e of t.splice(0))e();return t}let i=r.inject(NS,()=>r.register({name:NS,id:jS},LS()));return i!==void 0&&t.push(i),t}const zS=[`slots`];function BS(e){try{let t=[],n=!1,r=()=>{n=!0;for(let e of t.splice(0))e()},i=(e,t)=>{try{t()}catch(t){console.error(`[dsh-tiddlywiki] ${e} failed:`,t)}};i(`styles`,()=>t.push(T()));let a=new ee,o,s;if(i(`quick-note widget`,()=>{o=Ub(),t.push(()=>o?.dispose())}),i(`sync controller`,()=>{s=Kb(),t.push(()=>s?.dispose())}),i(`sidebar entry`,()=>t.push(st(a))),i(`center panel`,()=>t.push(tn(a))),o!==void 0&&s!==void 0){let e=o,n=s;i(`knowledge FAB`,()=>t.push(Xb(a,e,n)))}i(`editor popup`,()=>t.push(mn)),e.slots!==void 0&&Le().then(r=>{if(n)return;let a=o;r.showQuickNoteDock&&a!==void 0&&i(`quick-note dock`,()=>{let n=e.slots?.inject(`conversation.input.dock`,()=>e.slots?.register({name:`conversation.input.dock`,id:`quick-note`,order:8,label:S(`chrome.dockQuickNote`)},Qb(a)));n!==void 0&&t.push(n)}),i(`wiki scope seat`,()=>{let n=e.slots?.inject(`conversation.input.left`,()=>e.slots?.register({name:`conversation.input.left`,id:`wiki-scope`,order:9,label:S(`chrome.scopeSlotLabel`)},ox()));n!==void 0&&t.push(n)}),i(`session summary tab`,()=>{let n=dx(e.slots,r);n!==void 0&&t.push(n)})}).catch(e=>{console.error(`[dsh-tiddlywiki] ui config load failed:`,e)}),i(`rightbar mount`,()=>{let r,i=a=>{let o=e.get?.(`sidebarRightTabs`);if(o===void 0){a<6&&!n&&(r=window.setTimeout(()=>i(a+1),500*(a+1)));return}Le().then(r=>{if(n||!r.showRightbarTab)return;let i=AS(o,e.slots);i!==void 0&&t.push(i)}).catch(e=>{console.error(`[dsh-tiddlywiki] rightbar config load failed:`,e)})};t.push(()=>{r!==void 0&&window.clearTimeout(r)}),i(0)}),i(`tool views`,()=>{e.slots!==void 0&&t.push(...TS(e.slots))}),i(`wiki link interceptor`,()=>t.push(bS())),i(`tid preview`,()=>t.push(...RS(e))),i(`locale registration`,()=>t.push(...p(e.get))),i(`settings section`,()=>{let n=e.slots?.inject(`settings.section`,()=>e.slots?.register({name:`settings.section`,id:`dsh-tiddlywiki`,order:50,label:S(`frame.settingsSection`)},Vx));n!==void 0&&t.push(n)}),e.effect===void 0?window.addEventListener(`pagehide`,r,{once:!0}):e.effect(()=>r,`dsh-tiddlywiki: client mount`)}catch(e){console.error(`[dsh-tiddlywiki] client half failed to start:`,e)}}exports.apply=BS,exports.inject=zS,exports.name=`dsh-tiddlywiki/client`;
965
+ `)){if(e.trim().length===0)continue;let t=/^([^\s:][^:]*):\s?(.*)$/.exec(e);if(t===null){s=!1;continue}let n=(t[1]??``).trim(),r=(t[2]??``).trim();n.length!==0&&(n===`title`?o=r:a.push([n,r]))}return n<0&&(!s||o===void 0&&a.length===0)?{fields:[],text:t}:{...o===void 0?{}:{title:o},fields:a,text:i}}function FS(e){let t=e?.content;if(typeof t==`string`)return t;if(typeof t!=`object`||!t)return``;let n=t;if(typeof n.text==`string`)return n.text;let r=n.pages,i=[];if(Array.isArray(r)){for(let e of r)if(typeof e==`object`&&e){let t=e;i.push({offset:typeof t.offset==`number`?t.offset:0,text:typeof t.text==`string`?t.text:``})}}else if(typeof r==`object`&&r){for(let[e,t]of Object.entries(r))if(typeof t==`object`&&t){let n=t;i.push({offset:Number(e)||0,text:typeof n.text==`string`?n.text:``})}}return i.sort((e,t)=>e.offset-t.offset).map(e=>e.text).join(``)}async function IS(e,t){try{let n=await fetch(De(ne,t),{method:`POST`,headers:{"content-type":`application/json`,"x-requested-with":`TiddlyWiki`},body:JSON.stringify({text:e}),signal:AbortSignal.timeout(15e3)});if(!n.ok)return null;let r=await n.text();return r.trim().length>0?r:null}catch{return null}}function LS(){return function(e){let t=FS(e),n=c.useMemo(()=>PS(t),[t]),[r,i]=c.useState(null),[a,o]=c.useState(!1);c.useEffect(()=>{let e=!0;return o(!1),n.text.trim().length===0?(i(null),()=>{e=!1}):(IS(n.text).then(t=>{e&&(t===null?(o(!0),i(null)):i(t))}),()=>{e=!1})},[n.text]);let s=[];n.title!==void 0&&n.title.length>0&&s.push(c.createElement(`div`,{key:`title`,className:`dsh-tw-tid-title`},`📄 ${n.title}`)),n.fields.length>0&&s.push(c.createElement(`div`,{key:`fields`,className:`dsh-tw-tid-fields`},n.fields.map(([e,t])=>c.createElement(`span`,{key:e,className:`dsh-tw-tid-field`},`${e}: ${t.length>60?`${t.slice(0,60)}…`:t}`))));let l;return l=t.trim().length===0?c.createElement(`div`,{className:`dsh-tw-tid-empty`},S(`frame.tidPreviewWaiting`)):n.text.trim().length===0?c.createElement(`div`,{className:`dsh-tw-tid-empty`},S(`frame.tidPreviewNoBody`)):r===null?c.createElement(`div`,{className:`dsh-tw-tid-raw`},a?c.createElement(`div`,{className:`dsh-tw-tid-note`},S(`frame.tidPreviewFallback`)):null,c.createElement(`pre`,null,n.text)):c.createElement(`div`,{className:`dsh-tw-tid-body`,dangerouslySetInnerHTML:{__html:r}}),c.createElement(`div`,{className:`dsh-tw-tid-preview`},...s,l)}}function RS(e){let t=[],n=e.get?.(`documentPreviews`);if(n===void 0)return t;try{t.push(n.register({id:jS,extensions:[...MS],priority:`external`,title:()=>S(`frame.tidPreviewTitle`),loading:`text-pages`,wrap:!1}))}catch(e){return console.warn(`[dsh-tiddlywiki] tid preview registration failed:`,e),t}let r=e.slots;if(r===void 0){for(let e of t.splice(0))e();return t}let i=r.inject(NS,()=>r.register({name:NS,key:jS},LS()));return i!==void 0&&t.push(i),t}const zS=[`slots`];function BS(e){try{let t=[],n=!1,r=()=>{n=!0;for(let e of t.splice(0))e()},i=(e,t)=>{try{t()}catch(t){console.error(`[dsh-tiddlywiki] ${e} failed:`,t)}};i(`styles`,()=>t.push(T()));let a=new ee,o,s;if(i(`quick-note widget`,()=>{o=Ub(),t.push(()=>o?.dispose())}),i(`sync controller`,()=>{s=Kb(),t.push(()=>s?.dispose())}),i(`sidebar entry`,()=>t.push(st(a))),i(`center panel`,()=>t.push(tn(a))),o!==void 0&&s!==void 0){let e=o,n=s;i(`knowledge FAB`,()=>t.push(Xb(a,e,n)))}i(`editor popup`,()=>t.push(mn)),e.slots!==void 0&&Le().then(r=>{if(n)return;let a=o;r.showQuickNoteDock&&a!==void 0&&i(`quick-note dock`,()=>{let n=e.slots?.inject(`conversation.input.dock`,()=>e.slots?.register({name:`conversation.input.dock`,id:`quick-note`,order:8,label:S(`chrome.dockQuickNote`)},Qb(a)));n!==void 0&&t.push(n)}),i(`wiki scope seat`,()=>{let n=e.slots?.inject(`conversation.input.left`,()=>e.slots?.register({name:`conversation.input.left`,id:`wiki-scope`,order:9,label:S(`chrome.scopeSlotLabel`)},ox()));n!==void 0&&t.push(n)}),i(`session summary tab`,()=>{let n=dx(e.slots,r);n!==void 0&&t.push(n)})}).catch(e=>{console.error(`[dsh-tiddlywiki] ui config load failed:`,e)}),i(`rightbar mount`,()=>{let r,i=a=>{let o=e.get?.(`sidebarRightTabs`);if(o===void 0){a<6&&!n&&(r=window.setTimeout(()=>i(a+1),500*(a+1)));return}Le().then(r=>{if(n||!r.showRightbarTab)return;let i=AS(o,e.slots);i!==void 0&&t.push(i)}).catch(e=>{console.error(`[dsh-tiddlywiki] rightbar config load failed:`,e)})};t.push(()=>{r!==void 0&&window.clearTimeout(r)}),i(0)}),i(`tool views`,()=>{e.slots!==void 0&&t.push(...TS(e.slots))}),i(`wiki link interceptor`,()=>t.push(bS())),i(`tid preview`,()=>t.push(...RS(e))),i(`locale registration`,()=>t.push(...p(e.get))),i(`settings section`,()=>{let n=e.slots?.inject(`settings.section`,()=>e.slots?.register({name:`settings.section`,id:`dsh-tiddlywiki`,order:50,label:S(`frame.settingsSection`)},Vx));n!==void 0&&t.push(n)}),e.effect===void 0?window.addEventListener(`pagehide`,r,{once:!0}):e.effect(()=>r,`dsh-tiddlywiki: client mount`)}catch(e){console.error(`[dsh-tiddlywiki] client half failed to start:`,e)}}exports.apply=BS,exports.inject=zS,exports.name=`dsh-tiddlywiki/client`;
966
966
  return module.exports;
967
967
  }
968
968
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-tiddlywiki",
3
3
  "description": "把 TiddlyWiki 5 变成你和 Agent 共用的本地文档中心(纯文本 + git):你在 DSH 界面里阅读、写作、记日记,Agent 用 16 个 tiddlywiki_* 工具读写同一批笔记;每条笔记都是你能读、能改、能审计、能随时带走的文件——不是自动记忆插件,不做后台抓取。可选一键发布到微信公众号草稿箱。 Turn TiddlyWiki 5 into a document center you and your agent share, in plain text under git — a knowledge base you can read, edit, audit and take with you, not an auto-memory plugin. Mounts via the official dsh plugin system; no DSH source changes.",
4
- "version": "0.33.0",
4
+ "version": "0.33.1",
5
5
  "type": "module",
6
6
  "keywords": [
7
7
  "dsh",
@@ -41,7 +41,9 @@ interface ClientContextFace {
41
41
  slots?: {
42
42
  inject(name: string, register: () => unknown): (() => void) | undefined
43
43
  register(
44
- opts: { name: string; id: string; order?: number; label?: string | (() => string) },
44
+ // keyed 子槽(如 `sidebar.right.tab.document`)按 `key` 配对;普通座位用 `id`。
45
+ // 两者都可选,由调用点按座位语义给其中一个(v0.33.1:`.tid` 预览就是 keyed 的)。
46
+ opts: { name: string; id?: string; key?: string; order?: number; label?: string | (() => string) },
45
47
  component: unknown,
46
48
  ): () => void
47
49
  }
@@ -11,10 +11,15 @@
11
11
  *
12
12
  * 契约(2026-10-26 从宿主 `app.asar` 的
13
13
  * `dsh-client-ui-sidebar-documentpreview/lib/client.js` 逐行核实,见 docs/development.md):
14
- * · 元数据:`ctx.documentPreviews.register({ id, extensions, …, loading })`,
14
+ * · 元数据:`ctx.documentPreviews.register({ id, extensions, priority, title, loading, wrap })`,
15
15
  * **按 id 去重**,`binaryExtensions` 必须在 `extensions` 内;
16
- * · 正文:以**同一个 id** 注入 keyed、Session 作用域的 `sidebar.right.tab.document`;
17
- * 正文拿到 `content: { kind: 'text', pages, text }`(`loading: 'text-pages'`)。
16
+ * `priority` 是**字符串**(宿主只判 `=== 'builtin' ? 0 : 1`,非 builtin 压过内置);
17
+ * `title` 宿主自己传的是**函数**;`wrap` 表示是否按纯文本换行(我们要 HTML,用 false)。
18
+ * · 正文:注入 keyed、Session 作用域的 `sidebar.right.tab.document`,**按 `key` 配对**:
19
+ * `ctx.slots.register({ name: 'sidebar.right.tab.document', key: <与元数据同一个 id> }, Body)`。
20
+ * ⚠️ 用 `id` 而不是 `key` 会**静默退化成内置纯文本**(v0.32.0 的实际症状)。
21
+ * · 正文 props:`{ content, useTabInfo, … }`,`loading: 'text-pages'` 时
22
+ * `content = { kind: 'text', pages }`,每页 `{ offset, lines, text }`。
18
23
  *
19
24
  * NEVER A BLANK PANEL
20
25
  * -------------------
@@ -33,12 +38,38 @@ import { parseTidText, TID_EXTENSIONS, TID_PREVIEW_ID, TID_PREVIEW_SLOT } from '
33
38
  export { parseTidText, TID_EXTENSIONS, TID_PREVIEW_ID, TID_PREVIEW_SLOT } from './tid-parse.ts'
34
39
  export type { ParsedTid } from './tid-parse.ts'
35
40
 
36
- /** 从预览正文的 props 里取文件文本(契约未公开,全部可选链)。 */
41
+ /**
42
+ * 从正文 props 里取文件文本。
43
+ *
44
+ * `loading: 'text-pages'` 时宿主给的是 `content = { kind: 'text', pages }`,每页是
45
+ * `{ offset, lines, text }`(见宿主 `documentpreview/lib/client.js` 的 `linesOf`)。
46
+ * `pages` 既可能是数组、也可能是 `{ offset: page }` 记录,所以两种都收;
47
+ * 另外也支持更简单的 `content.text`(某些加载模式直接给整段)。
48
+ */
37
49
  function textOf(props: unknown): string {
38
- const content = (props as { content?: { kind?: unknown; text?: unknown } } | undefined)?.content
39
- if (typeof content?.text === 'string') return content.text
40
- const raw = (props as { content?: unknown } | undefined)?.content
41
- return typeof raw === 'string' ? raw : ''
50
+ const content = (props as { content?: unknown } | undefined)?.content
51
+ if (typeof content === 'string') return content
52
+ if (content === null || typeof content !== 'object') return ''
53
+ const bag = content as { text?: unknown; pages?: unknown }
54
+ if (typeof bag.text === 'string') return bag.text
55
+ const rawPages = bag.pages
56
+ const list: Array<{ offset: number; text: string }> = []
57
+ if (Array.isArray(rawPages)) {
58
+ for (const page of rawPages) {
59
+ if (page !== null && typeof page === 'object') {
60
+ const p = page as { offset?: unknown; text?: unknown }
61
+ list.push({ offset: typeof p.offset === 'number' ? p.offset : 0, text: typeof p.text === 'string' ? p.text : '' })
62
+ }
63
+ }
64
+ } else if (rawPages !== null && typeof rawPages === 'object') {
65
+ for (const [offset, page] of Object.entries(rawPages as Record<string, unknown>)) {
66
+ if (page !== null && typeof page === 'object') {
67
+ const p = page as { text?: unknown }
68
+ list.push({ offset: Number(offset) || 0, text: typeof p.text === 'string' ? p.text : '' })
69
+ }
70
+ }
71
+ }
72
+ return list.sort((a, b) => a.offset - b.offset).map((p) => p.text).join('')
42
73
  }
43
74
 
44
75
  /** 把一段 wikitext 换成宿主**净化过**的 HTML;失败回 null(调用方退回原文)。 */
@@ -127,8 +158,14 @@ interface DocumentPreviewsFace {
127
158
  id: string
128
159
  extensions: string[]
129
160
  binaryExtensions?: string[]
130
- priority?: number
131
- title?: string
161
+ /**
162
+ * **字符串**,不是数字:宿主只判 `priority === 'builtin' ? 0 : 1`,
163
+ * rank 高的先命中,也就是「非 builtin(外部实现)压过内置」。宿主内置的
164
+ * 纯文本/Markdown 都是 `'builtin'`,所以我们要声明 `'external'`。
165
+ */
166
+ priority?: 'builtin' | 'external'
167
+ /** 展示名。宿主自己的实现传的是**函数**(`() => t('viewer.text')`),照抄。 */
168
+ title?: string | (() => string)
132
169
  loading?: 'text-pages' | 'bytes-complete' | 'renderer'
133
170
  wrap?: boolean
134
171
  }): () => void
@@ -139,7 +176,11 @@ interface PreviewCtx {
139
176
  get?: (name: string) => unknown
140
177
  slots?: {
141
178
  inject(name: string, register: () => unknown): (() => void) | undefined
142
- register(opts: { name: string; id: string }, component: unknown): (() => void) | undefined
179
+ /**
180
+ * keyed 子槽按 **`key`** 配对(宿主写的是 `register({ name, key: BODY_ID }, C)`)——
181
+ * 不是 `id`。用 `id` 会静默退化成内置纯文本,正是 v0.32.0 的实际症状。
182
+ */
183
+ register(opts: { name: string; key: string }, component: unknown): (() => void) | undefined
143
184
  }
144
185
  }
145
186
 
@@ -158,9 +199,12 @@ export function registerTidPreview(ctx: PreviewCtx): Array<() => void> {
158
199
  previews.register({
159
200
  id: TID_PREVIEW_ID,
160
201
  extensions: [...TID_EXTENSIONS],
161
- priority: 100,
162
- title: t('frame.tidPreviewTitle'),
202
+ // 外部实现压过宿主的纯文本兜底(宿主按 `priority === 'builtin'` 分档)。
203
+ priority: 'external',
204
+ // 宿主自己传的是函数(`) => t('viewer.text')`),语言切换时能重取。
205
+ title: () => t('frame.tidPreviewTitle'),
163
206
  loading: 'text-pages',
207
+ wrap: false,
164
208
  }),
165
209
  )
166
210
  } catch (err) {
@@ -175,7 +219,7 @@ export function registerTidPreview(ctx: PreviewCtx): Array<() => void> {
175
219
  return disposers
176
220
  }
177
221
  const bodyDispose = slots.inject(TID_PREVIEW_SLOT, () =>
178
- slots.register({ name: TID_PREVIEW_SLOT, id: TID_PREVIEW_ID }, createTidPreviewBody()),
222
+ slots.register({ name: TID_PREVIEW_SLOT, key: TID_PREVIEW_ID }, createTidPreviewBody()),
179
223
  )
180
224
  if (bodyDispose !== undefined) disposers.push(bodyDispose)
181
225
  return disposers