feat(line): LINE Messaging API channel adapter#715
Open
stanleykao72 wants to merge 4 commits intonextlevelbuilder:mainfrom
Open
feat(line): LINE Messaging API channel adapter#715stanleykao72 wants to merge 4 commits intonextlevelbuilder:mainfrom
stanleykao72 wants to merge 4 commits intonextlevelbuilder:mainfrom
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Channel+WebhookChannelinterfaces, mounts at/webhook/lineNew files
internal/channels/line/factory.goInstanceLoaderinternal/channels/line/channel.gointernal/channels/line/handlers.gointernal/channels/line/send.gointernal/channels/line/format.gointernal/channels/line/constants.goModified files
cmd/gateway.gocmd/gateway_channels_setup.gointernal/config/config_channels.goLineConfigstructinternal/channels/channel.goTypeLineconstantinternal/http/channel_instances.goDesign decisions
InstanceLoaderpattern (same as Telegram/Discord) — channel config stored in DB, not config.jsonTest plan
🤖 Generated with Claude Code