@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,122 @@
1
+ /**
2
+ * Read-only Bash command allowlist.
3
+ *
4
+ * Used by worker.onApprovalRequest to short-circuit the dashboard approval
5
+ * flow for safe read-only commands under the `r_allow` policy. Designed
6
+ * fail-closed: anything that *might* be a write or a compound construct
7
+ * (pipes, redirects, command substitution, leading env assignment, line
8
+ * continuation) makes the command NOT match — it falls through to the
9
+ * existing human approval flow.
10
+ *
11
+ * Note: this only certifies the command shape is read-only. It does NOT
12
+ * gate the *target* — `cat /etc/shadow` still passes because `cat` is
13
+ * read-only. Secret/permission defenses rely on the executor's cwd being
14
+ * pinned to a sandbox + the agent prompt; not on this list.
15
+ *
16
+ * Read/Grep/Glob (claude) and codex's built-in read tools are never hooked
17
+ * by the approval gate, so they never reach this code path.
18
+ */
19
+ // Single-token read-only commands. Bash built-ins and coreutils that only
20
+ // inspect the filesystem / process state.
21
+ export const READONLY_SINGLE = [
22
+ // 文件/目录浏览
23
+ "ls", "tree",
24
+ // 文件读取
25
+ "cat", "head", "tail", "wc",
26
+ // 元信息
27
+ "file", "stat",
28
+ // 搜索
29
+ "find", "fd", "rg", "grep", "ag", "ack",
30
+ // shell 内建只读
31
+ "echo", "pwd",
32
+ // 系统只读快照
33
+ "whoami", "uname", "date",
34
+ // 注:`env`/`printenv` 故意不放行 —— 全量打印环境变量会泄露 API_KEY 等
35
+ // secret。`curl`/`wget`/`nc`/`bash -c`/`sh -c`/`eval`/`source` 也不放行。
36
+ ];
37
+ // `<head> <sub>` 双 token 只读组合,主要覆盖多子命令工具(git/rotom)。
38
+ // 用 `${head} ${sub}` 字符串集合做精确匹配,避免 startsWith 误中。
39
+ export const READONLY_MULTI = [
40
+ // git 只读子命令(写类 add/commit/push/pull/reset/checkout/stash/clean/rm/tag/config 一律不放行)
41
+ "git log", "git status", "git diff", "git show",
42
+ "git branch", "git remote", "git rev-parse", "git ls-files",
43
+ "git blame", "git ls-tree", "git shortlog", "git describe",
44
+ // rotom 只读子命令(同样不放行 issue/agent 增删改类)
45
+ "rotom status", "rotom whoami", "rotom --version", "rotom -v", "rotom help",
46
+ // 运行时版本号(纯只读)
47
+ "node --version", "node -v",
48
+ "npm --version", "npm -v",
49
+ "pnpm --version", "pnpm -v",
50
+ "python --version", "python -V",
51
+ "python3 --version", "python3 -V",
52
+ ];
53
+ const SINGLE_SET = new Set(READONLY_SINGLE);
54
+ const MULTI_SET = new Set(READONLY_MULTI);
55
+ // 多子命令工具的 head —— 只有这些才查 MULTI 集合,避免 `cat_ls` 这种误中。
56
+ const MULTI_HEADS = new Set([
57
+ "git", "rotom", "node", "npm", "pnpm", "python", "python3",
58
+ ]);
59
+ // 任一命中即视为复合/危险命令,fail-closed。逐项列出便于单测定位。
60
+ const DANGER_PATTERNS = [
61
+ /\|/, // 管道: cat a | rm b
62
+ /&/, // && / || / 后台 &
63
+ /;/, // 语句分隔: ls; rm x
64
+ />/, // 重定向写: echo > y
65
+ /</, // 重定向读 / heredoc
66
+ /`/, // 反引号命令替换
67
+ /\$\(/, // $(...) 命令替换
68
+ /\$\{/, // ${...} 参数展开(防 ${...} 嵌套命令)
69
+ /\\/, // 行续 `ls \\\n&& rm` 或转义
70
+ ];
71
+ // 前导 env 赋值:`FOO=bar cmd ...` / `PATH=/x cmd ...`,防 PATH 劫持。
72
+ const LEADING_ENV_ASSIGN = /^[A-Za-z_]\w*=\S+/;
73
+ /**
74
+ * Returns true iff `command` is unambiguously read-only and safe to
75
+ * auto-approve. Conservative on purpose — when in doubt, returns false and
76
+ * lets the human approval flow handle it.
77
+ *
78
+ * See file-top JSDoc for the safety contract.
79
+ */
80
+ export function isReadonlyCommand(command) {
81
+ // 1. 非字符串 / 空
82
+ if (typeof command !== "string")
83
+ return false;
84
+ const trimmed = command.trim();
85
+ if (!trimmed)
86
+ return false;
87
+ // 2. 危险字符短路
88
+ for (const p of DANGER_PATTERNS) {
89
+ if (p.test(trimmed))
90
+ return false;
91
+ }
92
+ // 3. 前导 env 赋值短路
93
+ if (LEADING_ENV_ASSIGN.test(trimmed))
94
+ return false;
95
+ // 4. tokenize
96
+ const tokens = trimmed.split(/\s+/).filter((t) => t.length > 0);
97
+ if (tokens.length === 0)
98
+ return false;
99
+ // 5. 取首个非 flag token 作为 head
100
+ const headRaw = tokens.find((t) => !t.startsWith("-"));
101
+ if (!headRaw)
102
+ return false;
103
+ // 6. basename 兼容 `/bin/ls` 形态(仅当 head 含 / 才做)
104
+ const head = headRaw.includes("/")
105
+ ? headRaw.split("/").pop() ?? headRaw
106
+ : headRaw;
107
+ // 7. 单 token 白名单
108
+ if (SINGLE_SET.has(head))
109
+ return true;
110
+ // 8. 多 token 白名单:head 必须是多子命令工具,第二个 token 原样查表。
111
+ // 不再过滤 flag —— 因为 MULTI 集合里既有 `git log`(positional sub)
112
+ // 也有 `node --version`(flag sub);MULTI 集合本身就是白名单,只要命中就放行。
113
+ // 例:`git status --short` → head=git, sub=status(第二个 token) → 命中。
114
+ // `node --version` → head=node, sub=--version → 命中。
115
+ if (MULTI_HEADS.has(head) && tokens.length >= 2) {
116
+ const sub = tokens[1];
117
+ if (MULTI_SET.has(`${head} ${sub}`))
118
+ return true;
119
+ }
120
+ // 9. 否则 fail-closed
121
+ return false;
122
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * rotom CLI 使用规则 — 注入到每一个 CLI agent prompt 的最前段。
3
+ *
4
+ * 设计要点:
5
+ * - 这是一段**短**的 meta 信息(不是 skill description),与所有 agent 看到的都一致。
6
+ * - 完整命令参考放在 `~/.rotom/SKILL.md`,由 rotom CLI 启动时把
7
+ * `skill/rotom-a2a-communicate/SKILL.md` 内联写入。Agent 需要时自行 Read。
8
+ * 提示里只放一句"去看 SKILL.md"+ 锚点名,不重复列命令清单。
9
+ * - 不同 provider (claude/codex/openclaw/hermes/generic) 各自的
10
+ * "skill 机制位置" 不一致 —— 因此 rotom 自己做一份"自家文档"放在约定路径,
11
+ * 不依赖任何 provider 的 skill 系统。
12
+ */
13
+ export const ROTOM_CLI_PROMPT_VERSION = "rotomCliPrompt@2026-06-29a";
14
+ export const ROTOM_CLI_PROMPT = `[rotom CLI]
15
+ 通过 Bash 调 \`rotom\`(身份自动,不要传 --as;详情 Read ~/.rotom/SKILL.md)。
16
+ - 写盘前必须有 in_progress issue(见下 [当前群活跃 issue])。
17
+ - **你的回复正文就是群消息**——写什么群里就显示什么。提问其他 agent 时直接在正文里写 \`@对方 <问题> #reply\`,**不要调 \`rotom group send\`**。系统检测到 #reply 自动起 5min 超时 timer。
18
+ - **被其他 agent 提问时,回复正文以 @提问者 开头**(例:\`@西花-claude 回复内容...\`)。
19
+ - 收到 [ask-bridge 复述] 系统消息后:对方没 @ 你但系统检测到回复了,基于复述继续任务。
20
+ - 普通 @ (不带 #reply) 不起 timer,只是提到对方。
21
+ - **要 @ 人前不知道群里谁是谁 / 各自岗位,调 \`rotom group members <groupId>\` 查**(返回 position / bio / category / status);按岗位匹配目标,不要按名字猜。
22
+ - exit≠0 看 stderr 第一行,先 \`rotom status\` 自检。
23
+ `;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Skill 极简指针层 —— prompt 末尾一行,告诉 agent 有多少可用 skill + 怎么查。
3
+ *
4
+ * skill 是非核心工作(能力,不是当前任务上下文),绝不展开 content、不抢核心任务 prompt。
5
+ * agent 按需 `rotom skill mine <groupId>` / `rotom skill get <name>` 拉取。
6
+ *
7
+ * count=0 → 不注入。
8
+ */
9
+ export function buildSkillPointerLayer(p) {
10
+ if (p.count === 0)
11
+ return null;
12
+ const groupIdHint = p.groupId ? ` <groupId>` : "";
13
+ return {
14
+ layer: "skill-pointer",
15
+ content: `[可用技能] ${p.count} 个。用 \`rotom skill mine${groupIdHint}\` 查列表,` +
16
+ `\`rotom skill get <name>\` 看详情;无关技能忽略,不要硬套。\n`,
17
+ source: "agent_skill_bindings count (runtime, group+agent scoped)",
18
+ };
19
+ }
@@ -0,0 +1,43 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as os from "node:os";
4
+ import { fileURLToPath } from "node:url";
5
+ const ROTOM_HOME = process.env.ROTOM_HOME || path.join(os.homedir(), ".rotom");
6
+ const ROTOM_SKILL_MD = path.join(ROTOM_HOME, "SKILL.md");
7
+ /**
8
+ * 把仓库内的 `skill/rotom-a2a-communicate/SKILL.md` 写到 `~/.rotom/SKILL.md`。
9
+ *
10
+ * 幂等:内容相同就跳过,不触发文件 mtime 变化(避免和正在跑的 agent 抢文件)。
11
+ * 这个文件是 rotom 自家的"完整 rotom CLI 命令参考" — 跟 `src/shared/rotom-cli-prompt.ts`
12
+ * 里的 [rotom CLI 使用规则] 段配对使用:prompt 段塞短 hint,agent 真要查命令时
13
+ * 自己 `Read ~/.rotom/SKILL.md`。这样不依赖任何 provider 的 skill 机制。
14
+ *
15
+ * 在 rotom CLI 与 executor 两个入口都调用,保证只起 executor 不跑 rotom 时也能落盘。
16
+ */
17
+ export function ensureRotomSkillMd() {
18
+ try {
19
+ const here = path.dirname(fileURLToPath(import.meta.url));
20
+ const skillSrc = path.join(here, "..", "..", "skill", "rotom-a2a-communicate", "SKILL.md");
21
+ if (!fs.existsSync(skillSrc)) {
22
+ return;
23
+ }
24
+ const content = fs.readFileSync(skillSrc, "utf-8");
25
+ let needsWrite = true;
26
+ if (fs.existsSync(ROTOM_SKILL_MD)) {
27
+ try {
28
+ const existing = fs.readFileSync(ROTOM_SKILL_MD, "utf-8");
29
+ if (existing === content)
30
+ needsWrite = false;
31
+ }
32
+ catch { /* 读失败 → 重写 */ }
33
+ }
34
+ if (needsWrite) {
35
+ if (!fs.existsSync(ROTOM_HOME))
36
+ fs.mkdirSync(ROTOM_HOME, { recursive: true });
37
+ fs.writeFileSync(ROTOM_SKILL_MD, content, "utf-8");
38
+ }
39
+ }
40
+ catch (err) {
41
+ process.stderr.write(`[rotom] WARN: failed to write ~/.rotom/SKILL.md: ${err.message}\n`);
42
+ }
43
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Issue slash command 白名单与解析。
3
+ *
4
+ * 设计:master 在 POST /groups/:groupId/issues 时调 parseSlashCommand(title)
5
+ * 解析首个 token;若命中 SLASH_COMMAND_REGISTRY 则写入 issues.slash_command。
6
+ * worker 收到 issue_assigned 后据此向底层 CLI 注入对应执行模式。
7
+ */
8
+ export const SLASH_COMMAND_REGISTRY = {
9
+ "/plan": {
10
+ name: "/plan",
11
+ backends: ["claude", "codex", "pi"],
12
+ description: "以计划模式执行:先输出方案,等待用户审批后才落盘",
13
+ },
14
+ };
15
+ /**
16
+ * 解析 title 首个 token。仅当 token 形如 /[a-z][a-z0-9-]* 才视为 slash command
17
+ * 候选;其他形如 "/path/to" 之类的不会被误判。
18
+ *
19
+ * - 命中注册表 → 返回 { command, stripped }。
20
+ * - 匹配前缀模式但未注册 → 返回 { command, stripped, unknown:true } 由调用方决定如何处理。
21
+ * - 完全不匹配 → 返回 null。
22
+ */
23
+ export function parseSlashCommand(title) {
24
+ if (!title)
25
+ return null;
26
+ const m = title.match(/^(\/[a-z][a-z0-9-]*)(?:\s+([\s\S]*))?$/);
27
+ if (!m)
28
+ return null;
29
+ const command = m[1];
30
+ const stripped = (m[2] || "").trim();
31
+ return {
32
+ command,
33
+ stripped,
34
+ known: Object.prototype.hasOwnProperty.call(SLASH_COMMAND_REGISTRY, command),
35
+ };
36
+ }
37
+ /**
38
+ * 给 Codex 用的 plan 模式 developerInstructions。Codex 没有原生 plan 模式,
39
+ * 通过开发者系统指令引导其"先方案后落盘"。
40
+ */
41
+ export function buildPlanModeInstruction() {
42
+ return [
43
+ "[plan-mode]",
44
+ "你当前处于「计划模式」执行任务。规则:",
45
+ "1. 先彻底理解需求并探索代码(只允许读操作:阅读文件、grep、列目录)。",
46
+ "2. 把方案以清晰的 Markdown 输出(包含:背景、改动文件清单、关键步骤、风险/回退、验证方式)。",
47
+ "3. 输出方案后立即停下,等待用户审批确认;未经确认不要执行任何写操作(写文件、改配置、跑构建/迁移、提交、推送等)。",
48
+ "4. 用户确认后再开始实现;用户驳回则根据反馈调整方案再次输出。",
49
+ ].join("\n");
50
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * 全 codebase 统一的时间格式化器:北京时间字符串 "YYYY-MM-DD HH:MM:SS.mmm"。
3
+ *
4
+ * 设计:
5
+ * - 不依赖服务器时区(永远输出北京时间,无视 process.env.TZ)
6
+ * - 不带 Z / 时区后缀——本地时间字符串,所见即所过滤
7
+ * - 字典序比较 = 时间顺序比较(毫秒精度),ORDER BY 直接生效
8
+ * - 替代 `new Date().toISOString()`(后者返回 UTC,显示和过滤要 mental 换算,
9
+ * 实测群消息轮询时把本地时间字符串拿去对比 UTC ISO 会 silently 滤掉)
10
+ *
11
+ * 用法:
12
+ * import { nowBeijing } from "../shared/time.js";
13
+ * const ts = nowBeijing(); // "2026-06-30 18:02:04.123"
14
+ *
15
+ * 存量数据(migration 046 之前)仍是 UTC ISO,046 会一次性转成北京时间字符串。
16
+ */
17
+ const BEIJING_OFFSET_MS = 8 * 3600 * 1000;
18
+ /** Internal: decompose a Date (or epoch ms) into Beijing-time calendar parts. */
19
+ function beijingParts(input) {
20
+ const d = input instanceof Date ? input : new Date(input);
21
+ const u = new Date(d.getTime() + BEIJING_OFFSET_MS);
22
+ const pad2 = (n) => String(n).padStart(2, "0");
23
+ const pad3 = (n) => String(n).padStart(3, "0");
24
+ return {
25
+ Y: String(u.getUTCFullYear()),
26
+ M: pad2(u.getUTCMonth() + 1),
27
+ D: pad2(u.getUTCDate()),
28
+ h: pad2(u.getUTCHours()),
29
+ m: pad2(u.getUTCMinutes()),
30
+ s: pad2(u.getUTCSeconds()),
31
+ ms: pad3(u.getUTCMilliseconds()),
32
+ };
33
+ }
34
+ export function nowBeijing() {
35
+ return toBeijing(Date.now());
36
+ }
37
+ /**
38
+ * 把任意时间戳(UTC ISO / Date / 毫秒数)转成北京时间字符串。
39
+ * 用于 migration 把存量 UTC ISO 行转成新格式,或边界处把外部输入归一。
40
+ */
41
+ export function toBeijing(input) {
42
+ const d = input instanceof Date ? input : new Date(input);
43
+ if (Number.isNaN(d.getTime())) {
44
+ throw new Error(`toBeijing: invalid time input: ${input}`);
45
+ }
46
+ const p = beijingParts(d);
47
+ return `${p.Y}-${p.M}-${p.D} ${p.h}:${p.m}:${p.s}.${p.ms}`;
48
+ }
49
+ /** Compact Beijing timestamp "YYYYMMDD-HHmmss" (e.g. for sortable upload filenames). */
50
+ export function toBeijingCompact(input = Date.now()) {
51
+ const d = input instanceof Date ? input : new Date(input);
52
+ if (Number.isNaN(d.getTime())) {
53
+ throw new Error(`toBeijingCompact: invalid time input: ${input}`);
54
+ }
55
+ const p = beijingParts(d);
56
+ return `${p.Y}${p.M}${p.D}-${p.h}${p.m}${p.s}`;
57
+ }
58
+ /** Year-month bucket "YYYY-MM" in Beijing time (used for upload directory layout). */
59
+ export function toBeijingYearMonth(input = Date.now()) {
60
+ const d = input instanceof Date ? input : new Date(input);
61
+ if (Number.isNaN(d.getTime())) {
62
+ throw new Error(`toBeijingYearMonth: invalid time input: ${input}`);
63
+ }
64
+ const p = beijingParts(d);
65
+ return `${p.Y}-${p.M}`;
66
+ }
67
+ /**
68
+ * 把一个北京时间字符串平移 deltaMs 毫秒,返回新的北京时间字符串。
69
+ * 用于"上一条消息后 1ms"这种 marker 计算——不依赖服务器时区。
70
+ *
71
+ * 注意:输入必须是北京时间字符串(不是 UTC ISO),否则解析会偏。
72
+ */
73
+ export function shiftBeijing(beijingStr, deltaMs) {
74
+ // 用 +08:00 显式标注时区,让 Date.parse 把字符串当作北京时间读出绝对 instant
75
+ const abs = Date.parse(beijingStr.replace(" ", "T") + "+08:00");
76
+ if (Number.isNaN(abs)) {
77
+ throw new Error(`shiftBeijing: invalid Beijing time string: ${beijingStr}`);
78
+ }
79
+ return toBeijing(abs + deltaMs);
80
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Issue title 自动生成与展示工具。
3
+ *
4
+ * 设计:合并 title/description 后,用户只填一个内容字段(description)。
5
+ * title 从 description 前 N 字符截断生成,用于列表、详情 header 等紧凑展示。
6
+ * 若 description 以 `/plan` 开头,截断后的 title 保留 `/plan` 前缀以便
7
+ * parseSlashCommand 解析;展示侧用 displayTitle 剥掉前缀避免污染。
8
+ */
9
+ export const TITLE_MAX_LENGTH = 40;
10
+ /**
11
+ * 从内容截断生成 title。
12
+ * - 折叠空白为单空格
13
+ * - 超长时优先在最后一个空格处截断,避免词中间断开
14
+ * - 截断后追加 …
15
+ */
16
+ export function truncateTitle(content) {
17
+ const text = (content || "").trim().replace(/\s+/g, " ");
18
+ if (text.length <= TITLE_MAX_LENGTH)
19
+ return text;
20
+ const slice = text.slice(0, TITLE_MAX_LENGTH);
21
+ const lastSpace = slice.lastIndexOf(" ");
22
+ const cut = lastSpace > TITLE_MAX_LENGTH * 0.5 ? slice.slice(0, lastSpace) : slice;
23
+ return cut.trim() + "…";
24
+ }
25
+ /**
26
+ * 展示用 title —— 若 slash_command 为 /plan,剥掉 title 开头的 `/plan ` 前缀。
27
+ * 列表已有 plan 徽标,title 本体无需重复暴露元信息。
28
+ */
29
+ export function displayTitle(issue) {
30
+ if (issue.slash_command === "/plan" && issue.title.startsWith("/plan")) {
31
+ return issue.title.slice("/plan".length).trim() || issue.title;
32
+ }
33
+ return issue.title;
34
+ }
35
+ /**
36
+ * 展示用 description —— 若 slash_command 为 /plan 且 description 以 /plan 开头,
37
+ * 剥掉前缀,避免元信息污染正文。
38
+ */
39
+ export function displayDescription(issue) {
40
+ const desc = issue.description || "";
41
+ if (issue.slash_command === "/plan" &&
42
+ desc.startsWith("/plan")) {
43
+ return desc.slice("/plan".length).trim();
44
+ }
45
+ return desc;
46
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * URL 抽取 + 规范化 —— 链接采集层的基础工具。
3
+ *
4
+ * 用法:从消息正文里抽 URL(markdown [text](url) + 裸 URL),规范化后供 dedup/入库。
5
+ *
6
+ * 设计原则:
7
+ * - 纯函数,无 IO,可单测
8
+ * - 解析失败 / 非 http(s) 直接丢
9
+ * - 规范化去掉追踪参数(utm_* / fbclid / gclid / ref 等)和 hash,host 小写,去 www. 前缀,删末尾 /
10
+ */
11
+ const MD_URL_RE = /\[(?:[^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;
12
+ const BARE_URL_RE = /(https?:\/\/[^\s<>"')]+[^\s<>"').,;:!?])/g;
13
+ /** 从文本抽 URL。返回 [{raw, index}],不去重(同一 URL 多次出现各算一条)。 */
14
+ export function extractUrls(text) {
15
+ if (!text)
16
+ return [];
17
+ const out = [];
18
+ const seen = new Set(); // 防同一位置被两个正则同时命中
19
+ // 1. markdown [text](url) 形式 —— 抓括号内的 url
20
+ for (const m of text.matchAll(MD_URL_RE)) {
21
+ const url = m[1];
22
+ const idx = (m.index ?? 0) + m[0].indexOf(url);
23
+ if (!seen.has(idx)) {
24
+ out.push({ raw: url, index: idx });
25
+ seen.add(idx);
26
+ }
27
+ }
28
+ // 2. 裸 URL —— 剥掉 trailing 标点
29
+ for (const m of text.matchAll(BARE_URL_RE)) {
30
+ const url = m[1];
31
+ const idx = (m.index ?? 0) + (m[0].length - url.length);
32
+ if (!seen.has(idx)) {
33
+ out.push({ raw: url, index: idx });
34
+ seen.add(idx);
35
+ }
36
+ }
37
+ // 按出现位置排序,context snippet 用
38
+ out.sort((a, b) => a.index - b.index);
39
+ return out;
40
+ }
41
+ const TRACKING_PARAMS = new Set([
42
+ "utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content",
43
+ "utm_id", "utm_name",
44
+ "fbclid", "gclid", "gclsrc", "dclid", "msclkid", "yclid",
45
+ "ref", "ref_src", "ref_url",
46
+ "_hsenc", "_hsmi", "hsCtaTracking",
47
+ "mc_cid", "mc_eid",
48
+ "si", // youtube shorts tracking
49
+ ]);
50
+ /**
51
+ * 规范化 URL。
52
+ * 失败(非法 URL / 非 http(s))返回 null,调用方丢弃。
53
+ */
54
+ export function normalizeUrl(raw) {
55
+ let parsed;
56
+ try {
57
+ parsed = new URL(raw);
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
63
+ return null;
64
+ // host:小写 + 去 www.
65
+ const host = parsed.hostname.toLowerCase().replace(/^www\./, "");
66
+ // path:去末尾 /,空 path 留空
67
+ let path = parsed.pathname;
68
+ if (path.length > 1 && path.endsWith("/"))
69
+ path = path.slice(0, -1);
70
+ // percent-encoded 大写一致性(避开大小写差异导致的 dedup miss)
71
+ try {
72
+ path = decodeURIComponent(path);
73
+ }
74
+ catch {
75
+ // decode 失败(乱码)保留原样
76
+ }
77
+ // query:剥追踪参数,剩余按 key 字典序排(dedup 友好)
78
+ const keepParams = [];
79
+ const search = parsed.searchParams;
80
+ if (search.size > 0) {
81
+ for (const [k, v] of search.entries()) {
82
+ if (TRACKING_PARAMS.has(k.toLowerCase()))
83
+ continue;
84
+ keepParams.push(`${encodeURIComponent(k)}=${encodeURIComponent(v)}`);
85
+ }
86
+ keepParams.sort();
87
+ }
88
+ const norm = `https://${host}${path}${keepParams.length > 0 ? `?${keepParams.join("&")}` : ""}`;
89
+ return { raw, norm, host };
90
+ }
91
+ /** 从文本 + URL 位置截 context snippet(url 前后各 100 字,去 newline)。 */
92
+ export function extractContextSnippet(text, urlIndex, urlLen, radius = 100) {
93
+ if (!text)
94
+ return "";
95
+ const start = Math.max(0, urlIndex - radius);
96
+ const end = Math.min(text.length, urlIndex + urlLen + radius);
97
+ const snippet = text.slice(start, end);
98
+ return snippet.replace(/\s+/g, " ").trim();
99
+ }