@adversity/coding-tool-x 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +12 -14
  3. package/dist/web/assets/index-Bu1oPcKu.js +4009 -0
  4. package/dist/web/assets/index-XSok7-mN.css +41 -0
  5. package/dist/web/index.html +2 -2
  6. package/package.json +5 -4
  7. package/src/config/default.js +1 -1
  8. package/src/index.js +2 -2
  9. package/src/server/api/agents.js +188 -0
  10. package/src/server/api/commands.js +261 -0
  11. package/src/server/api/config-export.js +122 -0
  12. package/src/server/api/config-templates.js +26 -5
  13. package/src/server/api/health-check.js +1 -89
  14. package/src/server/api/permissions.js +370 -0
  15. package/src/server/api/rules.js +188 -0
  16. package/src/server/api/skills.js +66 -14
  17. package/src/server/api/workspaces.js +30 -55
  18. package/src/server/index.js +7 -11
  19. package/src/server/services/agents-service.js +179 -1
  20. package/src/server/services/commands-service.js +231 -47
  21. package/src/server/services/config-export-service.js +209 -0
  22. package/src/server/services/config-templates-service.js +481 -107
  23. package/src/server/services/format-converter.js +506 -0
  24. package/src/server/services/health-check.js +1 -315
  25. package/src/server/services/permission-templates-service.js +339 -0
  26. package/src/server/services/repo-scanner-base.js +678 -0
  27. package/src/server/services/rules-service.js +179 -1
  28. package/src/server/services/skill-service.js +114 -61
  29. package/src/server/services/workspace-service.js +52 -1
  30. package/dist/web/assets/index-D1AYlFLZ.js +0 -3220
  31. package/dist/web/assets/index-aL3cKxSK.css +0 -41
  32. package/docs/CHANGELOG.md +0 -582
  33. package/docs/DIRECTORY_MIGRATION.md +0 -112
  34. package/docs/PROJECT_STRUCTURE.md +0 -396
@@ -5,15 +5,21 @@
5
5
  * 规则目录:
6
6
  * - 用户级: ~/.claude/rules/
7
7
  * - 项目级: .claude/rules/
8
+ *
9
+ * 支持从 GitHub 仓库扫描和安装规则
8
10
  */
9
11
 
10
12
  const fs = require('fs');
11
13
  const path = require('path');
12
14
  const os = require('os');
15
+ const { RepoScannerBase } = require('./repo-scanner-base');
13
16
 
14
17
  // 规则目录路径
15
18
  const USER_RULES_DIR = path.join(os.homedir(), '.claude', 'rules');
16
19
 
20
+ // 默认仓库源
21
+ const DEFAULT_REPOS = [];
22
+
17
23
  /**
18
24
  * 确保目录存在
19
25
  */
@@ -134,12 +140,94 @@ function scanRulesDir(dir, basePath, scope) {
134
140
  return rules;
135
141
  }
136
142
 
