Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'yarn'
cache-dependency-path: async-code-web/yarn.lock
- run: yarn install
- run: yarn build
cache: 'npm'
cache-dependency-path: async-code-web/package-lock.json
- run: npm ci
- run: npm run lint
- run: npm run build

backend-static-check:
runs-on: ubuntu-latest
Expand All @@ -28,5 +29,9 @@ jobs:
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install backend deps
run: python -m pip install -r server/requirements.txt
- name: Static analysis
run: python -m compileall -q server
- name: Unit tests
run: python -m unittest discover -s server/tests -p "test_*.py"
14 changes: 14 additions & 0 deletions MILESTONES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Milestones

## v0.1

- Branch selector UI and read-only branch discovery
- Task stage tracking in backend + UI
- Local mode data export/reset
- CI parity (lint/build/tests)

## v0.2

- Stronger secret redaction end-to-end (logs + patches)
- Better PR failure recovery (conflict summary + retry UX)
- Docker safety knobs (socket/privileged policy) with sane defaults
6 changes: 6 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Release Checklist

- Run CI locally: `python -m compileall -q server`, `python -m unittest discover -s server/tests -p "test_*.py"`, `npm --prefix async-code-web run lint`, `npm --prefix async-code-web run build`
- Verify secrets are not committed (especially `server/.env`, `server/local_db.json`, tokens in logs)
- Smoke test: start with `docker compose up --build` and create a task end-to-end (clone → patch → PR)
- Tag and create a GitHub release
1 change: 1 addition & 0 deletions async-code-web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ npm run dev
```bash
npm run lint
npm run build
npm run test
```

更多使用说明请看仓库根目录 `README.md` 与 `GUIDE.md`。
142 changes: 132 additions & 10 deletions async-code-web/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState, useEffect } from "react";
import { useState, useEffect, useRef } from "react";
import { Github, GitBranch, Code2, ExternalLink, CheckCircle, Clock, XCircle, AlertCircle, FileText, Eye, GitCommit, Bell, Settings, LogOut, User, FolderGit2, Plus, Archive, ArchiveRestore } from "lucide-react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
Expand Down Expand Up @@ -35,7 +35,12 @@ export default function Home() {
const supabaseEnabled = isSupabaseConfigured();
const [prompt, setPrompt] = useState("");
const [selectedProject, setSelectedProject] = useState<string>("");
const [customRepoUrl, setCustomRepoUrl] = useState("");
const [branch, setBranch] = useState("main");
const [availableBranches, setAvailableBranches] = useState<string[]>([]);
const [isLoadingBranches, setIsLoadingBranches] = useState(false);
const [branchLoadError, setBranchLoadError] = useState<string | null>(null);
const lastRepoUrlRef = useRef<string>("");
const [githubToken, setGithubToken] = useState("");
const [rememberToken, setRememberToken] = useState(false);
const [model, setModel] = useState("codex");
Expand Down Expand Up @@ -76,9 +81,24 @@ export default function Home() {
setArchivedTaskIds(new Set());
}
}

const savedCustomRepoUrl = localStorage.getItem('last-custom-repo-url');
if (savedCustomRepoUrl) {
setCustomRepoUrl(savedCustomRepoUrl);
}
}
}, []);

// 记住最近使用的自定义仓库地址
useEffect(() => {
if (typeof window === 'undefined') return;
if (!customRepoUrl.trim()) {
localStorage.removeItem('last-custom-repo-url');
return;
}
localStorage.setItem('last-custom-repo-url', customRepoUrl.trim());
}, [customRepoUrl]);

