Commit 9b479a5e authored by xuchentao's avatar xuchentao

feat: add coding agent and manual version workflow

parent 9b34b595
......@@ -40,9 +40,14 @@ WEBAGENT_NGINX_AUTO_RELOAD=false
WEBAGENT_NGINX_BIN=nginx
# 待连接域名的所有权、证书和 DNS 自动检查间隔,最小 15000ms。
WEBAGENT_DOMAIN_CHECK_INTERVAL_MS=60000
# 可选:任意兼容 OpenAI Chat Completions 的模型服务。
# 默认示例使用国内可访问的 DeepSeek;也可以替换为企业内部网关,
# 或阿里云百炼兼容地址:https://dashscope.aliyuncs.com/compatible-mode/v1
# Coding Agent 使用 Qwen Code SDK,并通过下面的 OpenAI-compatible 配置连接模型。
# 模式由租户点击聊天输入框左下角的 Agent 模式按钮明确选择;默认固定为 local,不会因存在密钥自动切换。
OPENAI_API_KEY=
OPENAI_BASE_URL=https://api.deepseek.com/v1
OPENAI_BASE_URL=https://api.deepseek.com
OPENAI_MODEL=deepseek-chat
# 新租户尚未保存选择时的默认值;建议保持 local。
WEBAGENT_AGENT_PROVIDER=local
WEBAGENT_AGENT_TIMEOUT_MS=600000
WEBAGENT_AGENT_MAX_TURNS=20
# 输出 SDK 调试日志;正常运行保持 0。
QWEN_SDK_DEBUG=0
# WebAgent
WebAgent 是一个本地 AI 官网工作台。它把每个用户网站保存为独立 Astro 项目,通过 Git 记录每次有效修改,并在隔离 Workspace 中完成构建验证后再更新正式项目
WebAgent 是一个本地 AI 官网工作台。它把每个用户网站保存为独立 Astro 项目。Agent 修改会先在隔离的工作草稿中累积并通过构建验证;只有用户在左侧“版本”中手动保存时,系统才创建 Git 版本
## 快速开始
......@@ -35,7 +35,7 @@ WEBAGENT_PRODUCTION_DIR=../WebAgent-production
# 或 WEBAGENT_SITES_DIR=/data/WebAgent-sites
```
未配置模型密钥时,内置演示指令支持修改主题颜色、增加服务卡片、增加企业优势等常见场景。配置 `OPENAI_API_KEY` 后,会使用兼容 OpenAI Chat Completions 的接口生成受约束的 `SitePatch`。示例默认使用国内可访问的 DeepSeek API,也可换成阿里云百炼或企业内部兼容网关。
点击聊天输入框左下角的 Agent 模式按钮,可以明确选择“模拟 Agent”或“模型 Agent”。选择按租户持久化,系统不会因为检测到模型密钥而自动切换。模拟 Agent 使用内置规则;模型 Agent 由独立 Worker 通过 Qwen Code SDK 运行,并在站点草稿 Worktree 中受限读写。系统随后独立复验 diff 与 Astro 构建,成功后更新未保存草稿的测试预览,不自动提交 Git。模型服务使用 OpenAI-compatible 配置,可连接 DeepSeek 或企业内部兼容网关。
项目的 `.npmrc` 只对当前仓库生效,依赖从 `registry.npmmirror.com` 安装,pnpm store 写入已忽略的 `.runtime/pnpm-store/`,不会污染全局 npm 配置。
......@@ -55,14 +55,18 @@ WEBAGENT_PRODUCTION_DIR=../WebAgent-production
- 从 Astro 模板创建独立企业官网
- 自动初始化网站 Git 仓库并提交初始版本
- 在隔离 Git Worktree 中应用 Agent 修改
- 构建通过后才写入正式项目并创建提交
- Agent 修改可在同一工作草稿中连续累积,构建通过后更新测试预览但不自动提交
- 在左侧“版本”中手动保存草稿、放弃未保存修改或恢复历史版本
- 恢复历史版本会创建新的恢复提交,不改写已有 Git 历史
- 本地 iframe 预览与自动刷新
- 持久化 Agent 队列、SSE 事件、取消、超时与 Worker 崩溃标记
- Qwen Code Provider 工具权限限制,以及系统侧 diff/build 二次验证
- 测试环境与生产环境使用独立目录和独立链接
- 用户确认后手动发布,并记录生产版本 commit 与发布时间
- 为每个官网绑定自定义域名,通过 TXT 验证所有权并检测 CNAME/A/AAAA
- 可接入 Cloudflare for SaaS 自动创建 Custom Hostname、展示证书验证记录并跟踪 HTTPS 状态
- 自定义域名使用独立根路径产物,域名故障不影响系统默认生产链接
- 浏览 Git 历史并安全回滚到任意版本
- 浏览 Git 历史并安全恢复任意版本;存在未保存草稿时禁止发布上线
- 生成站点、测试预览和生产产物保存在独立的可配置目录,构建缓存等临时数据隔离在 `.runtime/`
详细目录约束见 [docs/storage-convention.md](docs/storage-convention.md),Agent 写入规则[docs/site-patch-schema.md](docs/site-patch-schema.md)
详细目录约束见 [docs/storage-convention.md](docs/storage-convention.md),Agent 运行架构见 [docs/agent-runtime.md](docs/agent-runtime.md),旧版受约束补丁格式[docs/site-patch-schema.md](docs/site-patch-schema.md)
......@@ -4,15 +4,20 @@
"private": true,
"type": "module",
"scripts": {
"dev": "tsx watch src/server.ts",
"dev": "concurrently -k -n api,worker \"pnpm dev:api\" \"pnpm dev:worker\"",
"dev:api": "tsx watch src/server.ts",
"dev:worker": "tsx watch src/worker.ts",
"build": "tsc -p tsconfig.json",
"start": "node dist/server.js",
"start": "concurrently -k -n api,worker \"pnpm start:api\" \"pnpm start:worker\"",
"start:api": "node dist/server.js",
"start:worker": "node dist/worker.js",
"typecheck": "tsc -p tsconfig.json --noEmit",
"test": "NODE_ENV=test WEBAGENT_INIT_TEST_DATA=true tsx --test src/**/*.test.ts",
"auth:admin": "tsx src/auth/admin-cli.ts"
},
"dependencies": {
"@fastify/cors": "^11.0.1",
"@qwen-code/sdk": "0.1.8",
"@webagent/shared": "workspace:*",
"dotenv": "^16.5.0",
"execa": "^9.5.2",
......@@ -21,6 +26,7 @@
},
"devDependencies": {
"@types/node": "^22.15.3",
"concurrently": "^9.2.0",
"tsx": "^4.19.4",
"typescript": "^5.8.3"
}
......
import path from "node:path";
import { readFile } from "node:fs/promises";
import type { SitePatch } from "@webagent/shared";
import { config } from "../config.js";
import { sitePatchSchema } from "../schemas.js";
import { selectContext } from "./context-selector.js";
import { agentSystemPrompt } from "./prompts.js";
export class AgentLoop {
async generate(projectPath: string, message: string): Promise<{ patch: SitePatch; mode: "model" | "local" }> {
if (config.openai.apiKey) return { patch: await this.generateWithModel(projectPath, message), mode: "model" };
return { patch: await this.generateLocally(projectPath, message), mode: "local" };
}
async repair(projectPath: string, request: string, error: string): Promise<SitePatch> {
if (!config.openai.apiKey) throw new Error("本地演示模式无法自动修复构建错误");
return this.generateWithModel(projectPath, request + "\n\n上一次构建失败,请修复当前文件。错误摘要:\n" + error.slice(-6000));
}
private async generateWithModel(projectPath: string, message: string): Promise<SitePatch> {
const context = await selectContext(projectPath);
const response = await fetch(config.openai.baseUrl + "/chat/completions", {
method: "POST",
headers: { "content-type": "application/json", authorization: "Bearer " + config.openai.apiKey },
body: JSON.stringify({
model: config.openai.model,
temperature: 0.75,
response_format: { type: "json_object" },
messages: [
{ role: "system", content: agentSystemPrompt },
{ role: "user", content: "用户要求:\n" + message + "\n\n当前项目文件:\n" + context },
],
}),
signal: AbortSignal.timeout(90000),
});
if (!response.ok) throw new Error("模型请求失败: " + response.status + " " + (await response.text()).slice(0, 500));
const data = await response.json() as { choices?: Array<{ message?: { content?: string } }> };
const content = data.choices?.[0]?.message?.content;
if (!content) throw new Error("模型没有返回 SitePatch");
const cleaned = content.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
return sitePatchSchema.parse(JSON.parse(cleaned)) as SitePatch;
async generateLocal(projectPath: string, message: string): Promise<SitePatch> {
return this.generateLocally(projectPath, message);
}
private async generateLocally(projectPath: string, message: string): Promise<SitePatch> {
......
import path from "node:path";
import { readdir, readFile } from "node:fs/promises";
const extensions = new Set([".astro", ".ts", ".css", ".json", ".md"]);
async function walk(directory: string, root: string, files: string[]): Promise<void> {
for (const entry of await readdir(directory, { withFileTypes: true })) {
const absolute = path.join(directory, entry.name);
if (entry.isDirectory()) await walk(absolute, root, files);
else if (extensions.has(path.extname(entry.name))) files.push(path.relative(root, absolute).replaceAll("\\", "/"));
}
}
export async function selectContext(projectPath: string): Promise<string> {
const paths: string[] = [];
await walk(path.join(projectPath, "src"), projectPath, paths);
const prioritized = paths.sort((a, b) => {
const score = (value: string) => value.includes("data/") ? 0 : value.includes("theme.css") ? 1 : value.includes("pages/") ? 2 : 3;
return score(a) - score(b) || a.localeCompare(b);
}).slice(0, 18);
const sections: string[] = [];
let total = 0;
for (const relativePath of prioritized) {
const content = await readFile(path.join(projectPath, relativePath), "utf8");
if (total + content.length > 60000) break;
sections.push("FILE: " + relativePath + "\n" + content);
total += content.length;
}
return sections.join("\n\n---\n\n");
}
import type { AgentRunEvent, AgentUsage } from "@webagent/shared";
export interface AgentToolPolicy {
readonly allowedCommands: readonly string[];
}
export interface AgentProviderInput {
runId: string;
workingDirectory: string;
prompt: string;
model?: string;
sessionId?: string;
resumeSession?: boolean;
timeoutMs: number;
maxTurns: number;
toolPolicy: AgentToolPolicy;
}
export interface AgentProviderResult {
status: "completed" | "failed" | "cancelled" | "timed_out";
summary?: string;
sessionId?: string;
usage?: AgentUsage;
error?: string;
}
export interface RunningAgent {
events: AsyncIterable<AgentRunEvent>;
result: Promise<AgentProviderResult>;
cancel(reason?: string): Promise<void>;
}
export interface CodingAgentProvider {
readonly name: string;
readonly version: string;
start(input: AgentProviderInput): Promise<RunningAgent>;
}
export const DEFAULT_AGENT_POLICY: AgentToolPolicy = {
allowedCommands: ["git status --short", "git diff --check", "git diff --no-ext-diff"],
};
export const AGENT_LIMITS = {
timeoutMs: 600_000,
maxTurns: 20,
maxFiles: 30,
maxDiffBytes: 1_048_576,
maxFileBytes: 262_144,
} as const;
import assert from "node:assert/strict";
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { execa } from "execa";
import { AgentDiffValidator } from "./diff-validator.js";
async function workspace() {
const root = await mkdtemp(path.join(os.tmpdir(), "webagent-diff-"));
await mkdir(path.join(root, "src"));
await writeFile(path.join(root, "src", "index.ts"), "export const title = 'before';\n");
await writeFile(path.join(root, "package.json"), "{}\n");
await execa("git", ["init", "--quiet"], { cwd: root });
await execa("git", ["config", "user.name", "WebAgent Test"], { cwd: root });
await execa("git", ["config", "user.email", "test@example.invalid"], { cwd: root });
await execa("git", ["add", "."], { cwd: root });
await execa("git", ["commit", "--quiet", "-m", "baseline"], { cwd: root });
return root;
}
test("AgentDiffValidator accepts source edits and rejects protected files", async () => {
const root = await workspace();
try {
await writeFile(path.join(root, "src", "index.ts"), "export const title = 'after';\n");
const allowed = await new AgentDiffValidator().validate(root);
assert.equal(allowed.passed, true);
assert.deepEqual(allowed.files, ["src/index.ts"]);
await writeFile(path.join(root, "package.json"), "{\"scripts\":{}}\n");
const rejected = await new AgentDiffValidator().validate(root);
assert.equal(rejected.passed, false);
assert.ok(rejected.violations.some((violation) => violation.includes("受保护路径")));
} finally {
await rm(root, { recursive: true, force: true });
}
});
import path from "node:path";
import { lstat, realpath } from "node:fs/promises";
import { execa } from "execa";
import { AGENT_LIMITS } from "./contracts.js";
export interface AgentDiffValidation {
passed: boolean;
diff: string;
files: string[];
totalBytes: number;
violations: string[];
}
const protectedPath = (file: string) => file === "package.json" || file === "pnpm-lock.yaml"
|| file === ".env" || file.startsWith(".env.") || file === ".git" || file.startsWith(".git/")
|| file === "dist" || file.startsWith("dist/") || file === "node_modules" || file.startsWith("node_modules/");
const allowedPath = (file: string) => file.startsWith("src/") || file.startsWith("public/images/");
export class AgentDiffValidator {
async validate(workspace: string): Promise<AgentDiffValidation> {
const status = await execa("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], { cwd: workspace });
const chunks = status.stdout.split("\0").filter(Boolean);
const files = [...new Set(chunks.map((entry) => {
const raw = entry.slice(3);
return raw.includes(" -> ") ? raw.split(" -> ").at(-1)! : raw;
}))].sort();
const violations: string[] = [];
let fileBytes = 0;
const root = await realpath(workspace);
if (!files.length) violations.push("Agent 没有产生文件修改");
if (files.length > AGENT_LIMITS.maxFiles) violations.push(`修改文件数 ${files.length} 超过限制 ${AGENT_LIMITS.maxFiles}`);
for (const file of files) {
if (file.includes("..") || file.startsWith("/") || file.includes("\\")) {
violations.push(`不安全路径: ${file}`);
continue;
}
if (protectedPath(file)) violations.push(`禁止修改受保护路径: ${file}`);
if (!allowedPath(file)) violations.push(`路径不在网站可编辑范围: ${file}`);
const candidate = path.resolve(workspace, file);
try {
const info = await lstat(candidate);
if (info.isSymbolicLink()) violations.push(`禁止符号链接: ${file}`);
if (info.isFile()) {
const resolved = await realpath(candidate);
if (!resolved.startsWith(root + path.sep)) violations.push(`文件解析到工作区外: ${file}`);
if (info.size > AGENT_LIMITS.maxFileBytes) violations.push(`文件超过 ${AGENT_LIMITS.maxFileBytes} 字节: ${file}`);
fileBytes += info.size;
}
} catch { /* deleted file */ }
}
await execa("git", ["add", "-N", "--", "."], { cwd: workspace });
const [diffResult, checkResult] = await Promise.all([
execa("git", ["diff", "--no-ext-diff", "--binary", "HEAD", "--"], { cwd: workspace }),
execa("git", ["diff", "--check", "HEAD", "--"], { cwd: workspace, reject: false }),
]);
if (checkResult.exitCode !== 0) violations.push("git diff --check 未通过: " + checkResult.stdout.slice(-1000));
const totalBytes = Buffer.byteLength(diffResult.stdout);
if (totalBytes > AGENT_LIMITS.maxDiffBytes) violations.push(`Diff 超过 ${AGENT_LIMITS.maxDiffBytes} 字节`);
return { passed: violations.length === 0, diff: diffResult.stdout, files, totalBytes: Math.max(totalBytes, fileBytes), violations };
}
}
import type { AgentFailureCategory } from "@webagent/shared";
export class AgentRunError extends Error {
constructor(public readonly category: AgentFailureCategory, message: string) {
super(message);
this.name = "AgentRunError";
}
}
export class AgentCancelledError extends AgentRunError {
constructor(message = "Agent 运行已取消") { super("cancelled", message); }
}
export class AgentTimeoutError extends AgentRunError {
constructor(message = "Agent 运行超时") { super("timeout", message); }
}
import type { AgentRunEvent } from "@webagent/shared";
import { applyPatch } from "../sites/apply-patch.js";
import { validatePatch } from "../security/path-policy.js";
import { AgentLoop } from "./agent-loop.js";
import type { AgentProviderInput, AgentProviderResult, CodingAgentProvider, RunningAgent } from "./contracts.js";
export class LocalAgentProvider implements CodingAgentProvider {
readonly name = "local";
readonly version = "1.0.0";
constructor(private readonly agent = new AgentLoop()) {}
async start(input: AgentProviderInput): Promise<RunningAgent> {
const controller = new AbortController();
const events: AgentRunEvent[] = [];
let done = false;
let wake: (() => void) | undefined;
const emit = (type: AgentRunEvent["type"], message: string, data?: Record<string, unknown>) => {
events.push({ runId: input.runId, type, timestamp: new Date().toISOString(), message, ...(data ? { data } : {}) });
wake?.(); wake = undefined;
};
const result = (async (): Promise<AgentProviderResult> => {
try {
emit("assistant", "本地 Agent 已开始分析修改要求");
const patch = await this.agent.generateLocal(input.workingDirectory, input.prompt);
if (controller.signal.aborted) return { status: "cancelled", error: "Agent 运行已取消" };
emit("tool_started", "正在校验并写入站点文件", { tool: "local_patch" });
await validatePatch(patch, input.workingDirectory);
await applyPatch(input.workingDirectory, patch);
emit("tool_completed", "站点文件修改完成", { files: patch.operations.map((operation) => operation.path) });
return { status: "completed", summary: patch.summary, usage: { turns: 1 } };
} catch (error) {
if (controller.signal.aborted) return { status: "cancelled", error: "Agent 运行已取消" };
return { status: "failed", error: error instanceof Error ? error.message : String(error) };
} finally {
done = true; wake?.();
}
})();
const stream: AsyncIterable<AgentRunEvent> = {
async *[Symbol.asyncIterator]() {
while (!done || events.length) {
if (!events.length) await new Promise<void>((resolve) => { wake = resolve; });
while (events.length) yield events.shift()!;
}
},
};
return { events: stream, result, cancel: async () => controller.abort() };
}
}
import crypto from "node:crypto";
import { access } from "node:fs/promises";
import type { AgentBuildValidation, AgentRunEvent, AgentRunStatus } from "@webagent/shared";
import { BuildError, BuildManager } from "../build/build-manager.js";
import { config, getPublicPreviewUrl } from "../config.js";
import { GitManager } from "../git/git-manager.js";
import { SiteRepository } from "../sites/site-repository.js";
import { AGENT_LIMITS, DEFAULT_AGENT_POLICY, type CodingAgentProvider, type RunningAgent } from "./contracts.js";
import { AgentDiffValidator } from "./diff-validator.js";
import { AgentCancelledError, AgentRunError, AgentTimeoutError } from "./errors.js";
import { AgentRunRepository } from "./run-repository.js";
import { assertAgentTransition } from "./state-machine.js";
export class AgentOrchestrator {
private active?: { runId: string; handle: RunningAgent };
constructor(
private readonly runs: AgentRunRepository,
private readonly providers: Map<string, CodingAgentProvider>,
private readonly sites: SiteRepository,
private readonly git: GitManager,
private readonly builds: BuildManager,
private readonly diffs = new AgentDiffValidator(),
) {}
async executeClaimed(runId: string): Promise<void> {
let timer: NodeJS.Timeout | undefined;
let cancelPoll: NodeJS.Timeout | undefined;
let project = "";
let workspace = "";
let baseCommit = "";
let creatingDraft = false;
let buildValidation: AgentBuildValidation | undefined;
try {
let run = this.requireRun(runId);
if (run.status !== "preparing_workspace") throw new AgentRunError("internal", "任务尚未被 Worker 正确领取");
const site = await this.sites.get(run.tenantId, run.siteId);
project = this.sites.getProjectPath(run.tenantId, run.siteId);
workspace = this.sites.getDraftPath(run.tenantId, run.siteId);
baseCommit = site.draftBaseCommit || site.currentCommit;
creatingDraft = !site.draftBaseCommit;
if (run.cancelRequested) throw new AgentCancelledError();
if (creatingDraft) await this.git.createPersistentWorktree(project, workspace, baseCommit);
else {
const exists = await access(workspace).then(() => true).catch(() => false);
if (!exists) throw new AgentRunError("workspace", "工作草稿目录不存在,请刷新站点后重试");
if (await this.git.currentCommit(workspace) !== baseCommit) throw new AgentRunError("workspace", "工作草稿基线与站点记录不一致");
}
this.runs.transition(runId, "preparing_workspace", { baselineCommit: baseCommit });
await this.sites.update(run.tenantId, run.siteId, { status: "building", lastError: undefined });
this.move(runId, "running_agent", "Agent 正在读取并修改网站");
run = this.requireRun(runId);
const provider = this.providers.get(run.provider);
if (!provider) throw new AgentRunError("provider", `未配置 Agent Provider: ${run.provider}`);
const handle = await provider.start({
runId, workingDirectory: workspace, prompt: run.prompt,
...(run.model ? { model: run.model } : {}),
...(run.sessionId ? { sessionId: run.sessionId, resumeSession: true } : {}),
timeoutMs: config.agent.timeoutMs || AGENT_LIMITS.timeoutMs,
maxTurns: config.agent.maxTurns || AGENT_LIMITS.maxTurns,
toolPolicy: DEFAULT_AGENT_POLICY,
});
this.active = { runId, handle };
let timedOut = false;
timer = setTimeout(() => { timedOut = true; void handle.cancel("Agent 运行超时"); }, config.agent.timeoutMs);
cancelPoll = setInterval(() => {
if (this.runs.get(runId)?.cancelRequested) void handle.cancel("用户取消了 Agent 运行");
}, 150);
const pump = (async () => { for await (const event of handle.events) this.runs.appendEvent(event); })();
const result = await handle.result;
await pump;
if (timer) { clearTimeout(timer); timer = undefined; }
this.runs.transition(runId, "running_agent", {
...(result.sessionId ? { sessionId: result.sessionId } : {}),
...(result.usage ? { usage: result.usage } : {}),
...(result.summary ? { summary: result.summary.slice(0, 1000) } : {}),
});
if (timedOut || result.status === "timed_out") throw new AgentTimeoutError();
if (result.status === "cancelled" || this.requireRun(runId).cancelRequested) throw new AgentCancelledError();
if (result.status === "failed") throw new AgentRunError("provider", result.error || "Agent Provider 运行失败");
this.move(runId, "checking_diff", "正在独立检查 Agent 文件改动");
const diff = await this.diffs.validate(workspace);
this.runs.transition(runId, "checking_diff", { changedFiles: diff.files });
if (!diff.passed) throw new AgentRunError("policy", diff.violations.join(";"));
if (this.requireRun(runId).cancelRequested) throw new AgentCancelledError();
this.move(runId, "validating_build", "正在独立构建网站");
const buildStarted = Date.now();
let dist: string;
try {
dist = await this.builds.build(workspace, "agent_" + runId, getPublicPreviewUrl(run.tenantId, run.siteId));
buildValidation = { passed: true, buildExitCode: 0, durationMs: Date.now() - buildStarted, output: "网站构建通过" };
this.runs.transition(runId, "validating_build", { validation: buildValidation });
} catch (error) {
const output = error instanceof BuildError ? error.output : error instanceof Error ? error.message : String(error);
buildValidation = { passed: false, buildExitCode: 1, durationMs: Date.now() - buildStarted, output: output.slice(-12_000) };
this.runs.transition(runId, "validating_build", { validation: buildValidation });
throw new AgentRunError("build", "Agent 修改未通过网站构建验证");
}
if (this.requireRun(runId).cancelRequested) throw new AgentCancelledError();
this.move(runId, "updating_preview", "构建通过,正在更新未保存草稿预览");
const latest = this.requireRun(runId);
const summary = (latest.summary || "更新网站").replace(/\s+/g, " ").slice(0, 120);
await this.builds.publishPreview(run.tenantId, run.siteId, dist);
await this.sites.update(run.tenantId, run.siteId, {
status: "ready", previewCommit: baseCommit,
draftBaseCommit: baseCommit, draftUpdatedAt: new Date().toISOString(), draftSummary: summary,
environmentVersion: 3, lastError: undefined,
});
this.move(runId, "completed", "Agent 修改已通过验证并保留为未保存草稿", {
endedAt: new Date().toISOString(), previewUrl: getPublicPreviewUrl(run.tenantId, run.siteId),
});
} catch (error) {
const run = this.runs.get(runId);
if (!run || ["completed", "failed", "cancelled", "timed_out"].includes(run.status)) return;
const known = error instanceof AgentRunError ? error : new AgentRunError("internal", error instanceof Error ? error.message : String(error));
const next: AgentRunStatus = known.category === "cancelled" ? "cancelled" : known.category === "timeout" ? "timed_out" : "failed";
let keepDraft = false;
if (workspace) keepDraft = await this.git.hasChanges(workspace).catch(() => false);
if (project && workspace && creatingDraft && !keepDraft) await this.git.removePersistentWorktree(project, workspace).catch(() => undefined);
if (run.tenantId && run.siteId) {
const site = await this.sites.get(run.tenantId, run.siteId).catch(() => undefined);
if (site) await this.sites.update(run.tenantId, run.siteId, {
status: site.previewCommit ? "ready" : "failed", lastError: known.message,
draftBaseCommit: keepDraft ? baseCommit : site.draftBaseCommit,
draftUpdatedAt: keepDraft ? new Date().toISOString() : site.draftUpdatedAt,
draftSummary: keepDraft ? "Agent 未完成的工作草稿" : site.draftSummary,
}).catch(() => undefined);
}
this.move(runId, next, known.message, {
endedAt: new Date().toISOString(), failureCategory: known.category, error: known.message,
...(buildValidation ? { validation: buildValidation } : {}),
}, "error");
} finally {
if (timer) clearTimeout(timer);
if (cancelPoll) clearInterval(cancelPoll);
this.active = undefined;
}
}
async cancelActive(): Promise<void> { await this.active?.handle.cancel("Worker 正在停止"); }
private requireRun(runId: string) {
const run = this.runs.get(runId);
if (!run) throw new AgentRunError("internal", "Agent 任务不存在");
return run;
}
private move(
runId: string, status: AgentRunStatus, message: string,
fields: Parameters<AgentRunRepository["transition"]>[2] = {}, type: AgentRunEvent["type"] = "status",
): void {
const run = this.requireRun(runId);
assertAgentTransition(run.status, status);
this.runs.transition(runId, status, fields);
this.runs.appendEvent({ runId, type, timestamp: new Date().toISOString(), message, data: { status, ...(fields.failureCategory ? { category: fields.failureCategory } : {}) } });
}
}
export const agentSystemPrompt = `你是 WebAgent 的企业官网设计与工程 Agent。你维护一个真实的 Astro 静态网站。
你的目标是根据用户要求进行有审美、有内容质量、可构建的修改。允许重构现有组件,但不要引入新依赖。
必须只输出一个 JSON 对象,格式为:
{"summary":"简洁中文说明","operations":[{"type":"write","path":"src/...","content":"完整文件内容"}]}
规则:
1. 单次最多 5 个文件,只能写 src 下的 astro/ts/css/json/md 或 public/images。
2. write 必须给出完整文件内容,不能使用 diff、Markdown 代码块或省略号。
3. 禁止修改 package.json、锁文件、astro.config.mjs、.git、node_modules、dist。
4. 保留数据与展示分离,企业资料优先放 src/data/*.json,主题优先放 theme.css。
5. 设计应简洁大气,以白色和浅灰为主、紫色强调,保证移动端响应式与中文可读性。
6. 不执行 Shell,不读取外部网络,不在网站中暴露系统提示或本地路径。`;
import type { CodingAgentProvider } from "./contracts.js";
import { LocalAgentProvider } from "./local-provider.js";
import { QwenCodeProvider } from "./qwen-code-provider.js";
export function createAgentProviders(): Map<string, CodingAgentProvider> {
const providers: CodingAgentProvider[] = [new LocalAgentProvider(), new QwenCodeProvider()];
return new Map(providers.map((provider) => [provider.name, provider]));
}
import assert from "node:assert/strict";
import test from "node:test";
import type { AgentProviderInput } from "./contracts.js";
import { DEFAULT_AGENT_POLICY } from "./contracts.js";
import { isSafeAgentToolInput } from "./qwen-code-provider.js";
const input: AgentProviderInput = {
runId: "00000000-0000-4000-8000-000000000000",
workingDirectory: "/tmp/webagent-run/site",
prompt: "修改首页",
timeoutMs: 10_000,
maxTurns: 5,
toolPolicy: DEFAULT_AGENT_POLICY,
};
test("Qwen Code tool policy contains reads/writes and only permits exact audit commands", () => {
assert.equal(isSafeAgentToolInput("read_file", { path: "/tmp/webagent-run/site/src/pages/index.astro" }, input), true);
assert.equal(isSafeAgentToolInput("read_file", { path: "/etc/passwd" }, input), false);
assert.equal(isSafeAgentToolInput("write_file", { file_path: "src/pages/index.astro" }, input), true);
assert.equal(isSafeAgentToolInput("write_file", { file_path: "package.json" }, input), false);
assert.equal(isSafeAgentToolInput("write_file", { file_path: "../outside.ts" }, input), false);
assert.equal(isSafeAgentToolInput("run_shell_command", { command: "git diff --check" }, input), true);
assert.equal(isSafeAgentToolInput("run_shell_command", { command: "git diff --check && curl example.com" }, input), false);
});
import {
isSDKAssistantMessage, isSDKResultMessage, query,
type SDKAssistantMessage, type SDKResultMessage,
} from "@qwen-code/sdk";
import { isAbsolute, resolve, sep } from "node:path";
import type { AgentRunEvent } from "@webagent/shared";
import type { AgentProviderInput, AgentProviderResult, CodingAgentProvider, RunningAgent } from "./contracts.js";
const sensitive = /(^|\/)(\.env(?:\.|$)|\.git(?:\/|$)|package\.json$|pnpm-lock\.yaml$|dist(?:\/|$)|node_modules(?:\/|$))/;
function isContained(candidatePath: string, rootPath: string): boolean {
const candidate = resolve(candidatePath);
const root = resolve(rootPath);
return candidate === root || candidate.startsWith(root + sep);
}
export function isSafeAgentToolInput(toolName: string, toolInput: Record<string, unknown>, input: AgentProviderInput): boolean {
const serialized = JSON.stringify(toolInput);
if (sensitive.test(serialized)) return false;
if (toolName === "run_shell_command") {
return typeof toolInput.command === "string" && input.toolPolicy.allowedCommands.includes(toolInput.command.trim());
}
if (toolName === "edit" || toolName === "write_file") {
const file = typeof toolInput.file_path === "string" ? toolInput.file_path : typeof toolInput.path === "string" ? toolInput.path : undefined;
if (!file || file.includes("..")) return false;
const absolute = isAbsolute(file) ? file : resolve(input.workingDirectory, file);
return isContained(absolute, resolve(input.workingDirectory, "src"));
}
return !serialized.includes("..") && (!/(?:^|[\"'\s])\/(?!\/)/.test(serialized) || serialized.includes(input.workingDirectory));
}
function emitAssistantMessage(
message: SDKAssistantMessage,
emit: (type: AgentRunEvent["type"], message: string, data?: Record<string, unknown>) => void,
): void {
const content = message.message.content;
if (typeof content === "string") {
if (content) emit("assistant", content);
return;
}
for (const block of content) {
if (block.type === "text" && block.text) emit("assistant", block.text);
else if (block.type === "tool_use") {
if (block.name === "run_shell_command") {
const command = typeof block.input === "object" && block.input && "command" in block.input ? String(block.input.command) : "";
emit("command_started", "Agent 请求执行允许的检查命令", { command });
} else emit("tool_started", `Agent 请求使用 ${block.name}`, { tool: block.name });
} else if (block.type === "tool_result") {
emit("tool_completed", block.is_error ? "工具执行失败" : "工具执行完成", { toolUseId: block.tool_use_id, isError: Boolean(block.is_error) });
}
}
}
function usageOf(message: SDKResultMessage) {
return {
inputTokens: message.usage.input_tokens,
outputTokens: message.usage.output_tokens,
totalTokens: message.usage.total_tokens ?? message.usage.input_tokens + message.usage.output_tokens,
turns: message.num_turns,
};
}
export class QwenCodeProvider implements CodingAgentProvider {
readonly name = "qwen-code";
readonly version = "0.1.8";
async start(input: AgentProviderInput): Promise<RunningAgent> {
const controller = new AbortController();
const events: AgentRunEvent[] = [];
let done = false;
let wake: (() => void) | undefined;
const emit = (type: AgentRunEvent["type"], message: string, data?: Record<string, unknown>) => {
events.push({ runId: input.runId, type, timestamp: new Date().toISOString(), message, ...(data ? { data } : {}) });
wake?.(); wake = undefined;
};
const session = query({
prompt: input.prompt,
options: {
cwd: input.workingDirectory,
...(input.model ? { model: input.model } : {}),
...(input.sessionId ? input.resumeSession ? { resume: input.sessionId } : { sessionId: input.sessionId } : {}),
permissionMode: "default",
abortController: controller,
maxSessionTurns: input.maxTurns,
includePartialMessages: false,
debug: process.env.QWEN_SDK_DEBUG === "1",
sandbox: true,
env: { ...process.env, SEATBELT_PROFILE: "restrictive-open" },
systemPrompt: {
type: "preset", preset: "qwen_code",
append: `你正在修改 WebAgent 的 Astro 网站。只能在 ${input.workingDirectory} 内工作。使用 read_file/glob/grep 检查代码;只能用 edit/write_file 修改 src;shell 只能执行明确允许的 Git 自检命令。完成编辑后重新读取每个改动文件,并依次执行 git diff --check、git diff --no-ext-diff、git status --short。发现问题必须修复。禁止安装依赖、构建、测试、部署、联网、computer-use 以及访问工作区外路径。`,
},
coreTools: ["read_file", "read_many_files", "list_directory", "glob", "grep_search", "edit", "write_file", "run_shell_command"],
excludeTools: [
"Read(/.env)", "Read(/.env.*)", "Read(/.qwen/**)", "Read(/.git/**)", "Read(/node_modules/**)", "Read(/dist/**)",
"Edit(/.env)", "Edit(/.env.*)", "Edit(/.qwen/**)", "Edit(/package.json)", "Edit(/pnpm-lock.yaml)", "Edit(/.git/**)", "Edit(/dist/**)", "Edit(/node_modules/**)",
"Bash(git push*)", "Bash(git remote*)", "Bash(pnpm add*)", "Bash(pnpm install*)", "Bash(npm install*)", "Bash(curl*)", "Bash(wget*)",
],
canUseTool: async (toolName, toolInput) => isSafeAgentToolInput(toolName, toolInput, input)
? { behavior: "allow", updatedInput: toolInput }
: { behavior: "deny", message: `WebAgent 安全策略拒绝了工具调用: ${toolName}` },
},
});
const result = (async (): Promise<AgentProviderResult> => {
let final: AgentProviderResult = { status: "failed", error: "Qwen Code 没有返回运行结果" };
try {
for await (const message of session) {
if (isSDKAssistantMessage(message)) emitAssistantMessage(message, emit);
else if (isSDKResultMessage(message)) {
const failed = message.subtype !== "success";
const error = failed && message.error ? message.error.message : failed ? "Qwen Code 运行失败" : undefined;
final = {
status: failed ? "failed" : "completed",
summary: message.subtype === "success" ? message.result : undefined,
sessionId: session.getSessionId(), usage: usageOf(message), ...(error ? { error } : {}),
};
}
}
return final;
} catch (error) {
if (controller.signal.aborted) return { status: "cancelled", error: "Qwen Code 已中止" };
return { status: "failed", error: error instanceof Error ? error.message : String(error) };
} finally {
done = true; wake?.();
}
})();
const stream: AsyncIterable<AgentRunEvent> = {
async *[Symbol.asyncIterator]() {
while (!done || events.length) {
if (!events.length) await new Promise<void>((resolve) => { wake = resolve; });
while (events.length) yield events.shift()!;
}
},
};
return {
events: stream,
result,
cancel: async () => {
controller.abort();
try { await session.interrupt(); } catch { /* session may already be closed */ }
try { await session.close(); } catch { /* session may already be closed */ }
},
};
}
}
import assert from "node:assert/strict";
import { mkdtemp, rm } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { AgentRunRepository } from "./run-repository.js";
test("AgentRunRepository persists, scopes and atomically claims runs", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "webagent-runs-"));
const repository = new AgentRunRepository(path.join(root, "runs.sqlite"));
try {
const created = repository.create({
tenantId: "tenant_a", siteId: "site_one", provider: "local", providerVersion: "1.0.0", prompt: "修改标题",
});
assert.equal(created.status, "queued");
assert.equal(repository.getScoped("tenant_a", "site_one", created.runId)?.runId, created.runId);
assert.equal(repository.getScoped("tenant_b", "site_one", created.runId), undefined);
assert.throws(() => repository.create({
tenantId: "tenant_a", siteId: "site_one", provider: "local", providerVersion: "1.0.0", prompt: "另一个任务",
}), /已有正在运行/);
const claimed = repository.claimNext();
assert.equal(claimed?.runId, created.runId);
assert.equal(claimed?.status, "preparing_workspace");
assert.equal(repository.claimNext(), undefined);
assert.ok(repository.events(created.runId).some((event) => event.data?.status === "preparing_workspace"));
assert.equal(repository.getAgentProvider("tenant_a"), "local");
assert.equal(repository.setAgentProvider("tenant_a", "qwen-code"), "qwen-code");
assert.equal(repository.getAgentProvider("tenant_a"), "qwen-code");
assert.equal(repository.getAgentProvider("tenant_b"), "local");
repository.requestCancel(created.runId);
assert.equal(repository.get(created.runId)?.cancelRequested, true);
} finally {
repository.close();
await rm(root, { recursive: true, force: true });
}
});
test("queued Agent runs can be cancelled before a worker claims them", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "webagent-cancel-"));
const repository = new AgentRunRepository(path.join(root, "runs.sqlite"));
try {
const run = repository.create({
tenantId: "tenant_a", siteId: "site_two", provider: "local", providerVersion: "1.0.0", prompt: "修改配色",
});
assert.equal(repository.requestCancel(run.runId), true);
assert.equal(repository.get(run.runId)?.status, "cancelled");
assert.equal(repository.claimNext(), undefined);
} finally {
repository.close();
await rm(root, { recursive: true, force: true });
}
});
import path from "node:path";
import { mkdirSync } from "node:fs";
import { DatabaseSync } from "node:sqlite";
import type {
AgentBuildValidation, AgentFailureCategory, AgentRunEvent, AgentRunInfo,
AgentRunStatus, AgentUsage,
} from "@webagent/shared";
import { config } from "../config.js";
type RunRow = {
run_id: string; tenant_id: string; site_id: string; provider: string; provider_version: string;
model: string | null; session_id: string | null; status: AgentRunStatus; prompt: string; summary: string | null;
created_at: string; started_at: string | null; ended_at: string | null; baseline_commit: string | null;
final_commit: string | null; changed_files: string; failure_category: AgentFailureCategory | null;
error: string | null; usage: string | null; validation: string | null; preview_url: string | null;
cancel_requested: number;
};
const terminalStates: readonly AgentRunStatus[] = ["completed", "failed", "cancelled", "timed_out"];
const activeStates: readonly AgentRunStatus[] = ["queued", "preparing_workspace", "running_agent", "checking_diff", "validating_build", "updating_preview"];
export class AgentRunRepository {
private readonly database: DatabaseSync;
constructor(databasePath = config.auth.databasePath) {
mkdirSync(path.dirname(databasePath), { recursive: true });
this.database = new DatabaseSync(databasePath);
this.database.exec("PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000;");
this.migrate();
}
create(input: {
tenantId: string; siteId: string; provider: string; providerVersion: string;
model?: string; sessionId?: string; prompt: string;
}): AgentRunInfo {
const runId = crypto.randomUUID();
const createdAt = new Date().toISOString();
try {
this.database.prepare(`
INSERT INTO agent_runs(
run_id,tenant_id,site_id,provider,provider_version,model,session_id,status,prompt,created_at
) VALUES(?,?,?,?,?,?,?,?,?,?)
`).run(runId, input.tenantId, input.siteId, input.provider, input.providerVersion, input.model ?? null, input.sessionId ?? null, "queued", input.prompt, createdAt);
} catch (error) {
if (String(error).includes("UNIQUE constraint failed")) throw Object.assign(new Error("该网站已有正在运行的 Agent 任务"), { statusCode: 409 });
throw error;
}
this.appendEvent({ runId, type: "status", timestamp: createdAt, message: "Agent 任务已进入队列", data: { status: "queued" } });
return this.get(runId)!;
}
get(runId: string): AgentRunInfo | undefined {
const row = this.database.prepare("SELECT * FROM agent_runs WHERE run_id = ?").get(runId) as RunRow | undefined;
return row ? this.toInfo(row) : undefined;
}
getScoped(tenantId: string, siteId: string, runId: string): AgentRunInfo | undefined {
const row = this.database.prepare("SELECT * FROM agent_runs WHERE run_id = ? AND tenant_id = ? AND site_id = ?")
.get(runId, tenantId, siteId) as RunRow | undefined;
return row ? this.toInfo(row) : undefined;
}
ownsSession(tenantId: string, siteId: string, sessionId: string): boolean {
return Boolean(this.database.prepare("SELECT 1 FROM agent_runs WHERE tenant_id = ? AND site_id = ? AND session_id = ? LIMIT 1")
.get(tenantId, siteId, sessionId));
}
getAgentProvider(tenantId: string): "local" | "qwen-code" {
const row = this.database.prepare("SELECT provider FROM agent_settings WHERE tenant_id = ?").get(tenantId) as { provider: string } | undefined;
return row?.provider === "qwen-code" ? "qwen-code" : row?.provider === "local" ? "local"
: config.agent.provider === "qwen-code" ? "qwen-code" : "local";
}
setAgentProvider(tenantId: string, provider: "local" | "qwen-code"): "local" | "qwen-code" {
this.database.prepare(`
INSERT INTO agent_settings(tenant_id, provider, updated_at) VALUES(?,?,?)
ON CONFLICT(tenant_id) DO UPDATE SET provider = excluded.provider, updated_at = excluded.updated_at
`).run(tenantId, provider, new Date().toISOString());
return provider;
}
claimNext(): AgentRunInfo | undefined {
this.database.exec("BEGIN IMMEDIATE");
try {
const row = this.database.prepare("SELECT run_id FROM agent_runs WHERE status = 'queued' AND cancel_requested = 0 ORDER BY created_at LIMIT 1").get() as { run_id: string } | undefined;
if (!row) { this.database.exec("COMMIT"); return undefined; }
const startedAt = new Date().toISOString();
const changed = this.database.prepare("UPDATE agent_runs SET status = 'preparing_workspace', started_at = ? WHERE run_id = ? AND status = 'queued'")
.run(startedAt, row.run_id);
if (Number(changed.changes) !== 1) { this.database.exec("ROLLBACK"); return undefined; }
this.insertEvent({ runId: row.run_id, type: "status", timestamp: startedAt, message: "正在准备隔离工作区", data: { status: "preparing_workspace" } });
this.database.exec("COMMIT");
return this.get(row.run_id);
} catch (error) {
this.database.exec("ROLLBACK");
throw error;
}
}
transition(runId: string, status: AgentRunStatus, fields: Partial<{
startedAt: string; endedAt: string; baselineCommit: string; finalCommit: string; changedFiles: string[];
failureCategory: AgentFailureCategory; error: string; usage: AgentUsage; validation: AgentBuildValidation;
previewUrl: string; sessionId: string; summary: string;
}> = {}): void {
const values: Record<string, string | undefined> = {
status,
started_at: fields.startedAt,
ended_at: fields.endedAt,
baseline_commit: fields.baselineCommit,
final_commit: fields.finalCommit,
changed_files: fields.changedFiles && JSON.stringify(fields.changedFiles),
failure_category: fields.failureCategory,
error: fields.error,
usage: fields.usage && JSON.stringify(fields.usage),
validation: fields.validation && JSON.stringify(fields.validation),
preview_url: fields.previewUrl,
session_id: fields.sessionId,
summary: fields.summary,
};
const entries = Object.entries(values).filter((entry): entry is [string, string] => entry[1] !== undefined);
this.database.prepare(`UPDATE agent_runs SET ${entries.map(([column]) => `${column} = ?`).join(", ")} WHERE run_id = ?`)
.run(...entries.map(([, value]) => value), runId);
}
requestCancel(runId: string): boolean {
const run = this.get(runId);
if (!run) return false;
if (terminalStates.includes(run.status)) return true;
const now = new Date().toISOString();
if (run.status === "queued") {
this.transition(runId, "cancelled", { endedAt: now, failureCategory: "cancelled", error: "任务在执行前被取消" });
this.appendEvent({ runId, type: "status", timestamp: now, message: "Agent 任务已取消", data: { status: "cancelled" } });
} else this.database.prepare("UPDATE agent_runs SET cancel_requested = 1 WHERE run_id = ?").run(runId);
return true;
}
appendEvent(event: AgentRunEvent): number { return this.insertEvent(event); }
events(runId: string, after = 0): AgentRunEvent[] {
const rows = this.database.prepare("SELECT * FROM agent_events WHERE run_id = ? AND sequence > ? ORDER BY sequence")
.all(runId, after) as Array<{ sequence: number; run_id: string; type: AgentRunEvent["type"]; timestamp: string; message: string; data: string | null }>;
return rows.map((row) => ({
sequence: row.sequence, runId: row.run_id, type: row.type, timestamp: row.timestamp, message: row.message,
...(row.data ? { data: JSON.parse(row.data) as Record<string, unknown> } : {}),
}));
}
recoverInterruptedRuns(): number {
const placeholders = activeStates.filter((state) => state !== "queued").map(() => "?").join(",");
const rows = this.database.prepare(`SELECT run_id FROM agent_runs WHERE status IN (${placeholders})`).all(...activeStates.filter((state) => state !== "queued")) as Array<{ run_id: string }>;
const now = new Date().toISOString();
for (const row of rows) {
this.transition(row.run_id, "failed", { endedAt: now, failureCategory: "internal", error: "Worker 重启中断了 Agent 运行,请重新提交要求" });
this.appendEvent({ runId: row.run_id, type: "error", timestamp: now, message: "Worker 重启中断了 Agent 运行", data: { category: "internal" } });
}
return rows.length;
}
close(): void { this.database.close(); }
private insertEvent(event: AgentRunEvent): number {
const result = this.database.prepare("INSERT INTO agent_events(run_id,type,timestamp,message,data) VALUES(?,?,?,?,?)")
.run(event.runId, event.type, event.timestamp, event.message, event.data ? JSON.stringify(event.data) : null);
return Number(result.lastInsertRowid);
}
private toInfo(row: RunRow): AgentRunInfo {
return {
runId: row.run_id, tenantId: row.tenant_id, siteId: row.site_id, provider: row.provider,
providerVersion: row.provider_version, ...(row.model ? { model: row.model } : {}),
...(row.session_id ? { sessionId: row.session_id } : {}), status: row.status, prompt: row.prompt,
...(row.summary ? { summary: row.summary } : {}), createdAt: row.created_at,
...(row.started_at ? { startedAt: row.started_at } : {}), ...(row.ended_at ? { endedAt: row.ended_at } : {}),
...(row.baseline_commit ? { baselineCommit: row.baseline_commit } : {}), ...(row.final_commit ? { finalCommit: row.final_commit } : {}),
changedFiles: JSON.parse(row.changed_files) as string[], ...(row.failure_category ? { failureCategory: row.failure_category } : {}),
...(row.error ? { error: row.error } : {}), ...(row.usage ? { usage: JSON.parse(row.usage) as AgentUsage } : {}),
...(row.validation ? { validation: JSON.parse(row.validation) as AgentBuildValidation } : {}),
...(row.preview_url ? { previewUrl: row.preview_url } : {}), cancelRequested: Boolean(row.cancel_requested),
};
}
private migrate(): void {
this.database.exec(`
CREATE TABLE IF NOT EXISTS agent_runs (
run_id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL, site_id TEXT NOT NULL,
provider TEXT NOT NULL, provider_version TEXT NOT NULL, model TEXT, session_id TEXT,
status TEXT NOT NULL, prompt TEXT NOT NULL, summary TEXT, created_at TEXT NOT NULL,
started_at TEXT, ended_at TEXT, baseline_commit TEXT, final_commit TEXT,
changed_files TEXT NOT NULL DEFAULT '[]', failure_category TEXT, error TEXT,
usage TEXT, validation TEXT, preview_url TEXT, cancel_requested INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS agent_events (
sequence INTEGER PRIMARY KEY AUTOINCREMENT, run_id TEXT NOT NULL REFERENCES agent_runs(run_id) ON DELETE CASCADE,
type TEXT NOT NULL, timestamp TEXT NOT NULL, message TEXT NOT NULL, data TEXT
);
CREATE INDEX IF NOT EXISTS idx_agent_events_run_sequence ON agent_events(run_id, sequence);
CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_runs_one_active_site ON agent_runs(tenant_id, site_id)
WHERE status IN ('queued','preparing_workspace','running_agent','checking_diff','validating_build','updating_preview');
CREATE TABLE IF NOT EXISTS agent_settings (
tenant_id TEXT PRIMARY KEY, provider TEXT NOT NULL CHECK(provider IN ('local','qwen-code')), updated_at TEXT NOT NULL
);
`);
}
}
import type { AgentRunStatus } from "@webagent/shared";
const transitions: Record<AgentRunStatus, readonly AgentRunStatus[]> = {
queued: ["preparing_workspace", "cancelled"],
preparing_workspace: ["running_agent", "failed", "cancelled", "timed_out"],
running_agent: ["checking_diff", "failed", "cancelled", "timed_out"],
checking_diff: ["validating_build", "failed", "cancelled", "timed_out"],
validating_build: ["updating_preview", "failed", "cancelled", "timed_out"],
updating_preview: ["completed", "failed", "cancelled", "timed_out"],
completed: [], failed: [], cancelled: [], timed_out: [],
};
export function assertAgentTransition(from: AgentRunStatus, to: AgentRunStatus): void {
if (!transitions[from].includes(to)) throw new Error(`无效的 Agent 状态转换: ${from} -> ${to}`);
}
import path from "node:path";
import { fileURLToPath } from "node:url";
import "dotenv/config";
import { config as loadEnv } from "dotenv";
const currentDir = path.dirname(fileURLToPath(import.meta.url));
const rootDir = path.resolve(currentDir, "../..");
loadEnv({ path: path.join(rootDir, ".env") });
const configuredSitesDir = process.env.WEBAGENT_SITES_DIR?.trim() || "../WebAgent-sites";
const sitesDir = path.isAbsolute(configuredSitesDir)
? configuredSitesDir
......@@ -28,6 +29,9 @@ const configuredDomainCheckInterval = Number(process.env.WEBAGENT_DOMAIN_CHECK_I
const domainCheckIntervalMs = Number.isFinite(configuredDomainCheckInterval)
? Math.max(15000, configuredDomainCheckInterval)
: 60000;
const configuredAgentTimeout = Number(process.env.WEBAGENT_AGENT_TIMEOUT_MS || 600000);
const configuredAgentTurns = Number(process.env.WEBAGENT_AGENT_MAX_TURNS || 20);
const configuredAgentProvider = process.env.WEBAGENT_AGENT_PROVIDER?.trim();
const environment = process.env.NODE_ENV?.trim() || "development";
const testDataFlag = process.env.WEBAGENT_INIT_TEST_DATA?.trim();
const testDataEnabled = testDataFlag == null || testDataFlag === ""
......@@ -72,6 +76,11 @@ export const config = {
baseUrl: (process.env.OPENAI_BASE_URL || "https://api.deepseek.com/v1").replace(/\/$/, ""),
model: process.env.OPENAI_MODEL || "deepseek-chat",
},
agent: {
provider: configuredAgentProvider === "qwen-code" ? "qwen-code" : "local",
timeoutMs: Number.isFinite(configuredAgentTimeout) ? Math.max(10_000, configuredAgentTimeout) : 600_000,
maxTurns: Number.isFinite(configuredAgentTurns) ? Math.max(1, Math.floor(configuredAgentTurns)) : 20,
},
domains: {
cnameTarget: (process.env.WEBAGENT_DOMAIN_CNAME_TARGET || "domains.webagent.local").trim().toLowerCase().replace(/\.$/, ""),
ipv4: process.env.WEBAGENT_DOMAIN_IPV4?.trim() || "",
......@@ -95,6 +104,7 @@ export const runtimePaths = {
uploads: path.join(config.runtimeDir, "uploads"),
pnpmStore: path.join(config.runtimeDir, "pnpm-store"),
logs: path.join(config.runtimeDir, "logs"),
agentRuns: path.join(config.runtimeDir, "agent-runs"),
domainMaps: path.join(config.runtimeDir, "nginx"),
};
......
......@@ -24,12 +24,6 @@ export class GitManager {
return result.stdout.trim();
}
async previousCommit(projectPath: string, commit = "HEAD"): Promise<string | undefined> {
await this.assertCommit(projectPath, commit);
const result = await execa("git", ["rev-list", "--parents", "-n", "1", commit], { cwd: projectPath });
return result.stdout.trim().split(/\s+/)[1];
}
async history(projectPath: string): Promise<GitHistoryItem[]> {
const current = await this.currentCommit(projectPath);
const result = await execa("git", ["log", "--date=iso-strict", "--format=%H%x1f%h%x1f%s%x1f%an%x1f%cI%x1e", "-30"], { cwd: projectPath });
......@@ -94,10 +88,10 @@ export class GitManager {
return this.currentCommit(projectPath);
}
async restoreAsCommit(projectPath: string, sourceCommit: string): Promise<string> {
async restoreAsCommit(projectPath: string, sourceCommit: string, message = "恢复版本 " + sourceCommit.slice(0, 7)): Promise<string> {
await this.assertCommit(projectPath, sourceCommit);
await execa("git", ["restore", "--source", sourceCommit, "--staged", "--worktree", "--", ".", ":(exclude)astro.config.mjs"], { cwd: projectPath });
return this.commit(projectPath, "Rollback to " + sourceCommit.slice(0, 7));
return this.commit(projectPath, message);
}
async assertCommit(projectPath: string, commit: string): Promise<void> {
......
......@@ -17,12 +17,22 @@ export const loginSchema = z.object({
export const chatSchema = z.object({
message: z.string().trim().min(2).max(2000),
model: z.string().trim().min(1).max(120).optional(),
sessionId: z.string().trim().min(1).max(200).optional(),
});
export const agentSettingsSchema = z.object({
provider: z.enum(["local", "qwen-code"]),
});
export const versionCommitSchema = z.object({
commit: z.string().regex(/^[0-9a-f]{7,40}$/),
});
export const saveDraftSchema = z.object({
message: z.string().trim().min(2, "版本说明至少 2 个字符").max(120),
});
export const addDomainSchema = z.object({
hostname: z.string().trim().min(4, "请输入完整域名").max(253),
});
......@@ -39,11 +49,3 @@ export const createTenantSchema = z.object({
export const tenantStatusSchema = z.object({ enabled: z.boolean() });
export const resetPasswordSchema = z.object({ password: z.string().min(8, "密码至少 8 个字符").max(200) });
export const sitePatchSchema = z.object({
summary: z.string().trim().min(2).max(200),
operations: z.array(z.discriminatedUnion("type", [
z.object({ type: z.literal("write"), path: z.string(), content: z.string() }),
z.object({ type: z.literal("delete"), path: z.string() }),
])).min(1).max(5),
});
......@@ -4,18 +4,18 @@ import Fastify from "fastify";
import cors from "@fastify/cors";
import { ZodError } from "zod";
import type { SessionInfo, TenantAdminInfo } from "@webagent/shared";
import { AgentLoop } from "./agent/agent-loop.js";
import { createAgentProviders } from "./agent/providers.js";
import { AgentRunRepository } from "./agent/run-repository.js";
import { AuthRepository } from "./auth/auth-repository.js";
import { BuildManager } from "./build/build-manager.js";
import { config, runtimePaths } from "./config.js";
import { GitManager } from "./git/git-manager.js";
import { PreviewProcessManager } from "./preview/preview-process-manager.js";
import {
addDomainSchema, chatSchema, createSiteSchema, createTenantSchema, loginSchema, resetPasswordSchema,
addDomainSchema, agentSettingsSchema, chatSchema, createSiteSchema, createTenantSchema, loginSchema, resetPasswordSchema, saveDraftSchema,
tenantStatusSchema, updateDomainSchema, versionCommitSchema,
} from "./schemas.js";
import { CreateSiteService } from "./sites/create-site.js";
import { SiteAgentService } from "./sites/site-agent-service.js";
import { SiteRepository } from "./sites/site-repository.js";
import { SiteVersionService } from "./sites/site-version-service.js";
import { SiteLifecycleService } from "./sites/site-lifecycle-service.js";
......@@ -39,7 +39,8 @@ const git = new GitManager();
const builds = new BuildManager();
const previews = new PreviewProcessManager();
const createSite = new CreateSiteService(sites, git, builds, previews);
const siteAgent = new SiteAgentService(sites, git, new AgentLoop());
const agentRuns = new AgentRunRepository();
const agentProviders = createAgentProviders();
const versions = new SiteVersionService(sites, git, builds, previews);
const domainRepository = new DomainRepository();
const domainRouting = new DomainRoutingConfig(domainRepository);
......@@ -56,7 +57,10 @@ app.addHook("onRequest", async (request, reply) => {
request.auth = session;
});
app.get("/api/health", async () => ({ status: "ok", service: "WebAgent", agentMode: config.openai.apiKey ? "model" : "local", timestamp: new Date().toISOString() }));
app.get("/api/health", async () => ({
status: "ok", service: "WebAgent", agentMode: config.agent.provider === "local" ? "local" : "model",
agentProvider: config.agent.provider, timestamp: new Date().toISOString(),
}));
app.post("/api/login", async (request) => {
const body = loginSchema.parse(request.body);
const result = auth.login(body.username, body.password);
......@@ -68,6 +72,18 @@ app.post("/api/logout", async (request) => {
return { success: true };
});
app.get("/api/session", async (request) => request.auth!);
app.get("/api/agent-settings", async (request) => ({
provider: agentRuns.getAgentProvider(requireTenant(request.auth)),
realAvailable: Boolean(config.openai.apiKey),
}));
app.put("/api/agent-settings", async (request) => {
const tenantId = requireTenant(request.auth);
const body = agentSettingsSchema.parse(request.body);
if (body.provider === "qwen-code" && !config.openai.apiKey) {
throw Object.assign(new Error("真实 Agent 尚未配置模型密钥"), { statusCode: 409 });
}
return { provider: agentRuns.setAgentProvider(tenantId, body.provider), realAvailable: Boolean(config.openai.apiKey) };
});
app.get("/api/sites", async (request) => sites.list(requireTenant(request.auth)));
app.get("/api/archived-sites", async (request) => sites.listArchived(requireTenant(request.auth)));
......@@ -79,9 +95,53 @@ app.delete<{ Params: { siteId: string } }>("/api/archived-sites/:siteId", async
await siteLifecycle.permanentlyDelete(requireTenant(request.auth), request.params.siteId);
return reply.code(204).send();
});
app.post<{ Params: { siteId: string } }>("/api/sites/:siteId/chat", async (request) => {
app.post<{ Params: { siteId: string } }>("/api/sites/:siteId/chat", async (request, reply) => {
const body = chatSchema.parse(request.body);
return siteAgent.execute(requireTenant(request.auth), request.params.siteId, body.message);
return reply.code(202).send(await enqueueAgentRun(requireTenant(request.auth), request.params.siteId, body));
});
app.post<{ Params: { siteId: string } }>("/api/sites/:siteId/agent-runs", async (request, reply) => {
const body = chatSchema.parse(request.body);
return reply.code(202).send(await enqueueAgentRun(requireTenant(request.auth), request.params.siteId, body));
});
app.get<{ Params: { siteId: string; runId: string } }>("/api/sites/:siteId/agent-runs/:runId", async (request, reply) => {
return agentRuns.getScoped(requireTenant(request.auth), request.params.siteId, request.params.runId)
|| reply.code(404).send({ error: "Agent 任务不存在" });
});
app.post<{ Params: { siteId: string; runId: string } }>("/api/sites/:siteId/agent-runs/:runId/cancel", async (request, reply) => {
const tenantId = requireTenant(request.auth);
if (!agentRuns.getScoped(tenantId, request.params.siteId, request.params.runId)) return reply.code(404).send({ error: "Agent 任务不存在" });
agentRuns.requestCancel(request.params.runId);
return reply.code(202).send({ cancelRequested: true });
});
app.get<{ Params: { siteId: string; runId: string } }>("/api/sites/:siteId/agent-runs/:runId/report", async (request, reply) => {
const run = agentRuns.getScoped(requireTenant(request.auth), request.params.siteId, request.params.runId);
return run || reply.code(404).send({ error: "Agent 任务不存在" });
});
app.get<{ Params: { siteId: string; runId: string } }>("/api/sites/:siteId/agent-runs/:runId/events", async (request, reply) => {
const tenantId = requireTenant(request.auth);
const run = agentRuns.getScoped(tenantId, request.params.siteId, request.params.runId);
if (!run) return reply.code(404).send({ error: "Agent 任务不存在" });
reply.hijack();
const response = reply.raw;
response.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive", "x-accel-buffering": "no" });
let cursor = Number(request.headers["last-event-id"] || 0);
const terminal = new Set(["completed", "failed", "cancelled", "timed_out"]);
const send = () => {
for (const event of agentRuns.events(request.params.runId, cursor)) {
cursor = event.sequence!;
response.write(`id: ${cursor}\nevent: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
}
const current = agentRuns.getScoped(tenantId, request.params.siteId, request.params.runId);
if (current && terminal.has(current.status)) {
response.write(`event: end\ndata: ${JSON.stringify({ status: current.status })}\n\n`);
response.end();
return true;
}
return false;
};
if (send()) return;
const timer = setInterval(() => { if (send()) clearInterval(timer); }, 150);
request.raw.on("close", () => clearInterval(timer));
});
app.post<{ Params: { siteId: string } }>("/api/sites/:siteId/build", async (request) => versions.rebuild(requireTenant(request.auth), request.params.siteId));
app.post<{ Params: { siteId: string } }>("/api/sites/:siteId/preview-version", async (request) => {
......@@ -89,6 +149,15 @@ app.post<{ Params: { siteId: string } }>("/api/sites/:siteId/preview-version", a
return versions.previewVersion(requireTenant(request.auth), request.params.siteId, body.commit);
});
app.post<{ Params: { siteId: string } }>("/api/sites/:siteId/preview-draft", async (request) => versions.previewDraft(requireTenant(request.auth), request.params.siteId));
app.post<{ Params: { siteId: string } }>("/api/sites/:siteId/save-draft", async (request) => {
const body = saveDraftSchema.parse(request.body);
return versions.saveDraft(requireTenant(request.auth), request.params.siteId, body.message);
});
app.post<{ Params: { siteId: string } }>("/api/sites/:siteId/discard-draft", async (request) => versions.discardDraft(requireTenant(request.auth), request.params.siteId));
app.post<{ Params: { siteId: string } }>("/api/sites/:siteId/restore-version", async (request) => {
const body = versionCommitSchema.parse(request.body);
return versions.restoreVersion(requireTenant(request.auth), request.params.siteId, body.commit);
});
app.post<{ Params: { siteId: string } }>("/api/sites/:siteId/publish", async (request) => {
const tenantId = requireTenant(request.auth);
const result = await versions.publish(tenantId, request.params.siteId);
......@@ -100,8 +169,6 @@ app.get<{ Params: { siteId: string } }>("/api/sites/:siteId/history", async (req
await sites.get(tenantId, request.params.siteId);
return git.history(sites.getProjectPath(tenantId, request.params.siteId));
});
app.post<{ Params: { siteId: string } }>("/api/sites/:siteId/undo", async (request) => versions.undo(requireTenant(request.auth), request.params.siteId));
app.get<{ Params: { siteId: string } }>("/api/sites/:siteId/domains", async (request) => domainService.list(requireTenant(request.auth), request.params.siteId));
app.post<{ Params: { siteId: string } }>("/api/sites/:siteId/domains", async (request, reply) => {
const body = addDomainSchema.parse(request.body);
......@@ -232,6 +299,29 @@ const checkPendingDomains = async () => {
setInterval(() => void checkPendingDomains(), config.domains.checkIntervalMs).unref();
setInterval(() => void siteLifecycle.purgeExpired().catch((error) => app.log.warn(error)), 24 * 60 * 60 * 1000).unref();
async function enqueueAgentRun(
tenantId: string, siteId: string,
input: { message: string; model?: string; sessionId?: string },
) {
const site = await sites.get(tenantId, siteId);
if (site.previewCommit && site.previewCommit !== site.currentCommit && !site.draftBaseCommit) {
throw Object.assign(new Error("当前正在查看历史版本,请先切换回最近保存版本再继续修改"), { statusCode: 409 });
}
const providerName = agentRuns.getAgentProvider(tenantId);
if (providerName === "qwen-code" && !config.openai.apiKey) {
throw Object.assign(new Error("真实 Agent 尚未配置模型密钥,请在设置中选择模拟 Agent 或联系管理员配置密钥"), { statusCode: 409 });
}
const provider = agentProviders.get(providerName);
if (!provider) throw Object.assign(new Error(`未配置 Agent Provider: ${providerName}`), { statusCode: 503 });
if (input.sessionId && !agentRuns.ownsSession(tenantId, siteId, input.sessionId)) {
throw Object.assign(new Error("不能恢复其他网站或未知的 Agent 会话"), { statusCode: 403 });
}
return agentRuns.create({
tenantId, siteId, provider: provider.name, providerVersion: provider.version,
model: input.model || config.openai.model, ...(input.sessionId ? { sessionId: input.sessionId } : {}), prompt: input.message,
});
}
function bearerToken(header: string | undefined): string | undefined {
const match = header?.match(/^Bearer\s+(.+)$/i);
return match?.[1];
......
import type { ChatResult, SitePatch } from "@webagent/shared";
import { AgentLoop } from "../agent/agent-loop.js";
import { GitManager } from "../git/git-manager.js";
import { validatePatch } from "../security/path-policy.js";
import { applyPatch } from "./apply-patch.js";
import { SiteRepository } from "./site-repository.js";
export class SiteAgentService {
constructor(
private readonly sites: SiteRepository, private readonly git: GitManager,
private readonly agent: AgentLoop,
) {}
async execute(tenantId: string, siteId: string, message: string): Promise<ChatResult> {
const site = await this.sites.get(tenantId, siteId);
const projectPath = this.sites.getProjectPath(tenantId, siteId);
const workspace = this.sites.getDraftPath(tenantId, siteId);
const baseCommit = site.draftBaseCommit || site.currentCommit;
const creatingDraft = !site.draftBaseCommit;
await this.sites.update(tenantId, siteId, { status: "building", lastError: undefined });
let generated: { patch: SitePatch; mode: "model" | "local" } | undefined;
try {
if (creatingDraft) await this.git.createPersistentWorktree(projectPath, workspace, baseCommit);
generated = await this.agent.generate(workspace, message);
await validatePatch(generated.patch, workspace);
await applyPatch(workspace, generated.patch);
const changedFiles = generated.patch.operations.map((item) => item.path);
const updatedAt = new Date().toISOString();
await this.sites.update(tenantId, siteId, {
status: "ready", draftBaseCommit: baseCommit, draftUpdatedAt: updatedAt,
draftSummary: generated.patch.summary, lastError: undefined,
});
return { summary: generated.patch.summary, draft: true, baseCommit, updatedAt, changedFiles, mode: generated.mode };
} catch (error) {
const details = error instanceof Error ? error.message : String(error);
const keepDraft = !creatingDraft || await this.git.hasChanges(workspace).catch(() => false);
if (!keepDraft) await this.git.removePersistentWorktree(projectPath, workspace);
await this.sites.update(tenantId, siteId, {
status: site.previewCommit ? "ready" : "failed", lastError: details,
draftBaseCommit: keepDraft ? baseCommit : undefined,
draftUpdatedAt: keepDraft ? new Date().toISOString() : undefined,
});
throw error;
}
}
}
import crypto from "node:crypto";
import path from "node:path";
import type { PreviewVersionResult, PublishResult, SiteInfo } from "@webagent/shared";
import type { DraftPreviewResult, PreviewVersionResult, PublishResult, SiteInfo } from "@webagent/shared";
import { getPublicPreviewUrl, getPublicProductionUrl, runtimePaths } from "../config.js";
import { BuildError, BuildManager } from "../build/build-manager.js";
import { GitManager } from "../git/git-manager.js";
......@@ -15,8 +15,12 @@ export class SiteVersionService {
) {}
async rebuild(tenantId: string, siteId: string): Promise<{ previewUrl: string }> {
const project = this.sites.getProjectPath(tenantId, siteId);
const site = await this.sites.get(tenantId, siteId);
if (site.draftBaseCommit) {
await this.previewDraft(tenantId, siteId);
return { previewUrl: site.previewUrl };
}
const project = this.sites.getProjectPath(tenantId, siteId);
let currentCommit = await this.git.currentCommit(project);
let previewCommit = site.previewCommit || site.currentCommit || currentCommit;
if (await ensureEnvironmentConfig(project)) {
......@@ -35,7 +39,7 @@ export class SiteVersionService {
return { commit: targetCommit, previewUrl: site.previewUrl };
}
async previewDraft(tenantId: string, siteId: string): Promise<PreviewVersionResult> {
async previewDraft(tenantId: string, siteId: string): Promise<DraftPreviewResult> {
const site = await this.sites.get(tenantId, siteId);
if (!site.draftBaseCommit) throw new Error("当前没有可预览的工作草稿");
const project = this.sites.getProjectPath(tenantId, siteId);
......@@ -47,14 +51,44 @@ export class SiteVersionService {
try {
await ensureEnvironmentConfig(workspace);
const dist = await this.builds.build(workspace, taskId, getPublicPreviewUrl(tenantId, siteId));
const draftCommit = await this.git.commit(workspace, "Agent: " + (site.draftSummary || "保存工作草稿"));
const published = await this.builds.publishPreview(tenantId, siteId, dist);
await this.previews.start(tenantId, siteId, published, site.previewPort);
await this.sites.update(tenantId, siteId, {
status: "ready", previewCommit: site.draftBaseCommit,
draftUpdatedAt: new Date().toISOString(),
environmentVersion: 3, lastError: undefined,
});
return { baseCommit: site.draftBaseCommit, previewUrl: site.previewUrl };
} catch (error) {
const details = error instanceof BuildError ? error.output.slice(-3000) : error instanceof Error ? error.message : String(error);
await this.sites.update(tenantId, siteId, { status: site.previewCommit ? "ready" : "failed", lastError: details });
throw error;
}
}
async saveDraft(tenantId: string, siteId: string, message: string): Promise<PreviewVersionResult> {
const site = await this.sites.get(tenantId, siteId);
if (!site.draftBaseCommit) throw new Error("当前没有可保存的未保存修改");
const project = this.sites.getProjectPath(tenantId, siteId);
const workspace = this.sites.getDraftPath(tenantId, siteId);
const draftHead = await this.git.currentCommit(workspace);
if (draftHead === site.draftBaseCommit && !await this.git.hasChanges(workspace)) {
throw new Error("工作草稿没有实际修改");
}
const taskId = "save_draft_" + crypto.randomBytes(4).toString("hex");
await this.sites.update(tenantId, siteId, { status: "building", lastError: undefined });
try {
await ensureEnvironmentConfig(workspace);
const dist = await this.builds.build(workspace, taskId, getPublicPreviewUrl(tenantId, siteId));
const draftCommit = await this.git.hasChanges(workspace)
? await this.git.commit(workspace, message)
: await this.git.currentCommit(workspace);
const commit = await this.git.fastForward(project, draftCommit, site.draftBaseCommit);
const published = await this.builds.publishPreview(tenantId, siteId, dist);
await this.previews.start(tenantId, siteId, published, site.previewPort);
await this.sites.update(tenantId, siteId, {
status: "ready", currentCommit: commit, previewCommit: commit,
previousCommit: site.currentCommit, draftBaseCommit: undefined,
draftUpdatedAt: undefined, draftSummary: undefined,
draftBaseCommit: undefined, draftUpdatedAt: undefined, draftSummary: undefined,
environmentVersion: 3, lastError: undefined,
});
await this.git.removePersistentWorktree(project, workspace);
......@@ -66,18 +100,25 @@ export class SiteVersionService {
}
}
async undo(tenantId: string, siteId: string): Promise<PreviewVersionResult> {
async discardDraft(tenantId: string, siteId: string): Promise<PreviewVersionResult> {
const site = await this.sites.get(tenantId, siteId);
if (site.draftBaseCommit) throw new Error("工作草稿尚未预览,请先预览草稿或继续编辑");
if (!site.draftBaseCommit) throw new Error("当前没有可放弃的未保存修改");
const project = this.sites.getProjectPath(tenantId, siteId);
if (site.previewCommit && site.previewCommit !== site.currentCommit) {
throw new Error("当前正在查看历史版本,请先切换回最近保存版本再撤销修改");
await this.buildPreview(site, site.currentCommit, "discard_draft");
await this.git.removePersistentWorktree(project, this.sites.getDraftPath(tenantId, siteId));
await this.sites.update(tenantId, siteId, {
draftBaseCommit: undefined, draftUpdatedAt: undefined, draftSummary: undefined,
});
return { commit: site.currentCommit, previewUrl: site.previewUrl };
}
const currentCommit = await this.git.currentCommit(project);
const targetCommit = site.previousCommit || await this.git.previousCommit(project, currentCommit);
if (!targetCommit) throw new Error("当前没有可撤销的上一版本");
async restoreVersion(tenantId: string, siteId: string, targetCommit: string): Promise<PreviewVersionResult> {
const site = await this.sites.get(tenantId, siteId);
if (site.draftBaseCommit) throw new Error("存在未保存修改,请先保存或放弃草稿");
const project = this.sites.getProjectPath(tenantId, siteId);
await this.git.assertCommit(project, targetCommit);
const taskId = "undo_" + crypto.randomBytes(4).toString("hex");
if (targetCommit === site.currentCommit) throw new Error("目标已经是当前保存版本");
const taskId = "restore_" + crypto.randomBytes(4).toString("hex");
const workspace = path.join(runtimePaths.builds, taskId, "workspace");
await this.sites.update(tenantId, siteId, { status: "building", lastError: undefined });
try {
......@@ -89,7 +130,7 @@ export class SiteVersionService {
await this.previews.start(tenantId, siteId, published, site.previewPort);
await this.sites.update(tenantId, siteId, {
status: "ready", currentCommit: commit, previewCommit: commit,
previousCommit: undefined, environmentVersion: 3, lastError: undefined,
environmentVersion: 3, lastError: undefined,
});
return { commit, previewUrl: site.previewUrl };
} catch (error) {
......@@ -102,6 +143,7 @@ export class SiteVersionService {
const project = this.sites.getProjectPath(tenantId, siteId);
const site = await this.sites.get(tenantId, siteId);
if (site.status !== "ready") throw new Error("测试环境尚未构建成功,不能发布到生产环境");
if (site.draftBaseCommit) throw new Error("存在未保存修改,请先在“版本”中保存或放弃草稿后再发布");
const taskId = "publish_" + crypto.randomBytes(4).toString("hex");
const workspace = path.join(runtimePaths.builds, taskId, "workspace");
await this.sites.update(tenantId, siteId, { publishStatus: "publishing", lastPublishError: undefined });
......
import { AgentOrchestrator } from "./agent/orchestrator.js";
import { createAgentProviders } from "./agent/providers.js";
import { AgentRunRepository } from "./agent/run-repository.js";
import { BuildManager } from "./build/build-manager.js";
import { GitManager } from "./git/git-manager.js";
import { SiteRepository } from "./sites/site-repository.js";
const runs = new AgentRunRepository();
const sites = new SiteRepository();
await sites.ensureRuntime();
runs.recoverInterruptedRuns();
const orchestrator = new AgentOrchestrator(runs, createAgentProviders(), sites, new GitManager(), new BuildManager());
let stopping = false;
const stop = () => { stopping = true; void orchestrator.cancelActive(); };
process.on("SIGTERM", stop);
process.on("SIGINT", stop);
while (!stopping) {
const run = runs.claimNext();
if (run) await orchestrator.executeClaimed(run.runId);
else await new Promise((resolve) => setTimeout(resolve, 250));
}
runs.close();
# Coding Agent 运行时
WebAgent 的 Agent API 与 Worker 是两个独立进程,共享 `WEBAGENT_DATABASE_PATH` 指向的 SQLite 队列。API 只负责鉴权、创建/查询/取消任务和输出 SSE;Worker 负责真实执行,避免长时间模型调用阻塞 HTTP 进程。
运行状态固定为:
```text
queued → preparing_workspace → running_agent → checking_diff
→ validating_build → updating_preview → completed
```
任一执行阶段都只能转入 `failed``cancelled``timed_out`。前端通过带 Bearer Token 的流式请求消费事件,断线后可通过 `Last-Event-ID` 从 SQLite 事件序号继续读取。
## Provider 边界
业务层只依赖 `CodingAgentProvider`,Qwen SDK 类型只存在于 `qwen-code-provider.ts`。Agent 模式由租户点击聊天输入框左下角的模式按钮明确选择并写入 SQLite:
- `模拟 Agent`:使用确定性的 `local` 演示 Provider;
- `真实 Agent`:使用 `qwen-code`,且必须已配置 `OPENAI_API_KEY`
- 未保存过选择时使用 `WEBAGENT_AGENT_PROVIDER`,默认固定为 `local`
密钥是否存在只决定“真实 Agent”是否可选,不会自动改变当前模式。模式切换只影响之后新创建的任务,不会中途替换正在执行的 Agent。
Qwen Code 使用 `OPENAI_API_KEY``OPENAI_BASE_URL``OPENAI_MODEL` 的 OpenAI-compatible 配置。SDK 权限模式固定为 `default`,不会启用 `yolo`
## 安全与版本
- Agent 只能读取工作草稿,写入限制在 `src/**`;shell 只允许三条只读 Git 自检命令。
- 禁止访问 `.env*``.git/**`、依赖、构建产物、package/lockfile、网络和发布工具。
- Provider 完成后,系统仍会独立检查路径、符号链接、文件数、文件大小、diff 大小和 `git diff --check`
- 系统独立执行网站构建;构建通过后只原子替换未保存草稿的测试预览,不创建 Git 提交。后续 Agent 任务继续使用同一草稿,因此多轮修改可以累积。
- 用户在左侧“版本”中填写说明并手动保存时,系统再次构建草稿,通过后才创建一个 Git 提交并将其设为当前保存版本。
- 用户可以放弃未保存草稿;系统会先确认当前保存版本可重新构建,再删除草稿,避免因构建异常丢失修改。
- 历史版本恢复会创建新的恢复提交,不使用强制重置,不改写已有版本历史。
- 存在未保存草稿时禁止发布生产环境,用户必须先保存或放弃草稿。
- 构建或策略检查失败时,原测试预览保持可用;若 Agent 已产生文件改动,会保留为可继续修复的草稿。
这些是应用层防线。生产部署还应让 Worker 使用独立低权限账号或容器,并配置网络隔离、CPU/内存/磁盘配额和日志审计。
## API
- `POST /api/sites/:siteId/agent-runs`:创建任务;
- `GET /api/sites/:siteId/agent-runs/:runId`:读取任务;
- `GET /api/sites/:siteId/agent-runs/:runId/events`:SSE 事件;
- `GET /api/sites/:siteId/agent-runs/:runId/report`:最终报告;
- `POST /api/sites/:siteId/agent-runs/:runId/cancel`:取消任务。
- `POST /api/sites/:siteId/save-draft`:验证并手动保存累积草稿;
- `POST /api/sites/:siteId/discard-draft`:恢复当前保存版本并放弃草稿;
- `POST /api/sites/:siteId/restore-version`:把指定历史版本恢复为一个新版本。
这些路由都受现有租户认证和站点归属校验保护。
......@@ -3,10 +3,10 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
Archive, ArrowLeft, ArrowRight, BookOpen, Bot, Check, CheckCircle2, ChevronDown, Clock3, Code2, Copy, ExternalLink,
Globe2, History, Laptop, LayoutGrid, Link2, LoaderCircle, MessageSquareText, Monitor,
Plus, RefreshCw, Rocket, RotateCcw, Search, Send, Settings2, ShieldCheck, Smartphone, Sparkles, Trash2, Undo2,
Plus, RefreshCw, Rocket, RotateCcw, Search, Send, Settings2, ShieldCheck, Smartphone, Sparkles, Trash2,
WandSparkles, X,
} from "lucide-react";
import type { CreateSiteInput, CreateTenantInput, DomainBinding, SessionInfo, SiteInfo, TenantAdminInfo } from "@webagent/shared";
import type { AgentRunEvent, CreateSiteInput, CreateTenantInput, DomainBinding, SessionInfo, SiteInfo, TenantAdminInfo } from "@webagent/shared";
import { api } from "./api";
type ChatMessage = { role: "user" | "agent"; text: string; meta?: string };
......@@ -16,7 +16,7 @@ export default function App() {
const [authenticated, setAuthenticated] = useState(() => Boolean(localStorage.getItem("webagent-session")));
const sessionQuery = useQuery({ queryKey: ["session"], queryFn: api.session, enabled: authenticated, retry: false });
const sitesQuery = useQuery({ queryKey: ["sites"], queryFn: api.sites, enabled: authenticated && sessionQuery.data?.accountType === "tenant_user" });
const healthQuery = useQuery({ queryKey: ["health"], queryFn: api.health });
const agentSettingsQuery = useQuery({ queryKey: ["agent-settings"], queryFn: api.agentSettings, enabled: authenticated && sessionQuery.data?.accountType === "tenant_user" });
const [currentSiteId, setCurrentSiteId] = useState(() => localStorage.getItem("webagent-current-site") || "");
const [creating, setCreating] = useState(false);
const [managing, setManaging] = useState(false);
......@@ -66,7 +66,7 @@ export default function App() {
return <Workspace
siteId={currentSiteId}
sites={sitesQuery.data}
agentMode={healthQuery.data?.agentMode || "local"}
agentMode={agentSettingsQuery.data?.provider === "qwen-code" ? "model" : "local"}
onSelectSite={setCurrentSiteId}
onCreate={() => setCreating(true)}
onManage={() => setManaging(true)}
......@@ -237,8 +237,10 @@ function Field({ label, hint, wide, children }: { label: string; hint?: string;
}
function RailSettings({ onLogout }: { onLogout: () => void }) {
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
const settingsRef = useRef<HTMLDivElement>(null);
const session = queryClient.getQueryData<SessionInfo>(["session"]);
useEffect(() => {
if (!open) return;
const close = (event: PointerEvent) => {
......@@ -250,12 +252,41 @@ function RailSettings({ onLogout }: { onLogout: () => void }) {
return <div className={`rail-settings ${open ? "open" : ""}`} ref={settingsRef}>
<button className="rail-settings-trigger" type="button" title="设置" data-tooltip="设置" aria-label="设置" aria-expanded={open} onClick={() => setOpen((value) => !value)}><Settings2 size={18} /></button>
{open && <div className="rail-settings-menu">
<div><span className="settings-avatar">U</span><span><strong>user</strong><small>管理员账号</small></span></div>
<div><span className="settings-avatar">{session?.username.slice(0, 1).toUpperCase() || "U"}</span><span><strong>{session?.username || "user"}</strong><small>租户账号</small></span></div>
<button type="button" onClick={onLogout}><ArrowLeft size={14} />退出登录</button>
</div>}
</div>;
}
function AgentModeSwitcher({ disabled }: { disabled: boolean }) {
const queryClient = useQueryClient();
const settingsQuery = useQuery({ queryKey: ["agent-settings"], queryFn: api.agentSettings });
const [open, setOpen] = useState(false);
const switcherRef = useRef<HTMLDivElement>(null);
const mutation = useMutation({
mutationFn: api.updateAgentSettings,
onSuccess: (settings) => { queryClient.setQueryData(["agent-settings"], settings); setOpen(false); },
});
useEffect(() => {
if (!open) return;
const close = (event: PointerEvent) => {
if (event.target instanceof Node && !switcherRef.current?.contains(event.target)) setOpen(false);
};
document.addEventListener("pointerdown", close);
return () => document.removeEventListener("pointerdown", close);
}, [open]);
const real = settingsQuery.data?.provider === "qwen-code";
return <div className={`composer-agent-switcher ${open ? "open" : ""}`} ref={switcherRef}>
<button className="agent-mode-trigger" type="button" disabled={disabled || mutation.isPending} aria-haspopup="menu" aria-expanded={open} onClick={() => setOpen((value) => !value)}>{real ? <span className="online-dot" /> : <Code2 size={12} />}<span>{real ? "模型 Agent" : "模拟 Agent"}</span><ChevronDown size={11} /></button>
{open && <div className="composer-agent-menu" role="menu">
<span>选择 Agent 模式</span>
<button className={!real ? "selected" : ""} type="button" role="menuitem" disabled={mutation.isPending} onClick={() => mutation.mutate("local")}><Code2 size={14} /><span><strong>模拟 Agent</strong><small>本地规则,不调用模型</small></span>{!real && <Check size={12} />}</button>
<button className={real ? "selected" : ""} type="button" role="menuitem" disabled={mutation.isPending || !settingsQuery.data?.realAvailable} onClick={() => mutation.mutate("qwen-code")}><Sparkles size={14} /><span><strong>模型 Agent</strong><small>{settingsQuery.data?.realAvailable ? "Qwen Code + DeepSeek" : "模型密钥未配置"}</small></span>{real && <Check size={12} />}</button>
{mutation.error && <small className="agent-mode-error">{mutation.error.message}</small>}
</div>}
</div>;
}
function CreateSiteWorkspace({ sites, currentSiteId, onCreated, onCancel, onSelectSite, onManage, onLogout }: { sites: SiteInfo[]; currentSiteId: string; onCreated: (site: SiteInfo) => void; onCancel?: () => void; onSelectSite: (id: string) => void; onManage: () => void; onLogout: () => void }) {
const selectedSite = sites.find((site) => site.siteId === currentSiteId) || sites[0];
return <main className="workspace create-workspace">
......@@ -410,11 +441,15 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate, onManage,
const [environment, setEnvironment] = useState<"preview" | "production">("preview");
const [pendingPreviewCommit, setPendingPreviewCommit] = useState("");
const [input, setInput] = useState("");
const [versionMessage, setVersionMessage] = useState("");
const [activeRunId, setActiveRunId] = useState("");
const [agentProgress, setAgentProgress] = useState("");
const [iframeVersion, setIframeVersion] = useState(0);
const [messages, setMessages] = useState<ChatMessage[]>([
{ role: "agent", text: "网站已准备好。告诉我你想调整的内容、配色或页面结构,修改会自动保存为工作草稿,由你确认后再更新预览。", meta: agentMode === "model" ? "模型 Agent 已连接" : "本地演示 Agent" },
{ role: "agent", text: "网站已准备好。多轮修改会累积为未保存草稿,并自动更新测试预览;你可以在“版本”中决定何时保存。", meta: agentMode === "model" ? "模型 Agent 已连接" : "本地演示 Agent" },
]);
const scrollRef = useRef<HTMLDivElement>(null);
const versionMessageKeyRef = useRef("");
const site = siteQuery.data;
const historicalPreview = Boolean(site?.previewCommit && site.previewCommit !== site.currentCommit);
useEffect(() => { scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: "smooth" }); }, [messages]);
......@@ -423,6 +458,24 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate, onManage,
setPendingPreviewCommit("");
setMessages([{ role: "agent", text: "已切换到这个网站。你可以继续描述修改要求。", meta: agentMode === "model" ? "模型 Agent 已连接" : "本地演示 Agent" }]);
}, [siteId, agentMode]);
useEffect(() => {
setVersionMessage("");
versionMessageKeyRef.current = "";
}, [siteId]);
useEffect(() => {
if (!site?.draftBaseCommit) {
if (versionMessageKeyRef.current) setVersionMessage("");
versionMessageKeyRef.current = "";
return;
}
const normalized = (site.draftSummary || "").replace(/```[\s\S]*?```/g, " ").replace(/[`*_#>]/g, "").replace(/\s+/g, " ").trim();
const suggestion = (normalized || "网站内容与视觉优化").slice(0, 120);
const key = `${site.draftBaseCommit}:${site.draftSummary || ""}`;
if (versionMessageKeyRef.current !== key) {
setVersionMessage(suggestion);
versionMessageKeyRef.current = key;
}
}, [site?.draftBaseCommit, site?.draftSummary]);
const refreshData = async () => {
await Promise.all([
......@@ -434,13 +487,18 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate, onManage,
};
const chatMutation = useMutation({
mutationFn: (message: string) => api.chat(siteId, message),
mutationFn: (message: string) => api.runAgent(siteId, message, {
onStarted: (run) => { setActiveRunId(run.runId); setAgentProgress("任务已进入队列"); },
onEvent: (event) => setAgentProgress(agentEventLabel(event)),
}),
onSuccess: async (result) => {
setMessages((items) => [...items, { role: "agent", text: result.summary, meta: (result.mode === "model" ? "AI 生成" : "本地规则") + " · 草稿已自动保存" }]);
setMessages((items) => [...items, { role: "agent", text: result.summary || "修改已完成。", meta: `${result.provider} · 已加入未保存草稿 · 尚未创建版本` }]);
await refreshData();
},
onError: (error) => setMessages((items) => [...items, { role: "agent", text: error.message, meta: "修改未生效,原版本保持可用" }]),
onError: (error) => setMessages((items) => [...items, { role: "agent", text: error.message, meta: "运行未完成;原测试版本保持可用" }]),
onSettled: () => { setActiveRunId(""); setAgentProgress(""); },
});
const cancelAgentMutation = useMutation({ mutationFn: () => api.cancelAgent(siteId, activeRunId) });
const rebuildMutation = useMutation({
mutationFn: () => api.rebuild(siteId),
onSuccess: async () => {
......@@ -466,18 +524,36 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate, onManage,
onSuccess: async (result) => {
setEnvironment("preview");
await refreshData();
setMessages((items) => [...items, { role: "agent", text: "工作草稿已构建并保存为新版本。", meta: "测试版本 · " + result.commit.slice(0, 7) }]);
setMessages((items) => [...items, { role: "agent", text: "未保存草稿已重新构建到测试预览。", meta: "草稿基于 · " + result.baseCommit.slice(0, 7) }]);
},
onError: (error) => setMessages((items) => [...items, { role: "agent", text: error.message, meta: "草稿仍已保留,可以继续编辑" }]),
});
const undoMutation = useMutation({
mutationFn: () => api.undo(siteId),
const saveDraftMutation = useMutation({
mutationFn: () => api.saveDraft(siteId, versionMessage.trim()),
onSuccess: async (result) => {
setVersionMessage(""); setEnvironment("preview");
setMessages((items) => [...items, { role: "agent", text: "累积修改已保存为新版本。", meta: "新版本 · " + result.commit.slice(0, 7) }]);
await refreshData();
},
onError: (error) => setMessages((items) => [...items, { role: "agent", text: error.message, meta: "未保存修改仍保留" }]),
});
const discardDraftMutation = useMutation({
mutationFn: () => api.discardDraft(siteId),
onSuccess: async (result) => {
setVersionMessage(""); setEnvironment("preview");
setMessages((items) => [...items, { role: "agent", text: "未保存修改已放弃。", meta: "恢复到保存版本 · " + result.commit.slice(0, 7) }]);
await refreshData();
},
onError: (error) => setMessages((items) => [...items, { role: "agent", text: error.message, meta: "放弃草稿失败" }]),
});
const restoreVersionMutation = useMutation({
mutationFn: (commit: string) => api.restoreVersion(siteId, commit),
onSuccess: async (result) => {
setEnvironment("preview");
setMessages((items) => [...items, { role: "agent", text: "已撤销最近一次修改。", meta: "测试环境已恢复 · " + result.commit.slice(0, 7) }]);
setMessages((items) => [...items, { role: "agent", text: "已从历史版本创建新的恢复版本。", meta: "恢复提交 · " + result.commit.slice(0, 7) }]);
await refreshData();
},
onError: (error) => setMessages((items) => [...items, { role: "agent", text: error.message, meta: "撤销失败,现有测试和线上版本保持可用" }]),
onError: (error) => setMessages((items) => [...items, { role: "agent", text: error.message, meta: "历史恢复失败" }]),
});
const publishMutation = useMutation({
mutationFn: () => api.publish(siteId),
......@@ -492,20 +568,18 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate, onManage,
const value = message.trim(); if (!value || chatMutation.isPending || (historicalPreview && !site?.draftBaseCommit)) return;
setMessages((items) => [...items, { role: "user", text: value }]); setInput(""); chatMutation.mutate(value);
};
const busy = chatMutation.isPending || rebuildMutation.isPending || previewVersionMutation.isPending || previewDraftMutation.isPending || undoMutation.isPending || publishMutation.isPending;
const busy = chatMutation.isPending || rebuildMutation.isPending || previewVersionMutation.isPending || previewDraftMutation.isPending || saveDraftMutation.isPending || discardDraftMutation.isPending || restoreVersionMutation.isPending || publishMutation.isPending;
if (!site) return <Splash />;
const previewVersion = historyQuery.data?.find((item) => item.hash === site.previewCommit);
const currentVersion = historyQuery.data?.find((item) => item.hash === site.currentCommit);
const productionVersion = historyQuery.data?.find((item) => item.hash === site.publishedCommit);
const productionMatchesPreview = Boolean(site.publishedCommit && site.publishedCommit === site.previewCommit);
const canPublish = site.status === "ready" && Boolean(site.previewCommit);
const canUndo = !historicalPreview && Boolean(site.previousCommit || currentVersion?.message.startsWith("Agent:"));
const productionMatchesPreview = Boolean(!site.draftBaseCommit && site.publishedCommit && site.publishedCommit === site.previewCommit);
const canPublish = site.status === "ready" && Boolean(site.previewCommit) && !site.draftBaseCommit;
const showingProduction = environment === "production";
const activeUrl = showingProduction ? site.productionUrl : site.previewUrl;
const environmentStatus = showingProduction
? site.lastPublishError ? "上线失败 · 线上未变" : !site.publishedCommit ? "尚未上线" : productionMatchesPreview ? "生产已同步" : "生产版本较旧"
: site.lastError ? "构建失败 · 预览未变" : site.status === "ready" ? "测试已同步" : "需要检查";
: site.lastError ? "构建失败 · 预览未变" : site.draftBaseCommit ? "未保存草稿预览" : site.status === "ready" ? "测试已同步" : "需要检查";
return <main className={`workspace ${tab === "domains" ? "domains-mode" : ""}`}>
<aside className="rail">
<Logo compact />
......@@ -523,21 +597,24 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate, onManage,
{message.role === "agent" && <span className="agent-avatar"><Sparkles size={14} /></span>}
<div><p>{message.text}</p>{message.meta && <small><Check size={11} />{message.meta}</small>}</div>
</div>)}
{chatMutation.isPending && <div className="agent-progress"><span className="agent-avatar"><Sparkles size={14} /></span><div><p><LoaderCircle className="spin" size={14} /> Agent 正在修改工作草稿</p><div className="progress-track"><i /></div><small>读取文件 → 生成修改 → 自动保存草稿</small></div></div>}
{chatMutation.isPending && <div className="agent-progress"><span className="agent-avatar"><Sparkles size={14} /></span><div><p><LoaderCircle className="spin" size={14} /> {agentProgress || "正在启动 Agent"}</p><div className="progress-track"><i /></div><small>隔离工作区 → Agent 修改 → Diff 检查 → 独立构建 → 更新未保存预览</small>{activeRunId && <button className="agent-cancel" type="button" disabled={cancelAgentMutation.isPending} onClick={() => cancelAgentMutation.mutate()}>{cancelAgentMutation.isPending ? "正在取消…" : "取消运行"}</button>}</div></div>}
{messages.length <= 1 && <div className="suggestions"><span>试试这些修改</span>{["把网站主色改成深蓝色", "增加三个服务卡片", "增加一个企业优势"].map((text) => <button key={text} onClick={() => send(text)}><Sparkles size={12} />{text}</button>)}</div>}
</div>
{historicalPreview && <div className="history-edit-lock"><div><History size={14} /><span>{site.draftBaseCommit ? "正在查看旧版本;工作草稿已保留,可以继续编辑或预览草稿" : "当前是只读历史预览;切换到“最近保存”版本后才能继续修改"}</span></div></div>}
<div className="composer">
<textarea rows={3} value={input} disabled={busy || (historicalPreview && !site.draftBaseCommit)} placeholder={historicalPreview && !site.draftBaseCommit ? "切换到最近保存版本后才能继续修改…" : site.draftBaseCommit ? "继续描述对工作草稿的修改…" : "描述你想要的修改,例如:让首屏更有科技感…"} onChange={(e) => setInput(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }} />
<div><span>{site.draftBaseCommit ? <><span className="draft-dot" />工作草稿自动保存</> : agentMode === "model" ? <><span className="online-dot" />模型 Agent</> : <><Code2 size={12} />本地演示模式</>}</span><button onClick={() => send()} disabled={!input.trim() || busy || (historicalPreview && !site.draftBaseCommit)}><Send size={15} /></button></div>
<textarea rows={3} value={input} disabled={busy || (historicalPreview && !site.draftBaseCommit)} placeholder={historicalPreview && !site.draftBaseCommit ? "切换到最近保存版本后才能继续修改…" : site.draftBaseCommit ? "继续累积未保存修改…" : "描述你想要的修改,例如:让首屏更有科技感…"} onChange={(e) => setInput(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }} />
<div><div className="composer-status"><AgentModeSwitcher disabled={busy} />{site.draftBaseCommit && <span className="draft-status"><span className="draft-dot" />未保存修改</span>}</div><button onClick={() => send()} disabled={!input.trim() || busy || (historicalPreview && !site.draftBaseCommit)}><Send size={15} /></button></div>
</div>
</> : tab === "history" ? <div className="history-list">
<div className="history-heading"><div><h3>测试版本</h3><p>点击右侧“预览”切换测试版本,生产环境不会改变</p></div><span>{historyQuery.data?.length || 0}</span></div>
<div className="history-heading"><div><h3>版本管理</h3><p>先累积修改,再手动保存为有意义的新版本</p></div><span>{historyQuery.data?.length || 0}</span></div>
{site.draftBaseCommit && <section className="draft-card">
<div className="draft-card-heading"><span><Sparkles size={13} /></span><div><strong>工作草稿</strong><small>自动保存于 {site.draftUpdatedAt ? formatTime(site.draftUpdatedAt) : "刚刚"} · 基于 {site.draftBaseCommit.slice(0, 7)}</small></div></div>
<div className="draft-card-actions"><button type="button" onClick={() => setTab("chat")}><MessageSquareText size={11} />继续编辑</button><button type="button" disabled={busy} onClick={() => previewDraftMutation.mutate()}>{previewDraftMutation.isPending ? <LoaderCircle className="spin" size={11} /> : <Monitor size={11} />}{previewDraftMutation.isPending ? "构建中" : "预览草稿"}</button></div>
<div className="draft-card-heading"><span><Sparkles size={13} /></span><div><strong>未保存修改</strong><small>更新于 {site.draftUpdatedAt ? formatTime(site.draftUpdatedAt) : "刚刚"} · 基于 {site.draftBaseCommit.slice(0, 7)}</small></div></div>
<p className="draft-card-summary">{site.draftSummary || "多轮 Agent 修改正在此处累积,尚未写入 Git 历史。"}</p>
<label className="version-message-field"><span>版本说明</span><input value={versionMessage} maxLength={120} placeholder="例如:首页内容与品牌视觉优化" onChange={(event) => setVersionMessage(event.target.value)} /></label>
<div className="draft-card-actions primary"><button type="button" disabled={busy || versionMessage.trim().length < 2} onClick={() => saveDraftMutation.mutate()}>{saveDraftMutation.isPending ? <LoaderCircle className="spin" size={11} /> : <CheckCircle2 size={11} />}{saveDraftMutation.isPending ? "保存中" : "保存为新版本"}</button><button type="button" disabled={busy} onClick={() => { if (window.confirm("确定放弃全部未保存修改吗?此操作不能撤销。")) discardDraftMutation.mutate(); }}><Trash2 size={11} />放弃修改</button></div>
<div className="draft-card-actions secondary"><button type="button" onClick={() => setTab("chat")}><MessageSquareText size={11} />继续编辑</button><button type="button" disabled={busy} onClick={() => previewDraftMutation.mutate()}>{previewDraftMutation.isPending ? <LoaderCircle className="spin" size={11} /> : <Monitor size={11} />}{previewDraftMutation.isPending ? "构建中" : "重新构建预览"}</button></div>
</section>}
{historicalPreview && <div className="history-edit-lock history-panel-lock"><div><History size={14} /><span>{site.draftBaseCommit ? "正在查看旧版本,工作草稿已保留" : "历史版本只能预览或上线;切换到“最近保存”版本后才能继续修改"}</span></div></div>}
{historicalPreview && <div className="history-edit-lock history-panel-lock"><div><History size={14} /><span>{site.draftBaseCommit ? "正在查看旧版本,未保存修改仍保留" : "当前只是在预览历史版本;可恢复为新的当前版本"}</span></div></div>}
{historyQuery.data?.map((item, index) => {
const isPreview = item.hash === site.previewCommit;
const isProduction = item.hash === site.publishedCommit;
......@@ -547,9 +624,10 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate, onManage,
<div className="history-row" aria-current={isPreview ? "page" : undefined}>
<span className="timeline"><i>{selecting ? <LoaderCircle className="spin" size={10} /> : isPreview ? <Check size={10} /> : ""}</i>{index < (historyQuery.data?.length || 0) - 1 && <span />}</span>
<span className="commit-info"><span><strong>{item.message}</strong>{isPreview && <em className="preview-badge">当前查看</em>}{isProduction && <em className="production-badge">线上版本</em>}{isCurrent && <em className="current-badge">最近保存</em>}</span><small><code>{item.shortHash}</code><span>·</span>{formatDate(item.date)}</small></span>
{isPreview
? <span className="history-row-state">正在查看</span>
: <button className="history-preview-button" type="button" disabled={busy} onClick={() => { setEnvironment("preview"); previewVersionMutation.mutate(item.hash); }}>{selecting ? <LoaderCircle className="spin" size={10} /> : <Monitor size={11} />}{selecting ? "切换中" : "预览"}</button>}
<span className="history-version-actions">
{isPreview ? <span className="history-row-state">正在查看</span> : <button className="history-preview-button" type="button" disabled={busy} onClick={() => { setEnvironment("preview"); previewVersionMutation.mutate(item.hash); }}>{selecting ? <LoaderCircle className="spin" size={10} /> : <Monitor size={11} />}{selecting ? "切换中" : "预览"}</button>}
{!isCurrent && <button className="history-restore-button" type="button" disabled={busy || Boolean(site.draftBaseCommit)} onClick={() => { if (window.confirm(`确定恢复版本 ${item.shortHash} 吗?系统会创建一个新的恢复版本。`)) restoreVersionMutation.mutate(item.hash); }}><RotateCcw size={11} />恢复</button>}
</span>
</div>
</article>;
})}
......@@ -557,7 +635,7 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate, onManage,
</section>
{tab === "domains" ? <DomainManagement site={site} /> : <section className="preview-shell">
<header className="preview-toolbar">
<div className="preview-title"><div><span className={`status-dot ${showingProduction ? "production" : ""}`} /><strong>{site.name}</strong><em className={showingProduction ? "production" : ""}>{showingProduction ? "生产环境" : "测试环境"}</em></div><span className={`build-status ${showingProduction ? site.lastPublishError ? "failed" : site.publishStatus : site.lastError ? "failed" : site.status}`}>{busy ? <LoaderCircle className="spin" size={12} /> : showingProduction ? site.lastPublishError ? <X size={12} /> : <Check size={12} /> : site.lastError ? <X size={12} /> : <Check size={12} />}{publishMutation.isPending ? "正在发布" : previewDraftMutation.isPending ? "正在构建草稿" : chatMutation.isPending ? "正在修改草稿" : rebuildMutation.isPending || previewVersionMutation.isPending || undoMutation.isPending ? "正在构建" : !showingProduction && site.draftBaseCommit ? "草稿已保存" : environmentStatus}</span></div>
<div className="preview-title"><div><span className={`status-dot ${showingProduction ? "production" : ""}`} /><strong>{site.name}</strong><em className={showingProduction ? "production" : ""}>{showingProduction ? "生产环境" : "测试环境"}</em></div><span className={`build-status ${showingProduction ? site.lastPublishError ? "failed" : site.publishStatus : site.lastError ? "failed" : site.status}`}>{busy ? <LoaderCircle className="spin" size={12} /> : showingProduction ? site.lastPublishError ? <X size={12} /> : <Check size={12} /> : site.lastError ? <X size={12} /> : <Check size={12} />}{publishMutation.isPending ? "正在发布" : saveDraftMutation.isPending ? "正在保存版本" : discardDraftMutation.isPending ? "正在放弃草稿" : previewDraftMutation.isPending ? "正在构建草稿" : chatMutation.isPending ? "正在修改草稿" : rebuildMutation.isPending || previewVersionMutation.isPending || restoreVersionMutation.isPending ? "正在构建" : environmentStatus}</span></div>
<div className="preview-modes">
<div className="environment-toggle"><button className={!showingProduction ? "active" : ""} onClick={() => setEnvironment("preview")}><Monitor size={13} />测试预览</button><button className={showingProduction ? "active" : ""} onClick={() => setEnvironment("production")}><Globe2 size={13} />生产站点</button></div>
<div className="device-toggle"><button className={device === "desktop" ? "active" : ""} onClick={() => setDevice("desktop")}><Laptop size={15} /></button><button className={device === "mobile" ? "active" : ""} onClick={() => setDevice("mobile")}><Smartphone size={15} /></button></div>
......@@ -566,7 +644,6 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate, onManage,
{!showingProduction && <button className="rebuild-button" disabled={busy} onClick={() => rebuildMutation.mutate()} title="重新构建测试环境"><RefreshCw className={rebuildMutation.isPending ? "spin" : ""} size={14} /><span>重新构建</span></button>}
<a className="environment-link preview-link" href={site.previewUrl} target="_blank" rel="noreferrer" title="打开测试环境"><Monitor size={13} /><span>测试链接</span></a>
<a className="environment-link production-link" href={site.productionUrl} target="_blank" rel="noreferrer" title="打开生产环境"><Globe2 size={13} /><span>生产链接</span></a>
{!showingProduction && <button className="undo-button" disabled={busy || !canUndo} onClick={() => { if (window.confirm("确定撤销最近一次修改吗?测试环境将恢复到上一份已保存版本,线上版本不会改变。")) undoMutation.mutate(); }} title={historicalPreview ? "请先切换回最近保存版本" : canUndo ? "恢复上一份已保存版本" : "当前没有可撤销的修改"}><Undo2 size={14} /><span>撤销最近修改</span></button>}
{!showingProduction && <button className="publish-button" disabled={busy || !canPublish} onClick={() => { if (window.confirm(productionMatchesPreview ? "确定重新发布当前版本吗?这会重新构建并更新生产产物。" : "确定将当前测试预览版本发布到生产环境吗?")) publishMutation.mutate(); }} title={site.status !== "ready" ? "测试环境构建成功后才能发布" : productionMatchesPreview ? "重新构建并发布当前版本" : "发布当前测试预览版本到生产环境"}>
{publishMutation.isPending ? <LoaderCircle className="spin" size={14} /> : <Rocket size={14} />}<span>{productionMatchesPreview ? "重新上线" : "上线当前版本"}</span>
</button>}
......@@ -576,11 +653,11 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate, onManage,
<div className={`browser-frame ${device}`}>
<div className="browser-chrome"><div><i /><i /><i /></div><span>{activeUrl || site.productionUrl || "/sites/" + site.siteId + "/"}</span><RefreshCw size={11} /></div>
<iframe key={environment + iframeVersion} title={site.name + (showingProduction ? " 生产站点" : " 测试预览")} src={activeUrl + "?v=" + iframeVersion} />
{(publishMutation.isPending || rebuildMutation.isPending || previewVersionMutation.isPending || previewDraftMutation.isPending || undoMutation.isPending) && <div className="preview-busy"><LoaderCircle className="spin" size={25} /><strong>{publishMutation.isPending ? "正在发布生产版本" : previewDraftMutation.isPending ? "正在构建工作草稿" : "正在生成测试版本"}</strong><span>{publishMutation.isPending ? "测试环境和原生产版本保持可用" : "当前测试预览保持可用"}</span></div>}
{(publishMutation.isPending || rebuildMutation.isPending || previewVersionMutation.isPending || previewDraftMutation.isPending || saveDraftMutation.isPending || discardDraftMutation.isPending || restoreVersionMutation.isPending) && <div className="preview-busy"><LoaderCircle className="spin" size={25} /><strong>{publishMutation.isPending ? "正在发布生产版本" : saveDraftMutation.isPending ? "正在保存新版本" : discardDraftMutation.isPending ? "正在放弃未保存修改" : previewDraftMutation.isPending ? "正在构建未保存草稿" : restoreVersionMutation.isPending ? "正在创建恢复版本" : "正在生成测试版本"}</strong><span>{publishMutation.isPending ? "测试环境和原生产版本保持可用" : "当前测试预览保持可用"}</span></div>}
</div>
</div>
<footer className="preview-footer">
<span>{showingProduction ? <Globe2 size={12} /> : <Monitor size={12} />}当前查看 · {showingProduction ? `生产版本 ${productionVersion?.shortHash || site.publishedCommit?.slice(0, 7) || "尚未上线"}` : `${historicalPreview ? "历史版本" : "测试版本"} ${previewVersion?.shortHash || site.currentCommit.slice(0, 7)}`} · {showingProduction ? site.publishedAt ? formatDate(site.publishedAt) : "尚未发布" : previewVersion ? formatDate(previewVersion.date) : formatDate(site.updatedAt)}{historicalPreview && !showingProduction ? " · 只读" : ""}</span>
<span>{showingProduction ? <Globe2 size={12} /> : <Monitor size={12} />}当前查看 · {showingProduction ? `生产版本 ${productionVersion?.shortHash || site.publishedCommit?.slice(0, 7) || "尚未上线"}` : site.draftBaseCommit ? `未保存草稿 · 基于 ${site.draftBaseCommit.slice(0, 7)}` : `${historicalPreview ? "历史版本" : "测试版本"} ${previewVersion?.shortHash || site.currentCommit.slice(0, 7)}`} · {showingProduction ? site.publishedAt ? formatDate(site.publishedAt) : "尚未发布" : site.draftBaseCommit ? site.draftUpdatedAt ? formatDate(site.draftUpdatedAt) : "刚刚" : previewVersion ? formatDate(previewVersion.date) : formatDate(site.updatedAt)}{historicalPreview && !showingProduction ? " · 只读" : ""}</span>
<span><Clock3 size={12} />{site.publishedAt ? `线上版本 ${productionVersion?.shortHash || site.publishedCommit?.slice(0, 7)} · ${formatDate(site.publishedAt)}` : "生产环境尚未发布"}</span>
</footer>
</section>}
......@@ -728,6 +805,18 @@ function formatDate(value: string) {
return new Intl.DateTimeFormat("zh-CN", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }).format(new Date(value));
}
function agentEventLabel(event: AgentRunEvent): string {
const status = typeof event.data?.status === "string" ? event.data.status : "";
const labels: Record<string, string> = {
queued: "任务已进入队列", preparing_workspace: "正在准备隔离工作区", running_agent: "Agent 正在读取并修改网站",
checking_diff: "正在检查文件改动与安全边界", validating_build: "正在独立构建网站",
updating_preview: "正在更新未保存草稿预览", completed: "修改已加入未保存草稿",
failed: "Agent 运行失败", cancelled: "Agent 运行已取消", timed_out: "Agent 运行超时",
};
if (status && labels[status]) return labels[status];
return event.message.length > 80 ? event.message.slice(0, 80) + "…" : event.message;
}
function formatTime(value: string) {
return new Intl.DateTimeFormat("zh-CN", { hour: "2-digit", minute: "2-digit" }).format(new Date(value));
}
import type { ChatResult, CreateSiteInput, CreateTenantInput, DomainBinding, DomainListResult, GitHistoryItem, PreviewVersionResult, PublishResult, SessionInfo, SiteInfo, TenantAdminInfo, TenantAdminSiteInfo } from "@webagent/shared";
import type { AgentRunEvent, AgentRunInfo, AgentSettings, CreateSiteInput, CreateTenantInput, DomainBinding, DomainListResult, DraftPreviewResult, GitHistoryItem, PreviewVersionResult, PublishResult, SessionInfo, SiteInfo, TenantAdminInfo, TenantAdminSiteInfo } from "@webagent/shared";
async function request<T>(url: string, options?: RequestInit): Promise<T> {
const headers = new Headers(options?.headers);
......@@ -23,6 +23,8 @@ export const api = {
logout: () => request<{ success: true }>("/api/logout", { method: "POST" }),
session: () => request<SessionInfo>("/api/session"),
health: () => request<{ status: string; agentMode: "model" | "local" }>("/api/health"),
agentSettings: () => request<AgentSettings>("/api/agent-settings"),
updateAgentSettings: (provider: AgentSettings["provider"]) => request<AgentSettings>("/api/agent-settings", { method: "PUT", body: JSON.stringify({ provider }) }),
sites: () => request<SiteInfo[]>("/api/sites"),
archivedSites: () => request<SiteInfo[]>("/api/archived-sites"),
site: (siteId: string) => request<SiteInfo>("/api/sites/" + siteId),
......@@ -30,13 +32,26 @@ export const api = {
archiveSite: (siteId: string) => request<SiteInfo>("/api/sites/" + siteId + "/archive", { method: "POST" }),
restoreSite: (siteId: string) => request<SiteInfo>("/api/archived-sites/" + siteId + "/restore", { method: "POST" }),
deleteSite: (siteId: string) => request<void>("/api/archived-sites/" + siteId, { method: "DELETE" }),
chat: (siteId: string, message: string) => request<ChatResult>("/api/sites/" + siteId + "/chat", { method: "POST", body: JSON.stringify({ message }) }),
runAgent: async (
siteId: string, message: string,
handlers: { onStarted?: (run: AgentRunInfo) => void; onEvent?: (event: AgentRunEvent) => void } = {},
) => {
const run = await request<AgentRunInfo>(`/api/sites/${siteId}/agent-runs`, { method: "POST", body: JSON.stringify({ message }) });
handlers.onStarted?.(run);
await consumeAgentEvents(siteId, run.runId, handlers.onEvent);
const report = await request<AgentRunInfo>(`/api/sites/${siteId}/agent-runs/${run.runId}/report`);
if (report.status !== "completed") throw new Error(report.error || `Agent 运行结束:${report.status}`);
return report;
},
cancelAgent: (siteId: string, runId: string) => request<{ cancelRequested: true }>(`/api/sites/${siteId}/agent-runs/${runId}/cancel`, { method: "POST" }),
history: (siteId: string) => request<GitHistoryItem[]>("/api/sites/" + siteId + "/history"),
previewVersion: (siteId: string, commit: string) => request<PreviewVersionResult>("/api/sites/" + siteId + "/preview-version", { method: "POST", body: JSON.stringify({ commit }) }),
previewDraft: (siteId: string) => request<PreviewVersionResult>("/api/sites/" + siteId + "/preview-draft", { method: "POST" }),
previewDraft: (siteId: string) => request<DraftPreviewResult>("/api/sites/" + siteId + "/preview-draft", { method: "POST" }),
saveDraft: (siteId: string, message: string) => request<PreviewVersionResult>("/api/sites/" + siteId + "/save-draft", { method: "POST", body: JSON.stringify({ message }) }),
discardDraft: (siteId: string) => request<PreviewVersionResult>("/api/sites/" + siteId + "/discard-draft", { method: "POST" }),
restoreVersion: (siteId: string, commit: string) => request<PreviewVersionResult>("/api/sites/" + siteId + "/restore-version", { method: "POST", body: JSON.stringify({ commit }) }),
rebuild: (siteId: string) => request<{ previewUrl: string }>("/api/sites/" + siteId + "/build", { method: "POST" }),
publish: (siteId: string) => request<PublishResult>("/api/sites/" + siteId + "/publish", { method: "POST" }),
undo: (siteId: string) => request<PreviewVersionResult>("/api/sites/" + siteId + "/undo", { method: "POST" }),
domains: (siteId: string) => request<DomainListResult>("/api/sites/" + siteId + "/domains"),
addDomain: (siteId: string, hostname: string) => request<DomainBinding>("/api/sites/" + siteId + "/domains", { method: "POST", body: JSON.stringify({ hostname }) }),
verifyDomain: (siteId: string, domainId: string) => request<DomainBinding>("/api/sites/" + siteId + "/domains/" + domainId + "/verify", { method: "POST" }),
......@@ -50,3 +65,31 @@ export const api = {
resetTenantPassword: (tenantId: string, userId: string, password: string) => request<{ success: true }>(`/api/admin/tenants/${tenantId}/accounts/${userId}/reset-password`, { method: "POST", body: JSON.stringify({ password }) }),
revokeTenantSessions: (tenantId: string, userId: string) => request<{ success: true }>(`/api/admin/tenants/${tenantId}/accounts/${userId}/revoke-sessions`, { method: "POST" }),
};
async function consumeAgentEvents(siteId: string, runId: string, onEvent?: (event: AgentRunEvent) => void): Promise<void> {
const token = localStorage.getItem("webagent-session");
const response = await fetch(`/api/sites/${siteId}/agent-runs/${runId}/events`, {
headers: token ? { authorization: "Bearer " + token } : undefined,
});
if (!response.ok || !response.body) {
const data = await response.json().catch(() => ({})) as { error?: string };
throw new Error(data.error || "无法连接 Agent 事件流");
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
buffer += decoder.decode(value, { stream: !done }).replaceAll("\r\n", "\n");
let boundary = buffer.indexOf("\n\n");
while (boundary >= 0) {
const block = buffer.slice(0, boundary);
buffer = buffer.slice(boundary + 2);
const eventName = block.split("\n").find((line) => line.startsWith("event:"))?.slice(6).trim();
const payload = block.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
if (payload && eventName !== "end") onEvent?.(JSON.parse(payload) as AgentRunEvent);
boundary = buffer.indexOf("\n\n");
}
if (done) break;
}
}
......@@ -16,14 +16,16 @@
.login-layout{grid-template-columns:1.05fr .95fr}.login-card-wrap{align-items:center}.login-card{width:min(100%,480px);padding:42px 42px 28px}.login-card-icon{display:grid;place-items:center;width:45px;height:45px;margin-bottom:22px;color:var(--primary);background:var(--soft);border:1px solid #ded6ff;border-radius:14px}.login-card .card-heading{margin-bottom:10px}.login-copy{margin:0 0 26px;color:var(--muted);font-size:12px;line-height:1.7}.login-fields{display:flex;flex-direction:column;gap:18px}.login-submit{margin-top:26px}.create-workspace{grid-template-columns:64px 320px 1fr}.rail-bottom{margin-top:auto;display:flex;flex-direction:column;gap:8px}.rail .rail-bottom .rail-create{margin-top:0!important}.rail .rail-logout{color:#9a9da8}.rail .rail-logout:hover{color:#b42318;background:#fff0ee}.create-workspace>.rail>.rail-logout{margin-top:auto}.create-control-panel{background:linear-gradient(180deg,#fff,#faf9ff)}.empty-site-heading{height:86px;display:flex;align-items:flex-start;justify-content:center;flex-direction:column;gap:5px;padding:16px 20px;border-bottom:1px solid var(--divider)}.empty-site-heading .logo{font-size:15px}.empty-site-heading .logo-mark{width:31px;height:31px}.empty-site-heading small{padding-left:42px;color:var(--muted);font-size:8px}.create-side-copy{padding:42px 28px}.create-side-copy>span{display:grid;place-items:center;width:38px;height:38px;color:var(--primary);background:var(--soft);border-radius:12px}.create-side-copy h2{margin:18px 0 9px;font-size:20px}.create-side-copy p{margin:0;color:var(--muted);font-size:11px;line-height:1.8}.create-side-copy ul{margin:25px 0 0;padding:0;list-style:none;color:var(--text-2);font-size:10px}.create-side-copy li{display:flex;align-items:center;gap:8px;margin-top:12px}.create-side-copy li::before{content:"";width:6px;height:6px;background:var(--primary);border-radius:50%;box-shadow:0 0 0 4px rgba(112,40,255,.08)}.workspace-create-content{min-width:0;min-height:0;overflow:auto;display:flex;align-items:flex-start;justify-content:center;padding:42px;background:linear-gradient(145deg,#f4f1ff,#f8f8fc)}.workspace-create-card{width:min(780px,100%);border:1px solid #fff;border-radius:20px}.workspace-create-card .card-heading p{margin:7px 0 0;color:var(--muted);font-size:10px}.workspace-create-cancel{height:31px;display:flex;align-items:center;gap:5px;padding:0 10px;color:var(--muted);background:#fff;border:1px solid var(--border);border-radius:8px;font-size:9px;font-weight:700}.workspace-create-cancel:hover{color:#b42318;background:#fff5f3}
.workspace{height:100vh;height:100dvh;max-height:100dvh;min-height:0;display:grid;grid-template-columns:64px 372px 1fr;overflow:hidden;background:#eff0f5}.rail{min-height:0;display:flex;flex-direction:column;align-items:center;padding:15px 0;background:#fff;border-right:1px solid var(--divider);z-index:3}.rail .logo-mark{width:35px;height:35px}.rail-nav{display:flex;flex-direction:column;gap:9px;margin-top:16px}.rail button{display:grid;place-items:center;width:38px;height:38px;color:var(--muted);background:transparent;border:0;border-radius:11px}.rail button:hover,.rail button.active{color:var(--primary);background:var(--soft)}.rail .rail-create{color:white!important;background:var(--gradient)!important;box-shadow:0 8px 20px rgba(112,40,255,.2)}.rail-create-top{flex:0 0 auto;margin-top:13px!important}.rail-create-top.active{box-shadow:0 0 0 3px rgba(112,40,255,.13),0 8px 20px rgba(112,40,255,.2)!important}.rail-settings{position:relative;margin-top:auto}.rail-settings.open .rail-settings-trigger{color:var(--primary);background:var(--soft)}.rail-settings-menu{position:absolute;left:47px;bottom:0;width:178px;padding:8px;background:rgba(255,255,255,.98);border:1px solid #dfd9ef;border-radius:13px;box-shadow:0 18px 48px rgba(36,16,95,.18);backdrop-filter:blur(16px)}.rail-settings-menu>div{display:flex;align-items:center;gap:9px;padding:7px 7px 10px;border-bottom:1px solid var(--divider)}.settings-avatar{display:grid;place-items:center;width:30px;height:30px;flex:0 0 auto;color:white;background:var(--gradient);border-radius:9px;font-size:11px;font-weight:800}.rail-settings-menu>div>span:last-child{min-width:0;display:flex;flex-direction:column;gap:3px}.rail-settings-menu strong{font-size:10px}.rail-settings-menu small{color:var(--muted);font-size:8px}.rail .rail-settings-menu>button{width:100%;height:33px;margin-top:5px;display:flex;align-items:center;justify-content:flex-start;gap:7px;padding:0 8px;color:#b42318;border-radius:8px;font-size:9px;font-weight:700}.rail .rail-settings-menu>button:hover{background:#fff0ee}
.control-panel{height:100%;min-height:0;min-width:0;overflow:hidden;display:flex;flex-direction:column;background:#fff;border-right:1px solid var(--border);z-index:2}.site-switcher{position:relative;z-index:20;flex:0 0 66px;height:66px;padding:8px 12px;border-bottom:1px solid var(--divider)}.site-switcher-trigger{width:100%;height:49px;display:flex;align-items:center;gap:10px;padding:5px 7px;color:var(--text);text-align:left;background:#fff;border:1px solid transparent;border-radius:12px;transition:.18s ease}.site-switcher-trigger:hover,.site-switcher.open .site-switcher-trigger{background:#faf9ff;border-color:#ded6ff;box-shadow:0 5px 18px rgba(36,16,95,.06)}.site-avatar,.site-menu-avatar{display:grid;place-items:center;flex:0 0 auto;color:white;background:var(--gradient);font-weight:800}.site-avatar{width:35px;height:35px;border-radius:10px;font-size:12px;box-shadow:0 7px 16px rgba(112,40,255,.18)}.site-switcher-copy,.site-menu-copy{min-width:0;display:flex;flex-direction:column}.site-switcher-copy{flex:1;gap:3px}.site-switcher-copy strong{overflow:hidden;font-size:12px;white-space:nowrap;text-overflow:ellipsis}.site-switcher-copy small{overflow:hidden;color:var(--muted);font-size:8px;white-space:nowrap;text-overflow:ellipsis}.site-switcher-chevron{display:grid;place-items:center;width:26px;height:26px;color:var(--muted);background:#f4f4f7;border-radius:8px;transition:.2s ease}.site-switcher.open .site-switcher-chevron{color:var(--primary);background:var(--soft);transform:rotate(180deg)}.site-switcher-menu{position:absolute;top:61px;left:12px;right:12px;padding:8px;background:rgba(255,255,255,.98);border:1px solid #dfd9ef;border-radius:14px;box-shadow:0 20px 52px rgba(36,16,95,.18),0 4px 12px rgba(36,16,95,.07);backdrop-filter:blur(18px);animation:site-menu-in .16s ease-out}@keyframes site-menu-in{from{opacity:0;transform:translateY(-5px) scale(.985)}to{opacity:1;transform:none}}.site-menu-heading{height:28px;display:flex;align-items:center;justify-content:space-between;padding:0 7px;color:var(--muted);font-size:8px;font-weight:750;letter-spacing:.04em}.site-menu-heading em{display:grid;place-items:center;width:19px;height:19px;color:var(--primary);background:var(--soft);border-radius:6px;font-size:8px;font-style:normal}.site-menu-list{display:flex;flex-direction:column;gap:3px}.site-menu-list>button{width:100%;min-width:0;height:52px;display:flex;align-items:center;gap:9px;padding:6px 8px;color:var(--text);text-align:left;background:transparent;border:0;border-radius:10px}.site-menu-list>button:hover{background:#f7f5ff}.site-menu-list>button.selected{background:#f4f0ff}.site-menu-avatar{width:32px;height:32px;border-radius:9px;font-size:11px}.site-menu-copy{flex:1;gap:4px}.site-menu-copy strong{overflow:hidden;font-size:10px;white-space:nowrap;text-overflow:ellipsis}.site-menu-copy small{overflow:hidden;color:var(--muted);font-size:8px;white-space:nowrap;text-overflow:ellipsis}.site-menu-status{width:7px;height:7px;border-radius:50%;background:#d0d1d8;box-shadow:0 0 0 3px rgba(208,209,216,.15)}.site-menu-status.ready{background:#7957d5;box-shadow:0 0 0 3px rgba(121,87,213,.1)}.site-menu-status.published{background:#20b486;box-shadow:0 0 0 3px rgba(32,180,134,.1)}.site-menu-status.failed{background:#d92d20;box-shadow:0 0 0 3px rgba(217,45,32,.1)}.site-menu-list>button>svg{flex:0 0 auto;color:var(--primary)}.site-switcher-menu>p{height:27px;display:flex;align-items:center;gap:5px;margin:6px 3px 0;padding:6px 5px 0;color:var(--muted);border-top:1px solid var(--divider);font-size:7px}.panel-tabs{flex:0 0 49px;height:49px;padding:0 14px;display:grid;grid-template-columns:1fr 1fr;gap:5px;align-items:center;border-bottom:1px solid var(--divider)}.panel-tabs button{height:35px;display:flex;align-items:center;justify-content:center;gap:7px;color:var(--muted);background:transparent;border:0;border-radius:9px;font-size:11px;font-weight:700}.panel-tabs button.active{color:var(--primary);background:var(--soft)}
.chat-scroll{flex:1 1 0;min-height:0;overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain;padding:20px 17px 28px}.chat-date{text-align:center;color:#aaaeba;font-size:9px;margin-bottom:22px}.message{display:flex;gap:9px;margin-bottom:17px}.message.user{justify-content:flex-end}.message>div{max-width:86%}.message p{margin:0;padding:12px 14px;color:var(--text-2);background:#f6f6f9;border-radius:4px 13px 13px 13px;font-size:12px;line-height:1.65}.message.user p{color:white;background:var(--gradient);border-radius:13px 4px 13px 13px;box-shadow:0 8px 18px rgba(112,40,255,.15)}.message small{display:flex;align-items:center;gap:4px;margin:6px 2px 0;color:#9a9da8;font-size:9px}.agent-avatar{flex:0 0 auto;display:grid;place-items:center;width:28px;height:28px;color:var(--primary);background:var(--soft);border:1px solid #e2dbff;border-radius:9px}.agent-progress{display:flex;gap:9px;margin:8px 0 18px}.agent-progress>div{flex:1;padding:12px 13px;background:#faf9ff;border:1px solid #ebe6ff;border-radius:4px 13px 13px 13px}.agent-progress p{display:flex;align-items:center;gap:7px;margin:0 0 10px;color:var(--text-2);font-size:11px;font-weight:700}.agent-progress small{color:var(--muted);font-size:9px}.progress-track{height:3px;margin-bottom:8px;overflow:hidden;background:#ebe7f5;border-radius:9px}.progress-track i{display:block;width:45%;height:100%;background:var(--gradient);border-radius:9px;animation:progress 1.5s ease-in-out infinite}@keyframes progress{50%{transform:translateX(120%)}}.suggestions{display:flex;flex-direction:column;gap:7px;margin:25px 37px 0}.suggestions>span{margin-bottom:3px;color:var(--muted);font-size:9px}.suggestions button{display:flex;align-items:center;gap:7px;padding:9px 11px;text-align:left;color:var(--text-2);background:#fff;border:1px solid var(--border);border-radius:9px;font-size:10px}.suggestions button:hover{color:var(--primary);border-color:#cdbdff;background:#faf9ff}
.chat-scroll{flex:1 1 0;min-height:0;overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain;padding:20px 17px 28px}.chat-date{text-align:center;color:#aaaeba;font-size:9px;margin-bottom:22px}.message{display:flex;gap:9px;margin-bottom:17px}.message.user{justify-content:flex-end}.message>div{max-width:86%}.message p{margin:0;padding:12px 14px;color:var(--text-2);background:#f6f6f9;border-radius:4px 13px 13px 13px;font-size:12px;line-height:1.65}.message.user p{color:white;background:var(--gradient);border-radius:13px 4px 13px 13px;box-shadow:0 8px 18px rgba(112,40,255,.15)}.message small{display:flex;align-items:center;gap:4px;margin:6px 2px 0;color:#9a9da8;font-size:9px}.agent-avatar{flex:0 0 auto;display:grid;place-items:center;width:28px;height:28px;color:var(--primary);background:var(--soft);border:1px solid #e2dbff;border-radius:9px}.agent-progress{display:flex;gap:9px;margin:8px 0 18px}.agent-progress>div{flex:1;padding:12px 13px;background:#faf9ff;border:1px solid #ebe6ff;border-radius:4px 13px 13px 13px}.agent-progress p{display:flex;align-items:center;gap:7px;margin:0 0 10px;color:var(--text-2);font-size:11px;font-weight:700}.agent-progress small{color:var(--muted);font-size:9px}.agent-cancel{display:block;margin-top:9px;padding:4px 7px;color:#87516a;background:#fff;border:1px solid #e7ceda;border-radius:6px;font-size:8px}.agent-cancel:disabled{opacity:.55}.progress-track{height:3px;margin-bottom:8px;overflow:hidden;background:#ebe7f5;border-radius:9px}.progress-track i{display:block;width:45%;height:100%;background:var(--gradient);border-radius:9px;animation:progress 1.5s ease-in-out infinite}@keyframes progress{50%{transform:translateX(120%)}}.suggestions{display:flex;flex-direction:column;gap:7px;margin:25px 37px 0}.suggestions>span{margin-bottom:3px;color:var(--muted);font-size:9px}.suggestions button{display:flex;align-items:center;gap:7px;padding:9px 11px;text-align:left;color:var(--text-2);background:#fff;border:1px solid var(--border);border-radius:9px;font-size:10px}.suggestions button:hover{color:var(--primary);border-color:#cdbdff;background:#faf9ff}
.composer{flex:0 0 auto;margin:0 14px 14px;padding:10px;background:#fff;border:1px solid #dcd8e8;border-radius:13px;box-shadow:0 5px 22px rgba(36,16,95,.07)}.composer textarea{width:100%;padding:2px 4px;resize:none;color:var(--text);background:transparent;border:0;outline:none;font-size:11px;line-height:1.55}.composer textarea::placeholder{color:#a5a7b0}.composer>div{display:flex;align-items:center;justify-content:space-between}.composer>div>span{display:flex;align-items:center;gap:5px;color:var(--muted);font-size:9px}.online-dot,.draft-dot{width:6px;height:6px;border-radius:50%}.online-dot{background:#20b486}.draft-dot{background:#d79614;box-shadow:0 0 0 3px rgba(215,150,20,.1)}.composer button{display:grid;place-items:center;width:29px;height:29px;color:white;background:var(--gradient);border:0;border-radius:8px}
.composer-status{display:flex;align-items:center;gap:9px}.draft-status{display:flex;align-items:center;gap:5px;color:var(--muted);font-size:8px}.composer-agent-switcher{position:relative}.composer .agent-mode-trigger{width:auto;height:27px;display:flex;align-items:center;gap:5px;padding:0 7px;color:var(--text-2);background:#f5f3fa;border:1px solid #e8e3f2;border-radius:7px;font-size:9px;font-weight:700}.composer .agent-mode-trigger:hover,.composer-agent-switcher.open .agent-mode-trigger{color:var(--primary);background:var(--soft);border-color:#d8cbff}.agent-mode-trigger>svg:last-child{transition:.16s}.composer-agent-switcher.open .agent-mode-trigger>svg:last-child{transform:rotate(180deg)}.composer-agent-menu{position:absolute;z-index:40;left:0;bottom:34px;width:236px;padding:7px;background:rgba(255,255,255,.99);border:1px solid #ddd6ec;border-radius:12px;box-shadow:0 17px 45px rgba(36,16,95,.2);backdrop-filter:blur(14px)}.composer-agent-menu>span{display:block;padding:3px 6px 7px;color:var(--muted);font-size:8px;font-weight:750}.composer .composer-agent-menu>button{width:100%;height:49px;display:grid;grid-template-columns:20px minmax(0,1fr) 14px;justify-items:start;gap:6px;padding:6px 8px;color:var(--text-2);text-align:left;background:#fff;border:1px solid transparent;border-radius:9px}.composer .composer-agent-menu>button:hover{color:var(--text);background:#faf8ff}.composer .composer-agent-menu>button.selected{color:var(--primary);background:var(--soft);border-color:#ddd2ff}.composer-agent-menu button>span{display:flex;min-width:0;flex-direction:column;gap:3px}.composer-agent-menu strong{font-size:9px}.composer-agent-menu small{color:var(--muted);font-size:8px}.composer-agent-menu button>svg:last-child{align-self:center;color:var(--primary)}.composer-agent-menu>.agent-mode-error{display:block;padding:6px;color:#b42318;font-size:8px}
.history-list{flex:1 1 0;min-height:0;overflow:auto;padding:16px 16px 22px}.environment-versions{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:22px}.environment-versions>button{min-width:0;padding:12px;text-align:left;background:#fafafd;border:1px solid var(--border);border-radius:12px}.environment-versions>button:hover,.environment-versions>button.active{border-color:#bca8ff;background:#faf8ff;box-shadow:0 5px 18px rgba(112,40,255,.08)}.environment-versions>button.production:hover,.environment-versions>button.production.active{border-color:#9bd8c6;background:#f4fcf9;box-shadow:0 5px 18px rgba(32,180,134,.08)}.environment-versions span{display:flex;align-items:center;gap:5px;color:var(--text-2);font-size:9px;font-weight:750}.environment-versions span>em{margin-left:auto;padding:2px 4px;color:var(--primary);background:var(--soft);border-radius:4px;font-size:7px;font-style:normal}.environment-versions .production span>em{color:#13765b;background:#e7f8f2}.environment-versions strong,.environment-versions small{display:block}.environment-versions strong{margin-top:13px;font-family:ui-monospace,SFMono-Regular,monospace;font-size:13px;letter-spacing:.02em}.environment-versions small{margin-top:5px;overflow:hidden;color:var(--muted);font-size:8px;white-space:nowrap;text-overflow:ellipsis}.history-heading{display:flex;justify-content:space-between;align-items:center;margin-bottom:18px}.history-heading h3{margin:0 0 4px;font-size:14px}.history-heading p{margin:0;color:var(--muted);font-size:9px}.history-heading>span{display:grid;place-items:center;width:25px;height:25px;color:var(--primary);background:var(--soft);border-radius:8px;font-size:10px;font-weight:800}.history-list article{display:grid;grid-template-columns:24px minmax(0,1fr) auto;gap:7px;min-height:69px}.history-list article.environment-version{margin:0 -5px;padding:0 5px;background:#fbfaff;border-radius:10px}.timeline{display:flex;align-items:center;flex-direction:column}.timeline i{display:grid;place-items:center;width:17px;height:17px;color:white;background:#d6d7df;border:3px solid white;box-shadow:0 0 0 1px #d6d7df;border-radius:50%;font-style:normal}.history-list article.environment-version .timeline i{background:var(--primary);box-shadow:0 0 0 1px var(--primary)}.timeline span{width:1px;flex:1;background:var(--divider)}.commit-info{min-width:0;padding-bottom:18px}.commit-info>div{display:flex;align-items:center;gap:5px;min-width:0}.commit-info strong{min-width:0;overflow:hidden;font-size:10px;line-height:1.4;white-space:nowrap;text-overflow:ellipsis}.commit-info em{flex:0 0 auto;padding:2px 5px;border-radius:5px;font-size:7px;font-style:normal}.commit-info .preview-badge{color:var(--primary);background:var(--soft)}.commit-info .production-badge{color:#13765b;background:#e7f8f2}.commit-info p{display:flex;align-items:center;gap:5px;margin:5px 0 0;color:var(--muted);font-size:9px}.commit-info code{font-size:9px}.history-actions{display:flex;align-items:flex-start;gap:4px}.history-actions button{display:grid;place-items:center;width:25px;height:25px;padding:0;color:var(--muted);background:white;border:1px solid var(--border);border-radius:7px}.history-actions button:hover{color:var(--primary);background:var(--soft)}
.preview-shell{min-height:0;min-width:0;overflow:hidden;display:flex;flex-direction:column;container-type:inline-size}.preview-toolbar{min-height:66px;display:grid;grid-template-columns:minmax(190px,1fr) auto max-content;grid-template-areas:"title modes actions";align-items:center;gap:12px;padding:0 18px;background:#fff;border-bottom:1px solid var(--border)}.preview-title{grid-area:title;display:flex;align-items:center;gap:10px;min-width:0}.preview-title>div{display:flex;align-items:center;gap:8px;min-width:0}.preview-title strong{overflow:hidden;font-size:12px;white-space:nowrap;text-overflow:ellipsis}.preview-title em{flex:0 0 auto;padding:3px 6px;color:#7957d5;background:#f2edff;border-radius:5px;font-size:8px;font-style:normal;font-weight:750}.preview-title em.production{color:#13765b;background:#e7f8f2}.status-dot{flex:0 0 auto;width:7px;height:7px;border-radius:50%;background:#7957d5;box-shadow:0 0 0 4px rgba(112,40,255,.1)}.status-dot.production{background:#20b486;box-shadow:0 0 0 4px rgba(32,180,134,.1)}.build-status{flex:0 0 auto;display:flex;align-items:center;gap:4px;padding:5px 7px;color:#188866;background:#ebfbf5;border-radius:7px;font-size:8px;font-weight:750}.build-status.failed{color:#b42318;background:#fff0ee}.build-status.unpublished{color:#986600;background:#fff8e5}.preview-modes{grid-area:modes;display:flex;align-items:center;gap:7px}.environment-toggle,.device-toggle{display:flex;padding:3px;background:#f2f2f6;border-radius:8px}.environment-toggle button{height:27px;display:flex;align-items:center;gap:5px;padding:0 9px;color:var(--muted);background:transparent;border:0;border-radius:6px;font-size:8px;font-weight:750}.environment-toggle button.active,.device-toggle button.active{color:var(--text);background:#fff;box-shadow:0 2px 7px rgba(36,16,95,.1)}.device-toggle button{display:grid;place-items:center;width:31px;height:27px;color:var(--muted);background:transparent;border:0;border-radius:6px}.toolbar-actions{grid-area:actions;display:flex;justify-content:flex-end;gap:6px;min-width:0}.toolbar-actions button,.toolbar-actions a{height:31px;display:flex;align-items:center;justify-content:center;gap:5px;padding:0 9px;color:var(--text-2);background:#fff;border:1px solid var(--border);border-radius:8px;font-size:8px;font-weight:750;white-space:nowrap}.toolbar-actions button:hover:not(:disabled),.toolbar-actions a:hover{color:var(--primary);background:var(--soft)}.toolbar-actions .environment-link.production-link{color:#13765b}.toolbar-actions .environment-link:disabled{color:#a8a9b0;background:#f5f5f7}.toolbar-actions .rebuild-button{color:var(--primary);border-color:#d8ccff;background:#faf8ff}.toolbar-actions .undo-button{color:#9a6100;border-color:#ecd8ad;background:#fffaf0}.toolbar-actions .publish-button{color:white;background:var(--gradient);border:0}.toolbar-actions .publish-button:hover:not(:disabled){color:white;background:var(--gradient);box-shadow:0 7px 17px rgba(112,40,255,.2)}.toolbar-actions .publish-button:disabled{color:#9a9da8;background:#efeff4;border:1px solid #e1e1e7;box-shadow:none}
.history-edit-lock{flex:0 0 auto;margin:0 14px 10px;padding:9px 11px;color:#6f4b00;background:#fff8e7;border:1px solid #f0dfb8;border-radius:10px;font-size:9px;line-height:1.5}.history-edit-lock>div{display:flex;align-items:flex-start;gap:7px}.history-edit-lock svg{flex:0 0 auto;margin-top:1px}.history-panel-lock{margin:0 0 14px}.history-list article{display:block;min-height:0;margin-bottom:5px}.history-list article.selected{background:#faf8ff;border-radius:11px}.history-row{width:100%;min-width:0;display:grid;grid-template-columns:24px minmax(0,1fr) auto;gap:8px;padding:9px 8px 5px;color:var(--text);text-align:left;background:transparent;border:1px solid transparent;border-radius:11px}.history-list article.selected .history-row{border-color:#ded4ff}.history-row .timeline{min-height:49px}.history-list article.selected .timeline i{background:var(--primary);box-shadow:0 0 0 1px var(--primary)}.history-row .commit-info{display:block;min-width:0;padding:0 0 10px}.commit-info>span{display:flex;align-items:center;gap:5px;min-width:0}.commit-info>span>strong{min-width:0;overflow:hidden;font-size:10px;line-height:1.4;white-space:nowrap;text-overflow:ellipsis}.commit-info em{flex:0 0 auto;padding:2px 5px;border-radius:5px;font-size:7px;font-style:normal}.commit-info .current-badge{color:#775400;background:#fff2c7}.commit-info>small{display:flex;align-items:center;gap:5px;margin-top:6px;color:var(--muted);font-size:8px}.commit-info>small code{font-size:8px}.history-row-state,.history-preview-button{align-self:start;min-height:24px;padding:4px 7px;border-radius:6px;font-size:7px;font-weight:750}.history-row-state{color:var(--primary);background:var(--soft)}.history-preview-button{display:flex;align-items:center;gap:4px;color:var(--text-2);background:#fff;border:1px solid var(--border)}.history-preview-button:hover:not(:disabled){color:var(--primary);background:var(--soft);border-color:#d8ccff}
.history-edit-lock{flex:0 0 auto;margin:0 14px 10px;padding:9px 11px;color:#6f4b00;background:#fff8e7;border:1px solid #f0dfb8;border-radius:10px;font-size:9px;line-height:1.5}.history-edit-lock>div{display:flex;align-items:flex-start;gap:7px}.history-edit-lock svg{flex:0 0 auto;margin-top:1px}.history-panel-lock{margin:0 0 14px}.draft-card{margin:0 0 14px;padding:12px;background:#fffaf0;border:1px solid #ecd8ad;border-radius:12px}.draft-card-heading{display:flex;align-items:center;gap:9px}.draft-card-heading>span{display:grid;place-items:center;width:28px;height:28px;flex:0 0 auto;color:#9a6100;background:#fff1cf;border-radius:8px}.draft-card-heading>div{min-width:0}.draft-card-heading strong,.draft-card-heading small{display:block}.draft-card-heading strong{font-size:10px}.draft-card-heading small{margin-top:4px;overflow:hidden;color:#8a6a30;font-size:8px;white-space:nowrap;text-overflow:ellipsis}.draft-card-actions{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-top:11px}.draft-card-actions button{height:29px;display:flex;align-items:center;justify-content:center;gap:5px;color:#765421;background:#fff;border:1px solid #ecd8ad;border-radius:7px;font-size:8px;font-weight:750}.draft-card-actions button:last-child{color:white;background:#b87808;border-color:#b87808}.draft-card-actions button:hover:not(:disabled){box-shadow:0 4px 12px rgba(154,97,0,.12)}.history-list article{display:block;min-height:0;margin-bottom:5px}.history-list article.selected{background:#faf8ff;border-radius:11px}.history-row{width:100%;min-width:0;display:grid;grid-template-columns:24px minmax(0,1fr) auto;gap:8px;padding:9px 8px 5px;color:var(--text);text-align:left;background:transparent;border:1px solid transparent;border-radius:11px}.history-list article.selected .history-row{border-color:#ded4ff}.history-row .timeline{min-height:49px}.history-list article.selected .timeline i{background:var(--primary);box-shadow:0 0 0 1px var(--primary)}.history-row .commit-info{display:block;min-width:0;padding:0 0 10px}.commit-info>span{display:flex;align-items:center;gap:5px;min-width:0}.commit-info>span>strong{min-width:0;overflow:hidden;font-size:10px;line-height:1.4;white-space:nowrap;text-overflow:ellipsis}.commit-info em{flex:0 0 auto;padding:2px 5px;border-radius:5px;font-size:7px;font-style:normal}.commit-info .current-badge{color:#775400;background:#fff2c7}.commit-info>small{display:flex;align-items:center;gap:5px;margin-top:6px;color:var(--muted);font-size:8px}.commit-info>small code{font-size:8px}.history-row-state,.history-preview-button{align-self:start;min-height:24px;padding:4px 7px;border-radius:6px;font-size:7px;font-weight:750}.history-row-state{color:var(--primary);background:var(--soft)}.history-preview-button{display:flex;align-items:center;gap:4px;color:var(--text-2);background:#fff;border:1px solid var(--border)}.history-preview-button:hover:not(:disabled){color:var(--primary);background:var(--soft);border-color:#d8ccff}
.browser-stage{position:relative;flex:1;min-height:0;display:flex;align-items:center;justify-content:center;padding:0;background:#eff0f5;overflow:auto}.browser-frame{position:relative;width:100%;height:100%;overflow:hidden;background:#fff;border:0;border-radius:0;box-shadow:none;transition:.35s ease}.browser-frame.mobile{width:390px;height:calc(100% - 36px);max-height:760px;border:1px solid #dfe0e7;border-radius:14px;box-shadow:0 15px 45px rgba(30,24,53,.1)}.browser-chrome{height:31px;display:grid;grid-template-columns:70px 1fr 70px;align-items:center;padding:0 10px;background:#fafafd;border-bottom:1px solid #ebeaf0}.browser-chrome>div{display:flex;gap:5px}.browser-chrome i{width:7px;height:7px;border-radius:50%;background:#e1e1e7}.browser-chrome i:first-child{background:#ff8b86}.browser-chrome i:nth-child(2){background:#ffd36b}.browser-chrome i:nth-child(3){background:#71d19a}.browser-chrome span{justify-self:center;width:min(390px,80%);padding:4px 10px;text-align:center;color:#9a9ca6;background:#f0f0f4;border-radius:5px;font-size:8px}.browser-chrome>svg{justify-self:end;color:#a7a9b3}.browser-frame iframe{display:block;width:100%;height:calc(100% - 31px);border:0;background:#fff}.production-empty{height:calc(100% - 31px);display:flex;align-items:center;justify-content:center;flex-direction:column;padding:30px;text-align:center;background:linear-gradient(180deg,#fbfffd,#f7faf9)}.production-empty>span{display:grid;place-items:center;width:52px;height:52px;color:#168064;background:#e8f8f2;border-radius:16px}.production-empty strong{margin-top:17px;font-size:15px}.production-empty p{max-width:360px;margin:8px 0 20px;color:var(--muted);font-size:10px;line-height:1.7}.production-empty button{height:36px;display:flex;align-items:center;gap:7px;padding:0 14px;color:white;background:linear-gradient(135deg,#15936e,#20b486);border:0;border-radius:9px;font-size:10px;font-weight:800}.preview-busy{position:absolute;inset:31px 0 0;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:8px;color:var(--primary);background:rgba(248,248,252,.72);backdrop-filter:blur(4px)}.preview-busy strong{color:var(--text);font-size:13px}.preview-busy span{color:var(--muted);font-size:10px}.preview-footer{height:29px;padding:0 22px;display:flex;align-items:center;justify-content:space-between;gap:18px;color:var(--muted);background:#fff;border-top:1px solid var(--divider);font-size:8px}.preview-footer span{min-width:0;display:flex;align-items:center;gap:5px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.preview-footer span:last-child{flex:0 1 auto}
.history-row-state{display:flex;align-items:center;justify-content:center}
.draft-card-summary{margin:10px 0 0;padding:8px 9px;color:#765421;background:rgba(255,255,255,.7);border-radius:7px;font-size:8px;line-height:1.55}.version-message-field{display:block;margin-top:10px}.version-message-field>span{display:block;margin-bottom:5px;color:#765421;font-size:8px;font-weight:750}.version-message-field input{width:100%;height:31px;padding:0 9px;color:var(--text);background:#fff;border:1px solid #e4c98f;border-radius:7px;outline:0;font-size:9px}.version-message-field input:focus{border-color:#b87808;box-shadow:0 0 0 3px rgba(184,120,8,.08)}.draft-card-actions.primary{grid-template-columns:1.3fr 1fr}.draft-card-actions.secondary{margin-top:6px}.draft-card-actions.primary button:first-child{color:white;background:#b87808;border-color:#b87808}.draft-card-actions.primary button:last-child{color:#b42318;background:#fff;border-color:#efc5bd}.draft-card-actions.secondary button,.draft-card-actions.secondary button:last-child{color:#765421;background:#fff;border-color:#ecd8ad}.version-quick-actions{margin:0 0 14px;padding:9px 10px;display:flex;align-items:center;gap:9px;background:#f8f7fb;border:1px solid var(--border);border-radius:10px}.version-quick-actions button{height:27px;display:flex;align-items:center;gap:5px;flex:0 0 auto;padding:0 8px;color:#8a5b00;background:#fffaf0;border:1px solid #ecd8ad;border-radius:7px;font-size:8px;font-weight:750}.version-quick-actions small{color:var(--muted);font-size:7px;line-height:1.4}.history-version-actions{align-self:start;display:flex;align-items:center;gap:4px}.history-restore-button{min-height:24px;display:flex;align-items:center;gap:4px;padding:4px 7px;color:#8a5b00;background:#fffaf0;border:1px solid #ecd8ad;border-radius:6px;font-size:7px;font-weight:750}.history-restore-button:hover:not(:disabled){color:#6e4800;background:#fff4d9}
.rail-bottom,.rail-logout{display:none}
.rail button{position:relative}.rail button[data-tooltip]::after{content:attr(data-tooltip);position:absolute;z-index:50;left:48px;top:50%;width:max-content;max-width:150px;padding:6px 9px;color:white;background:#282435;border-radius:7px;box-shadow:0 7px 18px rgba(25,20,42,.2);font-size:9px;font-weight:700;line-height:1;white-space:nowrap;opacity:0;pointer-events:none;transform:translate(4px,-50%);transition:opacity .14s ease,transform .14s ease}.rail button[data-tooltip]:hover::after,.rail button[data-tooltip]:focus-visible::after{opacity:1;transform:translate(0,-50%)}.rail .rail-create-top{color:var(--muted)!important;background:transparent!important;box-shadow:none!important}.rail .rail-create-top:hover,.rail .rail-create-top.active{color:var(--primary)!important;background:var(--soft)!important}.rail .rail-create-top.active{box-shadow:inset 0 0 0 1px #e5ddff!important}
@media(max-width:1100px){.create-layout{grid-template-columns:1fr;gap:48px;padding-top:40px}.create-intro{text-align:center}.create-intro>p,.create-intro h1{margin-left:auto;margin-right:auto}.intro-tag{margin:auto}.feature-row{max-width:620px;margin-left:auto;margin-right:auto}.create-card-wrap{justify-content:center}.workspace{grid-template-columns:58px 340px 1fr}.create-workspace{grid-template-columns:58px 280px 1fr}.workspace-create-content{padding:28px}}
......
......@@ -2,6 +2,7 @@
"name": "webagent",
"version": "0.1.0",
"private": true,
"engines": { "node": ">=22.0.0" },
"packageManager": "pnpm@11.7.0",
"scripts": {
"dev": "pnpm --parallel --stream --filter @webagent/backend --filter @webagent/frontend dev",
......
......@@ -29,6 +29,9 @@ importers:
'@fastify/cors':
specifier: ^11.0.1
version: 11.3.0
'@qwen-code/sdk':
specifier: 0.1.8
version: 0.1.8(typescript@5.8.3)
'@webagent/shared':
specifier: workspace:*
version: link:../shared
......@@ -48,6 +51,9 @@ importers:
'@types/node':
specifier: ^22.15.3
version: 22.20.1
concurrently:
specifier: ^9.2.0
version: 9.2.4
tsx:
specifier: ^4.19.4
version: 4.23.1
......@@ -732,6 +738,12 @@ packages:
'@fastify/proxy-addr@5.1.0':
resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==}
'@hono/node-server@1.19.14':
resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==}
engines: {node: '>=18.14.1'}
peerDependencies:
hono: ^4
'@img/colour@1.1.0':
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
engines: {node: '>=18'}
......@@ -918,12 +930,29 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@modelcontextprotocol/sdk@1.29.0':
resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==}
engines: {node: '>=18'}
peerDependencies:
'@cfworker/json-schema': ^4.1.1
zod: ^3.25 || ^4.0
peerDependenciesMeta:
'@cfworker/json-schema':
optional: true
'@oslojs/encoding@1.1.0':
resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==}
'@pinojs/redact@0.4.0':
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
'@qwen-code/sdk@0.1.8':
resolution: {integrity: sha512-b75LytcbAX2Jqk1C4pe9oX4vMY0is+AvRTTcoo90d2FdlM0dT97AfOp8+JWwK9dJImZyB7TcJ3vFmA6irUZBWw==}
engines: {node: '>=22.0.0'}
hasBin: true
peerDependencies:
typescript: '>=5.0.0'
'@rollup/pluginutils@5.4.0':
resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==}
engines: {node: '>=14.0.0'}
......@@ -1189,6 +1218,10 @@ packages:
abstract-logging@2.0.1:
resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==}
accepts@2.0.0:
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
engines: {node: '>= 0.6'}
acorn@8.17.0:
resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==}
engines: {node: '>=0.4.0'}
......@@ -1282,6 +1315,10 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
body-parser@2.3.0:
resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==}
engines: {node: '>=18'}
boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
......@@ -1294,6 +1331,18 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
bytes@3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'}
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
call-bound@1.0.4:
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
engines: {node: '>= 0.4'}
camelcase@8.0.0:
resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==}
engines: {node: '>=16'}
......@@ -1304,6 +1353,10 @@ packages:
ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
chalk@5.6.2:
resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
......@@ -1358,12 +1411,37 @@ packages:
common-ancestor-path@1.0.1:
resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==}
concurrently@9.2.4:
resolution: {integrity: sha512-TZ0CEhyzvFjgtAvHTusDMgj7wNdihCh7LLLrzdUOXIhdlnL2JBBGA9eJxR24rtqgmdjh3OA3hrN1rCHj6HM8qA==}
engines: {node: '>=18'}
hasBin: true
content-disposition@1.1.0:
resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==}
engines: {node: '>=18'}
content-type@1.0.5:
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
engines: {node: '>= 0.6'}
content-type@2.0.0:
resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==}
engines: {node: '>=18'}
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
cookie-es@1.2.3:
resolution: {integrity: sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==}
cookie-signature@1.2.2:
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
engines: {node: '>=6.6.0'}
cookie@0.7.1:
resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==}
engines: {node: '>= 0.6'}
cookie@1.0.1:
resolution: {integrity: sha512-Xd8lFX4LM9QEEwxQpF9J9NTUh8pmdJO0cyRJhFiDoLTk2eH8FXlRv2IFGYVadZpqI3j8fhNrSdKCeYPxiAhLXw==}
engines: {node: '>=18'}
......@@ -1372,6 +1450,10 @@ packages:
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
engines: {node: '>=18'}
cors@2.8.6:
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
engines: {node: '>= 0.10'}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
......@@ -1421,6 +1503,10 @@ packages:
defu@6.1.7:
resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
......@@ -1470,6 +1556,13 @@ packages:
resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==}
engines: {node: '>=4'}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
electron-to-chromium@1.5.389:
resolution: {integrity: sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==}
......@@ -1482,6 +1575,10 @@ packages:
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
encodeurl@2.0.0:
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
engines: {node: '>= 0.8'}
entities@4.2.0:
resolution: {integrity: sha512-wEJa03bJgqEwPnkUqYdgmcfUXfm6+4hePQhntIvRy/1/+C4dFuhYHsgKBRjbQ6OWBh42P+VhAoCDO77DUh0e/Q==}
engines: {node: '>=0.12'}
......@@ -1490,9 +1587,21 @@ packages:
resolution: {integrity: sha512-/iP1rZrSEJ0DTlPiX+jbzlA3eVkY/e8L8SozroF395fIqE3TYF/Nz7YOMAawta+vLmyJ/hkGNNPcSbMADCCXbg==}
engines: {node: '>=0.12'}
es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'}
es-errors@1.3.0:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
es-module-lexer@1.7.0:
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
es-object-atoms@1.1.2:
resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==}
engines: {node: '>= 0.4'}
esbuild@0.25.0:
resolution: {integrity: sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==}
engines: {node: '>=18'}
......@@ -1512,6 +1621,9 @@ packages:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
escape-html@1.0.3:
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
escape-string-regexp@5.0.0:
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
engines: {node: '>=12'}
......@@ -1522,13 +1634,35 @@ packages:
estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
etag@1.8.1:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
eventemitter3@5.0.4:
resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
eventsource-parser@3.1.0:
resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==}
engines: {node: '>=18.0.0'}
eventsource@3.0.7:
resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==}
engines: {node: '>=18.0.0'}
execa@9.6.1:
resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==}
engines: {node: ^18.19.0 || >=20.5.0}
express-rate-limit@8.6.0:
resolution: {integrity: sha512-XKJXDsASUOo0LLtFwW5hCcQGH0N4WQc/Rn8/Pvoia+TJFOkkFPvrtW9lZOeeNcxQJspvOIERMwiRLsVFlhHEkA==}
engines: {node: '>= 16'}
peerDependencies:
express: '>= 4.11'
express@5.2.1:
resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==}
engines: {node: '>= 18'}
extend@3.0.2:
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
......@@ -1572,6 +1706,10 @@ packages:
resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
engines: {node: '>=18'}
finalhandler@2.1.1:
resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==}
engines: {node: '>= 18.0.0'}
find-my-way@9.6.0:
resolution: {integrity: sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ==}
engines: {node: '>=20'}
......@@ -1587,11 +1725,22 @@ packages:
resolution: {integrity: sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==}
engines: {node: '>=20'}
forwarded@0.2.0:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'}
fresh@2.0.0:
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
engines: {node: '>= 0.8'}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
gensync@1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
......@@ -1604,6 +1753,14 @@ packages:
resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==}
engines: {node: '>=18'}
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
get-proto@1.0.1:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
get-stream@9.0.1:
resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==}
engines: {node: '>=18'}
......@@ -1611,9 +1768,25 @@ packages:
github-slugger@2.0.0:
resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
h3@1.15.11:
resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==}
has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'}
hasown@2.0.4:
resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
engines: {node: '>= 0.4'}
hast-util-from-html@2.0.3:
resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==}
......@@ -1644,6 +1817,10 @@ packages:
hastscript@9.0.1:
resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
hono@4.12.31:
resolution: {integrity: sha512-zJIHFrl6bq3RDd2YusFNCDlM8qUprxKswyi/OPzPyzKDdyBXDqWx8bZlZ7R+saTdSTatUmb3O7K4SspGPaEOQg==}
engines: {node: '>=16.9.0'}
html-escaper@3.0.3:
resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==}
......@@ -1653,13 +1830,40 @@ packages:
http-cache-semantics@4.2.0:
resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==}
http-errors@2.0.0:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'}
http-errors@2.0.1:
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
engines: {node: '>= 0.8'}
human-signals@8.0.1:
resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==}
engines: {node: '>=18.18.0'}
iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
iconv-lite@0.7.3:
resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==}
engines: {node: '>=0.10.0'}
import-meta-resolve@4.2.0:
resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==}
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
ip-address@10.2.0:
resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==}
engines: {node: '>= 12'}
ipaddr.js@1.9.1:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'}
ipaddr.js@2.4.0:
resolution: {integrity: sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==}
engines: {node: '>= 10'}
......@@ -1685,6 +1889,9 @@ packages:
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
engines: {node: '>=12'}
is-promise@4.0.0:
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
is-stream@4.0.1:
resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==}
engines: {node: '>=18'}
......@@ -1700,6 +1907,9 @@ packages:
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
jose@6.2.3:
resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
......@@ -1718,6 +1928,9 @@ packages:
json-schema-traverse@1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
json-schema-typed@8.0.2:
resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==}
json5@2.2.3:
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
engines: {node: '>=6'}
......@@ -1764,6 +1977,10 @@ packages:
markdown-table@3.0.4:
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
mdast-util-definitions@6.0.0:
resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==}
......@@ -1809,6 +2026,14 @@ packages:
mdn-data@2.27.1:
resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
media-typer@1.1.0:
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
engines: {node: '>= 0.8'}
merge-descriptors@2.0.0:
resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==}
engines: {node: '>=18'}
micromark-core-commonmark@2.0.0:
resolution: {integrity: sha512-jThOz/pVmAYUtkroV3D5c1osFXAMv9e0ypGDOIZuCeAe91/sD6BoE2Sjzt30yuXtwOYUmySOhMas/PVyh02itA==}
......@@ -1893,6 +2118,14 @@ packages:
micromark@4.0.0:
resolution: {integrity: sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==}
mime-db@1.54.0:
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
engines: {node: '>= 0.6'}
mime-types@3.0.2:
resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
engines: {node: '>=18'}
mrmime@2.0.1:
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
engines: {node: '>=10'}
......@@ -1908,6 +2141,10 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
negotiator@1.0.0:
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
engines: {node: '>= 0.6'}
neotraverse@0.6.18:
resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==}
engines: {node: '>= 10'}
......@@ -1936,6 +2173,14 @@ packages:
nth-check@2.0.1:
resolution: {integrity: sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==}
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
object-inspect@1.13.4:
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
engines: {node: '>= 0.4'}
ofetch@1.5.1:
resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==}
......@@ -1946,6 +2191,13 @@ packages:
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
engines: {node: '>=14.0.0'}
on-finished@2.4.1:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'}
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
oniguruma-parser@0.12.2:
resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==}
......@@ -1977,6 +2229,10 @@ packages:
parse5@7.0.0:
resolution: {integrity: sha512-y/t8IXSPWTuRZqXc0ajH/UwDj4mnqLEbSttNbThcFhGrZuOyoyvNBO85PBp2jQa55wY9d07PBNjsK8ZP3K5U6g==}
parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
path-browserify@1.0.1:
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
......@@ -1988,6 +2244,9 @@ packages:
resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
engines: {node: '>=12'}
path-to-regexp@8.4.2:
resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==}
piccolore@0.1.3:
resolution: {integrity: sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==}
......@@ -2012,6 +2271,10 @@ packages:
resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==}
hasBin: true
pkce-challenge@5.0.0:
resolution: {integrity: sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==}
engines: {node: '>=16.20.0'}
postcss@8.5.18:
resolution: {integrity: sha512-xdB1oSLHbz1vRWgCDalrCqEFTWzFlhqFC5tIHLMOSUIjhm3XXQ1qrFy8S/ESr1JYRRXqM3c1QFiMZUJdUTqyMQ==}
engines: {node: ^10 || ^12 || >=14}
......@@ -2042,12 +2305,32 @@ packages:
property-information@7.2.0:
resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==}
proxy-addr@2.0.7:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'}
qs@6.15.3:
resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==}
engines: {node: '>=0.6'}
quick-format-unescaped@4.0.4:
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
radix3@1.1.2:
resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==}
range-parser@1.3.0:
resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==}
engines: {node: '>= 0.6'}
raw-body@3.0.0:
resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==}
engines: {node: '>= 0.8'}
raw-body@3.0.2:
resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==}
engines: {node: '>= 0.10'}
react-dom@19.2.7:
resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==}
peerDependencies:
......@@ -2155,6 +2438,13 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
router@2.2.0:
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
engines: {node: '>= 18'}
rxjs@7.8.2:
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
safe-regex2@5.1.1:
resolution: {integrity: sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==}
hasBin: true
......@@ -2163,6 +2453,9 @@ packages:
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
engines: {node: '>=10'}
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
sax@1.6.0:
resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==}
engines: {node: '>=11.0.0'}
......@@ -2182,9 +2475,20 @@ packages:
engines: {node: '>=10'}
hasBin: true
send@1.2.1:
resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==}
engines: {node: '>= 18'}
serve-static@2.2.1:
resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==}
engines: {node: '>= 18'}
set-cookie-parser@2.6.0:
resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==}
setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
sharp@0.34.5:
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
......@@ -2197,9 +2501,29 @@ packages:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
shell-quote@1.9.0:
resolution: {integrity: sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==}
engines: {node: '>= 0.4'}
shiki@3.23.0:
resolution: {integrity: sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==}
side-channel-list@1.0.1:
resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==}
engines: {node: '>= 0.4'}
side-channel-map@1.0.1:
resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
engines: {node: '>= 0.4'}
side-channel-weakmap@1.0.2:
resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
engines: {node: '>= 0.4'}
side-channel@1.1.1:
resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==}
engines: {node: '>= 0.4'}
signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
......@@ -2225,6 +2549,14 @@ packages:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
statuses@2.0.1:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'}
statuses@2.0.2:
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
engines: {node: '>= 0.8'}
string-width@4.1.0:
resolution: {integrity: sha512-NrX+1dVVh+6Y9dnQ19pR0pP4FiEIlUvdTGn8pw6CKTNq5sgib2nIhmUNT5TAmhWmvKr3WcxBcP3E8nWezuipuQ==}
engines: {node: '>=8'}
......@@ -2256,6 +2588,14 @@ packages:
resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==}
engines: {node: '>=18'}
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
supports-color@8.1.1:
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
engines: {node: '>=10'}
svgo@4.0.2:
resolution: {integrity: sha512-ekx94z1rRc5LDi6oSUaeRnYhd0UOJxdtQCL2rF8xpWxD3TPAsISWOrxezqGovqS38GRZOdpDfvQe3ts6F7nsng==}
engines: {node: '>=16'}
......@@ -2280,6 +2620,14 @@ packages:
resolution: {integrity: sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==}
engines: {node: '>=20'}
toidentifier@1.0.1:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
tree-kill@1.2.2:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true
trim-lines@3.0.1:
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
......@@ -2308,6 +2656,10 @@ packages:
resolution: {integrity: sha512-ADn2w7hVPcK6w1I0uWnM//y1rLXZhzB9mr0a3OirzclKF1Wp6VzevUmzz/NRAWunOT6E8HrnpGY7xOfc6K57fA==}
engines: {node: '>=16'}
type-is@2.1.0:
resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==}
engines: {node: '>= 18'}
typesafe-path@0.2.2:
resolution: {integrity: sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==}
......@@ -2371,6 +2723,10 @@ packages:
unist-util-visit@5.1.0:
resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==}
unpipe@1.0.0:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'}
unstorage@1.17.5:
resolution: {integrity: sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==}
peerDependencies:
......@@ -2439,6 +2795,10 @@ packages:
peerDependencies:
browserslist: '>= 4.21.0'
vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
vfile-location@5.0.3:
resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
......@@ -2632,6 +2992,9 @@ packages:
resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==}
engines: {node: '>=18'}
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
xxhash-wasm@1.1.0:
resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==}
......@@ -2660,6 +3023,10 @@ packages:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
yargs@17.7.2:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'}
yargs@17.7.3:
resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==}
engines: {node: '>=12'}
......@@ -3187,6 +3554,10 @@ snapshots:
'@fastify/forwarded': 3.0.1
ipaddr.js: 2.4.0
'@hono/node-server@1.19.14(hono@4.12.31)':
dependencies:
hono: 4.12.31
'@img/colour@1.1.0':
optional: true
......@@ -3317,10 +3688,41 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.4.14
'@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)':
dependencies:
'@hono/node-server': 1.19.14(hono@4.12.31)
ajv: 8.20.0
ajv-formats: 3.0.1(ajv@8.20.0)
content-type: 1.0.5
cors: 2.8.6
cross-spawn: 7.0.6
eventsource: 3.0.7
eventsource-parser: 3.1.0
express: 5.2.1
express-rate-limit: 8.6.0(express@5.2.1)
hono: 4.12.31
jose: 6.2.3
json-schema-typed: 8.0.2
pkce-challenge: 5.0.0
raw-body: 3.0.0
zod: 3.25.76
zod-to-json-schema: 3.25.2(zod@3.25.76)
transitivePeerDependencies:
- supports-color
'@oslojs/encoding@1.1.0': {}
'@pinojs/redact@0.4.0': {}
'@qwen-code/sdk@0.1.8(typescript@5.8.3)':
dependencies:
'@modelcontextprotocol/sdk': 1.29.0(zod@3.25.76)
typescript: 5.8.3
zod: 3.25.76
transitivePeerDependencies:
- '@cfworker/json-schema'
- supports-color
'@rollup/pluginutils@5.4.0(rollup@4.62.2)':
dependencies:
'@types/estree': 1.0.9
......@@ -3568,6 +3970,11 @@ snapshots:
abstract-logging@2.0.1: {}
accepts@2.0.0:
dependencies:
mime-types: 3.0.2
negotiator: 1.0.0
acorn@8.17.0: {}
ajv-draft-04@1.0.0(ajv@8.20.0):
......@@ -3733,6 +4140,20 @@ snapshots:
baseline-browser-mapping@2.10.43: {}
body-parser@2.3.0:
dependencies:
bytes: 3.1.2
content-type: 2.0.0
debug: 4.4.3
http-errors: 2.0.1
iconv-lite: 0.7.3
on-finished: 2.4.1
qs: 6.15.3
raw-body: 3.0.2
type-is: 2.1.0
transitivePeerDependencies:
- supports-color
boolbase@1.0.0: {}
boxen@8.0.1:
......@@ -3754,12 +4175,29 @@ snapshots:
node-releases: 2.0.51
update-browserslist-db: 1.2.3(browserslist@4.28.6)
bytes@3.1.2: {}
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
function-bind: 1.1.2
call-bound@1.0.4:
dependencies:
call-bind-apply-helpers: 1.0.2
get-intrinsic: 1.3.0
camelcase@8.0.0: {}
caniuse-lite@1.0.30001805: {}
ccount@2.0.1: {}
chalk@4.1.2:
dependencies:
ansi-styles: 4.3.0
supports-color: 7.2.0
chalk@5.6.2: {}
character-entities-html4@2.1.0: {}
......@@ -3800,14 +4238,38 @@ snapshots:
common-ancestor-path@1.0.1: {}
concurrently@9.2.4:
dependencies:
chalk: 4.1.2
rxjs: 7.8.2
shell-quote: 1.9.0
supports-color: 8.1.1
tree-kill: 1.2.2
yargs: 17.7.2
content-disposition@1.1.0: {}
content-type@1.0.5: {}
content-type@2.0.0: {}
convert-source-map@2.0.0: {}
cookie-es@1.2.3: {}
cookie-signature@1.2.2: {}
cookie@0.7.1: {}
cookie@1.0.1: {}
cookie@1.1.1: {}
cors@2.8.6:
dependencies:
object-assign: 4.1.1
vary: 1.1.2
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
......@@ -3856,6 +4318,8 @@ snapshots:
defu@6.1.7: {}
depd@2.0.0: {}
dequal@2.0.3: {}
destr@2.0.5: {}
......@@ -3899,6 +4363,14 @@ snapshots:
dset@3.1.4: {}
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
es-errors: 1.3.0
gopd: 1.2.0
ee-first@1.1.1: {}
electron-to-chromium@1.5.389: {}
emmet@2.4.11:
......@@ -3910,12 +4382,22 @@ snapshots:
emoji-regex@8.0.0: {}
encodeurl@2.0.0: {}
entities@4.2.0: {}
entities@4.3.0: {}
es-define-property@1.0.1: {}
es-errors@1.3.0: {}
es-module-lexer@1.7.0: {}
es-object-atoms@1.1.2:
dependencies:
es-errors: 1.3.0
esbuild@0.25.0:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.0
......@@ -4004,6 +4486,8 @@ snapshots:
escalade@3.2.0: {}
escape-html@1.0.3: {}
escape-string-regexp@5.0.0: {}
estree-walker@2.0.2: {}
......@@ -4012,8 +4496,16 @@ snapshots:
dependencies:
'@types/estree': 1.0.9
etag@1.8.1: {}
eventemitter3@5.0.4: {}
eventsource-parser@3.1.0: {}
eventsource@3.0.7:
dependencies:
eventsource-parser: 3.1.0
execa@9.6.1:
dependencies:
'@sindresorhus/merge-streams': 4.0.0
......@@ -4029,6 +4521,47 @@ snapshots:
strip-final-newline: 4.0.0
yoctocolors: 2.1.2
express-rate-limit@8.6.0(express@5.2.1):
dependencies:
debug: 4.4.3
express: 5.2.1
ip-address: 10.2.0
transitivePeerDependencies:
- supports-color
express@5.2.1:
dependencies:
accepts: 2.0.0
body-parser: 2.3.0
content-disposition: 1.1.0
content-type: 1.0.5
cookie: 0.7.1
cookie-signature: 1.2.2
debug: 4.4.3
depd: 2.0.0
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
finalhandler: 2.1.1
fresh: 2.0.0
http-errors: 2.0.1
merge-descriptors: 2.0.0
mime-types: 3.0.2
on-finished: 2.4.1
once: 1.4.0
parseurl: 1.3.3
proxy-addr: 2.0.7
qs: 6.15.3
range-parser: 1.3.0
router: 2.2.0
send: 1.2.1
serve-static: 2.2.1
statuses: 2.0.2
type-is: 2.1.0
vary: 1.1.2
transitivePeerDependencies:
- supports-color
extend@3.0.2: {}
fast-decode-uri-component@1.0.1: {}
......@@ -4084,6 +4617,17 @@ snapshots:
dependencies:
is-unicode-supported: 2.1.0
finalhandler@2.1.1:
dependencies:
debug: 4.4.3
encodeurl: 2.0.0
escape-html: 1.0.3
on-finished: 2.4.1
parseurl: 1.3.3
statuses: 2.0.2
transitivePeerDependencies:
- supports-color
find-my-way@9.6.0:
dependencies:
fast-deep-equal: 3.1.3
......@@ -4100,15 +4644,39 @@ snapshots:
dependencies:
tiny-inflate: 1.0.3
forwarded@0.2.0: {}
fresh@2.0.0: {}
fsevents@2.3.3:
optional: true
function-bind@1.1.2: {}
gensync@1.0.0-beta.2: {}
get-caller-file@2.0.5: {}
get-east-asian-width@1.6.0: {}
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
es-define-property: 1.0.1
es-errors: 1.3.0
es-object-atoms: 1.1.2
function-bind: 1.1.2
get-proto: 1.0.1
gopd: 1.2.0
has-symbols: 1.1.0
hasown: 2.0.4
math-intrinsics: 1.1.0
get-proto@1.0.1:
dependencies:
dunder-proto: 1.0.1
es-object-atoms: 1.1.2
get-stream@9.0.1:
dependencies:
'@sec-ant/readable-stream': 0.4.1
......@@ -4116,6 +4684,8 @@ snapshots:
github-slugger@2.0.0: {}
gopd@1.2.0: {}
h3@1.15.11:
dependencies:
cookie-es: 1.2.3
......@@ -4128,6 +4698,14 @@ snapshots:
ufo: 1.6.4
uncrypto: 0.1.3
has-flag@4.0.0: {}
has-symbols@1.1.0: {}
hasown@2.0.4:
dependencies:
function-bind: 1.1.2
hast-util-from-html@2.0.3:
dependencies:
'@types/hast': 3.0.5
......@@ -4215,16 +4793,48 @@ snapshots:
property-information: 7.2.0
space-separated-tokens: 2.0.2
hono@4.12.31: {}
html-escaper@3.0.3: {}
html-void-elements@3.0.0: {}
http-cache-semantics@4.2.0: {}
http-errors@2.0.0:
dependencies:
depd: 2.0.0
inherits: 2.0.4
setprototypeof: 1.2.0
statuses: 2.0.1
toidentifier: 1.0.1
http-errors@2.0.1:
dependencies:
depd: 2.0.0
inherits: 2.0.4
setprototypeof: 1.2.0
statuses: 2.0.2
toidentifier: 1.0.1
human-signals@8.0.1: {}
iconv-lite@0.6.3:
dependencies:
safer-buffer: 2.1.2
iconv-lite@0.7.3:
dependencies:
safer-buffer: 2.1.2
import-meta-resolve@4.2.0: {}
inherits@2.0.4: {}
ip-address@10.2.0: {}
ipaddr.js@1.9.1: {}
ipaddr.js@2.4.0: {}
iron-webcrypto@1.2.1: {}
......@@ -4239,6 +4849,8 @@ snapshots:
is-plain-obj@4.1.0: {}
is-promise@4.0.0: {}
is-stream@4.0.1: {}
is-unicode-supported@2.1.0: {}
......@@ -4249,6 +4861,8 @@ snapshots:
isexe@2.0.0: {}
jose@6.2.3: {}
js-tokens@4.0.0: {}
js-yaml@4.3.0:
......@@ -4263,6 +4877,8 @@ snapshots:
json-schema-traverse@1.0.0: {}
json-schema-typed@8.0.2: {}
json5@2.2.3: {}
jsonc-parser@2.3.0: {}
......@@ -4303,6 +4919,8 @@ snapshots:
markdown-table@3.0.4: {}
math-intrinsics@1.1.0: {}
mdast-util-definitions@6.0.0:
dependencies:
'@types/mdast': 4.0.4
......@@ -4426,6 +5044,10 @@ snapshots:
mdn-data@2.27.1: {}
media-typer@1.1.0: {}
merge-descriptors@2.0.0: {}
micromark-core-commonmark@2.0.0:
dependencies:
decode-named-character-reference: 1.3.0
......@@ -4617,6 +5239,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
mime-db@1.54.0: {}
mime-types@3.0.2:
dependencies:
mime-db: 1.54.0
mrmime@2.0.1: {}
ms@2.1.3: {}
......@@ -4625,6 +5253,8 @@ snapshots:
nanoid@3.3.16: {}
negotiator@1.0.0: {}
neotraverse@0.6.18: {}
nlcst-to-string@4.0.0:
......@@ -4648,6 +5278,10 @@ snapshots:
dependencies:
boolbase: 1.0.0
object-assign@4.1.1: {}
object-inspect@1.13.4: {}
ofetch@1.5.1:
dependencies:
destr: 2.0.5
......@@ -4658,6 +5292,14 @@ snapshots:
on-exit-leak-free@2.1.2: {}
on-finished@2.4.1:
dependencies:
ee-first: 1.1.1
once@1.4.0:
dependencies:
wrappy: 1.0.2
oniguruma-parser@0.12.2: {}
oniguruma-to-es@4.3.6:
......@@ -4694,12 +5336,16 @@ snapshots:
dependencies:
entities: 4.3.0
parseurl@1.3.3: {}
path-browserify@1.0.1: {}
path-key@3.1.1: {}
path-key@4.0.0: {}
path-to-regexp@8.4.2: {}
piccolore@0.1.3: {}
picocolors@1.1.1: {}
......@@ -4728,6 +5374,8 @@ snapshots:
sonic-boom: 4.2.1
thread-stream: 4.2.0
pkce-challenge@5.0.0: {}
postcss@8.5.18:
dependencies:
nanoid: 3.3.16
......@@ -4753,10 +5401,36 @@ snapshots:
property-information@7.2.0: {}
proxy-addr@2.0.7:
dependencies:
forwarded: 0.2.0
ipaddr.js: 1.9.1
qs@6.15.3:
dependencies:
es-define-property: 1.0.1
side-channel: 1.1.1
quick-format-unescaped@4.0.4: {}
radix3@1.1.2: {}
range-parser@1.3.0: {}
raw-body@3.0.0:
dependencies:
bytes: 3.1.2
http-errors: 2.0.0
iconv-lite: 0.6.3
unpipe: 1.0.0
raw-body@3.0.2:
dependencies:
bytes: 3.1.2
http-errors: 2.0.1
iconv-lite: 0.7.3
unpipe: 1.0.0
react-dom@19.2.7(react@19.2.7):
dependencies:
react: 19.2.7
......@@ -4920,12 +5594,28 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.62.2
fsevents: 2.3.3
router@2.2.0:
dependencies:
debug: 4.4.3
depd: 2.0.0
is-promise: 4.0.0
parseurl: 1.3.3
path-to-regexp: 8.4.2
transitivePeerDependencies:
- supports-color
rxjs@7.8.2:
dependencies:
tslib: 2.8.1
safe-regex2@5.1.1:
dependencies:
ret: 0.5.0
safe-stable-stringify@2.5.0: {}
safer-buffer@2.1.2: {}
sax@1.6.0: {}
scheduler@0.27.0: {}
......@@ -4936,8 +5626,35 @@ snapshots:
semver@7.8.5: {}
send@1.2.1:
dependencies:
debug: 4.4.3
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
fresh: 2.0.0
http-errors: 2.0.1
mime-types: 3.0.2
ms: 2.1.3
on-finished: 2.4.1
range-parser: 1.3.0
statuses: 2.0.2
transitivePeerDependencies:
- supports-color
serve-static@2.2.1:
dependencies:
encodeurl: 2.0.0
escape-html: 1.0.3
parseurl: 1.3.3
send: 1.2.1
transitivePeerDependencies:
- supports-color
set-cookie-parser@2.6.0: {}
setprototypeof@1.2.0: {}
sharp@0.34.5:
dependencies:
'@img/colour': 1.1.0
......@@ -4976,6 +5693,8 @@ snapshots:
shebang-regex@3.0.0: {}
shell-quote@1.9.0: {}
shiki@3.23.0:
dependencies:
'@shikijs/core': 3.23.0
......@@ -4987,6 +5706,34 @@ snapshots:
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.5
side-channel-list@1.0.1:
dependencies:
es-errors: 1.3.0
object-inspect: 1.13.4
side-channel-map@1.0.1:
dependencies:
call-bound: 1.0.4
es-errors: 1.3.0
get-intrinsic: 1.3.0
object-inspect: 1.13.4
side-channel-weakmap@1.0.2:
dependencies:
call-bound: 1.0.4
es-errors: 1.3.0
get-intrinsic: 1.3.0
object-inspect: 1.13.4
side-channel-map: 1.0.1
side-channel@1.1.1:
dependencies:
es-errors: 1.3.0
object-inspect: 1.13.4
side-channel-list: 1.0.1
side-channel-map: 1.0.1
side-channel-weakmap: 1.0.2
signal-exit@4.1.0: {}
sisteransi@1.0.5: {}
......@@ -5003,6 +5750,10 @@ snapshots:
split2@4.2.0: {}
statuses@2.0.1: {}
statuses@2.0.2: {}
string-width@4.1.0:
dependencies:
emoji-regex: 8.0.0
......@@ -5040,6 +5791,14 @@ snapshots:
strip-final-newline@4.0.0: {}
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
supports-color@8.1.1:
dependencies:
has-flag: 4.0.0
svgo@4.0.2:
dependencies:
commander: 11.1.0
......@@ -5065,6 +5824,10 @@ snapshots:
toad-cache@3.7.4: {}
toidentifier@1.0.1: {}
tree-kill@1.2.2: {}
trim-lines@3.0.1: {}
trough@2.2.0: {}
......@@ -5073,8 +5836,7 @@ snapshots:
optionalDependencies:
typescript: 5.8.3
tslib@2.8.1:
optional: true
tslib@2.8.1: {}
tsx@4.23.1:
dependencies:
......@@ -5084,6 +5846,12 @@ snapshots:
type-fest@4.21.0: {}
type-is@2.1.0:
dependencies:
content-type: 2.0.0
media-typer: 1.1.0
mime-types: 3.0.2
typesafe-path@0.2.2: {}
typescript-auto-import-cache@0.3.6:
......@@ -5170,6 +5938,8 @@ snapshots:
unist-util-is: 6.0.1
unist-util-visit-parents: 6.0.2
unpipe@1.0.0: {}
unstorage@1.17.5:
dependencies:
anymatch: 3.1.3
......@@ -5187,6 +5957,8 @@ snapshots:
escalade: 3.2.0
picocolors: 1.1.1
vary@1.1.2: {}
vfile-location@5.0.3:
dependencies:
'@types/unist': 3.0.3
......@@ -5352,7 +6124,7 @@ snapshots:
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
string-width: 4.1.0
strip-ansi: 6.0.1
wrap-ansi@9.0.0:
......@@ -5361,6 +6133,8 @@ snapshots:
string-width: 7.2.0
strip-ansi: 7.2.0
wrappy@1.0.2: {}
xxhash-wasm@1.1.0: {}
y18n@5.0.8: {}
......@@ -5388,6 +6162,16 @@ snapshots:
yargs-parser@21.1.1: {}
yargs@17.7.2:
dependencies:
cliui: 8.0.1
escalade: 3.2.0
get-caller-file: 2.0.5
require-directory: 2.1.1
string-width: 4.2.3
y18n: 5.0.8
yargs-parser: 21.1.1
yargs@17.7.3:
dependencies:
cliui: 8.0.1
......
......@@ -5,5 +5,6 @@ packages:
- astro-template
allowBuilds:
better-sqlite3: true
esbuild: true
sharp: true
......@@ -25,7 +25,6 @@ export interface SiteInfo {
productionUrl?: string;
currentCommit: string;
previewCommit?: string;
previousCommit?: string;
draftBaseCommit?: string;
draftUpdatedAt?: string;
draftSummary?: string;
......@@ -116,6 +115,71 @@ export interface ChatResult {
mode: "model" | "local";
}
export const AGENT_RUN_STATES = [
"queued", "preparing_workspace", "running_agent", "checking_diff",
"validating_build", "updating_preview", "completed", "failed",
"cancelled", "timed_out",
] as const;
export type AgentRunStatus = (typeof AGENT_RUN_STATES)[number];
export type AgentFailureCategory = "provider" | "workspace" | "policy" | "build" | "cancelled" | "timeout" | "internal";
export type AgentEventType = "status" | "assistant" | "tool_started" | "tool_completed" | "command_started" | "command_completed" | "warning" | "error";
export interface AgentUsage {
inputTokens?: number;
outputTokens?: number;
totalTokens?: number;
turns?: number;
}
export interface AgentBuildValidation {
passed: boolean;
buildExitCode: number;
durationMs: number;
output: string;
}
export interface AgentRunEvent {
sequence?: number;
runId: string;
type: AgentEventType;
timestamp: string;
message: string;
data?: Record<string, unknown>;
}
export interface AgentRunInfo {
runId: string;
tenantId: string;
siteId: string;
provider: string;
providerVersion: string;
model?: string;
sessionId?: string;
status: AgentRunStatus;
prompt: string;
summary?: string;
createdAt: string;
startedAt?: string;
endedAt?: string;
baselineCommit?: string;
finalCommit?: string;
changedFiles: string[];
failureCategory?: AgentFailureCategory;
error?: string;
usage?: AgentUsage;
validation?: AgentBuildValidation;
previewUrl?: string;
cancelRequested: boolean;
}
export type AgentProviderMode = "local" | "qwen-code";
export interface AgentSettings {
provider: AgentProviderMode;
realAvailable: boolean;
}
export interface PublishResult {
productionUrl: string;
commit: string;
......@@ -127,6 +191,11 @@ export interface PreviewVersionResult {
commit: string;
}
export interface DraftPreviewResult {
previewUrl: string;
baseCommit: string;
}
export type DomainOwnershipStatus = "pending" | "verified" | "failed";
export type DomainDnsStatus = "pending" | "valid" | "invalid";
export type DomainDeploymentStatus = "pending" | "deploying" | "active" | "failed";
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment