PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.10
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.10
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 0.8.9 0.8.8 0.8.7 All 34 releases
← All changes | includes/ai-copilot/analysis.php +54 -205 0.8.91.1.10 View file →
@@ -1,27 +1,39 @@
1 1 <?php
2 2 /**
3 - * Desktop Mode — AI Copilot analysis: prompts, schemas, meta storage.
3 + * OpenStation — AI Copilot analysis: prompts, schemas, meta storage.
4 4 *
5 5 * This module owns:
6 - * - JSON Schema definitions for each entity type (filterable).
7 - * - Prompt builders that convert WP entities into chat message arrays.
8 - * - Meta read/write helpers so job callbacks never touch meta keys
9 - * directly; the key names live in one place.
6 + * - The JSON Schema for comment analysis (filterable).
7 + * - The prompt builder that converts a comment into a chat message array.
8 + * - Meta read/write helpers so the job callback never touches meta keys
9 + * directly; the key name lives in one place.
10 10 *
11 - * Meta key used for all entity types: `_desktop_mode_ai_analysis` (prefixed
12 - * underscore → hidden from the Custom Fields UI by default).
11 + * Nothing is analyzed automatically. Comment analysis runs on demand only,
12 + * through the `desktop-mode/analyze-comment` ability; posts, pages and terms
13 + * are not analyzed at all.
13 14 *
14 - * @package WPDesktopMode
15 + * Meta key: `_desktop_mode_ai_analysis` (prefixed underscore → hidden from
16 + * the Custom Fields UI by default).
17 + *
18 + * @package OpenStation
15 19 */
16 20
17 21 defined( 'ABSPATH' ) || exit;
18 22
19 -/** Meta key used across posts, terms, and comments. */
20 -const DESKTOP_MODE_AI_META_KEY = '_desktop_mode_ai_analysis';
23 +/**
24 + * Meta key used to store the per-comment AI analysis.
25 + *
26 + * The VALUE keeps its pre-rebrand spelling on purpose: it is a
27 + * persisted or externally-visible identifier, so renaming it would
28 + * orphan data already written by live installs (or break a live
29 + * URL). The mismatch between this constant's name and its value is
30 + * deliberate — it is NOT a half-finished rename.
31 + */
32 +const OPENSTATION_AI_META_KEY = '_desktop_mode_ai_analysis';
21 33
22 -/** Max characters of post content / comment text sent to OpenAI. */
23 -const DESKTOP_MODE_AI_CONTENT_MAX_CHARS = 3000;
34 +/** Max characters of comment text sent to the provider. */
35 +const OPENSTATION_AI_CONTENT_MAX_CHARS = 3000;
24 36
25 37 // ---------------------------------------------------------------------------
26 38 // JSON Schemas
27 39 // ---------------------------------------------------------------------------
@@ -26,59 +38,16 @@
26 38 // JSON Schemas
27 39 // ---------------------------------------------------------------------------
28 40
29 41 /**
30 - * JSON Schema for post/page/term analysis.
31 - *
32 - * OpenAI strict mode requires `additionalProperties: false` at every level
33 - * and all properties listed in `required`.
34 - *
35 - * @since 0.14.0
36 - *
37 - * @return array
38 - */
39 -function desktop_mode_ai_schema_content() {
40 - $schema = array(
41 - 'type' => 'object',
42 - 'additionalProperties' => false,
43 - 'required' => array( 'topic', 'ai_summary' ),
44 - 'properties' => array(
45 - 'topic' => array(
46 - 'type' => 'string',
47 - 'description' => 'A concise topic label (max 10 words) capturing the main subject.',
48 - ),
49 - 'ai_summary' => array(
50 - 'type' => 'string',
51 - 'description' => 'A 2-3 sentence summary of the content, written in plain language.',
52 - ),
53 - ),
54 - );
55 -
56 - /**
57 - * Filters the JSON Schema used for post/page/term AI analysis.
58 - *
59 - * Must comply with OpenAI strict JSON Schema rules: every object level
60 - * needs `additionalProperties: false` and all property names listed in
61 - * `required`. Changing the shape here also requires updating any JS or
62 - * PHP code that reads `_desktop_mode_ai_analysis` meta.
63 - *
64 - * @since 0.14.0
65 - *
66 - * @param array $schema The JSON Schema array.
67 - */
68 - return (array) apply_filters( 'desktop_mode_ai_schema_content', $schema );
69 -}
70 -
71 -/**
72 42 * JSON Schema for comment analysis.
73 43 *
74 - * Extends the content schema with `harmful` and `spam` booleans.
44 + * Captures a topic label and summary plus the `harmful` and `spam`
45 + * booleans that drive the comments-window spam score.
75 46 *
76 - * @since 0.14.0
77 - *
78 47 * @return array
79 48 */
80 -function desktop_mode_ai_schema_comment() {
49 +function openstation_ai_schema_comment() {
81 50 $schema = array(
82 51 'type' => 'object',
83 52 'additionalProperties' => false,
84 53 'required' => array( 'topic', 'ai_summary', 'harmful', 'spam' ),
@@ -104,13 +73,11 @@
104 73
105 74 /**
106 75 * Filters the JSON Schema used for comment AI analysis.
107 76 *
108 - * @since 0.14.0
109 - *
110 77 * @param array $schema The JSON Schema array.
111 78 */
112 - return (array) apply_filters( 'desktop_mode_ai_schema_comment', $schema );
79 + return (array) apply_filters( 'openstation_ai_schema_comment', $schema );
113 80 }
114 81
115 82 // ---------------------------------------------------------------------------
116 83 // Prompt builders
@@ -116,113 +83,21 @@
116 83 // Prompt builders
117 84 // ---------------------------------------------------------------------------
118 85
119 86 /**
120 - * Builds the messages array for a post or page.
121 - *
122 - * @since 0.14.0
123 - *
124 - * @param WP_Post $post
125 - * @return array Chat messages array.
126 - */
127 -function desktop_mode_ai_messages_for_post( WP_Post $post ) {
128 - $type = ucfirst( $post->post_type );
129 - $title = wp_strip_all_tags( $post->post_title );
130 - $content = wp_strip_all_tags( $post->post_content );
131 - $content = preg_replace( '/\s+/', ' ', trim( $content ) );
132 - $content = mb_substr( $content, 0, DESKTOP_MODE_AI_CONTENT_MAX_CHARS );
133 - $excerpt = wp_strip_all_tags( $post->post_excerpt );
134 -
135 - $user_text = "Analyze the following WordPress {$type}.\n\n";
136 - $user_text .= "Title: {$title}\n";
137 - if ( $excerpt ) {
138 - $user_text .= "Excerpt: {$excerpt}\n";
139 - }
140 - $user_text .= "Content:\n{$content}";
141 -
142 - /**
143 - * Filters the user message sent to OpenAI for post/page analysis.
144 - *
145 - * @since 0.14.0
146 - *
147 - * @param string $user_text The composed user message.
148 - * @param WP_Post $post The post being analyzed.
149 - */
150 - $user_text = (string) apply_filters( 'desktop_mode_ai_post_prompt', $user_text, $post );
151 -
152 - return array(
153 - array(
154 - 'role' => 'system',
155 - 'content' => 'You are a content analysis assistant for a WordPress site. Analyze the provided content objectively and return structured data exactly matching the required schema.',
156 - ),
157 - array(
158 - 'role' => 'user',
159 - 'content' => $user_text,
160 - ),
161 - );
162 -}
163 -
164 -/**
165 - * Builds the messages array for a taxonomy term (category, tag, etc.).
166 - *
167 - * @since 0.14.0
168 - *
169 - * @param WP_Term $term
170 - * @return array Chat messages array.
171 - */
172 -function desktop_mode_ai_messages_for_term( WP_Term $term ) {
173 - $taxonomy = ucwords( str_replace( '_', ' ', $term->taxonomy ) );
174 - $name = $term->name;
175 - $description = wp_strip_all_tags( $term->description );
176 - $description = mb_substr( preg_replace( '/\s+/', ' ', trim( $description ) ), 0, DESKTOP_MODE_AI_CONTENT_MAX_CHARS );
177 -
178 - $user_text = "Analyze the following WordPress {$taxonomy} term.\n\n";
179 - $user_text .= "Name: {$name}\n";
180 - if ( $description ) {
181 - $user_text .= "Description: {$description}";
182 - } else {
183 - $user_text .= 'No description provided. Base your analysis on the term name alone.';
184 - }
185 -
186 - /**
187 - * Filters the user message sent to OpenAI for term analysis.
188 - *
189 - * @since 0.14.0
190 - *
191 - * @param string $user_text The composed user message.
192 - * @param WP_Term $term The term being analyzed.
193 - */
194 - $user_text = (string) apply_filters( 'desktop_mode_ai_term_prompt', $user_text, $term );
195 -
196 - return array(
197 - array(
198 - 'role' => 'system',
199 - 'content' => 'You are a content analysis assistant for a WordPress site. Analyze the provided taxonomy term and return structured data exactly matching the required schema.',
200 - ),
201 - array(
202 - 'role' => 'user',
203 - 'content' => $user_text,
204 - ),
205 - );
206 -}
207 -
208 -/**
209 87 * Builds the messages array for a comment.
210 88 *
211 - * When the parent post has an existing AI analysis, its summary is
212 - * included so the model can accurately judge `spam` (off-topic detection
213 - * requires knowing what the post is about).
89 + * The parent post's title is included so the model can judge `spam`
90 + * (off-topic detection requires knowing what the post is about).
214 91 *
215 - * @since 0.14.0
216 - *
217 92 * @param WP_Comment $comment
218 93 * @return array Chat messages array.
219 94 */
220 -function desktop_mode_ai_messages_for_comment( WP_Comment $comment ) {
95 +function openstation_ai_messages_for_comment( WP_Comment $comment ) {
221 96 $text = wp_strip_all_tags( $comment->comment_content );
222 - $text = mb_substr( preg_replace( '/\s+/', ' ', trim( $text ) ), 0, DESKTOP_MODE_AI_CONTENT_MAX_CHARS );
97 + $text = mb_substr( preg_replace( '/\s+/', ' ', trim( $text ) ), 0, OPENSTATION_AI_CONTENT_MAX_CHARS );
223 98
224 - $user_text = "Analyze the following WordPress comment.\n\n";
99 + $user_text = "Analyze the following WordPress comment.\n\n";
225 100 $user_text .= "Comment:\n{$text}\n\n";
226 101
227 102 // Give the model post context so it can evaluate relevance (spam).
228 103 $post_id = (int) $comment->comment_post_ID;
@@ -230,13 +105,8 @@
230 105 $post = get_post( $post_id );
231 106 if ( $post instanceof WP_Post ) {
232 107 $user_text .= 'Post title: ' . wp_strip_all_tags( $post->post_title ) . "\n";
233 108 }
234 -
235 - $post_analysis = desktop_mode_ai_get_meta( 'post', $post_id );
236 - if ( $post_analysis && ! empty( $post_analysis['ai_summary'] ) ) {
237 - $user_text .= 'Post summary: ' . $post_analysis['ai_summary'] . "\n";
238 - }
239 109 }
240 110
241 111 $user_text .= "\n\nClassification rules:\n";
242 112 $user_text .= "- `harmful = true`: the comment is hostile, insulting, or demeaning — e.g. attacks on the author's competence, aggressive rhetoric, threats, hate speech. Tone matters: an angry rant calling the article \"garbage\" is harmful even without explicit language.\n";
@@ -241,19 +111,17 @@
241 111 $user_text .= "\n\nClassification rules:\n";
242 112 $user_text .= "- `harmful = true`: the comment is hostile, insulting, or demeaning — e.g. attacks on the author's competence, aggressive rhetoric, threats, hate speech. Tone matters: an angry rant calling the article \"garbage\" is harmful even without explicit language.\n";
243 113 $user_text .= "- `spam = true`: the comment is promotional or off-topic — e.g. commercial links, ALL CAPS sales copy, \"CLICK HERE\" / \"BOOK NOW\", generic praise unrelated to the post.\n";
244 114 $user_text .= "- These are INDEPENDENT flags. A hostile but on-topic comment is harmful=true, spam=false. A promotional but politely worded comment is spam=true, harmful=false. Both can be true simultaneously.\n";
245 - $user_text .= "- The `topic` and `ai_summary` fields MUST capture the tone and sentiment so that search queries like \"negative comment\", \"angry reader\", or \"spam\" return the correct results.";
115 + $user_text .= '- The `topic` and `ai_summary` fields MUST capture the tone and sentiment so that search queries like "negative comment", "angry reader", or "spam" return the correct results.';
246 116
247 117 /**
248 - * Filters the user message sent to OpenAI for comment analysis.
118 + * Filters the user message sent to the provider for comment analysis.
249 119 *
250 - * @since 0.14.0
251 - *
252 120 * @param string $user_text The composed user message.
253 121 * @param WP_Comment $comment The comment being analyzed.
254 122 */
255 - $user_text = (string) apply_filters( 'desktop_mode_ai_comment_prompt', $user_text, $comment );
123 + $user_text = (string) apply_filters( 'openstation_ai_comment_prompt', $user_text, $comment );
256 124
257 125 return array(
258 126 array(
259 127 'role' => 'system',
@@ -270,20 +138,22 @@
270 138 // Meta read / write
271 139 // ---------------------------------------------------------------------------
272 140
273 141 /**
274 - * Saves an AI analysis result as meta for the given entity.
142 + * Saves an AI analysis result as comment meta.
275 143 *
276 - * @since 0.14.0
144 + * Comments are the only entity the copilot analyzes. The `$entity_type`
145 + * parameter is retained for call-site/signature stability but only
146 + * `'comment'` is supported — any other value is a no-op that returns false.
277 147 *
278 - * @param string $entity_type 'post' | 'term' | 'comment'.
279 - * @param int $entity_id
280 - * @param array $analysis The structured output array from OpenAI.
148 + * @param string $entity_type Only `'comment'` is supported.
149 + * @param int $entity_id Comment ID.
150 + * @param array $analysis The structured output array from the provider.
281 151 * @return bool
282 152 */
283 -function desktop_mode_ai_save_meta( $entity_type, $entity_id, array $analysis ) {
153 +function openstation_ai_save_meta( $entity_type, $entity_id, array $analysis ) {
284 154 $entity_id = (int) $entity_id;
285 - if ( $entity_id <= 0 ) {
155 + if ( $entity_id <= 0 || 'comment' !== $entity_type ) {
286 156 return false;
287 157 }
288 158
289 159 // Stamp when the analysis was performed so consumers can detect staleness.
@@ -288,48 +158,27 @@
288 158
289 159 // Stamp when the analysis was performed so consumers can detect staleness.
290 160 $analysis['analyzed_at'] = time();
291 161
292 - switch ( $entity_type ) {
293 - case 'post':
294 - return false !== update_post_meta( $entity_id, DESKTOP_MODE_AI_META_KEY, $analysis );
295 -
296 - case 'term':
297 - return false !== update_term_meta( $entity_id, DESKTOP_MODE_AI_META_KEY, $analysis );
298 -
299 - case 'comment':
300 - return false !== update_comment_meta( $entity_id, DESKTOP_MODE_AI_META_KEY, $analysis );
301 - }
302 -
303 - return false;
162 + return false !== update_comment_meta( $entity_id, OPENSTATION_AI_META_KEY, $analysis );
304 163 }
305 164
306 165 /**
307 - * Retrieves a previously saved AI analysis, or null if none exists.
166 + * Retrieves a previously saved comment AI analysis, or null if none exists.
308 167 *
309 - * @since 0.14.0
168 + * Only `'comment'` is supported (see {@see openstation_ai_save_meta}); any
169 + * other `$entity_type` returns null.
310 170 *
311 - * @param string $entity_type 'post' | 'term' | 'comment'.
312 - * @param int $entity_id
171 + * @param string $entity_type Only `'comment'` is supported.
172 + * @param int $entity_id Comment ID.
313 173 * @return array|null
314 174 */
315 -function desktop_mode_ai_get_meta( $entity_type, $entity_id ) {
175 +function openstation_ai_get_meta( $entity_type, $entity_id ) {
316 176 $entity_id = (int) $entity_id;
317 - if ( $entity_id <= 0 ) {
177 + if ( $entity_id <= 0 || 'comment' !== $entity_type ) {
318 178 return null;
319 179 }
320 180
321 - $raw = null;
322 - switch ( $entity_type ) {
323 - case 'post':
324 - $raw = get_post_meta( $entity_id, DESKTOP_MODE_AI_META_KEY, true );
325 - break;
326 - case 'term':
327 - $raw = get_term_meta( $entity_id, DESKTOP_MODE_AI_META_KEY, true );
328 - break;
329 - case 'comment':
330 - $raw = get_comment_meta( $entity_id, DESKTOP_MODE_AI_META_KEY, true );
331 - break;
332 - }
181 + $raw = get_comment_meta( $entity_id, OPENSTATION_AI_META_KEY, true );
333 182
334 183 return is_array( $raw ) && ! empty( $raw ) ? $raw : null;
335 184 }