@konglx/rotom 2.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/README.md +417 -0
  2. package/bin/mesh-master.sh +439 -0
  3. package/bin/rotom +29 -0
  4. package/bin/rotom-link.sh +136 -0
  5. package/bin/rotom-send-with-status +57 -0
  6. package/bin/rotom-up.sh +428 -0
  7. package/dist/cli/ask.js +62 -0
  8. package/dist/cli/common.js +321 -0
  9. package/dist/cli/config.js +65 -0
  10. package/dist/cli/directory.js +17 -0
  11. package/dist/cli/executor.js +58 -0
  12. package/dist/cli/fed.js +91 -0
  13. package/dist/cli/group.js +273 -0
  14. package/dist/cli/identity.js +62 -0
  15. package/dist/cli/init.js +268 -0
  16. package/dist/cli/issue.js +202 -0
  17. package/dist/cli/join.js +170 -0
  18. package/dist/cli/link.js +47 -0
  19. package/dist/cli/master.js +51 -0
  20. package/dist/cli/memory.js +307 -0
  21. package/dist/cli/note.js +68 -0
  22. package/dist/cli/repo.js +77 -0
  23. package/dist/cli/rotom.js +277 -0
  24. package/dist/cli/routes.js +118 -0
  25. package/dist/cli/run.js +45 -0
  26. package/dist/cli/schedule.js +237 -0
  27. package/dist/cli/skill.js +173 -0
  28. package/dist/cli/team.js +106 -0
  29. package/dist/executor/claude-code-hook.cjs +80 -0
  30. package/dist/executor/cli-executor.js +8 -0
  31. package/dist/executor/executors/claude-code.js +780 -0
  32. package/dist/executor/executors/codex.js +719 -0
  33. package/dist/executor/executors/hermes-cli.js +855 -0
  34. package/dist/executor/executors/openclaw.js +467 -0
  35. package/dist/executor/executors/pi.js +514 -0
  36. package/dist/executor/index.js +269 -0
  37. package/dist/executor/jsonrpc-transport.js +125 -0
  38. package/dist/executor/process-runner.js +101 -0
  39. package/dist/executor/reasoning-status.js +83 -0
  40. package/dist/executor/repo-cache.js +502 -0
  41. package/dist/executor/session-store.js +188 -0
  42. package/dist/executor/worker-chat.js +257 -0
  43. package/dist/executor/worker-connection.js +89 -0
  44. package/dist/executor/worker-issue.js +264 -0
  45. package/dist/executor/worker.js +877 -0
  46. package/dist/link/pending-requests.js +72 -0
  47. package/dist/link/server.js +233 -0
  48. package/dist/link/visibility-store.js +58 -0
  49. package/dist/master/api/agents.js +333 -0
  50. package/dist/master/api/artifacts.js +271 -0
  51. package/dist/master/api/domains.js +64 -0
  52. package/dist/master/api/groups.js +635 -0
  53. package/dist/master/api/guidance-templates.js +147 -0
  54. package/dist/master/api/index.js +89 -0
  55. package/dist/master/api/issues-patrol.js +172 -0
  56. package/dist/master/api/issues.js +663 -0
  57. package/dist/master/api/links-patrol.js +168 -0
  58. package/dist/master/api/links.js +114 -0
  59. package/dist/master/api/memory.js +259 -0
  60. package/dist/master/api/messages.js +157 -0
  61. package/dist/master/api/notes.js +77 -0
  62. package/dist/master/api/schedule-patterns.js +133 -0
  63. package/dist/master/api/schedules.js +272 -0
  64. package/dist/master/api/sessions.js +158 -0
  65. package/dist/master/api/share.js +269 -0
  66. package/dist/master/api/skills.js +190 -0
  67. package/dist/master/api/teams.js +122 -0
  68. package/dist/master/api/uploads.js +245 -0
  69. package/dist/master/auth.js +134 -0
  70. package/dist/master/dashboard/animations/calico-dozing.apng +0 -0
  71. package/dist/master/dashboard/animations/calico-error.apng +0 -0
  72. package/dist/master/dashboard/animations/calico-happy.apng +0 -0
  73. package/dist/master/dashboard/animations/calico-notification.apng +0 -0
  74. package/dist/master/dashboard/animations/calico-sleeping.apng +0 -0
  75. package/dist/master/dashboard/animations/calico-thinking.apng +0 -0
  76. package/dist/master/dashboard/animations/calico-waking.apng +0 -0
  77. package/dist/master/dashboard/assets/ApprovalCard-C38VV6ko.css +1 -0
  78. package/dist/master/dashboard/assets/ApprovalCard-CHPh2dmE.js +17 -0
  79. package/dist/master/dashboard/assets/ArtifactPanel-P_2gAP7v.js +1 -0
  80. package/dist/master/dashboard/assets/ArtifactPanel-aGHySny5.css +1 -0
  81. package/dist/master/dashboard/assets/css.worker-DaIe3gwK.js +84 -0
  82. package/dist/master/dashboard/assets/editor.worker-BCzxt1at.js +12 -0
  83. package/dist/master/dashboard/assets/html.worker-CKrFyw_2.js +461 -0
  84. package/dist/master/dashboard/assets/index-CChrTn81.css +32 -0
  85. package/dist/master/dashboard/assets/index-Dhu4SN1z.js +181 -0
  86. package/dist/master/dashboard/assets/json.worker-B7c_PmGb.js +49 -0
  87. package/dist/master/dashboard/assets/markdown-CeN5IgdF.js +29 -0
  88. package/dist/master/dashboard/assets/monaco-core-DyX1CsEw.css +1 -0
  89. package/dist/master/dashboard/assets/monaco-core-oQiQUisy.js +833 -0
  90. package/dist/master/dashboard/assets/monaco-setup-CiOPQdmo.js +1 -0
  91. package/dist/master/dashboard/assets/react-vendor-C8IxlyCR.js +67 -0
  92. package/dist/master/dashboard/assets/ts.worker-BhkL8olL.js +51334 -0
  93. package/dist/master/dashboard/assets/useMonaco-ILb4vyPh.js +12 -0
  94. package/dist/master/dashboard/assets/vite-preload-CxJPbCTl.js +1 -0
  95. package/dist/master/dashboard/debug-auth.html +197 -0
  96. package/dist/master/dashboard/favicon.ico +0 -0
  97. package/dist/master/dashboard/index.html +20 -0
  98. package/dist/master/dashboard/rotom-avatar.png +0 -0
  99. package/dist/master/db/agent-sessions.js +60 -0
  100. package/dist/master/db/agent-visibility.js +64 -0
  101. package/dist/master/db/agents.js +119 -0
  102. package/dist/master/db/ask-bridges.js +157 -0
  103. package/dist/master/db/build-update.js +59 -0
  104. package/dist/master/db/core.js +82 -0
  105. package/dist/master/db/domains.js +80 -0
  106. package/dist/master/db/groups.js +316 -0
  107. package/dist/master/db/guidance-templates.js +58 -0
  108. package/dist/master/db/index.js +12 -0
  109. package/dist/master/db/internal.js +45 -0
  110. package/dist/master/db/issues-patrol.js +81 -0
  111. package/dist/master/db/issues.js +373 -0
  112. package/dist/master/db/links.js +221 -0
  113. package/dist/master/db/master-node.js +43 -0
  114. package/dist/master/db/memory.js +272 -0
  115. package/dist/master/db/messages.js +210 -0
  116. package/dist/master/db/notes.js +55 -0
  117. package/dist/master/db/schedule-patterns.js +56 -0
  118. package/dist/master/db/schedules.js +135 -0
  119. package/dist/master/db/skills.js +144 -0
  120. package/dist/master/db/team.js +88 -0
  121. package/dist/master/db/types.js +10 -0
  122. package/dist/master/db.js +12 -0
  123. package/dist/master/embedded.js +133 -0
  124. package/dist/master/federation/client.js +283 -0
  125. package/dist/master/federation/identity.js +133 -0
  126. package/dist/master/federation/manager.js +267 -0
  127. package/dist/master/federation/publisher.js +87 -0
  128. package/dist/master/federation/self-publisher.js +69 -0
  129. package/dist/master/federation/server.js +487 -0
  130. package/dist/master/group-paths.js +208 -0
  131. package/dist/master/offline-queue.js +38 -0
  132. package/dist/master/opc-bootstrap.js +245 -0
  133. package/dist/master/patrol-terminal.js +275 -0
  134. package/dist/master/repo-scan.js +188 -0
  135. package/dist/master/router.js +214 -0
  136. package/dist/master/scheduler-handlers.js +510 -0
  137. package/dist/master/scheduler.js +201 -0
  138. package/dist/master/server.js +203 -0
  139. package/dist/master/services/link-collector.js +82 -0
  140. package/dist/master/services/link-patrol-bootstrap.js +50 -0
  141. package/dist/master/services/memory-extract-prompt.js +34 -0
  142. package/dist/master/services/patrol-bootstrap.js +63 -0
  143. package/dist/master/share-tokens.js +56 -0
  144. package/dist/master/terminal-hub.js +300 -0
  145. package/dist/master/uploads.js +108 -0
  146. package/dist/master/util/fs.js +100 -0
  147. package/dist/master/util/paths.js +50 -0
  148. package/dist/master/util/persona.js +10 -0
  149. package/dist/master/ws-hub/connection.js +928 -0
  150. package/dist/master/ws-hub/conversation.js +290 -0
  151. package/dist/master/ws-hub/directory.js +70 -0
  152. package/dist/master/ws-hub/dispatch-enrich.js +34 -0
  153. package/dist/master/ws-hub/hub.js +136 -0
  154. package/dist/master/ws-hub/index.js +9 -0
  155. package/dist/master/ws-hub/internal.js +35 -0
  156. package/dist/master/ws-hub/routing.js +295 -0
  157. package/dist/master/ws-hub/sessions.js +130 -0
  158. package/dist/master/ws-hub.js +11 -0
  159. package/dist/shared/agent-profile.js +44 -0
  160. package/dist/shared/constants.js +55 -0
  161. package/dist/shared/dedup.js +33 -0
  162. package/dist/shared/group-context.js +62 -0
  163. package/dist/shared/json-codec.js +33 -0
  164. package/dist/shared/logger.js +136 -0
  165. package/dist/shared/mention.js +22 -0
  166. package/dist/shared/network.js +40 -0
  167. package/dist/shared/parse.js +18 -0
  168. package/dist/shared/prompt-composer.js +171 -0
  169. package/dist/shared/protocol/client-messages.js +8 -0
  170. package/dist/shared/protocol/enums.js +6 -0
  171. package/dist/shared/protocol/federation.js +62 -0
  172. package/dist/shared/protocol/guards.js +87 -0
  173. package/dist/shared/protocol/server-messages.js +8 -0
  174. package/dist/shared/protocol/types.js +8 -0
  175. package/dist/shared/protocol.js +19 -0
  176. package/dist/shared/readonly-allowlist.js +122 -0
  177. package/dist/shared/rotom-cli-prompt.js +23 -0
  178. package/dist/shared/skill-context.js +19 -0
  179. package/dist/shared/skill-md.js +43 -0
  180. package/dist/shared/slash-commands.js +50 -0
  181. package/dist/shared/time.js +80 -0
  182. package/dist/shared/title.js +46 -0
  183. package/dist/shared/url-extractor.js +99 -0
  184. package/migrations/001-schema.sql +942 -0
  185. package/package.json +68 -0
  186. package/scripts/fix-node-pty-perms.mjs +46 -0
  187. package/skill/rotom-a2a-communicate/SKILL.md +257 -0
  188. package/skill/rotom-bus-host/SKILL.md +78 -0
  189. package/skill/rotom-bus-host/scripts/poll-replies.sh +148 -0
