dsh-tiddlywiki 0.5.0 → 0.7.2
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 +22 -5
- package/lib/client.bundle.js +5 -3
- package/lib/client.js +5 -3
- package/lib/index.js +309 -5
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/client/note-widget.ts +5 -1
- package/src/client/panel.ts +20 -6
- package/src/host/config.ts +7 -0
- package/src/host/routes.ts +225 -8
- package/src/host/wiki.ts +12 -0
- package/src/index.ts +48 -5
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
|
|
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
|
|
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/名)
|
|
95
|
+
- **文件上传**:点「📎 上传」或直接把文件拖进编辑器——文件存到 wiki 的 `files/` 文件夹并随 git 同步;图片插入 ``、其它文件插入 `[名](/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 # 首次启动一次性写入「插件说明」笔记
|
package/lib/client.bundle.js
CHANGED
|
@@ -827,9 +827,11 @@ function mountPanel(state) {
|
|
|
827
827
|
showError("无法访问 /dsh-tiddlywiki/status");
|
|
828
828
|
return;
|
|
829
829
|
}
|
|
830
|
-
if (payload.status === "running"
|
|
830
|
+
if (payload.status === "running") {
|
|
831
831
|
refreshAttempts = 0;
|
|
832
|
-
showFrame(payload.
|
|
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"
|
|
836
|
+
if (payload.status === "running") {
|
|
837
837
|
refreshAttempts = 0;
|
|
838
|
-
showFrame(payload.
|
|
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,11 +846,157 @@ 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()
|
|
819
853
|
});
|
|
820
854
|
};
|
|
855
|
+
/**
|
|
856
|
+
* GET /dsh-tiddlywiki/agent/sessions — visible ordinary sessions for the TW
|
|
857
|
+
* one-click picker (excludes subagent sessions, activity-descending).
|
|
858
|
+
*/
|
|
859
|
+
const handleAgentSessions = async (_req, res) => {
|
|
860
|
+
try {
|
|
861
|
+
const sc = deps.getSessionController();
|
|
862
|
+
if (sc === void 0) {
|
|
863
|
+
json$1(res, {
|
|
864
|
+
ok: false,
|
|
865
|
+
error: "session service unavailable"
|
|
866
|
+
}, 503);
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
json$1(res, {
|
|
870
|
+
ok: true,
|
|
871
|
+
items: ((await sc.list({}, AbortSignal.timeout(1e4))).items ?? []).filter((s) => s.parentSessionId === void 0).map((s) => ({
|
|
872
|
+
sessionId: s.sessionId,
|
|
873
|
+
cwd: s.cwd ?? null,
|
|
874
|
+
running: !!s.running,
|
|
875
|
+
blank: !!s.blank,
|
|
876
|
+
updatedAt: s.updatedAt ?? 0
|
|
877
|
+
})).sort((a, b) => b.updatedAt - a.updatedAt)
|
|
878
|
+
});
|
|
879
|
+
} catch (err) {
|
|
880
|
+
json$1(res, {
|
|
881
|
+
ok: false,
|
|
882
|
+
error: err instanceof Error ? err.message : String(err)
|
|
883
|
+
}, 500);
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
/**
|
|
887
|
+
* POST /dsh-tiddlywiki/agent/send — deliver a note to one agent session as a
|
|
888
|
+
* queued user message (sessionController.prompt, the same API the GUI chat
|
|
889
|
+
* input uses). Guards: feature switch, optional shared token, body shape.
|
|
890
|
+
*/
|
|
891
|
+
const handleAgentSend = async (req, res) => {
|
|
892
|
+
try {
|
|
893
|
+
if (!deps.sendToAgentEnabled()) {
|
|
894
|
+
json$1(res, {
|
|
895
|
+
ok: false,
|
|
896
|
+
error: "send-to-agent is disabled"
|
|
897
|
+
}, 403);
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
const token = deps.sendToAgentToken().trim();
|
|
901
|
+
if (token.length > 0) {
|
|
902
|
+
const got = req.headers["x-send-to-agent-token"];
|
|
903
|
+
if ((typeof got === "string" ? got : Array.isArray(got) ? got[0] ?? "" : "") !== token) {
|
|
904
|
+
json$1(res, {
|
|
905
|
+
ok: false,
|
|
906
|
+
error: "unauthorized"
|
|
907
|
+
}, 401);
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
const body = JSON.parse(await readBody$1(req));
|
|
912
|
+
const sessionId = typeof body.sessionId === "string" && body.sessionId.trim().length > 0 ? body.sessionId.trim() : "";
|
|
913
|
+
const text = typeof body.text === "string" && body.text.trim().length > 0 ? body.text.trim() : "";
|
|
914
|
+
if (sessionId.length === 0 || text.length === 0) {
|
|
915
|
+
json$1(res, {
|
|
916
|
+
ok: false,
|
|
917
|
+
error: "sessionId and text are required"
|
|
918
|
+
}, 400);
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
const sc = deps.getSessionController();
|
|
922
|
+
if (sc === void 0) {
|
|
923
|
+
json$1(res, {
|
|
924
|
+
ok: false,
|
|
925
|
+
error: "session service unavailable"
|
|
926
|
+
}, 503);
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
const requestId = `tw-send-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
930
|
+
await sc.prompt({
|
|
931
|
+
requestId,
|
|
932
|
+
sessionId,
|
|
933
|
+
mode: "queue",
|
|
934
|
+
content: [{
|
|
935
|
+
type: "text",
|
|
936
|
+
text
|
|
937
|
+
}]
|
|
938
|
+
}, AbortSignal.timeout(2e4));
|
|
939
|
+
json$1(res, {
|
|
940
|
+
ok: true,
|
|
941
|
+
requestId,
|
|
942
|
+
sessionId
|
|
943
|
+
});
|
|
944
|
+
} catch (err) {
|
|
945
|
+
json$1(res, {
|
|
946
|
+
ok: false,
|
|
947
|
+
error: err instanceof Error ? err.message : String(err)
|
|
948
|
+
}, 500);
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
/**
|
|
952
|
+
* POST /dsh-tiddlywiki/agent/create — create (or adopt) one ordinary session,
|
|
953
|
+
* optionally inside a workspace path. The picker uses it for "new workspace /
|
|
954
|
+
* new session"; the created session's cwd becomes its workspace. The directory
|
|
955
|
+
* is materialised so a brand-new workspace actually exists on disk.
|
|
956
|
+
*/
|
|
957
|
+
const handleAgentCreate = async (req, res) => {
|
|
958
|
+
try {
|
|
959
|
+
if (!deps.sendToAgentEnabled()) {
|
|
960
|
+
json$1(res, {
|
|
961
|
+
ok: false,
|
|
962
|
+
error: "send-to-agent is disabled"
|
|
963
|
+
}, 403);
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
const token = deps.sendToAgentToken().trim();
|
|
967
|
+
if (token.length > 0) {
|
|
968
|
+
const got = req.headers["x-send-to-agent-token"];
|
|
969
|
+
if ((typeof got === "string" ? got : Array.isArray(got) ? got[0] ?? "" : "") !== token) {
|
|
970
|
+
json$1(res, {
|
|
971
|
+
ok: false,
|
|
972
|
+
error: "unauthorized"
|
|
973
|
+
}, 401);
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
const body = JSON.parse(await readBody$1(req));
|
|
978
|
+
const cwd = typeof body.cwd === "string" ? body.cwd.trim() : "";
|
|
979
|
+
const sc = deps.getSessionController();
|
|
980
|
+
if (sc === void 0) {
|
|
981
|
+
json$1(res, {
|
|
982
|
+
ok: false,
|
|
983
|
+
error: "session service unavailable"
|
|
984
|
+
}, 503);
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
if (cwd.length > 0) await mkdir(cwd, { recursive: true });
|
|
988
|
+
json$1(res, {
|
|
989
|
+
ok: true,
|
|
990
|
+
sessionId: (await sc.create({ cwd: cwd.length > 0 ? cwd : void 0 })).sessionId,
|
|
991
|
+
cwd: cwd || null
|
|
992
|
+
});
|
|
993
|
+
} catch (err) {
|
|
994
|
+
json$1(res, {
|
|
995
|
+
ok: false,
|
|
996
|
+
error: err instanceof Error ? err.message : String(err)
|
|
997
|
+
}, 500);
|
|
998
|
+
}
|
|
999
|
+
};
|
|
821
1000
|
const handleNote = async (req, res) => {
|
|
822
1001
|
try {
|
|
823
1002
|
const body = JSON.parse(await readBody$1(req));
|
|
@@ -879,7 +1058,7 @@ function registerRoutes(ctx, deps) {
|
|
|
879
1058
|
json$1(res, {
|
|
880
1059
|
ok: true,
|
|
881
1060
|
...result,
|
|
882
|
-
twUrl:
|
|
1061
|
+
twUrl: TW_PROXY_PATH
|
|
883
1062
|
});
|
|
884
1063
|
} catch (err) {
|
|
885
1064
|
json$1(res, {
|
|
@@ -1109,7 +1288,7 @@ function registerRoutes(ctx, deps) {
|
|
|
1109
1288
|
ok: true,
|
|
1110
1289
|
name: candidate,
|
|
1111
1290
|
path: `files/${candidate}`,
|
|
1112
|
-
url:
|
|
1291
|
+
url: `${TW_PROXY_PATH}files/${encodeURIComponent(candidate)}`,
|
|
1113
1292
|
size: buf.length,
|
|
1114
1293
|
type: req.headers["content-type"] ?? "application/octet-stream"
|
|
1115
1294
|
});
|
|
@@ -1157,6 +1336,62 @@ function registerRoutes(ctx, deps) {
|
|
|
1157
1336
|
}, 502);
|
|
1158
1337
|
}
|
|
1159
1338
|
};
|
|
1339
|
+
/**
|
|
1340
|
+
* SAME-ORIGIN proxy /dsh-tiddlywiki/tw/<rest> → TW root /<rest>. Serves the
|
|
1341
|
+
* ENTIRE TW frontend (index HTML, /files/*, the TiddlyWeb API) to the
|
|
1342
|
+
* browser through the DSH origin, so the embedded editor works from any
|
|
1343
|
+
* host/domain the user reaches DSH on (loopback, LAN, Tailscale, domain,
|
|
1344
|
+
* HTTPS). The browser never talks to the loopback TW child directly; DSH
|
|
1345
|
+
* does, on the same machine. Binary responses are buffered losslessly
|
|
1346
|
+
* (arrayBuffer) — unlike the /api JSON proxy, this route must never .text().
|
|
1347
|
+
*/
|
|
1348
|
+
const handleTwProxy = async (req, res) => {
|
|
1349
|
+
if (deps.getClient() === void 0) {
|
|
1350
|
+
json$1(res, {
|
|
1351
|
+
ok: false,
|
|
1352
|
+
error: "wiki service is not running"
|
|
1353
|
+
}, 503);
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
1357
|
+
const rest = url.pathname.replace(new RegExp(`^${TW_PROXY_PREFIX}(?=/|$)`), "") || "/";
|
|
1358
|
+
try {
|
|
1359
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
1360
|
+
const headers = forwardHeaders(req.headers);
|
|
1361
|
+
if (method === "PUT" || method === "DELETE" || method === "POST") headers["x-requested-with"] = "TiddlyWiki";
|
|
1362
|
+
const init = {
|
|
1363
|
+
method,
|
|
1364
|
+
headers,
|
|
1365
|
+
signal: AbortSignal.timeout(3e4)
|
|
1366
|
+
};
|
|
1367
|
+
if (method === "PUT" || method === "POST") init.body = await readBodyBuffer(req, MAX_UPLOAD_BYTES);
|
|
1368
|
+
const upstream = await fetch(`${deps.server.url}${rest}${url.search}`, init);
|
|
1369
|
+
const data = Buffer.from(await upstream.arrayBuffer());
|
|
1370
|
+
const responseHeaders = {
|
|
1371
|
+
"content-type": upstream.headers.get("content-type") ?? "application/octet-stream",
|
|
1372
|
+
"cache-control": upstream.headers.get("cache-control") ?? "no-store"
|
|
1373
|
+
};
|
|
1374
|
+
for (const name of [
|
|
1375
|
+
"etag",
|
|
1376
|
+
"last-modified",
|
|
1377
|
+
"content-disposition"
|
|
1378
|
+
]) {
|
|
1379
|
+
const value = upstream.headers.get(name);
|
|
1380
|
+
if (value !== null) responseHeaders[name] = value;
|
|
1381
|
+
}
|
|
1382
|
+
res.writeHead(upstream.status, responseHeaders);
|
|
1383
|
+
res.end(data);
|
|
1384
|
+
} catch (err) {
|
|
1385
|
+
if (res.headersSent) {
|
|
1386
|
+
res.end();
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
json$1(res, {
|
|
1390
|
+
ok: false,
|
|
1391
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1392
|
+
}, 502);
|
|
1393
|
+
}
|
|
1394
|
+
};
|
|
1160
1395
|
const disposers = [
|
|
1161
1396
|
ctx.webServer.register({
|
|
1162
1397
|
kind: "exact",
|
|
@@ -1221,12 +1456,40 @@ function registerRoutes(ctx, deps) {
|
|
|
1221
1456
|
handleRestart(req, res);
|
|
1222
1457
|
}
|
|
1223
1458
|
}),
|
|
1459
|
+
ctx.webServer.register({
|
|
1460
|
+
kind: "exact",
|
|
1461
|
+
path: `${ROUTE_PREFIX}/agent/sessions`,
|
|
1462
|
+
handler: (req, res) => {
|
|
1463
|
+
handleAgentSessions(req, res);
|
|
1464
|
+
}
|
|
1465
|
+
}),
|
|
1466
|
+
ctx.webServer.register({
|
|
1467
|
+
kind: "exact",
|
|
1468
|
+
path: `${ROUTE_PREFIX}/agent/send`,
|
|
1469
|
+
handler: (req, res) => {
|
|
1470
|
+
handleAgentSend(req, res);
|
|
1471
|
+
}
|
|
1472
|
+
}),
|
|
1473
|
+
ctx.webServer.register({
|
|
1474
|
+
kind: "exact",
|
|
1475
|
+
path: `${ROUTE_PREFIX}/agent/create`,
|
|
1476
|
+
handler: (req, res) => {
|
|
1477
|
+
handleAgentCreate(req, res);
|
|
1478
|
+
}
|
|
1479
|
+
}),
|
|
1224
1480
|
ctx.webServer.register({
|
|
1225
1481
|
kind: "prefix",
|
|
1226
1482
|
path: `${ROUTE_PREFIX}/api`,
|
|
1227
1483
|
handler: (req, res) => {
|
|
1228
1484
|
handleApiProxy(req, res);
|
|
1229
1485
|
}
|
|
1486
|
+
}),
|
|
1487
|
+
ctx.webServer.register({
|
|
1488
|
+
kind: "prefix",
|
|
1489
|
+
path: `${TW_PROXY_PREFIX}`,
|
|
1490
|
+
handler: (req, res) => {
|
|
1491
|
+
handleTwProxy(req, res);
|
|
1492
|
+
}
|
|
1230
1493
|
})
|
|
1231
1494
|
];
|
|
1232
1495
|
return () => {
|
|
@@ -2827,13 +3090,42 @@ const DEFAULTS = {
|
|
|
2827
3090
|
ui: {
|
|
2828
3091
|
showQuickNote: true,
|
|
2829
3092
|
showPanelStatus: true,
|
|
2830
|
-
showSyncButton: true
|
|
3093
|
+
showSyncButton: true,
|
|
3094
|
+
sendToAgent: { enabled: true }
|
|
2831
3095
|
},
|
|
2832
3096
|
auth: {
|
|
2833
3097
|
username: "",
|
|
2834
3098
|
password: ""
|
|
2835
3099
|
}
|
|
2836
3100
|
};
|
|
3101
|
+
/** Config tiddler steering TW's frontend API base (tiddlywebadaptor). */
|
|
3102
|
+
const TW_WEB_HOST_TIDDLER = "$:/config/tiddlyweb/host";
|
|
3103
|
+
/** The legacy default value this plugin replaces with the same-origin proxy. */
|
|
3104
|
+
const TW_WEB_HOST_DEFAULT = "$protocol$//$host$/";
|
|
3105
|
+
/**
|
|
3106
|
+
* Point TW's frontend at the same-origin DSH proxy (remote-access mode, R1).
|
|
3107
|
+
* The tiddlywebadaptor builds every API URL from $:/config/tiddlyweb/host; its
|
|
3108
|
+
* default `$protocol$//$host$/` resolves to the iframe's origin ROOT, which
|
|
3109
|
+
* 404s whenever the browser is not on the same machine as TW. Written only
|
|
3110
|
+
* when the tiddler is missing or still the legacy default — a user override is
|
|
3111
|
+
* honored (e.g. someone who really does expose TW on a dedicated origin).
|
|
3112
|
+
*/
|
|
3113
|
+
async function ensureTwWebHost(client) {
|
|
3114
|
+
if (client === void 0) return;
|
|
3115
|
+
let current;
|
|
3116
|
+
try {
|
|
3117
|
+
current = (await client.get(TW_WEB_HOST_TIDDLER))?.text;
|
|
3118
|
+
} catch {
|
|
3119
|
+
current = void 0;
|
|
3120
|
+
}
|
|
3121
|
+
if (current !== void 0 && current.trim() !== TW_WEB_HOST_DEFAULT) return;
|
|
3122
|
+
await client.put({
|
|
3123
|
+
title: TW_WEB_HOST_TIDDLER,
|
|
3124
|
+
text: TW_PROXY_PATH,
|
|
3125
|
+
type: "text/plain",
|
|
3126
|
+
tags: []
|
|
3127
|
+
});
|
|
3128
|
+
}
|
|
2837
3129
|
/** Expand $VAR / ${VAR} / %VAR% from process.env (config uses $DSH_HOME). */
|
|
2838
3130
|
function expandEnvPath(input) {
|
|
2839
3131
|
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 +3298,11 @@ function apply(ctx, rawConfig = {}) {
|
|
|
3006
3298
|
try {
|
|
3007
3299
|
await server.start();
|
|
3008
3300
|
await configStore.load(client());
|
|
3301
|
+
try {
|
|
3302
|
+
await ensureTwWebHost(client());
|
|
3303
|
+
} catch (err) {
|
|
3304
|
+
console.warn("[dsh-tiddlywiki] ensuring $:/config/tiddlyweb/host:", err);
|
|
3305
|
+
}
|
|
3009
3306
|
try {
|
|
3010
3307
|
const seedClient = client();
|
|
3011
3308
|
if (seedClient !== void 0) await seedDocNote(seedClient);
|
|
@@ -3034,6 +3331,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
3034
3331
|
})();
|
|
3035
3332
|
ctx.inject(["webServer"], (webCtx) => {
|
|
3036
3333
|
const ws = webCtx.webServer;
|
|
3334
|
+
const getSessionController = () => ctx.get("sessionController");
|
|
3037
3335
|
const disposeRoutes = registerRoutes({ webServer: ws }, {
|
|
3038
3336
|
server,
|
|
3039
3337
|
getClient: client,
|
|
@@ -3041,7 +3339,13 @@ function apply(ctx, rawConfig = {}) {
|
|
|
3041
3339
|
autoCommit: () => committer?.touch(),
|
|
3042
3340
|
noteDefaults: () => ({ tag: effectiveNoteTag() }),
|
|
3043
3341
|
uiDefaults: () => effectiveUi(),
|
|
3044
|
-
getWikiPath: () => wikiPath
|
|
3342
|
+
getWikiPath: () => wikiPath,
|
|
3343
|
+
getSessionController,
|
|
3344
|
+
sendToAgentEnabled: () => eff().ui?.sendToAgent?.enabled !== false,
|
|
3345
|
+
sendToAgentToken: () => {
|
|
3346
|
+
const token = eff().ui?.sendToAgent?.token;
|
|
3347
|
+
return typeof token === "string" ? token : "";
|
|
3348
|
+
}
|
|
3045
3349
|
});
|
|
3046
3350
|
const disposeAdmin = registerAdminRoutes({ webServer: ws }, {
|
|
3047
3351
|
server,
|
|
@@ -3066,6 +3370,6 @@ function apply(ctx, rawConfig = {}) {
|
|
|
3066
3370
|
}, "dsh-tiddlywiki: host teardown");
|
|
3067
3371
|
}
|
|
3068
3372
|
//#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 };
|
|
3373
|
+
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
3374
|
|
|
3071
3375
|
//# sourceMappingURL=index.js.map
|