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.
- package/LICENSE +21 -21
- package/README.md +8 -1
- package/cordis.patch.yml +13 -13
- package/lib/client.bundle.js +53 -26
- package/lib/client.js +53 -26
- package/lib/index.js +136 -25
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/endpoints.ts +10 -10
- package/src/client/index.ts +36 -0
- package/src/client/markdown-editor.ts +128 -128
- package/src/client/panel.ts +4 -0
- package/src/client/rightbar-tab.ts +440 -0
- package/src/client/session-summary.ts +279 -279
- package/src/client/settings-page.ts +1 -0
- package/src/client/state.ts +39 -39
- package/src/client/styles.ts +27 -0
- package/src/client/theme-sync.ts +312 -312
- package/src/client/toast.ts +22 -22
- package/src/client/tool-views.ts +604 -604
- package/src/client/ui-config.ts +64 -61
- package/src/host/config.ts +7 -0
- package/src/host/http.ts +57 -57
- package/src/host/routes.ts +2 -0
- package/src/host/seed-home.ts +1 -1
- package/src/host/seed-render.ts +66 -66
- package/src/host/seed-send-to-agent.ts +63 -63
- package/src/host/tools.ts +40 -5
- package/src/host/tw-api.ts +108 -16
- package/src/host/wiki.ts +299 -299
- package/src/index.ts +9 -8
- package/src/sdk.ts +204 -204
|
@@ -1,63 +1,63 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Generated from the wiki bundle (do not hand-edit the constant).
|
|
3
|
-
* Source: send-to-agent.bundle.json
|
|
4
|
-
*
|
|
5
|
-
* The "发送给 Agent" TW button, packaged as a TiddlyWiki plugin bundle
|
|
6
|
-
* (`{"tiddlers": {...}}`), seeded into fresh wikis by seedSendToAgent.
|
|
7
|
-
*
|
|
8
|
-
* @module dsh-tiddlywiki/host/seed-send-to-agent
|
|
9
|
-
*/
|
|
10
|
-
import type { TiddlyWebClient } from './tw-api.ts'
|
|
11
|
-
|
|
12
|
-
/** The packaged plugin tiddler title (a TW system tiddler, type application/json). */
|
|
13
|
-
export const SEND_TO_AGENT_PLUGIN_TITLE = '$:/plugins/dsh/send-to-agent'
|
|
14
|
-
|
|
15
|
-
/** One-time marker: presence means "the button was offered once — hands off". */
|
|
16
|
-
export const SEND_TO_AGENT_MARKER_TITLE = '$:/plugins/dsh-tiddlywiki/seed-send-to-agent'
|
|
17
|
-
|
|
18
|
-
/** The bundle's JSON text (`{"tiddlers": {...}}`), exactly as TW stores it. */
|
|
19
|
-
export const SEND_TO_AGENT_BUNDLE_TEXT = "{\n \"tiddlers\": {\n \"$:/plugins/dsh/send-to-agent/plugin.info\": {\n \"title\": \"$:/plugins/dsh/send-to-agent/plugin.info\",\n \"type\": \"application/json\",\n \"text\": \"{\\\"title\\\":\\\"$:/plugins/dsh/send-to-agent\\\",\\\"name\\\":\\\"Send to Agent\\\",\\\"description\\\":\\\"把当前笔记一键发送给 DSH Agent(TiddlyWiki → DSH 会话注入)\\\",\\\"author\\\":\\\"dsh-tiddlywiki\\\",\\\"version\\\":\\\"0.3.4\\\",\\\"plugin-type\\\":\\\"plugin\\\"}\"\n },\n \"$:/plugins/dsh/send-to-agent/startup.js\": {\n \"title\": \"$:/plugins/dsh/send-to-agent/startup.js\",\n \"type\": \"application/javascript\",\n \"module-type\": \"startup\",\n \"text\": \"/*\\\\\\ntitle: $:/plugins/dsh/send-to-agent/startup.js\\ntype: application/javascript\\nmodule-type: startup\\n\\n\\\\*/\\n(function(){\\n\\n/*jslint node: true, browser: true */\\n/*global $tw: false */\\n\\\"use strict\\\";\\n\\nexports.name = \\\"dsh-send-to-agent\\\";\\nexports.after = [\\\"story\\\"];\\nexports.platforms = [\\\"browser\\\"];\\n\\nfunction readConfig() {\\n\\tvar config = { enabled: true, endpoint: \\\"\\\", token: \\\"\\\" };\\n\\ttry {\\n\\t\\tvar t = $tw.wiki.getTiddler(\\\"$:/plugins/dsh-tiddlywiki/config\\\");\\n\\t\\tif (t && t.fields && typeof t.fields.text === \\\"string\\\") {\\n\\t\\t\\tvar parsed = JSON.parse(t.fields.text);\\n\\t\\t\\tvar s2a = parsed && parsed.ui && parsed.ui.sendToAgent;\\n\\t\\t\\tif (s2a) {\\n\\t\\t\\t\\tif (typeof s2a.enabled === \\\"boolean\\\") { config.enabled = s2a.enabled; }\\n\\t\\t\\t\\tif (typeof s2a.endpoint === \\\"string\\\" && s2a.endpoint.length > 0) { config.endpoint = s2a.endpoint; }\\n\\t\\t\\t\\tif (typeof s2a.token === \\\"string\\\" && s2a.token.length > 0) { config.token = s2a.token; }\\n\\t\\t\\t}\\n\\t\\t}\\n\\t} catch (e) {}\\n\\treturn config;\\n}\\n\\nfunction baseEndpoint() {\\n\\tvar config = readConfig();\\n\\tif (config.endpoint.length > 0) { return config.endpoint.replace(/\\\\/+$/, \\\"\\\"); }\\n\\tif (typeof location !== \\\"undefined\\\" && location.origin) { return location.origin + \\\"/dsh-tiddlywiki\\\"; }\\n\\treturn \\\"/dsh-tiddlywiki\\\";\\n}\\n\\nfunction notify(msg) {\\n\\tif ($tw.notifier && typeof $tw.notifier.display === \\\"function\\\") {\\n\\t\\t$tw.notifier.display(msg);\\n\\t} else if (typeof alert === \\\"function\\\") {\\n\\t\\talert(msg);\\n\\t}\\n}\\n\\nfunction doSend(payload, sessionId, note) {\\n\\tvar config = readConfig();\\n\\tvar lines = [];\\n\\tlines.push(\\\"《\\\" + payload.title + \\\"》\\\");\\n\\tlines.push(\\\"标签: \\\" + (payload.tags || []).join(\\\", \\\"));\\n\\tlines.push(\\\"类型: \\\" + (payload.type || \\\"无\\\"));\\n\\tlines.push(\\\"\\\");\\n\\tlines.push(\\\"【待办说明】以下内容是我(用户)提前编辑在 TiddlyWiki 知识库中的待办事项,通过「发送给 Agent」一键发送给你处理。请按内容执行;如有任何不清楚的地方,请主动向我提问,不要臆测或擅自发挥。\\\");\\n\\tlines.push(\\\"\\\");\\n\\tlines.push(payload.text || \\\"\\\");\\n\\t// 附加说明放在消息最后:正文之后、作为我(用户)的最终补充要求,优先遵循。\\n\\tif (note && String(note).trim().length > 0) {\\n\\t\\tlines.push(\\\"\\\");\\n\\t\\tlines.push(\\\"【附加说明】\\\" + String(note).trim());\\n\\t}\\n\\tvar text = lines.join(\\\"\\\\n\\\");\\n\\tvar headers = { \\\"Content-Type\\\": \\\"application/json\\\" };\\n\\tif (config.token.length > 0) { headers[\\\"x-send-to-agent-token\\\"] = config.token; }\\n\\t$tw.utils.httpRequest({\\n\\t\\turl: baseEndpoint() + \\\"/agent/send\\\",\\n\\t\\ttype: \\\"POST\\\",\\n\\t\\theaders: headers,\\n\\t\\tdata: JSON.stringify({ sessionId: sessionId, text: text }),\\n\\t\\tcallback: function(err, data) {\\n\\t\\t\\tvar parsed = null;\\n\\t\\t\\ttry { parsed = JSON.parse(data || \\\"\\\"); } catch (e) {}\\n\\t\\t\\tif (err) { notify(\\\"发送失败:\\\" + err); return; }\\n\\t\\t\\tif (parsed && parsed.ok) {\\n\\t\\t\\t\\tnotify(\\\"已发送 ✓ 会话 \\\" + sessionId.slice(0, 8));\\n\\t\\t\\t} else {\\n\\t\\t\\t\\tnotify(\\\"发送失败:\\\" + ((parsed && parsed.error) || data || \\\"未知错误\\\"));\\n\\t\\t\\t}\\n\\t\\t}\\n\\t});\\n}\\n\\nfunction closeOverlay(overlay, escHandler) {\\n\\tif (escHandler) { document.removeEventListener(\\\"keydown\\\", escHandler); }\\n\\tif (overlay && overlay.parentNode) { overlay.parentNode.removeChild(overlay); }\\n}\\n\\nfunction showPicker(payload, items, modes, defaultId, permissions) {\\n\\tif (typeof document === \\\"undefined\\\") { return; }\\n\\t// remove any previously-open picker\\n\\tvar old = document.getElementById(\\\"dsh-send-picker\\\");\\n\\tif (old && old.parentNode) { old.parentNode.removeChild(old); }\\n\\n\\tvar state = { workspace: null, mode: \\\"\\\", note: \\\"\\\", permission: \\\"\\\" };\\n\\n\\tvar overlay = document.createElement(\\\"div\\\");\\n\\toverlay.id = \\\"dsh-send-picker\\\";\\n\\toverlay.setAttribute(\\\"style\\\", \\\"position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.35);z-index:99999;display:flex;align-items:center;justify-content:center;\\\");\\n\\tvar box = document.createElement(\\\"div\\\");\\n\\tbox.setAttribute(\\\"style\\\", \\\"background:#fff;color:#333;border-radius:8px;padding:14px;min-width:340px;max-width:560px;max-height:82vh;display:flex;flex-direction:column;box-shadow:0 8px 30px rgba(0,0,0,.3);font-family:system-ui,-apple-system,sans-serif;\\\");\\n\\n\\t// header\\n\\tvar header = document.createElement(\\\"div\\\");\\n\\theader.setAttribute(\\\"style\\\", \\\"display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;\\\");\\n\\tvar h = document.createElement(\\\"h3\\\");\\n\\th.setAttribute(\\\"style\\\", \\\"margin:0;font-size:15px;\\\");\\n\\th.textContent = \\\"发送给 Agent · 选择目标\\\";\\n\\tvar closeX = document.createElement(\\\"button\\\");\\n\\tcloseX.type = \\\"button\\\";\\n\\tcloseX.textContent = \\\"✕\\\";\\n\\tcloseX.title = \\\"关闭\\\";\\n\\tcloseX.setAttribute(\\\"style\\\", \\\"border:none;background:transparent;font-size:15px;cursor:pointer;color:#888;padding:2px 8px;border-radius:4px;\\\");\\n\\tcloseX.addEventListener(\\\"click\\\", function() { closeOverlay(overlay, escHandler); });\\n\\theader.appendChild(h);\\n\\theader.appendChild(closeX);\\n\\tbox.appendChild(header);\\n\\n\\t// description\\n\\tvar desc = document.createElement(\\\"p\\\");\\n\\tdesc.setAttribute(\\\"style\\\", \\\"margin:0 0 8px;font-size:12px;color:#888;word-break:break-all;\\\");\\n\\tdesc.textContent = \\\"《\\\" + payload.title + \\\"》将作为消息注入所选会话\\\";\\n\\tbox.appendChild(desc);\\n\\n\\t// 附加说明(可选)— any extra context the user wants to attach to the\\n\\t// message, e.g. what to focus on or how to handle it.\\n\\tvar noteRow = document.createElement(\\\"div\\\");\\n\\tnoteRow.setAttribute(\\\"style\\\", \\\"margin:0 0 8px;\\\");\\n\\tvar noteLbl = document.createElement(\\\"label\\\");\\n\\tnoteLbl.setAttribute(\\\"style\\\", \\\"display:block;font-size:12px;color:#666;margin-bottom:4px;\\\");\\n\\tnoteLbl.textContent = \\\"附加说明(可选,随笔记一起发给 Agent)\\\";\\n\\tnoteRow.appendChild(noteLbl);\\n\\tvar noteTa = document.createElement(\\\"textarea\\\");\\n\\tnoteTa.placeholder = \\\"例如:请重点看第 3 条;我希望你按 XX 方式处理…\\\";\\n\\tnoteTa.setAttribute(\\\"style\\\", \\\"width:100%;box-sizing:border-box;padding:6px 8px;border:1px solid #ccc;border-radius:6px;font-size:13px;min-height:44px;resize:vertical;font-family:inherit;\\\");\\n\\tnoteTa.addEventListener(\\\"input\\\", function() { state.note = noteTa.value; });\\n\\tnoteRow.appendChild(noteTa);\\n\\tbox.appendChild(noteRow);\\n\\n\\t// 工作模式(Agent 预设)selector — applies to newly created sessions; the\\n\\t// modes come from GET /agent/modes (id/name/description + deployment\\n\\t// default). When the modes endpoint is unreachable (e.g. an older host) the\\n\\t// row degrades to a hint and no mode is sent — DSH uses its default.\\n\\tvar modeRow = document.createElement(\\\"div\\\");\\n\\tmodeRow.setAttribute(\\\"style\\\", \\\"margin:0 0 8px;\\\");\\n\\tif (modes && modes.length > 0) {\\n\\t\\tvar modeLbl = document.createElement(\\\"label\\\");\\n\\t\\tmodeLbl.setAttribute(\\\"style\\\", \\\"display:block;font-size:12px;color:#666;margin-bottom:4px;\\\");\\n\\t\\tmodeLbl.textContent = \\\"工作模式(Agent 预设)— 用于新建会话\\\";\\n\\t\\tmodeRow.appendChild(modeLbl);\\n\\t\\tvar sel = document.createElement(\\\"select\\\");\\n\\t\\tsel.setAttribute(\\\"style\\\", \\\"width:100%;box-sizing:border-box;padding:6px 8px;border:1px solid #ccc;border-radius:6px;font-size:13px;background:#fff;\\\");\\n\\t\\tvar optDefault = document.createElement(\\\"option\\\");\\n\\t\\toptDefault.value = \\\"\\\";\\n\\t\\toptDefault.textContent = \\\"(默认模式)\\\";\\n\\t\\tsel.appendChild(optDefault);\\n\\t\\tmodes.forEach(function(m) {\\n\\t\\t\\tvar o = document.createElement(\\\"option\\\");\\n\\t\\t\\to.value = m.id || \\\"\\\";\\n\\t\\t\\to.textContent = (m.name || m.id) + (m.isDefault ? \\\"(默认)\\\" : \\\"\\\");\\n\\t\\t\\tsel.appendChild(o);\\n\\t\\t});\\n\\t\\t// preselect the deployment default when listed\\n\\t\\tif (defaultId) {\\n\\t\\t\\tfor (var i = 0; i < sel.options.length; i++) {\\n\\t\\t\\t\\tif (sel.options[i].value === defaultId) { sel.selectedIndex = i; break; }\\n\\t\\t\\t}\\n\\t\\t}\\n\\t\\tstate.mode = sel.options[sel.selectedIndex] ? sel.options[sel.selectedIndex].value : \\\"\\\";\\n\\t\\tsel.addEventListener(\\\"change\\\", function() { state.mode = sel.value; });\\n\\t\\tmodeRow.appendChild(sel);\\n\\t} else {\\n\\t\\tvar modeHint = document.createElement(\\\"p\\\");\\n\\t\\tmodeHint.setAttribute(\\\"style\\\", \\\"margin:0;font-size:12px;color:#999;\\\");\\n\\t\\tmodeHint.textContent = \\\"(未获取到可用工作模式,将使用 DSH 默认模式)\\\";\\n\\t\\tmodeRow.appendChild(modeHint);\\n\\t}\\n\\tbox.appendChild(modeRow);\\n\\n\\t// 权限(权限预设)selector — applies to newly created sessions only; the\\n\\t// options come from GET /agent/modes' `permissions` roster (each bundles a\\n\\t// sandbox mode + approval policy). Existing sessions keep their own\\n\\t// permission, so this only affects \\\"新建会话并发送\\\".\\n\\tvar permRow = document.createElement(\\\"div\\\");\\n\\tpermRow.setAttribute(\\\"style\\\", \\\"margin:0 0 8px;\\\");\\n\\tif (permissions && Array.isArray(permissions.items) && permissions.items.length > 0) {\\n\\t\\tvar permLbl = document.createElement(\\\"label\\\");\\n\\t\\tpermLbl.setAttribute(\\\"style\\\", \\\"display:block;font-size:12px;color:#666;margin-bottom:4px;\\\");\\n\\t\\tpermLbl.textContent = \\\"权限(权限预设)— 用于新建会话\\\";\\n\\t\\tpermRow.appendChild(permLbl);\\n\\t\\tvar psel = document.createElement(\\\"select\\\");\\n\\t\\tpsel.setAttribute(\\\"style\\\", \\\"width:100%;box-sizing:border-box;padding:6px 8px;border:1px solid #ccc;border-radius:6px;font-size:13px;background:#fff;\\\");\\n\\t\\tpermissions.items.forEach(function(p) {\\n\\t\\t\\tvar o = document.createElement(\\\"option\\\");\\n\\t\\t\\to.value = p.value || \\\"\\\";\\n\\t\\t\\to.textContent = p.name || p.value;\\n\\t\\t\\tif (p.description) { o.title = p.description; }\\n\\t\\t\\tpsel.appendChild(o);\\n\\t\\t});\\n\\t\\t// preselect the deployment default when listed\\n\\t\\tif (permissions.defaultId) {\\n\\t\\t\\tfor (var j = 0; j < psel.options.length; j++) {\\n\\t\\t\\t\\tif (psel.options[j].value === permissions.defaultId) { psel.selectedIndex = j; break; }\\n\\t\\t\\t}\\n\\t\\t}\\n\\t\\tstate.permission = psel.options[psel.selectedIndex] ? psel.options[psel.selectedIndex].value : \\\"\\\";\\n\\t\\tpsel.addEventListener(\\\"change\\\", function() { state.permission = psel.value; });\\n\\t\\tpermRow.appendChild(psel);\\n\\t} else {\\n\\t\\tvar permHint = document.createElement(\\\"p\\\");\\n\\t\\tpermHint.setAttribute(\\\"style\\\", \\\"margin:0;font-size:12px;color:#999;\\\");\\n\\t\\tpermHint.textContent = \\\"(未获取到权限预设,新建会话将使用 DSH 默认权限)\\\";\\n\\t\\tpermRow.appendChild(permHint);\\n\\t}\\n\\tbox.appendChild(permRow);\\n\\n\\t// scrollable body\\n\\tvar body = document.createElement(\\\"div\\\");\\n\\tbody.setAttribute(\\\"style\\\", \\\"overflow:auto;flex:1;min-height:0;\\\");\\n\\tbox.appendChild(body);\\n\\n\\t// footer with cancel\\n\\tvar footer = document.createElement(\\\"div\\\");\\n\\tfooter.setAttribute(\\\"style\\\", \\\"display:flex;justify-content:flex-end;gap:8px;margin-top:10px;\\\");\\n\\tvar cancel = document.createElement(\\\"button\\\");\\n\\tcancel.type = \\\"button\\\";\\n\\tcancel.textContent = \\\"取消\\\";\\n\\tcancel.setAttribute(\\\"style\\\", \\\"padding:6px 14px;border:1px solid #ccc;border-radius:6px;background:#fff;cursor:pointer;font-size:13px;\\\");\\n\\tcancel.addEventListener(\\\"click\\\", function() { closeOverlay(overlay, escHandler); });\\n\\tfooter.appendChild(cancel);\\n\\tbox.appendChild(footer);\\n\\n\\toverlay.appendChild(box);\\n\\tdocument.body.appendChild(overlay);\\n\\n\\t// backdrop click + Esc close\\n\\toverlay.addEventListener(\\\"click\\\", function(e) {\\n\\t\\tif (e.target === overlay) { closeOverlay(overlay, escHandler); }\\n\\t});\\n\\tvar escHandler = function(e) { if (e.key === \\\"Escape\\\") { closeOverlay(overlay, escHandler); } };\\n\\tdocument.addEventListener(\\\"keydown\\\", escHandler);\\n\\n\\t// group sessions by workspace (cwd)\\n\\tvar groups = {};\\n\\tvar order = [];\\n\\t(items || []).forEach(function(s) {\\n\\t\\tvar key = (s.cwd && String(s.cwd).length > 0) ? s.cwd : \\\"__default__\\\";\\n\\t\\tif (!groups[key]) { groups[key] = { cwd: s.cwd, sessions: [], max: 0 }; order.push(key); }\\n\\t\\tgroups[key].sessions.push(s);\\n\\t\\tif ((s.updatedAt || 0) > groups[key].max) { groups[key].max = s.updatedAt || 0; }\\n\\t});\\n\\torder.sort(function(a, b) { return groups[b].max - groups[a].max; });\\n\\n\\tfunction btnStyle() {\\n\\t\\treturn \\\"display:block;width:100%;text-align:left;padding:8px 10px;margin:4px 0;border:1px solid #ddd;border-radius:6px;background:#f7f7f7;cursor:pointer;font-size:13px;\\\";\\n\\t}\\n\\tfunction primaryBtnStyle() {\\n\\t\\treturn \\\"display:block;width:100%;text-align:center;padding:7px 10px;margin:6px 0 0;border:1px solid #4a90d9;border-radius:6px;background:#4a90d9;color:#fff;cursor:pointer;font-size:13px;\\\";\\n\\t}\\n\\tfunction smallBtnStyle() {\\n\\t\\treturn \\\"margin:0 0 6px;padding:4px 10px;border:1px solid #ccc;border-radius:6px;background:#fff;cursor:pointer;font-size:12px;color:#555;\\\";\\n\\t}\\n\\n\\tfunction shortSessionLabel(s) {\\n\\t\\tvar id = s.sessionId || \\\"\\\";\\n\\t\\tvar parts = id.split(\\\"-\\\");\\n\\t\\tvar short = (parts.length > 1 ? parts[parts.length - 1] : id).slice(-10);\\n\\t\\tvar when = \\\"\\\";\\n\\t\\tif (s.updatedAt) {\\n\\t\\t\\tvar diff = Date.now() - s.updatedAt;\\n\\t\\t\\tif (diff < 60000) { when = \\\"刚刚\\\"; }\\n\\t\\t\\telse if (diff < 3600000) { when = Math.floor(diff / 60000) + \\\" 分钟前\\\"; }\\n\\t\\t\\telse if (diff < 86400000) { when = Math.floor(diff / 3600000) + \\\" 小时前\\\"; }\\n\\t\\t\\telse {\\n\\t\\t\\t\\tvar d = new Date(s.updatedAt);\\n\\t\\t\\t\\twhen = (d.getMonth() + 1) + \\\"-\\\" + d.getDate() + \\\" \\\" + (\\\"0\\\" + d.getHours()).slice(-2) + \\\":\\\" + (\\\"0\\\" + d.getMinutes()).slice(-2);\\n\\t\\t\\t}\\n\\t\\t}\\n\\t\\treturn \\\"#\\\" + short + (when ? \\\" · \\\" + when : \\\"\\\") + (s.running ? \\\" · ●运行中\\\" : \\\"\\\") + (s.blank ? \\\" · (空)\\\" : \\\"\\\");\\n\\t}\\n\\n\\tfunction modeName(id) {\\n\\t\\tif (!id) { return \\\"\\\"; }\\n\\t\\tfor (var i = 0; i < (modes || []).length; i++) {\\n\\t\\t\\tif (modes[i].id === id) { return modes[i].name || modes[i].id; }\\n\\t\\t}\\n\\t\\treturn id;\\n\\t}\\n\\n\\tfunction render() {\\n\\t\\tbody.innerHTML = \\\"\\\";\\n\\t\\tif (state.workspace === null) { renderWorkspaces(); } else { renderSessions(state.workspace); }\\n\\t}\\n\\n\\tfunction renderWorkspaces() {\\n\\t\\tif (order.length === 0) {\\n\\t\\t\\tvar none = document.createElement(\\\"p\\\");\\n\\t\\t\\tnone.textContent = \\\"还没有任何会话——在下方新建一个吧\\\";\\n\\t\\t\\tnone.setAttribute(\\\"style\\\", \\\"color:#999;font-size:13px;margin:0 0 8px;\\\");\\n\\t\\t\\tbody.appendChild(none);\\n\\t\\t} else {\\n\\t\\t\\torder.forEach(function(key) {\\n\\t\\t\\t\\tvar g = groups[key];\\n\\t\\t\\t\\tvar label = g.cwd || \\\"(默认工作区)\\\";\\n\\t\\t\\t\\tvar b = document.createElement(\\\"button\\\");\\n\\t\\t\\t\\tb.type = \\\"button\\\";\\n\\t\\t\\t\\tb.setAttribute(\\\"style\\\", btnStyle());\\n\\t\\t\\t\\tb.textContent = \\\"📁 \\\" + label + \\\" · \\\" + g.sessions.length + \\\" 个会话\\\";\\n\\t\\t\\t\\tb.title = g.cwd || \\\"默认工作区\\\";\\n\\t\\t\\t\\tb.addEventListener(\\\"click\\\", function() { state.workspace = key; render(); });\\n\\t\\t\\t\\tbody.appendChild(b);\\n\\t\\t\\t});\\n\\t\\t}\\n\\t\\t// new workspace / session\\n\\t\\tvar newRow = document.createElement(\\\"div\\\");\\n\\t\\tnewRow.setAttribute(\\\"style\\\", \\\"margin-top:10px;border-top:1px solid #eee;padding-top:8px;\\\");\\n\\t\\tvar lbl = document.createElement(\\\"p\\\");\\n\\t\\tlbl.textContent = \\\"新建工作区 / 会话\\\";\\n\\t\\tlbl.setAttribute(\\\"style\\\", \\\"margin:0 0 6px;font-size:12px;color:#666;\\\");\\n\\t\\tnewRow.appendChild(lbl);\\n\\t\\tvar input = document.createElement(\\\"input\\\");\\n\\t\\tinput.type = \\\"text\\\";\\n\\t\\tinput.placeholder = \\\"输入工作区路径,留空为默认工作区\\\";\\n\\t\\tinput.setAttribute(\\\"style\\\", \\\"width:100%;box-sizing:border-box;padding:6px 8px;border:1px solid #ccc;border-radius:6px;font-size:13px;\\\");\\n\\t\\tnewRow.appendChild(input);\\n\\t\\tvar go = document.createElement(\\\"button\\\");\\n\\t\\tgo.type = \\\"button\\\";\\n\\t\\tgo.textContent = \\\"创建并发送\\\";\\n\\t\\tgo.setAttribute(\\\"style\\\", primaryBtnStyle());\\n\\t\\tgo.addEventListener(\\\"click\\\", function() { createAndSend(payload, input.value.trim()); });\\n\\t\\tnewRow.appendChild(go);\\n\\t\\tinput.addEventListener(\\\"keydown\\\", function(e) { if (e.key === \\\"Enter\\\") { createAndSend(payload, input.value.trim()); } });\\n\\t\\tbody.appendChild(newRow);\\n\\t}\\n\\n\\tfunction renderSessions(key) {\\n\\t\\tvar g = groups[key];\\n\\t\\tvar back = document.createElement(\\\"button\\\");\\n\\t\\tback.type = \\\"button\\\";\\n\\t\\tback.textContent = \\\"← 返回工作区列表\\\";\\n\\t\\tback.setAttribute(\\\"style\\\", smallBtnStyle());\\n\\t\\tback.addEventListener(\\\"click\\\", function() { state.workspace = null; render(); });\\n\\t\\tbody.appendChild(back);\\n\\n\\t\\tvar wsName = document.createElement(\\\"p\\\");\\n\\t\\twsName.textContent = g.cwd ? \\\"📁 \\\" + g.cwd : \\\"(默认工作区)\\\";\\n\\t\\twsName.setAttribute(\\\"style\\\", \\\"margin:0 0 6px;font-size:13px;font-weight:600;word-break:break-all;\\\");\\n\\t\\tbody.appendChild(wsName);\\n\\n\\t\\t(g.sessions || []).forEach(function(s) {\\n\\t\\t\\tvar b = document.createElement(\\\"button\\\");\\n\\t\\t\\tb.type = \\\"button\\\";\\n\\t\\t\\tb.setAttribute(\\\"style\\\", btnStyle());\\n\\t\\t\\tvar label = shortSessionLabel(s);\\n\\t\\t\\tif (s.agentPreset) { label += \\\" · 🧭 \\\" + modeName(s.agentPreset); }\\n\\t\\t\\tb.textContent = label;\\n\\t\\t\\tb.title = \\\"会话 \\\" + s.sessionId + (s.agentPreset ? \\\" · 工作模式 \\\" + modeName(s.agentPreset) : \\\"\\\");\\n\\t\\t\\tb.addEventListener(\\\"click\\\", function() { closeOverlay(overlay, escHandler); doSend(payload, s.sessionId, state.note); });\\n\\t\\t\\tbody.appendChild(b);\\n\\t\\t});\\n\\n\\t\\tvar newBtn = document.createElement(\\\"button\\\");\\n\\t\\tnewBtn.type = \\\"button\\\";\\n\\t\\tnewBtn.textContent = \\\"➕ 在此工作区新建会话并发送\\\";\\n\\t\\tnewBtn.setAttribute(\\\"style\\\", primaryBtnStyle());\\n\\t\\tnewBtn.addEventListener(\\\"click\\\", function() { createAndSend(payload, g.cwd); });\\n\\t\\tbody.appendChild(newBtn);\\n\\t}\\n\\n\\tfunction createAndSend(payload2, cwd) {\\n\\t\\tvar config = readConfig();\\n\\t\\tvar headers = { \\\"Content-Type\\\": \\\"application/json\\\" };\\n\\t\\tif (config.token.length > 0) { headers[\\\"x-send-to-agent-token\\\"] = config.token; }\\n\\t\\tvar body = { cwd: cwd || \\\"\\\" };\\n\\t\\tif (state.mode && String(state.mode).length > 0) { body.mode = state.mode; }\\n\\t\\tif (state.permission && String(state.permission).length > 0) { body.permission = state.permission; }\\n\\t\\t$tw.utils.httpRequest({\\n\\t\\t\\turl: baseEndpoint() + \\\"/agent/create\\\",\\n\\t\\t\\ttype: \\\"POST\\\",\\n\\t\\t\\theaders: headers,\\n\\t\\t\\tdata: JSON.stringify(body),\\n\\t\\t\\tcallback: function(err, data) {\\n\\t\\t\\t\\tvar parsed = null;\\n\\t\\t\\t\\ttry { parsed = JSON.parse(data || \\\"\\\"); } catch (e) {}\\n\\t\\t\\t\\tif (err) { notify(\\\"新建会话失败:\\\" + err); return; }\\n\\t\\t\\t\\tif (parsed && parsed.ok && parsed.sessionId) {\\n\\t\\t\\t\\t\\tcloseOverlay(overlay, escHandler);\\n\\t\\t\\t\\t\\tvar modeNote = parsed.mode ? \\\"(模式 \\\" + modeName(parsed.mode) + \\\")\\\" : \\\"\\\";\\n\\t\\t\\t\\t\\tvar permNote = parsed.permissionApplied ? \\\"(权限 \\\" + (parsed.permission || \\\"\\\") + \\\")\\\" : \\\"\\\";\\n\\t\\t\\t\\t\\tnotify(\\\"已创建会话并发送 ✓ \\\" + parsed.sessionId.slice(0, 8) + modeNote + permNote);\\n\\t\\t\\t\\t\\tdoSend(payload2, parsed.sessionId, state.note);\\n\\t\\t\\t\\t} else {\\n\\t\\t\\t\\t\\tnotify(\\\"新建会话失败:\\\" + ((parsed && parsed.error) || data || \\\"未知错误\\\"));\\n\\t\\t\\t\\t}\\n\\t\\t\\t}\\n\\t\\t});\\n\\t}\\n\\n\\trender();\\n}\\n\\nfunction handleSend(title) {\\n\\tif (!title) { notify(\\\"无法确定当前笔记标题\\\"); return; }\\n\\tvar tiddler = $tw.wiki.getTiddler(title);\\n\\tif (!tiddler) { notify(\\\"找不到笔记:\\\" + title); return; }\\n\\tvar config = readConfig();\\n\\tif (!config.enabled) { notify(\\\"「发送给 Agent」特性未启用(可在 TW 配置中打开)\\\"); return; }\\n\\tvar payload = {\\n\\t\\ttitle: title,\\n\\t\\ttext: tiddler.fields.text || \\\"\\\",\\n\\t\\ttags: tiddler.fields.tags || [],\\n\\t\\ttype: tiddler.fields.type || \\\"\\\"\\n\\t};\\n\\tvar headers = {};\\n\\tif (config.token.length > 0) { headers[\\\"x-send-to-agent-token\\\"] = config.token; }\\n\\t$tw.utils.httpRequest({\\n\\t\\turl: baseEndpoint() + \\\"/agent/sessions\\\",\\n\\t\\ttype: \\\"GET\\\",\\n\\t\\theaders: headers,\\n\\t\\tcallback: function(err, data) {\\n\\t\\t\\tvar parsed = null;\\n\\t\\t\\ttry { parsed = JSON.parse(data || \\\"\\\"); } catch (e) {}\\n\\t\\t\\tif (err) { notify(\\\"获取会话列表失败:\\\" + err); return; }\\n\\t\\t\\tif (!parsed || !parsed.ok || !parsed.items) { notify(\\\"获取会话列表失败:\\\" + ((parsed && parsed.error) || \\\"未知错误\\\")); return; }\\n\\t\\t\\t// fetch the available 工作模式 (Agent presets) alongside; degrade to\\n\\t\\t\\t// an empty roster (default mode) when the endpoint is missing.\\n\\t\\t\\t$tw.utils.httpRequest({\\n\\t\\t\\t\\turl: baseEndpoint() + \\\"/agent/modes\\\",\\n\\t\\t\\t\\ttype: \\\"GET\\\",\\n\\t\\t\\t\\theaders: headers,\\n\\t\\t\\t\\tcallback: function(err2, data2) {\\n\\t\\t\\t\\t\\tvar modes = [];\\n\\t\\t\\t\\t\\tvar defaultId = \\\"\\\";\\n\\t\\t\\t\\t\\tvar permissions = null;\\n\\t\\t\\t\\t\\tvar parsed2 = null;\\n\\t\\t\\t\\t\\ttry { parsed2 = JSON.parse(data2 || \\\"\\\"); } catch (e) {}\\n\\t\\t\\t\\t\\tif (!err2 && parsed2 && parsed2.ok) {\\n\\t\\t\\t\\t\\t\\tif (Array.isArray(parsed2.items)) {\\n\\t\\t\\t\\t\\t\\t\\tmodes = parsed2.items;\\n\\t\\t\\t\\t\\t\\t\\tdefaultId = parsed2.defaultId || \\\"\\\";\\n\\t\\t\\t\\t\\t\\t}\\n\\t\\t\\t\\t\\t\\tif (parsed2.permissions && Array.isArray(parsed2.permissions.items)) {\\n\\t\\t\\t\\t\\t\\t\\tpermissions = parsed2.permissions;\\n\\t\\t\\t\\t\\t\\t}\\n\\t\\t\\t\\t\\t}\\n\\t\\t\\t\\t\\tshowPicker(payload, parsed.items, modes, defaultId, permissions);\\n\\t\\t\\t\\t}\\n\\t\\t\\t});\\n\\t\\t}\\n\\t});\\n}\\n\\nexports.startup = function() {\\n\\tconsole.log(\\\"[dsh-send-to-agent] startup ran\\\");\\n\\tif (!$tw.rootWidget || typeof $tw.rootWidget.addEventListener !== \\\"function\\\") { return; }\\n\\t$tw.rootWidget.addEventListener(\\\"dsh-send-to-agent\\\", function(event) {\\n\\t\\thandleSend(event.param);\\n\\t});\\n};\\n\\n})();\\n\"\n },\n \"$:/plugins/dsh/send-to-agent/ui/icon\": {\n \"title\": \"$:/plugins/dsh/send-to-agent/ui/icon\",\n \"tags\": [\n \"$:/tags/Image\"\n ],\n \"text\": \"\\\\parameters (size:\\\"22pt\\\")\\n<svg width=<<size>> height=<<size>> class=\\\"tc-image-send-to-agent tc-image-button\\\" viewBox=\\\"0 0 24 24\\\"><path d=\\\"M3.478 2.404a.75.75 0 0 0-.926.941l2.432 7.905H13.5a.75.75 0 0 1 0 1.5H4.984l-2.432 7.905a.75.75 0 0 0 .926.94 60.519 60.519 0 0 0 18.445-8.986.75.75 0 0 0 0-1.218A60.517 60.517 0 0 0 3.478 2.404Z\\\"/></svg>\\n\"\n },\n \"$:/plugins/dsh/send-to-agent/ui/ViewToolbar/SendToAgent\": {\n \"title\": \"$:/plugins/dsh/send-to-agent/ui/ViewToolbar/SendToAgent\",\n \"type\": \"text/vnd.tiddlywiki\",\n \"tags\": [\n \"$:/tags/ViewToolbar\"\n ],\n \"icon\": \"$:/plugins/dsh/send-to-agent/ui/icon\",\n \"caption\": \"发送给 Agent\",\n \"description\": \"把当前笔记一键发送给 DSH Agent(TiddlyWiki → DSH 会话注入)\",\n \"text\": \"\\\\whitespace trim\\n<$button message=\\\"dsh-send-to-agent\\\"\\n\\tparam=<<currentTiddler>>\\n\\ttooltip=\\\"把当前笔记一键发送给 DSH Agent\\\"\\n\\taria-label=\\\"把当前笔记一键发送给 DSH Agent\\\"\\n\\tclass=<<tv-config-toolbar-class>>\\n>\\n\\t<%if [<tv-config-toolbar-icons>match[yes]] %>\\n\\t\\t{{$:/plugins/dsh/send-to-agent/ui/icon}}\\n\\t<%endif%>\\n\\t<%if [<tv-config-toolbar-text>match[yes]] %>\\n\\t\\t<span class=\\\"tc-btn-text\\\">\\n\\t\\t<$text text=\\\"发送给 Agent\\\"/>\\n\\t</span>\\n\\t<%endif%>\\n</$button>\\n\"\n },\n \"$:/core/ui/ControlPanel/Toolbars/ItemTemplate\": {\n \"title\": \"$:/core/ui/ControlPanel/Toolbars/ItemTemplate\",\n \"type\": \"text/vnd.tiddlywiki\",\n \"text\": \"\\\\define config-title()\\n$(config-base)$$(currentTiddler)$\\n\\\\end\\n\\\\whitespace trim\\n\\n<$draggable tiddler=<<currentTiddler>>>\\n<$checkbox tiddler=<<config-title>> field=\\\"text\\\" checked=\\\"show\\\" unchecked=\\\"hide\\\" default=\\\"show\\\"/>\\n \\n<span class=\\\"tc-icon-wrapper\\\"><$transclude tiddler={{!!icon}}/></span>\\n \\n<$transclude field=\\\"caption\\\"/>\\n -- \\n<i class=\\\"tc-muted\\\"><$transclude field=\\\"description\\\"/></i>\\n</$draggable>\\n\"\n }\n }\n}"
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Seed the "发送给 Agent" TW button exactly once per wiki (mirrors the doc-note
|
|
23
|
-
* one-shot policy). The marker records the offer; afterwards the bundle is
|
|
24
|
-
* user-owned — deleting it and restarting dsh web does NOT recreate it, and
|
|
25
|
-
* edits are never overwritten. With `opts.force` the bundle is (re)written even
|
|
26
|
-
* when it already exists and the marker is (re)written — the settings page uses
|
|
27
|
-
* this for "重新初始化". Returns whether a bundle was written this call.
|
|
28
|
-
* Never throws.
|
|
29
|
-
*/
|
|
30
|
-
export async function seedSendToAgent(client: TiddlyWebClient, opts?: { force?: boolean }): Promise<boolean> {
|
|
31
|
-
const force = opts?.force === true
|
|
32
|
-
if (!force) {
|
|
33
|
-
const marker = await client.get(SEND_TO_AGENT_MARKER_TITLE).catch(() => undefined)
|
|
34
|
-
if (marker !== undefined) return false
|
|
35
|
-
}
|
|
36
|
-
const existing = await client.get(SEND_TO_AGENT_PLUGIN_TITLE).catch(() => undefined)
|
|
37
|
-
let wrote = false
|
|
38
|
-
if (force || existing === undefined) {
|
|
39
|
-
await client.put({
|
|
40
|
-
title: SEND_TO_AGENT_PLUGIN_TITLE,
|
|
41
|
-
text: SEND_TO_AGENT_BUNDLE_TEXT,
|
|
42
|
-
type: 'application/json',
|
|
43
|
-
tags: [],
|
|
44
|
-
// TW only REGISTERS a wiki tiddler as a plugin (boot.js
|
|
45
|
-
// registerPluginTiddlers) when the OUTER tiddler carries a
|
|
46
|
-
// `plugin-type` field — without it the bundle is never unpacked and its
|
|
47
|
-
// startup module never runs in the embedded TW. Mirrors what the live
|
|
48
|
-
// wiki's send-to-agent bundle tiddler carries.
|
|
49
|
-
'plugin-type': 'plugin',
|
|
50
|
-
name: 'Send to Agent',
|
|
51
|
-
author: 'dsh-tiddlywiki',
|
|
52
|
-
version: '0.3.2',
|
|
53
|
-
description: '把当前笔记一键发送给 DSH Agent(TiddlyWiki → DSH 会话注入)',
|
|
54
|
-
})
|
|
55
|
-
wrote = true
|
|
56
|
-
}
|
|
57
|
-
// Record the offer regardless, so an existing bundle (upgrade from a
|
|
58
|
-
// pre-seed wiki) also becomes user-owned from here on.
|
|
59
|
-
await client
|
|
60
|
-
.put({ title: SEND_TO_AGENT_MARKER_TITLE, text: 'seeded-once', type: 'text/plain', tags: [] })
|
|
61
|
-
.catch(() => undefined)
|
|
62
|
-
return wrote
|
|
63
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Generated from the wiki bundle (do not hand-edit the constant).
|
|
3
|
+
* Source: send-to-agent.bundle.json
|
|
4
|
+
*
|
|
5
|
+
* The "发送给 Agent" TW button, packaged as a TiddlyWiki plugin bundle
|
|
6
|
+
* (`{"tiddlers": {...}}`), seeded into fresh wikis by seedSendToAgent.
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-tiddlywiki/host/seed-send-to-agent
|
|
9
|
+
*/
|
|
10
|
+
import type { TiddlyWebClient } from './tw-api.ts'
|
|
11
|
+
|
|
12
|
+
/** The packaged plugin tiddler title (a TW system tiddler, type application/json). */
|
|
13
|
+
export const SEND_TO_AGENT_PLUGIN_TITLE = '$:/plugins/dsh/send-to-agent'
|
|
14
|
+
|
|
15
|
+
/** One-time marker: presence means "the button was offered once — hands off". */
|
|
16
|
+
export const SEND_TO_AGENT_MARKER_TITLE = '$:/plugins/dsh-tiddlywiki/seed-send-to-agent'
|
|
17
|
+
|
|
18
|
+
/** The bundle's JSON text (`{"tiddlers": {...}}`), exactly as TW stores it. */
|
|
19
|
+
export const SEND_TO_AGENT_BUNDLE_TEXT = "{\n \"tiddlers\": {\n \"$:/plugins/dsh/send-to-agent/plugin.info\": {\n \"title\": \"$:/plugins/dsh/send-to-agent/plugin.info\",\n \"type\": \"application/json\",\n \"text\": \"{\\\"title\\\":\\\"$:/plugins/dsh/send-to-agent\\\",\\\"name\\\":\\\"Send to Agent\\\",\\\"description\\\":\\\"把当前笔记一键发送给 DSH Agent(TiddlyWiki → DSH 会话注入)\\\",\\\"author\\\":\\\"dsh-tiddlywiki\\\",\\\"version\\\":\\\"0.3.4\\\",\\\"plugin-type\\\":\\\"plugin\\\"}\"\n },\n \"$:/plugins/dsh/send-to-agent/startup.js\": {\n \"title\": \"$:/plugins/dsh/send-to-agent/startup.js\",\n \"type\": \"application/javascript\",\n \"module-type\": \"startup\",\n \"text\": \"/*\\\\\\ntitle: $:/plugins/dsh/send-to-agent/startup.js\\ntype: application/javascript\\nmodule-type: startup\\n\\n\\\\*/\\n(function(){\\n\\n/*jslint node: true, browser: true */\\n/*global $tw: false */\\n\\\"use strict\\\";\\n\\nexports.name = \\\"dsh-send-to-agent\\\";\\nexports.after = [\\\"story\\\"];\\nexports.platforms = [\\\"browser\\\"];\\n\\nfunction readConfig() {\\n\\tvar config = { enabled: true, endpoint: \\\"\\\", token: \\\"\\\" };\\n\\ttry {\\n\\t\\tvar t = $tw.wiki.getTiddler(\\\"$:/plugins/dsh-tiddlywiki/config\\\");\\n\\t\\tif (t && t.fields && typeof t.fields.text === \\\"string\\\") {\\n\\t\\t\\tvar parsed = JSON.parse(t.fields.text);\\n\\t\\t\\tvar s2a = parsed && parsed.ui && parsed.ui.sendToAgent;\\n\\t\\t\\tif (s2a) {\\n\\t\\t\\t\\tif (typeof s2a.enabled === \\\"boolean\\\") { config.enabled = s2a.enabled; }\\n\\t\\t\\t\\tif (typeof s2a.endpoint === \\\"string\\\" && s2a.endpoint.length > 0) { config.endpoint = s2a.endpoint; }\\n\\t\\t\\t\\tif (typeof s2a.token === \\\"string\\\" && s2a.token.length > 0) { config.token = s2a.token; }\\n\\t\\t\\t}\\n\\t\\t}\\n\\t} catch (e) {}\\n\\treturn config;\\n}\\n\\nfunction baseEndpoint() {\\n\\tvar config = readConfig();\\n\\tif (config.endpoint.length > 0) { return config.endpoint.replace(/\\\\/+$/, \\\"\\\"); }\\n\\tif (typeof location !== \\\"undefined\\\" && location.origin) { return location.origin + \\\"/dsh-tiddlywiki\\\"; }\\n\\treturn \\\"/dsh-tiddlywiki\\\";\\n}\\n\\nfunction notify(msg) {\\n\\tif ($tw.notifier && typeof $tw.notifier.display === \\\"function\\\") {\\n\\t\\t$tw.notifier.display(msg);\\n\\t} else if (typeof alert === \\\"function\\\") {\\n\\t\\talert(msg);\\n\\t}\\n}\\n\\nfunction doSend(payload, sessionId, note) {\\n\\tvar config = readConfig();\\n\\tvar lines = [];\\n\\tlines.push(\\\"《\\\" + payload.title + \\\"》\\\");\\n\\tlines.push(\\\"标签: \\\" + (payload.tags || []).join(\\\", \\\"));\\n\\tlines.push(\\\"类型: \\\" + (payload.type || \\\"无\\\"));\\n\\tlines.push(\\\"\\\");\\n\\tlines.push(\\\"【待办说明】以下内容是我(用户)提前编辑在 TiddlyWiki 知识库中的待办事项,通过「发送给 Agent」一键发送给你处理。请按内容执行;如有任何不清楚的地方,请主动向我提问,不要臆测或擅自发挥。\\\");\\n\\tlines.push(\\\"\\\");\\n\\tlines.push(payload.text || \\\"\\\");\\n\\t// 附加说明放在消息最后:正文之后、作为我(用户)的最终补充要求,优先遵循。\\n\\tif (note && String(note).trim().length > 0) {\\n\\t\\tlines.push(\\\"\\\");\\n\\t\\tlines.push(\\\"【附加说明】\\\" + String(note).trim());\\n\\t}\\n\\tvar text = lines.join(\\\"\\\\n\\\");\\n\\tvar headers = { \\\"Content-Type\\\": \\\"application/json\\\" };\\n\\tif (config.token.length > 0) { headers[\\\"x-send-to-agent-token\\\"] = config.token; }\\n\\t$tw.utils.httpRequest({\\n\\t\\turl: baseEndpoint() + \\\"/agent/send\\\",\\n\\t\\ttype: \\\"POST\\\",\\n\\t\\theaders: headers,\\n\\t\\tdata: JSON.stringify({ sessionId: sessionId, text: text }),\\n\\t\\tcallback: function(err, data) {\\n\\t\\t\\tvar parsed = null;\\n\\t\\t\\ttry { parsed = JSON.parse(data || \\\"\\\"); } catch (e) {}\\n\\t\\t\\tif (err) { notify(\\\"发送失败:\\\" + err); return; }\\n\\t\\t\\tif (parsed && parsed.ok) {\\n\\t\\t\\t\\tnotify(\\\"已发送 ✓ 会话 \\\" + sessionId.slice(0, 8));\\n\\t\\t\\t} else {\\n\\t\\t\\t\\tnotify(\\\"发送失败:\\\" + ((parsed && parsed.error) || data || \\\"未知错误\\\"));\\n\\t\\t\\t}\\n\\t\\t}\\n\\t});\\n}\\n\\nfunction closeOverlay(overlay, escHandler) {\\n\\tif (escHandler) { document.removeEventListener(\\\"keydown\\\", escHandler); }\\n\\tif (overlay && overlay.parentNode) { overlay.parentNode.removeChild(overlay); }\\n}\\n\\nfunction showPicker(payload, items, modes, defaultId, permissions) {\\n\\tif (typeof document === \\\"undefined\\\") { return; }\\n\\t// remove any previously-open picker\\n\\tvar old = document.getElementById(\\\"dsh-send-picker\\\");\\n\\tif (old && old.parentNode) { old.parentNode.removeChild(old); }\\n\\n\\tvar state = { workspace: null, mode: \\\"\\\", note: \\\"\\\", permission: \\\"\\\" };\\n\\n\\tvar overlay = document.createElement(\\\"div\\\");\\n\\toverlay.id = \\\"dsh-send-picker\\\";\\n\\toverlay.setAttribute(\\\"style\\\", \\\"position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.35);z-index:99999;display:flex;align-items:center;justify-content:center;\\\");\\n\\tvar box = document.createElement(\\\"div\\\");\\n\\tbox.setAttribute(\\\"style\\\", \\\"background:#fff;color:#333;border-radius:8px;padding:14px;min-width:340px;max-width:560px;max-height:82vh;display:flex;flex-direction:column;box-shadow:0 8px 30px rgba(0,0,0,.3);font-family:system-ui,-apple-system,sans-serif;\\\");\\n\\n\\t// header\\n\\tvar header = document.createElement(\\\"div\\\");\\n\\theader.setAttribute(\\\"style\\\", \\\"display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;\\\");\\n\\tvar h = document.createElement(\\\"h3\\\");\\n\\th.setAttribute(\\\"style\\\", \\\"margin:0;font-size:15px;\\\");\\n\\th.textContent = \\\"发送给 Agent · 选择目标\\\";\\n\\tvar closeX = document.createElement(\\\"button\\\");\\n\\tcloseX.type = \\\"button\\\";\\n\\tcloseX.textContent = \\\"✕\\\";\\n\\tcloseX.title = \\\"关闭\\\";\\n\\tcloseX.setAttribute(\\\"style\\\", \\\"border:none;background:transparent;font-size:15px;cursor:pointer;color:#888;padding:2px 8px;border-radius:4px;\\\");\\n\\tcloseX.addEventListener(\\\"click\\\", function() { closeOverlay(overlay, escHandler); });\\n\\theader.appendChild(h);\\n\\theader.appendChild(closeX);\\n\\tbox.appendChild(header);\\n\\n\\t// description\\n\\tvar desc = document.createElement(\\\"p\\\");\\n\\tdesc.setAttribute(\\\"style\\\", \\\"margin:0 0 8px;font-size:12px;color:#888;word-break:break-all;\\\");\\n\\tdesc.textContent = \\\"《\\\" + payload.title + \\\"》将作为消息注入所选会话\\\";\\n\\tbox.appendChild(desc);\\n\\n\\t// 附加说明(可选)— any extra context the user wants to attach to the\\n\\t// message, e.g. what to focus on or how to handle it.\\n\\tvar noteRow = document.createElement(\\\"div\\\");\\n\\tnoteRow.setAttribute(\\\"style\\\", \\\"margin:0 0 8px;\\\");\\n\\tvar noteLbl = document.createElement(\\\"label\\\");\\n\\tnoteLbl.setAttribute(\\\"style\\\", \\\"display:block;font-size:12px;color:#666;margin-bottom:4px;\\\");\\n\\tnoteLbl.textContent = \\\"附加说明(可选,随笔记一起发给 Agent)\\\";\\n\\tnoteRow.appendChild(noteLbl);\\n\\tvar noteTa = document.createElement(\\\"textarea\\\");\\n\\tnoteTa.placeholder = \\\"例如:请重点看第 3 条;我希望你按 XX 方式处理…\\\";\\n\\tnoteTa.setAttribute(\\\"style\\\", \\\"width:100%;box-sizing:border-box;padding:6px 8px;border:1px solid #ccc;border-radius:6px;font-size:13px;min-height:44px;resize:vertical;font-family:inherit;\\\");\\n\\tnoteTa.addEventListener(\\\"input\\\", function() { state.note = noteTa.value; });\\n\\tnoteRow.appendChild(noteTa);\\n\\tbox.appendChild(noteRow);\\n\\n\\t// 工作模式(Agent 预设)selector — applies to newly created sessions; the\\n\\t// modes come from GET /agent/modes (id/name/description + deployment\\n\\t// default). When the modes endpoint is unreachable (e.g. an older host) the\\n\\t// row degrades to a hint and no mode is sent — DSH uses its default.\\n\\tvar modeRow = document.createElement(\\\"div\\\");\\n\\tmodeRow.setAttribute(\\\"style\\\", \\\"margin:0 0 8px;\\\");\\n\\tif (modes && modes.length > 0) {\\n\\t\\tvar modeLbl = document.createElement(\\\"label\\\");\\n\\t\\tmodeLbl.setAttribute(\\\"style\\\", \\\"display:block;font-size:12px;color:#666;margin-bottom:4px;\\\");\\n\\t\\tmodeLbl.textContent = \\\"工作模式(Agent 预设)— 用于新建会话\\\";\\n\\t\\tmodeRow.appendChild(modeLbl);\\n\\t\\tvar sel = document.createElement(\\\"select\\\");\\n\\t\\tsel.setAttribute(\\\"style\\\", \\\"width:100%;box-sizing:border-box;padding:6px 8px;border:1px solid #ccc;border-radius:6px;font-size:13px;background:#fff;\\\");\\n\\t\\tvar optDefault = document.createElement(\\\"option\\\");\\n\\t\\toptDefault.value = \\\"\\\";\\n\\t\\toptDefault.textContent = \\\"(默认模式)\\\";\\n\\t\\tsel.appendChild(optDefault);\\n\\t\\tmodes.forEach(function(m) {\\n\\t\\t\\tvar o = document.createElement(\\\"option\\\");\\n\\t\\t\\to.value = m.id || \\\"\\\";\\n\\t\\t\\to.textContent = (m.name || m.id) + (m.isDefault ? \\\"(默认)\\\" : \\\"\\\");\\n\\t\\t\\tsel.appendChild(o);\\n\\t\\t});\\n\\t\\t// preselect the deployment default when listed\\n\\t\\tif (defaultId) {\\n\\t\\t\\tfor (var i = 0; i < sel.options.length; i++) {\\n\\t\\t\\t\\tif (sel.options[i].value === defaultId) { sel.selectedIndex = i; break; }\\n\\t\\t\\t}\\n\\t\\t}\\n\\t\\tstate.mode = sel.options[sel.selectedIndex] ? sel.options[sel.selectedIndex].value : \\\"\\\";\\n\\t\\tsel.addEventListener(\\\"change\\\", function() { state.mode = sel.value; });\\n\\t\\tmodeRow.appendChild(sel);\\n\\t} else {\\n\\t\\tvar modeHint = document.createElement(\\\"p\\\");\\n\\t\\tmodeHint.setAttribute(\\\"style\\\", \\\"margin:0;font-size:12px;color:#999;\\\");\\n\\t\\tmodeHint.textContent = \\\"(未获取到可用工作模式,将使用 DSH 默认模式)\\\";\\n\\t\\tmodeRow.appendChild(modeHint);\\n\\t}\\n\\tbox.appendChild(modeRow);\\n\\n\\t// 权限(权限预设)selector — applies to newly created sessions only; the\\n\\t// options come from GET /agent/modes' `permissions` roster (each bundles a\\n\\t// sandbox mode + approval policy). Existing sessions keep their own\\n\\t// permission, so this only affects \\\"新建会话并发送\\\".\\n\\tvar permRow = document.createElement(\\\"div\\\");\\n\\tpermRow.setAttribute(\\\"style\\\", \\\"margin:0 0 8px;\\\");\\n\\tif (permissions && Array.isArray(permissions.items) && permissions.items.length > 0) {\\n\\t\\tvar permLbl = document.createElement(\\\"label\\\");\\n\\t\\tpermLbl.setAttribute(\\\"style\\\", \\\"display:block;font-size:12px;color:#666;margin-bottom:4px;\\\");\\n\\t\\tpermLbl.textContent = \\\"权限(权限预设)— 用于新建会话\\\";\\n\\t\\tpermRow.appendChild(permLbl);\\n\\t\\tvar psel = document.createElement(\\\"select\\\");\\n\\t\\tpsel.setAttribute(\\\"style\\\", \\\"width:100%;box-sizing:border-box;padding:6px 8px;border:1px solid #ccc;border-radius:6px;font-size:13px;background:#fff;\\\");\\n\\t\\tpermissions.items.forEach(function(p) {\\n\\t\\t\\tvar o = document.createElement(\\\"option\\\");\\n\\t\\t\\to.value = p.value || \\\"\\\";\\n\\t\\t\\to.textContent = p.name || p.value;\\n\\t\\t\\tif (p.description) { o.title = p.description; }\\n\\t\\t\\tpsel.appendChild(o);\\n\\t\\t});\\n\\t\\t// preselect the deployment default when listed\\n\\t\\tif (permissions.defaultId) {\\n\\t\\t\\tfor (var j = 0; j < psel.options.length; j++) {\\n\\t\\t\\t\\tif (psel.options[j].value === permissions.defaultId) { psel.selectedIndex = j; break; }\\n\\t\\t\\t}\\n\\t\\t}\\n\\t\\tstate.permission = psel.options[psel.selectedIndex] ? psel.options[psel.selectedIndex].value : \\\"\\\";\\n\\t\\tpsel.addEventListener(\\\"change\\\", function() { state.permission = psel.value; });\\n\\t\\tpermRow.appendChild(psel);\\n\\t} else {\\n\\t\\tvar permHint = document.createElement(\\\"p\\\");\\n\\t\\tpermHint.setAttribute(\\\"style\\\", \\\"margin:0;font-size:12px;color:#999;\\\");\\n\\t\\tpermHint.textContent = \\\"(未获取到权限预设,新建会话将使用 DSH 默认权限)\\\";\\n\\t\\tpermRow.appendChild(permHint);\\n\\t}\\n\\tbox.appendChild(permRow);\\n\\n\\t// scrollable body\\n\\tvar body = document.createElement(\\\"div\\\");\\n\\tbody.setAttribute(\\\"style\\\", \\\"overflow:auto;flex:1;min-height:0;\\\");\\n\\tbox.appendChild(body);\\n\\n\\t// footer with cancel\\n\\tvar footer = document.createElement(\\\"div\\\");\\n\\tfooter.setAttribute(\\\"style\\\", \\\"display:flex;justify-content:flex-end;gap:8px;margin-top:10px;\\\");\\n\\tvar cancel = document.createElement(\\\"button\\\");\\n\\tcancel.type = \\\"button\\\";\\n\\tcancel.textContent = \\\"取消\\\";\\n\\tcancel.setAttribute(\\\"style\\\", \\\"padding:6px 14px;border:1px solid #ccc;border-radius:6px;background:#fff;cursor:pointer;font-size:13px;\\\");\\n\\tcancel.addEventListener(\\\"click\\\", function() { closeOverlay(overlay, escHandler); });\\n\\tfooter.appendChild(cancel);\\n\\tbox.appendChild(footer);\\n\\n\\toverlay.appendChild(box);\\n\\tdocument.body.appendChild(overlay);\\n\\n\\t// backdrop click + Esc close\\n\\toverlay.addEventListener(\\\"click\\\", function(e) {\\n\\t\\tif (e.target === overlay) { closeOverlay(overlay, escHandler); }\\n\\t});\\n\\tvar escHandler = function(e) { if (e.key === \\\"Escape\\\") { closeOverlay(overlay, escHandler); } };\\n\\tdocument.addEventListener(\\\"keydown\\\", escHandler);\\n\\n\\t// group sessions by workspace (cwd)\\n\\tvar groups = {};\\n\\tvar order = [];\\n\\t(items || []).forEach(function(s) {\\n\\t\\tvar key = (s.cwd && String(s.cwd).length > 0) ? s.cwd : \\\"__default__\\\";\\n\\t\\tif (!groups[key]) { groups[key] = { cwd: s.cwd, sessions: [], max: 0 }; order.push(key); }\\n\\t\\tgroups[key].sessions.push(s);\\n\\t\\tif ((s.updatedAt || 0) > groups[key].max) { groups[key].max = s.updatedAt || 0; }\\n\\t});\\n\\torder.sort(function(a, b) { return groups[b].max - groups[a].max; });\\n\\n\\tfunction btnStyle() {\\n\\t\\treturn \\\"display:block;width:100%;text-align:left;padding:8px 10px;margin:4px 0;border:1px solid #ddd;border-radius:6px;background:#f7f7f7;cursor:pointer;font-size:13px;\\\";\\n\\t}\\n\\tfunction primaryBtnStyle() {\\n\\t\\treturn \\\"display:block;width:100%;text-align:center;padding:7px 10px;margin:6px 0 0;border:1px solid #4a90d9;border-radius:6px;background:#4a90d9;color:#fff;cursor:pointer;font-size:13px;\\\";\\n\\t}\\n\\tfunction smallBtnStyle() {\\n\\t\\treturn \\\"margin:0 0 6px;padding:4px 10px;border:1px solid #ccc;border-radius:6px;background:#fff;cursor:pointer;font-size:12px;color:#555;\\\";\\n\\t}\\n\\n\\tfunction shortSessionLabel(s) {\\n\\t\\tvar id = s.sessionId || \\\"\\\";\\n\\t\\tvar parts = id.split(\\\"-\\\");\\n\\t\\tvar short = (parts.length > 1 ? parts[parts.length - 1] : id).slice(-10);\\n\\t\\tvar when = \\\"\\\";\\n\\t\\tif (s.updatedAt) {\\n\\t\\t\\tvar diff = Date.now() - s.updatedAt;\\n\\t\\t\\tif (diff < 60000) { when = \\\"刚刚\\\"; }\\n\\t\\t\\telse if (diff < 3600000) { when = Math.floor(diff / 60000) + \\\" 分钟前\\\"; }\\n\\t\\t\\telse if (diff < 86400000) { when = Math.floor(diff / 3600000) + \\\" 小时前\\\"; }\\n\\t\\t\\telse {\\n\\t\\t\\t\\tvar d = new Date(s.updatedAt);\\n\\t\\t\\t\\twhen = (d.getMonth() + 1) + \\\"-\\\" + d.getDate() + \\\" \\\" + (\\\"0\\\" + d.getHours()).slice(-2) + \\\":\\\" + (\\\"0\\\" + d.getMinutes()).slice(-2);\\n\\t\\t\\t}\\n\\t\\t}\\n\\t\\treturn \\\"#\\\" + short + (when ? \\\" · \\\" + when : \\\"\\\") + (s.running ? \\\" · ●运行中\\\" : \\\"\\\") + (s.blank ? \\\" · (空)\\\" : \\\"\\\");\\n\\t}\\n\\n\\tfunction modeName(id) {\\n\\t\\tif (!id) { return \\\"\\\"; }\\n\\t\\tfor (var i = 0; i < (modes || []).length; i++) {\\n\\t\\t\\tif (modes[i].id === id) { return modes[i].name || modes[i].id; }\\n\\t\\t}\\n\\t\\treturn id;\\n\\t}\\n\\n\\tfunction render() {\\n\\t\\tbody.innerHTML = \\\"\\\";\\n\\t\\tif (state.workspace === null) { renderWorkspaces(); } else { renderSessions(state.workspace); }\\n\\t}\\n\\n\\tfunction renderWorkspaces() {\\n\\t\\tif (order.length === 0) {\\n\\t\\t\\tvar none = document.createElement(\\\"p\\\");\\n\\t\\t\\tnone.textContent = \\\"还没有任何会话——在下方新建一个吧\\\";\\n\\t\\t\\tnone.setAttribute(\\\"style\\\", \\\"color:#999;font-size:13px;margin:0 0 8px;\\\");\\n\\t\\t\\tbody.appendChild(none);\\n\\t\\t} else {\\n\\t\\t\\torder.forEach(function(key) {\\n\\t\\t\\t\\tvar g = groups[key];\\n\\t\\t\\t\\tvar label = g.cwd || \\\"(默认工作区)\\\";\\n\\t\\t\\t\\tvar b = document.createElement(\\\"button\\\");\\n\\t\\t\\t\\tb.type = \\\"button\\\";\\n\\t\\t\\t\\tb.setAttribute(\\\"style\\\", btnStyle());\\n\\t\\t\\t\\tb.textContent = \\\"📁 \\\" + label + \\\" · \\\" + g.sessions.length + \\\" 个会话\\\";\\n\\t\\t\\t\\tb.title = g.cwd || \\\"默认工作区\\\";\\n\\t\\t\\t\\tb.addEventListener(\\\"click\\\", function() { state.workspace = key; render(); });\\n\\t\\t\\t\\tbody.appendChild(b);\\n\\t\\t\\t});\\n\\t\\t}\\n\\t\\t// new workspace / session\\n\\t\\tvar newRow = document.createElement(\\\"div\\\");\\n\\t\\tnewRow.setAttribute(\\\"style\\\", \\\"margin-top:10px;border-top:1px solid #eee;padding-top:8px;\\\");\\n\\t\\tvar lbl = document.createElement(\\\"p\\\");\\n\\t\\tlbl.textContent = \\\"新建工作区 / 会话\\\";\\n\\t\\tlbl.setAttribute(\\\"style\\\", \\\"margin:0 0 6px;font-size:12px;color:#666;\\\");\\n\\t\\tnewRow.appendChild(lbl);\\n\\t\\tvar input = document.createElement(\\\"input\\\");\\n\\t\\tinput.type = \\\"text\\\";\\n\\t\\tinput.placeholder = \\\"输入工作区路径,留空为默认工作区\\\";\\n\\t\\tinput.setAttribute(\\\"style\\\", \\\"width:100%;box-sizing:border-box;padding:6px 8px;border:1px solid #ccc;border-radius:6px;font-size:13px;\\\");\\n\\t\\tnewRow.appendChild(input);\\n\\t\\tvar go = document.createElement(\\\"button\\\");\\n\\t\\tgo.type = \\\"button\\\";\\n\\t\\tgo.textContent = \\\"创建并发送\\\";\\n\\t\\tgo.setAttribute(\\\"style\\\", primaryBtnStyle());\\n\\t\\tgo.addEventListener(\\\"click\\\", function() { createAndSend(payload, input.value.trim()); });\\n\\t\\tnewRow.appendChild(go);\\n\\t\\tinput.addEventListener(\\\"keydown\\\", function(e) { if (e.key === \\\"Enter\\\") { createAndSend(payload, input.value.trim()); } });\\n\\t\\tbody.appendChild(newRow);\\n\\t}\\n\\n\\tfunction renderSessions(key) {\\n\\t\\tvar g = groups[key];\\n\\t\\tvar back = document.createElement(\\\"button\\\");\\n\\t\\tback.type = \\\"button\\\";\\n\\t\\tback.textContent = \\\"← 返回工作区列表\\\";\\n\\t\\tback.setAttribute(\\\"style\\\", smallBtnStyle());\\n\\t\\tback.addEventListener(\\\"click\\\", function() { state.workspace = null; render(); });\\n\\t\\tbody.appendChild(back);\\n\\n\\t\\tvar wsName = document.createElement(\\\"p\\\");\\n\\t\\twsName.textContent = g.cwd ? \\\"📁 \\\" + g.cwd : \\\"(默认工作区)\\\";\\n\\t\\twsName.setAttribute(\\\"style\\\", \\\"margin:0 0 6px;font-size:13px;font-weight:600;word-break:break-all;\\\");\\n\\t\\tbody.appendChild(wsName);\\n\\n\\t\\t(g.sessions || []).forEach(function(s) {\\n\\t\\t\\tvar b = document.createElement(\\\"button\\\");\\n\\t\\t\\tb.type = \\\"button\\\";\\n\\t\\t\\tb.setAttribute(\\\"style\\\", btnStyle());\\n\\t\\t\\tvar label = shortSessionLabel(s);\\n\\t\\t\\tif (s.agentPreset) { label += \\\" · 🧭 \\\" + modeName(s.agentPreset); }\\n\\t\\t\\tb.textContent = label;\\n\\t\\t\\tb.title = \\\"会话 \\\" + s.sessionId + (s.agentPreset ? \\\" · 工作模式 \\\" + modeName(s.agentPreset) : \\\"\\\");\\n\\t\\t\\tb.addEventListener(\\\"click\\\", function() { closeOverlay(overlay, escHandler); doSend(payload, s.sessionId, state.note); });\\n\\t\\t\\tbody.appendChild(b);\\n\\t\\t});\\n\\n\\t\\tvar newBtn = document.createElement(\\\"button\\\");\\n\\t\\tnewBtn.type = \\\"button\\\";\\n\\t\\tnewBtn.textContent = \\\"➕ 在此工作区新建会话并发送\\\";\\n\\t\\tnewBtn.setAttribute(\\\"style\\\", primaryBtnStyle());\\n\\t\\tnewBtn.addEventListener(\\\"click\\\", function() { createAndSend(payload, g.cwd); });\\n\\t\\tbody.appendChild(newBtn);\\n\\t}\\n\\n\\tfunction createAndSend(payload2, cwd) {\\n\\t\\tvar config = readConfig();\\n\\t\\tvar headers = { \\\"Content-Type\\\": \\\"application/json\\\" };\\n\\t\\tif (config.token.length > 0) { headers[\\\"x-send-to-agent-token\\\"] = config.token; }\\n\\t\\tvar body = { cwd: cwd || \\\"\\\" };\\n\\t\\tif (state.mode && String(state.mode).length > 0) { body.mode = state.mode; }\\n\\t\\tif (state.permission && String(state.permission).length > 0) { body.permission = state.permission; }\\n\\t\\t$tw.utils.httpRequest({\\n\\t\\t\\turl: baseEndpoint() + \\\"/agent/create\\\",\\n\\t\\t\\ttype: \\\"POST\\\",\\n\\t\\t\\theaders: headers,\\n\\t\\t\\tdata: JSON.stringify(body),\\n\\t\\t\\tcallback: function(err, data) {\\n\\t\\t\\t\\tvar parsed = null;\\n\\t\\t\\t\\ttry { parsed = JSON.parse(data || \\\"\\\"); } catch (e) {}\\n\\t\\t\\t\\tif (err) { notify(\\\"新建会话失败:\\\" + err); return; }\\n\\t\\t\\t\\tif (parsed && parsed.ok && parsed.sessionId) {\\n\\t\\t\\t\\t\\tcloseOverlay(overlay, escHandler);\\n\\t\\t\\t\\t\\tvar modeNote = parsed.mode ? \\\"(模式 \\\" + modeName(parsed.mode) + \\\")\\\" : \\\"\\\";\\n\\t\\t\\t\\t\\tvar permNote = parsed.permissionApplied ? \\\"(权限 \\\" + (parsed.permission || \\\"\\\") + \\\")\\\" : \\\"\\\";\\n\\t\\t\\t\\t\\tnotify(\\\"已创建会话并发送 ✓ \\\" + parsed.sessionId.slice(0, 8) + modeNote + permNote);\\n\\t\\t\\t\\t\\tdoSend(payload2, parsed.sessionId, state.note);\\n\\t\\t\\t\\t} else {\\n\\t\\t\\t\\t\\tnotify(\\\"新建会话失败:\\\" + ((parsed && parsed.error) || data || \\\"未知错误\\\"));\\n\\t\\t\\t\\t}\\n\\t\\t\\t}\\n\\t\\t});\\n\\t}\\n\\n\\trender();\\n}\\n\\nfunction handleSend(title) {\\n\\tif (!title) { notify(\\\"无法确定当前笔记标题\\\"); return; }\\n\\tvar tiddler = $tw.wiki.getTiddler(title);\\n\\tif (!tiddler) { notify(\\\"找不到笔记:\\\" + title); return; }\\n\\tvar config = readConfig();\\n\\tif (!config.enabled) { notify(\\\"「发送给 Agent」特性未启用(可在 TW 配置中打开)\\\"); return; }\\n\\tvar payload = {\\n\\t\\ttitle: title,\\n\\t\\ttext: tiddler.fields.text || \\\"\\\",\\n\\t\\ttags: tiddler.fields.tags || [],\\n\\t\\ttype: tiddler.fields.type || \\\"\\\"\\n\\t};\\n\\tvar headers = {};\\n\\tif (config.token.length > 0) { headers[\\\"x-send-to-agent-token\\\"] = config.token; }\\n\\t$tw.utils.httpRequest({\\n\\t\\turl: baseEndpoint() + \\\"/agent/sessions\\\",\\n\\t\\ttype: \\\"GET\\\",\\n\\t\\theaders: headers,\\n\\t\\tcallback: function(err, data) {\\n\\t\\t\\tvar parsed = null;\\n\\t\\t\\ttry { parsed = JSON.parse(data || \\\"\\\"); } catch (e) {}\\n\\t\\t\\tif (err) { notify(\\\"获取会话列表失败:\\\" + err); return; }\\n\\t\\t\\tif (!parsed || !parsed.ok || !parsed.items) { notify(\\\"获取会话列表失败:\\\" + ((parsed && parsed.error) || \\\"未知错误\\\")); return; }\\n\\t\\t\\t// fetch the available 工作模式 (Agent presets) alongside; degrade to\\n\\t\\t\\t// an empty roster (default mode) when the endpoint is missing.\\n\\t\\t\\t$tw.utils.httpRequest({\\n\\t\\t\\t\\turl: baseEndpoint() + \\\"/agent/modes\\\",\\n\\t\\t\\t\\ttype: \\\"GET\\\",\\n\\t\\t\\t\\theaders: headers,\\n\\t\\t\\t\\tcallback: function(err2, data2) {\\n\\t\\t\\t\\t\\tvar modes = [];\\n\\t\\t\\t\\t\\tvar defaultId = \\\"\\\";\\n\\t\\t\\t\\t\\tvar permissions = null;\\n\\t\\t\\t\\t\\tvar parsed2 = null;\\n\\t\\t\\t\\t\\ttry { parsed2 = JSON.parse(data2 || \\\"\\\"); } catch (e) {}\\n\\t\\t\\t\\t\\tif (!err2 && parsed2 && parsed2.ok) {\\n\\t\\t\\t\\t\\t\\tif (Array.isArray(parsed2.items)) {\\n\\t\\t\\t\\t\\t\\t\\tmodes = parsed2.items;\\n\\t\\t\\t\\t\\t\\t\\tdefaultId = parsed2.defaultId || \\\"\\\";\\n\\t\\t\\t\\t\\t\\t}\\n\\t\\t\\t\\t\\t\\tif (parsed2.permissions && Array.isArray(parsed2.permissions.items)) {\\n\\t\\t\\t\\t\\t\\t\\tpermissions = parsed2.permissions;\\n\\t\\t\\t\\t\\t\\t}\\n\\t\\t\\t\\t\\t}\\n\\t\\t\\t\\t\\tshowPicker(payload, parsed.items, modes, defaultId, permissions);\\n\\t\\t\\t\\t}\\n\\t\\t\\t});\\n\\t\\t}\\n\\t});\\n}\\n\\nexports.startup = function() {\\n\\tconsole.log(\\\"[dsh-send-to-agent] startup ran\\\");\\n\\tif (!$tw.rootWidget || typeof $tw.rootWidget.addEventListener !== \\\"function\\\") { return; }\\n\\t$tw.rootWidget.addEventListener(\\\"dsh-send-to-agent\\\", function(event) {\\n\\t\\thandleSend(event.param);\\n\\t});\\n};\\n\\n})();\\n\"\n },\n \"$:/plugins/dsh/send-to-agent/ui/icon\": {\n \"title\": \"$:/plugins/dsh/send-to-agent/ui/icon\",\n \"tags\": [\n \"$:/tags/Image\"\n ],\n \"text\": \"\\\\parameters (size:\\\"22pt\\\")\\n<svg width=<<size>> height=<<size>> class=\\\"tc-image-send-to-agent tc-image-button\\\" viewBox=\\\"0 0 24 24\\\"><path d=\\\"M3.478 2.404a.75.75 0 0 0-.926.941l2.432 7.905H13.5a.75.75 0 0 1 0 1.5H4.984l-2.432 7.905a.75.75 0 0 0 .926.94 60.519 60.519 0 0 0 18.445-8.986.75.75 0 0 0 0-1.218A60.517 60.517 0 0 0 3.478 2.404Z\\\"/></svg>\\n\"\n },\n \"$:/plugins/dsh/send-to-agent/ui/ViewToolbar/SendToAgent\": {\n \"title\": \"$:/plugins/dsh/send-to-agent/ui/ViewToolbar/SendToAgent\",\n \"type\": \"text/vnd.tiddlywiki\",\n \"tags\": [\n \"$:/tags/ViewToolbar\"\n ],\n \"icon\": \"$:/plugins/dsh/send-to-agent/ui/icon\",\n \"caption\": \"发送给 Agent\",\n \"description\": \"把当前笔记一键发送给 DSH Agent(TiddlyWiki → DSH 会话注入)\",\n \"text\": \"\\\\whitespace trim\\n<$button message=\\\"dsh-send-to-agent\\\"\\n\\tparam=<<currentTiddler>>\\n\\ttooltip=\\\"把当前笔记一键发送给 DSH Agent\\\"\\n\\taria-label=\\\"把当前笔记一键发送给 DSH Agent\\\"\\n\\tclass=<<tv-config-toolbar-class>>\\n>\\n\\t<%if [<tv-config-toolbar-icons>match[yes]] %>\\n\\t\\t{{$:/plugins/dsh/send-to-agent/ui/icon}}\\n\\t<%endif%>\\n\\t<%if [<tv-config-toolbar-text>match[yes]] %>\\n\\t\\t<span class=\\\"tc-btn-text\\\">\\n\\t\\t<$text text=\\\"发送给 Agent\\\"/>\\n\\t</span>\\n\\t<%endif%>\\n</$button>\\n\"\n },\n \"$:/core/ui/ControlPanel/Toolbars/ItemTemplate\": {\n \"title\": \"$:/core/ui/ControlPanel/Toolbars/ItemTemplate\",\n \"type\": \"text/vnd.tiddlywiki\",\n \"text\": \"\\\\define config-title()\\n$(config-base)$$(currentTiddler)$\\n\\\\end\\n\\\\whitespace trim\\n\\n<$draggable tiddler=<<currentTiddler>>>\\n<$checkbox tiddler=<<config-title>> field=\\\"text\\\" checked=\\\"show\\\" unchecked=\\\"hide\\\" default=\\\"show\\\"/>\\n \\n<span class=\\\"tc-icon-wrapper\\\"><$transclude tiddler={{!!icon}}/></span>\\n \\n<$transclude field=\\\"caption\\\"/>\\n -- \\n<i class=\\\"tc-muted\\\"><$transclude field=\\\"description\\\"/></i>\\n</$draggable>\\n\"\n }\n }\n}"
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Seed the "发送给 Agent" TW button exactly once per wiki (mirrors the doc-note
|
|
23
|
+
* one-shot policy). The marker records the offer; afterwards the bundle is
|
|
24
|
+
* user-owned — deleting it and restarting dsh web does NOT recreate it, and
|
|
25
|
+
* edits are never overwritten. With `opts.force` the bundle is (re)written even
|
|
26
|
+
* when it already exists and the marker is (re)written — the settings page uses
|
|
27
|
+
* this for "重新初始化". Returns whether a bundle was written this call.
|
|
28
|
+
* Never throws.
|
|
29
|
+
*/
|
|
30
|
+
export async function seedSendToAgent(client: TiddlyWebClient, opts?: { force?: boolean }): Promise<boolean> {
|
|
31
|
+
const force = opts?.force === true
|
|
32
|
+
if (!force) {
|
|
33
|
+
const marker = await client.get(SEND_TO_AGENT_MARKER_TITLE).catch(() => undefined)
|
|
34
|
+
if (marker !== undefined) return false
|
|
35
|
+
}
|
|
36
|
+
const existing = await client.get(SEND_TO_AGENT_PLUGIN_TITLE).catch(() => undefined)
|
|
37
|
+
let wrote = false
|
|
38
|
+
if (force || existing === undefined) {
|
|
39
|
+
await client.put({
|
|
40
|
+
title: SEND_TO_AGENT_PLUGIN_TITLE,
|
|
41
|
+
text: SEND_TO_AGENT_BUNDLE_TEXT,
|
|
42
|
+
type: 'application/json',
|
|
43
|
+
tags: [],
|
|
44
|
+
// TW only REGISTERS a wiki tiddler as a plugin (boot.js
|
|
45
|
+
// registerPluginTiddlers) when the OUTER tiddler carries a
|
|
46
|
+
// `plugin-type` field — without it the bundle is never unpacked and its
|
|
47
|
+
// startup module never runs in the embedded TW. Mirrors what the live
|
|
48
|
+
// wiki's send-to-agent bundle tiddler carries.
|
|
49
|
+
'plugin-type': 'plugin',
|
|
50
|
+
name: 'Send to Agent',
|
|
51
|
+
author: 'dsh-tiddlywiki',
|
|
52
|
+
version: '0.3.2',
|
|
53
|
+
description: '把当前笔记一键发送给 DSH Agent(TiddlyWiki → DSH 会话注入)',
|
|
54
|
+
})
|
|
55
|
+
wrote = true
|
|
56
|
+
}
|
|
57
|
+
// Record the offer regardless, so an existing bundle (upgrade from a
|
|
58
|
+
// pre-seed wiki) also becomes user-owned from here on.
|
|
59
|
+
await client
|
|
60
|
+
.put({ title: SEND_TO_AGENT_MARKER_TITLE, text: 'seeded-once', type: 'text/plain', tags: [] })
|
|
61
|
+
.catch(() => undefined)
|
|
62
|
+
return wrote
|
|
63
|
+
}
|
package/src/host/tools.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* @module dsh-tiddlywiki/host/tools
|
|
16
16
|
*/
|
|
17
17
|
import { defineTool } from '../sdk.ts'
|
|
18
|
+
import { isBinaryType } from './tw-api.ts'
|
|
18
19
|
import type { TiddlyWebClient, Tiddler } from './tw-api.ts'
|
|
19
20
|
import type { GitFace } from './git.ts'
|
|
20
21
|
|
|
@@ -148,7 +149,7 @@ export function registerTiddlywikiTools(ctx: ToolsCtx, deps: ToolsDeps): Array<(
|
|
|
148
149
|
// ── tiddlywiki_search ────────────────────────────────────────────────────
|
|
149
150
|
register(defineTool({
|
|
150
151
|
name: 'tiddlywiki_search',
|
|
151
|
-
description: '检索 TiddlyWiki 持久知识库:按关键词(可选 tags 数组 / since 修改时间 / type / limit)搜索非系统 tiddler
|
|
152
|
+
description: '检索 TiddlyWiki 持久知识库:按关键词(可选 tags 数组 / since 修改时间 / type / limit)搜索非系统 tiddler,返回标题、标签、修改时间与摘要片段。二进制 tiddler(图片等附件,正文为 base64)不参与检索。',
|
|
152
153
|
parameters: {
|
|
153
154
|
query: { type: 'string', description: '搜索关键词(大小写不敏感,子串匹配)', required: true },
|
|
154
155
|
tags: { type: 'array', items: { type: 'string' }, description: '可选:要求同时包含的标签(AND)' },
|
|
@@ -200,7 +201,7 @@ export function registerTiddlywikiTools(ctx: ToolsCtx, deps: ToolsDeps): Array<(
|
|
|
200
201
|
// ── tiddlywiki_recent ────────────────────────────────────────────────────
|
|
201
202
|
register(defineTool({
|
|
202
203
|
name: 'tiddlywiki_recent',
|
|
203
|
-
description: '查看 TiddlyWiki 知识库最近修改的笔记(按修改时间倒序,排除系统 tiddler
|
|
204
|
+
description: '查看 TiddlyWiki 知识库最近修改的笔记(按修改时间倒序,排除系统 tiddler 与图片等二进制附件),返回标题、标签、修改时间与摘要。适合开工时快速了解近期动态。',
|
|
204
205
|
parameters: {
|
|
205
206
|
limit: { type: 'integer', description: '可选:返回条数(默认 15,最大 200)' },
|
|
206
207
|
since: { type: 'string', description: '可选:只返回修改时间不早于该 ISO 时间的 tiddler' },
|
|
@@ -252,7 +253,7 @@ export function registerTiddlywikiTools(ctx: ToolsCtx, deps: ToolsDeps): Array<(
|
|
|
252
253
|
// ── tiddlywiki_get ───────────────────────────────────────────────────────
|
|
253
254
|
register(defineTool({
|
|
254
255
|
name: 'tiddlywiki_get',
|
|
255
|
-
description: '读取一个 TiddlyWiki tiddler
|
|
256
|
+
description: '读取一个 TiddlyWiki tiddler 的完整内容(标题、全文、标签、自定义字段)。二进制 tiddler(图片等附件)只返回元数据,不返回 base64 正文。',
|
|
256
257
|
parameters: {
|
|
257
258
|
title: { type: 'string', description: 'tiddler 标题(精确匹配)', required: true },
|
|
258
259
|
},
|
|
@@ -260,6 +261,15 @@ export function registerTiddlywikiTools(ctx: ToolsCtx, deps: ToolsDeps): Array<(
|
|
|
260
261
|
schema: { type: 'json' },
|
|
261
262
|
render: (_args, value: GetResult) => {
|
|
262
263
|
if (value.notFound) return [{ type: 'text', text: `tiddler「${value.title}」不存在。可用 tiddlywiki_search 检索,或用 tiddlywiki_put 新建。` }]
|
|
264
|
+
if (value.binary === true) {
|
|
265
|
+
const lines = [`tiddler「${value.title}」是二进制附件(type=${value.binaryType ?? '?'},base64 正文约 ${value.binaryChars ?? 0} 字符),不返回正文。`]
|
|
266
|
+
if (value.tags.length > 0) lines.push(`标签: ${value.tags.join(', ')}`)
|
|
267
|
+
if (value.modified !== null) lines.push(`修改: ${value.modified}`)
|
|
268
|
+
const fields = Object.entries(value.fields)
|
|
269
|
+
if (fields.length > 0) lines.push(`字段: ${fields.map(([k, v]) => `${k}=${String(v)}`).join(', ')}`)
|
|
270
|
+
lines.push(`如需查看附件本身,可打开 [${value.title}](/dsh-tiddlywiki/tw/#${encodeURIComponent(value.title)})。`)
|
|
271
|
+
return [{ type: 'text', text: lines.join('\n') }]
|
|
272
|
+
}
|
|
263
273
|
const lines = [`tiddler「${value.title}」`]
|
|
264
274
|
if (value.tags.length > 0) lines.push(`标签: ${value.tags.join(', ')}`)
|
|
265
275
|
if (value.modified !== null) lines.push(`修改: ${value.modified}`)
|
|
@@ -274,7 +284,21 @@ export function registerTiddlywikiTools(ctx: ToolsCtx, deps: ToolsDeps): Array<(
|
|
|
274
284
|
const wiki = requireWiki()
|
|
275
285
|
const t = await wiki.get(args.title)
|
|
276
286
|
if (t === undefined) return { notFound: true, title: args.title, text: '', tags: [], fields: {}, modified: null }
|
|
277
|
-
|
|
287
|
+
const binary = isBinaryType(typeof t.type === 'string' ? t.type : undefined)
|
|
288
|
+
const result: GetResult = {
|
|
289
|
+
notFound: false,
|
|
290
|
+
title: t.title,
|
|
291
|
+
text: binary ? '' : (t.text ?? ''),
|
|
292
|
+
tags: t.tags ?? [],
|
|
293
|
+
fields: pickFields(t),
|
|
294
|
+
modified: typeof t.modified === 'string' ? t.modified : null,
|
|
295
|
+
}
|
|
296
|
+
if (binary) {
|
|
297
|
+
result.binary = true
|
|
298
|
+
result.binaryType = typeof t.type === 'string' ? t.type : undefined
|
|
299
|
+
result.binaryChars = (t.text ?? '').length
|
|
300
|
+
}
|
|
301
|
+
return result
|
|
278
302
|
},
|
|
279
303
|
}))
|
|
280
304
|
|
|
@@ -577,7 +601,18 @@ interface SearchHit { title: string; tags: string[]; modified: string | null; sn
|
|
|
577
601
|
interface SearchResult { query: string; tags: string[]; since: string | null; type: string | null; total: number; results: SearchHit[] }
|
|
578
602
|
interface RecentResult { since: string | null; results: SearchHit[] }
|
|
579
603
|
interface TagListResult { count: number; tags: Array<{ tag: string; count: number }> }
|
|
580
|
-
interface GetResult {
|
|
604
|
+
interface GetResult {
|
|
605
|
+
notFound: boolean
|
|
606
|
+
title: string
|
|
607
|
+
text: string
|
|
608
|
+
tags: string[]
|
|
609
|
+
fields: Record<string, unknown>
|
|
610
|
+
modified: string | null
|
|
611
|
+
/** Binary attachment (image/audio/…): `text` is withheld, these carry its type+size. */
|
|
612
|
+
binary?: boolean
|
|
613
|
+
binaryType?: string
|
|
614
|
+
binaryChars?: number
|
|
615
|
+
}
|
|
581
616
|
interface PutResult { ok: boolean; title: string; tags: string[]; type: string | null; typeDefaulted?: boolean; fields: Record<string, unknown> | null }
|
|
582
617
|
interface BatchResult { ok: boolean; written: number; skipped: number; items: Array<{ title: string; written: boolean; skipped: boolean }> }
|
|
583
618
|
interface RenameResult { ok: boolean; from: string; to: string; refsUpdated: number; refsTiddlers: number; warning?: string }
|
package/src/host/tw-api.ts
CHANGED
|
@@ -15,8 +15,10 @@
|
|
|
15
15
|
*
|
|
16
16
|
* SEARCH (R2): the server blocks arbitrary `filter=` queries with 403 unless
|
|
17
17
|
* the exact filter is whitelisted in $:/config/Server/ExternalFilters. So
|
|
18
|
-
* `search()` fetches the
|
|
19
|
-
*
|
|
18
|
+
* `search()` fetches the TEXT-BEARING listing WITH text (TEXT_LIST_FILTER +
|
|
19
|
+
* `?exclude=` a sentinel, whitelist self-healed on first 403) and matches
|
|
20
|
+
* locally — one request, no 403, no per-tiddler round-trips, and binary
|
|
21
|
+
* tiddlers (images etc.) never ship their base64 payload (v0.16.20).
|
|
20
22
|
*
|
|
21
23
|
* @module dsh-tiddlywiki/host/tw-api
|
|
22
24
|
*/
|
|
@@ -42,6 +44,61 @@ const CSRF_HEADER = { 'x-requested-with': 'TiddlyWiki' }
|
|
|
42
44
|
/** Sentinel `exclude` value: excludes nothing, so `text` stays in the list. */
|
|
43
45
|
const LIST_WITH_TEXT_EXCLUDE = '__dsh_tw_none__'
|
|
44
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Recipe-list filter that keeps ONLY text-bearing tiddlers (notes): no `type`
|
|
49
|
+
* field at all, or a `text/*` type. Binary tiddlers (images/audio/video/fonts/
|
|
50
|
+
* PDF/zip…) carry their payload as a base64 `text` field — on a big wiki (e.g.
|
|
51
|
+
* thousands of scanned book pages) the full listing serializes 500+MB of base64
|
|
52
|
+
* and times every agent read out. This filter drops them SERVER-SIDE so the
|
|
53
|
+
* listing stays ~6MB (v0.16.20, verified on a 2418-tiddler wiki:
|
|
54
|
+
* 515MB/17s → 6MB/0.3s).
|
|
55
|
+
*
|
|
56
|
+
* TW 5.4.1 filter facts that matter here (all verified empirically):
|
|
57
|
+
* - `prefix`/`match` operators match the TITLE only — field matching needs
|
|
58
|
+
* `regexp:<field>` / `field:<field>`.
|
|
59
|
+
* - `[has[type]]` = "has a non-empty type field"; `[has:type[]]` is a DIFFERENT
|
|
60
|
+
* call (suffix "type" + empty operand) that matches everything.
|
|
61
|
+
* - space-separated operations UNION (no prefix = "or"); `+` means
|
|
62
|
+
* intersection.
|
|
63
|
+
* - negated `regexp:type`/`field:type` DROP type-less tiddlers (their field
|
|
64
|
+
* string is null), so "exclude binary" must be written as the positive
|
|
65
|
+
* "no type OR text-ish" union below.
|
|
66
|
+
*
|
|
67
|
+
* FILENAME BUDGET (Windows, verified): the whitelist tiddler below is stored
|
|
68
|
+
* as a .tid file whose name contains the ENTIRE filter string, so the filter
|
|
69
|
+
* must stay short — a ~180-char filter produced a ~217-char filename that
|
|
70
|
+
* broke `git add` on Windows ("Filename too long", MAX_PATH) and killed the
|
|
71
|
+
* auto-committer. 86 chars keeps the file at ~123 chars even on a deep wiki
|
|
72
|
+
* path. (Consequence: application/json tiddlers are not searchable — on this
|
|
73
|
+
* wiki there are none, and they are config/data tiddlers, not notes.)
|
|
74
|
+
*/
|
|
75
|
+
export const TEXT_LIST_FILTER = [
|
|
76
|
+
'[all[tiddlers]!is[system]!has[type]]',
|
|
77
|
+
'[all[tiddlers]!is[system]regexp:type[(?i)^text/]]',
|
|
78
|
+
].join(' ')
|
|
79
|
+
|
|
80
|
+
/** TiddlyWeb blocks every non-default filter with 403 unless the EXACT filter
|
|
81
|
+
* string is whitelisted at `$:/config/Server/ExternalFilters/<filter>` = "yes".
|
|
82
|
+
* This client writes that tiddler once on 403 (self-heal, idempotent) and
|
|
83
|
+
* retries; the tiddler lives in the wiki and travels with its git history. */
|
|
84
|
+
function externalFilterWhitelistTitle(filter: string): string {
|
|
85
|
+
return `$:/config/Server/ExternalFilters/${filter}`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Binary MIME type prefixes — their `text` field is base64 payload, not content. */
|
|
89
|
+
const BINARY_TYPE_PREFIXES = ['image/', 'audio/', 'video/', 'font/']
|
|
90
|
+
const BINARY_TYPE_EXACT = new Set([
|
|
91
|
+
'application/octet-stream', 'application/pdf', 'application/zip', 'application/gzip',
|
|
92
|
+
'application/x-gzip', 'application/x-7z-compressed', 'application/x-rar-compressed',
|
|
93
|
+
'application/epub+zip', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
94
|
+
])
|
|
95
|
+
|
|
96
|
+
/** True when a tiddler's `type` marks it as binary (base64 in `text`). */
|
|
97
|
+
export function isBinaryType(type: string | undefined): boolean {
|
|
98
|
+
if (typeof type !== 'string' || type.length === 0) return false
|
|
99
|
+
return BINARY_TYPE_PREFIXES.some((p) => type.startsWith(p)) || BINARY_TYPE_EXACT.has(type)
|
|
100
|
+
}
|
|
101
|
+
|
|
45
102
|
/** Split TW's whitespace-joined tags string into an array. */
|
|
46
103
|
function normalizeTags(tags: unknown): string[] | undefined {
|
|
47
104
|
if (tags === undefined) return undefined
|
|
@@ -115,32 +172,65 @@ export class TiddlyWebClient {
|
|
|
115
172
|
* List tiddlers via the default server filter. Arbitrary `filter=` queries
|
|
116
173
|
* are blocked by the server (403) unless whitelisted, so callers needing a
|
|
117
174
|
* subset should use search(); a supplied filter that is 403-blocked falls
|
|
118
|
-
* back to the
|
|
175
|
+
* back to the text-bearing filter.
|
|
176
|
+
*
|
|
177
|
+
* `includeText=true` returns only TEXT-BEARING tiddlers WITH their `text`
|
|
178
|
+
* (never the base64 payloads of binary tiddlers): on 403 the whitelist
|
|
179
|
+
* tiddler for TEXT_LIST_FILTER is written once and the request retried
|
|
180
|
+
* (self-heal); if that write fails (read-only wiki) it degrades to the
|
|
181
|
+
* skinny listing (title/tags matchable, empty snippets).
|
|
119
182
|
*/
|
|
120
183
|
async list(filter?: string, includeText = false): Promise<Tiddler[]> {
|
|
184
|
+
if (!includeText) return this.fetchListing(filter)
|
|
185
|
+
const explicit = filter !== undefined && filter.length > 0
|
|
186
|
+
const target = explicit ? (filter as string) : TEXT_LIST_FILTER
|
|
187
|
+
let res = await this.requestListWithText(target)
|
|
188
|
+
if (res.status === 403) {
|
|
189
|
+
await this.ensureExternalFilterWhitelist(TEXT_LIST_FILTER).catch(() => undefined)
|
|
190
|
+
res = await this.requestListWithText(TEXT_LIST_FILTER)
|
|
191
|
+
}
|
|
192
|
+
if (res.status === 403) return this.fetchListing(undefined)
|
|
193
|
+
if (!res.ok) throw new Error(`TiddlyWeb recipe list HTTP ${res.status}`)
|
|
194
|
+
return this.parseList(res)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** GET the recipe listing WITHOUT text payloads (server default excludes `text`). */
|
|
198
|
+
private async fetchListing(filter?: string): Promise<Tiddler[]> {
|
|
121
199
|
const params = new URLSearchParams()
|
|
122
|
-
if (includeText) params.set('exclude', LIST_WITH_TEXT_EXCLUDE)
|
|
123
200
|
if (filter !== undefined && filter.length > 0) params.set('filter', filter)
|
|
124
201
|
const query = params.toString()
|
|
125
202
|
let res = await this.request(`/recipes/default/tiddlers.json${query.length > 0 ? `?${query}` : ''}`)
|
|
126
203
|
if (!res.ok && res.status === 403 && filter !== undefined && filter.length > 0) {
|
|
127
|
-
// Filter not whitelisted → refetch with the default filter.
|
|
128
|
-
|
|
129
|
-
if (includeText) retry.set('exclude', LIST_WITH_TEXT_EXCLUDE)
|
|
130
|
-
const retryQuery = retry.toString()
|
|
131
|
-
res = await this.request(`/recipes/default/tiddlers.json${retryQuery.length > 0 ? `?${retryQuery}` : ''}`)
|
|
204
|
+
// Filter not whitelisted → refetch with the default (whitelisted) filter.
|
|
205
|
+
res = await this.request('/recipes/default/tiddlers.json')
|
|
132
206
|
}
|
|
133
207
|
if (!res.ok) throw new Error(`TiddlyWeb recipe list HTTP ${res.status}`)
|
|
208
|
+
return this.parseList(res)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** GET the recipe listing WITH text for a specific (whitelisted) filter. */
|
|
212
|
+
private async requestListWithText(filter: string): Promise<Response> {
|
|
213
|
+
return this.request(`/recipes/default/tiddlers.json?filter=${encodeURIComponent(filter)}&exclude=${LIST_WITH_TEXT_EXCLUDE}`)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Write `$:/config/Server/ExternalFilters/<filter>` = "yes" (idempotent). */
|
|
217
|
+
private async ensureExternalFilterWhitelist(filter: string): Promise<void> {
|
|
218
|
+
await this.put({ title: externalFilterWhitelistTitle(filter), text: 'yes' })
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private async parseList(res: Response): Promise<Tiddler[]> {
|
|
134
222
|
const data = (await res.json()) as Array<Record<string, unknown>> | { tiddlers?: Array<Record<string, unknown>> }
|
|
135
223
|
const items = Array.isArray(data) ? data : (data.tiddlers ?? [])
|
|
136
224
|
return items.map(normalizeTiddler)
|
|
137
225
|
}
|
|
138
226
|
|
|
139
227
|
/**
|
|
140
|
-
* Search
|
|
141
|
-
* local case-insensitive substring matching on title + text, optional
|
|
142
|
-
* tags (AND), a `since` modified-time floor, an exact `type`, capped at
|
|
143
|
-
* `limit`. Robust against the server's external-filter 403
|
|
228
|
+
* Search text-bearing tiddlers: one request (text-bearing listing with text)
|
|
229
|
+
* plus local case-insensitive substring matching on title + text, optional
|
|
230
|
+
* exact tags (AND), a `since` modified-time floor, an exact `type`, capped at
|
|
231
|
+
* `limit`. Robust against the server's external-filter 403 (whitelist is
|
|
232
|
+
* self-healed). Binary tiddlers (images/audio/…) are NOT in the listing, so
|
|
233
|
+
* they can never match — a deliberate flood guard on big wikis.
|
|
144
234
|
*/
|
|
145
235
|
async search(query: string, options: SearchOptions = {}): Promise<{ items: Tiddler[]; total: number }> {
|
|
146
236
|
const items = await this.list(undefined, true)
|
|
@@ -166,9 +256,11 @@ export class TiddlyWebClient {
|
|
|
166
256
|
}
|
|
167
257
|
|
|
168
258
|
/**
|
|
169
|
-
* List the most recently modified
|
|
170
|
-
* (missing/modified-less tiddlers sort to the tail).
|
|
171
|
-
* tiddlers modified at/after that instant.
|
|
259
|
+
* List the most recently modified TEXT-BEARING (non-system, non-binary)
|
|
260
|
+
* tiddlers, newest first (missing/modified-less tiddlers sort to the tail).
|
|
261
|
+
* `since` keeps only tiddlers modified at/after that instant. Binary
|
|
262
|
+
* attachments never appear — a book import touching hundreds of images must
|
|
263
|
+
* not crowd the "最近修改" list.
|
|
172
264
|
*/
|
|
173
265
|
async recent(limit = 15, since?: string): Promise<Tiddler[]> {
|
|
174
266
|
const items = await this.list(undefined, true)
|