mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* 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>
444 lines
14 KiB
TypeScript
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);
|
|
});
|