agentquad 0.3.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 (163) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +318 -0
  3. package/dist-web/assets/index-CMaXwixo.js +1234 -0
  4. package/dist-web/assets/index-DBHApzV1.css +32 -0
  5. package/dist-web/assets/inter-cyrillic-400-normal-HOLc17fK.woff +0 -0
  6. package/dist-web/assets/inter-cyrillic-400-normal-obahsSVq.woff2 +0 -0
  7. package/dist-web/assets/inter-cyrillic-500-normal-BasfLYem.woff2 +0 -0
  8. package/dist-web/assets/inter-cyrillic-500-normal-CxZf_p3X.woff +0 -0
  9. package/dist-web/assets/inter-cyrillic-600-normal-4D_pXhcN.woff +0 -0
  10. package/dist-web/assets/inter-cyrillic-600-normal-CWCymEST.woff2 +0 -0
  11. package/dist-web/assets/inter-cyrillic-700-normal-CjBOestx.woff2 +0 -0
  12. package/dist-web/assets/inter-cyrillic-700-normal-DrXBdSj3.woff +0 -0
  13. package/dist-web/assets/inter-cyrillic-ext-400-normal-BQZuk6qB.woff2 +0 -0
  14. package/dist-web/assets/inter-cyrillic-ext-400-normal-DQukG94-.woff +0 -0
  15. package/dist-web/assets/inter-cyrillic-ext-500-normal-B0yAr1jD.woff2 +0 -0
  16. package/dist-web/assets/inter-cyrillic-ext-500-normal-BmqWE9Dz.woff +0 -0
  17. package/dist-web/assets/inter-cyrillic-ext-600-normal-Bcila6Z-.woff +0 -0
  18. package/dist-web/assets/inter-cyrillic-ext-600-normal-Dfes3d0z.woff2 +0 -0
  19. package/dist-web/assets/inter-cyrillic-ext-700-normal-BjwYoWNd.woff2 +0 -0
  20. package/dist-web/assets/inter-cyrillic-ext-700-normal-LO58E6JB.woff +0 -0
  21. package/dist-web/assets/inter-greek-400-normal-B4URO6DV.woff2 +0 -0
  22. package/dist-web/assets/inter-greek-400-normal-q2sYcFCs.woff +0 -0
  23. package/dist-web/assets/inter-greek-500-normal-BIZE56-Y.woff2 +0 -0
  24. package/dist-web/assets/inter-greek-500-normal-Xzm54t5V.woff +0 -0
  25. package/dist-web/assets/inter-greek-600-normal-BZpKdvQh.woff +0 -0
  26. package/dist-web/assets/inter-greek-600-normal-plRanbMR.woff2 +0 -0
  27. package/dist-web/assets/inter-greek-700-normal-BUv2fZ6O.woff +0 -0
  28. package/dist-web/assets/inter-greek-700-normal-C3JjAnD8.woff2 +0 -0
  29. package/dist-web/assets/inter-greek-ext-400-normal-DGGRlc-M.woff2 +0 -0
  30. package/dist-web/assets/inter-greek-ext-400-normal-KugGGMne.woff +0 -0
  31. package/dist-web/assets/inter-greek-ext-500-normal-2j5mBUwD.woff +0 -0
  32. package/dist-web/assets/inter-greek-ext-500-normal-C4iEst2y.woff2 +0 -0
  33. package/dist-web/assets/inter-greek-ext-600-normal-B8X0CLgF.woff +0 -0
  34. package/dist-web/assets/inter-greek-ext-600-normal-DRtmH8MT.woff2 +0 -0
  35. package/dist-web/assets/inter-greek-ext-700-normal-BoQ6DsYi.woff +0 -0
  36. package/dist-web/assets/inter-greek-ext-700-normal-qfdV9bQt.woff2 +0 -0
  37. package/dist-web/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
  38. package/dist-web/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
  39. package/dist-web/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
  40. package/dist-web/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
  41. package/dist-web/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
  42. package/dist-web/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
  43. package/dist-web/assets/inter-latin-700-normal-BLAVimhd.woff +0 -0
  44. package/dist-web/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
  45. package/dist-web/assets/inter-latin-ext-400-normal-77YHD8bZ.woff +0 -0
  46. package/dist-web/assets/inter-latin-ext-400-normal-C1nco2VV.woff2 +0 -0
  47. package/dist-web/assets/inter-latin-ext-500-normal-BxGbmqWO.woff +0 -0
  48. package/dist-web/assets/inter-latin-ext-500-normal-CV4jyFjo.woff2 +0 -0
  49. package/dist-web/assets/inter-latin-ext-600-normal-CIVaiw4L.woff +0 -0
  50. package/dist-web/assets/inter-latin-ext-600-normal-D2bJ5OIk.woff2 +0 -0
  51. package/dist-web/assets/inter-latin-ext-700-normal-Ca8adRJv.woff2 +0 -0
  52. package/dist-web/assets/inter-latin-ext-700-normal-TidjK2hL.woff +0 -0
  53. package/dist-web/assets/inter-vietnamese-400-normal-Bbgyi5SW.woff +0 -0
  54. package/dist-web/assets/inter-vietnamese-400-normal-DMkecbls.woff2 +0 -0
  55. package/dist-web/assets/inter-vietnamese-500-normal-DOriooB6.woff2 +0 -0
  56. package/dist-web/assets/inter-vietnamese-500-normal-mJboJaSs.woff +0 -0
  57. package/dist-web/assets/inter-vietnamese-600-normal-BuLX-rYi.woff +0 -0
  58. package/dist-web/assets/inter-vietnamese-600-normal-Cc8MFFhd.woff2 +0 -0
  59. package/dist-web/assets/inter-vietnamese-700-normal-BZaoP0fm.woff +0 -0
  60. package/dist-web/assets/inter-vietnamese-700-normal-DlLaEgI2.woff2 +0 -0
  61. package/dist-web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  62. package/dist-web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  63. package/dist-web/assets/jetbrains-mono-cyrillic-700-normal-BWTpRfYl.woff2 +0 -0
  64. package/dist-web/assets/jetbrains-mono-cyrillic-700-normal-CEoEElIJ.woff +0 -0
  65. package/dist-web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  66. package/dist-web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  67. package/dist-web/assets/jetbrains-mono-greek-700-normal-C6CZE3T8.woff2 +0 -0
  68. package/dist-web/assets/jetbrains-mono-greek-700-normal-DEigVDxa.woff +0 -0
  69. package/dist-web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  70. package/dist-web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  71. package/dist-web/assets/jetbrains-mono-latin-700-normal-BYuf6tUa.woff2 +0 -0
  72. package/dist-web/assets/jetbrains-mono-latin-700-normal-D3wTyLJW.woff +0 -0
  73. package/dist-web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  74. package/dist-web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  75. package/dist-web/assets/jetbrains-mono-latin-ext-700-normal-CZipNAKV.woff2 +0 -0
  76. package/dist-web/assets/jetbrains-mono-latin-ext-700-normal-CxPITLHs.woff +0 -0
  77. package/dist-web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  78. package/dist-web/assets/jetbrains-mono-vietnamese-700-normal-BDLVIk2r.woff +0 -0
  79. package/dist-web/assets/logo-D4DDtU-r.png +0 -0
  80. package/dist-web/favicon.png +0 -0
  81. package/dist-web/index.html +14 -0
  82. package/package.json +88 -0
  83. package/src/ask-user-buttons.js +142 -0
  84. package/src/claude-transcript.js +203 -0
  85. package/src/cli.js +1040 -0
  86. package/src/codex-event-emitter.js +111 -0
  87. package/src/codex-prompt-detector.js +53 -0
  88. package/src/codex-sidecar.js +52 -0
  89. package/src/codex-transcript.js +74 -0
  90. package/src/config.js +692 -0
  91. package/src/data/claude-code-commands.json +52 -0
  92. package/src/db.js +1503 -0
  93. package/src/dispatch.js +13 -0
  94. package/src/export/todoMarkdown.js +246 -0
  95. package/src/first-run-wizard.js +82 -0
  96. package/src/git/gitStatus.js +139 -0
  97. package/src/lark-api-client.js +205 -0
  98. package/src/lark-bot.js +510 -0
  99. package/src/lark-card.js +88 -0
  100. package/src/lark-config-service.js +16 -0
  101. package/src/lark-event-client.js +107 -0
  102. package/src/lark-image.js +99 -0
  103. package/src/lark-markdown.js +51 -0
  104. package/src/lark-video.js +163 -0
  105. package/src/mcp/audit.js +34 -0
  106. package/src/mcp/server.js +83 -0
  107. package/src/mcp/tools/destructive/index.js +252 -0
  108. package/src/mcp/tools/openclaw/index.js +405 -0
  109. package/src/mcp/tools/read/index.js +269 -0
  110. package/src/mcp/tools/write/index.js +157 -0
  111. package/src/openclaw-bridge.js +566 -0
  112. package/src/openclaw-hook-installer.js +338 -0
  113. package/src/openclaw-hook.js +908 -0
  114. package/src/openclaw-wizard.js +2442 -0
  115. package/src/pending-questions.js +297 -0
  116. package/src/pricing.js +45 -0
  117. package/src/prompt-render.js +36 -0
  118. package/src/pty.js +992 -0
  119. package/src/routes/ai-terminal.js +1228 -0
  120. package/src/routes/git.js +89 -0
  121. package/src/routes/openclaw-hook.js +67 -0
  122. package/src/routes/openclaw-inbound.js +36 -0
  123. package/src/routes/recurringRules.js +80 -0
  124. package/src/routes/reports.js +50 -0
  125. package/src/routes/search.js +46 -0
  126. package/src/routes/stats.js +31 -0
  127. package/src/routes/telegram-config.js +152 -0
  128. package/src/routes/telegram-sync.js +221 -0
  129. package/src/routes/templates.js +63 -0
  130. package/src/routes/todos.js +649 -0
  131. package/src/routes/transcripts.js +75 -0
  132. package/src/routes/uploads.js +107 -0
  133. package/src/routes/wiki.js +142 -0
  134. package/src/search/fts.js +209 -0
  135. package/src/search/index.js +199 -0
  136. package/src/search/transcripts.js +148 -0
  137. package/src/server.js +1791 -0
  138. package/src/session-input-dispatcher.js +256 -0
  139. package/src/stats/markdown.js +42 -0
  140. package/src/stats/report.js +207 -0
  141. package/src/summarize.js +84 -0
  142. package/src/system-rules.js +52 -0
  143. package/src/telegram-bot.js +875 -0
  144. package/src/telegram-commands.js +149 -0
  145. package/src/telegram-config-service.js +84 -0
  146. package/src/telegram-image.js +95 -0
  147. package/src/telegram-loading-status.js +112 -0
  148. package/src/telegram-markdown.js +82 -0
  149. package/src/telegram-reaction-tracker.js +69 -0
  150. package/src/telegram-video.js +75 -0
  151. package/src/templates/claude-hooks/notify.js +103 -0
  152. package/src/transcript.js +305 -0
  153. package/src/transcripts/blocks.js +56 -0
  154. package/src/transcripts/index.js +222 -0
  155. package/src/transcripts/indexer.js +34 -0
  156. package/src/transcripts/matcher.js +70 -0
  157. package/src/transcripts/scanner.js +259 -0
  158. package/src/usage-footer.js +170 -0
  159. package/src/usage-parser.js +132 -0
  160. package/src/wiki/guide.js +44 -0
  161. package/src/wiki/index.js +232 -0
  162. package/src/wiki/redact.js +34 -0
  163. package/src/wiki/sources.js +122 -0