143
+ /**
144
+ * Rules 仓库扫描器
145
+ */
146
+ class RulesRepoScanner extends RepoScannerBase {
147
+ constructor() {
148
+ super({
149
+ type: 'rules',
150
+ installDir: USER_RULES_DIR,
151
+ markerFile: null, // 直接扫描 .md 文件
152
+ fileExtension: '.md',
153
+ defaultRepos: DEFAULT_REPOS
154
+ });
155
+ }
156
+
157
+ /**
158
+ * 获取并解析单个规则文件
159
+ */
160
+ async fetchAndParseItem(file, repo, baseDir) {
161
+ try {
162
+ // 计算相对路径
163
+ const relativePath = baseDir ? file.path.slice(baseDir.length + 1) : file.path;
164
+ const fileName = path.basename(file.path, '.md');
165
+ const directory = path.dirname(relativePath);
166
+
167
+ // 获取文件内容
168
+ const content = await this.fetchRawContent(repo, file.path);
169
+ const { frontmatter, body } = this.parseFrontmatter(content);
170
+
171
+ return {
172
+ key: `${repo.owner}/${repo.name}:${relativePath}`,
173
+ name: fileName,
174
+ fileName,
175
+ directory: directory === '.' ? null : directory,
176
+ scope: 'remote',
177
+ path: relativePath,
178
+ repoPath: file.path,
179
+ paths: frontmatter.paths || '',
180
+ body,
181
+ fullContent: content,
182
+ installed: this.isInstalled(relativePath),
183
+ readmeUrl: `https://github.com/${repo.owner}/${repo.name}/blob/${repo.branch}/${file.path}`,
184
+ repoOwner: repo.owner,
185
+ repoName: repo.name,
186
+ repoBranch: repo.branch,
187
+ repoDirectory: repo.directory || ''
188
+ };
189
+ } catch (err) {
190
+ console.warn(`[RulesRepoScanner] Parse rule ${file.path} error:`, err.message);
191
+ return null;
192
+ }
193
+ }
194
+
195
+ /**
196
+ * 检查规则是否已安装
197
+ */
198
+ isInstalled(relativePath) {
199
+ const fullPath = path.join(this.installDir, relativePath);
200
+ return fs.existsSync(fullPath);
201
+ }
202
+
203
+ /**
204
+ * 获取去重 key
205
+ */
206
+ getDedupeKey(item) {
207
+ return item.directory ? `${item.directory}/${item.fileName}`.toLowerCase() : item.fileName.toLowerCase();
208
+ }
209
+
210
+ /**
211
+ * 安装规则
212
+ */
213
+ async installRule(item) {
214
+ const repo = {
215
+ owner: item.repoOwner,
216
+ name: item.repoName,
217
+ branch: item.repoBranch
218
+ };
219
+
220
+ return this.installFromRepo(item.repoPath, repo, item.path);
221
+ }
222
+ }
223
+
137
224
  /**
138
225
  * Rules 服务类
139
226
  */
140
227
  class RulesService {
141
228
  constructor() {
142
229
  this.userRulesDir = USER_RULES_DIR;
230
+ this.repoScanner = new RulesRepoScanner();
143
231
  ensureDir(this.userRulesDir);
144
232
  }
145
233
 
@@ -172,6 +260,51 @@ class RulesService {
172
260
  };
173
261
  }
174
262
 
263
+ /**
264
+ * 获取所有规则(包括远程仓库)
265
+ */
266
+ async listAllRules(projectPath = null, forceRefresh = false) {
267
+ // 获取本地规则
268
+ const { rules: localRules, userCount, projectCount } = this.listRules(projectPath);
269
+
270
+ // 获取远程规则
271
+ let remoteRules = [];
272
+ try {
273
+ remoteRules = await this.repoScanner.listRemoteItems(forceRefresh);
274
+
275
+ // 更新安装状态
276
+ for (const rule of remoteRules) {
277
+ rule.installed = this.repoScanner.isInstalled(rule.path);
278
+ }
279
+ } catch (err) {
280
+ console.warn('[RulesService] Failed to fetch remote rules:', err.message);
281
+ }
282
+
283
+ // 合并列表(本地优先)
284
+ const allRules = [...localRules];
285
+ const localKeys = new Set(localRules.map(r =>
286
+ r.directory ? `${r.directory}/${r.fileName}`.toLowerCase() : r.fileName.toLowerCase()
287
+ ));
288
+
289
+ for (const remote of remoteRules) {
290
+ const key = remote.directory ? `${remote.directory}/${remote.fileName}`.toLowerCase() : remote.fileName.toLowerCase();
291
+ if (!localKeys.has(key)) {
292
+ allRules.push(remote);
293
+ }
294
+ }
295
+
296
+ // 排序
297
+ allRules.sort((a, b) => a.path.toLowerCase().localeCompare(b.path.toLowerCase()));
298
+
299
+ return {
300
+ rules: allRules,
301
+ total: allRules.length,
302
+ userCount,
303
+ projectCount,
304
+ remoteCount: remoteRules.length
305
+ };
306
+ }
307
+
175
308
  /**
176
309
  * 获取单个规则详情
177
310
  */
@@ -394,8 +527,53 @@ class RulesService {
394
527
  directories
395
528
  };
396
529
  }
