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.
- package/LICENSE +21 -0
- package/README.md +318 -0
- package/dist-web/assets/index-CMaXwixo.js +1234 -0
- package/dist-web/assets/index-DBHApzV1.css +32 -0
- package/dist-web/assets/inter-cyrillic-400-normal-HOLc17fK.woff +0 -0
- package/dist-web/assets/inter-cyrillic-400-normal-obahsSVq.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-500-normal-BasfLYem.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-500-normal-CxZf_p3X.woff +0 -0
- package/dist-web/assets/inter-cyrillic-600-normal-4D_pXhcN.woff +0 -0
- package/dist-web/assets/inter-cyrillic-600-normal-CWCymEST.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-700-normal-CjBOestx.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-700-normal-DrXBdSj3.woff +0 -0
- package/dist-web/assets/inter-cyrillic-ext-400-normal-BQZuk6qB.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-ext-400-normal-DQukG94-.woff +0 -0
- package/dist-web/assets/inter-cyrillic-ext-500-normal-B0yAr1jD.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-ext-500-normal-BmqWE9Dz.woff +0 -0
- package/dist-web/assets/inter-cyrillic-ext-600-normal-Bcila6Z-.woff +0 -0
- package/dist-web/assets/inter-cyrillic-ext-600-normal-Dfes3d0z.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-ext-700-normal-BjwYoWNd.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-ext-700-normal-LO58E6JB.woff +0 -0
- package/dist-web/assets/inter-greek-400-normal-B4URO6DV.woff2 +0 -0
- package/dist-web/assets/inter-greek-400-normal-q2sYcFCs.woff +0 -0
- package/dist-web/assets/inter-greek-500-normal-BIZE56-Y.woff2 +0 -0
- package/dist-web/assets/inter-greek-500-normal-Xzm54t5V.woff +0 -0
- package/dist-web/assets/inter-greek-600-normal-BZpKdvQh.woff +0 -0
- package/dist-web/assets/inter-greek-600-normal-plRanbMR.woff2 +0 -0
- package/dist-web/assets/inter-greek-700-normal-BUv2fZ6O.woff +0 -0
- package/dist-web/assets/inter-greek-700-normal-C3JjAnD8.woff2 +0 -0
- package/dist-web/assets/inter-greek-ext-400-normal-DGGRlc-M.woff2 +0 -0
- package/dist-web/assets/inter-greek-ext-400-normal-KugGGMne.woff +0 -0
- package/dist-web/assets/inter-greek-ext-500-normal-2j5mBUwD.woff +0 -0
- package/dist-web/assets/inter-greek-ext-500-normal-C4iEst2y.woff2 +0 -0
- package/dist-web/assets/inter-greek-ext-600-normal-B8X0CLgF.woff +0 -0
- package/dist-web/assets/inter-greek-ext-600-normal-DRtmH8MT.woff2 +0 -0
- package/dist-web/assets/inter-greek-ext-700-normal-BoQ6DsYi.woff +0 -0
- package/dist-web/assets/inter-greek-ext-700-normal-qfdV9bQt.woff2 +0 -0
- package/dist-web/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
- package/dist-web/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
- package/dist-web/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
- package/dist-web/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
- package/dist-web/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
- package/dist-web/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
- package/dist-web/assets/inter-latin-700-normal-BLAVimhd.woff +0 -0
- package/dist-web/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
- package/dist-web/assets/inter-latin-ext-400-normal-77YHD8bZ.woff +0 -0
- package/dist-web/assets/inter-latin-ext-400-normal-C1nco2VV.woff2 +0 -0
- package/dist-web/assets/inter-latin-ext-500-normal-BxGbmqWO.woff +0 -0
- package/dist-web/assets/inter-latin-ext-500-normal-CV4jyFjo.woff2 +0 -0
- package/dist-web/assets/inter-latin-ext-600-normal-CIVaiw4L.woff +0 -0
- package/dist-web/assets/inter-latin-ext-600-normal-D2bJ5OIk.woff2 +0 -0
- package/dist-web/assets/inter-latin-ext-700-normal-Ca8adRJv.woff2 +0 -0
- package/dist-web/assets/inter-latin-ext-700-normal-TidjK2hL.woff +0 -0
- package/dist-web/assets/inter-vietnamese-400-normal-Bbgyi5SW.woff +0 -0
- package/dist-web/assets/inter-vietnamese-400-normal-DMkecbls.woff2 +0 -0
- package/dist-web/assets/inter-vietnamese-500-normal-DOriooB6.woff2 +0 -0
- package/dist-web/assets/inter-vietnamese-500-normal-mJboJaSs.woff +0 -0
- package/dist-web/assets/inter-vietnamese-600-normal-BuLX-rYi.woff +0 -0
- package/dist-web/assets/inter-vietnamese-600-normal-Cc8MFFhd.woff2 +0 -0
- package/dist-web/assets/inter-vietnamese-700-normal-BZaoP0fm.woff +0 -0
- package/dist-web/assets/inter-vietnamese-700-normal-DlLaEgI2.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist-web/assets/jetbrains-mono-cyrillic-700-normal-BWTpRfYl.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-cyrillic-700-normal-CEoEElIJ.woff +0 -0
- package/dist-web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist-web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-greek-700-normal-C6CZE3T8.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-greek-700-normal-DEigVDxa.woff +0 -0
- package/dist-web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist-web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-latin-700-normal-BYuf6tUa.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-latin-700-normal-D3wTyLJW.woff +0 -0
- package/dist-web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist-web/assets/jetbrains-mono-latin-ext-700-normal-CZipNAKV.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-latin-ext-700-normal-CxPITLHs.woff +0 -0
- package/dist-web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist-web/assets/jetbrains-mono-vietnamese-700-normal-BDLVIk2r.woff +0 -0
- package/dist-web/assets/logo-D4DDtU-r.png +0 -0
- package/dist-web/favicon.png +0 -0
- package/dist-web/index.html +14 -0
- package/package.json +88 -0
- package/src/ask-user-buttons.js +142 -0
- package/src/claude-transcript.js +203 -0
- package/src/cli.js +1040 -0
- package/src/codex-event-emitter.js +111 -0
- package/src/codex-prompt-detector.js +53 -0
- package/src/codex-sidecar.js +52 -0
- package/src/codex-transcript.js +74 -0
- package/src/config.js +692 -0
- package/src/data/claude-code-commands.json +52 -0
- package/src/db.js +1503 -0
- package/src/dispatch.js +13 -0
- package/src/export/todoMarkdown.js +246 -0
- package/src/first-run-wizard.js +82 -0
- package/src/git/gitStatus.js +139 -0
- package/src/lark-api-client.js +205 -0
- package/src/lark-bot.js +510 -0
- package/src/lark-card.js +88 -0
- package/src/lark-config-service.js +16 -0
- package/src/lark-event-client.js +107 -0
- package/src/lark-image.js +99 -0
- package/src/lark-markdown.js +51 -0
- package/src/lark-video.js +163 -0
- package/src/mcp/audit.js +34 -0
- package/src/mcp/server.js +83 -0
- package/src/mcp/tools/destructive/index.js +252 -0
- package/src/mcp/tools/openclaw/index.js +405 -0
- package/src/mcp/tools/read/index.js +269 -0
- package/src/mcp/tools/write/index.js +157 -0
- package/src/openclaw-bridge.js +566 -0
- package/src/openclaw-hook-installer.js +338 -0
- package/src/openclaw-hook.js +908 -0
- package/src/openclaw-wizard.js +2442 -0
- package/src/pending-questions.js +297 -0
- package/src/pricing.js +45 -0
- package/src/prompt-render.js +36 -0
- package/src/pty.js +992 -0
- package/src/routes/ai-terminal.js +1228 -0
- package/src/routes/git.js +89 -0
- package/src/routes/openclaw-hook.js +67 -0
- package/src/routes/openclaw-inbound.js +36 -0
- package/src/routes/recurringRules.js +80 -0
- package/src/routes/reports.js +50 -0
- package/src/routes/search.js +46 -0
- package/src/routes/stats.js +31 -0
- package/src/routes/telegram-config.js +152 -0
- package/src/routes/telegram-sync.js +221 -0
- package/src/routes/templates.js +63 -0
- package/src/routes/todos.js +649 -0
- package/src/routes/transcripts.js +75 -0
- package/src/routes/uploads.js +107 -0
- package/src/routes/wiki.js +142 -0
- package/src/search/fts.js +209 -0
- package/src/search/index.js +199 -0
- package/src/search/transcripts.js +148 -0
- package/src/server.js +1791 -0
- package/src/session-input-dispatcher.js +256 -0
- package/src/stats/markdown.js +42 -0
- package/src/stats/report.js +207 -0
- package/src/summarize.js +84 -0
- package/src/system-rules.js +52 -0
- package/src/telegram-bot.js +875 -0
- package/src/telegram-commands.js +149 -0
- package/src/telegram-config-service.js +84 -0
- package/src/telegram-image.js +95 -0
- package/src/telegram-loading-status.js +112 -0
- package/src/telegram-markdown.js +82 -0
- package/src/telegram-reaction-tracker.js +69 -0
- package/src/telegram-video.js +75 -0
- package/src/templates/claude-hooks/notify.js +103 -0
- package/src/transcript.js +305 -0
- package/src/transcripts/blocks.js +56 -0
- package/src/transcripts/index.js +222 -0
- package/src/transcripts/indexer.js +34 -0
- package/src/transcripts/matcher.js +70 -0
- package/src/transcripts/scanner.js +259 -0
- package/src/usage-footer.js +170 -0
- package/src/usage-parser.js +132 -0
- package/src/wiki/guide.js +44 -0
- package/src/wiki/index.js +232 -0
- package/src/wiki/redact.js +34 -0
- 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
|
+
}
|