@@ -0,0 +1,89 @@
1
+ import { Router } from 'express'
2
+ import { resolve, isAbsolute } from 'node:path'
3
+ import { readGitStatus, readGitDiff } from '../git/gitStatus.js'
4
+
5
+ export function createGitRouter() {
6
+ const cache = new Map()
7
+
8
+ function invalidate(workDir) {
9
+ if (!workDir) return
10
+ cache.delete(resolve(workDir))
11
+ }
12
+
13
+ function validateAbsolute(workDir) {
14
+ if (!workDir || typeof workDir !== 'string') return null
15
+ if (!isAbsolute(workDir)) return null
16
+ return resolve(workDir)
17
+ }
18
+
19
+ async function computeStatus(key) {
20
+ const status = await readGitStatus(key)
21
+ const entry = { status, timestamp: Date.now(), inflight: null }
22
+ cache.set(key, entry)
23
+ return entry
24
+ }
25
+
26
+ async function getOrComputeStatus(key) {
27
+ const existing = cache.get(key)
28
+ if (existing) {
29
+ if (existing.inflight) {
30
+ const status = await existing.inflight
31
+ return cache.get(key) || { status, timestamp: Date.now(), inflight: null }
32
+ }
33
+ return existing
34
+ }
35
+ const placeholder = { status: null, timestamp: 0, inflight: null }
36
+ placeholder.inflight = readGitStatus(key)
37
+ cache.set(key, placeholder)
38
+ const status = await placeholder.inflight
39
+ const entry = { status, timestamp: Date.now(), inflight: null }
40
+ cache.set(key, entry)
41
+ return entry
42
+ }
43
+
44
+ const router = Router()
45
+
46
+ router.get('/status', async (req, res) => {
47
+ const key = validateAbsolute(req.query.workDir)
48
+ if (!key) {
49
+ res.status(400).json({ ok: false, error: 'bad_request' })
50
+ return
51
+ }
52
+ try {
53
+ const entry = await getOrComputeStatus(key)
54
+ res.json({ ok: true, status: entry.status, timestamp: entry.timestamp })
55
+ } catch (e) {
56
+ res.status(500).json({ ok: false, error: e?.message || 'internal_error' })
57
+ }
58
+ })
59
+
60
+ router.post('/refresh', async (req, res) => {
61
+ const key = validateAbsolute(req.body?.workDir)
62
+ if (!key) {
63
+ res.status(400).json({ ok: false, error: 'bad_request' })
64
+ return
65
+ }
66
+ try {
67
+ const entry = await computeStatus(key)
68
+ res.json({ ok: true, status: entry.status, timestamp: entry.timestamp })
69
+ } catch (e) {
70
+ res.status(500).json({ ok: false, error: e?.message || 'internal_error' })
71
+ }
72
+ })
73
+
74
+ router.get('/diff', async (req, res) => {
75
+ const key = validateAbsolute(req.query.workDir)
76
+ if (!key) {
77
+ res.status(400).json({ ok: false, error: 'bad_request' })
78
+ return
79
+ }
80
+ try {
81
+ const diff = await readGitDiff(key)
82
+ res.json({ ok: true, diff })
83
+ } catch (e) {
84
+ res.status(500).json({ ok: false, error: e?.message || 'internal_error' })
85
+ }
86
+ })
87
+
88
+ return { router, invalidate }
89
+ }
@@ -0,0 +1,67 @@
1
+ import { Router } from 'express'
2
+
3
+ /**
4
+ * POST /api/openclaw/hook
5
+ * body: { source?, path?, event, sessionId, targetUserId?, todoId?, todoTitle?, hookPayload?,
6
+ * nativeId?, transcript_path?, cwd?, raw_event_payload?, promptText?, matchedPattern? }
7
+ *
8
+ * Claude Code hook 脚本(~/.agentquad/claude-hooks/notify.js)调用此端点(默认 source=claude)。
9
+ * Codex 事件来源(jsonl emitter / detector)也走同一端点,通过 source/path 字段区分。
10
+ * 端到端逻辑都委托给 openclaw-hook handler,路由层只做 body 校验与字段转发。
11
+ */
12
+ export function createOpenClawHookRouter({ hookHandler } = {}) {
13
+ if (!hookHandler) throw new Error('hookHandler required')
14
+ const router = Router()
15
+
16
+ router.post('/', async (req, res) => {
17
+ try {
18
+ const {
19
+ source = 'claude',
20
+ path = null,
21
+ event,
22
+ sessionId,
23
+ nativeId,
24
+ targetUserId,
25
+ todoId,
26
+ todoTitle,
27
+ hookPayload,
28
+ transcript_path,
29
+ cwd,
30
+ raw_event_payload,
31
+ promptText,
32
+ matchedPattern,
33
+ } = req.body || {}
34
+
35
+ if (source === 'codex' && path !== 'jsonl' && path !== 'detector') {
36
+ return res.status(400).json({ ok: false, error: 'unsupported_body_shape' })
37
+ }
38
+
39
+ if (!event || typeof event !== 'string') {
40
+ return res.status(400).json({ ok: false, error: 'event_required' })
41
+ }
42
+
43
+ const result = await hookHandler.handle({
44
+ source,
45
+ path,
46
+ event,
47
+ sessionId: sessionId || null,
48
+ nativeId: nativeId || null,
49
+ todoId: todoId || null,
50
+ todoTitle: todoTitle || null,
51
+ targetUserId: targetUserId || null,
52
+ hookPayload: hookPayload || null,
53
+ transcript_path: transcript_path || null,
54
+ cwd: cwd || null,
55
+ raw_event_payload: raw_event_payload || null,
56
+ promptText: promptText || null,
57
+ matchedPattern: matchedPattern || null,
58
+ })
59
+
60
+ return res.json({ ok: result.ok, ...result })
61
+ } catch (e) {
62
+ return res.status(500).json({ ok: false, error: e?.message || 'hook_handle_failed' })
63
+ }
64
+ })
65
+
66
+ return router
67
+ }
@@ -0,0 +1,36 @@
1
+ import { Router } from 'express'
2
+
3
+ /**
4
+ * POST /api/openclaw/inbound
5
+ * body: { from: string, text: string }
6
+ *
7
+ * OpenClaw skill 把每条用户微信消息转发到这里,由 wizard 状态机
8
+ * 自己决定怎么响应。返回 { reply: string, action?: string, ... },
9
+ * skill 把 reply 转发给用户即可。
10
+ */
11
+ export function createOpenClawInboundRouter({ wizard } = {}) {
12
+ if (!wizard) throw new Error('wizard required')
13
+ const router = Router()
14
+
15
+ router.post('/', async (req, res) => {
16
+ try {
17
+ const { from, text } = req.body || {}
18
+ if (!from || typeof from !== 'string') {
19
+ return res.status(400).json({ ok: false, error: 'from_required' })
20
+ }
21
+ if (!text || typeof text !== 'string') {
22
+ return res.status(400).json({ ok: false, error: 'text_required' })
23
+ }
24
+ const result = await wizard.handleInbound({ peer: from, text })
25
+ return res.json({ ok: true, ...result })
26
+ } catch (e) {
27
+ return res.status(500).json({ ok: false, error: e?.message || 'inbound_failed' })
28
+ }
29
+ })
30
+
31
+ router.get('/state', (_req, res) => {
32
+ return res.json({ ok: true, ...wizard.describe() })
33
+ })
34
+
35
+ return router
36
+ }
@@ -0,0 +1,80 @@
1
+ import { Router } from 'express'
2
+
3
+ const USER_ERRORS = new Set([
4
+ 'invalid_frequency',
5
+ 'weekdays_required',
6
+ 'month_days_required',
7
+ 'invalid_quadrant',
8
+ 'title_required',
9
+ ])
10
+
11
+ function handleError(res, e) {
12
+ if (USER_ERRORS.has(e?.message)) {
13
+ res.status(400).json({ ok: false, error: e.message })
14
+ return
15
+ }
16
+ res.status(500).json({ ok: false, error: e?.message || 'internal_error' })
17
+ }
18
+
19
+ export function createRecurringRulesRouter({ db }) {
20
+ const router = Router()
21
+
22
+ router.get('/:id', (req, res) => {
23
+ try {
24
+ const rule = db.getRecurringRule(req.params.id)
25
+ if (!rule) {
26
+ res.status(404).json({ ok: false, error: 'not_found' })
27
+ return
28
+ }
29
+ res.json({ ok: true, rule })
30
+ } catch (e) {
31
+ handleError(res, e)
32
+ }
33
+ })
34
+
35
+ router.post('/', (req, res) => {
36
+ try {
37
+ const result = db.createRecurringRule(req.body || {})
38
+ res.json({ ok: true, ...result })
39
+ } catch (e) {
40
+ handleError(res, e)
41
+ }
42
+ })
43
+
44
+ router.put('/:id', (req, res) => {
45
+ try {
46
+ const rule = db.updateRecurringRule(req.params.id, req.body || {})
47
+ if (!rule) {
48
+ res.status(404).json({ ok: false, error: 'not_found' })
49
+ return
50
+ }
51
+ res.json({ ok: true, rule })
52
+ } catch (e) {
53
+ handleError(res, e)
54
+ }
55
+ })
56
+
57
+ router.post('/:id/deactivate', (req, res) => {
58
+ try {
59
+ const rule = db.setRecurringRuleActive(req.params.id, false)
60
+ if (!rule) {
61
+ res.status(404).json({ ok: false, error: 'not_found' })
62
+ return
63
+ }
64
+ res.json({ ok: true, rule })
65
+ } catch (e) {
66
+ handleError(res, e)
67
+ }
68
+ })
69
+
70
+ router.delete('/:id', (req, res) => {
71
+ try {
72
+ db.deleteRecurringRule(req.params.id)
73
+ res.json({ ok: true })
74
+ } catch (e) {
75
+ handleError(res, e)
76
+ }
77
+ })
78
+
79
+ return router
80
+ }
@@ -0,0 +1,50 @@
1
+ import express from 'express'
2
+
3
+ // 把时间戳按"本地日期"分桶(key 形如 '2026-04-22')
4
+ function localDateKey(ts) {
5
+ const d = new Date(ts)
6
+ const y = d.getFullYear()
7
+ const m = String(d.getMonth() + 1).padStart(2, '0')
8
+ const day = String(d.getDate()).padStart(2, '0')
9
+ return `${y}-${m}-${day}`
10
+ }
11
+
12
+ export function createReportsRouter({ db }) {
13
+ const router = express.Router()
14
+
15
+ router.get('/done', (req, res) => {
16
+ try {
17
+ const since = Number(req.query.since)
18
+ const until = Number(req.query.until)
19
+ if (!Number.isFinite(since) || !Number.isFinite(until) || since >= until) {
20
+ res.status(400).json({ ok: false, error: 'invalid_range' })
21
+ return
22
+ }
23
+
24
+ const list = db.listCompletedTodos({ since, until })
25
+ const byDay = new Map()
26
+ for (const t of list) {
27
+ const key = localDateKey(t.completedAt)
28
+ byDay.set(key, (byDay.get(key) || 0) + 1)
29
+ }
30
+ const dailyCounts = [...byDay.entries()]
31
+ .map(([date, count]) => ({ date, count }))
32
+ .sort((a, b) => (a.date < b.date ? 1 : -1))
33
+
34
+ const missedCount = db.countMissedInRange({ since, until })
35
+
36
+ res.json({
37
+ ok: true,
38
+ range: { since, until },
39
+ list,
40
+ dailyCounts,
41
+ missedCount,
42
+ total: list.length,
43
+ })
44
+ } catch (e) {
45
+ res.status(500).json({ ok: false, error: e.message })
46
+ }
47
+ })
48
+
49
+ return router
50
+ }
@@ -0,0 +1,46 @@
1
+ import { Router } from 'express'
2
+
3
+ /**
4
+ * 统一搜索 HTTP 端点。
5
+ *
6
+ * GET /api/search?q=...&scopes=todos,comments,wiki,ai_sessions&includeArchived=false&limit=20
7
+ *
8
+ * 返回:
9
+ * {
10
+ * ok: true,
11
+ * total: 37,
12
+ * results: [{ scope, todoId, todoTitle, snippet, score, ...scope-specific-ids }, ...]
13
+ * }
14
+ */
15
+ export function createSearchRouter({ searchService } = {}) {
16
+ if (!searchService || typeof searchService.search !== 'function') {
17
+ throw new Error('searchService_required')
18
+ }
19
+ const router = Router()
20
+
21
+ router.get('/', (req, res) => {
22
+ try {
23
+ const q = String(req.query.q || '').trim()
24
+ if (!q) {
25
+ res.status(400).json({ ok: false, error: 'query_required' })
26
+ return
27
+ }
28
+ let scopes
29
+ const rawScopes = req.query.scopes
30
+ if (typeof rawScopes === 'string' && rawScopes.length) {
31
+ scopes = rawScopes.split(',').map((s) => s.trim()).filter(Boolean)
32
+ }
33
+ const includeArchived =
34
+ req.query.includeArchived === 'true' || req.query.includeArchived === '1'
35
+ const limit = req.query.limit == null ? undefined : Number(req.query.limit)
36
+ const out = searchService.search({ query: q, scopes, includeArchived, limit })
37
+ res.json({ ok: true, ...out })
38
+ } catch (e) {
39
+ const msg = e?.message || String(e)
40
+ const status = msg === 'query_required' ? 400 : 500
41
+ res.status(status).json({ ok: false, error: msg })
42
+ }
43
+ })
44
+
45
+ return router
46
+ }
@@ -0,0 +1,31 @@
1
+ import express from 'express'
2
+ import { buildReport } from '../stats/report.js'
3
+ import { renderMarkdown } from '../stats/markdown.js'
4
+
5
+ export function createStatsRouter({ db, getPricing }) {
6
+ const router = express.Router()
7
+
8
+ function parseRange(req) {
9
+ const s = Number(req.query.since)
10
+ const u = Number(req.query.until)
11
+ if (!Number.isFinite(s) || !Number.isFinite(u) || s >= u) return null
12
+ return { since: s, until: u }
13
+ }
14
+
15
+ router.get('/report', (req, res) => {
16
+ const range = parseRange(req)
17
+ if (!range) return res.status(400).json({ ok: false, error: 'invalid_range' })
18
+ const report = buildReport(db, { ...range, pricing: getPricing() })
19
+ res.json({ ok: true, report })
20
+ })
21
+
22
+ router.get('/report.md', (req, res) => {
23
+ const range = parseRange(req)
24
+ if (!range) return res.status(400).send('invalid range')
25
+ const report = buildReport(db, { ...range, pricing: getPricing() })
26
+ res.set('Content-Type', 'text/markdown; charset=utf-8')
27
+ res.send(renderMarkdown(report))
28
+ })
29
+
30
+ return router
31
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * /api/config/telegram/* 路由:
3
+ * POST /test —— getMe 连通性测试
4
+ * POST /probe-chat-id —— 启动一个 probe 窗口(Task 9 添加)
5
+ * GET /probe-chat-id/stream —— SSE 实时推命中(Task 9 添加)
6
+ *
7
+ * 依赖:
8
+ * - getConfig: () => 当前配置
9
+ * - getTelegramBot: () => 当前 telegramBot 实例(可能 null,比如 enabled=false)
10
+ * - probeRegistry: createProbeRegistry() 返回的对象(Task 9 用)
11
+ */
12
+ import { Router } from 'express'
13
+ import { isMaskedToken } from '../telegram-config-service.js'
14
+ import * as telegramBot from '../telegram-bot.js'
15
+
16
+ const { readBotTokenWithSource } = telegramBot
17
+
18
+ const TELEGRAM_API = 'https://api.telegram.org'
19
+
20
+ async function getMeWithToken(token, fetchFn) {
21
+ const res = await fetchFn(`${TELEGRAM_API}/bot${token}/getMe`, {
22
+ method: 'POST',
23
+ headers: { 'Content-Type': 'application/json' },
24
+ body: JSON.stringify({}),
25
+ })
26
+ const data = await res.json().catch(() => null)
27
+ if (!res.ok || !data?.ok) throw new Error(data?.description || `HTTP ${res.status}`)
28
+ return data.result
29
+ }
30
+
31
+ // Node 的 undici fetch 失败时 e.message 只有光秃秃的 "fetch failed",真正的网络
32
+ // 原因藏在 e.cause.message。前端只展示 e.message 没法排查(用户截图就是这个症状)。
33
+ function describeFetchError(e) {
34
+ const base = e?.message || 'unknown'
35
+ const causeMsg = e?.cause?.message
36
+ if (causeMsg && causeMsg !== base) return `${base}: ${causeMsg}`
37
+ return base
38
+ }
39
+
40
+ export function createTelegramConfigRouter({ getConfig, getTelegramBot, probeRegistry, fetchFn }) {
41
+ if (typeof getConfig !== 'function') throw new Error('getConfig required')
42
+ if (typeof getTelegramBot !== 'function') throw new Error('getTelegramBot required')
43
+
44
+ // 没注入 fetchFn 时按需取 proxy-aware fetcher,跟 telegram-bot.js 一致;
45
+ // 这样设置页"测试"按钮和 bot 长轮询走同一条出口,不会一个能连一个 fetch failed。
46
+ // 每次请求都 resolve 一遍:让 HTTPS_PROXY 改了之后不用重启 AgentQuad。
47
+ const resolveFetch = fetchFn
48
+ ? async () => fetchFn
49
+ : () => telegramBot.getProxyFetch()
50
+
51
+ const router = Router()
52
+
53
+ // POST /test —— getMe 探测
54
+ router.post('/test', async (req, res) => {
55
+ const inputToken = typeof req.body?.botToken === 'string' ? req.body.botToken.trim() : ''
56
+ if (inputToken && !isMaskedToken(inputToken)) {
57
+ try {
58
+ const f = await resolveFetch()
59
+ const me = await getMeWithToken(inputToken, f)
60
+ return res.json({
61
+ ok: true,
62
+ botId: me.id,
63
+ botUsername: me.username || null,
64
+ botFirstName: me.first_name || null,
65
+ source: 'input',
66
+ })
67
+ } catch (e) {
68
+ return res.json({ ok: false, errorReason: describeFetchError(e), source: 'input' })
69
+ }
70
+ }
71
+
72
+ const { token, source } = readBotTokenWithSource(getConfig)
73
+ if (!token) {
74
+ return res.json({ ok: false, errorReason: 'token_missing', source })
75
+ }
76
+ try {
77
+ const f = await resolveFetch()
78
+ const me = await getMeWithToken(token, f)
79
+ res.json({
80
+ ok: true,
81
+ botId: me.id,
82
+ botUsername: me.username || null,
83
+ botFirstName: me.first_name || null,
84
+ source,
85
+ })
86
+ } catch (e) {
87
+ res.json({ ok: false, errorReason: describeFetchError(e), source })
88
+ }
89
+ })
90
+
91
+ // POST /probe-chat-id —— 启动 probe 窗口
92
+ router.post('/probe-chat-id', (req, res) => {
93
+ if (!probeRegistry) {
94
+ return res.status(500).json({ ok: false, reason: 'no_registry' })
95
+ }
96
+ const bot = getTelegramBot()
97
+ if (!bot || typeof bot.setProbeListener !== 'function') {
98
+ return res.json({ ok: false, reason: 'bot_not_running' })
99
+ }
100
+ const r = probeRegistry.startProbe(Number(req.body?.durationSec) || 60)
101
+ if (!r.ok) {
102
+ return res.json({ ok: false, reason: r.reason })
103
+ }
104
+ bot.setProbeListener((hit) => probeRegistry.record(hit))
105
+ res.json({ ok: true, durationSec: r.durationSec, expiresAt: r.expiresAt })
106
+ })
107
+
108
+ // GET /probe-chat-id/stream —— SSE 推命中
109
+ router.get('/probe-chat-id/stream', (req, res) => {
110
+ if (!probeRegistry) {
111
+ return res.status(500).json({ ok: false, reason: 'no_registry' })
112
+ }
113
+ res.setHeader('Content-Type', 'text/event-stream')
114
+ res.setHeader('Cache-Control', 'no-cache')
115
+ res.setHeader('Connection', 'keep-alive')
116
+ res.flushHeaders()
117
+
118
+ // 立即把 snapshot 推一遍(让重连客户端看到已有 hits)
119
+ for (const hit of probeRegistry.snapshot().hits) {
120
+ res.write(`data: ${JSON.stringify(hit)}\n\n`)
121
+ }
122
+
123
+ const unsub = probeRegistry.subscribe((hit) => {
124
+ if (hit === null) {
125
+ res.write(`event: done\ndata: {}\n\n`)
126
+ res.end()
127
+ return
128
+ }
129
+ res.write(`data: ${JSON.stringify(hit)}\n\n`)
130
+ })
131
+
132
+ // 每 25 秒发个 ping 防止反向代理掐
133
+ const pingInterval = setInterval(() => {
134
+ try { res.write(`: ping\n\n`) } catch {}
135
+ }, 25_000)
136
+
137
+ req.on('close', () => {
138
+ clearInterval(pingInterval)
139
+ unsub()
140
+ })
141
+ })
142
+
143
+ // POST /probe-chat-id/stop —— 主动停
144
+ router.post('/probe-chat-id/stop', (_req, res) => {
145
+ const bot = getTelegramBot()
146
+ if (bot && typeof bot.setProbeListener === 'function') bot.setProbeListener(null)
147
+ if (probeRegistry) probeRegistry.stopProbe()
148
+ res.json({ ok: true })
149
+ })
150
+
151
+ return router
152
+ }