mirror of
https://github.com/chenhg5/cc-connect.git
synced 2026-10-25 17:36:43 +08:00
main
2
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
523f109a98 |
fix(claudecode): detect missing work_dir at startup with clear error (#1425)
When work_dir does not exist, New() now returns `claudecode: work_dir "/bad/path" does not exist` instead of the confusing `fork/exec … no such file or directory` message that blamed the Claude binary. Also fixes existing tests that used hard-coded paths not guaranteed to exist by switching them to t.TempDir(). Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |