@dsh-plugin/dsh-synapse 0.0.0-stage → 0.1.0-dev.41044228975

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.
Files changed (150) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE.md +18 -0
  3. package/README.md +136 -2
  4. package/cordis.patch.yml +3 -0
  5. package/lib/bot.cjs +8314 -0
  6. package/lib/bot.cjs.map +7 -0
  7. package/lib/build.json +1 -0
  8. package/lib/client.js +46 -0
  9. package/lib/host/agent.d.ts +21 -0
  10. package/lib/host/agent.js +151 -0
  11. package/lib/host/agent.js.map +1 -0
  12. package/lib/host/bot.d.ts +17 -0
  13. package/lib/host/bot.js +158 -0
  14. package/lib/host/bot.js.map +1 -0
  15. package/lib/host/client.d.ts +2 -0
  16. package/lib/host/client.js +19 -0
  17. package/lib/host/client.js.map +1 -0
  18. package/lib/host/config.d.ts +24 -0
  19. package/lib/host/config.js +192 -0
  20. package/lib/host/config.js.map +1 -0
  21. package/lib/host/files.d.ts +29 -0
  22. package/lib/host/files.js +188 -0
  23. package/lib/host/files.js.map +1 -0
  24. package/lib/host/gateway.d.ts +33 -0
  25. package/lib/host/gateway.js +237 -0
  26. package/lib/host/gateway.js.map +1 -0
  27. package/lib/host/github.d.ts +47 -0
  28. package/lib/host/github.js +219 -0
  29. package/lib/host/github.js.map +1 -0
  30. package/lib/host/http.d.ts +4 -0
  31. package/lib/host/http.js +41 -0
  32. package/lib/host/http.js.map +1 -0
  33. package/lib/host/index.d.ts +32 -0
  34. package/lib/host/index.js +192 -0
  35. package/lib/host/index.js.map +1 -0
  36. package/lib/host/inference-worker.d.ts +1 -0
  37. package/lib/host/inference-worker.js +72 -0
  38. package/lib/host/inference-worker.js.map +1 -0
  39. package/lib/host/mcp.d.ts +138 -0
  40. package/lib/host/mcp.js +104 -0
  41. package/lib/host/mcp.js.map +1 -0
  42. package/lib/host/memory.d.ts +26 -0
  43. package/lib/host/memory.js +155 -0
  44. package/lib/host/memory.js.map +1 -0
  45. package/lib/host/panel.d.ts +1 -0
  46. package/lib/host/panel.html +174 -0
  47. package/lib/host/panel.js +388 -0
  48. package/lib/host/panel.js.map +1 -0
  49. package/lib/host/runtime.d.ts +21 -0
  50. package/lib/host/runtime.js +145 -0
  51. package/lib/host/runtime.js.map +1 -0
  52. package/lib/host/types.d.ts +112 -0
  53. package/lib/host/types.js +2 -0
  54. package/lib/host/types.js.map +1 -0
  55. package/package.json +94 -4
  56. package/scripts/build.mjs +90 -0
  57. package/scripts/check.mjs +13 -0
  58. package/scripts/dump-npm-latest.mjs +30 -0
  59. package/scripts/model-smoke.mjs +25 -0
  60. package/src/agent.ts +203 -0
  61. package/src/bot.ts +173 -0
  62. package/src/client.ts +20 -0
  63. package/src/config.ts +217 -0
  64. package/src/files.ts +206 -0
  65. package/src/gateway.ts +276 -0
  66. package/src/github.ts +263 -0
  67. package/src/http.ts +41 -0
  68. package/src/index.ts +227 -0
  69. package/src/inference-worker.ts +89 -0
  70. package/src/mcp.ts +112 -0
  71. package/src/memory.ts +178 -0
  72. package/src/panel.html +174 -0
  73. package/src/panel.ts +440 -0
  74. package/src/runtime.ts +164 -0
  75. package/src/types.ts +106 -0
  76. package/test/core.test.mjs +279 -0
  77. package/test/fixtures/mcp-server.mjs +14 -0
  78. package/test/integration.test.mjs +253 -0
  79. package/test/panel.test.mjs +73 -0
  80. package/tsconfig.json +18 -0
  81. package/vendor/gugle-chat-bot/LICENSE +661 -0
  82. package/vendor/gugle-chat-bot/package.json +57 -0
  83. package/vendor/gugle-chat-bot/src/allow-rule.yml +9345 -0
  84. package/vendor/gugle-chat-bot/src/api.json +4285 -0
  85. package/vendor/gugle-chat-bot/src/command/index.ts +141 -0
  86. package/vendor/gugle-chat-bot/src/config/features.ts +548 -0
  87. package/vendor/gugle-chat-bot/src/config/index.ts +184 -0
  88. package/vendor/gugle-chat-bot/src/config/manager.ts +357 -0
  89. package/vendor/gugle-chat-bot/src/constants/index.ts +13 -0
  90. package/vendor/gugle-chat-bot/src/custom.ts +548 -0
  91. package/vendor/gugle-chat-bot/src/event/index.ts +118 -0
  92. package/vendor/gugle-chat-bot/src/features/admin/index.ts +414 -0
  93. package/vendor/gugle-chat-bot/src/features/bili/image/impl/video.ts +39 -0
  94. package/vendor/gugle-chat-bot/src/features/bili/image/index.ts +37 -0
  95. package/vendor/gugle-chat-bot/src/features/bili/index.ts +65 -0
  96. package/vendor/gugle-chat-bot/src/features/bili/template/style/video.css +97 -0
  97. package/vendor/gugle-chat-bot/src/features/bili/template/video.html +57 -0
  98. package/vendor/gugle-chat-bot/src/features/discord-bridge/index.ts +1128 -0
  99. package/vendor/gugle-chat-bot/src/features/discord-bridge/markdown.ts +105 -0
  100. package/vendor/gugle-chat-bot/src/features/discord-bridge/slash.ts +355 -0
  101. package/vendor/gugle-chat-bot/src/features/discord-bridge/source.ts +96 -0
  102. package/vendor/gugle-chat-bot/src/features/discord-bridge/template/message.html +14 -0
  103. package/vendor/gugle-chat-bot/src/features/discord-bridge/template/style/message.css +144 -0
  104. package/vendor/gugle-chat-bot/src/features/github/api.ts +93 -0
  105. package/vendor/gugle-chat-bot/src/features/github/binding.ts +161 -0
  106. package/vendor/gugle-chat-bot/src/features/github/image/impl/issue.ts +132 -0
  107. package/vendor/gugle-chat-bot/src/features/github/image/impl/pr.ts +144 -0
  108. package/vendor/gugle-chat-bot/src/features/github/image/impl/release.ts +160 -0
  109. package/vendor/gugle-chat-bot/src/features/github/image/index.ts +310 -0
  110. package/vendor/gugle-chat-bot/src/features/github/index.ts +302 -0
  111. package/vendor/gugle-chat-bot/src/features/github/template/issue.html +118 -0
  112. package/vendor/gugle-chat-bot/src/features/github/template/pull_request.html +125 -0
  113. package/vendor/gugle-chat-bot/src/features/github/template/release.html +53 -0
  114. package/vendor/gugle-chat-bot/src/features/github/template/style/issue.css +468 -0
  115. package/vendor/gugle-chat-bot/src/features/github/template/style/pull_request.css +469 -0
  116. package/vendor/gugle-chat-bot/src/features/github/template/style/release.css +412 -0
  117. package/vendor/gugle-chat-bot/src/features/hermes/client.ts +184 -0
  118. package/vendor/gugle-chat-bot/src/features/hermes/config.ts +210 -0
  119. package/vendor/gugle-chat-bot/src/features/hermes/context.ts +67 -0
  120. package/vendor/gugle-chat-bot/src/features/hermes/history.ts +86 -0
  121. package/vendor/gugle-chat-bot/src/features/hermes/index.ts +1533 -0
  122. package/vendor/gugle-chat-bot/src/features/hermes/message.ts +319 -0
  123. package/vendor/gugle-chat-bot/src/features/hermes/reaction.ts +181 -0
  124. package/vendor/gugle-chat-bot/src/features/hermes/render.ts +118 -0
  125. package/vendor/gugle-chat-bot/src/features/hermes/reply-units.ts +331 -0
  126. package/vendor/gugle-chat-bot/src/features/hermes/skills.ts +406 -0
  127. package/vendor/gugle-chat-bot/src/features/hermes/template/approval.html +30 -0
  128. package/vendor/gugle-chat-bot/src/features/hermes/template/progress.html +21 -0
  129. package/vendor/gugle-chat-bot/src/features/hermes/template/style/approval.css +81 -0
  130. package/vendor/gugle-chat-bot/src/features/hermes/template/style/progress.css +120 -0
  131. package/vendor/gugle-chat-bot/src/features/hermes/trigger.ts +86 -0
  132. package/vendor/gugle-chat-bot/src/features/hermes/types.ts +327 -0
  133. package/vendor/gugle-chat-bot/src/features/management/index.ts +156 -0
  134. package/vendor/gugle-chat-bot/src/features/minecraft/index.ts +127 -0
  135. package/vendor/gugle-chat-bot/src/features/modrinth/index.ts +71 -0
  136. package/vendor/gugle-chat-bot/src/features/parentheses/index.ts +117 -0
  137. package/vendor/gugle-chat-bot/src/features/peak-valley-timer/index.ts +452 -0
  138. package/vendor/gugle-chat-bot/src/features/poke/index.ts +24 -0
  139. package/vendor/gugle-chat-bot/src/features/version-tracker/index.ts +78 -0
  140. package/vendor/gugle-chat-bot/src/features/welcome/index.ts +58 -0
  141. package/vendor/gugle-chat-bot/src/github-ip.json +5814 -0
  142. package/vendor/gugle-chat-bot/src/image/index.ts +278 -0
  143. package/vendor/gugle-chat-bot/src/index.ts +695 -0
  144. package/vendor/gugle-chat-bot/src/logger/index.ts +54 -0
  145. package/vendor/gugle-chat-bot/src/result.json +359 -0
  146. package/vendor/gugle-chat-bot/src/type/bili.d.ts +825 -0
  147. package/vendor/gugle-chat-bot/src/type/event.d.ts +146 -0
  148. package/vendor/gugle-chat-bot/src/type/github.d.ts +446 -0
  149. package/vendor/gugle-chat-bot/src/type/index.d.ts +254 -0
  150. package/vendor/gugle-chat-bot/tsconfig.json +46 -0
