dsh-tiddlywiki 0.5.0 → 0.6.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 CHANGED
@@ -15,7 +15,7 @@
15
15
  | 能力 | 说明 |
16
16
  |---|---|
17
17
  | 🤖 **Agent 工具** | `tiddlywiki_search` / `get` / `put` / `batch_put` / `rename` / `delete` / `recent` / `list_tags` / `git_sync` / `git_resolve` 十个工具:检索、读写、批量、重命名、删除、git 同步与冲突解决 |
18
- | 🧭 **内嵌编辑器** | 侧边栏「TiddlyWiki」入口 → 中央列内嵌完整 TW 5 编辑器(iframe 直连 TW 服务) |
18
+ | 🧭 **内嵌编辑器** | 侧边栏「TiddlyWiki」入口 → 中央列内嵌完整 TW 5 编辑器(**同源代理**,经 DSH origin 访问,Tailscale/内网/域名/HTTPS 均可用) |
19
19
  | 📝 **快速笔记** | 右下角「知识库」悬浮按钮 → 快速笔记卡片:**CodeMirror 6** Markdown 编辑器(语法高亮 + 撤销/重做)、文件上传、多选/自动补全 tag、**草稿自动保存(刷新不丢)**、**「🕘 最近」一键载入旧笔记**,Ctrl+Enter 保存;可整体隐藏 |
20
20
  | 🔄 **一键同步** | 「知识库」按钮 →「🔁 同步」:pull → commit → push;FAB 上的状态点实时反映 git 状态(已同步/待提交/可更新/离线) |
21
21
  | ⚙️ **设置页** | DSH 设置 →「TiddlyWiki 知识库」:插件/主题/语言管理与运行配置,应用后自动重启 TW |
@@ -86,13 +86,13 @@ dsh plugin --profile web add link:/path/to/your/dsh-tiddlywiki
86
86
 
87
87
  ### 🧑‍💻 给人:界面操作
88
88
 
89
- **🧭 中央列编辑器** — 侧边栏「TiddlyWiki」按钮开关中央编辑器面板(iframe 直连 TW 服务),完整 TW 5 编辑器。
89
+ **🧭 中央列编辑器** — 侧边栏「TiddlyWiki」按钮开关中央编辑器面板(**同源代理**:iframe 指向 `<DSH origin>/dsh-tiddlywiki/tw/`,由 DSH 转发到回环上的 TW 服务),完整 TW 5 编辑器。
90
90
 
91
91
  **📝 快速笔记** — 右下角「**知识库**」悬浮按钮 →「📝 快速笔记」(可折叠):
92
92
  - **CodeMirror 6 编辑器**:真正的 Markdown 语法树高亮(标题/列表/代码/链接/表格/任务清单/删除线等,GFM),支持撤销/重做与行内编辑体验;
93
93
  - **草稿自动保存**:正文/标题/标签 500ms 防抖写入本地,关掉卡片或刷新页面都不丢;重开自动恢复,可一键「丢弃」;
94
94
  - **🕘 最近**:一键列出最近修改的笔记,点标题直接载入编辑器继续改(不需要开完整 TW 去找);
95
- - **文件上传**:点「📎 上传」或直接把文件拖进编辑器——文件存到 wiki 的 `files/` 文件夹并随 git 同步;图片插入 `![名](/files/名)`、其它文件插入 `[名](/files/名)`,在 TW 里可直接打开;
95
+ - **文件上传**:点「📎 上传」或直接把文件拖进编辑器——文件存到 wiki 的 `files/` 文件夹并随 git 同步;图片插入 `![名](/dsh-tiddlywiki/tw/files/名)`、其它文件插入 `[名](/dsh-tiddlywiki/tw/files/名)`(**同源代理 URL**,在 TW 与快速笔记预览里都能打开);
96
96
  - **Ctrl+Enter 保存**为独立 tiddler;笔记 `type` 自动设为 `text/markdown`,所以上传的图片/链接在 TW 里按 Markdown 正常渲染;
97
97
  - 点「**✏️ 在 TW 中编辑**」→ 保存后弹出**独立小窗**(可拖动/缩放)加载 TW 原生编辑器编辑该条。
