padosoft/askmydocs-connector-base
Composer 安装命令:
composer require padosoft/askmydocs-connector-base
包简介
Framework primitives for AskMyDocs connectors — interface, base helpers, registry, OAuth vault, sync job, scheduler, exceptions. Extend ConnectorInterface to ingest any data source as RAG-ingestible knowledge.
README 文档
README
Framework primitives for AskMyDocs connectors — write a Laravel package, plug it into any AskMyDocs instance.
Implement ConnectorInterface on your favourite data source (Google Drive, Notion, Confluence, a CSV bucket, an internal API, ...) and let AskMyDocs ingest it as RAG-grounded knowledge with OAuth, encrypted-at-rest credentials, retry-aware queued syncs, per-tenant isolation, and a cadence scheduler — all wired automatically by composer discovery.
Table of contents
- Why this package
- What you get
- Architecture at a glance
- Installation
- Quick start — write your first connector in 50 lines
- The 10-method contract
- How auto-discovery works
- Credential vault — encrypted, atomic, tenant-scoped
- Scheduler + sync job
- Multi-tenancy (R30 + R31)
- Configuration reference
- Testing
- Roadmap
- License
Why this package
AskMyDocs is an enterprise-grade RAG + canonical knowledge compilation system. Out of the box it ingests markdown from disk, the chat UI, an HTTP API, and a Git-driven workflow.
But the knowledge people actually want to query lives in Google Drive, Notion, Confluence, Jira, OneDrive, Evernote, Fabric, Slack, Salesforce, HubSpot, a private S3 bucket, a custom CRM — anywhere except markdown-on-disk.
This package is the smallest possible surface for shipping a new connector:
- A 10-method
ConnectorInterfaceyou implement. - A
BaseConnectorthat gives you OAuth state-token CSRF, credential refresh, and tenant-scoped installation lookup for free. - A registry that auto-discovers your package the moment somebody
composer requires it — zero config edits on the consumer side. - An
OAuthCredentialVaultthat handles encryption-at-rest, refresh-token semantics, and atomic concurrent writes (no read-modify-write data loss on shared cursor blobs). - A queued
ConnectorSyncJobwith exponential backoff, tenant restore, and failure-state recording. - A cadence scheduler that walks active installations every minute and dispatches due syncs.
- Per-tenant isolation baked into every query (R30 / R31 — see Multi-tenancy).
Write the connector. Ship the package. Composer-require it from any AskMyDocs install. Done.
What you get
| Surface | Class | What it does |
|---|---|---|
| Contract | ConnectorInterface |
10 methods every connector implements |
| Base | BaseConnector |
OAuth state-token CSRF, refresh helper, tenant lookup |
| Registry | ConnectorRegistry |
Boot-time R23 validation + composer-extra auto-discovery |
| Vault | Auth\OAuthCredentialVault |
AES-encrypted tokens, atomic setExtraKey (R21), tenant scope |
| Scheduler | Scheduling\SyncScheduler |
Cadence walker, chunkById(100), active-only filter |
| Job | ConnectorSyncJob |
$tries=3, exponential backoff, tenant-restore safety |
| Models | ConnectorInstallation + ConnectorCredential |
BelongsToTenant trait, cascade delete |
| Migrations | connector_installations + connector_credentials |
Auto-loaded by the service provider |
| Exceptions | ConnectorAuthException, ConnectorApiException, ConnectorPaginationLimitException, RegistryConfigurationException |
Distinct failure semantics: auth = no retry, api = retry, paginator-limit = partial success |
| DTOs | SyncResult, HealthStatus |
Immutable outcomes |
| Tenancy | Support\TenantContext + Models\Concerns\BelongsToTenant |
Request-scoped tenant, auto-fill on creating |
Architecture at a glance
┌──────────────────────────┐
│ Your connector package │
│ composer extra.askmydocs│
└─────────────┬────────────┘
│ auto-discovered
▼
┌────────────────────┐ ┌──────────────────┐ ┌──────────────────────────┐
│ Cadence scheduler │ ──────▶ │ ConnectorRegistry│ ◀────── │ Host: config/connectors │
│ (every minute) │ │ R23 boot-time │ │ ::built_in (optional) │
└────────┬───────────┘ │ FQCN validation │ └──────────────────────────┘
│ └─────────┬────────┘
│ dispatch │ resolve by key()
▼ ▼
┌────────────────────┐ ┌──────────────────┐ ┌──────────────────────────┐
│ ConnectorSyncJob │ ──────▶ │ Your connector │ ──────▶ │ OAuthCredentialVault │
│ tenant-restore │ │ syncIncremental()│ │ AES + lockForUpdate (R21)│
│ tries=3, backoff │ └─────────┬────────┘ └──────────────────────────┘
└────────────────────┘ │
│ fetches changed docs
▼
┌────────────────────────┐
│ Host ingest pipeline │
│ (e.g. AskMyDocs │
│ IngestDocumentJob) │
└────────────────────────┘
Installation
composer require padosoft/askmydocs-connector-base
The service provider is auto-discovered (Laravel package discovery). The package ships its own migrations — run them:
php artisan migrate
Want to copy the migrations into your host app's database/migrations/ (e.g. to tweak tenant_id length)? Publish them:
php artisan vendor:publish --tag=connector-migrations
Same for the config:
php artisan vendor:publish --tag=connector-config
Wire the scheduler from your host app's bootstrap/app.php:
use Padosoft\AskMyDocsConnectorBase\Scheduling\SyncScheduler; ->withSchedule(function (Schedule $schedule): void { (new SyncScheduler)->registerSchedules($schedule); })
That's it. Connector packages installed via composer are now auto-discovered and synced on cadence.
Quick start — write your first connector in 50 lines
Create a new Laravel package. Add padosoft/askmydocs-connector-base to its require. Declare your connector class FQCN under extra.askmydocs.connectors:
// composer.json (your package) { "name": "you/askmydocs-connector-myapi", "require": { "padosoft/askmydocs-connector-base": "^1.0" }, "autoload": { "psr-4": { "You\\AskMyDocsConnectorMyApi\\": "src/" } }, "extra": { "askmydocs": { "connectors": [ "You\\AskMyDocsConnectorMyApi\\MyApiConnector" ] } } }
Implement the connector:
namespace You\AskMyDocsConnectorMyApi; use Carbon\Carbon; use Illuminate\Http\Request; use Padosoft\AskMyDocsConnectorBase\BaseConnector; use Padosoft\AskMyDocsConnectorBase\HealthStatus; use Padosoft\AskMyDocsConnectorBase\SyncResult; use Padosoft\AskMyDocsConnectorBase\Exceptions\ConnectorAuthException; final class MyApiConnector extends BaseConnector { public function key(): string { return 'my-api'; } public function displayName(): string { return 'My API'; } public function oauthScopes(): array { return ['read:docs']; } public function initiateOAuth(int $installationId): string { $state = $this->issueOAuthState($installationId); return 'https://my-api.example.com/oauth/authorize?state='.$state.'&...'; } public function handleOAuthCallback(int $installationId, Request $request): void { if (! $this->consumeOAuthState($installationId, (string) $request->query('state'))) { throw new ConnectorAuthException('Bad state'); } // Exchange code -> token, then: $this->vault->setCredentials($installationId, 'access-token', refreshToken: 'refresh'); } public function syncFull(int $installationId): SyncResult { return $this->syncIncremental($installationId, null); } public function syncIncremental(int $installationId, ?Carbon $since): SyncResult { // Fetch changed docs, dispatch host ingest jobs, count them. return new SyncResult( documentsAdded: 5, documentsUpdated: 2, documentsRemoved: 0, errors: [], completedAt: Carbon::now(), ); } public function disconnect(int $installationId): void { $this->vault->clearCredentials($installationId); } public function health(int $installationId): HealthStatus { return HealthStatus::healthy(); } }
composer require you/askmydocs-connector-myapi in any AskMyDocs install — the registry auto-discovers it, the scheduler starts dispatching it on cadence, the admin UI lists it in the available-connectors picker.
The 10-method contract
Every connector implements 10 methods (3 metadata + 1 scope + 2 OAuth + 2 sync + 1 disconnect + 1 health):
| Method | Purpose | Throws |
|---|---|---|
key(): string |
Stable kebab-case identifier (google-drive, notion). Used as URL slug + connector_installations.connector_name. |
— |
displayName(): string |
Human label shown in the admin UI. | — |
iconUrl(): string |
Connector logo URL. BaseConnector provides a default that resolves public/connectors/{key}.svg via asset(). |
— |
oauthScopes(): array |
List of scope strings the provider requires. Surfaced to the user in the install confirmation dialog. | — |
initiateOAuth(int): string |
Build the provider's authorization URL. Use $this->issueOAuthState() for CSRF. |
ConnectorAuthException |
handleOAuthCallback(int, Request): void |
Exchange auth code -> tokens, verify state, persist via $this->vault->setCredentials(). |
ConnectorAuthException on any failure |
syncFull(int): SyncResult |
Full discovery + ingestion. Long-running. Called at install + operator re-sync. | propagates upstream errors |
syncIncremental(int, ?Carbon): SyncResult |
Delta since $since. Falls back to syncFull when $since === null. Called by the cadence scheduler. |
ConnectorApiException for transient (retry), ConnectorAuthException for credentials (no retry) |
disconnect(int): void |
Clear credentials, optionally revoke at provider. | swallow / log; framework deletes installation row after |
health(int): HealthStatus |
Fast (under 2s) side-effect-free probe. | returns HealthStatus::errored(...) instead of throwing |
How auto-discovery works
ConnectorRegistry merges two sources at boot:
config/connectors.php::built_in— FQCN list for connectors the host app wires by hand (rare).composer.lockpackages — every entry whoseextra.askmydocs.connectorsis a non-empty array of FQCNs.
Each FQCN is resolved through the container and instanceof-checked against ConnectorInterface (R23). Failure modes:
- Class missing ->
RegistryConfigurationException: '...' does not exist - Class exists but doesn't implement ->
RegistryConfigurationException: '...' does not implement ConnectorInterface - Two connectors return the same
key()->RegistryConfigurationException: Duplicate connector key '...' - Container can't instantiate ->
RegistryConfigurationException: '...' could not be resolved
All boot-time. No silent fallthrough to a confusing "undefined method" later.
Credential vault — encrypted, atomic, tenant-scoped
OAuthCredentialVault is the single chokepoint for every connector's tokens:
- AES-256 encryption at rest via Laravel
Crypt. The DB row never sees plaintext. - Tenant-scoped reads — every query joins to
connector_installationsand filters by the activeTenantContext. Cross-tenant reads returnnull, not the wrong tenant's tokens. - Refresh-aware —
getAccessToken()returnsnullfor expired tokens. Connectors callgetRefreshToken()to rotate via the provider's/oauth2/tokenendpoint, thensetCredentials()to persist the rotated pair. - R21 — atomic
setExtraKey— concurrent writers updating different keys inextra_json(e.g. one connector storingbot_id, another storingchanges_page_token) MUST NOT race. Implementation:
DB::transaction(function () use (...) { $row = ConnectorCredential::query() ->where(...) ->lockForUpdate() // SELECT ... FOR UPDATE ->first(); if ($row === null) { throw new ConnectorAuthException('credential row was deleted concurrently'); } $extra = $row->extra_json ?? []; $extra[$key] = $value; $row->extra_json = $extra; $row->save(); // same transaction });
A read-modify-write without the lock loses siblings under contention. The package was extracted from AskMyDocs precisely after this race was caught + fixed in production.
Scheduler + sync job
SyncScheduler::registerSchedules($schedule) registers one everyMinute() closure. The closure walks every STATUS_ACTIVE installation in chunkById(100) and dispatches ConnectorSyncJob for each that's due (i.e. last_sync_at + cadenceMinutes <= now()).
ConnectorSyncJob:
$tries = 3,$backoff = [60, 300, 900]— three attempts at 1m / 5m / 15m spacing.$timeout = 600— 10 min hard ceiling.- Tenant restore in
finally— the job setsTenantContextto the dispatching tenant on entry, restores the prior value on exit. Long-lived queue workers handling jobs back-to-back for different tenants are R30-safe. - Status guards — non-
ACTIVEinstallations short-circuit. De-registered connectors flip toSTATUS_ERROREDwith a clear message. - Failure semantics —
ConnectorAuthExceptionmarkserroredand exhausts retries (no point retrying bad credentials).ConnectorApiExceptionand other throwables fail-and-retry per the backoff.
Multi-tenancy (R30 + R31)
Every model uses the BelongsToTenant trait:
- R31 (write-side) —
tenant_idauto-fills fromTenantContext::current()oncreatingunless the caller passes an explicit value. - R30 (read-side) —
forTenant($id)scope for explicit query scoping. Two tenants legitimately install the same connector under differenttenant_ids — the composite UNIQUE(tenant_id, connector_name)makes the row pair structurally legal.
Host applications with their own TenantContext rebind via a container alias — both surfaces observe the same active tenant.
Configuration reference
// config/connectors.php (publishable with --tag=connector-config) return [ 'built_in' => [ // \App\Connectors\BuiltIn\MyHostConnector::class, ], 'default_sync_cadence_minutes' => env('CONNECTOR_DEFAULT_SYNC_CADENCE_MINUTES', 15), 'per_connector_cadence' => [ // 'google-drive' => 10, // 'notion' => 30, ], 'oauth_state_ttl_seconds' => env('CONNECTOR_OAUTH_STATE_TTL_SECONDS', 600), 'sync_job_queue' => env('CONNECTOR_SYNC_JOB_QUEUE', 'default'), 'providers' => [ // Per-connector packages merge their own block here from their // own service providers via mergeConfigFrom(). ], ];
Testing
composer install vendor/bin/phpunit
Tests use Orchestra Testbench with SQLite in-memory. The default suite has zero external dependencies — every Laravel facade is in scope, every Crypt::encryptString() call uses a per-test APP_KEY, every model uses RefreshDatabase.
For connector packages built ON TOP of this base, follow the standard padosoft testing pattern: a default tests/Unit/ suite that uses Http::fake() (zero cost, runs in CI), plus an opt-in tests/Live/ suite that hits the real provider API (skipped when the env var is missing, invoked explicitly by maintainers).
Roadmap
- v1.1 — Optional
ChunkerInterfacere-export once the AskMyDocs chunker value-object surface stabilises, so per-connector packages can ship provider-specific chunkers (already used in AskMyDocs forConfluencePageChunker,JiraIssueChunker,AtomicNoteChunker). - v1.2 — Optional admin-trail helpers (audit event emission, PII redaction at the ingest boundary) lifted from AskMyDocs' host-side
BaseConnectorsubclass into anAuditableBaseConnectormixin for hosts that want them out of the box. - v2.0 —
MCPConnectorInterfacecompanion for chat-time tool registration (Model Context Protocol). Connectors register tools the agent calls during a chat turn, complementing today's batch-sync model. Tracks the v4.5+ AskMyDocs agentic roadmap.
Community PRs welcome — open an issue first to discuss scope.
License
Apache-2.0 (c) Padosoft / Lorenzo Padovani. See LICENSE.
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 2
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: Apache-2.0
- 更新时间: 2026-06-18