yo-bug 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +160 -0
  3. package/bin/cli.ts +17 -0
  4. package/bin/install.ts +34 -0
  5. package/bin/mcp.ts +2 -0
  6. package/dist/bin/cli.d.ts +3 -0
  7. package/dist/bin/cli.d.ts.map +1 -0
  8. package/dist/bin/cli.js +19 -0
  9. package/dist/bin/cli.js.map +1 -0
  10. package/dist/bin/install.d.ts +3 -0
  11. package/dist/bin/install.d.ts.map +1 -0
  12. package/dist/bin/install.js +28 -0
  13. package/dist/bin/install.js.map +1 -0
  14. package/dist/bin/mcp.d.ts +3 -0
  15. package/dist/bin/mcp.d.ts.map +1 -0
  16. package/dist/bin/mcp.js +3 -0
  17. package/dist/bin/mcp.js.map +1 -0
  18. package/dist/src/detect/dev-server.d.ts +11 -0
  19. package/dist/src/detect/dev-server.d.ts.map +1 -0
  20. package/dist/src/detect/dev-server.js +92 -0
  21. package/dist/src/detect/dev-server.js.map +1 -0
  22. package/dist/src/detect/spawn-dev.d.ts +4 -0
  23. package/dist/src/detect/spawn-dev.d.ts.map +1 -0
  24. package/dist/src/detect/spawn-dev.js +37 -0
  25. package/dist/src/detect/spawn-dev.js.map +1 -0
  26. package/dist/src/install/detect-ai-tool.d.ts +7 -0
  27. package/dist/src/install/detect-ai-tool.d.ts.map +1 -0
  28. package/dist/src/install/detect-ai-tool.js +38 -0
  29. package/dist/src/install/detect-ai-tool.js.map +1 -0
  30. package/dist/src/install/write-mcp-config.d.ts +6 -0
  31. package/dist/src/install/write-mcp-config.d.ts.map +1 -0
  32. package/dist/src/install/write-mcp-config.js +47 -0
  33. package/dist/src/install/write-mcp-config.js.map +1 -0
  34. package/dist/src/mcp/index.d.ts +2 -0
  35. package/dist/src/mcp/index.d.ts.map +1 -0
  36. package/dist/src/mcp/index.js +263 -0
  37. package/dist/src/mcp/index.js.map +1 -0
  38. package/dist/src/mcp/tools/checklist.d.ts +22 -0
  39. package/dist/src/mcp/tools/checklist.d.ts.map +1 -0
  40. package/dist/src/mcp/tools/checklist.js +58 -0
  41. package/dist/src/mcp/tools/checklist.js.map +1 -0
  42. package/dist/src/mcp/tools/get-feedback.d.ts +13 -0
  43. package/dist/src/mcp/tools/get-feedback.d.ts.map +1 -0
  44. package/dist/src/mcp/tools/get-feedback.js +27 -0
  45. package/dist/src/mcp/tools/get-feedback.js.map +1 -0
  46. package/dist/src/mcp/tools/list-feedbacks.d.ts +20 -0
  47. package/dist/src/mcp/tools/list-feedbacks.d.ts.map +1 -0
  48. package/dist/src/mcp/tools/list-feedbacks.js +29 -0
  49. package/dist/src/mcp/tools/list-feedbacks.js.map +1 -0
  50. package/dist/src/mcp/tools/resolve-feedback.d.ts +8 -0
  51. package/dist/src/mcp/tools/resolve-feedback.d.ts.map +1 -0
  52. package/dist/src/mcp/tools/resolve-feedback.js +28 -0
  53. package/dist/src/mcp/tools/resolve-feedback.js.map +1 -0
  54. package/dist/src/mcp/tools/start-session.d.ts +11 -0
  55. package/dist/src/mcp/tools/start-session.d.ts.map +1 -0
  56. package/dist/src/mcp/tools/start-session.js +56 -0
  57. package/dist/src/mcp/tools/start-session.js.map +1 -0
  58. package/dist/src/mcp/tools/stop-session.d.ts +6 -0
  59. package/dist/src/mcp/tools/stop-session.d.ts.map +1 -0
  60. package/dist/src/mcp/tools/stop-session.js +80 -0
  61. package/dist/src/mcp/tools/stop-session.js.map +1 -0
  62. package/dist/src/mcp/tools/test-history.d.ts +33 -0
  63. package/dist/src/mcp/tools/test-history.d.ts.map +1 -0
  64. package/dist/src/mcp/tools/test-history.js +66 -0
  65. package/dist/src/mcp/tools/test-history.js.map +1 -0
  66. package/dist/src/server/feedback-api.d.ts +4 -0
  67. package/dist/src/server/feedback-api.d.ts.map +1 -0
  68. package/dist/src/server/feedback-api.js +152 -0
  69. package/dist/src/server/feedback-api.js.map +1 -0
  70. package/dist/src/server/html-injector.d.ts +10 -0
  71. package/dist/src/server/html-injector.d.ts.map +1 -0
  72. package/dist/src/server/html-injector.js +34 -0
  73. package/dist/src/server/html-injector.js.map +1 -0
  74. package/dist/src/server/proxy-server.d.ts +8 -0
  75. package/dist/src/server/proxy-server.d.ts.map +1 -0
  76. package/dist/src/server/proxy-server.js +87 -0
  77. package/dist/src/server/proxy-server.js.map +1 -0
  78. package/dist/src/server/sdk-serve.d.ts +3 -0
  79. package/dist/src/server/sdk-serve.d.ts.map +1 -0
  80. package/dist/src/server/sdk-serve.js +20 -0
  81. package/dist/src/server/sdk-serve.js.map +1 -0
  82. package/dist/src/storage/store.d.ts +21 -0
  83. package/dist/src/storage/store.d.ts.map +1 -0
  84. package/dist/src/storage/store.js +138 -0
  85. package/dist/src/storage/store.js.map +1 -0
  86. package/dist/src/storage/types.d.ts +74 -0
  87. package/dist/src/storage/types.d.ts.map +1 -0
  88. package/dist/src/storage/types.js +2 -0
  89. package/dist/src/storage/types.js.map +1 -0
  90. package/dist/vibe-feedback.js +399 -0
  91. package/package.json +67 -0
  92. package/src/client/annotation-mode/canvas-editor.ts +178 -0
  93. package/src/client/annotation-mode/history.ts +32 -0
  94. package/src/client/annotation-mode/region-selector.ts +123 -0
  95. package/src/client/annotation-mode/screenshot.ts +17 -0
  96. package/src/client/annotation-mode/toolbar.ts +139 -0
  97. package/src/client/annotation-mode/tools/arrow.ts +57 -0
  98. package/src/client/annotation-mode/tools/base-tool.ts +25 -0
  99. package/src/client/annotation-mode/tools/circle.ts +37 -0
  100. package/src/client/annotation-mode/tools/freehand.ts +23 -0
  101. package/src/client/annotation-mode/tools/rect.ts +32 -0
  102. package/src/client/annotation-mode/tools/text.ts +93 -0
  103. package/src/client/api/client.ts +48 -0
  104. package/src/client/capture/action-recorder.ts +157 -0
  105. package/src/client/capture/console-interceptor.ts +65 -0
  106. package/src/client/capture/context-buffer.ts +23 -0
  107. package/src/client/capture/error-interceptor.ts +52 -0
  108. package/src/client/capture/network-interceptor.ts +143 -0
  109. package/src/client/core/event-bus.ts +20 -0
  110. package/src/client/core/i18n.ts +83 -0
  111. package/src/client/core/sdk.ts +373 -0
  112. package/src/client/core/shadow-host.ts +27 -0
  113. package/src/client/element-mode/highlighter.ts +79 -0
  114. package/src/client/element-mode/inspector.ts +73 -0
  115. package/src/client/element-mode/selector.ts +22 -0
  116. package/src/client/index.ts +10 -0
  117. package/src/client/styles/sdk.css.ts +222 -0
  118. package/src/client/ui/checklist-panel.ts +279 -0
  119. package/src/client/ui/feedback-panel.ts +149 -0
  120. package/src/client/ui/floating-button.ts +103 -0
  121. package/src/client/ui/toast.ts +17 -0
  122. package/src/client/ui/verify-panel.ts +111 -0
  123. package/src/detect/dev-server.ts +110 -0
  124. package/src/detect/spawn-dev.ts +50 -0
  125. package/src/install/detect-ai-tool.ts +49 -0
  126. package/src/install/write-mcp-config.ts +49 -0
  127. package/src/mcp/index.ts +327 -0
  128. package/src/mcp/tools/checklist.ts +61 -0
  129. package/src/mcp/tools/get-feedback.ts +34 -0
  130. package/src/mcp/tools/list-feedbacks.ts +37 -0
  131. package/src/mcp/tools/resolve-feedback.ts +34 -0
  132. package/src/mcp/tools/start-session.ts +65 -0
  133. package/src/mcp/tools/stop-session.ts +93 -0
  134. package/src/mcp/tools/test-history.ts +97 -0
  135. package/src/server/feedback-api.ts +164 -0
  136. package/src/server/html-injector.ts +41 -0
  137. package/src/server/proxy-server.ts +107 -0
  138. package/src/server/sdk-serve.ts +24 -0
  139. package/src/storage/store.ts +172 -0
  140. package/src/storage/types.ts +68 -0
