Files
cc-connect/go.mod
T
7b93b085cb feat(googlechat): add Google Chat platform adapter (#1424)
* feat(googlechat): scaffold platform skeleton

Add a Google Chat platform adapter registered as "googlechat", wired
into selective compilation via plugin_platform_googlechat.go (build tag
no_googlechat) and the Makefile ALL_PLATFORMS list.

This commit lands the skeleton only: option parsing/validation in New
(gws_path, project, target, subscription, allow_from, trigger,
session_scope, credentials_file) plus stub Start/Reply/Send/Stop.
Receive, send and gating follow in subsequent commits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(googlechat): receive messages via gws events subscribe

Start() now supervises a `gws events +subscribe` subprocess: stdout is
scanned as NDJSON (one Workspace Events CloudEvent per line, buffer
enlarged for the full message resource), stderr is logged, and the
process is restarted with a small backoff if it exits while the context
is alive. Stop() cancels the context to kill it.

parseEvent decodes chat.message.v1.created envelopes, ignores non-human
senders (so the app never replies to its own posts), and dispatches a
core.Message carrying the space/thread reply context. Trigger-word,
allow_from gating and session scoping follow in a later commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(googlechat): send replies via gws chat messages create

Reply and Send post content through `gws chat spaces messages create`,
using the generic raw method (--params/--json) rather than the +send
helper so replies can be threaded: when the reply context carries a
thread name, the request sets messageReplyOption and thread.name so the
answer lands in the originating thread.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(googlechat): add trigger-word, allow_from and session scoping

parseEvent now gates and routes incoming messages:

- trigger word: when configured, only messages starting with the prefix
  are handled (prefix stripped) for user-OAuth mode without a Chat App;
  otherwise argumentText (mention markup already removed by Google) is
  used, falling back to text.
- allow_from via core.AllowList, and a core.IsOldMessage guard on
  createTime so a restart does not replay backlog.
- session_scope space|thread|user via buildSessionKey, plus
  ReconstructReplyCtx so proactive sends reach the right space/thread.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(googlechat): add config example

Document the googlechat platform options in config.example.toml,
including the gws prerequisites and the two operating modes (@mention
with a Chat App service account, vs trigger-word with user OAuth).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(googlechat): add event parsing and reply-arg unit tests

Cover parseEvent (mention vs trigger mode, argumentText fallback,
non-human and wrong-type skipping, allow_from, old-message guard),
buildSessionKey for each scope, ReconstructReplyCtx round-trips, and
buildCreateArgs threaded vs top-level argument shapes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(googlechat): switch to Chat app Pub/Sub receive + service-account send

Pivot the platform from the Workspace Events API to a registered Google
Chat app's native Cloud Pub/Sub connection. The subscription is fixed, so
there is no Workspace Events subscription expiry or per-restart resource
leak (the two operational problems of the previous approach).

- receive: parse the Chat-app event format — gws wraps each Pub/Sub
  message as {"data":{"type":"MESSAGE","message":{...}}} — instead of
  Workspace Events CloudEvents; pull a fixed subscription via
  `gws events +subscribe --subscription`.
- send: post via the Chat REST API authenticated as the app's service
  account (chat.bot scope) so replies appear as the bot. gws has no
  service-account auth, so sending is native Go via
  golang.org/x/oauth2/google; receiving still uses gws (its own OAuth).
- config: require `subscription`; `credentials_file` is the SA key used
  for replies. Drop the Workspace Events target/project auto-create.

Verified end-to-end against a real Chat app: the receive format matches
gws output and a service-account reply posts as the bot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(googlechat): add GCP/Chat app setup guide

Add docs/googlechat.md documenting the end-to-end Google Chat setup:
enabling the Chat + Pub/Sub APIs, creating the Pub/Sub topic/subscription
and granting the Chat push service account publisher, creating a service
account key for bot replies, configuring the Chat app (LIVE, classic
model, Cloud Pub/Sub connection, visibility), cc-connect config, and an
FAQ covering the issues hit during bring-up (app must be LIVE to send,
Workspace-only, add-on vs classic). Link it from the README platform
table.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(googlechat): receive via native Pub/Sub client instead of gws

Replace the `gws events +subscribe` subprocess with a native streaming pull
using cloud.google.com/go/pubsub. Sending was already native Go, so this
removes the gws binary dependency entirely — the platform is now fully native.

Both directions now authenticate with the same service-account key, so
`credentials_file` is required (it also needs roles/pubsub.subscriber on the
subscription for the receive path). The `gws_path` option is removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(googlechat): drop trigger, centralize session-key constants, harden session_scope (#5)

- Remove the `trigger` field and its prefix-matching branch from extractContent;
  Google Chat already scopes delivery so a prefix filter is redundant and subtly
  broken for @mention spaces (m.Text still contains the markup).
- Extract sessionKeyPrefix and threadSep constants used by both buildSessionKey
  and ReconstructReplyCtx so the encode/decode pair stays in sync automatically.
- Add a slog.Warn in the normalizeSessionScope default branch so typos like
  "thraed" surface at startup instead of silently falling back to "space".
- Update tests, docs/googlechat.md, and config.example.toml accordingly.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(googlechat): implement FormattingInstructions for Google Chat text formatting (#7)

* feat(googlechat): implement FormattingInstructions for Google Chat text formatting

Adds FormattingInstructions() to *Platform so the engine injects
Google Chat-specific formatting guidance into the agent system prompt,
steering agents away from unsupported ## headings and [text](url) links.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(googlechat): add block quote and display-text link to FormattingInstructions

Add missing formatting rules per reviewer feedback:
- Block quote syntax (>text)
- Display-text hyperlink syntax (<url|text>) alongside raw URL auto-linking

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(googlechat): fix test call after base-branch refactor of newTestPlatform

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(googlechat): implement streaming preview (PreviewStarter + MessageUpdater) (#6)

* feat(googlechat): implement streaming preview via PreviewStarter + MessageUpdater

Adds SendPreviewStart (POST a threaded message, return resource-name handle)
and UpdateMessage (PATCH spaces.messages with updateMask=text) so the engine's
real-time streaming-preview path is activated for Google Chat.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(googlechat): extract doRequest helper + drain response bodies

Extract a `doRequest` helper that centralizes HTTP execution and non-2xx
error handling. Both `post()` and `SendPreviewStart` become thin wrappers,
eliminating the duplicated logic that would otherwise diverge on future
changes (retries, auth header tweaks, etc.).

Drain response bodies on success paths in `post()`, `SendPreviewStart`,
and `UpdateMessage` so the transport can return TCP connections to the
pool. Addresses the performance issue where each streaming PATCH would
open a new TLS+TCP handshake.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(googlechat): send images and files via attachment upload (#8)

* feat(googlechat): implement SendImage and SendFile via attachment upload

Adds media-upload support to the Google Chat platform so cc-connect can
send images and files (e.g. /diff HTML output) instead of returning
ErrNotSupported.

Two-step flow for each send:
1. Upload raw bytes to the Chat media endpoint (multipart/related POST
   to https://chat.googleapis.com/upload/v1/{space}/attachments:upload)
   and retrieve the attachmentDataRef resource name.
2. Create a Chat message that references the data ref, respecting thread
   context the same way as the existing text send path.

Shared helper postAttachment handles both steps; SendImage and SendFile
delegate to it with appropriate filename/MIME defaults. Compile-time
interface assertions ensure *Platform satisfies core.ImageSender and
core.FileSender.

Adds TestBuildAttachmentRequest mirroring TestBuildSendRequest to cover
the message body shape (attachment data ref + thread, reply option URL).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(googlechat): address code-review feedback on SendImage/SendFile

- Add rc.space == "" guard to postAttachment (bug fix: prevents malformed
  upload URL when ReconstructReplyCtx("googlechat:") yields empty space)
- Pre-size bytes.Buffer in uploadAttachment to avoid a full double-copy of
  attachment data on large files
- Extract httpErrorBody helper to deduplicate the 3 error-drain sites
- Extract messageURL/applyThread helpers to deduplicate threading URL logic
  shared between buildSendRequest and buildAttachmentRequest
- Extract coalesce helper and simplify SendImage/SendFile
- Use doRequest in postAttachment for the create-message step
- Add HTTP-level unit tests: uploadAttachment (success, 403 error,
  empty resourceName), postAttachment (missing space, two-step flow),
  SendImage/SendFile (default filename/MIME fallbacks)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(googlechat): use doRequest in uploadAttachment and drain response body

Two follow-up fixes from code review:
- Route upload request through doRequest so it benefits from any future
  centralized logging/retry/tracing alongside post() and postAttachment().
- Drain remaining bytes from resp.Body after json.Decode so the HTTP
  transport can return the connection to the pool (partial reads prevent
  connection reuse, causing a new TLS handshake per SendImage/SendFile call).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(googlechat): drain resp.Body on decode error in uploadAttachment

Deferred drain ensures the HTTP connection is returned to the pool
even when JSON decoding fails, not just on success.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(googlechat): handle Stop error, remove redundant drain defer, add compile-time assertion (#12)

- Return psClient.Close() error from Stop() instead of silently discarding it
- Remove redundant drain defer in uploadAttachment; the json.Decoder already
  consumes the body, and defer resp.Body.Close() handles cleanup
- Add compile-time assertion var _ core.ReplyContextReconstructor = (*Platform)(nil)
  alongside the existing ImageSender and FileSender guards

Closes #10

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(googlechat): use doRequest in uploadAttachment and drain response body

Two follow-up fixes from code review:
- Route upload request through doRequest so it benefits from any future
  centralized logging/retry/tracing alongside post() and postAttachment().
- Drain remaining bytes from resp.Body after json.Decode so the HTTP
  transport can return the connection to the pool (partial reads prevent
  connection reuse, causing a new TLS handshake per SendImage/SendFile call).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(googlechat): drain resp.Body on decode error in uploadAttachment

Deferred drain ensures the HTTP connection is returned to the pool
even when JSON decoding fails, not just on success.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(googlechat): clear lint issues in googlechat package (#13)

* fix(googlechat): resolve package lint errors

Handle all body/multipart close errors, remove deprecated auth option usage, and tighten HTTP test writers so golangci-lint passes for platform/googlechat.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(googlechat): ack Pub/Sub message after handler, nack on panic

Previously m.Ack() was called before p.handler(), so a handler panic or
failure would silently drop the message without redelivery. Move ack to
after handler completion and nack on panic so Pub/Sub can redeliver.

Introduce the ackable interface and dispatchMessage to make the timing
testable without a real Pub/Sub client, and add three regression tests
covering the success, panic, and parse-failure paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(googlechat): add unit tests for New() error cases (#14)

Closes #11

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-13 16:49:41 +08:00

107 lines
4.7 KiB
AMPL

module github.com/chenhg5/cc-connect
go 1.25.0
require (
cloud.google.com/go/pubsub/v2 v2.4.0
github.com/BurntSushi/toml v1.6.0
github.com/bwmarrin/discordgo v0.29.0
github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/creack/pty v1.1.24
github.com/go-telegram/bot v1.20.0
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.0
github.com/larksuite/oapi-sdk-go/v3 v3.5.3
github.com/line/line-bot-sdk-go/v8 v8.19.0
github.com/mdp/qrterminal/v3 v3.2.1
github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1
github.com/robfig/cron/v3 v3.0.1
github.com/slack-go/slack v0.16.0
github.com/stretchr/testify v1.11.1
golang.org/x/crypto v0.50.0
golang.org/x/oauth2 v0.36.0
google.golang.org/api v0.272.0
maunium.net/go/mautrix v0.27.0
modernc.org/sqlite v1.49.1
rsc.io/qr v0.2.0
)
require (
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/auth v0.18.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/iam v1.5.3 // indirect
filippo.io/edwards25519 v1.2.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.9.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
github.com/googleapis/gax-go/v2 v2.18.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mattn/go-sqlite3 v1.14.42 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/zerolog v1.35.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yuin/goldmark v1.8.2 // indirect
go.einride.tech/aip v0.83.0 // indirect
go.mau.fi/util v0.9.8 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.42.0 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
go.opentelemetry.io/otel/sdk v1.42.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect
go.opentelemetry.io/otel/trace v1.42.0 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/term v0.42.0 // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
google.golang.org/grpc v1.79.3 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.72.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)