agentquad 0.4.5 → 0.4.8
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/README.md +4 -3
- package/dist-web/assets/index-DdqC2CwH.css +32 -0
- package/dist-web/assets/{index-By--XlP3.js → index-DkI6ZJx_.js} +411 -399
- package/dist-web/assets/logo-Cxw7XzHl.png +0 -0
- package/dist-web/favicon.png +0 -0
- package/dist-web/index.html +2 -2
- package/package.json +7 -1
- package/src/claude-prompt-detector.js +72 -0
- package/src/cli.js +1 -1
- package/src/codex-hook-installer.js +1 -1
- package/src/codex-prompt-detector.js +104 -13
- package/src/config.js +33 -5
- package/src/db.js +77 -31
- package/src/export/todoMarkdown.js +1 -9
- package/src/lark-bot.js +44 -5
- package/src/mcp/tools/destructive/index.js +22 -16
- package/src/mcp/tools/openclaw/index.js +12 -16
- package/src/mcp/tools/read/index.js +7 -7
- package/src/mcp/tools/write/index.js +9 -6
- package/src/openclaw-bridge.js +176 -28
- package/src/openclaw-hook-installer.js +2 -1
- package/src/openclaw-hook.js +127 -9
- package/src/openclaw-wizard.js +168 -191
- package/src/permission-prompt.js +113 -31
- package/src/prompt-render.js +0 -8
- package/src/pty.js +183 -49
- package/src/routes/ai-terminal.js +90 -26
- package/src/routes/telegram-sync.js +7 -5
- package/src/routes/todos.js +8 -14
- package/src/server.js +90 -12
- package/src/session-input-dispatcher.js +48 -4
- package/src/stats/report.js +1 -6
- package/src/telegram-bot.js +82 -15
- package/src/telegram-loading-status.js +1 -1
- package/src/templates/claude-hooks/notify.js +1 -1
- package/src/templates/codex-hooks/notify.js +1 -1
- package/src/wiki/index.js +1 -1
- package/src/wiki/sources.js +0 -1
- package/dist-web/assets/index-8A0oLLcX.css +0 -32
- package/dist-web/assets/logo-D4DDtU-r.png +0 -0
package/src/openclaw-hook.js
CHANGED
|
@@ -83,6 +83,9 @@ const BOX_HORIZONTAL = /[─━┄┅┈┉═]/g
|
|
|
83
83
|
const BOX_VERTICAL = /[│┃┆┇┊┋║]/g
|
|
84
84
|
const BOX_CORNERS = /[┌┍┎┏┐┑┒┓└┕┖┗┘┙┚┛┌┐└┘╭╮╯╰╓╒╕╖╙╘╛╜╔╗╚╝]/g
|
|
85
85
|
const BOX_TEES = /[├┝┞┟┠┡┢┣┤┥┦┧┨┩┪┫┬┭┮┯┰┱┲┳┴┵┶┷┸┹┺┻┼┽┾┿╀╁╂╃╄╅╆╇╈╉╊╋╠╣╦╩╬]/g
|
|
86
|
+
// Unicode Block Elements (U+2580-259F):▀▁▂▃▄▅▆▇█ ▉▊▋▌▍▎▏ ▐░▒▓▔▕▖▗▘▙▚▛▜▝▞▟
|
|
87
|
+
// Cursor TUI 用这些画状态栏 / 进度条 / 边框,发到 IM 一串看就是黑条。
|
|
88
|
+
const BOX_BLOCK = /[▀-▟]/g
|
|
86
89
|
|
|
87
90
|
function cleanBoxDrawing(s) {
|
|
88
91
|
return String(s || '')
|
|
@@ -90,6 +93,7 @@ function cleanBoxDrawing(s) {
|
|
|
90
93
|
.replace(BOX_VERTICAL, '|') // 竖线 → |
|
|
91
94
|
.replace(BOX_CORNERS, '+') // 角 → +
|
|
92
95
|
.replace(BOX_TEES, '+') // 三叉 → +
|
|
96
|
+
.replace(BOX_BLOCK, '') // 块元素直接删(连成一片就是黑条,没语义)
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
function compactBlankLines(s) {
|
|
@@ -114,6 +118,15 @@ const PROMPT_LINE = /^\s*(❯|⏵|►|→)/
|
|
|
114
118
|
const AUTO_MODE_LINE = /(auto mode (on|off)|shift\+tab to cycle|ctrl\+[a-z]\b)/i
|
|
115
119
|
const BORDER_LINE = /^[\s\-=_|+~]+$/
|
|
116
120
|
|
|
121
|
+
// Cursor TUI 底部状态栏噪声 —— 每次 stop hook 触发时这些行都会出现在 PTY tail,
|
|
122
|
+
// 完全没信息量,发到 IM 是纯刷屏。
|
|
123
|
+
// "Opus 4.6 (Thinking) 200K High · 23.6%" → 模型选择器 + context %
|
|
124
|
+
// "Auto-run" / "Manual" → 模式指示
|
|
125
|
+
// "~/Desktop/code/crazyCombo/quadtodo · main" → cwd · git branch
|
|
126
|
+
const CURSOR_MODEL_LINE = /^\s*(Opus|Sonnet|Haiku|GPT|Claude|Codex|Composer)\s+[\w.-]+.*·\s*[\d.]+%\s*$/i
|
|
127
|
+
const CURSOR_AUTORUN_LINE = /^\s*(Auto-run|Manual|Auto)\s*$/i
|
|
128
|
+
const CURSOR_CWD_BRANCH_LINE = /^\s*~?\/[^\s·]+(?:\/[^\s·]+)*\s+·\s+[\w./-]+\s*$/
|
|
129
|
+
|
|
117
130
|
function isSpinnerOnly(line) {
|
|
118
131
|
// 全部是 spinner 字符(含空格)
|
|
119
132
|
const trimmed = line.replace(/\s+/g, '')
|
|
@@ -130,6 +143,9 @@ function isStatusLine(line) {
|
|
|
130
143
|
if (PROMPT_LINE.test(line)) return true
|
|
131
144
|
if (AUTO_MODE_LINE.test(line)) return true
|
|
132
145
|
if (BORDER_LINE.test(line)) return true
|
|
146
|
+
if (CURSOR_MODEL_LINE.test(line)) return true
|
|
147
|
+
if (CURSOR_AUTORUN_LINE.test(line)) return true
|
|
148
|
+
if (CURSOR_CWD_BRANCH_LINE.test(line)) return true
|
|
133
149
|
return false
|
|
134
150
|
}
|
|
135
151
|
|
|
@@ -444,6 +460,7 @@ export function createOpenClawHookHandler(deps = {}) {
|
|
|
444
460
|
* 处理一条 hook 事件 —— 统一入口,按 source/path 分发。
|
|
445
461
|
* - source=codex,path=jsonl → handleCodexJsonl(Phase C)
|
|
446
462
|
* - source=codex,path=detector → handleCodexDetector(Phase E 占位,目前直接拒绝)
|
|
463
|
+
* - source=claude,path=detector→ handleClaudeDetector(PTY 兜底 Notification hook 不 fire)
|
|
447
464
|
* - 其它(默认 claude) → handleClaude(保留原逻辑,签名不变)
|
|
448
465
|
* 返回 { ok, action: 'sent'|'skipped'|'failed', reason? }
|
|
449
466
|
*/
|
|
@@ -451,6 +468,7 @@ export function createOpenClawHookHandler(deps = {}) {
|
|
|
451
468
|
const source = req?.source || 'claude'
|
|
452
469
|
if (source === 'codex' && req?.path === 'jsonl') return handleCodexJsonl(req)
|
|
453
470
|
if (source === 'codex' && req?.path === 'detector') return handleCodexDetector(req)
|
|
471
|
+
if (source === 'claude' && req?.path === 'detector') return handleClaudeDetector(req)
|
|
454
472
|
return handleClaude(req)
|
|
455
473
|
}
|
|
456
474
|
|
|
@@ -566,12 +584,12 @@ export function createOpenClawHookHandler(deps = {}) {
|
|
|
566
584
|
// bridge.postText 的形参是 `message`,不是 `text`——早期实现写错字段名,
|
|
567
585
|
// 导致 bridge 收到 message=undefined 直接走 message_required 短路返回。
|
|
568
586
|
try {
|
|
569
|
-
const r = await codexBridge.
|
|
587
|
+
const r = await codexBridge.broadcastText({ sessionId: quadtodoSessionId, message: fullText })
|
|
570
588
|
if (!r?.ok) {
|
|
571
|
-
logger.warn?.(`[codex-hook]
|
|
589
|
+
logger.warn?.(`[codex-hook] broadcastText returned not-ok: reason=${r?.reason} detail=${r?.detail || ''}`)
|
|
572
590
|
return { ok: false, reason: 'post_failed', detail: r?.reason }
|
|
573
591
|
}
|
|
574
|
-
logger.info?.(`[codex-hook]
|
|
592
|
+
logger.info?.(`[codex-hook] broadcastText OK sessionId=${quadtodoSessionId} event=${event} len=${fullText.length}`)
|
|
575
593
|
} catch (e) {
|
|
576
594
|
logger.warn?.(`[codex-hook] postText threw: ${e.message}`)
|
|
577
595
|
return { ok: false, reason: 'post_failed', detail: e?.message }
|
|
@@ -628,6 +646,61 @@ export function createOpenClawHookHandler(deps = {}) {
|
|
|
628
646
|
return { ok: true, action: 'sent', source: 'codex', event, nativeId, matchedPattern }
|
|
629
647
|
}
|
|
630
648
|
|
|
649
|
+
// ─── Claude PTY-detector 分支 ────────────────────────────────────────────────
|
|
650
|
+
// Notification hook 不 fire 的兜底(实测 permissions.defaultMode='auto' 时 model
|
|
651
|
+
// classifier 触发的权限框走不到 hook)。两件事:
|
|
652
|
+
// 1) 翻 session.status → pending_confirm(web 端 deriveAiState 据此显示"待确认")
|
|
653
|
+
// 2) 推 IM 权限卡 / 按钮(跟真 Notification 走同一份 cooldown,避免双推)
|
|
654
|
+
// 与 handleCodexDetector 同构,但走 broadcastText(保持跟 Claude 真 Notification 一致),
|
|
655
|
+
// header 标题区别成 "Claude Code 等待你的响应"。
|
|
656
|
+
async function handleClaudeDetector({ event, sessionId, promptText } = {}) {
|
|
657
|
+
if (!sessionId) return { ok: false, reason: 'no_sessionId' }
|
|
658
|
+
const sess = aiTerminal?.sessions?.get(sessionId)
|
|
659
|
+
if (!sess) return { ok: false, reason: 'session_gone' }
|
|
660
|
+
|
|
661
|
+
// 1) 翻状态。markPendingConfirm 默认只接受 running → pending_confirm,但 PTY-detector
|
|
662
|
+
// 要求 anchor + ≥2 数字选项才 emit,假阳性概率极低;显式 allowIdleFlip=true 让它
|
|
663
|
+
// 在 status=idle 时也能翻(覆盖 auto 模式权限框在 Stop hook 后才浮出的场景)。
|
|
664
|
+
try {
|
|
665
|
+
aiTerminal?.markPendingConfirm?.(sessionId, {
|
|
666
|
+
source: 'claude-pty-detector',
|
|
667
|
+
promptText,
|
|
668
|
+
allowIdleFlip: true,
|
|
669
|
+
})
|
|
670
|
+
} catch { /* ignore */ }
|
|
671
|
+
|
|
672
|
+
// 2) IM 推送资格 & cooldown 跟真 Notification 共享 → 不会双推
|
|
673
|
+
if (!isPermissionReminderEligible(sessionId)) {
|
|
674
|
+
return { ok: true, action: 'skipped', reason: 'im_push_not_eligible' }
|
|
675
|
+
}
|
|
676
|
+
const cd = notificationCooldownMs()
|
|
677
|
+
if (cd > 0 && isOnCooldown(sessionId, 'notification', cd)) {
|
|
678
|
+
return { ok: true, action: 'skipped', reason: 'notification_cooldown', cooldownMs: cd }
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// 3) 拼消息:直接用 PTY detector 抽出来的 promptText(已经过 anchor + cleanPtyTail 清洗)
|
|
682
|
+
const todoId = sess.todoId
|
|
683
|
+
let todoTitle = todoId
|
|
684
|
+
try {
|
|
685
|
+
const todo = await db.getTodo?.(todoId)
|
|
686
|
+
todoTitle = todo?.title || todoId
|
|
687
|
+
} catch { /* ignore */ }
|
|
688
|
+
const idTail = todoId ? String(todoId).slice(-3) : '???'
|
|
689
|
+
let message = `[#t${idTail}] 任务「${todoTitle}」\n\n${promptText || '(no prompt text)'}`
|
|
690
|
+
message = buildPermissionNotificationMessage(message)
|
|
691
|
+
const replyMarkup = buildPermissionReplyMarkup(sessionId)
|
|
692
|
+
|
|
693
|
+
let result
|
|
694
|
+
try {
|
|
695
|
+
result = await openclaw.broadcastText({ sessionId, message, replyMarkup })
|
|
696
|
+
} catch (e) {
|
|
697
|
+
logger.warn?.(`[claude-detector] broadcastText failed: ${e.message}`)
|
|
698
|
+
return { ok: false, reason: 'post_failed', detail: e?.message }
|
|
699
|
+
}
|
|
700
|
+
if (result?.ok !== false) recordSent(sessionId, 'notification')
|
|
701
|
+
return { ok: true, action: 'sent', source: 'claude', path: 'detector', event }
|
|
702
|
+
}
|
|
703
|
+
|
|
631
704
|
// ─── Claude 分支(既有实现,原 handle() 主体不变)─────────────────────────────
|
|
632
705
|
async function handleClaude({ event, sessionId, todoId, todoTitle, hookPayload } = {}) {
|
|
633
706
|
if (!event) return { ok: false, action: 'failed', reason: 'event_required' }
|
|
@@ -642,11 +715,56 @@ export function createOpenClawHookHandler(deps = {}) {
|
|
|
642
715
|
logger.warn?.(`[openclaw-hook] hook fired with no registered route: event=${evt} sid=${sessionId} todoId=${todoId || 'null'}`)
|
|
643
716
|
}
|
|
644
717
|
|
|
718
|
+
// 0) user-prompt-submit → echo user's prompt to all IM channels (minus origin)
|
|
719
|
+
if (evt === 'user-prompt-submit') {
|
|
720
|
+
if (!sessionId) return { ok: true, action: 'skipped', reason: 'no_session' }
|
|
721
|
+
const promptRaw =
|
|
722
|
+
(hookPayload && typeof hookPayload === 'object' && (
|
|
723
|
+
hookPayload.user_prompt ||
|
|
724
|
+
hookPayload.prompt ||
|
|
725
|
+
hookPayload.user_message ||
|
|
726
|
+
hookPayload.message
|
|
727
|
+
)) || ''
|
|
728
|
+
const prompt = String(promptRaw).trim()
|
|
729
|
+
if (!prompt) return { ok: true, action: 'skipped', reason: 'empty_prompt' }
|
|
730
|
+
|
|
731
|
+
// 截断:>2000 字符 → 取前 2000 + 末尾标注总字数
|
|
732
|
+
const MAX = 2000
|
|
733
|
+
const truncated = prompt.length > MAX
|
|
734
|
+
? `${prompt.slice(0, MAX)}\n… [共 ${prompt.length} 字]`
|
|
735
|
+
: prompt
|
|
736
|
+
const message = `👤 ${truncated}`
|
|
737
|
+
|
|
738
|
+
let originChannel = null
|
|
739
|
+
try {
|
|
740
|
+
originChannel = sessionInputDispatcher?.consumeOrigin?.(sessionId, prompt) || null
|
|
741
|
+
} catch (e) {
|
|
742
|
+
logger.warn?.(`[openclaw-hook] consumeOrigin threw: ${e.message}`)
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
try {
|
|
746
|
+
await openclaw?.broadcastEcho?.({ sessionId, message, excludeChannel: originChannel })
|
|
747
|
+
} catch (e) {
|
|
748
|
+
logger.warn?.(`[openclaw-hook] broadcastEcho threw: ${e.message}`)
|
|
749
|
+
}
|
|
750
|
+
return { ok: true, action: 'echoed', origin: originChannel, length: prompt.length }
|
|
751
|
+
}
|
|
752
|
+
|
|
645
753
|
// 1) ask_user pending 时 Stop 静默
|
|
646
754
|
if (evt === 'stop' && hasPendingAskUser(sessionId)) {
|
|
647
755
|
return { ok: true, action: 'skipped', reason: 'ask_user_pending' }
|
|
648
756
|
}
|
|
649
757
|
|
|
758
|
+
// 1a) Cursor 的 stop hook 每轮 fire 3 次(实测 13ms 内连发,cursor 内部架构使然)。
|
|
759
|
+
// 给 cursor session 加 5 秒短 cooldown 去重 —— 5s 足够吞掉同一轮内的连发,
|
|
760
|
+
// 又远小于用户两轮对话的最小间隔。Claude/Codex 不受影响(它们 stop 一轮一发)。
|
|
761
|
+
if (evt === 'stop' && sessionId) {
|
|
762
|
+
const sess = aiTerminal?.sessions?.get?.(sessionId)
|
|
763
|
+
if (sess?.tool === 'cursor' && isOnCooldown(sessionId, 'stop', 5_000)) {
|
|
764
|
+
return { ok: true, action: 'skipped', reason: 'cursor_stop_dedup' }
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
650
768
|
const permissionReminderEligible = evt === 'notification' && isPermissionReminderEligible(sessionId)
|
|
651
769
|
|
|
652
770
|
// 注:原来这里会立即 notifyWebTurnDone。已经挪到 ③ 读完 JSONL 之后,
|
|
@@ -833,8 +951,8 @@ export function createOpenClawHookHandler(deps = {}) {
|
|
|
833
951
|
// footer 永远附在最末尾(即使消息被截短到附件也要保留,让用户能看到费用)
|
|
834
952
|
if (usageFooter) message = `${message}\n\n${usageFooter}`
|
|
835
953
|
|
|
836
|
-
// 4) 推送(
|
|
837
|
-
const result = await openclaw.
|
|
954
|
+
// 4) 推送(broadcastText 扇出到 session 所有绑定 channel;接受可选 attachment)
|
|
955
|
+
const result = await openclaw.broadcastText({
|
|
838
956
|
sessionId,
|
|
839
957
|
message,
|
|
840
958
|
attachment: attachmentPath, // bridge 转给 telegramBot.sendDocument
|
|
@@ -843,7 +961,7 @@ export function createOpenClawHookHandler(deps = {}) {
|
|
|
843
961
|
|
|
844
962
|
// 5) SessionEnd 后处理:close topic + 改名 ✅ + 清状态
|
|
845
963
|
if (evt === 'session-end') {
|
|
846
|
-
const route = openclaw.resolveRoute?.(sessionId)
|
|
964
|
+
const route = openclaw.resolveRoute?.(sessionId, 'telegram')
|
|
847
965
|
if (route?.threadId && telegramBot) {
|
|
848
966
|
try {
|
|
849
967
|
await telegramBot.closeForumTopic({ chatId: route.targetUserId, threadId: route.threadId })
|
|
@@ -904,14 +1022,14 @@ export function createOpenClawHookHandler(deps = {}) {
|
|
|
904
1022
|
}
|
|
905
1023
|
// Stop / session-end → 清掉 lark "在思考" reaction(如果是 lark route)
|
|
906
1024
|
if ((evt === 'stop' || evt === 'session-end') && sessionId && larkBot?.clearReactionsForSession) {
|
|
907
|
-
const route = openclaw.resolveRoute?.(sessionId)
|
|
1025
|
+
const route = openclaw.resolveRoute?.(sessionId, 'lark')
|
|
908
1026
|
if (route?.channel === 'lark') {
|
|
909
1027
|
larkBot.clearReactionsForSession(sessionId).catch((e) => logger.warn?.(`[openclaw-hook] clearReactionsForSession failed: ${e.message}`))
|
|
910
1028
|
}
|
|
911
1029
|
}
|
|
912
1030
|
// Stop / session-end → 清掉 telegram "✍" reaction(如果是 telegram route)
|
|
913
1031
|
if ((evt === 'stop' || evt === 'session-end') && sessionId && reactionTracker?.clearReactionsForSession) {
|
|
914
|
-
const route = openclaw.resolveRoute?.(sessionId)
|
|
1032
|
+
const route = openclaw.resolveRoute?.(sessionId, 'telegram')
|
|
915
1033
|
if (route?.channel === 'telegram') {
|
|
916
1034
|
reactionTracker.clearReactionsForSession(sessionId).catch((e) => logger.warn?.(`[openclaw-hook] tg clearReactionsForSession failed: ${e.message}`))
|
|
917
1035
|
}
|
|
@@ -938,4 +1056,4 @@ export function createOpenClawHookHandler(deps = {}) {
|
|
|
938
1056
|
}
|
|
939
1057
|
}
|
|
940
1058
|
|
|
941
|
-
export const __test__ = { buildMessage, shortTodoId }
|
|
1059
|
+
export const __test__ = { buildMessage, shortTodoId, extractTailSnippet }
|