530
+
531
+ // ==================== 仓库管理 ====================
532
+
533
+ /**
534
+ * 获取仓库列表
535
+ */
536
+ getRepos() {
537
+ return this.repoScanner.loadRepos();
538
+ }
539
+
540
+ /**
541
+ * 添加仓库
542
+ */
543
+ addRepo(repo) {
544
+ return this.repoScanner.addRepo(repo);
545
+ }
546
+
547
+ /**
548
+ * 删除仓库
549
+ */
550
+ removeRepo(owner, name, directory = '') {
551
+ return this.repoScanner.removeRepo(owner, name, directory);
552
+ }
553
+
554
+ /**
555
+ * 切换仓库启用状态
556
+ */
557
+ toggleRepo(owner, name, directory = '', enabled) {
558
+ return this.repoScanner.toggleRepo(owner, name, directory, enabled);
559
+ }
560
+
561
+ /**
562
+ * 从远程仓库安装规则
563
+ */
564
+ async installFromRemote(rule) {
565
+ return this.repoScanner.installRule(rule);
566
+ }
567
+
568
+ /**
569
+ * 卸载规则
570
+ */
571
+ uninstallRule(relativePath) {
572
+ return this.repoScanner.uninstall(relativePath);
573
+ }
397
574
  }
398
575
 
399
576
  module.exports = {
400
- RulesService
577
+ RulesService,
578
+ DEFAULT_REPOS
401
579
  };
@@ -13,10 +13,17 @@ const http = require('http');
13
13
  const { createWriteStream } = require('fs');
14
14
  const { pipeline } = require('stream/promises');
15
15
  const AdmZip = require('adm-zip');
16
+ const {
17
+ parseSkillContent,
18
+ detectSkillFormat,
19
+ convertSkillToCodex,
20
+ convertSkillToClaude
21
+ } = require('./format-converter');
16
22
 
17
23
  // 默认仓库源 - 只预设官方仓库,其他由用户手动添加
24
+ // directory 字段支持指定仓库子目录
18
25
  const DEFAULT_REPOS = [
19
- { owner: 'anthropics', name: 'skills', branch: 'main', enabled: true }
26
+ { owner: 'anthropics', name: 'skills', branch: 'main', directory: '', enabled: true }
20
27
  ];
21
28
 
22
29
  // 缓存有效期(5分钟)