package/src/panel.html ADDED
@@ -0,0 +1,174 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <meta charset="utf-8" /><meta
4
+ name="viewport"
5
+ content="width=device-width,initial-scale=1"
6
+ /><title>Synapse · 通道与记忆</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: light dark;
10
+ font-family: system-ui, sans-serif;
11
+ background: Canvas;
12
+ color: CanvasText;
13
+ }
14
+ * {
15
+ box-sizing: border-box;
16
+ }
17
+ body {
18
+ margin: 0;
19
+ }
20
+ main {
21
+ max-width: 1100px;
22
+ margin: auto;
23
+ padding: 32px 24px;
24
+ }
25
+ header {
26
+ display: flex;
27
+ justify-content: space-between;
28
+ align-items: center;
29
+ gap: 20px;
30
+ }
31
+ h1 {
32
+ margin: 0;
33
+ font-size: 32px;
34
+ }
35
+ header p {
36
+ opacity: 0.65;
37
+ }
38
+ nav {
39
+ display: flex;
40
+ gap: 8px;
41
+ flex-wrap: wrap;
42
+ border-bottom: 1px solid #8885;
43
+ padding: 20px 0;
44
+ }
45
+ button,
46
+ input,
47
+ textarea,
48
+ select {
49
+ font: inherit;
50
+ color: inherit;
51
+ background: transparent;
52
+ border: 1px solid #8887;
53
+ border-radius: 7px;
54
+ padding: 9px;
55
+ }
56
+ button {
57
+ cursor: pointer;
58
+ }
59
+ button:hover,
60
+ button[aria-selected="true"] {
61
+ background: #34877825;
62
+ border-color: #348778;
63
+ }
64
+ button:disabled {
65
+ opacity: 0.5;
66
+ cursor: wait;
67
+ }
68
+ textarea {
69
+ width: 100%;
70
+ min-height: 180px;
71
+ resize: vertical;
72
+ font:
73
+ 14px/1.6 ui-monospace,
74
+ monospace;
75
+ }
76
+ input:not([type="checkbox"]),
77
+ select {
78
+ width: 100%;
79
+ }
80
+ label {
81
+ display: block;
82
+ margin: 12px 0 5px;
83
+ }
84
+ fieldset {
85
+ border: 1px solid #8885;
86
+ border-radius: 10px;
87
+ padding: 18px;
88
+ margin: 18px 0;
89
+ min-width: 0;
90
+ }
91
+ legend {
92
+ padding: 0 8px;
93
+ font-weight: 600;
94
+ }
95
+ section {
96
+ padding: 18px 0;
97
+ }
98
+ pre {
99
+ white-space: pre-wrap;
100
+ overflow-wrap: anywhere;
101
+ max-height: 500px;
102
+ overflow: auto;
103
+ padding: 16px;
104
+ border: 1px solid #8885;
105
+ border-radius: 8px;
106
+ }
107
+ article {
108
+ border-bottom: 1px solid #8885;
109
+ padding: 12px 0;
110
+ }
111
+ .actions {
112
+ display: flex;
113
+ gap: 8px;
114
+ flex-wrap: wrap;
115
+ margin: 12px 0;
116
+ }
117
+ .muted {
118
+ font-size: 13px;
119
+ opacity: 0.7;
120
+ }
121
+ #notice {
122
+ min-height: 24px;
123
+ padding: 12px 0;
124
+ }
125
+ #notice.error {
126
+ color: #d85050;
127
+ }
128
+ .two {
129
+ display: grid;
130
+ grid-template-columns: 1fr 2fr;
131
+ gap: 24px;
132
+ }
133
+ .row {
134
+ padding: 12px;
135
+ border-left: 2px solid #348778;
136
+ margin: 10px 0;
137
+ }
138
+ details {
139
+ padding: 8px 0;
140
+ }
141
+ summary {
142
+ cursor: pointer;
143
+ }
144
+ @media (max-width: 650px) {
145
+ main {
146
+ padding: 20px 12px;
147
+ }
148
+ .two {
149
+ grid-template-columns: 1fr;
150
+ }
151
+ header {
152
+ display: block;
153
+ }
154
+ }
155
+ </style>
156
+ <main>
157
+ <header>
158
+ <div>
159
+ <h1>Synapse</h1>
160
+ <p>QQ · Discord · GitHub,共享知识,各司其职。</p>
161
+ </div>
162
+ <span id="status">正在连接 DSH…</span>
163
+ </header>
164
+ <nav aria-label="管理模块" id="tabs"></nav>
165
+ <div id="notice" role="status" aria-live="polite"></div>
166
+ <div id="content"></div>
167
+ <footer class="muted">
168
+ 配置保存在当前 DSH
169
+ profile。模型首次使用时下载;聊天与审查记忆分开检索。<br />AGPL-3.0 ·
170
+ <a href="https://github.com/dsh-plugins/dsh-synapse">获取完整源码</a>
171
+ </footer>
172
+ </main>
173
+ <script type="module" src="/synapse-panel.js"></script>
174
+ </html>
package/src/panel.ts ADDED
@@ -0,0 +1,440 @@
1
+ const $ = (id) => document.getElementById(id);
2
+ function node<K extends keyof HTMLElementTagNameMap>(
3
+ tag: K,
4
+ text?: string,
5
+ attrs: Record<string, unknown> = {},
6
+ ): HTMLElementTagNameMap[K] {
7
+ const n = document.createElement(tag);
8
+ if (text !== undefined) n.textContent = text;
9
+ Object.assign(n, attrs);
10
+ return n;
11
+ }
12
+ function notify(text, error = false) {
13
+ $("notice").textContent = text;
14
+ $("notice").className = error ? "error" : "";
15
+ }
16
+ async function request(path, data?) {
17
+ const r = await fetch(path, {
18
+ cache: "no-store",
19
+ ...(data === undefined
20
+ ? {}
21
+ : {
22
+ method: "POST",
23
+ headers: { "Content-Type": "application/json" },
24
+ body: JSON.stringify(data),
25
+ }),
26
+ });
27
+ const result = await r.json();
28
+ if (!r.ok || result.ok === false)
29
+ throw new Error(result.error || result.message || `HTTP ${r.status}`);
30
+ return result;
31
+ }
32
+ const api = (action, data?) => request("/api/dsh-synapse/" + action, data);
33
+ function button(label, action) {
34
+ const b = node("button", label);
35
+ b.onclick = async () => {
36
+ b.disabled = true;
37
+ try {
38
+ await action();
39
+ } catch (e) {
40
+ notify(e.message, true);
41
+ } finally {
42
+ b.disabled = false;
43
+ }
44
+ };
45
+ return b;
46
+ }
47
+ function labeled(parent, label, input) {
48
+ const id = "field-" + ++sequence;
49
+ input.id = id;
50
+ const l = node("label", label, { htmlFor: id });
51
+ parent.append(l, input);
52
+ return input;
53
+ }
54
+ let sequence = 0,
55
+ dirty = false,
56
+ view,
57
+ draft,
58
+ active = "配置";
59
+ const tabs = ["配置", "Skill", "MCP", "Hermes 记忆", "GitHub 审查"];
60
+ for (const label of tabs) {
61
+ const b = button(label, async () => {
62
+ if (dirty && !confirm("当前修改尚未保存,放弃修改并切换?")) return;
63
+ dirty = false;
64
+ active = label;
65
+ await render();
66
+ });
67
+ b.setAttribute("role", "tab");
68
+ $("tabs").append(b);
69
+ }
70
+ async function loadConfig() {
71
+ view = (await request("/api/dshloader/config/dsh-synapse")).value;
72
+ draft = structuredClone(view.value);
73
+ }
74
+ async function saveConfig() {
75
+ const saved = await request("/api/dshloader/config/dsh-synapse", {
76
+ value: draft,
77
+ expectedRevision: view.revision,
78
+ });
79
+ view = saved.value;
80
+ draft = structuredClone(view.value);
81
+ dirty = false;
82
+ notify("已保存。通道正在重新连接;可刷新状态检查结果。");
83
+ }
84
+ function field(parent, f, obj, key = f.key) {
85
+ if (f.type === "group" || f.type === "object") {
86
+ const box = node("fieldset");
87
+ box.append(node("legend", f.label));
88
+ parent.append(box);
89
+ const target = f.type === "object" ? (obj[key] ??= {}) : obj;
90
+ for (const child of f.fields) field(box, child, target);
91
+ return;
92
+ }
93
+ if (f.type === "array") {
94
+ const box = node("fieldset");
95
+ box.append(node("legend", f.label));
96
+ parent.append(box);
97
+ const list = node("div");
98
+ box.append(list);
99
+ const redraw = () => {
100
+ list.replaceChildren();
101
+ (obj[key] ||= []).forEach((_, i) => {
102
+ const row = node("div", undefined, { className: "row" });
103
+ field(row, { ...f.item, label: `第 ${i + 1} 项` }, obj[key], i);
104
+ row.append(
105
+ button("删除", () => {
106
+ obj[key].splice(i, 1);
107
+ dirty = true;
108
+ redraw();
109
+ }),
110
+ );
111
+ list.append(row);
112
+ });
113
+ };
114
+ redraw();
115
+ box.append(
116
+ button("添加", () => {
117
+ obj[key].push(defaultOf(f.item));
118
+ dirty = true;
119
+ redraw();
120
+ }),
121
+ );
122
+ return;
123
+ }
124
+ let control;
125
+ if (f.type === "boolean") {
126
+ control = node("input", undefined, {
127
+ type: "checkbox",
128
+ checked: obj[key] === true,
129
+ });
130
+ control.onchange = () => {
131
+ obj[key] = control.checked;
132
+ dirty = true;
133
+ };
134
+ } else if (f.type === "select") {
135
+ control = node("select");
136
+ for (const opt of f.options)
137
+ control.append(node("option", opt.label, { value: opt.value }));
138
+ control.value = obj[key];
139
+ control.onchange = () => {
140
+ obj[key] = control.value;
141
+ dirty = true;
142
+ };
143
+ } else {
144
+ const isJSON = String(key).toLowerCase().includes("json");
145
+ control = node(f.type === "textarea" || isJSON ? "textarea" : "input");
146
+ if (control.tagName === "INPUT")
147
+ control.type =
148
+ f.type === "password"
149
+ ? "password"
150
+ : f.type === "number"
151
+ ? "number"
152
+ : "text";
153
+ const secret = obj[key] && typeof obj[key] === "object";
154
+ control.value = secret ? "" : (obj[key] ?? "");
155
+ if (secret)
156
+ control.placeholder = "已保存(不回显)。留空保持不变;输入替换。";
157
+ control.oninput = () => {
158
+ obj[key] = f.type === "number" ? Number(control.value) : control.value;
159
+ dirty = true;
160
+ };
161
+ if (f.min !== undefined) control.min = f.min;
162
+ if (f.max !== undefined) control.max = f.max;
163
+ }
164
+ labeled(parent, f.label, control);
165
+ if (f.description)
166
+ parent.append(node("div", f.description, { className: "muted" }));
167
+ if (f.type === "password")
168
+ parent.append(
169
+ button("清空此值", () => {
170
+ obj[key] = "";
171
+ control.value = "";
172
+ control.placeholder = "已清空,保存后生效";
173
+ dirty = true;
174
+ }),
175
+ );
176
+ }
177
+ function defaultOf(f) {
178
+ if (f.default !== undefined) return structuredClone(f.default);
179
+ if (f.type === "object")
180
+ return Object.fromEntries(f.fields.map((x) => [x.key, defaultOf(x)]));
181
+ if (f.type === "array") return [];
182
+ if (f.type === "boolean") return false;
183
+ if (f.type === "select") return f.options[0].value;
184
+ if (f.type === "number") return f.min || 0;
185
+ return "";
186
+ }
187
+ async function configPanel(root, onlyMcp = false) {
188
+ await loadConfig();
189
+ root.append(
190
+ node(
191
+ "p",
192
+ onlyMcp
193
+ ? "添加或修改 MCP 服务,保存后可预览工具。仅允许列表中的工具可被聊天自动调用。"
194
+ : "所有通道和上游功能均保存在当前 profile;保存使用修订号检查并发修改。",
195
+ ),
196
+ );
197
+ for (const f of view.template.fields)
198
+ if (onlyMcp ? f.key === "mcp" : f.key !== "mcp") field(root, f, draft);
199
+ root.append(button("保存配置", saveConfig));
200
+ if (!onlyMcp) {
201
+ root.append(
202
+ button("载入上游功能 JSON 以预览 / 修改", async () => {
203
+ const rows = await api("features");
204
+ for (const row of rows) {
205
+ const f = draft.features.find((f) => f.id === row.id);
206
+ if (f) f.json = JSON.stringify(row.value, null, 2);
207
+ }
208
+ dirty = true;
209
+ root.replaceChildren();
210
+ root.append(
211
+ node("p", "已载入当前功能配置,可能包含密钥。修改后保存。"),
212
+ );
213
+ for (const f of view.template.fields)
214
+ if (f.key !== "mcp") field(root, f, draft);
215
+ root.append(button("保存配置", saveConfig));
216
+ }),
217
+ );
218
+ } else {
219
+ const box = node("section");
220
+ root.append(box);
221
+ for (const s of draft.mcp) {
222
+ box.append(
223
+ button(`预览 ${s.name} 工具`, async () => {
224
+ preview.textContent = JSON.stringify(
225
+ await api("mcp/tools", { name: s.name }),
226
+ null,
227
+ 2,
228
+ );
229
+ }),
230
+ );
231
+ }
232
+ const preview = node("pre", "工具列表显示在这里");
233
+ box.append(preview);
234
+ }
235
+ }
236
+ async function skillPanel(root) {
237
+ const layout = node("div", undefined, { className: "two" }),
238
+ list = node("div"),
239
+ editor = node("div");
240
+ layout.append(list, editor);
241
+ root.append(layout);
242
+ const name = labeled(editor, "Skill 名称", node("input")),
243
+ text = labeled(editor, "Markdown 文档", node("textarea"));
244
+ text.style.minHeight = "400px";
245
+ let revision = null,
246
+ inherited = false;
247
+ name.oninput = text.oninput = () => {
248
+ dirty = true;
249
+ };
250
+ const file = labeled(
251
+ editor,
252
+ "导入 SKILL.md",
253
+ node("input", undefined, {
254
+ type: "file",
255
+ accept: ".md,text/markdown,text/plain",
256
+ }),
257
+ );
258
+ file.onchange = async () => {
259
+ const f = file.files?.[0];
260
+ if (f) {
261
+ text.value = await f.text();
262
+ dirty = true;
263
+ }
264
+ };
265
+ const preview = node("pre", "选择 Skill 预览或创建新 Skill。");
266
+ editor.append(preview);
267
+ const refresh = async () => {
268
+ list.replaceChildren();
269
+ for (const s of await api("skills"))
270
+ list.append(
271
+ button(s.name + (s.inherited ? " · Home" : ""), () => {
272
+ name.value = s.name;
273
+ text.value = s.content;
274
+ revision = s.revision;
275
+ inherited = s.inherited;
276
+ preview.textContent = s.content;
277
+ dirty = false;
278
+ }),
279
+ );
280
+ };
281
+ editor.append(
282
+ button("预览文档", () => {
283
+ preview.textContent = text.value;
284
+ }),
285
+ button("新建", () => {
286
+ name.value = "";
287
+ text.value = "";
288
+ revision = null;
289
+ inherited = false;
290
+ dirty = false;
291
+ }),
292
+ button("保存 Skill", async () => {
293
+ const saved = await api("skills/save", {
294
+ name: name.value,
295
+ content: text.value,
296
+ revision,
297
+ });
298
+ revision = saved.revision;
299
+ dirty = false;
300
+ notify("Skill 已保存,可被运行中的聊天检索。");
301
+ await refresh();
302
+ }),
303
+ button("删除 Skill", async () => {
304
+ if (inherited) throw new Error("Home Skill 为继承项,请在 Home 中管理");
305
+ if (!confirm(`删除 ${name.value}?`)) return;
306
+ await api("skills/delete", { name: name.value, revision });
307
+ name.value = "";
308
+ text.value = "";
309
+ revision = null;
310
+ dirty = false;
311
+ await refresh();
312
+ notify("Skill 已删除");
313
+ }),
314
+ );
315
+ await refresh();
316
+ }
317
+ async function memoryPanel(root) {
318
+ const channel = labeled(root, "记忆分类", node("select"));
319
+ channel.append(
320
+ node("option", "聊天(QQ / Discord 共享)", { value: "chat" }),
321
+ node("option", "GitHub 审查", { value: "review" }),
322
+ );
323
+ const markdown = labeled(root, "粘贴 Hermes 记忆 Markdown", node("textarea"));
324
+ markdown.oninput = () => {
325
+ dirty = true;
326
+ };
327
+ const preview = node("pre", "导入前可预览分块。");
328
+ root.append(
329
+ button("预览分块", async () => {
330
+ preview.textContent = JSON.stringify(
331
+ await api("memory/import", {
332
+ markdown: markdown.value,
333
+ channel: channel.value,
334
+ preview: true,
335
+ }),
336
+ null,
337
+ 2,
338
+ );
339
+ }),
340
+ button("批量导入", async () => {
341
+ notify("正在生成本地嵌入;首次下载模型可能需要几分钟…");
342
+ preview.textContent = JSON.stringify(
343
+ await api("memory/import", {
344
+ markdown: markdown.value,
345
+ channel: channel.value,
346
+ }),
347
+ null,
348
+ 2,
349
+ );
350
+ dirty = false;
351
+ notify("记忆导入完成");
352
+ }),
353
+ );
354
+ const query = labeled(root, "检索记忆", node("input"));
355
+ root.append(
356
+ button("混合检索", async () => {
357
+ preview.textContent = JSON.stringify(
358
+ await api("memory/search", {
359
+ query: query.value,
360
+ channel: channel.value,
361
+ }),
362
+ null,
363
+ 2,
364
+ );
365
+ }),
366
+ button("最近记忆", async () => {
367
+ preview.textContent = JSON.stringify(
368
+ await api(`memory?channel=${channel.value}`),
369
+ null,
370
+ 2,
371
+ );
372
+ }),
373
+ preview,
374
+ );
375
+ }
376
+ async function reviewPanel(root) {
377
+ root.append(
378
+ node("p", "Webhook:" + location.origin + "/hooks/dsh-synapse/github"),
379
+ node(
380
+ "p",
381
+ "配置 GitHub Secret 和仓库允许列表后启用;未开启自动发布时,结果仅保存在此处。",
382
+ ),
383
+ );
384
+ const list = node("div");
385
+ root.append(button("刷新任务", refresh), list);
386
+ async function refresh() {
387
+ list.replaceChildren();
388
+ for (const job of await api("reviews")) {
389
+ const a = node("article");
390
+ a.append(
391
+ node("strong", `${job.event} · ${job.state} · ${job.id}`),
392
+ node("pre", job.error || job.result || "等待处理"),
393
+ );
394
+ if (job.state === "failed")
395
+ a.append(
396
+ button("重试", async () => {
397
+ await api("reviews/retry", { id: job.id });
398
+ await refresh();
399
+ }),
400
+ );
401
+ list.append(a);
402
+ }
403
+ }
404
+ await refresh();
405
+ }
406
+ async function render() {
407
+ for (const b of $("tabs").children)
408
+ b.setAttribute("aria-selected", String(b.textContent === active));
409
+ const root = $("content");
410
+ root.replaceChildren();
411
+ notify("");
412
+ try {
413
+ if (active === "配置") await configPanel(root);
414
+ if (active === "MCP") await configPanel(root, true);
415
+ if (active === "Skill") await skillPanel(root);
416
+ if (active === "Hermes 记忆") await memoryPanel(root);
417
+ if (active === "GitHub 审查") await reviewPanel(root);
418
+ } catch (e) {
419
+ notify(e.message, true);
420
+ }
421
+ }
422
+ async function status() {
423
+ try {
424
+ const s = await api("status");
425
+ $("status").textContent = s.lastError
426
+ ? "需要处理:" + s.lastError
427
+ : "机器人:" + s.bot;
428
+ } catch (e) {
429
+ $("status").textContent = e.message;
430
+ }
431
+ }
432
+ window.addEventListener("beforeunload", (e) => {
433
+ if (dirty) {
434
+ e.preventDefault();
435
+ e.returnValue = "";
436
+ }
437
+ });
438
+ await render();
439
+ await status();
440
+ setInterval(status, 10000);