opscale/fields 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

opscale/fields

Composer 安装命令:

composer require opscale/fields

包简介

Family of custom fields for Laravel Nova. Includes AI: a conversational assistant that proposes values for the fields of a resource form.

README 文档

README

Family of custom fields for Laravel Nova. The first member is AI: a conversational assistant that lives inside a resource form, chats with the user, and proposes values for the other fields of the form. The user accepts or discards each proposal — the AI never writes anything by itself.

use Opscale\Fields\AI;

public function fields(NovaRequest $request): array
{
    return [
        Text::make('Name'),
        Textarea::make('Description'),
        BelongsTo::make('Category', 'category'),

        AI::make('Assistant')
            ->fields(['name', 'description', 'category'])
            ->agent(ProductCopilot::class)
            ->context(fn (NovaRequest $r) => ['tenant_id' => $r->user()->tenant_id])
            ->onlyOnForms(),
    ];
}
  • ->fields([...]) — which form attributes the assistant may propose values for. The proposeFields tool schema handed to the agent is derived from this single list, so it can never drift from the form.
  • ->agent(...) — a laravel/ai agent class extending Opscale\Fields\AI\Agents\FieldAgent.
  • ->context(...) — extra server-side context (tenant, user...). Client input is never trusted for authorization.
  • ->maxDuration(120) — seconds after which the stream self-terminates (see footguns).

The field is headless: no database column, a no-op on save, forms only. If the panel's JavaScript ever fails, the form keeps working — the assistant is strictly additive and can be ignored entirely.

Requirements

Dependency Version Why pinned
PHP ^8.3 required by laravel/ai
Laravel ^12 | ^13 required by laravel/ai
laravel/nova ~5.9.0 (pinned) accepted values are pushed into native fields through Nova's internal event bus ({formUniqueId}-{attribute}-value / -change). That naming is undocumented API; re-verify it in vendor/laravel/nova/public/app.js (getFieldAttributeValueEventName, listenToValueChanges) and the laravel-nova npm mixins (src/mixins/FormField.js) before raising the constraint.
laravel/ai ^0.9 streams the Vercel UI Message Stream v1 (x-vercel-ai-ui-message-stream: v1)
@ai-sdk/vue (bundled) ~4.0.22 + ai ~7.0.22 consumes exactly that protocol version — bump both sides of the protocol together

Installation

composer require opscale/fields

Publish laravel/ai's config and conversation tables (the field persists chat history with RemembersConversations):

php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"
php artisan migrate

Configure your provider key on the server — it never reaches the browser:

OPENAI_API_KEY=sk-...        # or ANTHROPIC_API_KEY, etc. (config/ai.php)

Writing an agent

Extend FieldAgent: describe your domain in persona() and expose lookup tools for relationship fields. Everything else (proposal schema, current form values, server context, the proposeFields tool, conversation memory) is injected by the field's endpoint.

use Opscale\Fields\AI\Agents\FieldAgent;

final class ProductCopilot extends FieldAgent
{
    protected function persona(): string
    {
        return 'You are a product catalog copilot... derive name, description, '
            .'price, SKU, stock and category from a short merchant description.';
    }

    protected function lookupTools(): iterable
    {
        yield new SearchCategories; // returns real category IDs from the DB
    }
}

Relationship fields are proposed as ID + display label: the ID is what fills the field, the label is what the user sees before accepting. The base instructions forbid inventing IDs — the model must pick them from a lookup tool.

The compression ratio

If the assistant asked one question per field, this would just be a slower form. The base prompt pushes the opposite: few high-level questions, many derived fields. Real exchange with the ProductCopilot demo (2 user answers → 6 fields proposed):

User: I want to list the wireless mouse we just received. Assistant: What price point and roughly how many units did you receive? User: Around $49.99, we got 200 units. Assistant: (proposes via proposeFields)

  • Name → "Wireless Pro Mouse"
  • Description → "Precision wireless mouse with ergonomic design..."
  • Price → 49.99
  • SKU → "WPM-WL-001"
  • Stock → 200
  • Category → Peripherals (ID 1, from searchCategories)

The user accepts all six, edits any of them by hand, or types "make the description more formal" to get a refined re-proposal — all inside the panel.

How it works

Vue panel (useChat, @ai-sdk/vue)
  └── POST nova-vendor/nova-ai-field/{resource}/ai/chat   ← Nova middleware
        └── ChatController re-resolves the AI field on the resource
              └── FieldAgent (laravel/ai) + RemembersConversations
                    ├── lookup tools (real DB queries)
                    └── proposeFields tool → inert handle(), no server effect
        └── ->stream()->usingVercelDataProtocol()   (SSE, UI Message Stream v1)

Panel reads proposeFields tool invocations from the typed message parts
  └── proposal list: field → value → [accept] [discard]
        └── accept: Nova.$emit(`{formUniqueId}-{attribute}-value`, value)

The model never returns text that needs parsing: proposals arrive as a typed tool-input-available part. proposeFields.handle() exists because laravel/ai's Tool contract requires it, but it is deliberately inert — it only acknowledges, executing nothing.

Footguns

  1. Proxy buffering. The endpoint already sends X-Accel-Buffering: no, but nginx setups with proxy_buffering on will still swallow the stream and deliver everything at once when it finishes. It looks like a code bug; it is not. Configure:

    location /nova-vendor/nova-ai-field/ {
        proxy_buffering off;
    }
  2. PHP-FPM workers. Every open SSE stream holds a worker. Fine for a Nova panel (internal users, low concurrency), but keep a bound on it: the field passes maxDuration (default 120s) as the stream timeout so a stuck stream frees its worker. Tune per field with ->maxDuration(seconds) and size pm.max_children accordingly.

  3. Nova is pinned (~5.9.0) because accepting a proposal uses Nova's internal event bus (see Requirements). When bumping Nova, re-check the event names before widening the constraint.

  4. Relationship fields. A BelongsTo is filled with an ID, never a label, so proposals carry both (value = ID, display = label). Known v1 limitation: Nova 5.9's BelongsTo form component does not subscribe to the -value event bus, so accepting a relation proposal may not visually fill the combobox — the user sees the proposed record's name in the panel and can select it manually. Text, textarea, number, markdown and similar native fields do subscribe and fill instantly.

Out of scope (v1)

  • Inline ✨ buttons on native Nova fields.
  • "Pending state" styling over native fields.
  • Any override of Nova's Vue components (fragile across upgrades — everything lives inside this field's panel).

Development

composer serve      # build workbench + Nova dev server (admin@laravel.com / password)
npm run quality     # duster + phpstan + pest
npm run watch       # rebuild JS on change

The workbench ships a Product resource wired to the ProductCopilot demo agent (workbench/app/Agents).

Package layout

src/
├── FieldServiceProvider.php   shared provider: one route group + ONE asset bundle
├── AI.php                      the field class (Opscale\Fields\AI)
└── AI/                         the field's support code (Opscale\Fields\AI\*)
    ├── Agents/FieldAgent.php
    ├── Http/Controllers/ChatController.php
    └── Tools/ProposeFields.php
resources/js/
├── field.js                    single entrypoint registering every field's components
└── ai/FormField.vue
routes/api.php                  grouped under nova-vendor/nova-ai-field

src/AI.php (class) and src/AI/ (support namespace) coexist on purpose — valid PSR-4, do not flatten. The package is multi-field by design: new fields add components to the same bundle, routes to the same group, and registrations to the same provider.

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 3
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-07-11

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固