@@ -70,10 +77,21 @@ class SkillService {
70
77
 
71
78
  /**
72
79
  * 添加仓库
80
+ * @param {Object} repo - 仓库配置
81
+ * @param {string} repo.owner - 仓库所有者
82
+ * @param {string} repo.name - 仓库名称
83
+ * @param {string} repo.branch - 分支名称
84
+ * @param {string} [repo.directory] - 扫描的子目录路径(可选)
85
+ * @param {boolean} repo.enabled - 是否启用
73
86
  */
74
87
  addRepo(repo) {
75
88
  const repos = this.loadRepos();
76
- const existingIndex = repos.findIndex(r => r.owner === repo.owner && r.name === repo.name);
89
+ // 使用 owner/name/directory 作为唯一标识
90
+ const existingIndex = repos.findIndex(r =>
91
+ r.owner === repo.owner &&
92
+ r.name === repo.name &&
93
+ (r.directory || '') === (repo.directory || '')
94
+ );
77
95
 
78
96
  if (existingIndex >= 0) {
79
97
  repos[existingIndex] = repo;
@@ -82,28 +100,52 @@ class SkillService {
82
100
  }
83
101
 
84
102
  this.saveRepos(repos);
103
+ // 清除缓存
104
+ this.skillsCache = null;
105
+ this.cacheTime = 0;
85
106
  return repos;
86
107
  }
87
108
 
88
109
  /**
89
110
  * 删除仓库
111
+ * @param {string} owner - 仓库所有者
112
+ * @param {string} name - 仓库名称
113
+ * @param {string} [directory=''] - 子目录路径
90
114
  */
91
- removeRepo(owner, name) {
115
+ removeRepo(owner, name, directory = '') {
92
116
  const repos = this.loadRepos();
93
- const filtered = repos.filter(r => !(r.owner === owner && r.name === name));
117
+ const filtered = repos.filter(r => !(
118
+ r.owner === owner &&
119
+ r.name === name &&
120
+ (r.directory || '') === directory
121
+ ));
94
122
  this.saveRepos(filtered);
123
+ // 清除缓存
124
+ this.skillsCache = null;
125
+ this.cacheTime = 0;
95
126
  return filtered;
96
127
  }
97
128
 
98
129
  /**
99
130
  * 切换仓库启用状态
131
+ * @param {string} owner - 仓库所有者
132
+ * @param {string} name - 仓库名称
133
+ * @param {string} [directory=''] - 子目录路径
134
+ * @param {boolean} enabled - 是否启用
100
135
  */
101
- toggleRepo(owner, name, enabled) {
136
+ toggleRepo(owner, name, directory = '', enabled) {
102
137
  const repos = this.loadRepos();
103
- const repo = repos.find(r => r.owner === owner && r.name === name);
138
+ const repo = repos.find(r =>
139
+ r.owner === owner &&
140
+ r.name === name &&
141
+ (r.directory || '') === directory
142
+ );
104
143
  if (repo) {
105
144
  repo.enabled = enabled;
106
145
  this.saveRepos(repos);
146
+ // 清除缓存
147
+ this.skillsCache = null;
148
+ this.cacheTime = 0;
107
149
  }
108
150
  return repos;
109
151
  }
@@ -229,6 +271,7 @@ class SkillService {
229
271
 
230
272
  /**
231
273
  * 从 GitHub 仓库获取技能列表(使用 Tree API 一次性获取)
274
+ * 支持指定子目录扫描
232
275
  */
233
276
  async fetchRepoSkills(repo) {
234
277
  const skills = [];
@@ -243,10 +286,21 @@ class SkillService {
243
286
  return skills;
244
287
  }
245
288
 
246
- // 找到所有 SKILL.md 文件
247
- const skillFiles = tree.tree.filter(item =>
248
- item.type === 'blob' && item.path.endsWith('/SKILL.md')
249
- );
289
+ // 获取基础目录(如果配置了 directory)
290
+ const baseDir = repo.directory || '';
291
+ const baseDirPrefix = baseDir ? `${baseDir}/` : '';
292
+
293
+ // 找到所有 SKILL.md 文件(如果配置了子目录,只扫描该目录下的)
294
+ const skillFiles = tree.tree.filter(item => {
295
+ if (item.type !== 'blob' || !item.path.endsWith('/SKILL.md')) {
296
+ return false;
297
+ }
298
+ // 如果配置了子目录,只返回该子目录下的文件
299
+ if (baseDir && !item.path.startsWith(baseDirPrefix)) {
300
+ return false;
301
+ }
302
+ return true;
303
+ });
250
304
 
251
305
  // 并行获取所有 SKILL.md 的内容(限制并发数)
252
306
  const batchSize = 5;
@@ -254,7 +308,7 @@ class SkillService {
254
308
  for (let i = 0; i < skillFiles.length; i += batchSize) {
255
309
  const batch = skillFiles.slice(i, i + batchSize);
256
310
  const results = await Promise.allSettled(
257
- batch.map(file => this.fetchAndParseSkill(file, repo))
311
+ batch.map(file => this.fetchAndParseSkill(file, repo, baseDir))
258
312
  );
259
313
 
260
314
  for (const result of results) {
@@ -273,26 +327,34 @@ class SkillService {
273
327
 
274
328
  /**
275
329
  * 获取并解析单个 SKILL.md
330
+ * @param {Object} file - GitHub tree 文件对象
331
+ * @param {Object} repo - 仓库配置
332
+ * @param {string} baseDir - 基础目录(用于计算相对路径)
276
333
  */
277
- async fetchAndParseSkill(file, repo) {
334
+ async fetchAndParseSkill(file, repo, baseDir = '') {
278
335
  try {
279
336
  // 从路径提取目录名 (e.g., "algorithmic-art/SKILL.md" -> "algorithmic-art")
280
- const directory = file.path.replace(/\/SKILL\.md$/, '');
337
+ const fullDirectory = file.path.replace(/\/SKILL\.md$/, '');
338
+
339
+ // 计算相对于 baseDir 的目录名(用于显示和安装)
340
+ const directory = baseDir ? fullDirectory.slice(baseDir.length + 1) : fullDirectory;
281
341
 
282
342
  // 使用 raw.githubusercontent.com 获取文件内容(不消耗 API 限额)
283
343
  const content = await this.fetchBlobContent(file.sha, repo, file.path);
284
344
  const metadata = this.parseSkillMd(content);
285
345
 
286
346
  return {
287
- key: `${repo.owner}/${repo.name}:${directory}`,
347
+ key: `${repo.owner}/${repo.name}:${fullDirectory}`,
288
348
  name: metadata.name || directory.split('/').pop(),
289
349
  description: metadata.description || '',
290
- directory,
350
+ directory, // 相对目录(用于安装)
351
+ fullDirectory, // 完整目录(用于从仓库下载)
291
352
  installed: this.isInstalled(directory),
292
- readmeUrl: `https://github.com/${repo.owner}/${repo.name}/tree/${repo.branch}/${directory}`,
353
+ readmeUrl: `https://github.com/${repo.owner}/${repo.name}/tree/${repo.branch}/${fullDirectory}`,
293
354
  repoOwner: repo.owner,
294
355
  repoName: repo.name,
295
356
  repoBranch: repo.branch,
357
+ repoDirectory: repo.directory || '', // 仓库配置的子目录
296
358
  license: metadata.license
297
359
  };
298
360
  } catch (err) {
@@ -545,55 +607,40 @@ class SkillService {
545
607
  }
546
608
 
547
609
  /**
548
- * 解析 SKILL.md 文件
610
+ * 解析 SKILL.md 文件(支持 Claude Code 和 Codex CLI 格式)
549
611
  */
550
612
  parseSkillMd(content) {
551
- const result = {
552
- name: null,
553
- description: null,
554
- license: null,
555
- allowedTools: [],
556
- metadata: {}
613
+ // 使用格式转换器统一解析
614
+ const parsed = parseSkillContent(content);
615
+
616
+ return {
617
+ name: parsed.name || null,
618
+ description: parsed.description || null,
619
+ license: parsed.license || null,
620
+ allowedTools: parsed.allowedTools ? [parsed.allowedTools] : [],
621
+ metadata: parsed.metadata || {},
622
+ shortDescription: parsed.shortDescription || null,
623
+ format: parsed.format
557
624
  };
625
+ }
558
626
 
559
- // 移除 BOM
560
- content = content.trim().replace(/^\uFEFF/, '');
561
-
562
- // 解析 YAML frontmatter
563
- const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
564
- if (!match) return result;
565
-
566
- const frontmatter = match[1];
567
-
568
- // 简单解析 YAML
569
- const lines = frontmatter.split('\n');
570
- for (const line of lines) {
571
- const colonIndex = line.indexOf(':');
572
- if (colonIndex === -1) continue;
573
-
574
- const key = line.slice(0, colonIndex).trim();
575
- let value = line.slice(colonIndex + 1).trim();
576
-
577
- // 去除引号
578
- if ((value.startsWith('"') && value.endsWith('"')) ||
579
- (value.startsWith("'") && value.endsWith("'"))) {
580
- value = value.slice(1, -1);
581
- }
627
+ /**
628
+ * 转换技能格式
629
+ * @param {string} content - 技能内容
630
+ * @param {string} targetFormat - 目标格式 ('claude' | 'codex')
631
+ */
632
+ convertSkillFormat(content, targetFormat) {
633
+ const sourceFormat = detectSkillFormat(content);
582
634
 
583
- switch (key) {
584
- case 'name':
585
- result.name = value;
586
- break;
587
- case 'description':
588
- result.description = value;
589
- break;
590
- case 'license':
591
- result.license = value;
592
- break;
593
- }
635
+ if (sourceFormat === targetFormat) {
636
+ return { content, warnings: [], format: targetFormat };
594
637
  }
595
638
 
596
- return result;
639
+ if (targetFormat === 'codex') {
640
+ return convertSkillToCodex(content);
641
+ } else {
642
+ return convertSkillToClaude(content);
643
+ }
597
644
  }
598
645
 
599
646
  /**
@@ -702,8 +749,11 @@ class SkillService {
702
749
 
703
750
  /**
704
751
  * 安装技能
752
+ * @param {string} directory - 本地安装目录(相对于 installDir)
753
+ * @param {Object} repo - 仓库配置
754
+ * @param {string} [fullDirectory] - 仓库中的完整路径(可选,默认与 directory 相同)
705
755
  */
706
- async installSkill(directory, repo) {
756
+ async installSkill(directory, repo, fullDirectory = null) {
707
757
  const dest = path.join(this.installDir, directory);
708
758
 
709
759
  // 已安装则跳过
@@ -711,6 +761,9 @@ class SkillService {
711
761
  return { success: true, message: 'Already installed' };
712
762
  }
713
763
 
764
+ // 使用 fullDirectory(仓库中的完整路径)或 directory(向后兼容)
765
+ const sourcePath = fullDirectory || directory;
766
+
714
767
  // 下载仓库 ZIP
715
768
  const zipUrl = `https://github.com/${repo.owner}/${repo.name}/archive/refs/heads/${repo.branch}.zip`;
716
769
  const tempDir = path.join(os.tmpdir(), `skill-${Date.now()}`);
@@ -736,10 +789,10 @@ class SkillService {
736
789
  }
737
790
 
738
791
  const repoDir = path.join(tempDir, extractedDirs[0]);
739
- const sourceDir = path.join(repoDir, directory);
792
+ const sourceDir = path.join(repoDir, sourcePath);
740
793
 
741
794
  if (!fs.existsSync(sourceDir)) {
742
- throw new Error(`Skill directory not found: ${directory}`);
795
+ throw new Error(`Skill directory not found: ${sourcePath}`);
743
796
  }
744
797
 
745
798
  // 复制到安装目录
@@ -4,6 +4,7 @@ const path = require('path');
4
4
  const { execSync } = require('child_process');
5
5
  const { PATHS } = require('../../config/paths');
6
6
  const configTemplatesService = require('./config-templates-service');
7
+ const permissionTemplatesService = require('./permission-templates-service');
7
8
 
8
9
  // 工作区配置文件路径
9
10
  const WORKSPACES_CONFIG = path.join(PATHS.base, 'workspaces.json');
@@ -151,7 +152,7 @@ function getGitWorktrees(repoPath) {
151
152
  * @param {Array} options.projects - 项目列表 [{sourcePath, name, createWorktree, branch}]
152
153
  */
153
154
  function createWorkspace(options) {
154
- const { name, description = '', baseDir, projects = [], configTemplateId } = options;
155
+ const { name, description = '', baseDir, projects = [], configTemplateId, permissionTemplate } = options;
155
156
 
156
157
  if (!name || name.trim() === '') {
157
158
  throw new Error('工作区名称不能为空');
@@ -299,6 +300,55 @@ function createWorkspace(options) {
299
300
  }
300
301
  }
301
302
 
303
+ // 应用权限模板(如果指定)
304
+ let permissionInfo = null;
305
+ if (permissionTemplate) {
306
+ try {
307
+ // 从权限模板服务获取模板
308
+ const template = permissionTemplatesService.getTemplateById(permissionTemplate);
309
+
310
+ if (template && template.permissions) {
311
+ // 为工作区中的每个项目应用权限设置
312
+ for (const proj of workspaceProjects) {
313
+ const projSettingsDir = path.join(proj.targetPath, '.claude');
314
+ const projSettingsFile = path.join(projSettingsDir, 'settings.json');
315
+
316
+ // 确保 .claude 目录存在
317
+ if (!fs.existsSync(projSettingsDir)) {
318
+ fs.mkdirSync(projSettingsDir, { recursive: true });
319
+ }
320
+
321
+ // 读取现有设置或创建新的
322
+ let settings = {};
323
+ if (fs.existsSync(projSettingsFile)) {
324
+ try {
325
+ settings = JSON.parse(fs.readFileSync(projSettingsFile, 'utf8'));
326
+ } catch (e) {
327
+ settings = {};
328
+ }
329
+ }
330
+
331
+ // 更新权限设置
332
+ settings.permissions = {
333
+ allow: template.permissions.allow || [],
334
+ deny: template.permissions.deny || []
335
+ };
336
+
337
+ // 保存设置
338
+ fs.writeFileSync(projSettingsFile, JSON.stringify(settings, null, 2), 'utf8');
339
+ }
340
+
341
+ permissionInfo = {
342
+ template: permissionTemplate,
343
+ appliedAt: new Date().toISOString()
344
+ };
345
+ }
346
+ } catch (permError) {
347
+ console.warn('应用权限模板失败:', permError.message);
348
+ // 不中断工作区创建流程
349
+ }
350
+ }
351
+
302
352
  // 保存工作区配置
303
353
  const workspaceId = generateWorkspaceId();
304
354
  const workspace = {
@@ -308,6 +358,7 @@ function createWorkspace(options) {
308
358
  path: workspacePath,
309
359
  projects: workspaceProjects,
310
360
  configTemplate: templateInfo,
361
+ permissionTemplate: permissionInfo,
311
362
  createdAt: new Date().toISOString(),
312
363
  lastUsed: new Date().toISOString()
313
364
  };