Class: Collavre::Api::V1::Mobile::AgentEventsController
- Inherits:
-
BaseController
- Object
- ActionController::API
- BaseController
- BaseController
- Collavre::Api::V1::Mobile::AgentEventsController
- Defined in:
- app/controllers/collavre/api/v1/mobile/agent_events_controller.rb
Overview
The heart of the loop. GET surfaces pending approvals (+ recent agent replies) with a stable spoken ref number and a decision-ready summary. POST :id/respond branches on the referenced event KIND:
(A) approval/permission comment → the spoken response is a BUTTON
press (allow|deny) → decide_claude_channel_permission! + relay.
(B) ordinary agent message → the spoken response is FREE TEXT passed
verbatim back to the agent as a reply comment (no server parsing).
Constant Summary collapse
- APPROVE =
Spoken decision verbs. The app no longer addresses approvals by ordinal, so a reply to an approval event is just an allow/deny verb; anything else asks for clarification rather than firing a decision.
/(승인|허용|적용|approve|allow|confirm|네|좋아|오케이|\bok\b|\byes\b)/i- DENY =
/(거절|거부|반려|deny|reject|취소|아니|\bno\b)/i
Instance Method Summary collapse
- #index ⇒ Object
-
#read ⇒ Object
The app calls this when it finishes reading a message ALOUD (whether or not the user then replies).
- #respond ⇒ Object
Instance Method Details
#index ⇒ Object
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
# File 'app/controllers/collavre/api/v1/mobile/agent_events_controller.rb', line 21 def index approvals = pending_approvals notices = # Every undecided approval is emitted on every poll — the server keeps # no per-client cursor. Re-speaking is prevented on the CLIENT (an # in-memory "already spoken" set), the same at-least-once shape as # notices: a pending approval keeps surfacing until it is decided # (which clears it from pending_approvals) or the app restarts (when a # still-pending approval SHOULD be re-surfaced). The old `since` cursor # filtered here, but a batch high-water-mark could burn past an approval # whose created_at trailed a newer notice, losing it forever. events = approvals.map do |c| { id: c.id, type: "approval_requested", title: title_for_topic(c.topic_id), summary: summarizer.approval_summary(comment: c, label: label_for_topic(c.topic_id)), speak: true, requires_response: true, topic_id: c.topic_id, created_at: c.created_at.iso8601(6) } end events += notices.map do |c| # The notice stands in for the origin comment it quotes; the app # lists/reads it against the ORIGIN thread and replies route there. origin_topic_id = c.quoted_comment&.topic_id || c.topic_id { id: c.id, type: "agent_reply", title: title_for_topic(origin_topic_id), summary: speakable(c.content), speak: true, requires_response: c.quoted_comment_id.present?, topic_id: origin_topic_id, created_at: c.created_at.iso8601(6) } end render json: events.sort_by { |e| e[:created_at] } end |
#read ⇒ Object
The app calls this when it finishes reading a message ALOUD (whether or not the user then replies). Reading = hearing it, so the inbox read pointer advances and the notice stops surfacing — the SAME read-state the inbox badge uses. A crash before this call leaves the notice unread, so the next poll re-reads it (at-least-once, not the lossy old cursor).
81 82 83 84 85 86 87 88 89 |
# File 'app/controllers/collavre/api/v1/mobile/agent_events_controller.rb', line 81 def read comment = Collavre::Comment.find_by(id: params[:id]) unless comment && (comment) return render json: { error: "Event not found" }, status: :not_found end mark_inbox_read(comment) head :ok end |
#respond ⇒ Object
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'app/controllers/collavre/api/v1/mobile/agent_events_controller.rb', line 59 def respond comment = Collavre::Comment.find_by(id: params[:id]) unless comment && (comment) return render json: { error: "Event not found" }, status: :not_found end # Acting on a notice means the user has heard it — clear it from the # unread inbox set so it isn't read again on the next poll. mark_inbox_read(comment) if comment. decide_on(comment) else relay_free_text(comment) end end |