Skip to content

feat(line): LINE Messaging API channel adapter#715

Open
stanleykao72 wants to merge 4 commits intonextlevelbuilder:mainfrom
stanleykao72:feat/line-channel
Open

feat(line): LINE Messaging API channel adapter#715
stanleykao72 wants to merge 4 commits intonextlevelbuilder:mainfrom
stanleykao72:feat/line-channel

Conversation

@stanleykao72
Copy link
Copy Markdown

Summary

  • New LINE Messaging API channel adapter — webhook-based, using official Go SDK (v7.21.0)
  • Implements Channel + WebhookChannel interfaces, mounts at /webhook/line
  • Reply API priority with Push API fallback (25s threshold)
  • Loading Animation support via raw HTTP (SDK doesn't expose it)

New files

File Purpose
internal/channels/line/factory.go DB instance creation via InstanceLoader
internal/channels/line/channel.go Start/Stop, WebhookHandler, LINE SDK init
internal/channels/line/handlers.go Signature verification, event dispatch, Loading Animation
internal/channels/line/send.go Reply/Push API with message splitting (5000 char limit)
internal/channels/line/format.go Markdown → LINE plain text conversion
internal/channels/line/constants.go LINE API constants

Modified files

File Change
cmd/gateway.go Register LINE channel factory
cmd/gateway_channels_setup.go Config-based LINE channel setup
internal/config/config_channels.go LineConfig struct
internal/channels/channel.go TypeLine constant
internal/http/channel_instances.go "line" in valid channel types

Design decisions

  • Reply API first, Push API fallback: LINE reply tokens expire in 30s. If agent takes longer, we switch to Push API automatically
  • No StreamingChannel: LINE doesn't support message editing, so streaming chunks is not possible
  • Message splitting: LINE has 5000 char limit per message bubble. Long responses are split at paragraph boundaries
  • Loading Animation: Shown immediately on webhook receipt so user sees the agent is working. Uses raw HTTP since LINE SDK v7 doesn't expose this API
  • DB-based channel instances: Uses InstanceLoader pattern (same as Telegram/Discord) — channel config stored in DB, not config.json

Test plan

  • E2E: LINE message → GoClaw → agent → LINE reply (tested with Field Recorder agent)
  • Loading Animation visible before agent responds
  • Long message splitting at 5000 chars
  • Group messages
  • Image/media messages
  • Multi-agent routing

🤖 Generated with Claude Code

stanleykao72 and others added 4 commits April 1, 2026 03:39
LINE channel for GoClaw — webhook-based, using official Go SDK (v7.21.0).
Implements Channel + WebhookChannel interfaces, mounts at /webhook/line.
Reply API priority with Push API fallback (25s threshold).

New files:
  internal/channels/line/ (6 files, ~450 lines)
  - factory.go: DB instance creation via InstanceLoader
  - channel.go: Start/Stop, WebhookHandler, LINE SDK init
  - handlers.go: Signature verification, event dispatch, Loading Animation
  - send.go: Reply/Push API with message splitting (5000 char limit)
  - format.go: Markdown → LINE plain text
  - constants.go: LINE API constants

Modified:
  - channel.go: TypeLine constant
  - gateway.go: RegisterFactory for LINE
  - gateway_channels_setup.go: Config-based LINE channel
  - config_channels.go: LineConfig struct
  - channel_instances.go: Added "line" to valid types

Constraint: LINE does not support message editing — no StreamingChannel
Constraint: LINE Reply API tokens expire in 30s — fallback to Push API
Rejected: Python bridge | Go code reusable for Phase 2, no extra deps
Rejected: Separate bridge process | WebhookChannel mounts on gateway port
Confidence: high
Scope-risk: narrow
Tested: E2E LINE message → GoClaw → Field Recorder agent → LINE reply
Not-tested: Group messages, image/media messages, pairing policy
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
LINE Loading Animation endpoint is /v2/bot/chat/loading/start (not /loading).
Added response body logging for non-200 status codes.

Constraint: LINE SDK does not expose Loading Animation API — using raw HTTP
Tested: Loading indicator visible in LINE chat before agent responds
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nbox

Phase 4 of km-closed-loop-architecture left "LINE → inbox" as an
aspirational claim — handlers.go switch only handled TextMessage and
ImageMessage, AudioMessage fell through to default and was silently
dropped. Phase 4's E2E test was actually a manual scp into inbox/.

This commit adds the AudioMessage path. New file audio.go owns:
- ContentType → extension mapping (m4a, mp3, wav, ogg, opus, aac)
- senderShort helper (first 8 chars of LINE userID, "unknown" fallback)
- downloadAudioContent (parallel to image's downloadContent so we don't
  perturb the image path)
- ingestLineAudio: download → rename per spec convention
  ({YYYYMMDD}_{HHMMSS}_line_{sender}.{ext}) → move to inbox →
  write {filename}.source.json sidecar with LINE provenance
- copyFile fallback for cross-device rename failures

handlers.go gets a 9-line case branch that calls ingestLineAudio and
RETURNS — deliberately bypassing HandleMessage. Audio messages have no
text content for the GoClaw agent and would just confuse it.

constants.go gains meetingsInboxDir = "/data/km/meetings/inbox" so the
path lives in one place.

Constraint: Must not perturb existing ImageMessage path (image-only
  downloadContent stays untouched; new downloadAudioContent is parallel)
Constraint: Audio MUST NOT call HandleMessage — agent expects text/image
Constraint: km-meeting-pipeline.sh upload cron is the consumer of inbox/
Constraint: /tmp may be tmpfs while /data is on disk → rename can fail
  cross-device → copyFile fallback included
Rejected: Refactor downloadContent to be generic image+audio | risks
  breaking existing image flow; parallel function is cheaper and safer
Rejected: Treat audio like image and put in mediaFiles | the agent
  would try to "see" an audio file as an image
Rejected: Process audio synchronously through nlm in the webhook
  handler | LINE expects fast webhook response (under 1s typically),
  nlm upload takes minutes — must defer to the cron pipeline
Rejected: Hardcode .m4a extension always | LINE actually serves multiple
  audio MIME types depending on the sender's device (iOS m4a, Android
  varying), so the mapping table is necessary
