| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109 |
- /**
- * 技能目录规范与共用小工具。
- *
- * 「插件与技能 → 技能」的**查看详情**与**新增/编辑**两个弹框必须保持同一套口径
- * (同样的目录规范、同样的 frontmatter 拆解、同样的体积展示),所以集中在这里,
- * 避免两边各写一份后悄悄漂移。
- */
- import type { SkillFile } from '../api/types';
- /** 可选规范子目录(顺序即页面展示顺序) */
- export const SKILL_STD_DIRS = [
- { key: 'scripts', desc: '自动化脚本或工具调用逻辑' },
- { key: 'references', desc: '长文档、规范、API 说明' },
- ] as const;
- /** 规范目录名集合:用来判断某个附属文件是否落在规范目录内 */
- export const SKILL_STD_DIR_KEYS: readonly string[] = SKILL_STD_DIRS.map((d) => d.key);
- /** 技能名合法字符(与后端 SkillUtil.isValidSkillName 对齐:小写字母/数字/短横线,1~64) */
- export const SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
- /** 文件大小展示 */
- export function formatFileSize(size?: number): string {
- if (size == null) return '';
- if (size < 1024) return `${size} B`;
- if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
- return `${(size / 1024 / 1024).toFixed(1)} MB`;
- }
- /**
- * 拆 SKILL.md 的 frontmatter:返回描述与正文;没有 frontmatter 时整段当正文。
- *
- * 后端 buildSkillMd 会把 description 用双引号包裹并转义,这里要还原,保证编辑往返一致。
- */
- export function splitSkillMd(raw: string): { description: string; body: string } {
- const text = raw.replace(/\r\n/g, '\n');
- const m = text.match(/^---\n([\s\S]*?)\n---\n?/);
- if (!m) return { description: '', body: text.trim() };
- let description = '';
- const dm = m[1].match(/^description:\s*(.*)$/m);
- if (dm) {
- description = dm[1]
- .trim()
- .replace(/^["']|["']$/g, '')
- .replace(/\\"/g, '"');
- }
- const body = text.slice(m[0].length).replace(/^\n+/, '').replace(/\s+$/, '');
- return { description, body };
- }
- /** 文件所属的第一层目录;根目录散文件返回空串 */
- export function topDirOf(path: string): string {
- const idx = path.indexOf('/');
- return idx > 0 ? path.slice(0, idx) : '';
- }
- /** 去掉规范目录前缀后的目录内相对路径(文件行展示用) */
- export function leafPath(path: string, dirKey: string): string {
- return path.startsWith(`${dirKey}/`) ? path.slice(dirKey.length + 1) : path;
- }
- /** 把附属文件按第一层目录分组,规范目录打头(空的也占位),其余目录按字典序排在后面 */
- export function groupSkillFiles(files: SkillFile[]) {
- const byDir = new Map<string, SkillFile[]>();
- for (const file of files) {
- const dir = topDirOf(file.path);
- const bucket = byDir.get(dir);
- if (bucket) bucket.push(file);
- else byDir.set(dir, [file]);
- }
- const rootFiles = byDir.get('') ?? [];
- byDir.delete('');
- const dirs = SKILL_STD_DIRS.map((d) => ({
- key: d.key as string,
- desc: d.desc as string,
- files: byDir.get(d.key) ?? [],
- }));
- for (const d of SKILL_STD_DIRS) byDir.delete(d.key);
- for (const [key, list] of [...byDir.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
- dirs.push({ key, desc: '', files: list });
- }
- return { dirs, rootFiles };
- }
- /** 图片后缀 → mime。只列位图:svg 是文本、后端会当 content 返回,走不到 base64 分支 */
- const IMAGE_MIME: Record<string, string> = {
- png: 'image/png',
- jpg: 'image/jpeg',
- jpeg: 'image/jpeg',
- gif: 'image/gif',
- webp: 'image/webp',
- bmp: 'image/bmp',
- ico: 'image/x-icon',
- };
- function extOf(path: string) {
- const idx = path.lastIndexOf('.');
- return idx > 0 ? path.slice(idx + 1).toLowerCase() : '';
- }
- /** 是否可用 <img> 预览的图片文件 */
- export function isImagePath(path: string): boolean {
- return !!IMAGE_MIME[extOf(path)];
- }
- /** 图片文件的 mime(非图片回落到 octet-stream) */
- export function imageMimeOf(path: string): string {
- return IMAGE_MIME[extOf(path)] || 'application/octet-stream';
- }
|