98
98
 
@@ -134,6 +134,20 @@ TW 的界面语言由**语言插件**决定,不是某个配置字符串。tidd
134
134
  - **配置自动应用**:在常规配置写入 `uiLanguage: "zh-Hans"`,每次启动 dsh web 时自动启用该语言并固定 `$:/language`(若尚未启用)。留空则不干预。
135
135
  - **想换繁中**:语言管理里改勾 `zh-Hant` / `zh-TW` 再应用。
136
136
 
137
+ ### 🌐 远程访问(Tailscale / 内网 / 域名 / HTTPS)
138
+
139
+ TW 子进程只监听 **127.0.0.1 回环**(更安全),**agent 工具、快速笔记、git 同步本来就全程走 DSH 宿主进程→回环 TW**,所以无论你从哪个入口访问 DSH,这些能力都不受影响。唯一受影响的是**浏览器里的 TW 编辑器 iframe**:早期版本让 iframe 直接指向 `http://127.0.0.1:<port>`——只有浏览器和 DSH 在同一台机器时才行;一旦通过 Tailscale / 内网 IP / 域名访问 DSH,`127.0.0.1` 会指向浏览器自己那台机器,编辑器就加载不出来了。
140
+
141
+ 从 **v0.6.0** 起,内嵌编辑器改为**同源代理**:
142
+
143
+ - 浏览器里的 TW 编辑器 iframe 指向 `<DSH origin>/dsh-tiddlywiki/tw/`(与 DSH 同源),DSH 把整个 TW 前端(页面 + `/files/*` + TiddlyWeb API)透传到回环上的 TW 服务;
144
+ - TW 前端的 API 基址由 wiki 内的 `$:/config/tiddlyweb/host` 控制,插件启动时把它固定为 `/dsh-tiddlywiki/tw/`(仅当缺失或仍是旧默认值时写入,用户自定义会被保留);
145
+ - 因为代理 URL 与端口无关,TW 重启也不会让编辑器 iframe 重新加载,编辑中的内容不丢。
146
+
147
+ 效果:**DSH 跑在服务器上、你用 Tailscale / 域名 / 内网 IP 从任意设备打开 DSH Web 时,中央列编辑器和「在 TW 中编辑」弹窗都正常工作**;以后 DSH 挂到域名 + HTTPS 反向代理后面也同样成立(同源、无 mixed-content、无 CORS)。
148
+
149
+ > ⚠️ 迁移说明:v0.6.0 之前上传的文件在笔记里写的是根路径 `/files/名`,本插件**不再**占用 DSH 根命名空间(避免与其他插件冲突),所以这些旧链接在嵌入编辑器里会失效;新上传的文件使用 `/dsh-tiddlywiki/tw/files/名` 前缀 URL,可直接打开。若需要旧链接,可在 TW 里把对应笔记的 `/files/` 前缀改回 `/dsh-tiddlywiki/tw/files/`。
150
+
137
151
  ---
138
152
 
139
153
  ## 🔄 同步与数据
@@ -174,6 +188,8 @@ TW 的界面语言由**语言插件**决定,不是某个配置字符串。tidd
174
188
 
175
189
  > **运行时配置**:设置页写入的 `$:/plugins/dsh-tiddlywiki/config` tiddler 是 `config:` 块之上的覆盖层(tiddler 优先、随 wiki 的 git 同步)。无需改动 cordis 也能改 note tag / git 开关 / ui 开关等。
176
190
  > `config:` 块随 profile 被 dsh-market 带走;wiki 数据走 git,不走 dsh-market。
191
+ >
192
+ > **远程访问(v0.6.0)**:TW 前端的 API 基址来自 `$:/config/tiddlyweb/host`,插件启动时固定为 `/dsh-tiddlywiki/tw/`(同源代理)。TW 子进程始终只监听 `127.0.0.1`;`auth.username/password` 仅在你想把 TW 直接暴露到非回环地址(绕过 DSH)时才需要,正常情况下无需配置。
177
193
 
178
194
  ---
179
195
 