Confidence: high
Scope-risk: narrow
Reversibility: clean
Tested: go build ./internal/channels/line/... passes
Not-tested: actual LINE webhook integration (waiting for E2E phase 5.3.5);
  cross-device rename fallback (would only trigger if /tmp and /data
  are on different filesystems on the production VPS — likely they are)
Not-tested: behavior with very large audio files (>200MB) — that's
  handled by the downstream km-meeting-pipeline.sh ffmpeg compression,
  not by this code
Directive: when adding more LINE message types in the future (Video,
  File), follow the same pattern: own *.go file, ingest helper that
  bypasses HandleMessage, sidecar with provenance. Do NOT extend
  downloadContent — keep image isolated.
Directive: the .source.json sidecar uses suffix .source.json (not
  .meta.json or just .json) to avoid colliding with km-meeting-pipeline.sh's
  own processing-state sidecars which use .json directly
Related: km-meeting-gdrive-ingestion change spec (esmith-specs commit
  2e5fd4a) for the broader scope this is part of

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Builds on cf5f681 (LINE AudioMessage handling) to complete the
"LINE → km-meeting-pipeline inbox" front-end. After this commit, both
audio attachments AND GDrive shared links pasted in LINE text reach
the inbox/ for downstream nlm transcription.

New file gdrive.go owns:
- gdriveURLRegex: matches https?://drive.google.com/file/d/{id}[/?...]
  (greedy up to next whitespace so query strings are captured)
- extractGdriveURLs: returns all matches per message body — supports
  multiple links in one LINE message (e.g. morning + afternoon meeting
  links pasted together)
- ingestGdriveResult: typed view of the JSON contract emitted by
  km-meeting-pipeline.sh ingest-gdrive subcommand
- ingestGdriveLinks: per-URL invocation of the script, parses JSON,
  replies with Chinese error message on failure, silent on success
- runIngestGdrive: subprocess wrapper that handles exec.ExitError
  correctly — the script's contract is "always emit JSON on stdout
  even on exit 1", so we parse stdout regardless of exit code
- replyGdriveError: maps error type → user-facing Chinese reply via
  the existing sendChunks (reply token + push fallback)
- getMeetingPipelineScript: honors KM_MEETING_PIPELINE_SCRIPT env var
  for dev/test override

handlers.go gets one line in the TextMessage case:
  go c.ingestGdriveLinks(msg.Text, chatID)

The goroutine runs in parallel with HandleMessage so the agent still
sees the user's text (e.g. "請整理 https://drive..." gets an agent
acknowledgment) while the file is being downloaded in the background.

constants.go gains meetingsPipelineScript pointing at the production
location on the VPS.

Constraint: km-meeting-pipeline.sh emits JSON on stdout for both
  success AND failure (exit 1) — runIngestGdrive must parse out
  even when cmd.Output() returns *exec.ExitError
Constraint: LINE webhook must respond fast (<1s typical) — ingestion
  is goroutine-spawned so the event handler returns immediately
Constraint: agent's HandleMessage path also tries to use the cached
  reply token — we ALSO try to use it for error replies; whoever
  finishes first wins, the loser falls back to PushMessage cleanly
Constraint: error messages must be actionable in Chinese — generic
  "download failed" is useless; we tell the user to either share
  the file with the bot account or change link permissions
Rejected: Strip GDrive URLs from msg.Text before HandleMessage |
  loses conversation context; user message "請整理 X" should still
  reach the agent so it can acknowledge
Rejected: Process all URLs concurrently | rclone/gdown are I/O bound
  on the same network interface; serial keeps logs readable and
  avoids hitting Google's rate limits
Rejected: Make ingestion synchronous to use the reply token reliably |
  239MB download + ffmpeg compress + nlm upload takes minutes, way
  beyond LINE's webhook timeout
Rejected: Use os/exec with explicit Stdout pipe vs cmd.Output() |
  cmd.Output() returns stdout even on non-zero exit via *exec.ExitError;
  our handling already covers that case
Rejected: Reply on success too | per design Open Q1, success is
  silent to avoid notification spam in busy LINE groups
Confidence: high
Scope-risk: narrow
Reversibility: clean — adds files and a single goroutine spawn line
Tested: go build ./internal/channels/line/... passes
Tested: 12 unit cases for extractGdriveURLs (positive: view URL,
  ?usp=sharing, ?usp=drivesdk, /edit, http, surrounding Chinese,
  multi-URL, newline boundary; negative: non-GDrive URL, GDrive
  folder URL, empty, plain text)
Not-tested: actual ingest-gdrive script invocation (waiting for VPS
  deploy + 5.3 E2E phase)
Not-tested: reply token race scenarios (deterministic only via
  controlled mock; in practice both paths fall back to push API
  which is also reliable)
Directive: when adding more LINE message types that need to invoke
  external scripts, follow the same pattern: stdout JSON contract,
  cmd.Output() with exit-error tolerance, env-var overridable
  script path constant, goroutine spawn from handlers.go
Related: cf5f681 (LINE AudioMessage handler — same change scope)
Related: esmith-specs commit b8c1b6f / 9fdaec0 (the matching script
  ingest-gdrive subcommand this code calls)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant