open-design/scripts/normalize-plugin-scenarios.ts
Eli-tangerine 8193981511
Keep PR 2400 changes without folder pickers (#2462)
* feat(daemon): add project working directory management and editor hand-off functionality

- Introduced new flags for project commands to manage working directories, including `--working-dir` and `--dir`.
- Implemented API routes for listing available editors and opening projects in selected editors.
- Added a hand-off button in the ChatPane header to facilitate opening project folders in local applications.
- Enhanced the HomeHero component to include working directory and design system settings, improving user experience in project creation.
- Created HomeHeroSettingsChips component for inline management of working directory and design system selection.

* feat(chat): implement voice transcription proxy and enhance UI components

- Added a new API route for voice transcription using OpenAI's `/audio/transcriptions` endpoint, allowing users to send audio blobs directly for transcription.
- Integrated multer for handling audio file uploads in memory, ensuring efficient processing without disk storage.
- Updated the HomeHero component to include example prompt suggestions for plugins, enhancing user interaction.
- Introduced the EditorIcon component to visually represent different editors in the hand-off menu, improving the user experience.
- Refined the HandoffButton component to utilize the new EditorIcon, providing a more cohesive interface for selecting editors.
- Enhanced CSS styles for various components to improve layout and responsiveness, including adjustments to tab and button sizes for better usability.

* style(workspace-shell): enhance layout and overflow handling

- Updated CSS for .workspace-shell to ensure full viewport width and height, with proper overflow management.
- Adjusted grid layout to prevent content overflow and maintain responsiveness.
- Modified styles for .workspace-tabs-chrome to improve width handling and prevent overflow issues.

* refactor(chat): remove voice transcription proxy and related components

- Deleted the voice transcription proxy implementation, including the associated API route and multer configuration.
- Removed the MicButton component from the ChatComposer and HomeHero components to streamline the UI.
- Updated HomeHero to include example suggestions without the voice input functionality.
- Adjusted CSS styles for various components to maintain layout consistency after the removal of the MicButton.

* feat(daemon): implement minting of HMAC tokens for working directory management

- Added a new function `mintImportTokenFromCurrentSecret` to generate HMAC tokens bound to a specified base directory, enhancing security for working directory operations.
- Updated the `desktop-auth.ts` file to include the new token minting functionality, which returns structured errors when the desktop auth secret is cleared.
- Introduced new IPC message types for minting import tokens in the sidecar protocol, allowing seamless integration with the daemon's working directory management.
- Enhanced the `WorkingDirPill` component to utilize the new token minting flow for secure directory selection in desktop builds.
- Updated CSS styles for the HomeHero component to accommodate new example suggestion features and maintain layout consistency.

* fix(HomeView): import HOME_HERO_CHIPS constant for improved chip management

- Updated the HomeView component to import the HOME_HERO_CHIPS constant from the chips module, enhancing the management of hero chips within the component.

* feat(daemon): implement mintImportTokenViaSidecar for secure working directory management

- Introduced the `mintImportTokenViaSidecar` function to facilitate the minting of HMAC tokens for desktop-import operations via the daemon's sidecar IPC. This allows CLI commands to bypass authentication when the desktop-auth gate is active.
- Updated the CLI to utilize the new token minting function when setting the working directory, ensuring secure access to trust-gated API endpoints.
- Enhanced the sidecar server to handle minting requests and return structured error messages for improved user feedback.
- Added tests to validate the new token minting functionality and its integration with the working directory management process.
- Refactored related components to support the new token flow, improving overall security and user experience.

* feat(HomeHero): enhance UI components and styles for improved user experience

- Updated HomeHero component to replace active dot indicators with Plug icons for better visual representation of active plugins.
- Adjusted CSS styles for various elements, including padding and dimensions, to enhance layout consistency and responsiveness.
- Introduced new styles for active type icons and improved hover effects for buttons.
- Updated HomeHeroSettingsChips to change button titles and icons for clarity.
- Added tests to ensure proper rendering and functionality of updated components.

* feat(ProjectDesignSystemPicker): enhance design system selection with preview functionality

- Updated the ProjectDesignSystemPicker component to include a preview feature for design systems, allowing users to see a preview of the selected design system.
- Implemented hover functionality to update the preview based on the hovered design system.
- Added fullscreen preview capability for a more immersive experience.
- Enhanced CSS styles for the design system picker to improve layout and responsiveness.
- Introduced tests to validate the new preview functionality and ensure proper interaction within the component.

* feat: refactor project metadata handling and enhance design system picker

- Updated the default scenario plugin ID retrieval to use project metadata, improving the logic for determining the appropriate plugin based on project intent.
- Enhanced the ProjectDesignSystemPicker and related components to support localized design system summaries and categories, improving user experience.
- Introduced new translations for working directory and design system picker components, ensuring better accessibility and usability across different locales.
- Added a new 'live-artifact' project type to the HomeHero chips, expanding the functionality for users creating refreshable artifacts.
- Updated tests to validate the new project metadata handling and design system picker functionalities.

* feat: enhance localization and styling for design system components

- Added French translations for working directory and design system picker components, improving accessibility for French-speaking users.
- Updated CSS styles for the pet task item to ensure consistent padding and layout.
- Introduced a new test suite for HomeHeroSettingsChips to validate localization and design system selection functionality.
- Enhanced ProjectDesignSystemPicker tests to ensure proper localization and interaction with design system categories.

* fix: update .gitignore to include all claude-sessions directories and remove specific session files

- Modified .gitignore to ensure all claude-sessions directories are ignored by using a wildcard pattern.
- Deleted two specific claude-sessions markdown files to clean up unnecessary session data.

* fix: repair home automation ci regressions

* fix: stabilize artifact consistency e2e

* Remove folder picker changes from PR 2400

---------

Co-authored-by: pftom <1043269994@qq.com>
Co-authored-by: qiongyu1999 <2694684348@qq.com>
2026-05-20 22:07:30 +08:00

444 lines
14 KiB
TypeScript

/* ─────────────────────────────────────────────────────────────────────────
* scripts/normalize-plugin-scenarios.ts
*
* Phase 3 dry-run: propose a `od.scenario` value for every visible plugin
* manifest under `plugins/_official/**`, mapped onto the 7 scenario lanes
* derived from the user-query analysis report.
*
* business-system | 业务系统 / 后台 / 数据看板
* presentation | 演示文稿 / 报告 / 课程
* app-prototype | App / 多屏产品原型
* landing | 官网 / Landing / 营销页
* brand-visual | 品牌视觉 / Logo / 设计系统
* dev-tool | 开发者工具 / 工程协作
* media-asset | 图片 / 视频 / 展示素材
* general | 兜底:现有信号无法稳定归类
*
* The script is intentionally read-only. It emits:
* - stdout: a markdown report grouped by proposed scenario so a human
* can scan whether assignments look right.
* - .tmp/plugin-scenario-mapping.json: machine-readable mapping the
* follow-up writer step will consume.
*
* Run: `pnpm exec tsx scripts/normalize-plugin-scenarios.ts`
* ─────────────────────────────────────────────────────────────────── */
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const officialRoot = path.join(repoRoot, 'plugins', '_official');
const outDir = path.join(repoRoot, '.tmp');
const outFile = path.join(outDir, 'plugin-scenario-mapping.json');
type Scenario =
| 'business-system'
| 'presentation'
| 'app-prototype'
| 'landing'
| 'brand-visual'
| 'dev-tool'
| 'media-asset'
| 'general';
const SCENARIO_LABEL: Record<Scenario, string> = {
'business-system': '业务系统 / 后台 / 数据看板',
presentation: '演示文稿 / 报告 / 课程',
'app-prototype': 'App / 多屏产品原型',
landing: '官网 / Landing / 营销页',
'brand-visual': '品牌视觉 / Logo / 设计系统',
'dev-tool': '开发者工具 / 工程协作',
'media-asset': '图片 / 视频 / 展示素材',
general: '兜底(待人工复核)',
};
interface Manifest {
name?: string;
title?: string;
description?: string;
tags?: string[];
od?: {
kind?: string;
taskKind?: string;
mode?: string;
scenario?: string;
surface?: string;
platform?: string;
};
}
interface Candidate {
manifestPath: string;
relPath: string;
id: string;
title: string;
currentScenario: string;
currentMode: string;
currentTaskKind: string;
tags: string[];
proposedScenario: Scenario;
reason: string;
}
async function listManifests(): Promise<string[]> {
const out: string[] = [];
async function walk(dir: string) {
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
await walk(full);
} else if (entry.isFile() && entry.name === 'open-design.json') {
out.push(full);
}
}
}
await walk(officialRoot);
return out.sort();
}
function toTagSet(tags: string[] | undefined): Set<string> {
return new Set((tags ?? []).map((t) => String(t).toLowerCase().trim()).filter(Boolean));
}
const BUSINESS_SYSTEM_TAGS = [
'dashboard',
'admin-panel',
'admin',
'analytics',
'control-panel',
'crm',
'erp',
'operations',
'reporting',
'workspace',
'kanban',
'inventory',
'logistics',
'fleet',
'finance-admin',
];
const PRESENTATION_TAGS = [
'slides',
'deck',
'presentation',
'pitch',
'pitch-deck',
'keynote',
'course',
'training',
'lecture',
'lesson',
'report-deck',
'editorial-deck',
];
const APP_TAGS = [
'mobile',
'ios',
'android',
'wechat',
'miniapp',
'mini-program',
'tablet',
'app',
'mobile-app',
'multi-screen',
'screen-flow',
'onboarding',
];
const LANDING_TAGS = [
'landing',
'landing-page',
'saas',
'marketing',
'marketing-site',
'hero',
'cta',
'pricing',
'b2b',
'product-site',
'homepage',
'portfolio',
'agency',
'studio',
'consulting',
];
const BRAND_VISUAL_TAGS = [
'design-system',
'brand',
'brand-visual',
'logo',
'typography',
'color-system',
'visual-language',
'identity',
'guideline',
'style-guide',
];
const DEV_TOOL_TAGS = [
'developer',
'developer-tool',
'cli',
'ide',
'agent',
'mcp',
'connector',
'plugin-authoring',
'automation',
'devops',
'runbook',
'figma-migration',
'code-migration',
'export',
'handoff',
];
const MEDIA_ASSET_TAGS = [
'image-asset',
'poster',
'screenshot',
'app-store-screenshot',
'render',
'thumbnail',
'storyboard',
'banner',
];
function anyTag(tags: Set<string>, candidates: readonly string[]): string | null {
for (const candidate of candidates) {
if (tags.has(candidate)) return candidate;
}
return null;
}
function classify(manifest: Manifest, relPath: string): { scenario: Scenario; reason: string } {
const od = manifest.od ?? {};
const mode = String(od.mode ?? '').toLowerCase();
const taskKind = String(od.taskKind ?? '').toLowerCase();
const scenario = String(od.scenario ?? '').toLowerCase();
const tags = toTagSet(manifest.tags);
const id = (manifest.name ?? '').toLowerCase();
const description = (manifest.description ?? '').toLowerCase();
// 1. Hard infrastructure scenarios go straight to dev-tool. These are
// the platform's own routing scenarios (figma migration, code migration,
// exports, plugin authoring, refine/tune), not artifact builders.
if (
taskKind === 'figma-migration' ||
taskKind === 'code-migration' ||
taskKind === 'tune-collab' ||
id === 'od-plugin-authoring' ||
id === 'od-design-refine' ||
id.endsWith('-export') ||
id.startsWith('od-')
&& (id.includes('export') || id.includes('migration') || id.includes('tune') || id.includes('refine'))
) {
return { scenario: 'dev-tool', reason: `infra:${taskKind || id}` };
}
// 2. Mode-based fast paths.
if (mode === 'design-system') {
return { scenario: 'brand-visual', reason: 'mode:design-system' };
}
if (mode === 'image' || mode === 'video' || mode === 'audio') {
return { scenario: 'media-asset', reason: `mode:${mode}` };
}
if (mode === 'deck') {
return { scenario: 'presentation', reason: 'mode:deck' };
}
// 3. Tag-based classification for the remaining prototype/utility plugins.
const businessHit = anyTag(tags, BUSINESS_SYSTEM_TAGS);
if (businessHit) return { scenario: 'business-system', reason: `tag:${businessHit}` };
const presentationHit = anyTag(tags, PRESENTATION_TAGS);
if (presentationHit) return { scenario: 'presentation', reason: `tag:${presentationHit}` };
const brandHit = anyTag(tags, BRAND_VISUAL_TAGS);
if (brandHit) return { scenario: 'brand-visual', reason: `tag:${brandHit}` };
const devHit = anyTag(tags, DEV_TOOL_TAGS);
if (devHit) return { scenario: 'dev-tool', reason: `tag:${devHit}` };
const landingHit = anyTag(tags, LANDING_TAGS);
if (landingHit) return { scenario: 'landing', reason: `tag:${landingHit}` };
const appHit = anyTag(tags, APP_TAGS);
if (appHit) return { scenario: 'app-prototype', reason: `tag:${appHit}` };
const mediaAssetHit = anyTag(tags, MEDIA_ASSET_TAGS);
if (mediaAssetHit) return { scenario: 'media-asset', reason: `tag:${mediaAssetHit}` };
// 4. Path-based defaults for directories that are uniformly one scenario.
if (relPath.includes('/image-templates/')) {
return { scenario: 'media-asset', reason: 'path:image-templates' };
}
if (relPath.includes('/video-templates/')) {
return { scenario: 'media-asset', reason: 'path:video-templates' };
}
if (relPath.includes('/design-systems/')) {
return { scenario: 'brand-visual', reason: 'path:design-systems' };
}
// 5. Description heuristics for prototype-mode plugins that didn't tag
// themselves. A dashboard-shaped description should still land in
// business-system even when the author forgot to tag it.
if (mode === 'prototype') {
if (/dashboard|admin|console|analytics|kpi|control panel/.test(description)) {
return { scenario: 'business-system', reason: 'desc:dashboard-ish' };
}
if (/landing|hero|cta|pricing|marketing/.test(description)) {
return { scenario: 'landing', reason: 'desc:landing-ish' };
}
if (/mobile|app screen|onboarding|sign[- ]?up flow/.test(description)) {
return { scenario: 'app-prototype', reason: 'desc:mobile-ish' };
}
if (/slide|deck|presentation|pitch/.test(description)) {
return { scenario: 'presentation', reason: 'desc:deck-ish' };
}
// The legacy "scenario": "operations" is a strong signal but we
// already caught it via the BUSINESS_SYSTEM_TAGS set when tags carry
// it. As a final hint, treat the literal scenario field if present.
if (scenario === 'operations') {
return { scenario: 'business-system', reason: 'scenario-field:operations' };
}
// Generic prototype with no other signal — most likely an app shell
// since the curated default for HomeHero's `Prototype` chip is
// example-web-prototype, which is closer to a web app/landing than a
// dashboard. We mark it as `general` so the human review step can
// re-route precisely instead of guessing.
return { scenario: 'general', reason: 'mode:prototype no-signal' };
}
return { scenario: 'general', reason: 'no-signal' };
}
async function main() {
const manifests = await listManifests();
const candidates: Candidate[] = [];
for (const manifestPath of manifests) {
const relPath = path.relative(repoRoot, manifestPath).split(path.sep).join('/');
let raw: string;
try {
raw = await readFile(manifestPath, 'utf8');
} catch (err) {
console.error(`[skip] ${relPath}: ${(err as Error).message}`);
continue;
}
let manifest: Manifest;
try {
manifest = JSON.parse(raw) as Manifest;
} catch (err) {
console.error(`[skip] ${relPath}: ${(err as Error).message}`);
continue;
}
// Skip atoms — they don't show up on the home grid and don't need a
// scenario assignment. The current facets.ts excludes them via
// `od.kind !== 'atom'` so this matches the visible-plugin contract.
if (manifest.od?.kind === 'atom') continue;
const { scenario, reason } = classify(manifest, relPath);
candidates.push({
manifestPath,
relPath,
id: manifest.name ?? '(unknown)',
title: manifest.title ?? manifest.name ?? '(untitled)',
currentScenario: String(manifest.od?.scenario ?? ''),
currentMode: String(manifest.od?.mode ?? ''),
currentTaskKind: String(manifest.od?.taskKind ?? ''),
tags: manifest.tags ?? [],
proposedScenario: scenario,
reason,
});
}
// Group by proposed scenario for the human report.
const byScenario = new Map<Scenario, Candidate[]>();
for (const candidate of candidates) {
const list = byScenario.get(candidate.proposedScenario) ?? [];
list.push(candidate);
byScenario.set(candidate.proposedScenario, list);
}
const scenarioOrder: Scenario[] = [
'business-system',
'presentation',
'app-prototype',
'landing',
'brand-visual',
'dev-tool',
'media-asset',
'general',
];
const total = candidates.length;
console.log(`# Plugin scenario mapping preview\n`);
console.log(`Scanned ${manifests.length} manifests, classified ${total} visible plugins (atoms skipped).\n`);
console.log(`## Distribution\n`);
console.log(`| Scenario | Count | Share |`);
console.log(`|---|---:|---:|`);
for (const scenario of scenarioOrder) {
const count = byScenario.get(scenario)?.length ?? 0;
const share = total > 0 ? ((count / total) * 100).toFixed(1) : '0.0';
console.log(`| ${SCENARIO_LABEL[scenario]} (\`${scenario}\`) | ${count} | ${share}% |`);
}
console.log('');
for (const scenario of scenarioOrder) {
const list = byScenario.get(scenario) ?? [];
if (list.length === 0) continue;
console.log(`## ${SCENARIO_LABEL[scenario]} (\`${scenario}\`) — ${list.length}\n`);
console.log(`| id | title | mode | tags (first 4) | reason |`);
console.log(`|---|---|---|---|---|`);
for (const candidate of list) {
const tagsPreview = candidate.tags.slice(0, 4).join(', ');
console.log(
`| \`${candidate.id}\` | ${candidate.title} | ${candidate.currentMode || '—'} | ${tagsPreview || '—'} | ${candidate.reason} |`,
);
}
console.log('');
}
await mkdir(outDir, { recursive: true });
await writeFile(
outFile,
JSON.stringify(
{
generatedAt: new Date().toISOString(),
total,
distribution: Object.fromEntries(
scenarioOrder.map((scenario) => [scenario, byScenario.get(scenario)?.length ?? 0]),
),
mappings: candidates.map((c) => ({
id: c.id,
path: c.relPath,
title: c.title,
currentScenario: c.currentScenario,
currentMode: c.currentMode,
currentTaskKind: c.currentTaskKind,
tags: c.tags,
proposedScenario: c.proposedScenario,
reason: c.reason,
})),
},
null,
2,
),
'utf8',
);
console.error(`\nMapping JSON written to ${path.relative(repoRoot, outFile)}`);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});