@@ -208,6 +224,7 @@ npm run selftest # headless:spawn TW → REST 读写 → git → 退出
208
224
  | `/dsh-tiddlywiki/upload` | POST | 文件上传到 `files/`(原始 body + `X-Filename`) |
209
225
  | `/dsh-tiddlywiki/restart` | POST | 重启 TW 子进程 |
210
226
  | `/dsh-tiddlywiki/api/*` | any | 透传到 TW 服务(JSON) |
227
+ | `/dsh-tiddlywiki/tw/*` | any | **同源 TW 代理**:整个 TW 前端(index + `/files/*` + TiddlyWeb API)→ 回环 TW(v0.6.0,远程访问核心) |
211
228
 
212
229
  ---
213
230
 
@@ -229,10 +246,10 @@ src/
229
246
  ├── index.ts # host 入口:装配 WikiServer/路由/工具/prompt/自动 commit
230
247
  ├── sdk.ts # 自包含 defineTool + dshHomePath(零 @deepseek-ai 运行时依赖)
231
248
  ├── host/
232
- │ ├── wiki.ts # WikiServer:spawn/kill/自愈/端口探测/就绪轮询
249
+ │ ├── wiki.ts # WikiServer:spawn/kill/自愈/端口探测/就绪轮询;TW_PROXY_PATH 同源代理路径
233
250
  │ ├── tw-api.ts # TiddlyWeb REST 客户端
234
251
  │ ├── git.ts # git init/commit/pull/push/sync/status + AutoCommitter
235
- │ ├── routes.ts # /status /note /edit /tags /sync /upload /restart /api/* 路由
252
+ │ ├── routes.ts # /status /note /edit /tags /sync /upload /restart /api/* /tw/* 路由
236
253
  │ ├── admin.ts # 设置页后台:tiddlywiki.info 读写 + 目录枚举 + /admin/* 路由
237
254
  │ ├── config.ts # ConfigStore:cordis config 基底 + 配置 tiddler 覆盖层
238
255
  │ ├── seed-notes.ts # 首次启动一次性写入「插件说明」笔记
@@ -827,9 +827,11 @@ function mountPanel(state) {
827
827
  showError("无法访问 /dsh-tiddlywiki/status");
828
828
  return;
829
829
  }