// 加载初始数据
useEffect(() => {
if (user?.id) {
Expand All @@ -87,6 +107,58 @@ export default function Home() {
}
}, [user?.id]);

// 根据选择的仓库自动拉取分支列表(用于下拉选择)
useEffect(() => {
if (!user?.id) return;

let repoUrl = "";
if (selectedProject && selectedProject !== 'custom') {
const project = projects.find((p) => p.id.toString() === selectedProject);
repoUrl = project?.repo_url || "";
} else if (selectedProject === 'custom') {
repoUrl = customRepoUrl.trim();
}

const repoChanged = repoUrl !== lastRepoUrlRef.current;
if (repoChanged) {
lastRepoUrlRef.current = repoUrl;
setAvailableBranches([]);
setBranchLoadError(null);
}

if (!repoUrl || !githubToken.trim()) return;

try {
ApiService.parseGitHubUrl(repoUrl);
} catch {
return;
}

let cancelled = false;
(async () => {
setIsLoadingBranches(true);
const result = await ApiService.getRepoBranches(user.id, githubToken, repoUrl);
if (cancelled) return;

if (result.status === 'success') {
const branches = Array.isArray(result.repo?.branches) ? result.repo?.branches : [];
setAvailableBranches(branches);
const defaultBranch = result.repo?.default_branch;
if (repoChanged && defaultBranch) {
setBranch(defaultBranch);
}
} else {
setBranchLoadError(result.error || '获取分支失败');
setAvailableBranches([]);
}
setIsLoadingBranches(false);
})();

return () => {
cancelled = true;
};
}, [user?.id, selectedProject, customRepoUrl, projects, githubToken]);

// GitHub 令牌变化时写入 sessionStorage;如选择“记住”则同步写入 localStorage
useEffect(() => {
if (typeof window !== 'undefined') {
Expand Down Expand Up @@ -260,9 +332,22 @@ export default function Home() {
repoUrl = project.repo_url;
projectId = project.id;
}
} else if (selectedProject === "custom") {
const url = customRepoUrl.trim();
if (!url) {
toast.error('请填写自定义仓库地址');
return;
}
try {
ApiService.parseGitHubUrl(url);
} catch (e) {
toast.error(`仓库地址无效:${String(e)}`);
return;
}
repoUrl = url;
projectId = undefined;
} else {
// 自定义仓库地址 - 需要单独的输入框
toast.error('自定义仓库地址输入尚未实现,请先选择或创建项目。');
toast.error('请选择一个项目或填写自定义仓库地址');
return;
}

Expand Down Expand Up @@ -518,20 +603,52 @@ export default function Home() {
)}
</div>

{selectedProject === 'custom' && (
<div className="space-y-2">
<Label htmlFor="custom-repo">自定义仓库地址</Label>
<Input
id="custom-repo"
value={customRepoUrl}
onChange={(e) => setCustomRepoUrl(e.target.value)}
placeholder="https://github.com/owner/repo 或 git@github.com:owner/repo.git"
/>
<p className="text-sm text-slate-500">目前仅支持 GitHub 仓库地址(HTTPS / SSH)。</p>
</div>
)}

{/* Branch and Model in a responsive grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="branch" className="flex items-center gap-2">
<GitBranch className="w-3 h-3" />
分支
</Label>
<Input
id="branch"
value={branch}
onChange={(e) => setBranch(e.target.value)}
placeholder="main"
className="w-full"
/>
{availableBranches.length > 0 ? (
<Select value={branch} onValueChange={setBranch}>
<SelectTrigger id="branch" className="w-full">
<SelectValue placeholder="选择分支" />
</SelectTrigger>
<SelectContent>
{availableBranches.map((b) => (
<SelectItem key={b} value={b}>{b}</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
id="branch"
value={branch}
onChange={(e) => setBranch(e.target.value)}
placeholder="main"
className="w-full"
/>
)}
{isLoadingBranches && (
<p className="text-sm text-slate-500">正在获取分支列表...</p>
)}
{!isLoadingBranches && branchLoadError && (
<p className="text-sm text-amber-700">{branchLoadError}</p>
)}
</div>

<div className="space-y-2">
Expand Down Expand Up @@ -688,6 +805,11 @@ export default function Home() {
<span>•</span>
<span>{new Date(task.created_at || '').toLocaleDateString()}</span>
</div>
{(task.status === 'running' || task.status === 'pending') && (
<div className="text-xs text-slate-600 mt-1">
阶段:{((task as any).stage || (task as any).execution_metadata?.stage || '—')}
</div>
)}
</div>
<div className="flex items-center gap-2">
{task.status === "completed" && (
Expand Down
62 changes: 60 additions & 2 deletions async-code-web/app/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ import { CodeAgentSettings } from "@/components/code-agent-settings";
import { ApiService } from "@/lib/api-service";
import { useAuth } from "@/contexts/auth-context";
import { ProtectedRoute } from "@/components/protected-route";
import { isSupabaseConfigured } from "@/lib/supabase";

import Link from "next/link";

export default function SettingsPage() {
const { user } = useAuth();
const supabaseEnabled = isSupabaseConfigured();
const [githubToken, setGithubToken] = useState("");
const [rememberToken, setRememberToken] = useState(false);
const [tokenValidation, setTokenValidation] = useState<{status: string; user?: string; repo?: {name?: string; permissions?: {read?: boolean; write?: boolean; create_branches?: boolean; admin?: boolean}}; error?: string} | null>(null);
Expand Down Expand Up @@ -106,6 +108,44 @@ export default function SettingsPage() {
}
};

const handleExportLocalDb = async () => {
if (!user?.id) {
toast.error('用户未登录');
return;
}
const result = await ApiService.exportLocalDb(user.id);
if (result.status !== 'success') {
toast.error(result.error || '导出失败');
return;
}

const payload = JSON.stringify(result.data, null, 2);
const blob = new Blob([payload], { type: 'application/json;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `async-code-local-db-${Date.now()}.json`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
toast.success('已导出本地数据');
};

const handleResetLocalDb = async () => {
if (!user?.id) {
toast.error('用户未登录');
return;
}
if (!confirm('确认清空本地模式下的项目与任务数据?此操作不可恢复。')) return;
const result = await ApiService.resetLocalDb(user.id);
if (result.status !== 'success') {
toast.error(result.error || '清空失败');
return;
}
toast.success('已清空本地数据');
};

return (
<ProtectedRoute>
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100">
Expand Down Expand Up @@ -185,10 +225,10 @@ export default function SettingsPage() {
type="url"
value={repoUrl}
onChange={(e) => setRepoUrl(e.target.value)}
placeholder="https://github.com/owner/repo"
placeholder="https://github.com/owner/repo 或 git@github.com:owner/repo.git"
/>
<p className="text-sm text-slate-600">
使用任意可访问仓库来测试令牌权限
使用任意可访问仓库来测试令牌权限(支持 HTTPS / SSH)
</p>
</div>

Expand Down Expand Up @@ -264,6 +304,24 @@ export default function SettingsPage() {
{/* Code Agent Settings */}
<CodeAgentSettings />

{!supabaseEnabled && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Code className="w-5 h-5" />
本地数据管理
</CardTitle>
<CardDescription>
仅在本地模式下可用:导出/清空当前用户的项目与任务数据
</CardDescription>
</CardHeader>
<CardContent className="flex gap-3">
<Button variant="outline" onClick={handleExportLocalDb}>导出本地数据</Button>
<Button variant="destructive" onClick={handleResetLocalDb}>清空本地数据</Button>
</CardContent>
</Card>
)}

{/* Token Creation Instructions */}
<Card className="bg-blue-50 border-blue-200">
<CardHeader>
Expand Down
19 changes: 18 additions & 1 deletion async-code-web/app/tasks/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,23 @@ interface TaskWithProject extends Task {
project?: Project
}

const formatMessageTimestamp = (value: unknown) => {
if (value === null || value === undefined || value === "") return "未知时间";
let date: Date;
if (typeof value === "number") {
const ms = value < 1e12 ? value * 1000 : value;
date = new Date(ms);
} else if (typeof value === "string" && /^\d+(\.\d+)?$/.test(value.trim())) {
const numeric = Number(value);
const ms = numeric < 1e12 ? numeric * 1000 : numeric;
date = new Date(ms);
} else {
date = new Date(value as string);
}
if (Number.isNaN(date.getTime())) return "未知时间";
return date.toLocaleString();
};

export default function TaskDetailPage() {
const { user } = useAuth();
const params = useParams();
Expand Down Expand Up @@ -415,7 +432,7 @@ export default function TaskDetailPage() {
{message.role === 'user' ? '你' : '助手'}
</Badge>
<span className="text-xs text-slate-500">
{new Date(message.timestamp).toLocaleString()}
{formatMessageTimestamp(message.timestamp)}
</span>
</div>
<p className="text-sm text-slate-700 whitespace-pre-wrap">
Expand Down
Loading