openclaw-weiyuan-init 1.0.117 → 1.0.119

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.
package/lib/commands.js CHANGED
@@ -60,6 +60,15 @@ const DEFAULT_SKILL_TSCONFIG = {
60
60
 
61
61
  const REQUIRED_SKILL_ACTIONS = ['project.list', 'task.list_all', 'context.candidates'];
62
62
 
63
+ function cleanCliRawText(raw) {
64
+ if (raw === undefined || raw === null) return '';
65
+ let text = String(raw).replace(/\r|\n/g, ' ').replace(/`/g, '').trim();
66
+ while ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
67
+ text = text.slice(1, -1).trim();
68
+ }
69
+ return text;
70
+ }
71
+
63
72
  function bundledSkillMetadataPath(fileName) {
64
73
  return path.join(__dirname, '..', 'workspace-weiyuan', 'weiyuan', fileName);
65
74
  }
@@ -457,7 +466,7 @@ function resolveWorkspacePath(workspaceOption) {
457
466
  function decodeInviteToken(token) {
458
467
  if (!token || typeof token !== 'string') return null;
459
468
  try {
460
- const normalized = token.replace(/-/g, '+').replace(/_/g, '/');
469
+ const normalized = cleanCliRawText(token).replace(/-/g, '+').replace(/_/g, '/');
461
470
  const pad = normalized.length % 4 === 0 ? '' : '='.repeat(4 - (normalized.length % 4));
462
471
  const raw = Buffer.from(normalized + pad, 'base64').toString('utf8');
463
472
  const parsed = JSON.parse(raw);
@@ -481,9 +490,10 @@ function decodeInviteToken(token) {
481
490
  }
482
491
 
483
492
  function normalizeServerUrl(raw) {
484
- if (!raw || typeof raw !== 'string') return '';
493
+ const cleaned = cleanCliRawText(raw);
494
+ if (!cleaned) return '';
485
495
  try {
486
- const url = new URL(raw.trim());
496
+ const url = new URL(cleaned);
487
497
  const p = (url.pathname || '/').replace(/\/+$/, '');
488
498
  if (!p || p === '/') url.pathname = '/api';
489
499
  url.hash = '';
@@ -493,6 +503,30 @@ function normalizeServerUrl(raw) {
493
503
  }
494
504
  }
495
505
 
506
+ function normalizeUpgradeUrl(raw) {
507
+ const cleaned = cleanCliRawText(raw);
508
+ if (!cleaned) return '';
509
+ try {
510
+ const url = new URL(cleaned);
511
+ url.hash = '';
512
+ return url.toString().replace(/\/+$/, '');
513
+ } catch (_) {
514
+ return '';
515
+ }
516
+ }
517
+
518
+ function normalizeDownloadUrl(raw) {
519
+ const cleaned = cleanCliRawText(raw);
520
+ if (!cleaned) return '';
521
+ try {
522
+ const url = new URL(cleaned);
523
+ url.hash = '';
524
+ return url.toString();
525
+ } catch (_) {
526
+ return '';
527
+ }
528
+ }
529
+
496
530
  function isAgentRegisterServerUrl(raw) {
497
531
  if (!raw || typeof raw !== 'string') return false;
498
532
  try {
@@ -641,6 +675,7 @@ async function printAgentConversationGuide(identityPath, projectId) {
641
675
  async function ensureSkillRuntime(weiyuanPath) {
642
676
  const pkgPath = path.join(weiyuanPath, 'package.json');
643
677
  const tsconfigPath = path.join(weiyuanPath, 'tsconfig.json');
678
+ const bundledManifestPath = bundledSkillMetadataPath('manifest.json');
644
679
  const capabilityCompatContent = `export const CAPABILITY_ACTION = {
645
680
  PROJECT_DELETE: "project.delete",
646
681
  TASK_DELETE: "task.delete",
@@ -656,8 +691,15 @@ async function ensureSkillRuntime(weiyuanPath) {
656
691
 
657
692
  export const CAPABILITY_ACTION_ALLOWLIST = new Set<string>(Object.values(CAPABILITY_ACTION))
658
693
  `;
694
+ let fallbackPackageJson = { ...DEFAULT_SKILL_PACKAGE_JSON };
695
+ try {
696
+ const bundledManifest = await fs.readJson(bundledManifestPath);
697
+ const bundledVersion = String(bundledManifest && bundledManifest.version || '').trim();
698
+ if (bundledVersion) fallbackPackageJson = { ...fallbackPackageJson, version: bundledVersion };
699
+ } catch (_) {
700
+ }
659
701
  if (!await fs.pathExists(pkgPath)) {
660
- await fs.writeJson(pkgPath, DEFAULT_SKILL_PACKAGE_JSON, { spaces: 2 });
702
+ await fs.writeJson(pkgPath, fallbackPackageJson, { spaces: 2 });
661
703
  }
662
704
  if (!await fs.pathExists(tsconfigPath)) {
663
705
  await fs.writeJson(tsconfigPath, DEFAULT_SKILL_TSCONFIG, { spaces: 2 });
@@ -706,10 +748,16 @@ async function runInit(options) {
706
748
  printBanner();
707
749
  const fixedMessages = getFixedMessages();
708
750
  let initIdentityInfo = null;
751
+ options = options || {};
752
+ options.server = cleanCliRawText(options.server || '');
753
+ options.upgrade = cleanCliRawText(options.upgrade || '');
754
+ options.download = cleanCliRawText(options.download || '');
755
+ options.invite = cleanCliRawText(options.invite || '');
756
+ options.project = cleanCliRawText(options.project || '');
757
+ options.code = cleanCliRawText(options.code || '');
709
758
 
710
759
  const invite = decodeInviteToken(options.invite);
711
760
  if (invite) {
712
- options.server = options.server || invite.api;
713
761
  options.upgrade = options.upgrade || invite.upgrade;
714
762
  options.project = options.project || invite.projectId;
715
763
  options.code = options.code || invite.code;
@@ -717,8 +765,8 @@ async function runInit(options) {
717
765
 
718
766
  const workspacePath = resolveWorkspacePath(options.workspace);
719
767
  const weiyuanPath = path.join(workspacePath, 'weiyuan');
720
- const upgradeBaseUrl = options.upgrade || DEFAULT_CONFIG.upgradeBaseUrl;
721
- const requestedDownloadUrl = options.download || DEFAULT_CONFIG.downloadUrl;
768
+ const upgradeBaseUrl = normalizeUpgradeUrl(options.upgrade || DEFAULT_CONFIG.upgradeBaseUrl) || DEFAULT_CONFIG.upgradeBaseUrl;
769
+ const requestedDownloadUrl = normalizeDownloadUrl(options.download || DEFAULT_CONFIG.downloadUrl);
722
770
  const serverCandidates = resolveServerCandidates(options, invite);
723
771
  let serverUrl = serverCandidates[0] || DEFAULT_CONFIG.serverUrl;
724
772
  const force = options.force || false;
@@ -810,42 +858,57 @@ async function runInit(options) {
810
858
  spinner = ora('检测到已有 weiyuan 运行目录,跳过下载与解压...').start();
811
859
  spinner.succeed('已复用现有 weiyuan 目录');
812
860
  } else {
813
- spinner = ora('解析下载包...').start();
814
- const source = await resolvePackageSource({
815
- downloadUrl: requestedDownloadUrl,
816
- upgradeBaseUrl,
817
- spinner
818
- });
819
- spinner.succeed(`下载源: ${source.downloadUrl}`);
820
- printReleaseNotes(source.releaseNotes);
821
-
822
- spinner = ora('下载 weiyuan skill...').start();
823
- const zipPath = path.join(weiyuanPath, source.zipFilename);
824
- const downloadSuccess = await downloadFile(source.downloadUrl, zipPath, spinner, source.sha256 || '');
825
- if (!downloadSuccess) {
826
- spinner.fail('下载失败');
827
- throw new Error('下载失败');
828
- }
829
- spinner.succeed(`下载完成: ${source.zipFilename}`);
830
-
831
- // 4. 解压
832
- spinner = ora('解压文件...').start();
833
- const extractSuccess = await extractZip(zipPath, weiyuanPath);
834
- if (!extractSuccess) {
835
- spinner.fail('解压失败');
836
- throw new Error('解压失败');
837
- }
838
- spinner.succeed('解压完成');
839
-
840
- // 5. 清理
841
- spinner = ora('清理临时文件...').start();
842
- try {
843
- await fs.remove(zipPath);
844
- await persistReleaseNotes(weiyuanPath, source.releaseNotes || null);
845
- spinner.succeed('清理完成');
846
- } catch (error) {
847
- spinner.warn('清理失败,可手动删除');
861
+ const maxDownloadAttempts = 2;
862
+ let downloadCompleted = false;
863
+ let lastDownloadError = null;
864
+ for (let attempt = 1; attempt <= maxDownloadAttempts; attempt += 1) {
865
+ let source = null;
866
+ let zipPath = '';
867
+ try {
868
+ spinner = ora(attempt > 1 ? `重新解析下载包(第 ${attempt} 次)...` : '解析下载包...').start();
869
+ source = await resolvePackageSource({
870
+ downloadUrl: requestedDownloadUrl,
871
+ upgradeBaseUrl,
872
+ spinner
873
+ });
874
+ spinner.succeed(`下载源: ${source.downloadUrl}`);
875
+ printReleaseNotes(source.releaseNotes);
876
+
877
+ spinner = ora(attempt > 1 ? `重新下载 weiyuan skill(第 ${attempt} 次)...` : '下载 weiyuan skill...').start();
878
+ zipPath = path.join(weiyuanPath, source.zipFilename);
879
+ await fs.remove(zipPath).catch(() => {});
880
+ const downloadSuccess = await downloadFile(source.downloadUrl, zipPath, spinner, source.sha256 || '');
881
+ if (!downloadSuccess) throw new Error('下载失败');
882
+ spinner.succeed(`下载完成: ${source.zipFilename}`);
883
+
884
+ spinner = ora('解压文件...').start();
885
+ const extractSuccess = await extractZip(zipPath, weiyuanPath);
886
+ if (!extractSuccess) throw new Error('解压失败');
887
+ spinner.succeed('解压完成');
888
+
889
+ spinner = ora('清理临时文件...').start();
890
+ try {
891
+ await fs.remove(zipPath);
892
+ await persistReleaseNotes(weiyuanPath, source.releaseNotes || null);
893
+ spinner.succeed('清理完成');
894
+ } catch (error) {
895
+ spinner.warn('清理失败,可手动删除');
896
+ }
897
+ downloadCompleted = true;
898
+ lastDownloadError = null;
899
+ break;
900
+ } catch (error) {
901
+ lastDownloadError = error;
902
+ if (zipPath) await fs.remove(zipPath).catch(() => {});
903
+ const msg = String(error && error.message || error || '');
904
+ const canRetry = attempt < maxDownloadAttempts && msg.includes('skill_package_hash_mismatch');
905
+ if (spinner) {
906
+ spinner.warn(canRetry ? '下载包哈希校验未通过,正在重新获取最新版本后重试...' : `下载失败:${msg || 'unknown_error'}`);
907
+ }
908
+ if (!canRetry) throw error;
909
+ }
848
910
  }
911
+ if (!downloadCompleted && lastDownloadError) throw lastDownloadError;
849
912
  }
850
913
 
851
914
  // 6. 准备 CLI 运行时
package/lib/downloader.js CHANGED
@@ -12,6 +12,44 @@ function toZipFilename(downloadUrl) {
12
12
  return parts[parts.length - 1] || 'weiyuan-openclaw-skill.zip';
13
13
  }
14
14
 
15
+ function parseVersionFromZipFilename(zipFilename) {
16
+ const match = /-v(\d+\.\d+\.\d+)\.zip$/i.exec(String(zipFilename || '').trim());
17
+ return match ? match[1] : '';
18
+ }
19
+
20
+ function buildFallbackReleaseNotes(version) {
21
+ const normalizedVersion = String(version || '').trim();
22
+ if (!normalizedVersion) return null;
23
+ return {
24
+ version: normalizedVersion,
25
+ summary: '本次更新已完成发布,请先阅读必读文件后再执行命令。',
26
+ changes: [
27
+ '已同步最新微元系统运行时与规则文件。',
28
+ '执行任何命令前,请先阅读 CHANGELOG_OPENCLAW.md、release-notes/latest.json、docs/OPENCLAW_WEIYUAN_CLI_QUICKSTART.md、docs/WEIYUAN_AGENT_RULES.md。'
29
+ ],
30
+ releasedAt: new Date().toISOString()
31
+ };
32
+ }
33
+
34
+ function normalizeReleaseNotesForResolvedVersion(releaseNotes, resolvedVersion) {
35
+ const version = String(resolvedVersion || '').trim();
36
+ if (!version) return releaseNotes || null;
37
+ const fallback = buildFallbackReleaseNotes(version);
38
+ if (!releaseNotes || typeof releaseNotes !== 'object') return fallback;
39
+ const notesVersion = String(releaseNotes.version || '').trim();
40
+ const summary = String(releaseNotes.summary || '').trim();
41
+ const changes = Array.isArray(releaseNotes.changes)
42
+ ? releaseNotes.changes.map((item) => String(item || '').trim()).filter(Boolean)
43
+ : [];
44
+ return {
45
+ version,
46
+ summary: summary || fallback.summary,
47
+ changes: changes.length > 0 ? changes : fallback.changes,
48
+ releasedAt: String(releaseNotes.releasedAt || '').trim() || fallback.releasedAt,
49
+ sourceVersion: notesVersion || undefined
50
+ };
51
+ }
52
+
15
53
  async function fetchLatestReleaseNotes(base, nonce) {
16
54
  const notesUrl = `${base}/RELEASE_NOTES_LATEST.json?nocache=${nonce}`;
17
55
  try {
@@ -34,7 +72,16 @@ async function fetchLatestReleaseNotes(base, nonce) {
34
72
 
35
73
  async function resolvePackageSource({ downloadUrl, upgradeBaseUrl, spinner = null }) {
36
74
  if (downloadUrl) {
37
- return { downloadUrl, zipFilename: toZipFilename(downloadUrl), version: null, sha256: '', from: 'download_url', releaseNotes: null };
75
+ const zipFilename = toZipFilename(downloadUrl);
76
+ const version = parseVersionFromZipFilename(zipFilename);
77
+ return {
78
+ downloadUrl,
79
+ zipFilename,
80
+ version: version || null,
81
+ sha256: '',
82
+ from: 'download_url',
83
+ releaseNotes: normalizeReleaseNotesForResolvedVersion(null, version)
84
+ };
38
85
  }
39
86
 
40
87
  const base = trimSlash(upgradeBaseUrl);
@@ -52,7 +99,14 @@ async function resolvePackageSource({ downloadUrl, upgradeBaseUrl, spinner = nul
52
99
  if (spinner) {
53
100
  spinner.text = `已解析最新版本: v${version}`;
54
101
  }
55
- return { downloadUrl: resolvedDownloadUrl, zipFilename, version, sha256, from: 'latest_json', releaseNotes };
102
+ return {
103
+ downloadUrl: resolvedDownloadUrl,
104
+ zipFilename,
105
+ version,
106
+ sha256,
107
+ from: 'latest_json',
108
+ releaseNotes: normalizeReleaseNotesForResolvedVersion(releaseNotes, version)
109
+ };
56
110
  }
57
111
  } catch (_e) {
58
112
  // fallback to txt
@@ -66,7 +120,14 @@ async function resolvePackageSource({ downloadUrl, upgradeBaseUrl, spinner = nul
66
120
  if (spinner) {
67
121
  spinner.text = `已解析最新版本: v${version}`;
68
122
  }
69
- return { downloadUrl: resolvedDownloadUrl, zipFilename, version, sha256: '', from: 'latest_version', releaseNotes };
123
+ return {
124
+ downloadUrl: resolvedDownloadUrl,
125
+ zipFilename,
126
+ version,
127
+ sha256: '',
128
+ from: 'latest_version',
129
+ releaseNotes: normalizeReleaseNotesForResolvedVersion(releaseNotes, version)
130
+ };
70
131
  }
71
132
 
72
133
  async function downloadFile(url, outputPath, spinner = null, expectedSha256 = '') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-weiyuan-init",
3
- "version": "1.0.117",
3
+ "version": "1.0.119",
4
4
  "description": "OpenClaw Weiyuan Skill 一键初始化工具",
5
5
  "main": "bin/cli.js",
6
6
  "bin": {
@@ -145,7 +145,7 @@ npm run weiyuan -- doc sync-check --project <projectId>
145
145
 
146
146
  说明:
147
147
 
148
- - 文件上传不再通过 CLI 执行,请直接在项目看板中点击上传按钮。
148
+ - 文件上传已恢复 CLI 直传能力,可使用 `doc upload --project <projectId> --file <路径>`;未显式提供 `--name` 或 `--type` 时会自动补默认值。
149
149
  - 文件删除保留 CLI 能力,智能体 CLI 与小精灵 CLI 都支持删除文件。
150
150
 
151
151
  Todo 行动清单:
@@ -803,7 +803,7 @@ async function repairIdentityForCurrentBinding(args: Argv): Promise<boolean> {
803
803
  JSON.stringify(nextJoinedProjectIds) !== JSON.stringify(identity.joinedProjectIds ?? []) ||
804
804
  nextCurrentProjectId !== String(identity.currentProjectId || "").trim() ||
805
805
  JSON.stringify(nextProjectCursors) !== JSON.stringify(identity.projectCursors ?? {})
806
- if (!changed) return false
806
+ if (!changed) return true
807
807
  await backupCurrentIdentityIfExists(filePath)
808
808
  await writeIdentity(filePath, nextIdentity)
809
809
  return true
@@ -5067,6 +5067,32 @@ function parseIssueAttachmentPaths(raw?: string): string[] {
5067
5067
  .slice(0, ISSUE_ATTACHMENT_MAX_FILES)
5068
5068
  }
5069
5069
 
5070
+ const DOC_UPLOAD_MIME_MAP: Record<string, string> = {
5071
+ ".txt": "text/plain",
5072
+ ".md": "text/markdown",
5073
+ ".markdown": "text/markdown",
5074
+ ".json": "application/json",
5075
+ ".csv": "text/csv",
5076
+ ".pdf": "application/pdf",
5077
+ ".zip": "application/zip",
5078
+ ".doc": "application/msword",
5079
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
5080
+ ".xls": "application/vnd.ms-excel",
5081
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
5082
+ ".ppt": "application/vnd.ms-powerpoint",
5083
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
5084
+ ".png": "image/png",
5085
+ ".jpg": "image/jpeg",
5086
+ ".jpeg": "image/jpeg",
5087
+ ".gif": "image/gif",
5088
+ ".webp": "image/webp",
5089
+ }
5090
+
5091
+ function guessDocMimeType(filePath: string): string {
5092
+ const ext = path.extname(String(filePath || "")).toLowerCase()
5093
+ return DOC_UPLOAD_MIME_MAP[ext] ?? "application/octet-stream"
5094
+ }
5095
+
5070
5096
  async function loadIssueAttachments(attachmentPath?: string): Promise<Array<{ fileName: string; mimeType: string; contentBase64: string }>> {
5071
5097
  const paths = parseIssueAttachmentPaths(attachmentPath)
5072
5098
  if (!paths.length) return []
@@ -5760,8 +5786,93 @@ async function cmdDocList(args: Argv): Promise<void> {
5760
5786
  }
5761
5787
 
5762
5788
  async function cmdDocCreate(args: Argv): Promise<void> {
5763
- void args
5764
- throw new Error("doc_upload_board_only")
5789
+ const filePath = optString(args.flags, "identity") ?? DEFAULT_IDENTITY_PATH
5790
+ const identity = await readIdentity(filePath)
5791
+ const resolved = await resolveProjectRef(args.flags, identity)
5792
+ const interactive = Boolean(args.flags.interactive || args.flags.prompt)
5793
+ const sourceFileRaw = interactive
5794
+ ? await askRequired("请选择要上传的文件路径: ", optString(args.flags, "file"))
5795
+ : mustString(args.flags, "file")
5796
+ const sourceFile = path.resolve(sourceFileRaw)
5797
+ let stat
5798
+ try {
5799
+ stat = await fs.stat(sourceFile)
5800
+ } catch {
5801
+ throw new Error(`invalid_doc_file_path: ${sourceFileRaw}`)
5802
+ }
5803
+ if (!stat.isFile()) throw new Error(`invalid_doc_file_path: ${sourceFileRaw}`)
5804
+ const bytes = await fs.readFile(sourceFile)
5805
+ const uploadNameFallback = path.basename(sourceFile)
5806
+ const uploadTypeFallback = guessDocMimeType(sourceFile)
5807
+ const docName = interactive
5808
+ ? await askRequired("请输入上传后显示的文件名: ", optString(args.flags, "name") ?? optString(args.flags, "title") ?? uploadNameFallback)
5809
+ : String(optString(args.flags, "name") ?? optString(args.flags, "title") ?? uploadNameFallback).trim()
5810
+ const docType = interactive
5811
+ ? await askRequired("请输入文件类型(可留空走自动识别): ", optString(args.flags, "type") ?? optString(args.flags, "docType") ?? uploadTypeFallback)
5812
+ : String(optString(args.flags, "type") ?? optString(args.flags, "docType") ?? uploadTypeFallback).trim()
5813
+ const fileSize = bytes.length
5814
+ if (fileSize > MAX_SHARED_DOC_FILE_BYTES) {
5815
+ throw new Error("doc_file_too_large: 单文件超过 10MB,请压缩后重试。")
5816
+ }
5817
+ const contentHash = sha256Hex(bytes)
5818
+ const storageType = optString(args.flags, "storageType")
5819
+ const storageProvider = optString(args.flags, "storageProvider")
5820
+ const storagePath = optString(args.flags, "storagePath") ?? sourceFile
5821
+ const tags = String(optString(args.flags, "tags") ?? "")
5822
+ .split(",")
5823
+ .map((x) => x.trim())
5824
+ .filter(Boolean)
5825
+ const relatedTaskIds = String(optString(args.flags, "tasks") ?? optString(args.flags, "task") ?? "")
5826
+ .split(",")
5827
+ .map((x) => x.trim())
5828
+ .filter(Boolean)
5829
+ const version = optString(args.flags, "version") ?? "v1"
5830
+ const description = optString(args.flags, "desc") ?? optString(args.flags, "description")
5831
+ const changeSummary = optString(args.flags, "summary")
5832
+ const idempotencyKey = createRequestId(`doc_upload_${path.basename(sourceFile)}`)
5833
+ const { status, json } = await signedRequest({
5834
+ serverBaseUrl: identity.serverBaseUrl,
5835
+ method: "POST",
5836
+ pathWithQuery: `/v1/projects/${resolved.projectId}/docs`,
5837
+ lobsterId: identity.lobsterId,
5838
+ secretKeyBase64: identity.secretKeyBase64,
5839
+ idempotencyKey,
5840
+ body: {
5841
+ docName,
5842
+ docType,
5843
+ fileSize,
5844
+ contentHash,
5845
+ contentBase64: bytes.toString("base64"),
5846
+ storageLocation: { type: storageType ?? undefined, provider: storageProvider, path: storagePath },
5847
+ version,
5848
+ relatedTaskIds,
5849
+ tags,
5850
+ description,
5851
+ changeSummary,
5852
+ },
5853
+ })
5854
+ if (status !== 200) {
5855
+ const msg = String((json as any)?.msg ?? (json as any)?.message ?? "")
5856
+ if (status === 409 && msg.includes("storage_quota_exceeded")) {
5857
+ throw new Error("storage_quota_exceeded: 存储空间已达上限,请联系 founder 升级容量后再上传。")
5858
+ }
5859
+ if (status === 409 && msg === "invalid_storage_profile") {
5860
+ throw new Error("invalid_storage_profile: 当前项目存储配置无效,请联系 founder 检查项目存储设置。")
5861
+ }
5862
+ throw new Error(JSON.stringify(json))
5863
+ }
5864
+ print({
5865
+ ok: true,
5866
+ projectId: resolved.projectId,
5867
+ projectName: resolved.projectName ?? "",
5868
+ docName,
5869
+ docType,
5870
+ sourceFile,
5871
+ fileSize,
5872
+ contentHash,
5873
+ uploadedAt: Date.now(),
5874
+ ...(json.data && typeof json.data === "object" ? (json.data as Record<string, unknown>) : {}),
5875
+ })
5765
5876
  }
5766
5877
 
5767
5878
  async function cmdDocDetail(args: Argv): Promise<void> {
@@ -18,7 +18,9 @@ function toUserError(raw: string): string {
18
18
  if (msg.includes("interactive_input_required")) return "必填内容不能为空,请补充参数后重试。"
19
19
  if (msg.includes("missing_project_name")) return "项目名不能为空,请通过 --name 提供有效项目名。"
20
20
  if (msg.includes("project_name_too_long")) return "项目名不能超过80个字符,请缩短后重试。"
21
- if (msg.includes("doc_upload_board_only")) return "文件上传已改为只支持在项目看板里点击上传按钮完成,CLI 不再执行上传。"
21
+ if (msg.includes("doc_upload_board_only")) return "当前运行时版本过旧,文件上传仍被禁用。请先更新微元系统后再试。"
22
+ if (msg.includes("invalid_doc_file_path")) return "上传文件路径不存在,或该路径不是一个可读取的文件。请检查 --file 后重试。"
23
+ if (msg.includes("invalid_storage_profile")) return "当前项目的存储配置异常,暂时无法上传文件。请联系项目 founder 检查存储设置。"
22
24
  if (msg.includes("invalid_project_name")) return "项目名仅支持中文/英文/数字/空格及 -_.()·,且不能包含HTML标签。"
23
25
  if (msg.includes("task_title_contains_html")) return "任务标题不能包含HTML标签(如 <script>)。"
24
26
  if (msg.includes("task_title_too_long")) return "任务标题不能超过120个字符,请缩短后重试。"
@@ -362,6 +362,90 @@ function extractNicknameFromText(text: string): string | undefined {
362
362
  return v || undefined
363
363
  }
364
364
 
365
+ function extractMemberProfileFieldFromText(
366
+ text: string,
367
+ field: "roleTitle" | "specialties" | "experienceSummary" | "strengthsSummary" | "taskPreferenceTags"
368
+ ): string | undefined {
369
+ const patterns: RegExp[] =
370
+ field === "roleTitle"
371
+ ? [
372
+ /(?:设置|修改|更改|更新)(?:我的)?(?:团队)?角色(?:与能力)?(?:为|叫|成)?\s*[“"']?([^”"'\n]+)[”"']?$/u,
373
+ /(?:我的)?(?:团队)?角色(?:是|为|叫|改成|改为)\s*[“"']?([^”"'\n]+)[”"']?$/u,
374
+ ]
375
+ : field === "specialties"
376
+ ? [
377
+ /(?:设置|修改|更改|更新)(?:我的)?(?:擅长方向|擅长|专长|特长方向)(?:为|成)?\s*[“"']?([^”"'\n]+)[”"']?$/u,
378
+ /(?:我的)?(?:擅长方向|擅长|专长|特长方向)(?:是|为|改成|改为)\s*[“"']?([^”"'\n]+)[”"']?$/u,
379
+ ]
380
+ : field === "experienceSummary"
381
+ ? [
382
+ /(?:设置|修改|更改|更新)(?:我的)?(?:经历概述|经历简介|经验概述|经验简介|经历)(?:为|成)?\s*[“"']?([^”"'\n]+)[”"']?$/u,
383
+ /(?:我的)?(?:经历概述|经历简介|经验概述|经验简介|经历)(?:是|为|改成|改为)\s*[“"']?([^”"'\n]+)[”"']?$/u,
384
+ ]
385
+ : field === "strengthsSummary"
386
+ ? [
387
+ /(?:设置|修改|更改|更新)(?:我的)?(?:特长概述|优势概述|优势简介|特长简介|特长)(?:为|成)?\s*[“"']?([^”"'\n]+)[”"']?$/u,
388
+ /(?:我的)?(?:特长概述|优势概述|优势简介|特长简介|特长)(?:是|为|改成|改为)\s*[“"']?([^”"'\n]+)[”"']?$/u,
389
+ ]
390
+ : [
391
+ /(?:设置|修改|更改|更新)(?:我的)?(?:偏好任务标签|任务偏好标签|偏好标签|任务标签)(?:为|成)?\s*[“"']?([^”"'\n]+)[”"']?$/u,
392
+ /(?:我的)?(?:偏好任务标签|任务偏好标签|偏好标签|任务标签)(?:是|为|改成|改为)\s*[“"']?([^”"'\n]+)[”"']?$/u,
393
+ ]
394
+ for (const pattern of patterns) {
395
+ const value = String(text.match(pattern)?.[1] ?? "").trim()
396
+ if (value) return value
397
+ }
398
+ return undefined
399
+ }
400
+
401
+ function hasMemberProfileCardIntent(text: string): boolean {
402
+ const compact = text.toLowerCase().replace(/\s+/g, "")
403
+ return (
404
+ compact.includes("成员角色卡片") ||
405
+ compact.includes("角色卡片") ||
406
+ compact.includes("成员卡片") ||
407
+ compact.includes("编辑成员信息") ||
408
+ compact.includes("修改成员信息") ||
409
+ compact.includes("设置角色卡片") ||
410
+ compact.includes("角色与能力") ||
411
+ compact.includes("我的角色") ||
412
+ compact.includes("修改角色") ||
413
+ compact.includes("更新角色") ||
414
+ compact.includes("团队角色") ||
415
+ compact.includes("擅长方向") ||
416
+ compact.includes("我的擅长") ||
417
+ compact.includes("我的专长") ||
418
+ compact.includes("经历概述") ||
419
+ compact.includes("经历简介") ||
420
+ compact.includes("经验概述") ||
421
+ compact.includes("我的经历") ||
422
+ compact.includes("特长概述") ||
423
+ compact.includes("优势概述") ||
424
+ compact.includes("我的特长") ||
425
+ compact.includes("我的优势") ||
426
+ compact.includes("偏好任务标签") ||
427
+ compact.includes("任务偏好标签")
428
+ )
429
+ }
430
+
431
+ function extractMemberRemarkIntent(text: string): { target?: string; remark?: string; clear?: boolean } | undefined {
432
+ const trimmed = String(text || "").trim()
433
+ if (!trimmed) return undefined
434
+ const clearMatch =
435
+ trimmed.match(/(?:清空|删除|移除|去掉)(?:我对)?\s*([^,,\s]+)\s*(?:的)?备注$/u) ??
436
+ trimmed.match(/(?:把)?\s*([^,,\s]+)\s*(?:的)?备注(?:清空|删除|移除|去掉)$/u)
437
+ if (clearMatch?.[1]) {
438
+ return { target: clearMatch[1].trim(), clear: true, remark: "-" }
439
+ }
440
+ const saveMatch =
441
+ trimmed.match(/(?:给|帮我给|把)?\s*([^,,\s]+)\s*(?:的)?备注(?:为|成)?\s*([^\n]+)$/u) ??
442
+ trimmed.match(/(?:把)?\s*([^,,\s]+)\s*(?:备注为|备注成)\s*([^\n]+)$/u)
443
+ if (saveMatch?.[1]) {
444
+ return { target: saveMatch[1].trim(), remark: String(saveMatch[2] ?? "").trim() }
445
+ }
446
+ return undefined
447
+ }
448
+
365
449
  function extractProjectNameForCreate(text: string): string | undefined {
366
450
  const m =
367
451
  text.match(/(?:创建|新建)(?:一个|个)?[“"'《【((]?\s*([^”"'》】))\n]+?项目)\s*(?:吧|呀|呢|的)?$/) ??
@@ -1761,22 +1845,33 @@ function fromText(input: SkillInput): string[] {
1761
1845
  if (!nickname) return ["member-profile-edit", "--identity", identity, "--project", input.projectId]
1762
1846
  return ["member-profile-set", "--identity", identity, "--project", input.projectId, "--nickname", nickname]
1763
1847
  }
1764
- if (
1765
- compact.includes("成员角色卡片") ||
1766
- compact.includes("角色卡片") ||
1767
- compact.includes("成员卡片") ||
1768
- compact.includes("编辑成员信息") ||
1769
- compact.includes("修改成员信息") ||
1770
- compact.includes("设置角色卡片")
1771
- ) {
1848
+ if (hasMemberProfileCardIntent(text)) {
1772
1849
  if (!input.projectId) return ["context", "candidates", "--identity", identity]
1773
- return ["member-profile-edit", "--identity", identity, "--project", input.projectId]
1850
+ const roleTitle = extractMemberProfileFieldFromText(text, "roleTitle")
1851
+ const specialties = extractMemberProfileFieldFromText(text, "specialties")
1852
+ const experienceSummary = extractMemberProfileFieldFromText(text, "experienceSummary")
1853
+ const strengthsSummary = extractMemberProfileFieldFromText(text, "strengthsSummary")
1854
+ const taskPreferenceTags = extractMemberProfileFieldFromText(text, "taskPreferenceTags")
1855
+ const argv = ["member-profile-edit", "--identity", identity, "--project", input.projectId]
1856
+ if (roleTitle) argv.push("--roleTitle", roleTitle)
1857
+ if (specialties) argv.push("--specialties", specialties)
1858
+ if (experienceSummary) argv.push("--experienceSummary", experienceSummary)
1859
+ if (strengthsSummary) argv.push("--strengthsSummary", strengthsSummary)
1860
+ if (taskPreferenceTags) argv.push("--taskPreferenceTags", taskPreferenceTags)
1861
+ return argv
1774
1862
  }
1775
- if (compact.includes("成员备注") || compact.includes("备注成员") || compact.includes("给成员备注") || compact.includes("修改备注")) {
1863
+ if (
1864
+ compact.includes("成员备注") ||
1865
+ compact.includes("备注成员") ||
1866
+ compact.includes("给成员备注") ||
1867
+ compact.includes("修改备注") ||
1868
+ compact.includes("清空备注") ||
1869
+ compact.includes("删除备注")
1870
+ ) {
1776
1871
  if (!input.projectId) return ["context", "candidates", "--identity", identity]
1777
- const m = text.match(/(?:给|帮我给)?\s*([^,,\s]+)\s*(?:备注为|备注成|备注)\s*([^\n]+)$/)
1778
- const target = (m?.[1] ?? "").trim()
1779
- const remark = (m?.[2] ?? "").trim()
1872
+ const remarkIntent = extractMemberRemarkIntent(text)
1873
+ const target = String(remarkIntent?.target ?? "").trim()
1874
+ const remark = String(remarkIntent?.remark ?? "").trim()
1780
1875
  if (target && remark) {
1781
1876
  return ["panel", "member-remark", "--identity", identity, "--project", input.projectId, "--target", target, "--remark", remark]
1782
1877
  }
@@ -2385,8 +2480,20 @@ function fromText(input: SkillInput): string[] {
2385
2480
  }
2386
2481
  if (lower.includes("上传文件") || lower.includes("上传文档") || lower.includes("新增文档")) {
2387
2482
  if (!input.projectId) return ["context", "candidates", "--identity", identity]
2388
- if (!input.file || !input.title || !input.docType) return ["doc", "upload", "--identity", identity, "--project", input.projectId, "--interactive"]
2389
- return ["doc", "upload", "--identity", identity, "--project", input.projectId, "--name", input.title, "--type", input.docType, "--file", input.file]
2483
+ if (!input.file) return ["doc", "upload", "--identity", identity, "--project", input.projectId, "--interactive"]
2484
+ return [
2485
+ "doc",
2486
+ "upload",
2487
+ "--identity",
2488
+ identity,
2489
+ "--project",
2490
+ input.projectId,
2491
+ "--file",
2492
+ input.file,
2493
+ ...(input.title ? ["--name", input.title] : []),
2494
+ ...(input.docType ? ["--type", input.docType] : []),
2495
+ ...(input.desc ? ["--desc", input.desc] : []),
2496
+ ]
2390
2497
  }
2391
2498
  if (lower.includes("老规矩") || lower.includes("上次那套")) {
2392
2499
  if (input.projectId) return ["capsule", "recommend", "--identity", identity, "--project", input.projectId, "--query", input.query ?? ""]
@@ -2659,8 +2766,8 @@ function fromAction(input: SkillInput): string[] {
2659
2766
  ]
2660
2767
  case "doc.upload":
2661
2768
  if (!input.projectId) throw new Error("missing_projectId")
2662
- if (!input.file || !input.title || !input.docType) {
2663
- return ["doc", "upload", "--identity", identity, "--project", input.projectId, "--interactive", ...(input.title ? ["--name", input.title] : []), ...(input.docType ? ["--type", input.docType] : []), ...(input.file ? ["--file", input.file] : [])]
2769
+ if (!input.file) {
2770
+ return ["doc", "upload", "--identity", identity, "--project", input.projectId, "--interactive", ...(input.title ? ["--name", input.title] : []), ...(input.docType ? ["--type", input.docType] : [])]
2664
2771
  }
2665
2772
  return [
2666
2773
  "doc",
@@ -2669,12 +2776,10 @@ function fromAction(input: SkillInput): string[] {
2669
2776
  identity,
2670
2777
  "--project",
2671
2778
  input.projectId,
2672
- "--name",
2673
- input.title,
2674
- "--type",
2675
- input.docType,
2676
2779
  "--file",
2677
2780
  input.file,
2781
+ ...(input.title ? ["--name", input.title] : []),
2782
+ ...(input.docType ? ["--type", input.docType] : []),
2678
2783
  ...(input.desc ? ["--desc", input.desc] : []),
2679
2784
  ]
2680
2785
  case "doc.delete":