830
- if (payload.status === "running" && typeof payload.url === "string") {
830
+ if (payload.status === "running") {
831
831
  refreshAttempts = 0;
832
- showFrame(payload.url);
832
+ if (typeof payload.twProxy === "string") showFrame(new URL(payload.twProxy, window.location.origin).href);
833
+ else if (typeof payload.url === "string") showFrame(payload.url);
834
+ else showError("服务未返回编辑器地址");
833
835
  return;
834
836
  }
835
837
  if (payload.status === "starting") {
@@ -27088,7 +27090,7 @@ function createNoteWidget() {
27088
27090
  }
27089
27091
  clearDraft();
27090
27092
  hideDraftBanner();
27091
- openEditorPopup(`${payload.twUrl}#${encodeURIComponent(payload.draftTitle)}`, payload.title ?? title);
27093
+ openEditorPopup(`${new URL(payload.twUrl, location.origin).href}#${encodeURIComponent(payload.draftTitle)}`, payload.title ?? title);
27092
27094
  toast(`已在弹出窗口打开「${payload.title ?? title}」编辑器`);
27093
27095
  } catch (err) {
27094
27096
  toast(`打开失败:${err instanceof Error ? err.message : String(err)}`);
package/lib/client.js CHANGED
@@ -833,9 +833,11 @@ function mountPanel(state) {
833
833
  showError("无法访问 /dsh-tiddlywiki/status");
834
834
  return;
835
835
  }
836
- if (payload.status === "running" && typeof payload.url === "string") {
836
+ if (payload.status === "running") {
837
837
  refreshAttempts = 0;
838
- showFrame(payload.url);
838
+ if (typeof payload.twProxy === "string") showFrame(new URL(payload.twProxy, window.location.origin).href);
839
+ else if (typeof payload.url === "string") showFrame(payload.url);
840
+ else showError("服务未返回编辑器地址");
839
841
  return;
840
842
  }
841
843
  if (payload.status === "starting") {
@@ -27094,7 +27096,7 @@ function createNoteWidget() {
27094
27096
  }
27095
27097
  clearDraft();
27096
27098
  hideDraftBanner();
27097
- openEditorPopup(`${payload.twUrl}#${encodeURIComponent(payload.draftTitle)}`, payload.title ?? title);
27099
+ openEditorPopup(`${new URL(payload.twUrl, location.origin).href}#${encodeURIComponent(payload.draftTitle)}`, payload.title ?? title);
27098
27100
  toast(`已在弹出窗口打开「${payload.title ?? title}」编辑器`);
27099
27101
  } catch (err) {
27100
27102
  toast(`打开失败:${err instanceof Error ? err.message : String(err)}`);
package/lib/index.js CHANGED
@@ -451,6 +451,16 @@ var AutoCommitter = class {
451
451
  */
452
452
  /** The DSH webserver route prefix (NOT a TW path-prefix; see module header). */
453
453
  const PATH_PREFIX = "/dsh-tiddlywiki";
454
+ /**
455
+ * Same-origin TW proxy route on the DSH webserver (remote-access mode, R1).
456
+ * The browser only ever talks to the DSH origin — which it already reaches
457
+ * over loopback, LAN, Tailscale, a domain or HTTPS — and DSH proxies to the
458
+ * loopback TW child. TW's frontend is pointed at this prefix via the
459
+ * `$:/config/tiddlyweb/host` tiddler so every API call stays same-origin.
460
+ */
461
+ const TW_PROXY_PREFIX = `${PATH_PREFIX}/tw`;
462
+ /** The proxy base path (trailing slash) handed to browsers / TW's frontend. */
463
+ const TW_PROXY_PATH = `${TW_PROXY_PREFIX}/`;
454
464
  /** How long to wait for the wiki to answer /status. */
455
465
  const READY_TIMEOUT_MS = 2e4;
456
466
  /** Poll cadence while waiting for readiness. */
@@ -720,6 +730,29 @@ function readBodyBuffer(req, limit = MAX_UPLOAD_BYTES) {
720
730
  req.on("error", rejectP);
721
731
  });
722
732
  }
733
+ /** Header names forwarded to the upstream TW service by the proxy routes. */
734
+ const FORWARD_HEADER_NAMES = [
735
+ "accept",
736
+ "accept-encoding",
737
+ "content-type",
738
+ "cookie",
739
+ "authorization",
740
+ "if-none-match",
741
+ "if-modified-since",
742
+ "origin",
743
+ "referer",
744
+ "user-agent"
745
+ ];
746
+ /** Copy a safe, string-valued subset of the request headers upstream. */
747
+ function forwardHeaders(headers) {
748
+ const out = {};
749
+ for (const name of FORWARD_HEADER_NAMES) {
750
+ const value = headers[name];
751
+ if (typeof value === "string") out[name] = value;
752
+ else if (Array.isArray(value) && value.length > 0) out[name] = value.join(", ");
753
+ }
754
+ return out;
755
+ }
723
756
  /**
724
757
  * Sanitize an uploaded filename into a safe bare name (no path separators,
725
758
  * no `..`, no control characters). Returns '' when nothing usable remains.
@@ -813,6 +846,7 @@ function registerRoutes(ctx, deps) {
813
846
  json$1(res, {
814
847
  ok: true,
815
848
  ...view,
849
+ twProxy: TW_PROXY_PATH,
816
850
  git: gitSummary,
817
851
  note: { tag: deps.noteDefaults().tag },
818
852
  ui: deps.uiDefaults()
@@ -879,7 +913,7 @@ function registerRoutes(ctx, deps) {
879
913
  json$1(res, {
880
914
  ok: true,
881
915
  ...result,
882
- twUrl: deps.server.url
916
+ twUrl: TW_PROXY_PATH
883
917
  });
884
918
  } catch (err) {
885
919
  json$1(res, {
@@ -1109,7 +1143,7 @@ function registerRoutes(ctx, deps) {
1109
1143
  ok: true,
1110
1144
  name: candidate,
1111
1145
  path: `files/${candidate}`,
1112
- url: `/files/${encodeURIComponent(candidate)}`,
1146
+ url: `${TW_PROXY_PATH}files/${encodeURIComponent(candidate)}`,
1113
1147
  size: buf.length,
1114
1148
  type: req.headers["content-type"] ?? "application/octet-stream"
1115
1149
  });
@@ -1157,6 +1191,62 @@ function registerRoutes(ctx, deps) {
1157
1191
  }, 502);
1158
1192
  }
1159
1193
  };
1194
+ /**
1195
+ * SAME-ORIGIN proxy /dsh-tiddlywiki/tw/<rest> → TW root /<rest>. Serves the
1196
+ * ENTIRE TW frontend (index HTML, /files/*, the TiddlyWeb API) to the
1197
+ * browser through the DSH origin, so the embedded editor works from any
1198
+ * host/domain the user reaches DSH on (loopback, LAN, Tailscale, domain,
1199
+ * HTTPS). The browser never talks to the loopback TW child directly; DSH
1200
+ * does, on the same machine. Binary responses are buffered losslessly
1201
+ * (arrayBuffer) — unlike the /api JSON proxy, this route must never .text().
1202
+ */
1203
+ const handleTwProxy = async (req, res) => {
1204
+ if (deps.getClient() === void 0) {
1205
+ json$1(res, {
1206
+ ok: false,
1207
+ error: "wiki service is not running"
1208
+ }, 503);
1209
+ return;
1210
+ }
1211
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
1212
+ const rest = url.pathname.replace(new RegExp(`^${TW_PROXY_PREFIX}(?=/|$)`), "") || "/";
1213
+ try {
1214
+ const method = (req.method ?? "GET").toUpperCase();
1215
+ const headers = forwardHeaders(req.headers);
1216
+ if (method === "PUT" || method === "DELETE" || method === "POST") headers["x-requested-with"] = "TiddlyWiki";
1217
+ const init = {
1218
+ method,
1219
+ headers,
1220
+ signal: AbortSignal.timeout(3e4)
1221
+ };
1222
+ if (method === "PUT" || method === "POST") init.body = await readBodyBuffer(req, MAX_UPLOAD_BYTES);
1223
+ const upstream = await fetch(`${deps.server.url}${rest}${url.search}`, init);
1224
+ const data = Buffer.from(await upstream.arrayBuffer());
1225
+ const responseHeaders = {
1226
+ "content-type": upstream.headers.get("content-type") ?? "application/octet-stream",
1227
+ "cache-control": upstream.headers.get("cache-control") ?? "no-store"
1228
+ };
1229
+ for (const name of [
1230
+ "etag",
1231
+ "last-modified",
1232
+ "content-disposition"
1233
+ ]) {
1234
+ const value = upstream.headers.get(name);
1235
+ if (value !== null) responseHeaders[name] = value;
1236
+ }
1237
+ res.writeHead(upstream.status, responseHeaders);
1238
+ res.end(data);
1239
+ } catch (err) {
1240
+ if (res.headersSent) {
1241
+ res.end();
1242
+ return;
1243
+ }
1244
+ json$1(res, {
1245
+ ok: false,
1246
+ error: err instanceof Error ? err.message : String(err)
1247
+ }, 502);
1248
+ }
1249
+ };
1160
1250
  const disposers = [
1161
1251
  ctx.webServer.register({
1162
1252
  kind: "exact",
@@ -1227,6 +1317,13 @@ function registerRoutes(ctx, deps) {
1227
1317
  handler: (req, res) => {
1228
1318
  handleApiProxy(req, res);
1229
1319
  }
1320
+ }),
1321
+ ctx.webServer.register({
1322
+ kind: "prefix",
1323
+ path: `${TW_PROXY_PREFIX}`,
1324
+ handler: (req, res) => {
1325
+ handleTwProxy(req, res);
1326
+ }
1230
1327
  })
1231
1328
  ];
1232
1329
  return () => {
@@ -2834,6 +2931,34 @@ const DEFAULTS = {
2834
2931
  password: ""
2835
2932
  }
2836
2933
  };
2934
+ /** Config tiddler steering TW's frontend API base (tiddlywebadaptor). */
2935
+ const TW_WEB_HOST_TIDDLER = "$:/config/tiddlyweb/host";
2936
+ /** The legacy default value this plugin replaces with the same-origin proxy. */
2937
+ const TW_WEB_HOST_DEFAULT = "$protocol$//$host$/";
2938
+ /**
2939
+ * Point TW's frontend at the same-origin DSH proxy (remote-access mode, R1).
2940
+ * The tiddlywebadaptor builds every API URL from $:/config/tiddlyweb/host; its
2941
+ * default `$protocol$//$host$/` resolves to the iframe's origin ROOT, which
2942
+ * 404s whenever the browser is not on the same machine as TW. Written only
2943
+ * when the tiddler is missing or still the legacy default — a user override is
2944
+ * honored (e.g. someone who really does expose TW on a dedicated origin).
2945
+ */
2946
+ async function ensureTwWebHost(client) {
2947
+ if (client === void 0) return;
2948
+ let current;
2949
+ try {
2950
+ current = (await client.get(TW_WEB_HOST_TIDDLER))?.text;
2951
+ } catch {
2952
+ current = void 0;
2953
+ }
2954
+ if (current !== void 0 && current.trim() !== TW_WEB_HOST_DEFAULT) return;
2955
+ await client.put({
2956
+ title: TW_WEB_HOST_TIDDLER,
2957
+ text: TW_PROXY_PATH,
2958
+ type: "text/plain",
2959
+ tags: []
2960
+ });
2961
+ }
2837
2962
  /** Expand $VAR / ${VAR} / %VAR% from process.env (config uses $DSH_HOME). */
2838
2963
  function expandEnvPath(input) {
2839
2964
  return input.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, k) => process.env[k] ?? "").replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, k) => process.env[k] ?? "").replace(/%([A-Za-z_][A-Za-z0-9_]*%)/g, (_, k) => process.env[k.slice(0, -1)] ?? "");
@@ -3006,6 +3131,11 @@ function apply(ctx, rawConfig = {}) {
3006
3131
  try {
3007
3132
  await server.start();
3008
3133
  await configStore.load(client());
3134
+ try {
3135
+ await ensureTwWebHost(client());
3136
+ } catch (err) {
3137
+ console.warn("[dsh-tiddlywiki] ensuring $:/config/tiddlyweb/host:", err);
3138
+ }
3009
3139
  try {
3010
3140
  const seedClient = client();
3011
3141
  if (seedClient !== void 0) await seedDocNote(seedClient);
@@ -3066,6 +3196,6 @@ function apply(ctx, rawConfig = {}) {
3066
3196
  }, "dsh-tiddlywiki: host teardown");
3067
3197
  }
3068
3198
  //#endregion
3069
- export { AutoCommitter, ConfigStore, DOC_NOTE_TAG, DOC_NOTE_TEXT, DOC_NOTE_TITLE, GitFace, PATH_PREFIX, TiddlyWebClient, WikiServer, apply, bundledCatalog, deepMerge, defineTool, dshHomePath, ensureLanguage, inject, name, normalizeThemes, openInTwEditor, readWikiInfo, registerAdminRoutes, registerRoutes, resolveTwRoot, seedDocNote, writeWikiInfo };
3199
+ export { AutoCommitter, ConfigStore, DOC_NOTE_TAG, DOC_NOTE_TEXT, DOC_NOTE_TITLE, GitFace, PATH_PREFIX, TW_PROXY_PATH, TW_PROXY_PREFIX, TW_WEB_HOST_TIDDLER, TiddlyWebClient, WikiServer, apply, bundledCatalog, deepMerge, defineTool, dshHomePath, ensureLanguage, ensureTwWebHost, inject, name, normalizeThemes, openInTwEditor, readWikiInfo, registerAdminRoutes, registerRoutes, resolveTwRoot, seedDocNote, writeWikiInfo };
3070
3200
 
3071
3201
  //# sourceMappingURL=index.js.map