baa-conductor


commit
2dd3664
parent
68a85cf
author
codex@macbookpro
date
2026-04-03 14:46:47 +0800 CST
feat: add conductor ui shell and app hosting
19 files changed,  +1839, -13
Raw patch view.
   1diff --git a/apps/conductor-daemon/package.json b/apps/conductor-daemon/package.json
   2index fecbbf39ec5c681de004178c12a4c5398c6fad9f..8110b642a05e925b3fd0ec6456ec00f71a30ab95 100644
   3--- a/apps/conductor-daemon/package.json
   4+++ b/apps/conductor-daemon/package.json
   5@@ -10,7 +10,7 @@
   6     "@baa-conductor/host-ops": "workspace:*"
   7   },
   8   "scripts": {
   9-    "build": "pnpm -C ../.. -F @baa-conductor/db build && pnpm -C ../.. -F @baa-conductor/artifact-db build && pnpm -C ../.. -F @baa-conductor/d1-client build && pnpm -C ../.. -F @baa-conductor/host-ops build && pnpm -C ../.. -F @baa-conductor/status-api build && pnpm exec tsc -p tsconfig.json",
  10+    "build": "pnpm -C ../.. -F @baa-conductor/db build && pnpm -C ../.. -F @baa-conductor/artifact-db build && pnpm -C ../.. -F @baa-conductor/d1-client build && pnpm -C ../.. -F @baa-conductor/host-ops build && pnpm -C ../.. -F @baa-conductor/status-api build && pnpm -C ../.. -F @baa-conductor/conductor-ui build && pnpm exec tsc -p tsconfig.json",
  11     "dev": "pnpm run build && node dist/index.js",
  12     "start": "node dist/index.js",
  13     "test": "pnpm run build && node --test src/index.test.js",
  14diff --git a/apps/conductor-daemon/src/index.test.js b/apps/conductor-daemon/src/index.test.js
  15index a8427b699caeadba57c7b0c83c6e47d9c1ace747..c03bfaf5c037ba0216687d5610f20f08194ad293 100644
  16--- a/apps/conductor-daemon/src/index.test.js
  17+++ b/apps/conductor-daemon/src/index.test.js
  18@@ -600,10 +600,50 @@ async function withArtifactStoreFixture(callback) {
  19   }
  20 }
  21 
  22+async function withConductorUiDistFixture(callback) {
  23+  const stateDir = mkdtempSync(join(tmpdir(), "baa-conductor-ui-dist-"));
  24+  const uiDistDir = join(stateDir, "dist");
  25+  const assetFile = "app-shell.js";
  26+
  27+  mkdirSync(join(uiDistDir, "assets"), {
  28+    recursive: true
  29+  });
  30+  writeFileSync(
  31+    join(uiDistDir, "index.html"),
  32+    [
  33+      "<!doctype html>",
  34+      "<html lang=\"zh-CN\">",
  35+      "<head><meta charset=\"UTF-8\" /><title>Conductor UI Shell Fixture</title></head>",
  36+      "<body>",
  37+      "<div id=\"app\">fixture</div>",
  38+      `<script type="module" src="/app/assets/${assetFile}"></script>`,
  39+      "</body>",
  40+      "</html>"
  41+    ].join("")
  42+  );
  43+  writeFileSync(join(uiDistDir, "assets", assetFile), "console.log('fixture ui shell');\n");
  44+
  45+  try {
  46+    return await callback({
  47+      assetFile,
  48+      uiDistDir
  49+    });
  50+  } finally {
  51+    rmSync(stateDir, {
  52+      force: true,
  53+      recursive: true
  54+    });
  55+  }
  56+}
  57+
  58 function parseJsonBody(response) {
  59   return JSON.parse(response.body);
  60 }
  61 
  62+function readBinaryBodyText(response) {
  63+  return typeof response.body === "string" ? response.body : Buffer.from(response.body).toString("utf8");
  64+}
  65+
  66 test(
  67   "BAA instruction extraction and parsing supports the four parameter forms and ignores ordinary code fences",
  68   () => {
  69@@ -7675,6 +7715,24 @@ test("ConductorRuntime serves health and migrated local API endpoints over HTTP"
  70     assert.match(statusViewUiHtml, /Readable automation state for people and browser controls\./u);
  71     assert.match(statusViewUiHtml, /HTML endpoint: <strong>\/v1\/status\/ui<\/strong>/u);
  72 
  73+    const appResponse = await fetch(`${baseUrl}/app`);
  74+    assert.equal(appResponse.status, 200);
  75+    assert.equal(appResponse.headers.get("cache-control"), "no-store");
  76+    assert.equal(appResponse.headers.get("content-type"), "text/html; charset=utf-8");
  77+    const appHtml = await appResponse.text();
  78+    assert.match(appHtml, /<div id="app"><\/div>/u);
  79+    const appAssetMatch = appHtml.match(/\/app\/assets\/[^"' )]+/u);
  80+    assert.ok(appAssetMatch);
  81+
  82+    const appControlResponse = await fetch(`${baseUrl}/app/control`);
  83+    assert.equal(appControlResponse.status, 200);
  84+    assert.equal(await appControlResponse.text(), appHtml);
  85+
  86+    const appAssetResponse = await fetch(`${baseUrl}${appAssetMatch[0]}`);
  87+    assert.equal(appAssetResponse.status, 200);
  88+    assert.equal(appAssetResponse.headers.get("cache-control"), "public, max-age=31536000, immutable");
  89+    assert.ok(appAssetResponse.headers.get("content-type"));
  90+
  91     const codexStatusResponse = await fetch(`${baseUrl}/v1/codex`);
  92     assert.equal(codexStatusResponse.status, 200);
  93     const codexStatusPayload = await codexStatusResponse.json();
  94@@ -7963,6 +8021,82 @@ test("handleConductorHttpRequest serves artifact files and robots.txt", async ()
  95   });
  96 });
  97 
  98+test("handleConductorHttpRequest serves conductor-ui shell assets and history fallback", async () => {
  99+  const { repository, snapshot } = await createLocalApiFixture();
 100+
 101+  await withConductorUiDistFixture(async ({ assetFile, uiDistDir }) => {
 102+    const context = {
 103+      repository,
 104+      snapshotLoader: () => snapshot,
 105+      uiDistDir
 106+    };
 107+
 108+    const appResponse = await handleConductorHttpRequest(
 109+      {
 110+        method: "GET",
 111+        path: "/app"
 112+      },
 113+      context
 114+    );
 115+    assert.equal(appResponse.status, 200);
 116+    assert.equal(appResponse.headers["cache-control"], "no-store");
 117+    assert.equal(appResponse.headers["content-type"], "text/html; charset=utf-8");
 118+    const appHtml = readBinaryBodyText(appResponse);
 119+    assert.match(appHtml, /Conductor UI Shell Fixture/u);
 120+
 121+    const appSlashResponse = await handleConductorHttpRequest(
 122+      {
 123+        method: "GET",
 124+        path: "/app/"
 125+      },
 126+      context
 127+    );
 128+    assert.equal(readBinaryBodyText(appSlashResponse), appHtml);
 129+
 130+    const historyResponse = await handleConductorHttpRequest(
 131+      {
 132+        method: "GET",
 133+        path: "/app/control"
 134+      },
 135+      context
 136+    );
 137+    assert.equal(historyResponse.status, 200);
 138+    assert.equal(readBinaryBodyText(historyResponse), appHtml);
 139+
 140+    const assetResponse = await handleConductorHttpRequest(
 141+      {
 142+        method: "GET",
 143+        path: `/app/assets/${assetFile}`
 144+      },
 145+      context
 146+    );
 147+    assert.equal(assetResponse.status, 200);
 148+    assert.equal(assetResponse.headers["cache-control"], "public, max-age=31536000, immutable");
 149+    assert.equal(assetResponse.headers["content-type"], "text/javascript; charset=utf-8");
 150+    assert.equal(readBinaryBodyText(assetResponse), "console.log('fixture ui shell');\n");
 151+
 152+    const missingAssetResponse = await handleConductorHttpRequest(
 153+      {
 154+        method: "GET",
 155+        path: "/app/assets/missing.js"
 156+      },
 157+      context
 158+    );
 159+    assert.equal(missingAssetResponse.status, 404);
 160+    assert.equal(parseJsonBody(missingAssetResponse).error, "not_found");
 161+
 162+    const assetRootResponse = await handleConductorHttpRequest(
 163+      {
 164+        method: "GET",
 165+        path: "/app/assets"
 166+      },
 167+      context
 168+    );
 169+    assert.equal(assetRootResponse.status, 404);
 170+    assert.equal(parseJsonBody(assetRootResponse).error, "not_found");
 171+  });
 172+});
 173+
 174 test("handleConductorHttpRequest serves code files and blocks unsafe paths", async () => {
 175   const { repository, snapshot } = await createLocalApiFixture();
 176   const codeRootDir = mkdtempSync(join(tmpdir(), "baa-conductor-code-route-"));
 177diff --git a/apps/conductor-daemon/src/local-api.ts b/apps/conductor-daemon/src/local-api.ts
 178index 58aa47a437480f6c8fcfce89c1d832f279c41a6e..1a2370d743d09a829aa0da6ab91f4122d0378c64 100644
 179--- a/apps/conductor-daemon/src/local-api.ts
 180+++ b/apps/conductor-daemon/src/local-api.ts
 181@@ -1,6 +1,7 @@
 182 import * as nodeFs from "node:fs";
 183 import { randomUUID } from "node:crypto";
 184 import * as nodePath from "node:path";
 185+import { fileURLToPath } from "node:url";
 186 import {
 187   buildArtifactRelativePath,
 188   buildArtifactPublicUrl,
 189@@ -120,11 +121,34 @@ const HOST_OPERATIONS_ROUTE_IDS = new Set(["host.exec", "host.files.read", "host
 190 const HOST_OPERATIONS_AUTH_HEADER = "Authorization: Bearer <BAA_SHARED_TOKEN>";
 191 const HOST_OPERATIONS_WWW_AUTHENTICATE = 'Bearer realm="baa-conductor-host-ops"';
 192 const DEFAULT_CODE_ROOT_DIR = "/Users/george/code/";
 193+const DEFAULT_CONDUCTOR_UI_DIST_DIR = resolve(fileURLToPath(new URL("../../conductor-ui/dist/", import.meta.url)));
 194 const CODE_ROUTE_CONTENT_TYPE = "text/plain; charset=utf-8";
 195 const STATUS_VIEW_HTML_HEADERS = {
 196   "cache-control": "no-store",
 197   "content-type": "text/html; charset=utf-8"
 198 } as const;
 199+const CONDUCTOR_UI_ENTRY_FILE = "index.html";
 200+const CONDUCTOR_UI_HTML_HEADERS = {
 201+  "cache-control": "no-store",
 202+  "content-type": "text/html; charset=utf-8"
 203+} as const;
 204+const CONDUCTOR_UI_ASSET_CACHE_CONTROL = "public, max-age=31536000, immutable";
 205+const CONDUCTOR_UI_ASSET_CONTENT_TYPES: Record<string, string> = {
 206+  ".css": "text/css; charset=utf-8",
 207+  ".ico": "image/x-icon",
 208+  ".jpeg": "image/jpeg",
 209+  ".jpg": "image/jpeg",
 210+  ".js": "text/javascript; charset=utf-8",
 211+  ".json": "application/json; charset=utf-8",
 212+  ".map": "application/json; charset=utf-8",
 213+  ".png": "image/png",
 214+  ".svg": "image/svg+xml",
 215+  ".ttf": "font/ttf",
 216+  ".txt": "text/plain; charset=utf-8",
 217+  ".webp": "image/webp",
 218+  ".woff": "font/woff",
 219+  ".woff2": "font/woff2"
 220+};
 221 const ROBOTS_TXT_BODY = "User-agent: *\nAllow: /artifact/";
 222 const SSE_RESPONSE_HEADERS = {
 223   "cache-control": "no-store",
 224@@ -308,6 +332,7 @@ interface LocalApiRequestContext {
 225   requestId: string;
 226   sharedToken: string | null;
 227   snapshotLoader: () => ConductorRuntimeApiSnapshot;
 228+  uiDistDir: string;
 229   url: URL;
 230 }
 231 
 232@@ -360,6 +385,7 @@ export interface ConductorLocalApiContext {
 233   repository: ControlPlaneRepository | null;
 234   sharedToken?: string | null;
 235   snapshotLoader: () => ConductorRuntimeApiSnapshot;
 236+  uiDistDir?: string | null;
 237   version?: string | null;
 238 }
 239 
 240@@ -445,6 +471,30 @@ const LOCAL_API_ROUTES: LocalApiRouteDefinition[] = [
 241     pathPattern: "/artifact/:artifact_scope/:artifact_file",
 242     summary: "读取 artifact 静态文件"
 243   },
 244+  {
 245+    id: "service.app.shell",
 246+    exposeInDescribe: false,
 247+    kind: "read",
 248+    method: "GET",
 249+    pathPattern: "/app",
 250+    summary: "读取 conductor-ui SPA 入口"
 251+  },
 252+  {
 253+    id: "service.app.assets",
 254+    exposeInDescribe: false,
 255+    kind: "read",
 256+    method: "GET",
 257+    pathPattern: "/app/assets/:app_asset_path*",
 258+    summary: "读取 conductor-ui 构建后的静态资源"
 259+  },
 260+  {
 261+    id: "service.app.history",
 262+    exposeInDescribe: false,
 263+    kind: "read",
 264+    method: "GET",
 265+    pathPattern: "/app/:app_path*",
 266+    summary: "为 conductor-ui 提供 SPA history fallback"
 267+  },
 268   {
 269     id: "service.code.read",
 270     kind: "read",
 271@@ -6572,6 +6622,114 @@ function buildPlainTextBinaryResponse(status: number, body: string): ConductorHt
 272   });
 273 }
 274 
 275+function resolveUiDistDir(value: string | null | undefined): string {
 276+  return resolve(normalizeOptionalString(value) ?? DEFAULT_CONDUCTOR_UI_DIST_DIR);
 277+}
 278+
 279+function buildUiUnavailableError(uiDistDir: string): LocalApiHttpError {
 280+  return new LocalApiHttpError(
 281+    503,
 282+    "ui_not_available",
 283+    `Conductor UI build output "${uiDistDir}" is not available. Run "pnpm -C apps/conductor-ui build" first.`
 284+  );
 285+}
 286+
 287+function getUiDistRealPath(uiDistDir: string): string {
 288+  try {
 289+    return realpathSync(uiDistDir);
 290+  } catch (error) {
 291+    if (isMissingFileError(error)) {
 292+      throw buildUiUnavailableError(uiDistDir);
 293+    }
 294+
 295+    throw error;
 296+  }
 297+}
 298+
 299+function readUiEntryFile(uiDistDir: string): Uint8Array {
 300+  const uiDistRealPath = getUiDistRealPath(uiDistDir);
 301+
 302+  try {
 303+    return readFileSync(join(uiDistRealPath, CONDUCTOR_UI_ENTRY_FILE));
 304+  } catch (error) {
 305+    if (isMissingFileError(error)) {
 306+      throw buildUiUnavailableError(uiDistDir);
 307+    }
 308+
 309+    throw error;
 310+  }
 311+}
 312+
 313+function buildUiAssetNotFoundError(pathname: string): LocalApiHttpError {
 314+  return new LocalApiHttpError(404, "not_found", `UI asset "${normalizePathname(pathname)}" was not found.`);
 315+}
 316+
 317+function assertAllowedUiAssetPath(assetPath: string, pathname: string): void {
 318+  if (assetPath.includes("\u0000") || assetPath.includes("\\") || assetPath.startsWith("/")) {
 319+    throw buildUiAssetNotFoundError(pathname);
 320+  }
 321+
 322+  const segments = assetPath.split("/");
 323+
 324+  if (segments.some((segment) => segment === "" || segment === "..")) {
 325+    throw buildUiAssetNotFoundError(pathname);
 326+  }
 327+}
 328+
 329+function resolveUiAssetFilePath(uiDistDir: string, assetPath: string, pathname: string): string {
 330+  if (assetPath.trim() === "") {
 331+    throw buildUiAssetNotFoundError(pathname);
 332+  }
 333+
 334+  assertAllowedUiAssetPath(assetPath, pathname);
 335+  const uiDistRealPath = getUiDistRealPath(uiDistDir);
 336+  const assetsRoot = resolve(uiDistRealPath, "assets");
 337+
 338+  try {
 339+    const resolvedPath = realpathSync(resolve(assetsRoot, assetPath));
 340+
 341+    if (isPathOutsideRoot(uiDistRealPath, resolvedPath)) {
 342+      throw buildUiAssetNotFoundError(pathname);
 343+    }
 344+
 345+    return resolvedPath;
 346+  } catch (error) {
 347+    if (error instanceof LocalApiHttpError) {
 348+      throw error;
 349+    }
 350+
 351+    if (isMissingFileError(error)) {
 352+      throw buildUiAssetNotFoundError(pathname);
 353+    }
 354+
 355+    throw error;
 356+  }
 357+}
 358+
 359+function getUiAssetContentType(filePath: string): string {
 360+  return CONDUCTOR_UI_ASSET_CONTENT_TYPES[extname(filePath).toLowerCase()] ?? "application/octet-stream";
 361+}
 362+
 363+async function handleConductorUiEntryRead(context: LocalApiRequestContext): Promise<ConductorHttpResponse> {
 364+  return binaryResponse(200, readUiEntryFile(context.uiDistDir), {
 365+    ...CONDUCTOR_UI_HTML_HEADERS
 366+  });
 367+}
 368+
 369+async function handleConductorUiAssetRead(context: LocalApiRequestContext): Promise<ConductorHttpResponse> {
 370+  const assetPath = context.params.app_asset_path ?? "";
 371+  const resolvedPath = resolveUiAssetFilePath(context.uiDistDir, assetPath, context.url.pathname);
 372+
 373+  return binaryResponse(200, readFileSync(resolvedPath), {
 374+    "cache-control": CONDUCTOR_UI_ASSET_CACHE_CONTROL,
 375+    "content-type": getUiAssetContentType(resolvedPath)
 376+  });
 377+}
 378+
 379+async function handleConductorUiHistoryRead(context: LocalApiRequestContext): Promise<ConductorHttpResponse> {
 380+  return handleConductorUiEntryRead(context);
 381+}
 382+
 383 function resolveCodeRootDir(value: string | null | undefined): string {
 384   return resolve(normalizeOptionalString(value) ?? DEFAULT_CODE_ROOT_DIR);
 385 }
 386@@ -7683,6 +7841,12 @@ async function dispatchBusinessRoute(
 387       return handleRobotsRead();
 388     case "service.artifact.read":
 389       return handleArtifactRead(context);
 390+    case "service.app.shell":
 391+      return handleConductorUiEntryRead(context);
 392+    case "service.app.assets":
 393+      return handleConductorUiAssetRead(context);
 394+    case "service.app.history":
 395+      return handleConductorUiHistoryRead(context);
 396     case "service.code.read":
 397       return handleCodeRead(context);
 398     case "service.health":
 399@@ -7982,6 +8146,7 @@ export async function handleConductorHttpRequest(
 400         requestId,
 401         sharedToken: normalizeOptionalString(context.sharedToken),
 402         snapshotLoader: context.snapshotLoader,
 403+        uiDistDir: resolveUiDistDir(context.uiDistDir),
 404         url
 405       },
 406       version
 407diff --git a/apps/conductor-ui/index.html b/apps/conductor-ui/index.html
 408new file mode 100644
 409index 0000000000000000000000000000000000000000..509a7f85dc3fb1f4e15de3b9d25a41e72fc6be41
 410--- /dev/null
 411+++ b/apps/conductor-ui/index.html
 412@@ -0,0 +1,18 @@
 413+<!doctype html>
 414+<html lang="zh-CN">
 415+  <head>
 416+    <meta charset="UTF-8" />
 417+    <meta
 418+      name="viewport"
 419+      content="width=device-width, initial-scale=1.0"
 420+    />
 421+    <title>Conductor UI</title>
 422+  </head>
 423+  <body>
 424+    <div id="app"></div>
 425+    <script
 426+      type="module"
 427+      src="/src/main.ts"
 428+    ></script>
 429+  </body>
 430+</html>
 431diff --git a/apps/conductor-ui/package.json b/apps/conductor-ui/package.json
 432new file mode 100644
 433index 0000000000000000000000000000000000000000..e30d5abb0c19168a46f30444deddf7ef6cfef334
 434--- /dev/null
 435+++ b/apps/conductor-ui/package.json
 436@@ -0,0 +1,23 @@
 437+{
 438+  "name": "@baa-conductor/conductor-ui",
 439+  "private": true,
 440+  "type": "module",
 441+  "scripts": {
 442+    "build": "vite build",
 443+    "dev": "vite",
 444+    "preview": "vite preview",
 445+    "typecheck": "vue-tsc --noEmit",
 446+    "test": "pnpm run typecheck",
 447+    "smoke": "pnpm run build"
 448+  },
 449+  "dependencies": {
 450+    "vue": "^3.5.18",
 451+    "vue-router": "^4.5.1"
 452+  },
 453+  "devDependencies": {
 454+    "@vitejs/plugin-vue": "^6.0.1",
 455+    "typescript": "^5.8.2",
 456+    "vite": "^7.1.1",
 457+    "vue-tsc": "^3.0.3"
 458+  }
 459+}
 460diff --git a/apps/conductor-ui/src/App.vue b/apps/conductor-ui/src/App.vue
 461new file mode 100644
 462index 0000000000000000000000000000000000000000..543cfdbab78c08f07ccec245e955b5d84ad0f7ab
 463--- /dev/null
 464+++ b/apps/conductor-ui/src/App.vue
 465@@ -0,0 +1,7 @@
 466+<template>
 467+  <RouterView />
 468+</template>
 469+
 470+<script setup lang="ts">
 471+import { RouterView } from "vue-router";
 472+</script>
 473diff --git a/apps/conductor-ui/src/env.d.ts b/apps/conductor-ui/src/env.d.ts
 474new file mode 100644
 475index 0000000000000000000000000000000000000000..158d2b152e84c89862547e85a0a3de20724e046f
 476--- /dev/null
 477+++ b/apps/conductor-ui/src/env.d.ts
 478@@ -0,0 +1,8 @@
 479+/// <reference types="vite/client" />
 480+
 481+declare module "*.vue" {
 482+  import type { DefineComponent } from "vue";
 483+
 484+  const component: DefineComponent<Record<string, never>, Record<string, never>, unknown>;
 485+  export default component;
 486+}
 487diff --git a/apps/conductor-ui/src/layouts/WorkbenchLayout.vue b/apps/conductor-ui/src/layouts/WorkbenchLayout.vue
 488new file mode 100644
 489index 0000000000000000000000000000000000000000..47b4c56213b333ea899ca9eb94ccb44854879cd7
 490--- /dev/null
 491+++ b/apps/conductor-ui/src/layouts/WorkbenchLayout.vue
 492@@ -0,0 +1,90 @@
 493+<template>
 494+  <div class="shell-page">
 495+    <div class="shell-backdrop shell-backdrop-left"></div>
 496+    <div class="shell-backdrop shell-backdrop-right"></div>
 497+    <main class="shell-frame">
 498+      <aside class="shell-sidebar">
 499+        <div class="shell-brand">
 500+          <p class="eyebrow">Conductor UI Shell</p>
 501+          <h1>正式工作台入口</h1>
 502+          <p class="lede">
 503+            `/app` 已经作为同源入口挂到 conductor。当前版本只提供可持续演进的壳和导航骨架。
 504+          </p>
 505+        </div>
 506+
 507+        <div class="session-card">
 508+          <span class="status-badge status-badge-warn">未接登录态</span>
 509+          <p class="session-copy">
 510+            首版不做浏览器 session 鉴权,后续会在 `/app/login` 和受保护写接口中补齐。
 511+          </p>
 512+        </div>
 513+
 514+        <nav class="nav-list">
 515+          <RouterLink
 516+            v-for="item in navItems"
 517+            :key="item.to"
 518+            :to="item.to"
 519+            class="nav-item"
 520+            :class="{ 'nav-item-active': route.path === item.to }"
 521+          >
 522+            <span class="nav-label">{{ item.label }}</span>
 523+            <span class="nav-note">{{ item.note }}</span>
 524+          </RouterLink>
 525+        </nav>
 526+
 527+        <section class="sidebar-panel">
 528+          <p class="sidebar-title">当前范围</p>
 529+          <ul class="sidebar-list">
 530+            <li>Vue 3 + Vite + TypeScript + Vue Router</li>
 531+            <li>`/app` 同源静态托管</li>
 532+            <li>Control / Channels / Login 占位路由</li>
 533+          </ul>
 534+        </section>
 535+      </aside>
 536+
 537+      <section class="shell-main">
 538+        <header class="hero-card">
 539+          <div>
 540+            <p class="eyebrow">Workspace</p>
 541+            <h2>{{ currentItem.label }}</h2>
 542+          </div>
 543+          <div class="hero-meta">
 544+            <span class="status-badge status-badge-info">Phase 1</span>
 545+            <span class="status-badge status-badge-muted">Shell Ready</span>
 546+          </div>
 547+        </header>
 548+
 549+        <RouterView />
 550+      </section>
 551+    </main>
 552+  </div>
 553+</template>
 554+
 555+<script setup lang="ts">
 556+import { computed } from "vue";
 557+import { RouterLink, RouterView, useRoute } from "vue-router";
 558+
 559+const route = useRoute();
 560+
 561+const navItems = [
 562+  {
 563+    label: "Control",
 564+    note: "系统态与 browser 运维入口",
 565+    to: "/control"
 566+  },
 567+  {
 568+    label: "Channels",
 569+    note: "未来正式业务线程入口",
 570+    to: "/channels"
 571+  },
 572+  {
 573+    label: "Login",
 574+    note: "预留浏览器 session 登录页",
 575+    to: "/login"
 576+  }
 577+] as const;
 578+
 579+const currentItem = computed(
 580+  () => navItems.find((item) => route.path === item.to) ?? navItems[0]
 581+);
 582+</script>
 583diff --git a/apps/conductor-ui/src/main.ts b/apps/conductor-ui/src/main.ts
 584new file mode 100644
 585index 0000000000000000000000000000000000000000..de370f30f625d260c8d695de6ae00b8089a21b27
 586--- /dev/null
 587+++ b/apps/conductor-ui/src/main.ts
 588@@ -0,0 +1,7 @@
 589+import { createApp } from "vue";
 590+
 591+import App from "./App.vue";
 592+import router from "./routes";
 593+import "./style.css";
 594+
 595+createApp(App).use(router).mount("#app");
 596diff --git a/apps/conductor-ui/src/routes/index.ts b/apps/conductor-ui/src/routes/index.ts
 597new file mode 100644
 598index 0000000000000000000000000000000000000000..ab6707085940fcf02581c96d965f53dfe3cc29bb
 599--- /dev/null
 600+++ b/apps/conductor-ui/src/routes/index.ts
 601@@ -0,0 +1,41 @@
 602+import { createRouter, createWebHistory } from "vue-router";
 603+
 604+import WorkbenchLayout from "../layouts/WorkbenchLayout.vue";
 605+import ChannelsPlaceholderView from "../views/ChannelsPlaceholderView.vue";
 606+import ControlOverviewView from "../views/ControlOverviewView.vue";
 607+import LoginPlaceholderView from "../views/LoginPlaceholderView.vue";
 608+
 609+const router = createRouter({
 610+  history: createWebHistory("/app/"),
 611+  routes: [
 612+    {
 613+      path: "/",
 614+      component: WorkbenchLayout,
 615+      children: [
 616+        {
 617+          path: "",
 618+          redirect: {
 619+            name: "control"
 620+          }
 621+        },
 622+        {
 623+          path: "control",
 624+          name: "control",
 625+          component: ControlOverviewView
 626+        },
 627+        {
 628+          path: "channels",
 629+          name: "channels",
 630+          component: ChannelsPlaceholderView
 631+        },
 632+        {
 633+          path: "login",
 634+          name: "login",
 635+          component: LoginPlaceholderView
 636+        }
 637+      ]
 638+    }
 639+  ]
 640+});
 641+
 642+export default router;
 643diff --git a/apps/conductor-ui/src/style.css b/apps/conductor-ui/src/style.css
 644new file mode 100644
 645index 0000000000000000000000000000000000000000..7e1aa0f64a1d7ab97e20d827b5ed274265305908
 646--- /dev/null
 647+++ b/apps/conductor-ui/src/style.css
 648@@ -0,0 +1,289 @@
 649+:root {
 650+  color: #142231;
 651+  background:
 652+    radial-gradient(circle at top left, rgba(255, 184, 92, 0.28), transparent 28%),
 653+    radial-gradient(circle at top right, rgba(22, 119, 122, 0.2), transparent 32%),
 654+    linear-gradient(180deg, #fffaf2 0%, #f3f8f7 100%);
 655+  font-family: "Avenir Next", "Segoe UI", sans-serif;
 656+  font-synthesis: none;
 657+  line-height: 1.5;
 658+  text-rendering: optimizeLegibility;
 659+  -webkit-font-smoothing: antialiased;
 660+  -moz-osx-font-smoothing: grayscale;
 661+}
 662+
 663+* {
 664+  box-sizing: border-box;
 665+}
 666+
 667+html,
 668+body,
 669+#app {
 670+  margin: 0;
 671+  min-height: 100%;
 672+}
 673+
 674+body {
 675+  min-width: 320px;
 676+}
 677+
 678+a {
 679+  color: inherit;
 680+  text-decoration: none;
 681+}
 682+
 683+.shell-page {
 684+  min-height: 100vh;
 685+  overflow: hidden;
 686+  position: relative;
 687+}
 688+
 689+.shell-backdrop {
 690+  filter: blur(18px);
 691+  position: absolute;
 692+  transform: rotate(-6deg);
 693+}
 694+
 695+.shell-backdrop-left {
 696+  background: rgba(244, 179, 84, 0.16);
 697+  border-radius: 48px;
 698+  height: 14rem;
 699+  left: -4rem;
 700+  top: 5rem;
 701+  width: 14rem;
 702+}
 703+
 704+.shell-backdrop-right {
 705+  background: rgba(20, 107, 110, 0.12);
 706+  border-radius: 48px;
 707+  height: 20rem;
 708+  right: -4rem;
 709+  top: 12rem;
 710+  width: 20rem;
 711+}
 712+
 713+.shell-frame {
 714+  display: grid;
 715+  gap: 1.5rem;
 716+  grid-template-columns: minmax(16rem, 22rem) minmax(0, 1fr);
 717+  margin: 0 auto;
 718+  max-width: 86rem;
 719+  padding: 2rem;
 720+  position: relative;
 721+}
 722+
 723+.shell-sidebar,
 724+.shell-main {
 725+  backdrop-filter: blur(14px);
 726+  background: rgba(255, 255, 255, 0.72);
 727+  border: 1px solid rgba(20, 34, 49, 0.08);
 728+  border-radius: 1.75rem;
 729+  box-shadow: 0 24px 60px rgba(34, 56, 72, 0.08);
 730+}
 731+
 732+.shell-sidebar {
 733+  display: flex;
 734+  flex-direction: column;
 735+  gap: 1.25rem;
 736+  padding: 1.5rem;
 737+}
 738+
 739+.shell-main {
 740+  display: flex;
 741+  flex-direction: column;
 742+  gap: 1.25rem;
 743+  padding: 1.5rem;
 744+}
 745+
 746+.shell-brand h1,
 747+.hero-card h2,
 748+.content-card h3 {
 749+  font-family: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", serif;
 750+  letter-spacing: -0.03em;
 751+  margin: 0;
 752+}
 753+
 754+.shell-brand h1 {
 755+  font-size: clamp(2rem, 3vw, 2.8rem);
 756+  line-height: 1.05;
 757+}
 758+
 759+.hero-card h2 {
 760+  font-size: clamp(1.8rem, 2.6vw, 2.4rem);
 761+}
 762+
 763+.eyebrow {
 764+  color: #0f696d;
 765+  font-size: 0.78rem;
 766+  font-weight: 700;
 767+  letter-spacing: 0.14em;
 768+  margin: 0 0 0.5rem;
 769+  text-transform: uppercase;
 770+}
 771+
 772+.lede,
 773+.content-copy,
 774+.session-copy {
 775+  color: #425566;
 776+  margin: 0;
 777+}
 778+
 779+.session-card,
 780+.sidebar-panel,
 781+.content-card,
 782+.hero-card {
 783+  border: 1px solid rgba(20, 34, 49, 0.08);
 784+  border-radius: 1.25rem;
 785+  padding: 1.1rem 1.2rem;
 786+}
 787+
 788+.session-card {
 789+  background: linear-gradient(135deg, rgba(255, 236, 208, 0.8), rgba(255, 255, 255, 0.7));
 790+}
 791+
 792+.sidebar-panel,
 793+.content-card,
 794+.hero-card {
 795+  background: rgba(255, 255, 255, 0.7);
 796+}
 797+
 798+.sidebar-title,
 799+.card-title {
 800+  font-size: 0.95rem;
 801+  font-weight: 700;
 802+  margin: 0 0 0.8rem;
 803+}
 804+
 805+.sidebar-list,
 806+.detail-list {
 807+  color: #425566;
 808+  display: grid;
 809+  gap: 0.55rem;
 810+  margin: 0;
 811+  padding-left: 1.2rem;
 812+}
 813+
 814+.nav-list {
 815+  display: grid;
 816+  gap: 0.7rem;
 817+}
 818+
 819+.nav-item {
 820+  background: rgba(245, 249, 248, 0.72);
 821+  border: 1px solid rgba(20, 34, 49, 0.06);
 822+  border-radius: 1rem;
 823+  display: block;
 824+  padding: 0.95rem 1rem;
 825+  transition:
 826+    transform 140ms ease,
 827+    border-color 140ms ease,
 828+    background 140ms ease;
 829+}
 830+
 831+.nav-item:hover {
 832+  border-color: rgba(15, 105, 109, 0.28);
 833+  transform: translateY(-1px);
 834+}
 835+
 836+.nav-item-active {
 837+  background: linear-gradient(135deg, rgba(15, 105, 109, 0.12), rgba(244, 179, 84, 0.08));
 838+  border-color: rgba(15, 105, 109, 0.26);
 839+}
 840+
 841+.nav-label {
 842+  display: block;
 843+  font-size: 1rem;
 844+  font-weight: 700;
 845+}
 846+
 847+.nav-note {
 848+  color: #587083;
 849+  display: block;
 850+  font-size: 0.88rem;
 851+  margin-top: 0.2rem;
 852+}
 853+
 854+.hero-card {
 855+  align-items: center;
 856+  display: flex;
 857+  justify-content: space-between;
 858+  gap: 1rem;
 859+}
 860+
 861+.hero-meta {
 862+  display: flex;
 863+  flex-wrap: wrap;
 864+  gap: 0.55rem;
 865+}
 866+
 867+.status-badge {
 868+  border-radius: 999px;
 869+  display: inline-flex;
 870+  font-size: 0.78rem;
 871+  font-weight: 700;
 872+  padding: 0.34rem 0.7rem;
 873+}
 874+
 875+.status-badge-info {
 876+  background: rgba(15, 105, 109, 0.12);
 877+  color: #0f696d;
 878+}
 879+
 880+.status-badge-muted {
 881+  background: rgba(66, 85, 102, 0.12);
 882+  color: #425566;
 883+}
 884+
 885+.status-badge-warn {
 886+  background: rgba(244, 179, 84, 0.22);
 887+  color: #8b5c12;
 888+}
 889+
 890+.content-grid {
 891+  display: grid;
 892+  gap: 1rem;
 893+  grid-template-columns: repeat(2, minmax(0, 1fr));
 894+}
 895+
 896+.content-card-primary {
 897+  background: linear-gradient(135deg, rgba(255, 246, 230, 0.92), rgba(237, 247, 245, 0.92));
 898+  grid-column: 1 / -1;
 899+}
 900+
 901+.metric-list {
 902+  display: grid;
 903+  gap: 0.85rem;
 904+}
 905+
 906+.metric-item {
 907+  align-items: baseline;
 908+  display: flex;
 909+  gap: 0.8rem;
 910+  justify-content: space-between;
 911+}
 912+
 913+.metric-item strong {
 914+  font-size: 1.02rem;
 915+}
 916+
 917+.metric-label {
 918+  color: #587083;
 919+  font-size: 0.9rem;
 920+}
 921+
 922+@media (max-width: 860px) {
 923+  .shell-frame {
 924+    grid-template-columns: minmax(0, 1fr);
 925+    padding: 1rem;
 926+  }
 927+
 928+  .hero-card,
 929+  .metric-item {
 930+    align-items: flex-start;
 931+    flex-direction: column;
 932+  }
 933+
 934+  .content-grid {
 935+    grid-template-columns: minmax(0, 1fr);
 936+  }
 937+}
 938diff --git a/apps/conductor-ui/src/views/ChannelsPlaceholderView.vue b/apps/conductor-ui/src/views/ChannelsPlaceholderView.vue
 939new file mode 100644
 940index 0000000000000000000000000000000000000000..b8a51466069576abbc71057d8d412c962733f31c
 941--- /dev/null
 942+++ b/apps/conductor-ui/src/views/ChannelsPlaceholderView.vue
 943@@ -0,0 +1,20 @@
 944+<template>
 945+  <section class="content-grid">
 946+    <article class="content-card content-card-primary">
 947+      <p class="eyebrow">Channels</p>
 948+      <h3>频道会成为人类和多平台成员协作的正式线程主体。</h3>
 949+      <p class="content-copy">
 950+        这个阶段先占住 `/app/channels` 路由,不把插件调试页误导成正式入口。
 951+      </p>
 952+    </article>
 953+
 954+    <article class="content-card">
 955+      <p class="card-title">后续任务</p>
 956+      <ul class="detail-list">
 957+        <li>只读频道列表与详情页</li>
 958+        <li>消息、artifact、execution 线程聚合</li>
 959+        <li>成员管理和正式写路径</li>
 960+      </ul>
 961+    </article>
 962+  </section>
 963+</template>
 964diff --git a/apps/conductor-ui/src/views/ControlOverviewView.vue b/apps/conductor-ui/src/views/ControlOverviewView.vue
 965new file mode 100644
 966index 0000000000000000000000000000000000000000..ef4ebdff02a0c079818a2b22c0860cc5d3390482
 967--- /dev/null
 968+++ b/apps/conductor-ui/src/views/ControlOverviewView.vue
 969@@ -0,0 +1,40 @@
 970+<template>
 971+  <section class="content-grid">
 972+    <article class="content-card content-card-primary">
 973+      <p class="eyebrow">Control</p>
 974+      <h3>首版工作区会优先承载系统总览和浏览器运维。</h3>
 975+      <p class="content-copy">
 976+        当前壳先固定信息架构和视觉层级,后续直接接入 `/v1/system/state`、`/v1/browser`、
 977+        `/v1/controllers`、`/v1/tasks`、`/v1/runs`、`/v1/codex`。
 978+      </p>
 979+    </article>
 980+
 981+    <article class="content-card">
 982+      <p class="card-title">计划接入</p>
 983+      <ul class="detail-list">
 984+        <li>系统模式、leader 与 queue depth</li>
 985+        <li>browser bridge、shell runtime、登录态记录</li>
 986+        <li>renewal conversations、links、jobs</li>
 987+        <li>tasks / runs / codex sessions 概览</li>
 988+      </ul>
 989+    </article>
 990+
 991+    <article class="content-card">
 992+      <p class="card-title">当前状态</p>
 993+      <div class="metric-list">
 994+        <div class="metric-item">
 995+          <span class="metric-label">入口</span>
 996+          <strong>/app/control</strong>
 997+        </div>
 998+        <div class="metric-item">
 999+          <span class="metric-label">读接口</span>
1000+          <strong>待接入</strong>
1001+        </div>
1002+        <div class="metric-item">
1003+          <span class="metric-label">写接口</span>
1004+          <strong>鉴权后开放</strong>
1005+        </div>
1006+      </div>
1007+    </article>
1008+  </section>
1009+</template>
1010diff --git a/apps/conductor-ui/src/views/LoginPlaceholderView.vue b/apps/conductor-ui/src/views/LoginPlaceholderView.vue
1011new file mode 100644
1012index 0000000000000000000000000000000000000000..f401f74fa1347ce23598e44408227a51c76fbc24
1013--- /dev/null
1014+++ b/apps/conductor-ui/src/views/LoginPlaceholderView.vue
1015@@ -0,0 +1,20 @@
1016+<template>
1017+  <section class="content-grid">
1018+    <article class="content-card content-card-primary">
1019+      <p class="eyebrow">Login</p>
1020+      <h3>浏览器 session 登录页将在下一阶段落地。</h3>
1021+      <p class="content-copy">
1022+        当前页面只明确声明一件事:这个工作台还没有接入正式登录态,不应被误判成可直接执行写操作的管理面。
1023+      </p>
1024+    </article>
1025+
1026+    <article class="content-card">
1027+      <p class="card-title">设计约束</p>
1028+      <ul class="detail-list">
1029+        <li>不在浏览器端保存长期共享 token</li>
1030+        <li>优先使用短期 session cookie</li>
1031+        <li>区分 `browser_admin` 与 `readonly`</li>
1032+      </ul>
1033+    </article>
1034+  </section>
1035+</template>
1036diff --git a/apps/conductor-ui/tsconfig.json b/apps/conductor-ui/tsconfig.json
1037new file mode 100644
1038index 0000000000000000000000000000000000000000..c24b466cf7a7b7f2137a772e61f8f215ed1bcc1c
1039--- /dev/null
1040+++ b/apps/conductor-ui/tsconfig.json
1041@@ -0,0 +1,11 @@
1042+{
1043+  "extends": "../../tsconfig.base.json",
1044+  "compilerOptions": {
1045+    "lib": ["ES2022", "DOM", "DOM.Iterable"],
1046+    "module": "ESNext",
1047+    "moduleResolution": "Bundler",
1048+    "jsx": "preserve",
1049+    "types": ["vite/client"]
1050+  },
1051+  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"]
1052+}
1053diff --git a/apps/conductor-ui/vite.config.ts b/apps/conductor-ui/vite.config.ts
1054new file mode 100644
1055index 0000000000000000000000000000000000000000..ca238eab7a41f2a53f18727635ff4b44fc628327
1056--- /dev/null
1057+++ b/apps/conductor-ui/vite.config.ts
1058@@ -0,0 +1,24 @@
1059+import { defineConfig } from "vite";
1060+import vue from "@vitejs/plugin-vue";
1061+
1062+const CONDUCTOR_API_TARGET = "http://100.71.210.78:4317";
1063+
1064+export default defineConfig({
1065+  base: "/app/",
1066+  plugins: [vue()],
1067+  server: {
1068+    host: "127.0.0.1",
1069+    port: 4318,
1070+    proxy: {
1071+      "/artifact": CONDUCTOR_API_TARGET,
1072+      "/describe": CONDUCTOR_API_TARGET,
1073+      "/health": CONDUCTOR_API_TARGET,
1074+      "/robots.txt": CONDUCTOR_API_TARGET,
1075+      "/v1": CONDUCTOR_API_TARGET,
1076+      "/version": CONDUCTOR_API_TARGET
1077+    }
1078+  },
1079+  build: {
1080+    outDir: "dist"
1081+  }
1082+});
1083diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
1084index c536902b68d285e0c875ffb9cf4c1f3b1adefc86..19be29bc356734af753e96691031962ad5a2c22c 100644
1085--- a/pnpm-lock.yaml
1086+++ b/pnpm-lock.yaml
1087@@ -31,6 +31,28 @@ importers:
1088         specifier: workspace:*
1089         version: link:../../packages/host-ops
1090 
1091+  apps/conductor-ui:
1092+    dependencies:
1093+      vue:
1094+        specifier: ^3.5.18
1095+        version: 3.5.32(typescript@5.9.3)
1096+      vue-router:
1097+        specifier: ^4.5.1
1098+        version: 4.6.4(vue@3.5.32(typescript@5.9.3))
1099+    devDependencies:
1100+      '@vitejs/plugin-vue':
1101+        specifier: ^6.0.1
1102+        version: 6.0.5(vite@7.3.1)(vue@3.5.32(typescript@5.9.3))
1103+      typescript:
1104+        specifier: ^5.8.2
1105+        version: 5.9.3
1106+      vite:
1107+        specifier: ^7.1.1
1108+        version: 7.3.1
1109+      vue-tsc:
1110+        specifier: ^3.0.3
1111+        version: 3.2.6(typescript@5.9.3)
1112+
1113   apps/status-api: {}
1114 
1115   apps/worker-runner: {}
1116@@ -63,11 +85,893 @@ importers:
1117 
1118 packages:
1119 
1120+  '@babel/helper-string-parser@7.27.1':
1121+    resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
1122+    engines: {node: '>=6.9.0'}
1123+
1124+  '@babel/helper-validator-identifier@7.28.5':
1125+    resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
1126+    engines: {node: '>=6.9.0'}
1127+
1128+  '@babel/parser@7.29.2':
1129+    resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==}
1130+    engines: {node: '>=6.0.0'}
1131+    hasBin: true
1132+
1133+  '@babel/types@7.29.0':
1134+    resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
1135+    engines: {node: '>=6.9.0'}
1136+
1137+  '@esbuild/aix-ppc64@0.27.7':
1138+    resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==}
1139+    engines: {node: '>=18'}
1140+    cpu: [ppc64]
1141+    os: [aix]
1142+
1143+  '@esbuild/android-arm64@0.27.7':
1144+    resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==}
1145+    engines: {node: '>=18'}
1146+    cpu: [arm64]
1147+    os: [android]
1148+
1149+  '@esbuild/android-arm@0.27.7':
1150+    resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==}
1151+    engines: {node: '>=18'}
1152+    cpu: [arm]
1153+    os: [android]
1154+
1155+  '@esbuild/android-x64@0.27.7':
1156+    resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==}
1157+    engines: {node: '>=18'}
1158+    cpu: [x64]
1159+    os: [android]
1160+
1161+  '@esbuild/darwin-arm64@0.27.7':
1162+    resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==}
1163+    engines: {node: '>=18'}
1164+    cpu: [arm64]
1165+    os: [darwin]
1166+
1167+  '@esbuild/darwin-x64@0.27.7':
1168+    resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==}
1169+    engines: {node: '>=18'}
1170+    cpu: [x64]
1171+    os: [darwin]
1172+
1173+  '@esbuild/freebsd-arm64@0.27.7':
1174+    resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==}
1175+    engines: {node: '>=18'}
1176+    cpu: [arm64]
1177+    os: [freebsd]
1178+
1179+  '@esbuild/freebsd-x64@0.27.7':
1180+    resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==}
1181+    engines: {node: '>=18'}
1182+    cpu: [x64]
1183+    os: [freebsd]
1184+
1185+  '@esbuild/linux-arm64@0.27.7':
1186+    resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==}
1187+    engines: {node: '>=18'}
1188+    cpu: [arm64]
1189+    os: [linux]
1190+
1191+  '@esbuild/linux-arm@0.27.7':
1192+    resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==}
1193+    engines: {node: '>=18'}
1194+    cpu: [arm]
1195+    os: [linux]
1196+
1197+  '@esbuild/linux-ia32@0.27.7':
1198+    resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==}
1199+    engines: {node: '>=18'}
1200+    cpu: [ia32]
1201+    os: [linux]
1202+
1203+  '@esbuild/linux-loong64@0.27.7':
1204+    resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==}
1205+    engines: {node: '>=18'}
1206+    cpu: [loong64]
1207+    os: [linux]
1208+
1209+  '@esbuild/linux-mips64el@0.27.7':
1210+    resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==}
1211+    engines: {node: '>=18'}
1212+    cpu: [mips64el]
1213+    os: [linux]
1214+
1215+  '@esbuild/linux-ppc64@0.27.7':
1216+    resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==}
1217+    engines: {node: '>=18'}
1218+    cpu: [ppc64]
1219+    os: [linux]
1220+
1221+  '@esbuild/linux-riscv64@0.27.7':
1222+    resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==}
1223+    engines: {node: '>=18'}
1224+    cpu: [riscv64]
1225+    os: [linux]
1226+
1227+  '@esbuild/linux-s390x@0.27.7':
1228+    resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==}
1229+    engines: {node: '>=18'}
1230+    cpu: [s390x]
1231+    os: [linux]
1232+
1233+  '@esbuild/linux-x64@0.27.7':
1234+    resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==}
1235+    engines: {node: '>=18'}
1236+    cpu: [x64]
1237+    os: [linux]
1238+
1239+  '@esbuild/netbsd-arm64@0.27.7':
1240+    resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==}
1241+    engines: {node: '>=18'}
1242+    cpu: [arm64]
1243+    os: [netbsd]
1244+
1245+  '@esbuild/netbsd-x64@0.27.7':
1246+    resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==}
1247+    engines: {node: '>=18'}
1248+    cpu: [x64]
1249+    os: [netbsd]
1250+
1251+  '@esbuild/openbsd-arm64@0.27.7':
1252+    resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==}
1253+    engines: {node: '>=18'}
1254+    cpu: [arm64]
1255+    os: [openbsd]
1256+
1257+  '@esbuild/openbsd-x64@0.27.7':
1258+    resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==}
1259+    engines: {node: '>=18'}
1260+    cpu: [x64]
1261+    os: [openbsd]
1262+
1263+  '@esbuild/openharmony-arm64@0.27.7':
1264+    resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==}
1265+    engines: {node: '>=18'}
1266+    cpu: [arm64]
1267+    os: [openharmony]
1268+
1269+  '@esbuild/sunos-x64@0.27.7':
1270+    resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==}
1271+    engines: {node: '>=18'}
1272+    cpu: [x64]
1273+    os: [sunos]
1274+
1275+  '@esbuild/win32-arm64@0.27.7':
1276+    resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==}
1277+    engines: {node: '>=18'}
1278+    cpu: [arm64]
1279+    os: [win32]
1280+
1281+  '@esbuild/win32-ia32@0.27.7':
1282+    resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==}
1283+    engines: {node: '>=18'}
1284+    cpu: [ia32]
1285+    os: [win32]
1286+
1287+  '@esbuild/win32-x64@0.27.7':
1288+    resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==}
1289+    engines: {node: '>=18'}
1290+    cpu: [x64]
1291+    os: [win32]
1292+
1293+  '@jridgewell/sourcemap-codec@1.5.5':
1294+    resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
1295+
1296+  '@rolldown/pluginutils@1.0.0-rc.2':
1297+    resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==}
1298+
1299+  '@rollup/rollup-android-arm-eabi@4.60.1':
1300+    resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==}
1301+    cpu: [arm]
1302+    os: [android]
1303+
1304+  '@rollup/rollup-android-arm64@4.60.1':
1305+    resolution: {integrity: sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==}
1306+    cpu: [arm64]
1307+    os: [android]
1308+
1309+  '@rollup/rollup-darwin-arm64@4.60.1':
1310+    resolution: {integrity: sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==}
1311+    cpu: [arm64]
1312+    os: [darwin]
1313+
1314+  '@rollup/rollup-darwin-x64@4.60.1':
1315+    resolution: {integrity: sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==}
1316+    cpu: [x64]
1317+    os: [darwin]
1318+
1319+  '@rollup/rollup-freebsd-arm64@4.60.1':
1320+    resolution: {integrity: sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==}
1321+    cpu: [arm64]
1322+    os: [freebsd]
1323+
1324+  '@rollup/rollup-freebsd-x64@4.60.1':
1325+    resolution: {integrity: sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==}
1326+    cpu: [x64]
1327+    os: [freebsd]
1328+
1329+  '@rollup/rollup-linux-arm-gnueabihf@4.60.1':
1330+    resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==}
1331+    cpu: [arm]
1332+    os: [linux]
1333+
1334+  '@rollup/rollup-linux-arm-musleabihf@4.60.1':
1335+    resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==}
1336+    cpu: [arm]
1337+    os: [linux]
1338+
1339+  '@rollup/rollup-linux-arm64-gnu@4.60.1':
1340+    resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==}
1341+    cpu: [arm64]
1342+    os: [linux]
1343+
1344+  '@rollup/rollup-linux-arm64-musl@4.60.1':
1345+    resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==}
1346+    cpu: [arm64]
1347+    os: [linux]
1348+
1349+  '@rollup/rollup-linux-loong64-gnu@4.60.1':
1350+    resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==}
1351+    cpu: [loong64]
1352+    os: [linux]
1353+
1354+  '@rollup/rollup-linux-loong64-musl@4.60.1':
1355+    resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==}
1356+    cpu: [loong64]
1357+    os: [linux]
1358+
1359+  '@rollup/rollup-linux-ppc64-gnu@4.60.1':
1360+    resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==}
1361+    cpu: [ppc64]
1362+    os: [linux]
1363+
1364+  '@rollup/rollup-linux-ppc64-musl@4.60.1':
1365+    resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==}
1366+    cpu: [ppc64]
1367+    os: [linux]
1368+
1369+  '@rollup/rollup-linux-riscv64-gnu@4.60.1':
1370+    resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==}
1371+    cpu: [riscv64]
1372+    os: [linux]
1373+
1374+  '@rollup/rollup-linux-riscv64-musl@4.60.1':
1375+    resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==}
1376+    cpu: [riscv64]
1377+    os: [linux]
1378+
1379+  '@rollup/rollup-linux-s390x-gnu@4.60.1':
1380+    resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==}
1381+    cpu: [s390x]
1382+    os: [linux]
1383+
1384+  '@rollup/rollup-linux-x64-gnu@4.60.1':
1385+    resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==}
1386+    cpu: [x64]
1387+    os: [linux]
1388+
1389+  '@rollup/rollup-linux-x64-musl@4.60.1':
1390+    resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==}
1391+    cpu: [x64]
1392+    os: [linux]
1393+
1394+  '@rollup/rollup-openbsd-x64@4.60.1':
1395+    resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==}
1396+    cpu: [x64]
1397+    os: [openbsd]
1398+
1399+  '@rollup/rollup-openharmony-arm64@4.60.1':
1400+    resolution: {integrity: sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==}
1401+    cpu: [arm64]
1402+    os: [openharmony]
1403+
1404+  '@rollup/rollup-win32-arm64-msvc@4.60.1':
1405+    resolution: {integrity: sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==}
1406+    cpu: [arm64]
1407+    os: [win32]
1408+
1409+  '@rollup/rollup-win32-ia32-msvc@4.60.1':
1410+    resolution: {integrity: sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==}
1411+    cpu: [ia32]
1412+    os: [win32]
1413+
1414+  '@rollup/rollup-win32-x64-gnu@4.60.1':
1415+    resolution: {integrity: sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==}
1416+    cpu: [x64]
1417+    os: [win32]
1418+
1419+  '@rollup/rollup-win32-x64-msvc@4.60.1':
1420+    resolution: {integrity: sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==}
1421+    cpu: [x64]
1422+    os: [win32]
1423+
1424+  '@types/estree@1.0.8':
1425+    resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
1426+
1427+  '@vitejs/plugin-vue@6.0.5':
1428+    resolution: {integrity: sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==}
1429+    engines: {node: ^20.19.0 || >=22.12.0}
1430+    peerDependencies:
1431+      vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
1432+      vue: ^3.2.25
1433+
1434+  '@volar/language-core@2.4.28':
1435+    resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==}
1436+
1437+  '@volar/source-map@2.4.28':
1438+    resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==}
1439+
1440+  '@volar/typescript@2.4.28':
1441+    resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==}
1442+
1443+  '@vue/compiler-core@3.5.32':
1444+    resolution: {integrity: sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==}
1445+
1446+  '@vue/compiler-dom@3.5.32':
1447+    resolution: {integrity: sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==}
1448+
1449+  '@vue/compiler-sfc@3.5.32':
1450+    resolution: {integrity: sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==}
1451+
1452+  '@vue/compiler-ssr@3.5.32':
1453+    resolution: {integrity: sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==}
1454+
1455+  '@vue/devtools-api@6.6.4':
1456+    resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
1457+
1458+  '@vue/language-core@3.2.6':
1459+    resolution: {integrity: sha512-xYYYX3/aVup576tP/23sEUpgiEnujrENaoNRbaozC1/MA9I6EGFQRJb4xrt/MmUCAGlxTKL2RmT8JLTPqagCkg==}
1460+
1461+  '@vue/reactivity@3.5.32':
1462+    resolution: {integrity: sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==}
1463+
1464+  '@vue/runtime-core@3.5.32':
1465+    resolution: {integrity: sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==}
1466+
1467+  '@vue/runtime-dom@3.5.32':
1468+    resolution: {integrity: sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==}
1469+
1470+  '@vue/server-renderer@3.5.32':
1471+    resolution: {integrity: sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==}
1472+    peerDependencies:
1473+      vue: 3.5.32
1474+
1475+  '@vue/shared@3.5.32':
1476+    resolution: {integrity: sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==}
1477+
1478+  alien-signals@3.1.2:
1479+    resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==}
1480+
1481+  csstype@3.2.3:
1482+    resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
1483+
1484+  entities@7.0.1:
1485+    resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
1486+    engines: {node: '>=0.12'}
1487+
1488+  esbuild@0.27.7:
1489+    resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==}
1490+    engines: {node: '>=18'}
1491+    hasBin: true
1492+
1493+  estree-walker@2.0.2:
1494+    resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
1495+
1496+  fdir@6.5.0:
1497+    resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
1498+    engines: {node: '>=12.0.0'}
1499+    peerDependencies:
1500+      picomatch: ^3 || ^4
1501+    peerDependenciesMeta:
1502+      picomatch:
1503+        optional: true
1504+
1505+  fsevents@2.3.3:
1506+    resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
1507+    engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
1508+    os: [darwin]
1509+
1510+  magic-string@0.30.21:
1511+    resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
1512+
1513+  muggle-string@0.4.1:
1514+    resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
1515+
1516+  nanoid@3.3.11:
1517+    resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
1518+    engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
1519+    hasBin: true
1520+
1521+  path-browserify@1.0.1:
1522+    resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
1523+
1524+  picocolors@1.1.1:
1525+    resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
1526+
1527+  picomatch@4.0.4:
1528+    resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
1529+    engines: {node: '>=12'}
1530+
1531+  postcss@8.5.8:
1532+    resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
1533+    engines: {node: ^10 || ^12 || >=14}
1534+
1535+  rollup@4.60.1:
1536+    resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==}
1537+    engines: {node: '>=18.0.0', npm: '>=8.0.0'}
1538+    hasBin: true
1539+
1540+  source-map-js@1.2.1:
1541+    resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
1542+    engines: {node: '>=0.10.0'}
1543+
1544+  tinyglobby@0.2.15:
1545+    resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
1546+    engines: {node: '>=12.0.0'}
1547+
1548   typescript@5.9.3:
1549     resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
1550     engines: {node: '>=14.17'}
1551     hasBin: true
1552 
1553+  vite@7.3.1:
1554+    resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
1555+    engines: {node: ^20.19.0 || >=22.12.0}
1556+    hasBin: true
1557+    peerDependencies:
1558+      '@types/node': ^20.19.0 || >=22.12.0
1559+      jiti: '>=1.21.0'
1560+      less: ^4.0.0
1561+      lightningcss: ^1.21.0
1562+      sass: ^1.70.0
1563+      sass-embedded: ^1.70.0
1564+      stylus: '>=0.54.8'
1565+      sugarss: ^5.0.0
1566+      terser: ^5.16.0
1567+      tsx: ^4.8.1
1568+      yaml: ^2.4.2
1569+    peerDependenciesMeta:
1570+      '@types/node':
1571+        optional: true
1572+      jiti:
1573+        optional: true
1574+      less:
1575+        optional: true
1576+      lightningcss:
1577+        optional: true
1578+      sass:
1579+        optional: true
1580+      sass-embedded:
1581+        optional: true
1582+      stylus:
1583+        optional: true
1584+      sugarss:
1585+        optional: true
1586+      terser:
1587+        optional: true
1588+      tsx:
1589+        optional: true
1590+      yaml:
1591+        optional: true
1592+
1593+  vscode-uri@3.1.0:
1594+    resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
1595+
1596+  vue-router@4.6.4:
1597+    resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==}
1598+    peerDependencies:
1599+      vue: ^3.5.0
1600+
1601+  vue-tsc@3.2.6:
1602+    resolution: {integrity: sha512-gYW/kWI0XrwGzd0PKc7tVB/qpdeAkIZLNZb10/InizkQjHjnT8weZ/vBarZoj4kHKbUTZT/bAVgoOr8x4NsQ/Q==}
1603+    hasBin: true
1604+    peerDependencies:
1605+      typescript: '>=5.0.0'
1606+
1607+  vue@3.5.32:
1608+    resolution: {integrity: sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==}
1609+    peerDependencies:
1610+      typescript: '*'
1611+    peerDependenciesMeta:
1612+      typescript:
1613+        optional: true
1614+
1615 snapshots:
1616 
1617+  '@babel/helper-string-parser@7.27.1': {}
1618+
1619+  '@babel/helper-validator-identifier@7.28.5': {}
1620+
1621+  '@babel/parser@7.29.2':
1622+    dependencies:
1623+      '@babel/types': 7.29.0
1624+
1625+  '@babel/types@7.29.0':
1626+    dependencies:
1627+      '@babel/helper-string-parser': 7.27.1
1628+      '@babel/helper-validator-identifier': 7.28.5
1629+
1630+  '@esbuild/aix-ppc64@0.27.7':
1631+    optional: true
1632+
1633+  '@esbuild/android-arm64@0.27.7':
1634+    optional: true
1635+
1636+  '@esbuild/android-arm@0.27.7':
1637+    optional: true
1638+
1639+  '@esbuild/android-x64@0.27.7':
1640+    optional: true
1641+
1642+  '@esbuild/darwin-arm64@0.27.7':
1643+    optional: true
1644+
1645+  '@esbuild/darwin-x64@0.27.7':
1646+    optional: true
1647+
1648+  '@esbuild/freebsd-arm64@0.27.7':
1649+    optional: true
1650+
1651+  '@esbuild/freebsd-x64@0.27.7':
1652+    optional: true
1653+
1654+  '@esbuild/linux-arm64@0.27.7':
1655+    optional: true
1656+
1657+  '@esbuild/linux-arm@0.27.7':
1658+    optional: true
1659+
1660+  '@esbuild/linux-ia32@0.27.7':
1661+    optional: true
1662+
1663+  '@esbuild/linux-loong64@0.27.7':
1664+    optional: true
1665+
1666+  '@esbuild/linux-mips64el@0.27.7':
1667+    optional: true
1668+
1669+  '@esbuild/linux-ppc64@0.27.7':
1670+    optional: true
1671+
1672+  '@esbuild/linux-riscv64@0.27.7':
1673+    optional: true
1674+
1675+  '@esbuild/linux-s390x@0.27.7':
1676+    optional: true
1677+
1678+  '@esbuild/linux-x64@0.27.7':
1679+    optional: true
1680+
1681+  '@esbuild/netbsd-arm64@0.27.7':
1682+    optional: true
1683+
1684+  '@esbuild/netbsd-x64@0.27.7':
1685+    optional: true
1686+
1687+  '@esbuild/openbsd-arm64@0.27.7':
1688+    optional: true
1689+
1690+  '@esbuild/openbsd-x64@0.27.7':
1691+    optional: true
1692+
1693+  '@esbuild/openharmony-arm64@0.27.7':
1694+    optional: true
1695+
1696+  '@esbuild/sunos-x64@0.27.7':
1697+    optional: true
1698+
1699+  '@esbuild/win32-arm64@0.27.7':
1700+    optional: true
1701+
1702+  '@esbuild/win32-ia32@0.27.7':
1703+    optional: true
1704+
1705+  '@esbuild/win32-x64@0.27.7':
1706+    optional: true
1707+
1708+  '@jridgewell/sourcemap-codec@1.5.5': {}
1709+
1710+  '@rolldown/pluginutils@1.0.0-rc.2': {}
1711+
1712+  '@rollup/rollup-android-arm-eabi@4.60.1':
1713+    optional: true
1714+
1715+  '@rollup/rollup-android-arm64@4.60.1':
1716+    optional: true
1717+
1718+  '@rollup/rollup-darwin-arm64@4.60.1':
1719+    optional: true
1720+
1721+  '@rollup/rollup-darwin-x64@4.60.1':
1722+    optional: true
1723+
1724+  '@rollup/rollup-freebsd-arm64@4.60.1':
1725+    optional: true
1726+
1727+  '@rollup/rollup-freebsd-x64@4.60.1':
1728+    optional: true
1729+
1730+  '@rollup/rollup-linux-arm-gnueabihf@4.60.1':
1731+    optional: true
1732+
1733+  '@rollup/rollup-linux-arm-musleabihf@4.60.1':
1734+    optional: true
1735+
1736+  '@rollup/rollup-linux-arm64-gnu@4.60.1':
1737+    optional: true
1738+
1739+  '@rollup/rollup-linux-arm64-musl@4.60.1':
1740+    optional: true
1741+
1742+  '@rollup/rollup-linux-loong64-gnu@4.60.1':
1743+    optional: true
1744+
1745+  '@rollup/rollup-linux-loong64-musl@4.60.1':
1746+    optional: true
1747+
1748+  '@rollup/rollup-linux-ppc64-gnu@4.60.1':
1749+    optional: true
1750+
1751+  '@rollup/rollup-linux-ppc64-musl@4.60.1':
1752+    optional: true
1753+
1754+  '@rollup/rollup-linux-riscv64-gnu@4.60.1':
1755+    optional: true
1756+
1757+  '@rollup/rollup-linux-riscv64-musl@4.60.1':
1758+    optional: true
1759+
1760+  '@rollup/rollup-linux-s390x-gnu@4.60.1':
1761+    optional: true
1762+
1763+  '@rollup/rollup-linux-x64-gnu@4.60.1':
1764+    optional: true
1765+
1766+  '@rollup/rollup-linux-x64-musl@4.60.1':
1767+    optional: true
1768+
1769+  '@rollup/rollup-openbsd-x64@4.60.1':
1770+    optional: true
1771+
1772+  '@rollup/rollup-openharmony-arm64@4.60.1':
1773+    optional: true
1774+
1775+  '@rollup/rollup-win32-arm64-msvc@4.60.1':
1776+    optional: true
1777+
1778+  '@rollup/rollup-win32-ia32-msvc@4.60.1':
1779+    optional: true
1780+
1781+  '@rollup/rollup-win32-x64-gnu@4.60.1':
1782+    optional: true
1783+
1784+  '@rollup/rollup-win32-x64-msvc@4.60.1':
1785+    optional: true
1786+
1787+  '@types/estree@1.0.8': {}
1788+
1789+  '@vitejs/plugin-vue@6.0.5(vite@7.3.1)(vue@3.5.32(typescript@5.9.3))':
1790+    dependencies:
1791+      '@rolldown/pluginutils': 1.0.0-rc.2
1792+      vite: 7.3.1
1793+      vue: 3.5.32(typescript@5.9.3)
1794+
1795+  '@volar/language-core@2.4.28':
1796+    dependencies:
1797+      '@volar/source-map': 2.4.28
1798+
1799+  '@volar/source-map@2.4.28': {}
1800+
1801+  '@volar/typescript@2.4.28':
1802+    dependencies:
1803+      '@volar/language-core': 2.4.28
1804+      path-browserify: 1.0.1
1805+      vscode-uri: 3.1.0
1806+
1807+  '@vue/compiler-core@3.5.32':
1808+    dependencies:
1809+      '@babel/parser': 7.29.2
1810+      '@vue/shared': 3.5.32
1811+      entities: 7.0.1
1812+      estree-walker: 2.0.2
1813+      source-map-js: 1.2.1
1814+
1815+  '@vue/compiler-dom@3.5.32':
1816+    dependencies:
1817+      '@vue/compiler-core': 3.5.32
1818+      '@vue/shared': 3.5.32
1819+
1820+  '@vue/compiler-sfc@3.5.32':
1821+    dependencies:
1822+      '@babel/parser': 7.29.2
1823+      '@vue/compiler-core': 3.5.32
1824+      '@vue/compiler-dom': 3.5.32
1825+      '@vue/compiler-ssr': 3.5.32
1826+      '@vue/shared': 3.5.32
1827+      estree-walker: 2.0.2
1828+      magic-string: 0.30.21
1829+      postcss: 8.5.8
1830+      source-map-js: 1.2.1
1831+
1832+  '@vue/compiler-ssr@3.5.32':
1833+    dependencies:
1834+      '@vue/compiler-dom': 3.5.32
1835+      '@vue/shared': 3.5.32
1836+
1837+  '@vue/devtools-api@6.6.4': {}
1838+
1839+  '@vue/language-core@3.2.6':
1840+    dependencies:
1841+      '@volar/language-core': 2.4.28
1842+      '@vue/compiler-dom': 3.5.32
1843+      '@vue/shared': 3.5.32
1844+      alien-signals: 3.1.2
1845+      muggle-string: 0.4.1
1846+      path-browserify: 1.0.1
1847+      picomatch: 4.0.4
1848+
1849+  '@vue/reactivity@3.5.32':
1850+    dependencies:
1851+      '@vue/shared': 3.5.32
1852+
1853+  '@vue/runtime-core@3.5.32':
1854+    dependencies:
1855+      '@vue/reactivity': 3.5.32
1856+      '@vue/shared': 3.5.32
1857+
1858+  '@vue/runtime-dom@3.5.32':
1859+    dependencies:
1860+      '@vue/reactivity': 3.5.32
1861+      '@vue/runtime-core': 3.5.32
1862+      '@vue/shared': 3.5.32
1863+      csstype: 3.2.3
1864+
1865+  '@vue/server-renderer@3.5.32(vue@3.5.32(typescript@5.9.3))':
1866+    dependencies:
1867+      '@vue/compiler-ssr': 3.5.32
1868+      '@vue/shared': 3.5.32
1869+      vue: 3.5.32(typescript@5.9.3)
1870+
1871+  '@vue/shared@3.5.32': {}
1872+
1873+  alien-signals@3.1.2: {}
1874+
1875+  csstype@3.2.3: {}
1876+
1877+  entities@7.0.1: {}
1878+
1879+  esbuild@0.27.7:
1880+    optionalDependencies:
1881+      '@esbuild/aix-ppc64': 0.27.7
1882+      '@esbuild/android-arm': 0.27.7
1883+      '@esbuild/android-arm64': 0.27.7
1884+      '@esbuild/android-x64': 0.27.7
1885+      '@esbuild/darwin-arm64': 0.27.7
1886+      '@esbuild/darwin-x64': 0.27.7
1887+      '@esbuild/freebsd-arm64': 0.27.7
1888+      '@esbuild/freebsd-x64': 0.27.7
1889+      '@esbuild/linux-arm': 0.27.7
1890+      '@esbuild/linux-arm64': 0.27.7
1891+      '@esbuild/linux-ia32': 0.27.7
1892+      '@esbuild/linux-loong64': 0.27.7
1893+      '@esbuild/linux-mips64el': 0.27.7
1894+      '@esbuild/linux-ppc64': 0.27.7
1895+      '@esbuild/linux-riscv64': 0.27.7
1896+      '@esbuild/linux-s390x': 0.27.7
1897+      '@esbuild/linux-x64': 0.27.7
1898+      '@esbuild/netbsd-arm64': 0.27.7
1899+      '@esbuild/netbsd-x64': 0.27.7
1900+      '@esbuild/openbsd-arm64': 0.27.7
1901+      '@esbuild/openbsd-x64': 0.27.7
1902+      '@esbuild/openharmony-arm64': 0.27.7
1903+      '@esbuild/sunos-x64': 0.27.7
1904+      '@esbuild/win32-arm64': 0.27.7
1905+      '@esbuild/win32-ia32': 0.27.7
1906+      '@esbuild/win32-x64': 0.27.7
1907+
1908+  estree-walker@2.0.2: {}
1909+
1910+  fdir@6.5.0(picomatch@4.0.4):
1911+    optionalDependencies:
1912+      picomatch: 4.0.4
1913+
1914+  fsevents@2.3.3:
1915+    optional: true
1916+
1917+  magic-string@0.30.21:
1918+    dependencies:
1919+      '@jridgewell/sourcemap-codec': 1.5.5
1920+
1921+  muggle-string@0.4.1: {}
1922+
1923+  nanoid@3.3.11: {}
1924+
1925+  path-browserify@1.0.1: {}
1926+
1927+  picocolors@1.1.1: {}
1928+
1929+  picomatch@4.0.4: {}
1930+
1931+  postcss@8.5.8:
1932+    dependencies:
1933+      nanoid: 3.3.11
1934+      picocolors: 1.1.1
1935+      source-map-js: 1.2.1
1936+
1937+  rollup@4.60.1:
1938+    dependencies:
1939+      '@types/estree': 1.0.8
1940+    optionalDependencies:
1941+      '@rollup/rollup-android-arm-eabi': 4.60.1
1942+      '@rollup/rollup-android-arm64': 4.60.1
1943+      '@rollup/rollup-darwin-arm64': 4.60.1
1944+      '@rollup/rollup-darwin-x64': 4.60.1
1945+      '@rollup/rollup-freebsd-arm64': 4.60.1
1946+      '@rollup/rollup-freebsd-x64': 4.60.1
1947+      '@rollup/rollup-linux-arm-gnueabihf': 4.60.1
1948+      '@rollup/rollup-linux-arm-musleabihf': 4.60.1
1949+      '@rollup/rollup-linux-arm64-gnu': 4.60.1
1950+      '@rollup/rollup-linux-arm64-musl': 4.60.1
1951+      '@rollup/rollup-linux-loong64-gnu': 4.60.1
1952+      '@rollup/rollup-linux-loong64-musl': 4.60.1
1953+      '@rollup/rollup-linux-ppc64-gnu': 4.60.1
1954+      '@rollup/rollup-linux-ppc64-musl': 4.60.1
1955+      '@rollup/rollup-linux-riscv64-gnu': 4.60.1
1956+      '@rollup/rollup-linux-riscv64-musl': 4.60.1
1957+      '@rollup/rollup-linux-s390x-gnu': 4.60.1
1958+      '@rollup/rollup-linux-x64-gnu': 4.60.1
1959+      '@rollup/rollup-linux-x64-musl': 4.60.1
1960+      '@rollup/rollup-openbsd-x64': 4.60.1
1961+      '@rollup/rollup-openharmony-arm64': 4.60.1
1962+      '@rollup/rollup-win32-arm64-msvc': 4.60.1
1963+      '@rollup/rollup-win32-ia32-msvc': 4.60.1
1964+      '@rollup/rollup-win32-x64-gnu': 4.60.1
1965+      '@rollup/rollup-win32-x64-msvc': 4.60.1
1966+      fsevents: 2.3.3
1967+
1968+  source-map-js@1.2.1: {}
1969+
1970+  tinyglobby@0.2.15:
1971+    dependencies:
1972+      fdir: 6.5.0(picomatch@4.0.4)
1973+      picomatch: 4.0.4
1974+
1975   typescript@5.9.3: {}
1976+
1977+  vite@7.3.1:
1978+    dependencies:
1979+      esbuild: 0.27.7
1980+      fdir: 6.5.0(picomatch@4.0.4)
1981+      picomatch: 4.0.4
1982+      postcss: 8.5.8
1983+      rollup: 4.60.1
1984+      tinyglobby: 0.2.15
1985+    optionalDependencies:
1986+      fsevents: 2.3.3
1987+
1988+  vscode-uri@3.1.0: {}
1989+
1990+  vue-router@4.6.4(vue@3.5.32(typescript@5.9.3)):
1991+    dependencies:
1992+      '@vue/devtools-api': 6.6.4
1993+      vue: 3.5.32(typescript@5.9.3)
1994+
1995+  vue-tsc@3.2.6(typescript@5.9.3):
1996+    dependencies:
1997+      '@volar/typescript': 2.4.28
1998+      '@vue/language-core': 3.2.6
1999+      typescript: 5.9.3
2000+
2001+  vue@3.5.32(typescript@5.9.3):
2002+    dependencies:
2003+      '@vue/compiler-dom': 3.5.32
2004+      '@vue/compiler-sfc': 3.5.32
2005+      '@vue/runtime-dom': 3.5.32
2006+      '@vue/server-renderer': 3.5.32(vue@3.5.32(typescript@5.9.3))
2007+      '@vue/shared': 3.5.32
2008+    optionalDependencies:
2009+      typescript: 5.9.3
2010diff --git a/tasks/T-S070.md b/tasks/T-S070.md
2011index 520a6e9b389f2bb262f8a64c0ed5d6b596a2be0f..afdb83e53aa42efbe8848a64aa926ee2e73eae53 100644
2012--- a/tasks/T-S070.md
2013+++ b/tasks/T-S070.md
2014@@ -2,7 +2,7 @@
2015 
2016 ## 状态
2017 
2018-- 当前状态:`待开始`
2019+- 当前状态:`已完成`
2020 - 规模预估:`M`
2021 - 依赖任务:无
2022 - 建议执行者:`Codex`(涉及 monorepo 构建、静态资源托管和本地 API 路由接入)
2023@@ -21,7 +21,7 @@
2024 
2025 - 仓库:`/Users/george/code/baa-conductor`
2026 - 分支基线:`main`
2027-- 提交:`b063524`
2028+- 提交:`68a85cf`
2029 
2030 ## 分支与 worktree(强制)
2031 
2032@@ -143,22 +143,42 @@
2033 
2034 ### 开始执行
2035 
2036-- 执行者:
2037-- 开始时间:
2038+- 执行者:`Codex`
2039+- 开始时间:`2026-04-03 14:25:00 CST`
2040 - 状态变更:`待开始` → `进行中`
2041 
2042 ### 完成摘要
2043 
2044-- 完成时间:
2045+- 完成时间:`2026-04-03 14:42:19 CST`
2046 - 状态变更:`进行中` → `已完成`
2047 - 修改了哪些文件:
2048+  - `apps/conductor-ui/package.json`
2049+  - `apps/conductor-ui/tsconfig.json`
2050+  - `apps/conductor-ui/vite.config.ts`
2051+  - `apps/conductor-ui/index.html`
2052+  - `apps/conductor-ui/src/*`
2053+  - `apps/conductor-daemon/package.json`
2054+  - `apps/conductor-daemon/src/local-api.ts`
2055+  - `apps/conductor-daemon/src/index.test.js`
2056+  - `pnpm-lock.yaml`
2057+  - `tasks/T-S070.md`
2058+  - `tasks/TASK_OVERVIEW.md`
2059 - 核心实现思路:
2060+  - 新建 `apps/conductor-ui/`,用 `Vue 3 + Vite + TypeScript + Vue Router` 提供最小工作台壳,固定 `base=/app/`,并在页面内明确标出“未接登录态”的占位状态
2061+  - 在 `conductor-daemon` 的 `local-api` 增加独立 `/app` 静态托管层,分别处理入口 HTML、`/app/assets/*` 构建产物和 `/app/*` history fallback,同时区分 `index.html no-store` 与 hashed assets 长缓存
2062+  - 扩展 `conductor-daemon` 测试,覆盖本地 handler 和真实 HTTP runtime 两层的 `/app`、`/app/control`、`/app/assets/*` 行为,确保不影响原有 `/v1/status/ui` 和 artifact 路由
2063 - 跑了哪些测试:
2064+  - `pnpm install`
2065+  - `pnpm -C apps/conductor-ui build`
2066+  - `pnpm -C apps/conductor-ui typecheck`
2067+  - `pnpm -C apps/conductor-daemon build`
2068+  - `pnpm -C apps/conductor-daemon test`
2069 
2070 ### 执行过程中遇到的问题
2071 
2072-- 
2073+- 新 worktree 初始没有安装前端依赖,先执行 `pnpm install` 更新 workspace 依赖和 lockfile 后才开始构建与测试
2074 
2075 ### 剩余风险
2076 
2077+- 当前 `/app` 仍是未鉴权的占位工作台壳;后续 `T-S071` 需要补浏览器 session 登录、登出和读写接口保护
2078 - 如果后续 UI 会话鉴权需要服务端注入运行时配置,可能还要在前端壳里补一层 `/app/config.json` 或内联 bootstrap 数据
2079diff --git a/tasks/TASK_OVERVIEW.md b/tasks/TASK_OVERVIEW.md
2080index 704310f5ec407bd56468c90df3690b3dac77e02d..d4dad569575247360594b93b32d0d7d3ca8d21af 100644
2081--- a/tasks/TASK_OVERVIEW.md
2082+++ b/tasks/TASK_OVERVIEW.md
2083@@ -3,7 +3,7 @@
2084 ## 当前基线
2085 
2086 - 日期:`2026-04-03`
2087-- 主分支基线:`main@d8e883d`
2088+- 主分支基线:`main@68a85cf`
2089 - canonical local API:`http://100.71.210.78:4317`
2090 - canonical public host:`https://conductor.makefile.so`
2091 - 当前活跃任务卡和近期刚完成的任务卡保留在本目录;较早已完成任务归档到 [`./archive/README.md`](./archive/README.md)
2092@@ -61,6 +61,10 @@
2093   - renewal dispatcher
2094   - `browser.proxy_delivery`
2095   - 旧 watchdog / Safari a11y 续命方案已随 `T-S074` 从仓库移除
2096+- Conductor UI 基础设施已落地:
2097+  - 新增 `apps/conductor-ui/` Vue 3 + Vite + TypeScript + Vue Router 脚手架
2098+  - `conductor-daemon` 已开始同源托管 `/app`
2099+  - `/app`、`/app/control` 和 `/app/assets/*` 已具备最小 SPA 壳、静态资源缓存和 history fallback
2100 
2101 ## 当前已确认的不一致
2102 
2103@@ -104,6 +108,7 @@
2104 | [`T-S065`](./T-S065.md) | policy 配置化 | M | 无 | Codex | 已完成 |
2105 | [`T-S073`](./T-S073.md) | 移除 conductor 内的 stagit 仓库静态页能力 | M | 无 | Codex | 已完成 |
2106 | [`T-S074`](./T-S074.md) | 删除旧版 watchdog 与 Safari a11y 续命方案 | S | 无 | Codex | 已完成 |
2107+| [`T-S070`](./T-S070.md) | Conductor UI 基础设施:Vue 3 脚手架与 `/app` 静态托管 | M | 无 | Codex | 已完成 |
2108 
2109 ### 当前下一波任务
2110 
2111@@ -111,18 +116,17 @@
2112 
2113 | 任务 | 标题 | 规模 | 依赖 | 建议 AI | 状态 |
2114 |---|---|---|---|---|---|
2115-| [`T-S070`](./T-S070.md) | Conductor UI 基础设施:Vue 3 脚手架与 `/app` 静态托管 | M | 无 | Codex | 待开始 |
2116 | [`T-S071`](./T-S071.md) | Conductor UI 会话鉴权:登录页与浏览器 session | M | T-S070 | Codex | 待开始 |
2117 | [`T-S072`](./T-S072.md) | Conductor UI `Control` 工作区首版 | L | T-S070, T-S071 | Codex | 待开始 |
2118 
2119 当前没有 open bug / open opt。
2120 
2121-如继续推进,建议直接回到 `T-S070 -> T-S071 -> T-S072` 的 Web UI 主线。
2122+如继续推进,建议直接进入 `T-S071 -> T-S072` 的 Web UI 主线。
2123 
2124 说明:
2125 
2126 - `T-S073` / `T-S074` 已完成,仓库边界收缩已结束
2127-- `T-S070` / `T-S071` / `T-S072` 仍是后续正式 Web UI 主线
2128+- `T-S070` 已完成,`T-S071` / `T-S072` 是后续正式 Web UI 主线
2129 - `Channels` 工作区和正式 `channel` 域模型继续留在 `T-S072` 之后再拆下一轮
2130 
2131 ### 已完成但保留作参考
2132@@ -147,6 +151,7 @@
2133 | T-S048 | Gemini 投递适配器 | ✅ |
2134 | T-S049 | 开放 chatgpt/gemini target | ✅ |
2135 | T-S050 | stagit git 静态页面 | ✅ |
2136+| T-S070 | Conductor UI 基础设施:Vue 3 脚手架与 `/app` 静态托管 | ✅ |
2137 | T-S073 | 移除 conductor 内的 stagit 仓库静态页能力 | ✅ |
2138 | T-S074 | 删除旧版 watchdog 与 Safari a11y 续命方案 | ✅ |
2139 | T-S052 | D1 数据库初始化 | ✅ |
2140@@ -212,12 +217,12 @@
2141 
2142 ## 当前主线判断
2143 
2144-Phase 1(浏览器主链)、Artifact 静态服务,以及 timed-jobs + 续命主线都已完成收口。`T-S060`、`T-S061`、`T-S062`、`T-S063`、`T-S064`、`T-S065`、`T-S066`、`T-S067`、`T-S068`、`T-S069` 已全部落地;当前主线没有 open bug blocker,也没有 open opt,`T-S073` / `T-S074` 已完成,剩余待开始任务只剩后续 Web UI 主线。
2145+Phase 1(浏览器主链)、Artifact 静态服务、timed-jobs + 续命主线,以及 Web UI 基础设施都已完成收口。`T-S060`、`T-S061`、`T-S062`、`T-S063`、`T-S064`、`T-S065`、`T-S066`、`T-S067`、`T-S068`、`T-S069`、`T-S070` 已全部落地;当前主线没有 open bug blocker,也没有 open opt,`T-S073` / `T-S074` 已完成,剩余待开始任务只剩后续 Web UI 工作区与鉴权主线。
2146 
2147 如果继续推进,建议:
2148 
2149 - 远端 `baa-conductor-artifact` 已在 `2026-04-01` 应用 [`packages/d1-client/src/d1-setup.sql`](../packages/d1-client/src/d1-setup.sql) 的最新 schema;后续如新建或重置 D1 环境,仍需先执行同一脚本
2150-- 直接回到 `T-S070 -> T-S071 -> T-S072` 的正式 Web UI 工作台主线
2151+- 直接进入 `T-S071 -> T-S072` 的正式 Web UI 工作台主线
2152 
2153 ## 现在该读什么
2154