Skip to content

Commit 7c6fd4e

Browse files
committed
v14.0: MySQL RDS + Plugin Login + Bug Auto-Upload to Community
v14.0-A: MySQL Cloud Database Setup - Connected Alibaba Cloud RDS MySQL 8.0.36 (rm-bp1t95uj3l81oe1c8oo.mysql.rds.aliyuncs.com) - URL password encoding: @ -> %40 - Alembic migration f03fbc681fc5 with 12 business tables - Auth tested: register/login/JWT all passing v14.0-B: VS Code Plugin Login Integration - extension.ts: Pass ExtensionContext to SidebarProvider - engineClient.ts: Added cloudLogin/cloudRegister/cloudGetMe/cloudShareDirect/cloudGetSuggestions - sidebarProvider.ts: Auth panel (email/username/password) + top auth bar + token SecretStorage - package.json: Added testpilotAI.cloudApiUrl config (default: https://testpilot.xinzaoai.com) - TypeScript compilation: zero errors - esbuild bundle: zero errors v14.0-C: Bug Auto-Upload to Community Experience Library - btnShareExperience refactored: fetch() -> vscode.postMessage('shareBug') - Added shareResult/suggestionsResult message handlers in webview - showSharePrompt auto-fills form + requests related experiences - Community tab integrated: share form + suggestions display - Flow: Test complete -> Auto-popup -> User confirm -> Cloud upload + show suggestions - Cleaned redundant #shareSection Files modified: - alembic/env.py: Added dotenv loading - alembic/versions/: f03fbc681fc5 (new) replaces b5417e864e9c (deleted) - extension/package.json: Added cloudApiUrl config - extension/src/extension.ts: ExtensionContext param - extension/src/engineClient.ts: 6 cloud methods + 2 helper methods - extension/src/sidebarProvider.ts: ~500 lines - Auth UI + share flow + handlers - 开发备忘录.md: Progress summary - 项目规划.md: v14.0-A/B/C marked complete
1 parent 01a0394 commit 7c6fd4e

8 files changed

Lines changed: 983 additions & 120 deletions

File tree

alembic/env.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515

1616
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
1717

18+
from dotenv import load_dotenv
19+
load_dotenv(Path(__file__).resolve().parent.parent / ".env")
20+
1821
from src.auth.models import Base
1922
import src.community.models # noqa: F401 — 确保社区模型被注册到 Base.metadata
2023

alembic/versions/b5417e864e9c_v13_community_tables.py renamed to alembic/versions/f03fbc681fc5_v14_initial_all_tables.py

Lines changed: 118 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
"""v13_community_tables
1+
"""v14_initial_all_tables
22
3-
Revision ID: b5417e864e9c
3+
Revision ID: f03fbc681fc5
44
Revises:
5-
Create Date: 2026-03-15 10:27:59.592217
5+
Create Date: 2026-03-24 16:07:57.600570
66
77
"""
88
from typing import Sequence, Union
@@ -12,7 +12,7 @@
1212

1313

1414
# revision identifiers, used by Alembic.
15-
revision: str = 'b5417e864e9c'
15+
revision: str = 'f03fbc681fc5'
1616
down_revision: Union[str, Sequence[str], None] = None
1717
branch_labels: Union[str, Sequence[str], None] = None
1818
depends_on: Union[str, Sequence[str], None] = None
@@ -21,6 +21,28 @@
2121
def upgrade() -> None:
2222
"""Upgrade schema."""
2323
# ### commands auto generated by Alembic - please adjust! ###
24+
op.create_table('users',
25+
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
26+
sa.Column('email', sa.String(length=255), nullable=False),
27+
sa.Column('username', sa.String(length=100), nullable=False),
28+
sa.Column('hashed_password', sa.String(length=255), nullable=False),
29+
sa.Column('role', sa.String(length=20), nullable=False),
30+
sa.Column('is_active', sa.Boolean(), nullable=False),
31+
sa.Column('max_tests_per_day', sa.Integer(), nullable=False),
32+
sa.Column('max_projects', sa.Integer(), nullable=False),
33+
sa.Column('max_ai_calls_per_day', sa.Integer(), nullable=False),
34+
sa.Column('storage_limit_mb', sa.Integer(), nullable=False),
35+
sa.Column('credits', sa.Integer(), nullable=False),
36+
sa.Column('credits_used', sa.Integer(), nullable=False),
37+
sa.Column('plan', sa.String(length=20), nullable=False),
38+
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
39+
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
40+
sa.PrimaryKeyConstraint('id')
41+
)
42+
with op.batch_alter_table('users', schema=None) as batch_op:
43+
batch_op.create_index(batch_op.f('ix_users_email'), ['email'], unique=True)
44+
batch_op.create_index(batch_op.f('ix_users_username'), ['username'], unique=True)
45+
2446
op.create_table('api_keys',
2547
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
2648
sa.Column('user_id', sa.Integer(), nullable=False),
@@ -89,6 +111,38 @@ def upgrade() -> None:
89111
batch_op.create_index(batch_op.f('ix_shared_experiences_status'), ['status'], unique=False)
90112
batch_op.create_index(batch_op.f('ix_shared_experiences_user_id'), ['user_id'], unique=False)
91113

114+
op.create_table('teams',
115+
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
116+
sa.Column('name', sa.String(length=200), nullable=False),
117+
sa.Column('description', sa.Text(), nullable=False),
118+
sa.Column('owner_id', sa.Integer(), nullable=False),
119+
sa.Column('invite_code', sa.String(length=32), nullable=False),
120+
sa.Column('max_members', sa.Integer(), nullable=False),
121+
sa.Column('is_active', sa.Boolean(), nullable=False),
122+
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
123+
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
124+
sa.ForeignKeyConstraint(['owner_id'], ['users.id'], ),
125+
sa.PrimaryKeyConstraint('id')
126+
)
127+
with op.batch_alter_table('teams', schema=None) as batch_op:
128+
batch_op.create_index(batch_op.f('ix_teams_invite_code'), ['invite_code'], unique=True)
129+
batch_op.create_index(batch_op.f('ix_teams_owner_id'), ['owner_id'], unique=False)
130+
131+
op.create_table('usage_records',
132+
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
133+
sa.Column('user_id', sa.Integer(), nullable=False),
134+
sa.Column('date', sa.String(length=10), nullable=False),
135+
sa.Column('test_count', sa.Integer(), nullable=False),
136+
sa.Column('ai_call_count', sa.Integer(), nullable=False),
137+
sa.Column('screenshot_count', sa.Integer(), nullable=False),
138+
sa.Column('storage_used_mb', sa.Float(), nullable=False),
139+
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
140+
sa.PrimaryKeyConstraint('id')
141+
)
142+
with op.batch_alter_table('usage_records', schema=None) as batch_op:
143+
batch_op.create_index('idx_usage_user_date', ['user_id', 'date'], unique=True)
144+
batch_op.create_index(batch_op.f('ix_usage_records_user_id'), ['user_id'], unique=False)
145+
92146
op.create_table('user_badges',
93147
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
94148
sa.Column('user_id', sa.Integer(), nullable=False),
@@ -128,6 +182,41 @@ def upgrade() -> None:
128182
with op.batch_alter_table('experience_votes', schema=None) as batch_op:
129183
batch_op.create_index('idx_vote_unique', ['experience_id', 'user_id', 'vote_type'], unique=True)
130184

185+
op.create_table('projects',
186+
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
187+
sa.Column('name', sa.String(length=200), nullable=False),
188+
sa.Column('description', sa.Text(), nullable=False),
189+
sa.Column('owner_id', sa.Integer(), nullable=False),
190+
sa.Column('team_id', sa.Integer(), nullable=True),
191+
sa.Column('base_url', sa.String(length=500), nullable=False),
192+
sa.Column('is_active', sa.Boolean(), nullable=False),
193+
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
194+
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
195+
sa.Column('test_count', sa.Integer(), nullable=False),
196+
sa.Column('last_pass_rate', sa.Float(), nullable=False),
197+
sa.Column('total_bugs_found', sa.Integer(), nullable=False),
198+
sa.ForeignKeyConstraint(['owner_id'], ['users.id'], ),
199+
sa.ForeignKeyConstraint(['team_id'], ['teams.id'], ),
200+
sa.PrimaryKeyConstraint('id')
201+
)
202+
with op.batch_alter_table('projects', schema=None) as batch_op:
203+
batch_op.create_index('idx_project_owner', ['owner_id', 'is_active'], unique=False)
204+
batch_op.create_index(batch_op.f('ix_projects_owner_id'), ['owner_id'], unique=False)
205+
batch_op.create_index(batch_op.f('ix_projects_team_id'), ['team_id'], unique=False)
206+
207+
op.create_table('team_members',
208+
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
209+
sa.Column('team_id', sa.Integer(), nullable=False),
210+
sa.Column('user_id', sa.Integer(), nullable=False),
211+
sa.Column('role', sa.String(length=20), nullable=False),
212+
sa.Column('joined_at', sa.DateTime(timezone=True), nullable=False),
213+
sa.ForeignKeyConstraint(['team_id'], ['teams.id'], ),
214+
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
215+
sa.PrimaryKeyConstraint('id')
216+
)
217+
with op.batch_alter_table('team_members', schema=None) as batch_op:
218+
batch_op.create_index('idx_team_member', ['team_id', 'user_id'], unique=True)
219+
131220
op.create_table('debug_snapshots',
132221
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
133222
sa.Column('user_id', sa.Integer(), nullable=False),
@@ -152,27 +241,27 @@ def upgrade() -> None:
152241
batch_op.create_index('idx_snapshot_user_resolved', ['user_id', 'resolved'], unique=False)
153242
batch_op.create_index(batch_op.f('ix_debug_snapshots_user_id'), ['user_id'], unique=False)
154243

155-
with op.batch_alter_table('users', schema=None) as batch_op:
156-
batch_op.add_column(sa.Column('credits', sa.Integer(), nullable=False))
157-
batch_op.add_column(sa.Column('credits_used', sa.Integer(), nullable=False))
158-
batch_op.add_column(sa.Column('plan', sa.String(length=20), nullable=False))
159-
160244
# ### end Alembic commands ###
161245

162246

163247
def downgrade() -> None:
164248
"""Downgrade schema."""
165249
# ### commands auto generated by Alembic - please adjust! ###
166-
with op.batch_alter_table('users', schema=None) as batch_op:
167-
batch_op.drop_column('plan')
168-
batch_op.drop_column('credits_used')
169-
batch_op.drop_column('credits')
170-
171250
with op.batch_alter_table('debug_snapshots', schema=None) as batch_op:
172251
batch_op.drop_index(batch_op.f('ix_debug_snapshots_user_id'))
173252
batch_op.drop_index('idx_snapshot_user_resolved')
174253

175254
op.drop_table('debug_snapshots')
255+
with op.batch_alter_table('team_members', schema=None) as batch_op:
256+
batch_op.drop_index('idx_team_member')
257+
258+
op.drop_table('team_members')
259+
with op.batch_alter_table('projects', schema=None) as batch_op:
260+
batch_op.drop_index(batch_op.f('ix_projects_team_id'))
261+
batch_op.drop_index(batch_op.f('ix_projects_owner_id'))
262+
batch_op.drop_index('idx_project_owner')
263+
264+
op.drop_table('projects')
176265
with op.batch_alter_table('experience_votes', schema=None) as batch_op:
177266
batch_op.drop_index('idx_vote_unique')
178267

@@ -183,6 +272,16 @@ def downgrade() -> None:
183272
batch_op.drop_index('idx_badge_user_type')
184273

185274
op.drop_table('user_badges')
275+
with op.batch_alter_table('usage_records', schema=None) as batch_op:
276+
batch_op.drop_index(batch_op.f('ix_usage_records_user_id'))
277+
batch_op.drop_index('idx_usage_user_date')
278+
279+
op.drop_table('usage_records')
280+
with op.batch_alter_table('teams', schema=None) as batch_op:
281+
batch_op.drop_index(batch_op.f('ix_teams_owner_id'))
282+
batch_op.drop_index(batch_op.f('ix_teams_invite_code'))
283+
284+
op.drop_table('teams')
186285
with op.batch_alter_table('shared_experiences', schema=None) as batch_op:
187286
batch_op.drop_index(batch_op.f('ix_shared_experiences_user_id'))
188287
batch_op.drop_index(batch_op.f('ix_shared_experiences_status'))
@@ -202,4 +301,9 @@ def downgrade() -> None:
202301
batch_op.drop_index(batch_op.f('ix_api_keys_user_id'))
203302

204303
op.drop_table('api_keys')
304+
with op.batch_alter_table('users', schema=None) as batch_op:
305+
batch_op.drop_index(batch_op.f('ix_users_username'))
306+
batch_op.drop_index(batch_op.f('ix_users_email'))
307+
308+
op.drop_table('users')
205309
# ### end Alembic commands ###

extension/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,11 @@
264264
"type": "string",
265265
"default": "",
266266
"description": "TestPilot AI 项目根目录路径(包含 cli.py 的目录),留空则自动从工作区检测"
267+
},
268+
"testpilotAI.cloudApiUrl": {
269+
"type": "string",
270+
"default": "https://testpilot.xinzaoai.com",
271+
"description": "TestPilot AI 云端 API 地址(登录/注册/经验库)"
267272
}
268273
}
269274
}

extension/src/engineClient.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,4 +392,113 @@ export class EngineClient {
392392
req.end();
393393
});
394394
}
395+
396+
// ── 云端认证 API ─────────────────────────────────
397+
398+
/** 云端 API 基础地址 */
399+
private get cloudUrl(): string {
400+
return vscode.workspace
401+
.getConfiguration("testpilotAI")
402+
.get<string>("cloudApiUrl", "https://testpilot.xinzaoai.com");
403+
}
404+
405+
/** 云端 POST(走 HTTPS,短超时) */
406+
private async _cloudPost<T>(path: string, body: unknown, token?: string): Promise<T> {
407+
const url = `${this.cloudUrl}${path}`;
408+
const headers: Record<string, string> = { "Content-Type": "application/json" };
409+
if (token) {
410+
headers["Authorization"] = `Bearer ${token}`;
411+
}
412+
const resp = await fetch(url, {
413+
method: "POST",
414+
headers,
415+
body: JSON.stringify(body),
416+
});
417+
if (!resp.ok) {
418+
const text = await resp.text();
419+
let msg = `HTTP ${resp.status}`;
420+
try { msg = JSON.parse(text).detail || msg; } catch { /* ignore */ }
421+
throw new Error(msg);
422+
}
423+
return resp.json() as Promise<T>;
424+
}
425+
426+
/** 云端 GET */
427+
private async _cloudGet<T>(path: string, token: string): Promise<T> {
428+
const url = `${this.cloudUrl}${path}`;
429+
const resp = await fetch(url, {
430+
method: "GET",
431+
headers: {
432+
"Content-Type": "application/json",
433+
"Authorization": `Bearer ${token}`,
434+
},
435+
});
436+
if (!resp.ok) {
437+
const text = await resp.text();
438+
let msg = `HTTP ${resp.status}`;
439+
try { msg = JSON.parse(text).detail || msg; } catch { /* ignore */ }
440+
throw new Error(msg);
441+
}
442+
return resp.json() as Promise<T>;
443+
}
444+
445+
/** 云端登录 */
446+
async cloudLogin(emailOrUsername: string, password: string): Promise<{
447+
access_token: string;
448+
token_type: string;
449+
user: { id: number; email: string; username: string; role: string };
450+
}> {
451+
return this._cloudPost("/auth/login", {
452+
email_or_username: emailOrUsername,
453+
password,
454+
});
455+
}
456+
457+
/** 云端注册 */
458+
async cloudRegister(email: string, username: string, password: string): Promise<{
459+
access_token: string;
460+
token_type: string;
461+
user: { id: number; email: string; username: string; role: string };
462+
}> {
463+
return this._cloudPost("/auth/register", { email, username, password });
464+
}
465+
466+
/** 获取当前用户信息 */
467+
async cloudGetMe(token: string): Promise<{
468+
id: number; email: string; username: string; role: string;
469+
credits: number; plan: string;
470+
}> {
471+
return this._cloudGet("/auth/me", token);
472+
}
473+
474+
// ── 云端经验库 API ───────────────────────────────
475+
476+
/** 直接分享经验到社区(Bug 自动上传) */
477+
async cloudShareDirect(token: string, data: {
478+
title: string;
479+
platform: string;
480+
framework: string;
481+
error_type: string;
482+
problem_desc: string;
483+
solution_desc: string;
484+
root_cause?: string;
485+
code_snippet?: string;
486+
tags?: string[];
487+
difficulty?: string;
488+
fix_pattern?: string;
489+
}): Promise<{
490+
ok: boolean;
491+
experience: Record<string, unknown>;
492+
score_breakdown: Record<string, unknown>;
493+
}> {
494+
return this._cloudPost("/api/v1/community/share/direct", data, token);
495+
}
496+
497+
/** 根据错误类型获取经验建议 */
498+
async cloudGetSuggestions(token: string, platform: string, errorType: string): Promise<{
499+
items: Array<{ id: number; title: string; solution_desc: string; upvote_count: number }>;
500+
}> {
501+
const params = new URLSearchParams({ platform, error_type: errorType, limit: "5" });
502+
return this._cloudGet(`/api/v1/community/experiences/suggest?${params}`, token);
503+
}
395504
}

extension/src/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export function activate(context: vscode.ExtensionContext): void {
2424
client = new EngineClient();
2525

2626
// 注册侧边栏
27-
const sidebarProvider = new SidebarProvider(context.extensionUri, client);
27+
const sidebarProvider = new SidebarProvider(context.extensionUri, client, context);
2828
context.subscriptions.push(
2929
vscode.window.registerWebviewViewProvider(SidebarProvider.viewType, sidebarProvider),
3030
);

0 commit comments

Comments
 (0)