@@ -0,0 +1,327 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { z } from 'zod';
4
+ import { FeedbackStore } from '../storage/store.js';
5
+ import { startTestSession } from './tools/start-session.js';
6
+ import { stopTestSession } from './tools/stop-session.js';
7
+ import { listFeedbacks } from './tools/list-feedbacks.js';
8
+ import { getFeedback } from './tools/get-feedback.js';
9
+ import { resolveFeedback } from './tools/resolve-feedback.js';
10
+ import { createChecklist, getChecklistStatus } from './tools/checklist.js';
11
+ import { saveTestRecord, getTestHistory } from './tools/test-history.js';
12
+
13
+ const store = new FeedbackStore();
14
+
15
+ const WORKFLOW_GUIDE = `# yo-bug — AI Workflow Guide
16
+
17
+ You have visual test feedback capabilities. Here is the complete workflow:
18
+
19
+ ## When to Use
20
+ - After writing or modifying frontend code, before telling the user "it's done"
21
+ - When the user says "let me test" or "something looks wrong"
22
+ - When you want the user to verify your changes
23
+
24
+ ## Complete Workflow (follow this order)
25
+
26
+ ### 1. Start test session
27
+ Call start_test_session() to launch the test environment.
28
+ This auto-detects the dev server, starts a reverse proxy with the test SDK injected, and opens the browser.
29
+ The user's project code is NOT modified — the SDK is injected via the proxy.
30
+
31
+ ### 2. Check test history (if applicable)
32
+ Call get_test_history(module) to see if this module has been tested before.
33
+ Look for frequently failing scenarios — make sure your checklist covers them.
34
+
35
+ ### 3. Push a test checklist
36
+ Call create_checklist(title, items) with structured test items.
37
+ Follow the 8 QA dimensions in the tool description. Each item needs:
38
+ - step: exactly what the user should do
39
+ - expect: exactly what they should see
40
+ - priority: "critical" for core paths, "normal" for edge cases
41
+ - dimension: which QA dimension this tests
42
+
43
+ ### 4. Wait for the user to test
44
+ Tell the user: "Test session is ready. Go through the checklist in the browser. When you find issues, press Alt+Q to quick-flag them, or Alt+S to screenshot and annotate."
45
+ The user tests at their own pace. Don't rush them.
46
+
47
+ ### 5. Check results
48
+ Call get_checklist_status() to see pass/fail results.
49
+ Call list_feedbacks() to see any bug reports the user submitted.
50
+ Call get_feedback(id) to see full details including:
51
+ - Annotated screenshots (returned as image content you can see)
52
+ - Element CSS selectors and computed styles
53
+ - Console errors and stack traces
54
+ - Failed network requests
55
+ - User's action recording (what they clicked/typed before the bug)
56
+
57
+ ### 6. Fix issues
58
+ Fix the code based on the feedback details.
59
+ After fixing each issue, call resolve_feedback(id).
60
+ This pushes a "Verify Fix" card to the user's browser — they click "Fixed" or "Still broken".
61
+
62
+ ### 7. Save test record
63
+ Call save_test_record(module, title, items, results, failedItems) to persist the test results.
64
+ Next time you modify this module, get_test_history will show what failed before.
65
+
66
+ ### 8. End session
67
+ When testing is complete, call stop_test_session().
68
+ This returns a session summary with stats and weak dimension analysis.
69
+
70
+ ## Keyboard Shortcuts (tell user about these)
71
+ - Alt+Q: Quick flag mode — click an element to instantly report it
72
+ - Alt+D: Detail mode — click an element, then describe the issue
73
+ - Alt+S: Screenshot mode — drag to select area, annotate with drawings
74
+ - Alt+X: Toggle test mode
75
+ - Esc: Exit test mode
76
+
77
+ ## Tips
78
+ - The user's browser automatically captures console errors, network failures, and action steps. You don't need to ask them to check DevTools.
79
+ - When you see a feedback with action steps, you can trace the exact reproduction path.
80
+ - Screenshots are returned as image content blocks — you can see the annotations directly.
81
+ - The checklist appears in the user's browser automatically. You don't need to list the items in chat.
82
+ `;
83
+
84
+ const server = new McpServer({
85
+ name: 'yo-bug',
86
+ version: '0.1.0',
87
+ });
88
+
89
+ // --- Resource: workflow guide ---
90
+ server.resource(
91
+ 'workflow-guide',
92
+ 'yo-bug://workflow',
93
+ { description: 'Complete workflow guide for using yo-bug. READ THIS FIRST before calling any tools.', mimeType: 'text/markdown' },
94
+ async () => ({
95
+ contents: [{
96
+ uri: 'yo-bug://workflow',
97
+ mimeType: 'text/markdown',
98
+ text: WORKFLOW_GUIDE,
99
+ }],
100
+ })
101
+ );
102
+
103
+ // --- Tool: start_test_session ---
104
+ server.tool(
105
+ 'start_test_session',
106
+ `Start test mode. Auto-detects dev server, starts a reverse proxy with injected test feedback SDK, and opens the browser.
107
+
108
+ After calling this, follow these steps:
109
+ 1. Call get_test_history(module) to check past test records
110
+ 2. Call create_checklist(title, items) to push a structured test plan
111
+ 3. Tell the user to test. Shortcuts: Alt+Q (quick flag), Alt+D (describe), Alt+S (screenshot), Esc (exit)
112
+ 4. Call get_checklist_status() and list_feedbacks() to read results
113
+ 5. Fix issues, call resolve_feedback(id) for each — user verifies in browser
114
+ 6. Call save_test_record() to persist results
115
+ 7. Call stop_test_session() when done`,
116
+ {
117
+ port: z.number().optional().describe('Dev server port. Auto-detected from package.json if not provided.'),
118
+ open: z.boolean().optional().describe('Whether to auto-open browser. Default: true.'),
119
+ },
120
+ async ({ port, open }) => {
121
+ const result = await startTestSession(store, { port, open });
122
+ return {
123
+ content: [{ type: 'text', text: result.message }],
124
+ isError: result.status === 'error',
125
+ };
126
+ }
127
+ );
128
+
129
+ // --- Tool: stop_test_session ---
130
+ server.tool(
131
+ 'stop_test_session',
132
+ 'Stop test mode. Shuts down the reverse proxy and any auto-started dev server. Returns a test session summary.',
133
+ {},
134
+ async () => {
135
+ const result = await stopTestSession(store);
136
+ return {
137
+ content: [{ type: 'text', text: result.message }],
138
+ };
139
+ }
140
+ );
141
+
142
+ // --- Tool: list_feedbacks ---
143
+ server.tool(
144
+ 'list_feedbacks',
145
+ 'List user-submitted test feedback. Returns a summary of each feedback item.',
146
+ {
147
+ status: z.enum(['open', 'verify', 'resolved', 'all']).optional().describe('Filter by status. Default: open.'),
148
+ type: z.string().optional().describe('Filter by problem type: bug, ui-issue, performance, feature-request, other.'),
149
+ limit: z.number().optional().describe('Max items to return. Default: 20.'),
150
+ },
151
+ async ({ status, type, limit }) => {
152
+ const result = await listFeedbacks(store, { status, type, limit });
153
+ return {
154
+ content: [{
155
+ type: 'text',
156
+ text: result.text + '\n\n' + JSON.stringify(result.items, null, 2),
157
+ }],
158
+ };
159
+ }
160
+ );
161
+
162
+ // --- Tool: get_feedback ---
163
+ server.tool(
164
+ 'get_feedback',
165
+ 'Get full details of a feedback item, including element info, console errors, network errors, action steps, and annotated screenshot (as image content).',
166
+ {
167
+ id: z.string().describe('Feedback ID.'),
168
+ },
169
+ async ({ id }) => {
170
+ const result = await getFeedback(store, { id });
171
+ if (result.error) {
172
+ return { content: [{ type: 'text', text: result.text! }], isError: true };
173
+ }
174
+ return { content: result.content! };
175
+ }
176
+ );
177
+
178
+ // --- Tool: resolve_feedback ---
179
+ server.tool(
180
+ 'resolve_feedback',
181
+ 'Mark a feedback item as fixed. This pushes a verification request to the browser — the user will confirm whether the fix actually works. Status changes to "verify" until user confirms.',
182
+ {
183
+ id: z.string().describe('Feedback ID.'),
184
+ },
185
+ async ({ id }) => {
186
+ const result = await resolveFeedback(store, { id });
187
+ return {
188
+ content: [{ type: 'text', text: result.text }],
189
+ isError: result.error,
190
+ };
191
+ }
192
+ );
193
+
194
+ // --- Tool: create_checklist ---
195
+ server.tool(
196
+ 'create_checklist',
197
+ `Create a test checklist and push it to the user's browser. Users can go through items one by one and mark each as passed/failed.
198
+
199
+ You MUST systematically generate the checklist based on the actual code changes, reviewing each of the 8 test dimensions below. Include items for every dimension that is relevant to your changes:
200
+
201
+ 1. Happy path (critical)
202
+ - Complete positive flow of core functionality
203
+ - Use realistic data (not "test"/"123")
204
+ - Verify results display correctly and remain interactive
205
+
206
+ 2. Empty/boundary values
207
+ - Submit all input fields empty
208
+ - Input only whitespace
209
+ - Input exceeding max length (200+ characters)
210
+ - Special characters: < > " ' & ; \` ${"${}"}
211
+ - Numeric fields: 0, negative, decimal, non-numeric text
212
+ - Date fields: past, future, invalid format
213
+ - Lists/tables: 0 items, 1 item, 100+ items
214
+
215
+ 3. Error states
216
+ - Submit while offline (DevTools Network → Offline)
217
+ - Server returns 400/500 error
218
+ - Request timeout (DevTools Network → Slow 3G)
219
+ - Error messages are clear and positioned near the problem
220
+ - Can recover and retry after an error
221
+
222
+ 4. Duplicate operations
223
+ - Rapidly click submit button 3 times
224
+ - Submit the same form data twice consecutively
225
+ - Trigger action while previous request is still pending
226
+ - Buttons/forms show disabled state during processing
227
+
228
+ 5. State recovery
229
+ - Refresh page (F5) after completing action — data persists?
230
+ - Browser back then forward — state correct?
231
+ - Direct URL access to a deep page
232
+ - Close tab and reopen
233
+ - Refresh mid-way through a form — data recovered or warning shown?
234
+
235
+ 6. Loading/async
236
+ - First load shows loading state (not blank)
237
+ - Failed data load shows empty state or error message
238
+ - Can interact with other parts while loading
239
+ - UI updates promptly after data changes
240
+
241
+ 7. Responsive
242
+ - Shrink browser to 375px width (or DevTools mobile mode)
243
+ - Key buttons and forms are usable, not obscured
244
+ - Text doesn't overflow or get clipped
245
+ - No horizontal scrollbar
246
+
247
+ 8. Interaction details
248
+ - Tab key follows logical focus order
249
+ - Enter key submits forms
250
+ - Escape key closes modals/cancels actions
251
+ - Disabled buttons are truly non-clickable
252
+ - Focus moves to the correct element after actions
253
+
254
+ Each item MUST have a specific step and expected result.
255
+ Step should be specific: "Type 200 'a' characters in the email field, click Submit".
256
+ Expected should be specific: "Red error text appears below email field: 'Invalid email format'".
257
+ Never write vague expectations like "should work normally".
258
+
259
+ Important: Before generating a checklist, call get_test_history to check historical test records for this module. Focus on previously failed scenarios. After testing is complete, call save_test_record to save the results.`,
260
+ {
261
+ title: z.string().describe('Checklist title. Briefly describe test scope, e.g. "Login feature test".'),
262
+ items: z.array(z.object({
263
+ step: z.string().describe('Specific action steps for the user. Be concrete: what to click, what to type, under what conditions.'),
264
+ expect: z.string().describe('Expected result after the action. Be concrete: what text appears, where it navigates, what changes.'),
265
+ priority: z.enum(['critical', 'normal']).optional().describe('critical = core path must pass, normal = boundary/error/detail scenario. Default: normal.'),
266
+ dimension: z.string().optional().describe('Test dimension: happy-path / empty-boundary / error-state / duplicate-ops / state-recovery / loading-async / responsive / interaction-detail'),
267
+ })).describe('Test items. List critical items first, then normal, ensuring systematic coverage.'),
268
+ },
269
+ async ({ title, items }) => {
270
+ const result = await createChecklist({ title, items });
271
+ return { content: [{ type: 'text', text: result.text }] };
272
+ }
273
+ );
274
+
275
+ // --- Tool: get_checklist_status ---
276
+ server.tool(
277
+ 'get_checklist_status',
278
+ 'Get current checklist status: which items passed, failed, or are pending, and any user feedback on failed items.',
279
+ {},
280
+ async () => {
281
+ const result = await getChecklistStatus();
282
+ return { content: [{ type: 'text', text: result.text }] };
283
+ }
284
+ );
285
+
286
+ // --- Tool: save_test_record ---
287
+ server.tool(
288
+ 'save_test_record',
289
+ 'Save test results for a module. Call after checklist testing is complete to accumulate test history. This data is used by get_test_history to identify frequently failing scenarios.',
290
+ {
291
+ module: z.string().describe('Feature module name, e.g. "login", "order-submit", "user-management".'),
292
+ title: z.string().describe('Test session title.'),
293
+ items: z.array(z.any()).describe('Test items (from get_checklist_status).'),
294
+ results: z.object({
295
+ passed: z.number(),
296
+ failed: z.number(),
297
+ total: z.number(),
298
+ }).describe('Test result counts.'),
299
+ failedItems: z.array(z.string()).describe('Descriptions of failed test items.'),
300
+ },
301
+ async ({ module, title, items, results, failedItems }) => {
302
+ const result = await saveTestRecord({ module, title, items, results, failedItems });
303
+ return { content: [{ type: 'text', text: result.text }] };
304
+ }
305
+ );
306
+
307
+ // --- Tool: get_test_history ---
308
+ server.tool(
309
+ 'get_test_history',
310
+ 'Get historical test records for a module, including frequently failing scenarios. Call this BEFORE generating a new checklist to ensure coverage of previously problematic areas.',
311
+ {
312
+ module: z.string().describe('Feature module name, e.g. "login", "order-submit", "user-management".'),
313
+ },
314
+ async ({ module }) => {
315
+ const result = await getTestHistory({ module });
316
+ return { content: [{ type: 'text', text: result.text }] };
317
+ }
318
+ );
319
+
320
+ // --- Start ---
321
+ async function main() {
322
+ await store.init();
323
+ const transport = new StdioServerTransport();
324
+ await server.connect(transport);
325
+ }
326
+
327
+ main().catch(console.error);
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Create a test checklist and push it to the browser.
3
+ */
4
+ export async function createChecklist(args: {
5
+ title: string;
6
+ items: { step: string; expect: string; priority?: string; dimension?: string }[];
7
+ }): Promise<{ text: string }> {
8
+ try {
9
+ const res = await fetch('http://localhost:3695/api/checklist', {
10
+ method: 'POST',
11
+ headers: { 'Content-Type': 'application/json' },
12
+ body: JSON.stringify({ title: args.title, items: args.items }),
13
+ });
14
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
15
+ const data = await res.json();
16
+ return {
17
+ text: `测试清单已推送到浏览器(${data.count} 项)。用户可以在页面上逐项测试并勾选结果。`,
18
+ };
19
+ } catch {
20
+ return { text: '推送失败。测试模式可能未启动。' };
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Get the current checklist status from the browser.
26
+ */
27
+ export async function getChecklistStatus(): Promise<{ text: string; data: any }> {
28
+ try {
29
+ const res = await fetch('http://localhost:3695/api/checklist');
30
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
31
+ const data = await res.json();
32
+
33
+ if (!data.items || data.items.length === 0) {
34
+ return { text: '当前没有测试清单。', data: null };
35
+ }
36
+
37
+ const passed = data.items.filter((i: any) => i.status === 'passed').length;
38
+ const failed = data.items.filter((i: any) => i.status === 'failed').length;
39
+ const pending = data.items.filter((i: any) => i.status === 'pending').length;
40
+
41
+ const lines = [
42
+ `测试清单: ${data.title}`,
43
+ `进度: ${passed} 通过 / ${failed} 失败 / ${pending} 待测`,
44
+ '',
45
+ ];
46
+
47
+ for (const item of data.items) {
48
+ const icon = item.status === 'passed' ? '[PASS]' :
49
+ item.status === 'failed' ? '[FAIL]' : '[ ]';
50
+ const priority = item.priority === 'critical' ? ' [!]' : '';
51
+ const dim = item.dimension ? ` [${item.dimension}]` : '';
52
+ lines.push(`${icon}${priority}${dim} ${item.step}`);
53
+ lines.push(` 预期: ${item.expect}`);
54
+ if (item.feedback) lines.push(` 反馈: ${item.feedback}`);
55
+ }
56
+
57
+ return { text: lines.join('\n'), data };
58
+ } catch {
59
+ return { text: '无法获取清单状态。测试模式可能未启动。', data: null };
60
+ }
61
+ }
@@ -0,0 +1,34 @@
1
+ import { FeedbackStore } from '../../storage/store.js';
2
+
3
+ export async function getFeedback(
4
+ store: FeedbackStore,
5
+ args: { id: string }
6
+ ) {
7
+ await store.init();
8
+ const item = await store.getById(args.id);
9
+ if (!item) {
10
+ return { error: true, text: `反馈 ${args.id} 不存在。` };
11
+ }
12
+
13
+ // Build content blocks
14
+ const content: any[] = [
15
+ {
16
+ type: 'text' as const,
17
+ text: JSON.stringify(item, null, 2),
18
+ },
19
+ ];
20
+
21
+ // Include screenshot if available
22
+ if (item.hasScreenshot) {
23
+ const screenshot = await store.getScreenshot(args.id);
24
+ if (screenshot) {
25
+ content.push({
26
+ type: 'image' as const,
27
+ data: screenshot.toString('base64'),
28
+ mimeType: 'image/png',
29
+ });
30
+ }
31
+ }
32
+
33
+ return { error: false, content };
34
+ }
@@ -0,0 +1,37 @@
1
+ import { FeedbackStore } from '../../storage/store.js';
2
+
3
+ export async function listFeedbacks(
4
+ store: FeedbackStore,
5
+ args: { status?: string; type?: string; limit?: number }
6
+ ) {
7
+ await store.init();
8
+ const result = await store.list({
9
+ status: args.status || 'open',
10
+ type: args.type,
11
+ limit: args.limit || 20,
12
+ });
13
+
14
+ if (result.items.length === 0) {
15
+ return { text: '当前没有反馈。', items: [] };
16
+ }
17
+
18
+ const summary = result.items.map((item) => ({
19
+ id: item.id,
20
+ mode: item.mode,
21
+ problemType: item.problemType,
22
+ description: item.description.slice(0, 100),
23
+ pageUrl: item.pageUrl,
24
+ status: item.status,
25
+ createdAt: item.createdAt,
26
+ hasScreenshot: item.hasScreenshot,
27
+ errorCount:
28
+ item.consoleErrors.length +
29
+ item.networkErrors.length +
30
+ item.unhandledErrors.length,
31
+ }));
32
+
33
+ return {
34
+ text: `共 ${result.total} 条反馈(显示 ${summary.length} 条)`,
35
+ items: summary,
36
+ };
37
+ }
@@ -0,0 +1,34 @@
1
+ import { FeedbackStore } from '../../storage/store.js';
2
+
3
+ export async function resolveFeedback(
4
+ store: FeedbackStore,
5
+ args: { id: string }
6
+ ) {
7
+ await store.init();
8
+ const item = await store.getById(args.id);
9
+ if (!item) {
10
+ return { error: true, text: `反馈 ${args.id} 不存在。` };
11
+ }
12
+
13
+ // Mark as "verify" — waiting for user to confirm the fix
14
+ await store.updateStatus(args.id, 'verify');
15
+
16
+ // Push verification request to browser
17
+ try {
18
+ await fetch('http://localhost:3695/api/verify', {
19
+ method: 'POST',
20
+ headers: { 'Content-Type': 'application/json' },
21
+ body: JSON.stringify({
22
+ feedbackId: args.id,
23
+ description: item.description,
24
+ problemType: item.problemType,
25
+ element: item.element?.selector,
26
+ }),
27
+ });
28
+ } catch {}
29
+
30
+ return {
31
+ error: false,
32
+ text: `反馈 ${args.id} 已标记为待验证。已通知用户在浏览器中确认修复效果。`,
33
+ };
34
+ }
@@ -0,0 +1,65 @@
1
+ import open from 'open';
2
+ import { detectDevServer } from '../../detect/dev-server.js';
3
+ import { spawnDevServer } from '../../detect/spawn-dev.js';
4
+ import { startProxyServer, isProxyRunning } from '../../server/proxy-server.js';
5
+ import { FeedbackStore } from '../../storage/store.js';
6
+
7
+ export async function startTestSession(
8
+ store: FeedbackStore,
9
+ args: { port?: number; open?: boolean }
10
+ ): Promise<{ proxyUrl: string; targetUrl: string; status: string; message: string }> {
11
+ if (isProxyRunning()) {
12
+ return {
13
+ proxyUrl: 'http://localhost:3695',
14
+ targetUrl: '',
15
+ status: 'already_running',
16
+ message: '测试模式已在运行中。浏览器访问 http://localhost:3695 即可测试。',
17
+ };
18
+ }
19
+
20
+ const cwd = process.cwd();
21
+ const info = await detectDevServer(cwd, args.port);
22
+
23
+ let message = '';
24
+
25
+ if (info.isRunning) {
26
+ message = `检测到 ${info.framework} dev server 已在运行 → localhost:${info.port}`;
27
+ } else if (info.command) {
28
+ message = `启动 ${info.framework} dev server...`;
29
+ const started = await spawnDevServer(cwd, info.command, info.port);
30
+ if (!started) {
31
+ return {
32
+ proxyUrl: '',
33
+ targetUrl: '',
34
+ status: 'error',
35
+ message: `Dev server 启动超时(端口 ${info.port})。请确认 dev server 能正常启动,或使用 port 参数手动指定端口。`,
36
+ };
37
+ }
38
+ message = `${info.framework} dev server 已启动 → localhost:${info.port}`;
39
+ } else {
40
+ return {
41
+ proxyUrl: '',
42
+ targetUrl: '',
43
+ status: 'error',
44
+ message: '未找到 package.json 或 dev 脚本。请使用 port 参数指定 dev server 端口。',
45
+ };
46
+ }
47
+
48
+ // Initialize store
49
+ await store.init();
50
+
51
+ // Start proxy
52
+ const { proxyUrl, targetUrl } = await startProxyServer(info.port, store);
53
+
54
+ // Open browser
55
+ if (args.open !== false) {
56
+ await open(proxyUrl);
57
+ }
58
+
59
+ return {
60
+ proxyUrl,
61
+ targetUrl,
62
+ status: 'running',
63
+ message: `${message}\n代理已启动 → ${proxyUrl}\n测试模式已开启,用户可以在浏览器中提交反馈。`,
64
+ };
65
+ }
@@ -0,0 +1,93 @@
1
+ import { stopProxyServer, isProxyRunning } from '../../server/proxy-server.js';
2
+ import { killDevServer, isDevServerSpawned } from '../../detect/spawn-dev.js';
3
+ import { FeedbackStore } from '../../storage/store.js';
4
+
5
+ export async function stopTestSession(
6
+ store: FeedbackStore
7
+ ): Promise<{ status: string; message: string }> {
8
+ if (!isProxyRunning()) {
9
+ return {
10
+ status: 'not_running',
11
+ message: '测试模式未在运行。',
12
+ };
13
+ }
14
+
15
+ // Gather session summary before stopping
16
+ const { items: allItems } = await store.list({ status: 'all', limit: 100 });
17
+ const openItems = allItems.filter(i => i.status === 'open');
18
+ const resolvedItems = allItems.filter(i => i.status === 'resolved');
19
+
20
+ // Count by type
21
+ const typeCounts: Record<string, number> = {};
22
+ for (const item of openItems) {
23
+ typeCounts[item.problemType] = (typeCounts[item.problemType] || 0) + 1;
24
+ }
25
+
26
+ // Check checklist status
27
+ let checklistSummary = '';
28
+ try {
29
+ const res = await fetch('http://localhost:3695/api/checklist');
30
+ const cl = await res.json();
31
+ if (cl.items && cl.items.length > 0) {
32
+ const passed = cl.items.filter((i: any) => i.status === 'passed').length;
33
+ const failed = cl.items.filter((i: any) => i.status === 'failed').length;
34
+ const pending = cl.items.filter((i: any) => i.status === 'pending').length;
35
+ checklistSummary = `\n测试清单「${cl.title}」: ${passed} 通过 / ${failed} 失败 / ${pending} 未测`;
36
+
37
+ // Identify weak dimensions
38
+ const failedDimensions = cl.items
39
+ .filter((i: any) => i.status === 'failed' && i.dimension)
40
+ .map((i: any) => i.dimension);
41
+ const pendingDimensions = cl.items
42
+ .filter((i: any) => i.status === 'pending' && i.dimension)
43
+ .map((i: any) => i.dimension);
44
+
45
+ if (failedDimensions.length > 0) {
46
+ const unique = [...new Set(failedDimensions)];
47
+ checklistSummary += `\n失败集中维度: ${unique.join('、')}`;
48
+ }
49
+ if (pendingDimensions.length > 0) {
50
+ const unique = [...new Set(pendingDimensions)];
51
+ checklistSummary += `\n未测维度: ${unique.join('、')}`;
52
+ }
53
+ }
54
+ } catch {}
55
+
56
+ // Stop proxy
57
+ stopProxyServer();
58
+
59
+ // Kill spawned dev server (if we started it)
60
+ if (isDevServerSpawned()) {
61
+ killDevServer();
62
+ }
63
+
64
+ // Build summary
65
+ const lines = ['测试模式已关闭。\n', '── 测试会话总结 ──'];
66
+
67
+ lines.push(`反馈: ${allItems.length} 条(${openItems.length} 待修复, ${resolvedItems.length} 已修复)`);
68
+
69
+ if (Object.keys(typeCounts).length > 0) {
70
+ const typeStr = Object.entries(typeCounts)
71
+ .map(([type, count]) => `${type}: ${count}`)
72
+ .join(', ');
73
+ lines.push(`问题分布: ${typeStr}`);
74
+ }
75
+
76
+ if (checklistSummary) {
77
+ lines.push(checklistSummary);
78
+ }
79
+
80
+ // Actionable conclusion
81
+ if (openItems.length > 0) {
82
+ lines.push(`\n建议: 还有 ${openItems.length} 条未修复反馈,请查看 list_feedbacks() 并逐一处理。`);
83
+ } else if (allItems.length > 0) {
84
+ lines.push('\n结论: 所有反馈已修复。');
85
+ } else {
86
+ lines.push('\n结论: 本次未收到反馈。');
87
+ }
88
+
89
+ return {
90
+ status: 'stopped',
91
+ message: lines.join('\n'),
92
+ };
93
+ }