PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.13
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.13
1.1.13 1.1.12 1.1.11 1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 All 37 releases
← All changes | includes/widgets/widget-drafts.php +73 -8 1.1.3 → 1.1.13 View file →
@@ -9,9 +9,10 @@
9 9 * Data source: WordPress REST API /wp/v2/posts?status=draft (edit
10 10 * context, scoped to the viewer with `author` — without it an editor
11 11 * or admin would see every draft on the site, not their own).
12 12 * Refresh: every 60 seconds while the tab is visible, plus an
13 - * immediate refresh when a window closes or blurs.
13 + * immediate refresh on every `os.post.changed` broadcast (a block-editor
14 + * save, a relayed content change) and when a window closes or blurs.
14 15 * Requires: OpenStation 0.18.0+ (openstation_register_widget).
15 16 *
16 17 * @package OpenStation
17 18 */
@@ -169,11 +170,11 @@
169 170 * @param WP_Post $post The draft being described.
170 171 * @return string
171 172 */
172 173 function openstation_drafts_ai_instructions( WP_Post $post ) {
173 - $instructions = 'You are a writing assistant for a WordPress author. Given a draft post\'s current title and content, help them finish and file it. Provide: exactly 3 concise, compelling title options (about 70 characters max each); one 1-2 sentence excerpt suitable as the post summary; 3 to 6 lowercase topical tags; 1 to 2 categories (strongly prefer the site\'s existing categories listed above — only propose a new concise name if none fit); and a readiness check.
174 + $instructions = 'You are a writing assistant for a WordPress author. Given a draft post\'s current title and content, help them finish and file it. Provide: exactly 3 concise, compelling title options (about 70 characters max each); one 1-2 sentence excerpt suitable as the post summary; 3 to 6 lowercase topical tags; 1 to 2 categories (prefer the site\'s existing categories, listed in the message; propose a new concise name only if none fit); and a readiness check.
174 175
175 -The readiness check MUST be strict and evidence-based. Judge only STRUCTURE and COMPLETENESS: does the draft have a clear introduction, enough substance/depth, at least one concrete example or detail, and a conclusion? The "missing" array lists only what is GENUINELY ABSENT from the text you were given. CRITICAL: never invent, guess, or hallucinate problems. Do NOT claim there are typos, misspellings, or cut-off/incomplete sentences unless you can quote the exact offending text verbatim from the draft — if you are not quoting real text, do not mention it. If the draft already has an intro, body with a concrete detail, and a conclusion and reads as complete, return an EMPTY "missing" array and say it looks ready in the summary.
176 +The readiness check judges structure and completeness only: does the draft have a clear introduction, enough substance, at least one concrete example or detail, and a conclusion? The "missing" array lists only what is absent from the text you were given. Mention a typo, misspelling or cut-off sentence only together with the exact text, quoted from the draft. When the draft has an introduction, a body with a concrete detail and a conclusion, return an empty "missing" array and say in the summary that it looks ready.
176 177
177 178 Write everything in the same language as the draft. Do not invent facts that are not supported by the content.';
178 179
179 180 /**
@@ -334,13 +335,9 @@
334 335 $json = new WP_Error( 'openstation_ai_failed', $e->getMessage() );
335 336 }
336 337
337 338 if ( is_wp_error( $json ) ) {
338 - return new WP_Error(
339 - 'openstation_ai_failed',
340 - $json->get_error_message(),
341 - array( 'status' => 502 )
342 - );
339 + return openstation_drafts_ai_failure( $json );
343 340 }
344 341
345 342 $data = json_decode( (string) $json, true );
346 343 if ( ! is_array( $data ) ) {
@@ -375,8 +372,76 @@
375 372 */
376 373 $suggestions = (array) apply_filters( 'openstation_drafts_ai_suggestions', $suggestions, $post );
377 374
378 375 return new WP_REST_Response( $suggestions, 200 );
376 +}
377 +
378 +/**
379 + * Turn a failed generation into the route's error.
380 + *
381 + * The route answers 502 for every provider failure: the failure is the
382 + * upstream's, not the caller's, and a provider's own 401 or 403 passed
383 + * through as the REST status would read as "your WordPress session is
384 + * invalid" to every client on the page. What the caller needs in order to
385 + * say something useful goes into the error data instead:
386 + *
387 + * - `reason`: `quota` (out of credits or rate limited), `auth` (the site's
388 + * key was rejected), `unavailable` (the provider could not be reached or
389 + * answered 5xx) or `other`.
390 + * - `provider_status`: the provider's own HTTP status, or null when the
391 + * failure never reached the provider.
392 + * - `detail`: the provider's message, verbatim, for the console and logs.
393 + *
394 + * The top-level message says what happened in plain words. The Core AI
395 + * Client reports a rejected request as `prompt_client_error` /
396 + * `prompt_upstream_server_error` with the provider's status in
397 + * `data.status` and a message of the shape "Too Many Requests (429) -
398 + * <provider text>", which is why the provider text never reached the
399 + * widget as anything but that string.
400 + *
401 + * @param WP_Error $error Failed generation.
402 + * @return WP_Error
403 + */
404 +function openstation_drafts_ai_failure( WP_Error $error ) {
405 + $code = (string) $error->get_error_code();
406 + $data = $error->get_error_data();
407 + $detail = (string) $error->get_error_message();
408 +
409 + $provider_status = null;
410 + if ( in_array( $code, array( 'prompt_client_error', 'prompt_upstream_server_error' ), true )
411 + && is_array( $data ) && isset( $data['status'] ) ) {
412 + $provider_status = (int) $data['status'];
413 + }
414 +
415 + if ( 'prompt_network_error' === $code || ( null !== $provider_status && $provider_status >= 500 ) ) {
416 + $reason = 'unavailable';
417 + $message = __( 'The AI provider could not be reached. Try again in a moment.', 'desktop-mode' );
418 + } elseif ( in_array( $provider_status, array( 402, 429 ), true ) ) {
419 + $reason = 'quota';
420 + $message = __( 'The AI provider has no credits left or is rate limiting this site. Check its plan and billing, or try again later.', 'desktop-mode' );
421 + } elseif ( in_array( $provider_status, array( 401, 403 ), true ) ) {
422 + $reason = 'auth';
423 + $message = __( 'The AI provider rejected this site’s API key. Check the key in Settings → Connectors.', 'desktop-mode' );
424 + } elseif ( null === $provider_status && preg_match( '/quota|credits?\b|billing|rate limit/i', $detail ) ) {
425 + // A provider that failed without a status (the SDK threw instead of
426 + // answering) can still say it was the account, not the request.
427 + $reason = 'quota';
428 + $message = __( 'The AI provider has no credits left or is rate limiting this site. Check its plan and billing, or try again later.', 'desktop-mode' );
429 + } else {
430 + $reason = 'other';
431 + $message = __( 'The AI provider could not produce suggestions.', 'desktop-mode' );
432 + }
433 +
434 + return new WP_Error(
435 + 'openstation_ai_failed',
436 + $message,
437 + array(
438 + 'status' => 502,
439 + 'reason' => $reason,
440 + 'provider_status' => $provider_status,
441 + 'detail' => $detail,
442 + )
443 + );
379 444 }
380 445
381 446 /**
382 447 * Trim, tag-strip and cap a list of model-supplied strings.