@@ -0,0 +1,271 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import { spawnSync } from "node:child_process";
4
+ import { resolveGroupArtifactRoot } from "../group-paths.js";
5
+ import { resolveGroupWorktreeInfo } from "../repo-scan.js";
6
+ import { readFileSafely, walkDir } from "../util/fs.js";
7
+ import { toBeijing } from "../../shared/time.js";
8
+ /** Walk up from `startPath` looking for a `.git` directory. Returns the repo
9
+ * root or null if we hit the filesystem root without finding one. Shared
10
+ * by /original, /diff and /refs so they don't each re-implement the walk.
11
+ *
12
+ * `startPath` can be either a file or directory path: we check it first
13
+ * (in case the caller already points at the repo root) before walking up. */
14
+ function findGitRoot(startPath) {
15
+ let cursor = fs.existsSync(startPath) && fs.statSync(startPath).isFile()
16
+ ? path.dirname(startPath)
17
+ : startPath;
18
+ const stopAt = path.parse(cursor).root;
19
+ while (cursor && cursor !== stopAt) {
20
+ if (fs.existsSync(path.join(cursor, ".git")))
21
+ return cursor;
22
+ const parent = path.dirname(cursor);
23
+ if (parent === cursor)
24
+ break;
25
+ cursor = parent;
26
+ }
27
+ return null;
28
+ }
29
+ /** 递归给 FileEntry 的 path 加前缀(如 `__repos/`),让注入的虚拟节点的 children
30
+ * 的 path 带上前缀,前端点击时 content API 能识别并切换 base。 */
31
+ function addPathPrefix(entries, prefix) {
32
+ return entries.map(e => ({
33
+ ...e,
34
+ path: prefix + e.path,
35
+ children: e.children ? addPathPrefix(e.children, prefix) : undefined,
36
+ }));
37
+ }
38
+ export function registerArtifactRoutes(apiRouter, db) {
39
+ apiRouter.get("/artifacts/:groupId", (req, res) => {
40
+ const groupDir = resolveGroupArtifactRoot(db, req.params.groupId);
41
+ if (!fs.existsSync(groupDir)) {
42
+ res.json([]);
43
+ return;
44
+ }
45
+ let files = walkDir(groupDir, groupDir);
46
+ // 配了 repo 的群:注入 `__repos` 虚拟节点(指向 primary worktree),
47
+ // 让 Dashboard 能浏览 worktree 代码 + group 产物。worktree 在
48
+ // ~/.rotom/repos/<repoName>-<id8>-wt/group-<groupId8>/,不在 groupDir 下。
49
+ // 用 `__repos` 避免和仓库里真实的 repos/ 目录冲突,`__` 前缀标记虚拟节点。
50
+ const wtInfo = resolveGroupWorktreeInfo(db, req.params.groupId);
51
+ if (wtInfo && wtInfo.primaryExists) {
52
+ try {
53
+ const wtFiles = walkDir(wtInfo.primaryPath, wtInfo.primaryPath);
54
+ // 给所有 path 加 `__repos/` 前缀,让 content/original/diff API 能识别
55
+ // (path 以 `__repos/` 开头时 base 换成 worktree)
56
+ const prefixed = addPathPrefix(wtFiles, "__repos/");
57
+ const filtered = files.filter(f => f.name !== "__repos");
58
+ filtered.push({
59
+ name: "__repos",
60
+ path: "__repos",
61
+ absPath: wtInfo.primaryPath,
62
+ size: 0,
63
+ modifiedTime: toBeijing(fs.statSync(wtInfo.primaryPath).mtime),
64
+ type: "directory",
65
+ children: prefixed,
66
+ });
67
+ filtered.sort((a, b) => {
68
+ if (a.type !== b.type)
69
+ return a.type === "directory" ? -1 : 1;
70
+ return a.name.localeCompare(b.name);
71
+ });
72
+ files = filtered;
73
+ }
74
+ catch {
75
+ // worktree 读取失败,保留原 files
76
+ }
77
+ }
78
+ res.json({
79
+ root: groupDir,
80
+ files,
81
+ });
82
+ });
83
+ apiRouter.get("/artifacts/:groupId/content", (req, res) => {
84
+ const filePath = req.query.path;
85
+ if (!filePath) {
86
+ res.status(400).json({ error: "path query parameter is required" });
87
+ return;
88
+ }
89
+ const groupDir = resolveGroupArtifactRoot(db, req.params.groupId);
90
+ // `__repos/...` 是虚拟注入节点(指向 primary worktree),不在 groupDir 下。
91
+ // 真实路径在 worktree 里,这里把 base 换成 worktree 路径,并剥掉 `__repos/` 前缀。
92
+ const wtInfo = resolveGroupWorktreeInfo(db, req.params.groupId);
93
+ let baseDir = groupDir;
94
+ let relPath = filePath;
95
+ if (wtInfo && wtInfo.primaryExists && filePath.startsWith("__repos/")) {
96
+ baseDir = wtInfo.primaryPath;
97
+ relPath = filePath.slice("__repos/".length);
98
+ }
99
+ const result = readFileSafely(baseDir, relPath);
100
+ if (result.kind === "outside-base") {
101
+ res.status(403).json({ error: "Invalid path" });
102
+ return;
103
+ }
104
+ if (result.kind === "missing") {
105
+ res.status(404).json({ error: "File not found" });
106
+ return;
107
+ }
108
+ if (result.kind === "too-large") {
109
+ res.json({
110
+ path: filePath,
111
+ content: `[File too large: ${(result.size / 1024).toFixed(1)}KB]`,
112
+ size: result.size,
113
+ type: "text",
114
+ });
115
+ return;
116
+ }
117
+ res.json({
118
+ path: filePath,
119
+ content: result.content,
120
+ size: result.size,
121
+ type: result.type,
122
+ });
123
+ });
124
+ apiRouter.get("/artifacts/:groupId/original", (req, res) => {
125
+ const filePath = req.query.path;
126
+ const base = req.query.base || "HEAD";
127
+ if (!filePath) {
128
+ res.status(400).json({ error: "path query parameter is required" });
129
+ return;
130
+ }
131
+ if (!/^[A-Za-z0-9_./~^@-]+$/.test(base)) {
132
+ res.status(400).json({ error: "Invalid base ref" });
133
+ return;
134
+ }
135
+ const groupDir = resolveGroupArtifactRoot(db, req.params.groupId);
136
+ const resolved = path.resolve(groupDir, filePath);
137
+ if (!resolved.startsWith(path.resolve(groupDir))) {
138
+ res.status(403).json({ error: "Invalid path" });
139
+ return;
140
+ }
141
+ const repoRoot = findGitRoot(resolved);
142
+ if (!repoRoot) {
143
+ res.json({ path: filePath, base, repoRoot: null, content: "", note: "目标文件不在 git 仓库中。" });
144
+ return;
145
+ }
146
+ const relInRepo = path.relative(repoRoot, resolved);
147
+ const result = spawnSync("git", ["show", `${base}:${relInRepo}`], {
148
+ cwd: repoRoot,
149
+ encoding: "utf-8",
150
+ maxBuffer: 5 * 1024 * 1024,
151
+ });
152
+ if (result.error) {
153
+ res.status(500).json({ error: `git show failed: ${result.error.message}` });
154
+ return;
155
+ }
156
+ if (result.status !== 0) {
157
+ res.json({
158
+ path: filePath,
159
+ base,
160
+ repoRoot,
161
+ relInRepo,
162
+ content: "",
163
+ note: result.stderr?.trim() || `file not present at ${base}`,
164
+ });
165
+ return;
166
+ }
167
+ res.json({
168
+ path: filePath,
169
+ base,
170
+ repoRoot,
171
+ relInRepo,
172
+ content: result.stdout,
173
+ });
174
+ });
175
+ apiRouter.get("/artifacts/:groupId/diff", (req, res) => {
176
+ const filePath = req.query.path;
177
+ const base = req.query.base || "HEAD";
178
+ if (!filePath) {
179
+ res.status(400).json({ error: "path query parameter is required" });
180
+ return;
181
+ }
182
+ if (!/^[A-Za-z0-9_./~^@-]+$/.test(base)) {
183
+ res.status(400).json({ error: "Invalid base ref" });
184
+ return;
185
+ }
186
+ const groupDir = resolveGroupArtifactRoot(db, req.params.groupId);
187
+ const resolved = path.resolve(groupDir, filePath);
188
+ if (!resolved.startsWith(path.resolve(groupDir))) {
189
+ res.status(403).json({ error: "Invalid path" });
190
+ return;
191
+ }
192
+ if (!fs.existsSync(resolved)) {
193
+ res.status(404).json({ error: "File not found" });
194
+ return;
195
+ }
196
+ const repoRoot = findGitRoot(resolved);
197
+ if (!repoRoot) {
198
+ res.json({ path: filePath, base, repoRoot: null, diff: "", note: "目标文件不在 git 仓库中,无法计算 diff。" });
199
+ return;
200
+ }
201
+ const relInRepo = path.relative(repoRoot, resolved);
202
+ const result = spawnSync("git", ["diff", "--no-color", base, "--", relInRepo], {
203
+ cwd: repoRoot,
204
+ encoding: "utf-8",
205
+ maxBuffer: 5 * 1024 * 1024,
206
+ });
207
+ if (result.error) {
208
+ res.status(500).json({ error: `git diff failed: ${result.error.message}` });
209
+ return;
210
+ }
211
+ if (result.status !== 0 && result.status !== null) {
212
+ res.status(500).json({
213
+ error: `git diff exited ${result.status}`,
214
+ stderr: result.stderr,
215
+ });
216
+ return;
217
+ }
218
+ res.json({
219
+ path: filePath,
220
+ base,
221
+ repoRoot,
222
+ relInRepo,
223
+ diff: result.stdout,
224
+ });
225
+ });
226
+ // List git refs (branches + tags) for the group's artifact root, used by the
227
+ // dashboard to populate the diff-base dropdown. Walks up from the group dir
228
+ // to find the nearest .git; returns empty lists (with a note) when not in a
229
+ // repo. The HEAD branch name is included so the UI can mark it as default.
230
+ apiRouter.get("/artifacts/:groupId/refs", (req, res) => {
231
+ const groupDir = resolveGroupArtifactRoot(db, req.params.groupId);
232
+ if (!fs.existsSync(groupDir)) {
233
+ res.json({ refs: [], heads: [], tags: [], head: "", note: "群产物目录不存在。" });
234
+ return;
235
+ }
236
+ const repoRoot = findGitRoot(groupDir);
237
+ if (!repoRoot) {
238
+ res.json({ refs: [], heads: [], tags: [], head: "", note: "目标目录不在 git 仓库中。" });
239
+ return;
240
+ }
241
+ const listResult = spawnSync("git", ["for-each-ref", "--format=%(refname:short)", "refs/heads/", "refs/tags/"], { cwd: repoRoot, encoding: "utf-8", maxBuffer: 1024 * 1024, timeout: 5000 });
242
+ if (listResult.error) {
243
+ res.status(500).json({ error: `git for-each-ref failed: ${listResult.error.message}` });
244
+ return;
245
+ }
246
+ if (listResult.status !== 0 && listResult.status !== null) {
247
+ res.status(500).json({
248
+ error: `git for-each-ref exited ${listResult.status}`,
249
+ stderr: listResult.stderr,
250
+ });
251
+ return;
252
+ }
253
+ const all = listResult.stdout
254
+ .split("\n")
255
+ .map((s) => s.trim())
256
+ .filter(Boolean);
257
+ const headResult = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
258
+ cwd: repoRoot,
259
+ encoding: "utf-8",
260
+ timeout: 3000,
261
+ });
262
+ const head = headResult.stdout?.trim() || "";
263
+ res.json({
264
+ refs: all,
265
+ heads: all.filter((r) => !r.startsWith("tags/")),
266
+ tags: all.filter((r) => r.startsWith("tags/")).map((r) => r.replace(/^tags\//, "")),
267
+ head,
268
+ repoRoot,
269
+ });
270
+ });
271
+ }
@@ -0,0 +1,64 @@
1
+ import { randomUUID } from "node:crypto";
2
+ export function registerDomainRoutes(apiRouter, db) {
3
+ apiRouter.get("/domains", (_req, res) => {
4
+ const domains = db.listDomains();
5
+ const enriched = domains.map(d => ({
6
+ ...d,
7
+ agentCount: db.countAgentsByDomain(d.name),
8
+ }));
9
+ res.json(enriched);
10
+ });
11
+ apiRouter.post("/domains", (req, res) => {
12
+ const { name, description } = req.body;
13
+ if (!name || typeof name !== "string" || !name.trim()) {
14
+ res.status(400).json({ error: "name is required" });
15
+ return;
16
+ }
17
+ const trimmed = name.trim();
18
+ const existing = db.getDomainByName(trimmed);
19
+ if (existing) {
20
+ res.status(409).json({ error: `Domain "${trimmed}" already exists` });
21
+ return;
22
+ }
23
+ const id = randomUUID();
24
+ db.insertDomain(id, trimmed, description);
25
+ res.status(201).json({ id, name: trimmed, description: description || null });
26
+ });
27
+ apiRouter.put("/domains/:id", (req, res) => {
28
+ const domain = db.getDomainById(req.params.id);
29
+ if (!domain) {
30
+ res.status(404).json({ error: "Domain not found" });
31
+ return;
32
+ }
33
+ const { name, description } = req.body;
34
+ if (name !== undefined && name !== domain.name) {
35
+ const dup = db.getDomainByName(name);
36
+ if (dup) {
37
+ res.status(409).json({ error: `Domain "${name}" already exists` });
38
+ return;
39
+ }
40
+ db.renameDomainInAgents(domain.name, name);
41
+ }
42
+ db.updateDomain(domain.id, { name, description });
43
+ res.json({ ok: true });
44
+ });
45
+ apiRouter.delete("/domains/:id", (req, res) => {
46
+ const domain = db.getDomainById(req.params.id);
47
+ if (!domain) {
48
+ res.status(404).json({ error: "Domain not found" });
49
+ return;
50
+ }
51
+ const count = db.countAgentsByDomain(domain.name);
52
+ if (count > 0) {
53
+ res.status(400).json({ error: `域「${domain.name}」仍有 ${count} 个员工,请先移走。` });
54
+ return;
55
+ }
56
+ const ruleCount = db.countCrossDomainRulesByDomain(domain.name);
57
+ if (ruleCount > 0) {
58
+ res.status(400).json({ error: `域「${domain.name}」仍有 ${ruleCount} 条跨域规则,请先删除。` });
59
+ return;
60
+ }
61
+ db.deleteDomain(domain.id);
62
+ res.json({ ok: true });
63
+ });
64
+ }