PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.8.8
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.8.8
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
desktop-mode / includes / ai-copilot / reindex.php

reindex.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.8.8, at includes/ai-copilot/reindex.php

305 lines 9.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Desktop Mode — AI Copilot bulk re-index endpoint.
4 *
5 * Finds posts, pages, and comments that have NOT yet been analyzed
6 * (i.e. they are missing the `_desktop_mode_ai_analysis` meta) and schedules
7 * an analysis cron job for each one. Then calls `spawn_cron()` so
8 * WordPress triggers the jobs immediately rather than waiting for the
9 * next organic page-load.
10 *
11 * This endpoint is intentionally idempotent — calling it multiple times
12 * is safe; entities that already have the meta are counted but skipped,
13 * and the deduplication transient in `desktop_mode_ai_schedule_job()` prevents
14 * double-queuing for entities that were just scheduled.
15 *
16 * Typical use cases:
17 * - Initial setup after enabling AI features.
18 * - Recovery after the hook was broken (e.g. the wp_doing_autosave bug).
19 * - Bulk re-analysis after changing the AI prompt or model.
20 *
21 * REST: POST /desktop-mode/v1/ai/reindex
22 *
23 * @package WPDesktopMode
24 */
25
26 defined( 'ABSPATH' ) || exit;
27
28 /**
29 * Registers the reindex REST route.
30 *
31 * @since 0.14.0
32 */
33 function desktop_mode_register_ai_reindex_rest_route() {
34 register_rest_route(
35 'desktop-mode/v1',
36 '/ai/reindex',
37 array(
38 'methods' => WP_REST_Server::CREATABLE,
39 'callback' => 'desktop_mode_rest_ai_reindex',
40 'permission_callback' => 'desktop_mode_rest_ai_reindex_permission',
41 'args' => array(
42 'entity_types' => array(
43 'required' => false,
44 'type' => 'array',
45 'default' => array( 'post', 'page', 'comment' ),
46 'items' => array(
47 'type' => 'string',
48 'enum' => array( 'post', 'page', 'comment' ),
49 ),
50 'description' => 'Which entity types to reindex. Defaults to all three.',
51 ),
52 'force' => array(
53 'required' => false,
54 'type' => 'boolean',
55 'default' => false,
56 'description' => 'When true, re-queues ALL entities, even those already indexed.',
57 ),
58 ),
59 )
60 );
61 }
62 add_action( 'rest_api_init', 'desktop_mode_register_ai_reindex_rest_route' );
63
64 /**
65 * Permission: logged-in admin with AI enabled.
66 *
67 * Restricted to `manage_options` (admins) so arbitrary logged-in users
68 * can't trigger bulk API calls that consume the site owner's OpenAI quota.
69 *
70 * @since 0.14.0
71 *
72 * @return bool|WP_Error
73 */
74 function desktop_mode_rest_ai_reindex_permission() {
75 if ( ! is_user_logged_in() || ! current_user_can( 'manage_options' ) ) {
76 return new WP_Error(
77 'desktop_mode_ai_forbidden',
78 'Only administrators can trigger a reindex.',
79 array( 'status' => 403 )
80 );
81 }
82 if ( ! desktop_mode_ai_is_enabled( get_current_user_id() ) ) {
83 return new WP_Error(
84 'desktop_mode_ai_disabled',
85 'AI features are not enabled. Enable them in OS Settings → AI Settings.',
86 array( 'status' => 403 )
87 );
88 }
89 return true;
90 }
91
92 /**
93 * POST /desktop-mode/v1/ai/reindex
94 *
95 * Queues analysis jobs for every unindexed entity of the requested types
96 * and immediately triggers WP-Cron so jobs start running.
97 *
98 * @since 0.14.0
99 *
100 * @param WP_REST_Request $request
101 * @return WP_REST_Response
102 */
103 function desktop_mode_rest_ai_reindex( WP_REST_Request $request ) {
104 $user_id = get_current_user_id();
105 $entity_types = (array) $request->get_param( 'entity_types' );
106 $force = (bool) $request->get_param( 'force' );
107
108 $queued = 0;
109 $already_indexed = 0;
110 $breakdown = array();
111
112 // -----------------------------------------------------------------------
113 // Posts
114 // -----------------------------------------------------------------------
115 if ( in_array( 'post', $entity_types, true ) ) {
116 $result = desktop_mode_ai_reindex_posts( 'post', $user_id, $force );
117 $queued += $result['queued'];
118 $already_indexed += $result['already_indexed'];
119 $breakdown['post'] = $result;
120 }
121
122 // -----------------------------------------------------------------------
123 // Pages
124 // -----------------------------------------------------------------------
125 if ( in_array( 'page', $entity_types, true ) ) {
126 $result = desktop_mode_ai_reindex_posts( 'page', $user_id, $force );
127 $queued += $result['queued'];
128 $already_indexed += $result['already_indexed'];
129 $breakdown['page'] = $result;
130 }
131
132 // -----------------------------------------------------------------------
133 // Comments
134 // -----------------------------------------------------------------------
135 if ( in_array( 'comment', $entity_types, true ) ) {
136 $result = desktop_mode_ai_reindex_comments( $user_id, $force );
137 $queued += $result['queued'];
138 $already_indexed += $result['already_indexed'];
139 $breakdown['comment'] = $result;
140 }
141
142 // Kick WP-Cron so jobs run without waiting for the next page-load.
143 // spawn_cron() is a no-op when DISABLE_WP_CRON is true; in that case
144 // the user needs to run `wp cron event run --due-now` manually.
145 if ( $queued > 0 ) {
146 spawn_cron();
147 }
148
149 return rest_ensure_response(
150 array(
151 'queued' => $queued,
152 'already_indexed' => $already_indexed,
153 'breakdown' => $breakdown,
154 'note' => $queued > 0
155 ? 'Jobs scheduled. If WP-Cron is disabled in your environment run: wp cron event run --due-now'
156 : 'Nothing to reindex.',
157 )
158 );
159 }
160
161 // ---------------------------------------------------------------------------
162 // Helpers
163 // ---------------------------------------------------------------------------
164
165 /**
166 * Queue analysis jobs for published posts/pages without the AI meta.
167 *
168 * Uses `fields => 'ids'` so we only load IDs — no post content pulled
169 * into memory for potentially hundreds of posts.
170 *
171 * @since 0.14.0
172 *
173 * @param string $post_type 'post' | 'page'.
174 * @param int $user_id The user whose API key the jobs should use.
175 * @param bool $force When true, re-queue even already-indexed posts.
176 * @return array{ queued: int, already_indexed: int }
177 */
178 function desktop_mode_ai_reindex_posts( $post_type, $user_id, $force = false ) {
179 $meta_query = $force
180 ? array()
181 : array(
182 array(
183 'key' => DESKTOP_MODE_AI_META_KEY,
184 'compare' => 'NOT EXISTS',
185 ),
186 );
187
188 $ids = get_posts(
189 array(
190 'post_type' => $post_type,
191 'post_status' => 'publish',
192 'posts_per_page' => -1,
193 'fields' => 'ids',
194 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- admin-initiated reindex: scans posts by EXISTS/NOT EXISTS on our AI-summary meta key so we know which posts still need a summary. Single-clause query against an indexed key.
195 'meta_query' => $meta_query,
196 )
197 );
198
199 // When not forcing, count already-indexed separately for the report.
200 $already_indexed = 0;
201 if ( ! $force ) {
202 $already_indexed = (int) ( new WP_Query(
203 array(
204 'post_type' => $post_type,
205 'post_status' => 'publish',
206 'posts_per_page' => 1,
207 'fields' => 'ids',
208 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- counter for the reindex report; single EXISTS clause.
209 'meta_query' => array(
210 array(
211 'key' => DESKTOP_MODE_AI_META_KEY,
212 'compare' => 'EXISTS',
213 ),
214 ),
215 )
216 ) )->found_posts;
217 }
218
219 $queued = 0;
220 foreach ( $ids as $post_id ) {
221 // Clear the debounce transient so force-reindex always goes through.
222 if ( $force ) {
223 delete_transient( 'desktop_mode_ai_q_' . md5( 'post_' . $post_id ) );
224 }
225 desktop_mode_ai_schedule_job(
226 'desktop_mode_ai_analyze_post',
227 array( (int) $post_id, $user_id ),
228 'post_' . $post_id
229 );
230 $queued++;
231 }
232
233 return array(
234 'queued' => $queued,
235 'already_indexed' => $already_indexed,
236 );
237 }
238
239 /**
240 * Queue analysis jobs for approved comments without the AI meta.
241 *
242 * @since 0.14.0
243 *
244 * @param int $user_id The user whose API key the jobs should use.
245 * @param bool $force When true, re-queue even already-indexed comments.
246 * @return array{ queued: int, already_indexed: int }
247 */
248 function desktop_mode_ai_reindex_comments( $user_id, $force = false ) {
249 $meta_query = $force
250 ? array()
251 : array(
252 array(
253 'key' => DESKTOP_MODE_AI_META_KEY,
254 'compare' => 'NOT EXISTS',
255 ),
256 );
257
258 $comments = get_comments(
259 array(
260 'status' => 'approve',
261 'type' => 'comment',
262 'fields' => 'ids',
263 'number' => 0, // 0 = no limit
264 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- admin-initiated comment reindex; same EXISTS/NOT EXISTS pattern as the post reindex above.
265 'meta_query' => $meta_query,
266 )
267 );
268
269 $already_indexed = 0;
270 if ( ! $force ) {
271 $already_indexed = (int) get_comments(
272 array(
273 'status' => 'approve',
274 'type' => 'comment',
275 'count' => true,
276 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- counter for the comment-reindex report; single EXISTS clause.
277 'meta_query' => array(
278 array(
279 'key' => DESKTOP_MODE_AI_META_KEY,
280 'compare' => 'EXISTS',
281 ),
282 ),
283 )
284 );
285 }
286
287 $queued = 0;
288 foreach ( $comments as $comment_id ) {
289 if ( $force ) {
290 delete_transient( 'desktop_mode_ai_q_' . md5( 'comment_' . $comment_id ) );
291 }
292 desktop_mode_ai_schedule_job(
293 'desktop_mode_ai_analyze_comment',
294 array( (int) $comment_id, $user_id ),
295 'comment_' . $comment_id
296 );
297 $queued++;
298 }
299
300 return array(
301 'queued' => $queued,
302 'already_indexed' => $already_indexed,
303 );
304 }
305