@qmuse/qmuse-cli 1.0.9-dev.4 → 1.0.10-dev.1

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/dist/cli.js CHANGED
@@ -7,12 +7,13 @@ import { recordOperation, withOperationLog } from './operation-log.js';
7
7
  * 用法见 `qmuse help`。
8
8
  */
9
9
  import { loadConfig, saveConfig, deleteSession, findConfigPath } from './config.js';
10
- import { hasAuth } from './client.js';
10
+ import { hasAuth, apiCall } from './client.js';
11
11
  import { log } from './logger.js';
12
12
  import * as api from './api.js';
13
13
  import * as login from './login.js';
14
14
  import { prompt, closePrompt } from './login.js';
15
- import { readFileSync } from 'node:fs';
15
+ import { randomBytes } from 'node:crypto';
16
+ import { spawn } from 'node:child_process';
16
17
  import path from 'node:path';
17
18
  import { homedir } from 'node:os';
18
19
  import { createImportArchive } from './import/archive.js';
@@ -24,16 +25,34 @@ import { resolveUpdateBase } from './import/update.js';
24
25
  import { clearPendingImport, completeManagedImport, createExternalProject, importArchitectureProtocol, loadManagedProject, saveManagedProject, resolveProjectEnvironment, setPendingImport, } from './import/project.js';
25
26
  import { clearImportFailureDiagnostic, writeImportFailureDiagnostic, importRetryGuidance, } from './import/diagnostic.js';
26
27
  import { checkCloudInit, checkCloudEntitlement, CLOUD_INIT_SUCCESS_MESSAGE, resolveCloudAppId, preflightCloudSchema, exportCloudSchema, parseCloudSecrets, setCloudSecrets } from './cloud.js';
28
+ import { cloudEnvironmentStatus, databaseOverview, enableCloudService, localDevContext, retryCloudEnvironment, } from './app/cloud.js';
29
+ import { createDeployTicket, lastDeployTicket, listDeployTickets, parseDeployTicketContent, waitDeployResult, } from './app/release.js';
27
30
  import { configurationGuidance } from './import/guidance.js';
28
- import { QMUSE_CLI_VERSION } from './version.js';
31
+ import { benefitEntitlements, pointBalance, pointConsumeStats, pointRecords, } from './app/account.js';
32
+ import { QMUSE_CLI_VERSION, QMUSE_CLI_NAME, QMUSE_CLI_DISTRIBUTION } from './version.js';
33
+ import { createRecord, deleteRecord, diagnoseRecordParams, getRecord, listRecords, queryAppMetadata, queryFormAuth, syncFormAuth, updateRecord, } from './app/data.js';
34
+ import { cancelChat, uploadAttachment, clearChatContext, continueChat, getChatState, summarizePendingInterrupts, listModels, queryChatByAppIdAndSprintId, } from './app/chat.js';
35
+ import { fetchSourceFiles, listAppVersions } from './app/version.js';
36
+ import { deploymentDetail, executionDetail, functionDetail, listFunctionDeployments, listFunctionExecutions, listFunctions, retryFunctionPublish, } from './app/fn.js';
37
+ import { deleteSecret, disableSecret, enableSecret, listSecrets, retrySecretSync } from './app/secret.js';
38
+ import { buildArtifactsStatus, ensureSandbox, previewHealth, previewInfo, querySandboxSetting, restartPreview, } from './app/sandbox.js';
39
+ import { mkdirSync, writeFileSync, rmSync, statSync, readFileSync } from 'node:fs';
40
+ import { createAppChat, generateCreateRequestId, normalizeAppType, } from './app/create.js';
41
+ import { deleteApp, queryAppInfo, queryAppListBySpaceId, updateApp, } from './app/api.js';
42
+ import { clearAppBinding, loadAppBinding, saveAppBinding, } from './app/binding.js';
29
43
  import { startUpdateNotice } from './update-notice.js';
44
+ import { recordEvent, setTelemetryCommand, currentCommandLabel } from './telemetry.js';
45
+ import { trackCommand } from './tracker.js';
46
+ import { buildCliError, sanitizeMessage } from './errors.js';
47
+ import { checkVersionGate, defaultUpgradeCommand, envPolicyProvider, registryPolicyProvider, shouldUpgrade } from './version-gate.js';
48
+ import { installAgentSkill, skillStatus, syncInstalledSkills, defaultTargetForAgent } from './agent-skill.js';
30
49
  import { startDevelopment } from './dev/index.js';
31
50
  import { createProject } from './create.js';
32
51
  import { pullProject } from './pull.js';
33
52
  import { withProjectLock } from './project-lock.js';
34
53
  // ---------------- 参数解析 ----------------
35
54
  const BOOL_FLAGS = new Set([
36
- '--help', '-h', '--version', '-V', '--json', '--status',
55
+ '--help', '-h', '-V', '--json', '--status',
37
56
  '--log', '--no-open', '--wait', '--force',
38
57
  '--acknowledge-empty-cloud-data',
39
58
  '--confirm-cloud-service',
@@ -112,14 +131,52 @@ function output(value, flags) {
112
131
  const json = flags['--json'] === true ? JSON.stringify(value, null, 2) : JSON.stringify(value);
113
132
  process.stdout.write(`${json}\n`);
114
133
  }
115
- function fail(message) {
116
- throw new Error(message);
134
+ /** 结构化 CLI 错误:fail() 可传 code/hint,供 --json 输出与排障使用。 */
135
+ export class CliError extends Error {
136
+ info;
137
+ constructor(info) {
138
+ super(info.message);
139
+ this.info = info;
140
+ this.name = 'CliError';
141
+ }
142
+ }
143
+ function fail(message, options) {
144
+ if (options?.code) {
145
+ throw new CliError(buildCliError({
146
+ errorCode: options.code,
147
+ errorMessage: sanitizeMessage(message),
148
+ ...(options.hint ? { hint: options.hint } : {}),
149
+ ...(options.details ? { details: options.details } : {}),
150
+ }));
151
+ }
152
+ throw new CliError(buildCliError({ errorMessage: sanitizeMessage(message) }));
153
+ }
154
+ /** 统一的「服务端调用失败」出口:自动带 code/hint/traceId,避免 43 处手写模板。 */
155
+ function failApi(r, prefix) {
156
+ const info = buildCliError({
157
+ errorCode: r.errorCode,
158
+ errorMessage: `${prefix}: ${r.errorMessage ?? r.errorCode ?? `HTTP ${r.status}`}`,
159
+ status: r.status,
160
+ ...(r.traceId ? { traceId: r.traceId } : {}),
161
+ ...(r.hint ? { hint: r.hint } : {}),
162
+ ...(r.retryAfterMs !== undefined ? { details: { retryAfterMs: r.retryAfterMs } } : {}),
163
+ });
164
+ throw new CliError(info);
117
165
  }
118
166
  function requireAuth(cfg) {
119
167
  if (!hasAuth(cfg)) {
120
168
  fail('当前环境未登录。请先运行 `qmuse login`。');
121
169
  }
122
170
  }
171
+ /** 汇总输出中的软性小节:失败不阻断整体,仅返回 unavailable 原因。 */
172
+ async function sectionOf(run) {
173
+ try {
174
+ return await run();
175
+ }
176
+ catch (err) {
177
+ return { unavailable: err instanceof Error ? err.message : String(err) };
178
+ }
179
+ }
123
180
  function readPublicConfig(value) {
124
181
  if (!value)
125
182
  return {};
@@ -275,9 +332,23 @@ async function resolveImportConfiguration(status, cfg, flags, cloudTarget) {
275
332
  return configured.data;
276
333
  }
277
334
  async function finishImport(status, sourceHash, cfg, flags, managedProject, rootDir) {
335
+ await finishImportWithSync(status, sourceHash, cfg, flags, managedProject, rootDir, undefined);
336
+ }
337
+ /** C04.14:导入结果附带同步反馈(源码变化、远端版本基线与结构化诊断)。 */
338
+ async function finishImportWithSync(status, sourceHash, cfg, flags, managedProject, rootDir, sync) {
278
339
  recordOperation('IMPORT_RESULT', { jobId: status.jobId, appId: status.appId, toVersionId: status.versionId,
279
340
  status: status.status, stage: status.stage, errorCode: status.failureDiagnostic?.code });
280
341
  if (status.status !== 'READY' || !status.appId || !status.versionId) {
342
+ // T01.18:非 READY 终局补 waiting/failed 事件——服务端未终态(待确认/待配置)与失败分别口径。
343
+ recordEvent({
344
+ kind: 'cli.task.state',
345
+ route: '/api/app-import/status',
346
+ stage: status.status === 'FAILED' ? 'terminal' : 'waiting',
347
+ operationId: status.jobId,
348
+ result: status.status === 'FAILED' ? 'failed' : 'waiting',
349
+ status: String(status.status ?? '').toLowerCase() || undefined,
350
+ ...(status.failureDiagnostic?.code ? { errorCode: String(status.failureDiagnostic.code) } : {}),
351
+ });
281
352
  const guidance = configurationGuidance(status, resolveProjectEnvironment(cfg, str(flags, '--env')), parseImportCloudMode(str(flags, '--cloud-mode')));
282
353
  if (flags['--json'] === true) {
283
354
  output({ success: false, ...status, ...(guidance ? { configurationRequired: guidance } : {}) }, flags);
@@ -335,6 +406,11 @@ async function finishImport(status, sourceHash, cfg, flags, managedProject, root
335
406
  }
336
407
  if (rootDir)
337
408
  await clearImportFailureDiagnostic(rootDir);
409
+ // T01.05/06:导入任务终态与上传来源(digest 为源码 sha256,不含内容)。
410
+ recordEvent({ kind: 'cli.transfer.result', route: '/api/app-import/source', result: 'ready',
411
+ ...(completedSourceHash ? { digest: completedSourceHash } : {}) });
412
+ recordEvent({ kind: 'cli.task.state', route: '/api/app-import/status', stage: 'READY',
413
+ operationId: status.jobId, result: 'ok', status: String(status.status ?? '').toLowerCase() || undefined });
338
414
  const result = {
339
415
  success: true,
340
416
  jobId: status.jobId,
@@ -345,6 +421,13 @@ async function finishImport(status, sourceHash, cfg, flags, managedProject, root
345
421
  openUrl,
346
422
  sourceHash: completedSourceHash,
347
423
  operation: status.operation || pending?.operation || (managedProject?.target ? 'UPDATE' : 'CREATE'),
424
+ sourceChanged: true,
425
+ ...(sync?.remoteChanged !== undefined ? { remoteChanged: sync.remoteChanged, baseVersionId: sync.baseVersionId } : {}),
426
+ ...(sync?.fileCount !== undefined ? { fileCount: sync.fileCount } : {}),
427
+ diagnostics: {
428
+ assetReport: '.qmuse/asset-preflight.json',
429
+ ...(status.failureDiagnostic ? { failureDiagnostic: status.failureDiagnostic } : {}),
430
+ },
348
431
  templateCommit: status.templateCommit,
349
432
  importerVersion: status.importerVersion,
350
433
  assetCount: status.assetCount,
@@ -524,6 +607,10 @@ async function cmdImport(command, flags) {
524
607
  appId: managedProject.target.appId,
525
608
  versionId: managedProject.target.lastVersionId,
526
609
  sourceHash: preflight.sourceHash,
610
+ sourceChanged: false,
611
+ remoteChanged: updateBase?.remoteChanged === true,
612
+ ...(updateBase ? { baseVersionId: updateBase.baseVersionId } : {}),
613
+ fileCount: preflight.fileCount,
527
614
  openUrl: getAppOpenUrl(cfg.webBaseUrl, managedProject.target.appId),
528
615
  };
529
616
  if (flags['--json'] === true)
@@ -619,7 +706,11 @@ async function cmdImport(command, flags) {
619
706
  process.exitCode = 130;
620
707
  return;
621
708
  }
622
- await finishImport(polled.status, preflight.sourceHash, cfg, flags, managedProject, rootDir);
709
+ await finishImportWithSync(polled.status, preflight.sourceHash, cfg, flags, managedProject, rootDir, {
710
+ remoteChanged: updateBase?.remoteChanged,
711
+ baseVersionId: updateBase?.baseVersionId,
712
+ fileCount: preflight.fileCount,
713
+ });
623
714
  }
624
715
  async function cmdCloud(command, flags) {
625
716
  const subcommand = command[1];
@@ -663,8 +754,88 @@ async function cmdCloud(command, flags) {
663
754
  process.exitCode = 1;
664
755
  return;
665
756
  }
757
+ if (['enable', 'status', 'pull-env'].includes(subcommand ?? '') || (subcommand === 'environment' && command[2] === 'retry')) {
758
+ const cfg = resolveCfg(flags);
759
+ requireAuth(cfg);
760
+ const appId = await resolveCloudAppId(process.cwd(), cfg, str(flags, '--env'), str(flags, '--app-id'));
761
+ if (subcommand === 'enable') {
762
+ // C14.02:复用既有开通服务与权益判断,返回真实资源与初始化状态。
763
+ const [enabled, entitlement] = await Promise.all([
764
+ enableCloudService(appId, cfg),
765
+ checkCloudEntitlement({ appId, spaceId: cfg.spaceId ?? "" }, cfg).then(v => v).catch(err => ({
766
+ unavailable: err instanceof Error ? err.message : String(err),
767
+ })),
768
+ ]);
769
+ if (!enabled.ok || !enabled.data)
770
+ fail(`开通云服务失败: ${enabled.errorMessage ?? enabled.errorCode ?? `HTTP ${enabled.status}`}`);
771
+ // T01.08:云资源开通任务来源。
772
+ recordEvent({ kind: 'cli.task.state', route: '/api/appwrite/cloud/storage/enable', stage: 'enabled', result: 'ok' });
773
+ output({ appId, activation: enabled.data, entitlement }, flags);
774
+ return;
775
+ }
776
+ if (subcommand === 'status') {
777
+ // C14.03:集中展示架构、实际云环境、初始化状态、公开资源与权益/失败原因。
778
+ const [status, entitlement] = await Promise.all([
779
+ cloudEnvironmentStatus(appId, cfg),
780
+ checkCloudEntitlement({ appId, spaceId: cfg.spaceId ?? "" }, cfg).then(v => v).catch(err => ({
781
+ unavailable: err instanceof Error ? err.message : String(err),
782
+ })),
783
+ ]);
784
+ if (!status.ok || !status.data)
785
+ fail(`查询云状态失败: ${status.errorMessage ?? status.errorCode ?? `HTTP ${status.status}`}`);
786
+ const data = status.data;
787
+ if (data.activationStatus === 'NOT_ENABLED') {
788
+ output({ appId, ...data, entitlement, hint: '云服务未开通:执行 qmuse cloud enable' }, flags);
789
+ return;
790
+ }
791
+ output({ appId, ...data, entitlement }, flags);
792
+ return;
793
+ }
794
+ if (subcommand === 'environment' && command[2] === 'retry') {
795
+ // C14.08:复用平台既有重试动作(幂等 + 分布式锁),不以新建资源替代原任务恢复。
796
+ const r = await retryCloudEnvironment(appId, cfg);
797
+ if (!r.ok || !r.data)
798
+ failApi(r, `重试云环境初始化失败`);
799
+ let current = r.data;
800
+ if (flags['--wait'] === true) {
801
+ const deadline = Date.now() + 5 * 60 * 1000;
802
+ while (!['ACTIVE', 'PENDING_FIRST_PUBLISH', 'INITIALIZATION_FAILED'].includes(String(current.activationStatus))) {
803
+ if (Date.now() > deadline) {
804
+ current = { ...current, timedOut: true };
805
+ break;
806
+ }
807
+ await new Promise(resolve => setTimeout(resolve, 5000));
808
+ const q = await cloudEnvironmentStatus(appId, cfg);
809
+ if (q.ok && q.data)
810
+ current = q.data;
811
+ }
812
+ }
813
+ output({ appId, ...current }, flags);
814
+ if (['INITIALIZATION_FAILED', 'NOT_ENABLED'].includes(String(current.activationStatus)))
815
+ process.exitCode = 1;
816
+ return;
817
+ }
818
+ if (subcommand === 'pull-env') {
819
+ // C14.09:导出实际环境公开连接与运行配置(不含管理凭证),供本地运行上下文复用。
820
+ const outPath = str(flags, '--output');
821
+ const r = await localDevContext(appId, cfg);
822
+ if (!r.ok || !r.data)
823
+ failApi(r, `查询本地开发上下文失败`);
824
+ const context = r.data;
825
+ const resolved = outPath ? path.resolve(outPath.replace(/^\$HOME(?=\/|$)/u, homedir())) : undefined;
826
+ if (resolved) {
827
+ mkdirSync(path.dirname(resolved), { recursive: true });
828
+ writeFileSync(resolved, `${JSON.stringify(context, null, 2)}\n`, { mode: 0o600 });
829
+ output({ success: true, appId, output: resolved, context }, flags);
830
+ }
831
+ else {
832
+ output({ appId, context }, flags);
833
+ }
834
+ return;
835
+ }
836
+ }
666
837
  if (subcommand !== 'init') {
667
- fail(`未知 cloud 子命令: ${subcommand ?? '(空)'}。可用: init、set-secret、schema preflight、schema export`);
838
+ fail(`未知 cloud 子命令: ${subcommand ?? '(空)'}。可用: init、enable、status、environment retry、pull-env、set-secret、schema preflight、schema export`);
668
839
  }
669
840
  if (!str(flags, '-d')?.trim()) {
670
841
  fail('用法: qmuse cloud init -d "<应用功能描述>"');
@@ -679,7 +850,216 @@ async function cmdCloud(command, flags) {
679
850
  process.stdout.write(`${CLOUD_INIT_SUCCESS_MESSAGE}\n`);
680
851
  }
681
852
  // ---------------- 命令实现 ----------------
853
+ /** 摘要发布单关键信息(发布/预览地址、安全检查、域名发布状态)——typed 契约解析。 */
854
+ function summarizeDeployTicket(ticket) {
855
+ if (!ticket)
856
+ return { ticket: null };
857
+ // query/list/last 返回带 content 字段;create 响应直接展开 content 字段。
858
+ const content = ticket.content !== undefined
859
+ ? parseDeployTicketContent(ticket)
860
+ : ticket;
861
+ const domainPublication = content.domainPublication;
862
+ const base = ticket;
863
+ return {
864
+ deployId: base.id,
865
+ appId: base.appId,
866
+ versionId: content.versionId,
867
+ status: base.status,
868
+ publishUrl: domainPublication?.appPath ?? domainPublication?.url ?? domainPublication?.publishedUrl,
869
+ previewUrl: content.preview_snapshot_url ?? content.previewThumbUrl,
870
+ infosecStatus: content.infosecStatus ?? undefined,
871
+ infosecResult: !content.infosecResult || content.infosecResult === null ? undefined : content.infosecResult,
872
+ mosecStatus: content.mosecScan?.status,
873
+ domainPublication,
874
+ updatedAt: ticket.updatedAt ?? ticket.gmtModified ?? undefined,
875
+ };
876
+ }
877
+ async function cmdDeploy(command, flags) {
878
+ const cfg = resolveCfg(flags);
879
+ requireAuth(cfg);
880
+ if (command.length > 1)
881
+ fail('用法: qmuse deploy --version <versionId> [--app-id <appId>] [--no-wait] [--json]');
882
+ const versionId = pickString(flags, '--version') ?? pickString(flags, '--version-id');
883
+ if (!versionId)
884
+ fail('用法: qmuse deploy --version <versionId> [--app-id <appId>] [--no-wait]');
885
+ const binding = loadAppBinding(process.cwd());
886
+ const appId = pickString(flags, '--app-id') ?? binding?.appId;
887
+ if (!appId)
888
+ fail('未指定应用:传 --app-id <appId> 或先执行 qmuse app use <appId>');
889
+ const created = await createDeployTicket(appId, versionId, cfg);
890
+ if (!created.ok || !created.data)
891
+ fail(`创建发布单失败: ${created.errorMessage ?? created.errorCode ?? `HTTP ${created.status}`}`);
892
+ const seed = created.data;
893
+ const deployId = String(seed['id'] ?? '');
894
+ // T01.08:发布受理来源(安全门禁/产物准备在服务端执行,同一 releaseId 可继续查询)。
895
+ recordEvent({ kind: 'cli.task.state', route: '/api/deploy/create', stage: 'accepted',
896
+ operationId: deployId || undefined, result: 'ok' });
897
+ const wait = await waitDeployResult(deployId, cfg, { noWait: flags['--no-wait'] === true });
898
+ const payload = {
899
+ success: !wait.timedOut,
900
+ appId,
901
+ versionId,
902
+ deployId: deployId || undefined,
903
+ timedOut: wait.timedOut || undefined,
904
+ error: wait.lastError,
905
+ ...summarizeDeployTicket(wait.ticket ?? seed),
906
+ };
907
+ if (flags['--json'] === true) {
908
+ output(payload, flags);
909
+ }
910
+ else {
911
+ if (wait.lastError)
912
+ log.error(`查询发布结果失败: ${wait.lastError}`);
913
+ log.info(`发布单 ${deployId}(appId=${appId} versionId=${versionId})状态:${payload.status ?? 'UNKNOWN'}`);
914
+ if (payload.publishUrl)
915
+ log.success(`发布地址:${String(payload.publishUrl)}`);
916
+ if (payload.timedOut)
917
+ log.info('等待超时:使用 qmuse release status <releaseId> 继续查询');
918
+ else if (!flags['--no-wait'])
919
+ log.info('继续查询:qmuse release status ' + deployId);
920
+ }
921
+ if (wait.timedOut || payload.status === 'DEPLOY_FAILED' || payload.status === 'CANCEL')
922
+ process.exitCode = 1;
923
+ // T01.08:发布终态来源(receipt 由服务端任务记录,这里保留 CLI 端可观测尾部)。
924
+ recordEvent({ kind: 'cli.task.state', route: '/api/deploy/query', stage: 'terminal',
925
+ operationId: deployId || undefined, status: wait.timedOut ? 'timeout' : String(payload.status ?? 'unknown').toLowerCase(),
926
+ result: wait.timedOut ? 'timeout' : payload.status === 'DEPLOYED' ? 'ok' : 'failed',
927
+ });
928
+ }
929
+ async function cmdRelease(command, flags) {
930
+ const cfg = resolveCfg(flags);
931
+ requireAuth(cfg);
932
+ const sub = command[1];
933
+ if (sub === 'status') {
934
+ const releaseId = command[2] || pickString(flags, '--release-id') || pickString(flags, '--id');
935
+ if (!releaseId)
936
+ fail('用法: qmuse release status <releaseId> [--wait] 或 qmuse release status --last --app-id <appId>');
937
+ const wait = await waitDeployResult(releaseId, cfg, { noWait: flags['--wait'] !== true });
938
+ const summary = summarizeDeployTicket(wait.ticket);
939
+ const lookupFailed = !wait.ticket;
940
+ if (flags['--json'] === true) {
941
+ output({ success: !wait.timedOut && !lookupFailed && summary.status !== 'DEPLOY_FAILED' && summary.status !== 'CANCEL', timedOut: wait.timedOut || undefined, error: wait.lastError, ...summary }, flags);
942
+ return;
943
+ }
944
+ log.info(`发布单 ${releaseId} 状态:${String(summary.status ?? 'UNKNOWN')}${wait.timedOut ? '(等待超时)' : ''}`);
945
+ if (summary.publishUrl)
946
+ log.success(`发布地址:${String(summary.publishUrl)}`);
947
+ process.exitCode = wait.timedOut || summary.status === 'DEPLOY_FAILED' || summary.status === 'CANCEL' ? 1 : 0;
948
+ return;
949
+ }
950
+ if (sub === 'list') {
951
+ let appId = pickString(flags, '--app-id') ?? loadAppBinding(process.cwd())?.appId;
952
+ if (!appId)
953
+ fail('用法: qmuse release list --app-id <appId>');
954
+ if (flags['--last'] === true) {
955
+ const r = await lastDeployTicket(appId, cfg);
956
+ if (!r.ok)
957
+ failApi(r, `查询最近发布失败`);
958
+ output(summarizeDeployTicket(r.data), flags);
959
+ return;
960
+ }
961
+ const r = await listDeployTickets(appId, cfg);
962
+ if (!r.ok || !r.data)
963
+ failApi(r, `查询发布记录失败`);
964
+ // 防御性兼容:老网关可能返回包装对象而非数组,运行时仍做数组判定。
965
+ const payload = r.data;
966
+ const list = Array.isArray(payload)
967
+ ? payload.map(item => summarizeDeployTicket(item))
968
+ : payload;
969
+ output({ appId, releases: list }, flags);
970
+ return;
971
+ }
972
+ fail(`未知 release 子命令: ${sub ?? '(空)'}。可用: status <releaseId> [--wait]、list --app-id <appId>`);
973
+ }
974
+ function devStatePath() {
975
+ return path.join(process.cwd(), '.qmuse', 'dev.json');
976
+ }
977
+ function writeDevState(state, _root) {
978
+ mkdirSync(path.dirname(devStatePath()), { recursive: true });
979
+ writeFileSync(devStatePath(), JSON.stringify(state, null, 2), 'utf8');
980
+ }
981
+ function readDevState() {
982
+ try {
983
+ const parsed = JSON.parse(readFileSync(devStatePath(), 'utf8'));
984
+ return parsed?.schemaVersion === 1 && typeof parsed.ppid === 'number' ? parsed : undefined;
985
+ }
986
+ catch {
987
+ return undefined;
988
+ }
989
+ }
990
+ function devProcessAlive(pid) {
991
+ try {
992
+ process.kill(pid, 0);
993
+ return true;
994
+ }
995
+ catch (err) {
996
+ return err.code === 'EPERM';
997
+ }
998
+ }
999
+ async function cmdDevSession(sub, flags) {
1000
+ const state = readDevState();
1001
+ const running = state && devProcessAlive(state.ppid);
1002
+ if (sub === 'status') {
1003
+ if (!state) {
1004
+ output({ running: false, reason: 'no-session-file' }, flags);
1005
+ return;
1006
+ }
1007
+ if (!running) {
1008
+ output({ running: false, reason: 'stale-session', pid: state.ppid }, flags);
1009
+ return;
1010
+ }
1011
+ output({
1012
+ running: true,
1013
+ pid: state.ppid,
1014
+ appId: state.appId,
1015
+ port: state.port,
1016
+ env: state.env,
1017
+ startedAt: state.startedAt,
1018
+ localUrl: `http://127.0.0.1:${state.port}/`,
1019
+ }, flags);
1020
+ return;
1021
+ }
1022
+ if (sub === 'stop') {
1023
+ if (!state || !running) {
1024
+ if (state)
1025
+ rmSync(devStatePath(), { force: true });
1026
+ if (flags['--json'] === true)
1027
+ output({ stopped: false, reason: 'not-running' }, flags);
1028
+ else
1029
+ log.warn('当前目录没有运行中的 qmuse dev 会话');
1030
+ return;
1031
+ }
1032
+ try {
1033
+ process.kill(state.ppid, 'SIGTERM');
1034
+ }
1035
+ catch { /* 已退出 */ }
1036
+ for (let waited = 0; waited < 8000 && devProcessAlive(state.ppid); waited += 200) {
1037
+ await new Promise(resolve => setTimeout(resolve, 200));
1038
+ }
1039
+ if (devProcessAlive(state.ppid)) {
1040
+ try {
1041
+ process.kill(state.ppid, 'SIGKILL');
1042
+ }
1043
+ catch { /* 忽略 */ }
1044
+ }
1045
+ rmSync(devStatePath(), { force: true });
1046
+ if (flags['--json'] === true)
1047
+ output({ stopped: true, pid: state.ppid, appId: state.appId }, flags);
1048
+ else
1049
+ log.success(`开发会话已停止(pid=${state.ppid},appId=${state.appId})`);
1050
+ return;
1051
+ }
1052
+ fail(`未知 dev 子命令: ${sub}。可用: status / stop`);
1053
+ }
682
1054
  async function cmdDev(command, flags) {
1055
+ const sub = command[1];
1056
+ if (sub === 'status' || sub === 'stop') {
1057
+ await cmdDevSession(sub, flags);
1058
+ return;
1059
+ }
1060
+ await cmdDevRun(command, flags);
1061
+ }
1062
+ async function cmdDevRun(command, flags) {
683
1063
  const cfg = resolveCfg(flags);
684
1064
  requireAuth(cfg);
685
1065
  if (flags['--port'] !== undefined && typeof flags['--port'] !== 'string')
@@ -704,6 +1084,14 @@ async function cmdDev(command, flags) {
704
1084
  log.warn('当前为单环境应用,开发云资源可能与正式页面共用。');
705
1085
  development.server.printUrls();
706
1086
  }
1087
+ writeDevState({
1088
+ schemaVersion: 1,
1089
+ ppid: process.pid,
1090
+ appId: development.context.appId,
1091
+ port,
1092
+ env: typeof flags['--env'] === 'string' ? flags['--env'] : 'default',
1093
+ startedAt: new Date().toISOString(),
1094
+ }, process.cwd());
707
1095
  await new Promise(resolve => {
708
1096
  const stop = () => {
709
1097
  process.off('SIGINT', stop);
@@ -750,7 +1138,7 @@ async function cmdWhoami(flags) {
750
1138
  log.info(`本地空间 : ${cfg.spaceId}`);
751
1139
  return;
752
1140
  }
753
- fail(`查询当前用户失败: ${result.errorMessage ?? result.errorCode ?? `HTTP ${result.status}`}`);
1141
+ failApi(result, '查询当前用户失败');
754
1142
  }
755
1143
  const currentUser = result.data;
756
1144
  const tenantName = tenantDisplayName(currentUser.tenantCode, currentUser.tenantName);
@@ -804,7 +1192,7 @@ async function cmdSpace(command, flags) {
804
1192
  // 空间发现接口不携带当前空间,确保账号切换后仍能列出新账号的空间。
805
1193
  const r = await api.listSpaces(num(flags, '--page-index', 1), num(flags, '--page-size', 20), { ...cfg, spaceId: undefined });
806
1194
  if (!r.ok || !r.data)
807
- fail(`查询空间失败: ${r.errorMessage ?? r.errorCode ?? `HTTP ${r.status}`}`);
1195
+ failApi(r, `查询空间失败`);
808
1196
  if (flags['--json'] === true) {
809
1197
  output(r.data, flags);
810
1198
  return;
@@ -821,160 +1209,1739 @@ async function cmdSpace(command, flags) {
821
1209
  const spaceId = command[2] || str(flags, '--space-id');
822
1210
  if (!spaceId)
823
1211
  fail('用法: qmuse space use <spaceId>');
1212
+ // 保存前先校验该空间当前账号可访问(C02.02):探测失败不写入默认选择。
1213
+ const probe = { ...cfg, spaceId };
1214
+ const access = await api.spaceInfo(probe);
1215
+ if (!access.ok || !access.data) {
1216
+ fail(`空间 ${spaceId} 不可用:${access.errorMessage ?? access.errorCode ?? `HTTP ${access.status}`};默认空间选择未变更`);
1217
+ }
1218
+ const spaceName = access.data?.cnName;
824
1219
  const next = { ...cfg, spaceId };
825
1220
  saveConfig(next, true);
826
- log.success(`已设为当前空间 spaceId=${spaceId}(后续请求带 x-space-id 头)`);
1221
+ log.success(`已设为当前空间 spaceId=${spaceId}${spaceName ? `(${spaceName})` : ''}(后续请求带 x-space-id 头)`);
827
1222
  return;
828
1223
  }
829
1224
  if (sub === 'info') {
830
1225
  const r = await api.spaceInfo(cfg);
831
1226
  if (!r.ok)
832
- fail(`查询当前空间失败: ${r.errorMessage ?? r.errorCode ?? `HTTP ${r.status}`}`);
1227
+ failApi(r, `查询当前空间失败`);
833
1228
  output(r.data, flags);
834
1229
  return;
835
1230
  }
836
1231
  fail(`未知 space 子命令: ${sub ?? '(空)'}。可用: list / use <spaceId> / info`);
837
1232
  }
838
- async function cmdLogin(flags) {
839
- const cfg = resolveCfg(flags);
840
- if (flags['--status'] === true) {
841
- const out = await login.loginStatus(cfg);
842
- if (out.success)
843
- log.success(out.message ?? '会话有效');
844
- else
845
- log.error(out.message ?? '会话无效');
846
- return;
1233
+ // ---------------- 统一创建 ----------------
1234
+ /** 读取首条创作内容:--file <路径|-> 或标准输入(管道);TTY 下要求显式 --file。 */
1235
+ async function readCreateContent(flags) {
1236
+ const file = str(flags, '--file');
1237
+ if (file && file !== '-') {
1238
+ try {
1239
+ return readFileSync(path.resolve(file), 'utf8');
1240
+ }
1241
+ catch (err) {
1242
+ throw new Error(`无法读取 --file:${err instanceof Error ? err.message : String(err)}`);
1243
+ }
847
1244
  }
848
- log.info(`登录方式:browser 目标:${cfg.baseUrl}${cfg.apiPrefix || ''}`);
849
- const out = await login.loginBrowser(cfg, {
850
- logRequestDetails: flags['--log'] === true,
851
- });
852
- if (out.success) {
853
- login.closePrompt();
854
- log.success(`登录成功(${out.method})`);
855
- if (flags['--json'] === true)
856
- output({ success: true, method: out.method }, flags);
1245
+ if (!file && process.stdin.isTTY === true) {
1246
+ fail('用法: qmuse app create <web|h5> --file <路径|->(--file - 从管道读取)');
857
1247
  }
858
- else {
859
- login.closePrompt();
860
- fail(out.message ?? '登录失败');
1248
+ const chunks = [];
1249
+ for await (const chunk of process.stdin) {
1250
+ chunks.push(chunk);
861
1251
  }
1252
+ return Buffer.concat(chunks).toString('utf8');
862
1253
  }
863
- async function cmdLogout(flags) {
864
- const cfg = resolveCfg(flags);
865
- const hadSession = !!(cfg.sessionId || '').trim();
866
- deleteSession(cfg);
867
- if (hadSession) {
868
- log.success('已清除当前环境的登录态和空间选择');
1254
+ /** 标题缺省时从内容首个非空行推导(去 markdown 标记,截断 40 字符)。 */
1255
+ function deriveCreateTitle(content) {
1256
+ const line = content
1257
+ .split('\n')
1258
+ .map(item => item.trim())
1259
+ .find(Boolean) ?? '';
1260
+ const cleaned = line.replace(/^#+\s*/u, '').replace(/[*`_\[\]]/gu, '').trim();
1261
+ const title = [...cleaned].slice(0, 40).join('');
1262
+ return title || '未命名应用';
1263
+ }
1264
+ function createAppOpenUrl(cfg, appId) {
1265
+ if (!appId)
1266
+ return '';
1267
+ try {
1268
+ return getAppOpenUrl(cfg.webBaseUrl, appId);
869
1269
  }
870
- else {
871
- log.info('当前未配置登录态;已清除空间选择');
1270
+ catch {
1271
+ return '';
872
1272
  }
873
1273
  }
874
- // ---------------- 帮助 & 入口 ----------------
875
- async function cmdCreate(command, flags) {
876
- if (command.length !== 2)
877
- fail('用法:qmuse create <directory>');
878
- const result = await createProject(command[1], resolveCfg(flags));
1274
+ function printAppCreateResult(result, openUrl, flags) {
879
1275
  if (flags['--json'] === true) {
880
- output(result, flags);
1276
+ const success = result.status === 'SUCCEEDED' || result.status === 'REUSED' || result.status === 'WAITING_INPUT';
1277
+ output({
1278
+ schemaVersion: 1,
1279
+ success,
1280
+ status: result.status,
1281
+ data: {
1282
+ ...(result.identity.appId ? { appId: result.identity.appId } : {}),
1283
+ ...(result.identity.chatId ? { chatId: result.identity.chatId } : {}),
1284
+ ...(result.identity.sprintId ? { sprintId: result.identity.sprintId } : {}),
1285
+ ...(result.versionId ? { versionId: result.versionId } : {}),
1286
+ ...(openUrl ? { openUrl } : {}),
1287
+ reused: result.reused,
1288
+ requestId: result.requestId,
1289
+ messageId: result.messageId,
1290
+ ...(result.created ? { created: result.created } : {}),
1291
+ },
1292
+ ...(result.errorText ? { errorText: result.errorText } : {}),
1293
+ ...(result.errorCode ? { errorCode: result.errorCode } : {}),
1294
+ ...(result.errorMessage ? { errorMessage: result.errorMessage } : {}),
1295
+ ...(result.traceId ? { traceId: result.traceId } : {}),
1296
+ }, flags);
1297
+ }
1298
+ else {
1299
+ switch (result.status) {
1300
+ case 'REUSED':
1301
+ log.success(`该请求此前已受理:appId=${result.identity.appId},已恢复身份,未重复创建或重新生成`);
1302
+ if (openUrl)
1303
+ log.raw(`${openUrl}\n`);
1304
+ break;
1305
+ case 'SUCCEEDED':
1306
+ log.success('应用创建完成,首轮生成已结束');
1307
+ if (openUrl)
1308
+ log.raw(`${openUrl}\n`);
1309
+ break;
1310
+ case 'WAITING_INPUT':
1311
+ log.warn('首轮生成在等待输入;请打开作品页继续,或用相同 --request-id 重新查询状态');
1312
+ if (openUrl)
1313
+ log.raw(`${openUrl}\n`);
1314
+ break;
1315
+ case 'INTERRUPTED':
1316
+ log.warn('已停止本地等待,服务端生成仍会继续;应用身份已保留');
1317
+ if (result.identity.appId)
1318
+ log.raw(`${openUrl}\n`);
1319
+ break;
1320
+ default:
1321
+ log.error(`创建失败:${result.errorText ?? result.errorMessage ?? '未知错误'}`);
1322
+ if (result.identity.appId) {
1323
+ log.warn(`应用已创建(${openUrl}),生成失败可认为首轮未完成,请勿将本次计为成功`);
1324
+ }
1325
+ }
1326
+ }
1327
+ }
1328
+ async function cmdApp(command, flags) {
1329
+ const sub = command[1];
1330
+ if (sub === 'create') {
1331
+ await cmdAppCreate(command, flags);
881
1332
  return;
882
1333
  }
883
- log.success(`模板已下载到 ${result.directory}`);
884
- log.info('进入项目目录后执行:npm install');
885
- log.info(`开发完成后执行:qmuse${str(flags, '--env') ? ` --env ${str(flags, '--env')}` : ''} import .`);
1334
+ await cmdAppManage(sub, command, flags);
886
1335
  }
887
- const HELP = `QMuse CLI —— 将本地 React 项目导入 QMuse
888
-
889
- 用法:
890
- qmuse [global flags] <command> [subcommand] [flags]
891
-
892
- 全局 flags:
893
- --env <env> 环境预设:prod / pre / local
894
- --json 结果以 JSON 输出
895
- --log 打印登录请求接口和页面地址(不含登录凭证)
896
-
897
- 全局 flags 可放在命令前或命令后
898
-
899
- 命令:
900
- whoami 查看当前环境、登录和空间状态
901
- login [--status] [--log] [--json] 通过 QMuse 网页登录
902
- logout 清除保存的登录态和空间选择
903
- config [get|path] 查看配置
904
-
905
- create <directory> 下载 QMuse 模板并解压到新项目目录
906
- pull [directory] 免费同步已导入应用的最新已保存源码(本地有改动时停止)
907
-
908
- cloud init -d "<应用功能描述>" 校验当前空间的 QMuse 云服务权益
909
- cloud set-secret KEY1,KEY2 --stdin 从标准输入 JSON 保存密钥,不输出密钥值
910
- [--app-id <id>] [--cloud-env dev|prod]
911
- cloud schema preflight --candidate <schema.json> 只读校验候选与云端 READY 基线
912
- cloud schema export [--app-id <id>] 输出当前开发环境的 READY schema
913
- dev [directory] [--port <port>] 连接已导入应用的开发云资源并启动本地 Vite
914
-
915
- assets upload <file> [more-files...] --json 上传媒体到 CDN,返回引用替换清单
916
- import <directory> [--space-id <id>] 导入本地 React 仓库
917
- [--title <title>] [--force] [--cloud-mode AUTO|EXTERNAL|QMUSE]
918
- [--public-config '<json>'|@file] [--acknowledge-empty-cloud-data]
919
- [--confirm-cloud-service]
920
- [--no-open] [--json] [--log]
921
- import status <jobId> [--wait] [--no-open] 查询或恢复等待导入任务
922
- import configure <jobId> --cloud-mode <mode> 补充云服务配置后继续任务
923
-
924
- space list 我加入的空间列表
925
- space use <spaceId> 设为当前空间(后续带 x-space-id 头)
926
- space info 当前空间信息
927
-
928
- 典型流程:
929
- qmuse login
930
- qmuse space list
931
- qmuse space use S640459450135
932
- qmuse create my-app
933
- qmuse import /path/to/react-vite-app
934
- `;
935
- async function main() {
936
- const argv = process.argv.slice(2);
937
- if (argv.length === 0) {
938
- process.stdout.write(HELP);
1336
+ function printAppLine(app) {
1337
+ log.raw(`${app.appId} ${(app.appType ?? '').padEnd(9)} ${(app.status ?? '').padEnd(10)} ${app.title ?? ''}\n`);
1338
+ }
1339
+ async function cmdAppManage(sub, command, flags) {
1340
+ const cfg = resolveCfg(flags);
1341
+ const appIdArg = command[2] || str(flags, '--app-id') || '';
1342
+ if (sub === 'list') {
1343
+ requireAuth(cfg);
1344
+ const spaceId = str(flags, '--space-id') || cfg.spaceId;
1345
+ if (!spaceId)
1346
+ fail('请先执行 qmuse space use <spaceId> 或传 --space-id');
1347
+ const r = await queryAppListBySpaceId(spaceId, {
1348
+ pageIndex: num(flags, '--page-index', 1),
1349
+ pageSize: num(flags, '--page-size', 20),
1350
+ searchKeyword: str(flags, '--keyword'),
1351
+ }, cfg);
1352
+ if (!r.ok || !r.data)
1353
+ failApi(r, `查询应用列表失败`);
1354
+ if (flags['--json'] === true) {
1355
+ output(r.data, flags);
1356
+ return;
1357
+ }
1358
+ log.info(`空间 ${spaceId} 共 ${r.data.total} 个应用:`);
1359
+ for (const app of r.data.list)
1360
+ printAppLine(app);
939
1361
  return;
940
1362
  }
941
- const { command, flags } = parseArgs(argv);
942
- if (flags['--help'] === true || flags['-h'] === true) {
943
- process.stdout.write(HELP);
1363
+ if (sub === 'info') {
1364
+ requireAuth(cfg);
1365
+ if (!appIdArg)
1366
+ fail('用法: qmuse app info <appId>');
1367
+ const r = await queryAppInfo(appIdArg, cfg);
1368
+ if (!r.ok || !r.data)
1369
+ failApi(r, `查询应用信息失败`);
1370
+ output(r.data, flags);
944
1371
  return;
945
1372
  }
946
- if (flags['--version'] === true || flags['-V'] === true) {
947
- process.stdout.write(`qmuse ${QMUSE_CLI_VERSION}\n`);
1373
+ if (sub === 'use') {
1374
+ requireAuth(cfg);
1375
+ if (!appIdArg)
1376
+ fail('用法: qmuse app use <appId>');
1377
+ const info = await queryAppInfo(appIdArg, cfg);
1378
+ if (!info.ok || !info.data) {
1379
+ fail(`应用不可用:${info.errorMessage ?? info.errorCode ?? `HTTP ${info.status}`};本地绑定未变更`);
1380
+ }
1381
+ const path = saveAppBinding({
1382
+ appId: appIdArg,
1383
+ ...(cfg.spaceId ? { spaceId: cfg.spaceId } : {}),
1384
+ ...(typeof info.data.title === 'string' ? { title: info.data.title } : {}),
1385
+ apiOrigin: cfg.baseUrl,
1386
+ });
1387
+ log.success(`已绑定应用 ${appIdArg}${info.data.title ? `(${info.data.title})` : ''},绑定记录 ${path}`);
948
1388
  return;
949
1389
  }
950
- const root = command[0];
951
- const finishUpdateNotice = startUpdateNotice({
952
- enabled: root === 'import' && process.stderr.isTTY === true && flags['--json'] !== true,
953
- notify: message => process.stderr.write(`${message}\n`),
954
- });
955
- process.once('exit', finishUpdateNotice);
956
- try {
957
- switch (root) {
958
- case 'whoami':
959
- await cmdWhoami(flags);
960
- return;
961
- case 'login':
962
- await cmdLogin(flags);
963
- return;
964
- case 'logout':
965
- await cmdLogout(flags);
966
- return;
967
- case 'config':
968
- await cmdConfig(command, flags);
969
- return;
970
- case 'create':
971
- await cmdCreate(command, flags);
972
- return;
973
- case 'pull': {
974
- if (command.length > 2 || Object.keys(flags).some(key => !['--env', '--json'].includes(key))) {
975
- throw new Error('用法:qmuse pull [directory] [--env prod|pre|local] [--json]');
976
- }
977
- const result = await withOperationLog({ directory: command[1] || '.', command: 'pull',
1390
+ if (sub === 'current') {
1391
+ const binding = loadAppBinding();
1392
+ if (!binding) {
1393
+ fail('当前目录没有应用绑定;使用 qmuse app use <appId> 或 qmuse import <dir> 建立');
1394
+ }
1395
+ if (flags['--json'] === true)
1396
+ output(binding, flags);
1397
+ else {
1398
+ log.info(`当前目录绑定应用:${binding.appId}${binding.title ? `(${binding.title})` : ''}`);
1399
+ if (binding.spaceId)
1400
+ log.info(`绑定空间:${binding.spaceId}`);
1401
+ if (binding.apiOrigin)
1402
+ log.info(`绑定环境:${binding.apiOrigin}`);
1403
+ }
1404
+ return;
1405
+ }
1406
+ if (sub === 'unlink') {
1407
+ const removed = clearAppBinding();
1408
+ if (!removed) {
1409
+ log.warn('当前目录没有应用绑定,无需解除');
1410
+ return;
1411
+ }
1412
+ log.success('已解除当前目录的应用绑定');
1413
+ return;
1414
+ }
1415
+ if (sub === 'update') {
1416
+ requireAuth(cfg);
1417
+ if (!appIdArg)
1418
+ fail('用法: qmuse app update <appId> [--title <t>] [--description <d>] [--require-login 0|1] [ --default-device <d>]');
1419
+ const patch = {
1420
+ ...(str(flags, '--title') ? { title: str(flags, '--title') } : {}),
1421
+ ...(flags['--description'] !== undefined ? { description: str(flags, '--description') } : {}),
1422
+ ...(str(flags, '--require-login') !== undefined ? { requireLogin: Number(str(flags, '--require-login')) } : {}),
1423
+ ...(str(flags, '--default-device') ? { defaultDevice: str(flags, '--default-device') } : {}),
1424
+ };
1425
+ try {
1426
+ const r = await updateApp(appIdArg, patch, cfg);
1427
+ if (!r.ok)
1428
+ failApi(r, `更新应用失败`);
1429
+ if (flags['--json'] === true)
1430
+ output({ success: true, appId: appIdArg, ...patch }, flags);
1431
+ else
1432
+ log.success(`应用 ${appIdArg} 信息已更新`);
1433
+ }
1434
+ catch (err) {
1435
+ fail(err instanceof Error ? err.message : String(err));
1436
+ }
1437
+ return;
1438
+ }
1439
+ if (sub === 'delete') {
1440
+ requireAuth(cfg);
1441
+ if (!appIdArg)
1442
+ fail('用法: qmuse app delete <appId> --yes');
1443
+ if (flags['--yes'] !== true)
1444
+ fail('删除应用不可恢复:请显式传入 --yes 确认');
1445
+ const r = await deleteApp(appIdArg, cfg);
1446
+ if (!r.ok)
1447
+ failApi(r, `删除应用失败`);
1448
+ if (loadAppBinding()?.appId === appIdArg)
1449
+ clearAppBinding();
1450
+ if (flags['--json'] === true)
1451
+ output({ success: true, deleted: true, appId: appIdArg }, flags);
1452
+ else
1453
+ log.success(`应用 ${appIdArg} 已删除`);
1454
+ return;
1455
+ }
1456
+ fail(`未知 app 子命令: ${sub ?? '(空)'}。可用: create / list / info / use / current / unlink / update / delete`);
1457
+ }
1458
+ async function cmdAppCreate(command, flags) {
1459
+ const typeArg = command[2] && !command[2].startsWith('-')
1460
+ ? command[2]
1461
+ : str(flags, '--type');
1462
+ const appType = normalizeAppType(typeArg);
1463
+ const cfg = resolveCfg(flags);
1464
+ requireAuth(cfg);
1465
+ const spaceId = str(flags, '--space-id') || cfg.spaceId || '';
1466
+ const content = await readCreateContent(flags);
1467
+ const title = str(flags, '--title') || deriveCreateTitle(content);
1468
+ const operationId = `op_${randomBytes(12).toString('hex')}`;
1469
+ const abortController = new AbortController();
1470
+ const onInterrupt = () => abortController.abort();
1471
+ process.once('SIGINT', onInterrupt);
1472
+ let announcedStream = false;
1473
+ let announcedInterrupt = false;
1474
+ const result = await createAppChat(cfg, {
1475
+ requestId: str(flags, '--request-id') || generateCreateRequestId(),
1476
+ appType,
1477
+ spaceId,
1478
+ title,
1479
+ content,
1480
+ ...(str(flags, '--model') ? { selectedModel: str(flags, '--model') } : {}),
1481
+ }, {
1482
+ signal: abortController.signal,
1483
+ onEvent: ({ sequence, type }) => {
1484
+ if (flags['--ndjson'] === true) {
1485
+ process.stdout.write(`${JSON.stringify({
1486
+ schemaVersion: 1,
1487
+ type: 'event',
1488
+ sequence,
1489
+ operationId,
1490
+ eventType: type,
1491
+ })}\n`);
1492
+ return;
1493
+ }
1494
+ if (flags['--json'] === true)
1495
+ return;
1496
+ if (sequence === 1 && !announcedStream) {
1497
+ announcedStream = true;
1498
+ log.info('已连接首轮生成流...');
1499
+ }
1500
+ if (type.startsWith('data-graph') && !announcedInterrupt) {
1501
+ announcedInterrupt = true;
1502
+ log.warn('检测到等待输入的生成帧');
1503
+ }
1504
+ },
1505
+ });
1506
+ process.removeListener('SIGINT', onInterrupt);
1507
+ if (flags['--ndjson'] === true) {
1508
+ process.stdout.write(`${JSON.stringify({
1509
+ schemaVersion: 1,
1510
+ type: 'result',
1511
+ sequence: (result.eventCount ?? 0) + 1,
1512
+ operationId,
1513
+ result: {
1514
+ status: result.status,
1515
+ appId: result.identity.appId || undefined,
1516
+ chatId: result.identity.chatId || undefined,
1517
+ sprintId: result.identity.sprintId || undefined,
1518
+ versionId: result.versionId,
1519
+ reused: result.reused,
1520
+ requestId: result.requestId,
1521
+ ...(result.errorText ? { errorText: result.errorText } : {}),
1522
+ },
1523
+ })}\n`);
1524
+ }
1525
+ else {
1526
+ printAppCreateResult(result, createAppOpenUrl(cfg, result.identity.appId), flags);
1527
+ }
1528
+ // T01.04:创建/恢复首轮流终态来源(不重复计 reused 恢复)。
1529
+ recordEvent({ kind: 'cli.sse.result', route: '/api/chat', result: result.status.toLowerCase(),
1530
+ appType, operationId: result.requestId,
1531
+ status: result.reused ? 'reused' : 'created' });
1532
+ switch (result.status) {
1533
+ case 'SUCCEEDED':
1534
+ case 'REUSED':
1535
+ return;
1536
+ case 'WAITING_INPUT':
1537
+ process.exitCode = 2;
1538
+ return;
1539
+ case 'INTERRUPTED':
1540
+ process.exitCode = 130;
1541
+ return;
1542
+ default:
1543
+ process.exitCode = 1;
1544
+ }
1545
+ }
1546
+ // ---------------- 对话 / 版本 / 云函数 / 秘密 / 沙箱(C05/C07/C08/C15/C16) ----------------
1547
+ /** 简化后的对象探测(state 摘要用,避免内联类型断言散落)。 */
1548
+ function isRecordLike(value) {
1549
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
1550
+ }
1551
+ function pickString(flags, key) {
1552
+ const v = flags[key];
1553
+ return typeof v === 'string' && v.trim() ? v.trim() : undefined;
1554
+ }
1555
+ async function readBodyFile(flags, label) {
1556
+ const raw = str(flags, '--file') ?? '';
1557
+ if (!raw || raw === '-') {
1558
+ if (process.stdin.isTTY === true)
1559
+ fail(`${label} 需要 --file <路径|->`);
1560
+ const chunks = [];
1561
+ for await (const chunk of process.stdin)
1562
+ chunks.push(chunk);
1563
+ return Buffer.concat(chunks).toString('utf8');
1564
+ }
1565
+ return readFileSync(path.resolve(raw), 'utf8');
1566
+ }
1567
+ async function cmdChat(command, flags) {
1568
+ const cfg = resolveCfg(flags);
1569
+ const sub = command[1];
1570
+ if (sub === 'history') {
1571
+ requireAuth(cfg);
1572
+ const appId = pickString(flags, '--app-id') ?? command[2];
1573
+ const sprintId = pickString(flags, '--sprint-id');
1574
+ if (!appId || !sprintId)
1575
+ fail('用法: qmuse chat history --app-id <appId> --sprint-id <sprintId>');
1576
+ const r = await queryChatByAppIdAndSprintId(appId, sprintId, cfg);
1577
+ if (!r.ok || !r.data)
1578
+ failApi(r, `查询会话失败`);
1579
+ output(r.data, flags);
1580
+ return;
1581
+ }
1582
+ if (sub === 'state') {
1583
+ requireAuth(cfg);
1584
+ const chatId = command[2] || pickString(flags, '--chat-id');
1585
+ if (!chatId)
1586
+ fail('用法: qmuse chat state <chatId>');
1587
+ const r = await getChatState(chatId, cfg);
1588
+ if (!r.ok || !r.data)
1589
+ failApi(r, `查询会话状态失败`);
1590
+ if (flags['--json'] === true) {
1591
+ output(r.data, flags); // 结构化输出保持原样(零契约变更)
1592
+ return;
1593
+ }
1594
+ // C05.04“断流先用已有 state 查询”:文本模式给出人读摘要 + 可复制的恢复命令。
1595
+ const status = isRecordLike(r.data) && typeof r.data['__chatStatus'] === 'string' ? r.data['__chatStatus'] : 'UNKNOWN';
1596
+ const messages = isRecordLike(r.data) && Array.isArray(r.data['messages']) ? r.data['messages'].length : 0;
1597
+ log.info(`会话 ${chatId} 状态:${status};消息 ${messages} 条`);
1598
+ const pending = summarizePendingInterrupts(r.data, chatId);
1599
+ if (pending.length === 0) {
1600
+ log.info('无待恢复中断(原始结构可用 --json 查看)');
1601
+ return;
1602
+ }
1603
+ log.raw(`
1604
+ `);
1605
+ pending.forEach((item, index) => {
1606
+ log.raw(`[待恢复 ${index + 1}] ${item.kind === 'form' ? '表单' : item.kind === 'bash-interception' ? '命令拦截' : '中断'} ${item.label}
1607
+ `);
1608
+ item.questions.forEach(question => {
1609
+ const meta = [question.control, question.required ? '必填' : ''].filter(Boolean).join(',');
1610
+ log.raw(` - ${question.questionId}(${meta})${question.label}
1611
+ `);
1612
+ if (question.options?.length)
1613
+ log.raw(` 选项:${question.options.join(' / ')}
1614
+ `);
1615
+ });
1616
+ log.raw(` 恢复:${item.resumeCommand}
1617
+ `);
1618
+ log.raw(` 答案结构:${item.answerHint}
1619
+ `);
1620
+ });
1621
+ return;
1622
+ }
1623
+ if (sub === 'cancel') {
1624
+ requireAuth(cfg);
1625
+ const chatId = command[2] || pickString(flags, '--chat-id');
1626
+ const appId = pickString(flags, '--app-id');
1627
+ if (!chatId || !appId)
1628
+ fail('用法: qmuse chat cancel <chatId> --app-id <appId>');
1629
+ const r = await cancelChat(appId, chatId, cfg);
1630
+ if (!r.ok)
1631
+ failApi(r, `取消失败`);
1632
+ if (flags['--json'] === true)
1633
+ output({ success: true, chatId, appId }, flags);
1634
+ else
1635
+ log.success(`已发送停止指令(appId=${appId} chatId=${chatId});服务端确认状态以 getChatState 为准`);
1636
+ return;
1637
+ }
1638
+ if (sub === 'resume') {
1639
+ // C05.04:恢复用户输入与云资源中断——同一会话与 checkpoint,明确 interruptId。
1640
+ requireAuth(cfg);
1641
+ const chatId = command[2] || pickString(flags, '--chat-id');
1642
+ const interruptId = pickString(flags, '--interrupt-id');
1643
+ const appId = pickString(flags, '--app-id') ?? loadAppBinding(process.cwd())?.appId;
1644
+ const sprintId = pickString(flags, '--sprint-id');
1645
+ if (!chatId || !interruptId || !appId) {
1646
+ fail('用法: qmuse chat resume <chatId> --interrupt-id <id> --file <answerFile> --app-id <appId> [--sprint-id <id>] [--ndjson]');
1647
+ }
1648
+ const answerRaw = (await readBodyFile(flags, 'chat resume')).trim();
1649
+ if (!answerRaw)
1650
+ fail('恢复回答不能为空:--file 传入 JSON(questions 答案或云资源决定)');
1651
+ let answer;
1652
+ try {
1653
+ answer = JSON.parse(answerRaw);
1654
+ }
1655
+ catch {
1656
+ fail('--file 内容必须是 JSON 对象(answers/data/response 之一)');
1657
+ }
1658
+ if (!answer || typeof answer !== 'object' || Array.isArray(answer))
1659
+ fail('--file 内容必须是 JSON 对象(answers/data/response 之一)');
1660
+ const answerObj = answer;
1661
+ const interruptResponse = { ...answerObj, interruptId };
1662
+ const operationId = `op_${randomBytes(12).toString('hex')}`;
1663
+ const abort = new AbortController();
1664
+ const onInterrupt = () => abort.abort();
1665
+ process.once('SIGINT', onInterrupt);
1666
+ const result = await continueChat(cfg, {
1667
+ appId,
1668
+ chatId,
1669
+ ...(sprintId ? { sprintId } : {}),
1670
+ interruptResponse,
1671
+ }, {
1672
+ signal: abort.signal,
1673
+ onEvent: ({ sequence, type }) => {
1674
+ if (flags['--ndjson'] === true) {
1675
+ process.stdout.write(`${JSON.stringify({ schemaVersion: 1, type: 'event', sequence, operationId, eventType: type })}\n`);
1676
+ }
1677
+ },
1678
+ });
1679
+ process.off('SIGINT', onInterrupt);
1680
+ if (flags['--ndjson'] === true) {
1681
+ process.stdout.write(`${JSON.stringify({ schemaVersion: 1, type: 'result', sequence: result.eventCount + 1, operationId, result: { status: result.status, chatId: result.chatId, ...(result.errorText ? { errorText: result.errorText } : {}) } })}\n`);
1682
+ }
1683
+ else if (flags['--json'] === true) {
1684
+ output({ schemaVersion: 1, success: result.status === 'SUCCEEDED' || result.status === 'WAITING_INPUT', status: result.status, interruptId, data: { chatId: result.chatId, toolConsumed: result.toolConsumed }, ...(result.errorText ? { errorText: result.errorText } : {}) }, flags);
1685
+ }
1686
+ else if (result.status === 'SUCCEEDED') {
1687
+ log.success('中断恢复完成');
1688
+ }
1689
+ else {
1690
+ log.error(`恢复未完成:${result.status}${result.errorText ? `,${result.errorText}` : ''}`);
1691
+ process.exitCode = result.status === 'WAITING_INPUT' ? 2 : 1;
1692
+ }
1693
+ // T01.04:恢复流终态来源(同一会话 checkpoint,不重跑首轮生成)。
1694
+ recordEvent({ kind: 'cli.sse.result', route: '/api/chat', result: result.status.toLowerCase(), operationId });
1695
+ return;
1696
+ }
1697
+ if (sub === 'clear') {
1698
+ requireAuth(cfg);
1699
+ const chatId = command[2] || pickString(flags, '--chat-id');
1700
+ const appId = pickString(flags, '--app-id');
1701
+ const sprintId = pickString(flags, '--sprint-id');
1702
+ if (!chatId || !appId || !sprintId)
1703
+ fail('用法: qmuse chat clear <chatId> --app-id <appId> --sprint-id <sprintId>');
1704
+ const r = await clearChatContext(appId, chatId, sprintId, cfg);
1705
+ if (!r.ok)
1706
+ failApi(r, `清理会话上下文失败`);
1707
+ if (flags['--json'] === true)
1708
+ output({ success: true, chatId }, flags);
1709
+ else
1710
+ log.success(`会话上下文已清理(chatId=${chatId})`);
1711
+ return;
1712
+ }
1713
+ if (sub === 'send') {
1714
+ requireAuth(cfg);
1715
+ const appId = pickString(flags, '--app-id');
1716
+ const chatId = pickString(flags, '--chat-id');
1717
+ const content = (await readBodyFile(flags, 'chat send')).trim();
1718
+ if (!content)
1719
+ fail('首条继续内容不能为空');
1720
+ if (!appId || !chatId)
1721
+ fail('用法: qmuse chat send --app-id <appId> --chat-id <chatId> --file <路径|-> [--model <m>] [--ndjson]');
1722
+ const model = pickString(flags, '--model') ?? 'auto';
1723
+ const skills = pickString(flags, '--skill');
1724
+ const skillVersion = pickString(flags, '--skill-version');
1725
+ if ((skills && !skillVersion) || (!skills && skillVersion)) {
1726
+ fail('--skill 与 --skill-version 必须同时提供(精确版本引用)');
1727
+ }
1728
+ if (model !== 'auto') {
1729
+ const models = await listModels(cfg);
1730
+ if (!models.ok || !models.data)
1731
+ fail(`模型列表查询失败: ${models.errorMessage ?? models.errorCode ?? `HTTP ${models.status}`}`);
1732
+ const known = collectModelIds(models.data);
1733
+ if (!known.includes(model)) {
1734
+ fail(`未知模型 ${model};可用: auto, ${known.slice(0, 8).join(', ')}${known.length > 8 ? ' …' : ''}`);
1735
+ }
1736
+ }
1737
+ const extraChat = {};
1738
+ if (skills)
1739
+ extraChat.skills = [skills];
1740
+ if (skillVersion)
1741
+ extraChat.skillVersionIds = { [skills]: skillVersion };
1742
+ const operationId = `op_${randomBytes(12).toString('hex')}`;
1743
+ const abort = new AbortController();
1744
+ const onInterrupt = () => abort.abort();
1745
+ process.once('SIGINT', onInterrupt);
1746
+ const result = await continueChat(cfg, {
1747
+ ...pickChatSeed(flags),
1748
+ ...extraChat,
1749
+ appId,
1750
+ chatId,
1751
+ selectedModel: model,
1752
+ message: { role: 'user', id: `mu_${randomBytes(12).toString('hex')}`, content },
1753
+ }, {
1754
+ signal: abort.signal,
1755
+ onEvent: ({ sequence, type }) => {
1756
+ if (flags['--ndjson'] === true) {
1757
+ process.stdout.write(`${JSON.stringify({ schemaVersion: 1, type: 'event', sequence, operationId, eventType: type })}\n`);
1758
+ }
1759
+ else if (flags['--json'] !== true && sequence === 1) {
1760
+ log.info('已连接生成流...');
1761
+ }
1762
+ },
1763
+ });
1764
+ process.removeListener('SIGINT', onInterrupt);
1765
+ if (flags['--ndjson'] === true) {
1766
+ process.stdout.write(`${JSON.stringify({ schemaVersion: 1, type: 'result', sequence: result.eventCount + 1, operationId, result: { status: result.status, chatId: result.chatId, ...(result.errorText ? { errorText: result.errorText } : {}) } })}\n`);
1767
+ }
1768
+ else if (flags['--json'] === true) {
1769
+ output({ schemaVersion: 1, success: result.status === 'SUCCEEDED' || result.status === 'WAITING_INPUT', status: result.status, data: { chatId: result.chatId, toolConsumed: result.toolConsumed }, ...(result.errorText ? { errorText: result.errorText } : {}) }, flags);
1770
+ }
1771
+ else if (result.status === 'SUCCEEDED') {
1772
+ log.success('本轮对话完成');
1773
+ }
1774
+ else {
1775
+ log.error(`对话未完成:${result.status}${result.errorText ? `,${result.errorText}` : ''}`);
1776
+ process.exitCode = result.status === 'WAITING_INPUT' ? 2 : 1;
1777
+ }
1778
+ // T01.04:已有会话 continue 流终态来源。
1779
+ recordEvent({ kind: 'cli.sse.result', route: '/api/chat', result: result.status.toLowerCase(), operationId });
1780
+ return;
1781
+ }
1782
+ if (sub === 'attachment') {
1783
+ if (command[2] !== 'upload')
1784
+ fail('用法: qmuse chat attachment upload <path> --app-id <appId> [--version <versionId>]');
1785
+ requireAuth(cfg);
1786
+ const appId = pickString(flags, '--app-id');
1787
+ const filePath = command[3];
1788
+ if (!appId || !filePath)
1789
+ fail('用法: qmuse chat attachment upload <path> --app-id <appId> [--version <versionId>]');
1790
+ let versionId = pickString(flags, '--version');
1791
+ if (!versionId) {
1792
+ const versions = await listAppVersions(appId, cfg);
1793
+ if (!versions.ok || !versions.data?.list?.length) {
1794
+ fail('未找到可上传的版本;请用 --version 指定版本 ID 或先完成首次创建/导入');
1795
+ }
1796
+ versionId = versions.data.list[0].id;
1797
+ log.info(`未指定 --version,使用最新版本 ${versionId}`);
1798
+ }
1799
+ const stats = statSync(path.resolve(filePath));
1800
+ const r = await uploadAttachment(cfg, path.resolve(filePath), versionId, appId);
1801
+ if (!r.ok || !r.data)
1802
+ failApi(r, `附件上传失败`);
1803
+ // T01.05:上传结果来源(资源归属校验由 /api/file/upload 版本权限完成)。
1804
+ recordEvent({ kind: 'cli.transfer.result', route: '/api/file/upload',
1805
+ ...(Number.isFinite(stats.size) && stats.size >= 0 ? { bytes: stats.size } : {}), result: 'ok' });
1806
+ output(r.data, flags);
1807
+ return;
1808
+ }
1809
+ fail(`未知 chat 子命令: ${sub ?? '(空)'}。可用: history / state / cancel / clear / send / resume / attachment`);
1810
+ }
1811
+ /** 继续对话 base 上下文:从绑定与 --model/--biz 已知字段拼装,未提供的字段遵循服务端可选性。 */
1812
+ function collectModelIds(data) {
1813
+ const items = Array.isArray(data) ? data : data?.list;
1814
+ if (!Array.isArray(items))
1815
+ return [];
1816
+ return items.map(item => item.modelId).filter((v) => typeof v === 'string');
1817
+ }
1818
+ function pickChatSeed(flags) {
1819
+ const biz = pickString(flags, '--biz');
1820
+ return biz ? { bizExtInfo: JSON.parse(biz) } : {};
1821
+ }
1822
+ async function cmdModels(_command, flags) {
1823
+ const cfg = resolveCfg(flags);
1824
+ requireAuth(cfg);
1825
+ const r = await listModels(cfg);
1826
+ if (!r.ok || !r.data)
1827
+ failApi(r, `查询模型列表失败`);
1828
+ output(r.data, flags);
1829
+ }
1830
+ async function cmdVersions(command, flags) {
1831
+ const cfg = resolveCfg(flags);
1832
+ requireAuth(cfg);
1833
+ const sub = command[1];
1834
+ if (sub === 'list') {
1835
+ const appId = pickString(flags, '--app-id');
1836
+ if (!appId)
1837
+ fail('用法: qmuse version list --app-id <appId>');
1838
+ const r = await listAppVersions(appId, cfg);
1839
+ if (!r.ok || !r.data)
1840
+ failApi(r, `查询版本失败`);
1841
+ if (flags['--json'] === true) {
1842
+ output(r.data, flags);
1843
+ return;
1844
+ }
1845
+ const versions = r.data.list ?? [];
1846
+ log.info(`应用 ${appId} 共 ${r.data.total ?? versions.length} 个版本:`);
1847
+ for (const v of versions) {
1848
+ log.raw(`${v.id} ${(v.status ?? '').padEnd(10)} ${v.gmtCreate ?? ''} ${v.remark ?? ''}\n`);
1849
+ }
1850
+ return;
1851
+ }
1852
+ if (sub === 'info') {
1853
+ const versionId = command[2];
1854
+ const appId = pickString(flags, '--app-id');
1855
+ if (!versionId || !appId)
1856
+ fail('用法: qmuse version info <versionId> --app-id <appId>');
1857
+ const r = await listAppVersions(appId, cfg);
1858
+ if (!r.ok || !r.data)
1859
+ failApi(r, `查询版本失败`);
1860
+ const version = (r.data.list ?? []).find(item => item.id === versionId);
1861
+ if (!version)
1862
+ fail(`应用 ${appId} 中未找到版本 ${versionId}`);
1863
+ output(version, flags);
1864
+ return;
1865
+ }
1866
+ fail(`未知 version 子命令: ${sub ?? '(空)'}。可用: list / info(diff/restore/artifact 待服务端契约开放)`);
1867
+ }
1868
+ async function cmdCodeExport(_command, flags) {
1869
+ const cfg = resolveCfg(flags);
1870
+ requireAuth(cfg);
1871
+ const appId = pickString(flags, '--app-id');
1872
+ const outDir = pickString(flags, '--output');
1873
+ if (!appId || !outDir)
1874
+ fail('用法: qmuse code export --app-id <appId> --output <dir>');
1875
+ const r = await fetchSourceFiles(appId, cfg);
1876
+ if (!r.ok || !r.data)
1877
+ failApi(r, `获取源码失败`);
1878
+ const requestedVersion = pickString(flags, '--version');
1879
+ if (requestedVersion && r.data.versionId && requestedVersion !== r.data.versionId) {
1880
+ fail(`服务端当前仅提供最新版源码导出:latest=${r.data.versionId},请求=${requestedVersion}`);
1881
+ }
1882
+ mkdirSync(path.resolve(outDir), { recursive: true });
1883
+ const files = Object.entries(r.data.files ?? {});
1884
+ for (const [filePath, file] of files) {
1885
+ const target = path.join(path.resolve(outDir), filePath);
1886
+ mkdirSync(path.dirname(target), { recursive: true });
1887
+ writeFileSync(target, file.value ?? '', 'utf8');
1888
+ }
1889
+ // T01.05:导出下载来源(资源、文件数与关联版本)。
1890
+ recordEvent({ kind: 'cli.transfer.result', route: '/api/app/code', result: 'ok',
1891
+ ...(r.data.versionId ? { status: 'version-synced' } : {}) });
1892
+ if (flags['--json'] === true) {
1893
+ output({ success: true, appId, versionId: r.data.versionId ?? null, fileCount: files.length, output: path.resolve(outDir) }, flags);
1894
+ }
1895
+ else {
1896
+ log.success(`已导出 ${files.length} 个文件到 ${path.resolve(outDir)}(versionId=${r.data.versionId ?? '(无版本)'})`);
1897
+ }
1898
+ }
1899
+ async function cmdFn(command, flags) {
1900
+ const cfg = resolveCfg(flags);
1901
+ requireAuth(cfg);
1902
+ const sub = command[1];
1903
+ const appId = pickString(flags, '--app-id');
1904
+ const functionId = pickString(flags, '--function-id');
1905
+ if (sub === 'list') {
1906
+ if (!appId)
1907
+ fail('用法: qmuse fn list --app-id <appId>');
1908
+ const r = await listFunctions(appId, cfg);
1909
+ if (!r.ok || !r.data)
1910
+ failApi(r, `查询云函数失败`);
1911
+ output(r.data, flags);
1912
+ return;
1913
+ }
1914
+ if (sub === 'info') {
1915
+ if (!appId || !functionId)
1916
+ fail('用法: qmuse fn info --app-id <appId> --function-id <fid>');
1917
+ const r = await functionDetail(appId, functionId, cfg);
1918
+ if (!r.ok || !r.data)
1919
+ failApi(r, `查询函数详情失败`);
1920
+ output(r.data, flags);
1921
+ return;
1922
+ }
1923
+ if (sub === 'retry') {
1924
+ if (!appId || !functionId)
1925
+ fail('用法: qmuse fn retry --app-id <appId> --function-id <fid>');
1926
+ const r = await retryFunctionPublish(appId, functionId, cfg);
1927
+ // T01.08:既有函数发布重试的受受理/结果来源事件。
1928
+ recordEvent({ kind: 'cli.task.state', route: '/api/appwrite/functions/retry-publish',
1929
+ stage: 'accepted', operationId: `${appId}:${functionId}`, result: r.ok ? 'ok' : 'failed' });
1930
+ if (!r.ok)
1931
+ failApi(r, `重试发布失败`);
1932
+ if (flags['--json'] === true)
1933
+ output({ success: true, appId, functionId }, flags);
1934
+ else
1935
+ log.success(`已重试函数 ${functionId} 发布`);
1936
+ return;
1937
+ }
1938
+ const subByPath = {
1939
+ 'deployment-list': () => listFunctionDeployments(appId, functionId, pageFor(flags), cfg),
1940
+ 'deployment-info': () => deploymentDetail(appId, functionId, command[2], cfg),
1941
+ 'execution-list': () => listFunctionExecutions(appId, functionId, pageFor(flags), cfg),
1942
+ 'execution-info': () => executionDetail(appId, functionId, command[2], cfg),
1943
+ };
1944
+ if (sub && subByPath[sub]) {
1945
+ if (!appId || !functionId)
1946
+ fail(`用法: qmuse fn ${sub} --app-id <appId> --function-id <fid> [资源ID]`);
1947
+ const r = await subByPath[sub]();
1948
+ if (!r.ok || !r.data)
1949
+ failApi(r, `查询失败`);
1950
+ output(r.data, flags);
1951
+ return;
1952
+ }
1953
+ fail(`未知 fn 子命令: ${sub ?? '(空)'}。可用: list / info / retry / deployment-list / deployment-info / execution-list / execution-info`);
1954
+ }
1955
+ function pageFor(flags) {
1956
+ const limit = pickString(flags, '--limit');
1957
+ const offset = pickString(flags, '--offset');
1958
+ return {
1959
+ ...(limit ? { limit: Number(limit) } : {}),
1960
+ ...(offset ? { offset: Number(offset) } : {}),
1961
+ };
1962
+ }
1963
+ async function cmdSecret(command, flags) {
1964
+ const cfg = resolveCfg(flags);
1965
+ requireAuth(cfg);
1966
+ const sub = command[1];
1967
+ const appId = pickString(flags, '--app-id');
1968
+ const secretId = command[2] || pickString(flags, '--secret-id');
1969
+ if (sub === 'list') {
1970
+ if (!appId)
1971
+ fail('用法: qmuse secret list --app-id <appId> [--cloud-env dev|prod]');
1972
+ const cloudEnv = pickString(flags, '--cloud-env');
1973
+ if (cloudEnv && cloudEnv !== 'dev' && cloudEnv !== 'prod')
1974
+ fail('--cloud-env 仅支持 dev / prod');
1975
+ const r = await listSecrets(appId, cfg, cloudEnv);
1976
+ if (!r.ok || !r.data)
1977
+ failApi(r, `查询秘密失败`);
1978
+ if (flags['--json'] === true) {
1979
+ output(r.data, flags);
1980
+ return;
1981
+ }
1982
+ const payload = r.data;
1983
+ const items = (payload.secrets ?? payload.list ?? []);
1984
+ log.info(`环境 ${cloudEnv ?? 'dev'}:共 ${typeof payload.total === 'number' ? payload.total : items.length} 个密钥`);
1985
+ items.forEach(item => {
1986
+ const state = `${String(item.status ?? '')}/${String(item.syncStatus ?? '')}${item.lastError ? `(${String(item.lastError).slice(0, 60)})` : ''}`;
1987
+ log.raw(` ${String(item.key)} [${item.id}] ${state}(更新于 ${String(item.updatedAt ?? '?')})
1988
+ `);
1989
+ });
1990
+ return;
1991
+ }
1992
+ if (sub === 'create') {
1993
+ if (!appId || !secretId)
1994
+ fail('用法: qmuse secret create <KEY> --app-id <appId> --value-stdin [--description <d>]');
1995
+ const value = (await readBodyFile(flags, 'secret create --value-stdin')).replace(/\n+$/, '');
1996
+ if (!value)
1997
+ fail('秘密值不能为空(--value-stdin 从管道/文件读取)');
1998
+ const key = secretId;
1999
+ const { createSecret } = await import('./app/secret.js');
2000
+ const r = await createSecret(appId, key, value, cfg, pickString(flags, '--description'));
2001
+ if (!r.ok || !r.data)
2002
+ failApi(r, `创建秘密失败`);
2003
+ if (flags['--json'] === true)
2004
+ output({ success: true, appId, key }, flags);
2005
+ else
2006
+ log.success(`秘密 ${key} 已创建/更新并进入同步流程`);
2007
+ return;
2008
+ }
2009
+ if (sub === 'enable' || sub === 'disable') {
2010
+ // 停用(会同步删除云上变量)是删除的前置条件;启用走反向同步。
2011
+ if (!appId || !secretId)
2012
+ fail(`用法: qmuse secret ${sub} <secretId> --app-id <appId> [--cloud-env dev|prod]`);
2013
+ const cloudEnv = pickString(flags, '--cloud-env');
2014
+ if (cloudEnv && cloudEnv !== 'dev' && cloudEnv !== 'prod')
2015
+ fail('--cloud-env 仅支持 dev / prod');
2016
+ const r = sub === 'disable'
2017
+ ? await disableSecret(appId, secretId, cfg, cloudEnv)
2018
+ : await enableSecret(appId, secretId, cfg, cloudEnv);
2019
+ if (!r.ok)
2020
+ failApi(r, `${sub === 'disable' ? '停用' : '启用'}秘密失败`);
2021
+ if (flags['--json'] === true)
2022
+ output({ success: true, appId, secretId, action: sub, env: cloudEnv ?? 'dev' }, flags);
2023
+ else
2024
+ log.success(`已${sub === 'disable' ? '停用' : '启用'}秘密 ${secretId}(${cloudEnv ?? 'dev'})`);
2025
+ return;
2026
+ }
2027
+ if (sub === 'delete') {
2028
+ if (!appId || !secretId)
2029
+ fail('用法: qmuse secret delete <secretId> --app-id <appId>');
2030
+ if (flags['--yes'] !== true)
2031
+ fail('删除秘密不可恢复:请显式传入 --yes');
2032
+ const cloudEnv = pickString(flags, '--cloud-env');
2033
+ if (cloudEnv && cloudEnv !== 'dev' && cloudEnv !== 'prod')
2034
+ fail('--cloud-env 仅支持 dev / prod');
2035
+ const r = await deleteSecret(appId, secretId, cfg, cloudEnv);
2036
+ if (!r.ok) {
2037
+ const reason = r.errorMessage ?? r.errorCode ?? `HTTP ${r.status}`;
2038
+ const hint = /系统异常|停用/i.test(String(reason))
2039
+ ? `;服务端仅允许删除"已停用且同步完成"的密钥:请先执行 qmuse secret disable ${secretId} --app-id ${appId} 后重试`
2040
+ : '';
2041
+ fail(`删除秘密失败: ${reason}${hint}`);
2042
+ }
2043
+ if (flags['--json'] === true)
2044
+ output({ success: true, appId, secretId, env: cloudEnv ?? 'dev' }, flags);
2045
+ else
2046
+ log.success(`已删除秘密 ${secretId}(${cloudEnv ?? 'dev'})`);
2047
+ return;
2048
+ }
2049
+ if (sub === 'sync') {
2050
+ const syncSub = command[2];
2051
+ const syncId = command[3] || pickString(flags, '--secret-id');
2052
+ const syncEnv = pickString(flags, '--cloud-env');
2053
+ if (syncEnv && syncEnv !== 'dev' && syncEnv !== 'prod')
2054
+ fail('--cloud-env 仅支持 dev / prod');
2055
+ if (!appId || !syncId)
2056
+ fail('用法: qmuse secret sync status|retry <secretId> --app-id <appId> [--cloud-env dev|prod]');
2057
+ if (syncSub === 'status') {
2058
+ const r = await listSecrets(appId, cfg, syncEnv);
2059
+ if (!r.ok || !r.data)
2060
+ failApi(r, `查询秘密同步状态失败`);
2061
+ const payload = r.data.secrets ?? r.data.list ?? r.data;
2062
+ const target = Array.isArray(payload)
2063
+ ? payload.find(item => item.secretId === syncId || item.key === syncId)
2064
+ : undefined;
2065
+ output(target ?? { secretId: syncId, found: false, list: payload }, flags);
2066
+ return;
2067
+ }
2068
+ if (syncSub === 'retry') {
2069
+ const r = await retrySecretSync(appId, syncId, cfg, syncEnv);
2070
+ // T01.08:秘密同步重试的结果来源事件(成功/失败都保留)。
2071
+ recordEvent({ kind: 'cli.task.state', route: '/api/appwrite/secrets/retry-sync',
2072
+ stage: 'accepted', operationId: syncId, result: r.ok ? 'ok' : 'failed',
2073
+ ...(r.errorCode ? { errorCode: r.errorCode } : {}) });
2074
+ if (!r.ok)
2075
+ failApi(r, `重试同步失败`);
2076
+ if (flags['--json'] === true)
2077
+ output({ success: true, secretId: syncId }, flags);
2078
+ else
2079
+ log.success(`已触发秘密 ${syncId} 重新同步`);
2080
+ return;
2081
+ }
2082
+ fail('用法: qmuse secret sync status|retry <secretId> --app-id <appId>');
2083
+ return;
2084
+ }
2085
+ fail(`未知 secret 子命令: ${sub ?? '(空)'}。可用: list / create / enable / disable / delete(先停用再删除) / sync status|retry`);
2086
+ }
2087
+ async function cmdSandbox(command, flags) {
2088
+ const cfg = resolveCfg(flags);
2089
+ requireAuth(cfg);
2090
+ const sub = command[1];
2091
+ const appId = pickString(flags, '--app-id');
2092
+ const sprintId = pickString(flags, '--sprint-id');
2093
+ const chatId = pickString(flags, '--chat-id');
2094
+ const sandboxId = pickString(flags, '--sandbox-id');
2095
+ if (sub === 'ensure') {
2096
+ if (!appId || !sprintId)
2097
+ fail('用法: qmuse sandbox ensure --app-id <appId> --sprint-id <sprintId>');
2098
+ const r = await ensureSandbox(appId, sprintId, cfg);
2099
+ if (!r.ok || !r.data)
2100
+ failApi(r, `确保沙箱失败`);
2101
+ output(r.data, flags);
2102
+ return;
2103
+ }
2104
+ if (sub === 'restart') {
2105
+ if (!chatId)
2106
+ fail('用法: qmuse sandbox restart --chat-id <chatId>(服务端按会话授权)');
2107
+ const r = await restartPreview(chatId, cfg);
2108
+ if (!r.ok || !r.data)
2109
+ failApi(r, `重启预览失败`);
2110
+ output(r.data, flags);
2111
+ return;
2112
+ }
2113
+ if (sub === 'setting-get') {
2114
+ if (!appId)
2115
+ fail('用法: qmuse sandbox setting-get --app-id <appId>');
2116
+ const r = await querySandboxSetting(appId, cfg);
2117
+ if (!r.ok || !r.data)
2118
+ failApi(r, `查询沙箱配置失败`);
2119
+ output(r.data, flags);
2120
+ return;
2121
+ }
2122
+ if (sub === 'status') {
2123
+ // C07.05:集中返回沙箱配置、预览、健康及失败原因(结果全部来自现有平台状态)。
2124
+ if (!appId)
2125
+ fail('用法: qmuse sandbox status --app-id <appId> [--sandbox-id <id>]');
2126
+ const resolveApi = async (call, label) => {
2127
+ try {
2128
+ const r = await call;
2129
+ if (!r.ok)
2130
+ return { unavailable: r.errorMessage ?? r.errorCode ?? `HTTP ${r.status}` };
2131
+ return r.data ?? null;
2132
+ }
2133
+ catch (err) {
2134
+ return { unavailable: err instanceof Error ? err.message : String(err), section: label };
2135
+ }
2136
+ };
2137
+ const settingPromise = resolveApi(querySandboxSetting(appId, cfg), 'setting');
2138
+ let discoveredSandboxId;
2139
+ let effectiveSandboxId = sandboxId;
2140
+ if (!effectiveSandboxId) {
2141
+ // 沙箱标识自动发现:previewHealth 对不匹配 id 的失败响应会暴露 actual=SBX_*(现有平台状态);
2142
+ // 与 setting 并行执行,随后 previewInfo/previewHealth 再并行,减少串行 RTT。
2143
+ const probe = await Promise.all([settingPromise, previewHealth(appId, 'qmuse-cli-discovery', cfg)]).then(([, p]) => p);
2144
+ const mismatch = /actual=(SBX_[a-z0-9]+)/iu.exec(`${probe.errorMessage ?? ''} ${JSON.stringify(probe.data ?? {})}`);
2145
+ if (mismatch) {
2146
+ discoveredSandboxId = mismatch[1];
2147
+ effectiveSandboxId = discoveredSandboxId;
2148
+ }
2149
+ }
2150
+ let preview = { unavailable: '缺少 --sandbox-id(活跃沙箱标识来自 dev 会话或 chat 上下文)' };
2151
+ let health = preview;
2152
+ if (effectiveSandboxId) {
2153
+ const [info, healthy] = await Promise.all([
2154
+ resolveApi(previewInfo(appId, effectiveSandboxId, cfg), 'preview'),
2155
+ resolveApi(previewHealth(appId, effectiveSandboxId, cfg), 'health'),
2156
+ ]);
2157
+ preview = info;
2158
+ health = healthy;
2159
+ }
2160
+ const setting = await settingPromise;
2161
+ const data = { appId, ...(effectiveSandboxId ? { sandboxId: effectiveSandboxId } : {}),
2162
+ ...(discoveredSandboxId ? { sandboxIdDiscovered: true } : {}),
2163
+ setting, preview, health };
2164
+ if (flags['--json'] === true) {
2165
+ output(data, flags);
2166
+ return;
2167
+ }
2168
+ log.info(`沙箱配置:${JSON.stringify(setting)}`);
2169
+ log.info(`预览信息:${JSON.stringify(preview)}`);
2170
+ log.info(`健康状态:${JSON.stringify(health)}`);
2171
+ return;
2172
+ }
2173
+ if (sub === 'health') {
2174
+ if (!appId || !sandboxId)
2175
+ fail('sandbox health 需要 --app-id 与 --sandbox-id(活跃沙箱标识来自 dev 会话 / socketInfo)');
2176
+ const r = await previewHealth(appId, sandboxId, cfg);
2177
+ if (!r.ok || !r.data)
2178
+ failApi(r, `查询沙箱失败`);
2179
+ output(r.data, flags);
2180
+ return;
2181
+ }
2182
+ if (sub === 'logs') {
2183
+ // C07.14:现有接口可提供的任务错误与构建日志;未取得日志必须显式标注,不解释为成功。
2184
+ if (!appId)
2185
+ fail('用法: qmuse sandbox logs --app-id <appId> [--sandbox-id <id>] [--version-id <id>] [--build-id <id>]');
2186
+ const versionId = pickString(flags, '--version-id');
2187
+ const buildId = pickString(flags, '--build-id');
2188
+ const sources = {};
2189
+ const artifacts = await buildArtifactsStatus(appId, {
2190
+ ...(sprintId ? { sprintId } : {}),
2191
+ ...(versionId ? { versionId } : {}),
2192
+ ...(buildId ? { buildId } : {}),
2193
+ artifactTarget: 'preview',
2194
+ }, cfg);
2195
+ sources.buildArtifacts = artifacts.ok
2196
+ ? artifacts.data
2197
+ : { unavailable: artifacts.errorMessage ?? artifacts.errorCode ?? `HTTP ${artifacts.status}` };
2198
+ if (sandboxId) {
2199
+ const info = await previewInfo(appId, sandboxId, cfg);
2200
+ sources.previewInfo = info.ok ? info.data : { unavailable: info.errorMessage ?? info.errorCode ?? `HTTP ${info.status}` };
2201
+ const health = await previewHealth(appId, sandboxId, cfg);
2202
+ sources.previewHealth = health.ok ? health.data : { unavailable: health.errorMessage ?? health.errorCode ?? `HTTP ${health.status}` };
2203
+ }
2204
+ // 日志判定:仅当响应中存在非空的日志类字段(logs/buildLogs/lines/errorLogs/taskError/logsUrl 等)才算取得,
2205
+ // 仅返回构建状态(fileCount/status)不得解释为成功(C07.14“不将未取得日志解释为成功”)。
2206
+ const isLogKey = (key) => /^(?:[a-z]+_)*logs?$/iu.test(key) // logs / build_logs / preview_log
2207
+ || /(?<=[a-z])[A-Z]Logs?$/.test(key) // buildLogs / taskLog(驼峰边界,排除 catalog)
2208
+ || /^(errorLogs?|taskError|logLines?|logsUrl|logUrl)$/iu.test(key);
2209
+ const hasLogContent = (value, depth = 0) => {
2210
+ if (depth > 4 || value == null || typeof value !== 'object')
2211
+ return false;
2212
+ // 数组本身不算日志:向内寻找日志类键(避免任意非空列表误判为“取得日志”)。
2213
+ if (Array.isArray(value))
2214
+ return value.some(item => hasLogContent(item, depth + 1));
2215
+ for (const [key, item] of Object.entries(value)) {
2216
+ if (isLogKey(key)) {
2217
+ if (typeof item === 'string' && item.trim().length > 0)
2218
+ return true;
2219
+ if (Array.isArray(item) && item.length > 0)
2220
+ return true;
2221
+ }
2222
+ if (hasLogContent(item, depth + 1))
2223
+ return true;
2224
+ }
2225
+ return false;
2226
+ };
2227
+ const anyLogs = Object.values(sources).some(v => v && typeof v === 'object' && !('unavailable' in v)
2228
+ && hasLogContent(v));
2229
+ const data = {
2230
+ appId,
2231
+ ...(sandboxId ? { sandboxId } : {}),
2232
+ ...(versionId ? { versionId } : {}),
2233
+ sources,
2234
+ logsAvailable: anyLogs,
2235
+ ...(anyLogs ? {} : { logsUnavailableReason: '现有平台接口未返回日志内容;请结合 --build-id/--version-id 重试或在开发会话内查看' }),
2236
+ };
2237
+ output(data, flags);
2238
+ if (!anyLogs)
2239
+ process.exitCode = 3; // 明确非成功:未取得日志
2240
+ return;
2241
+ }
2242
+ fail(`未知 sandbox 子命令: ${sub ?? '(空)'}。可用: ensure / restart / status / health / logs / setting-get`);
2243
+ }
2244
+ // ---------------- 数据域(C13) ----------------
2245
+ async function cmdDataSources(command, flags) {
2246
+ const cfg = resolveCfg(flags);
2247
+ requireAuth(cfg);
2248
+ const sub = command[1];
2249
+ const appId = pickString(flags, '--app-id');
2250
+ const sourceId = pickString(flags, '--source-id') ?? command[2];
2251
+ if (!appId)
2252
+ fail('用法: qmuse data-source list|info --app-id <appId>');
2253
+ const versionId = pickString(flags, '--version')
2254
+ ?? ((await listAppVersions(appId, cfg)).data?.list ?? [])[0]?.id;
2255
+ if (!versionId)
2256
+ fail('查询版本失败:无法确定 devAppVersionId');
2257
+ const r = await queryAppMetadata(appId, versionId, cfg);
2258
+ if (!r.ok || !r.data)
2259
+ failApi(r, `查询应用数据元信息失败`);
2260
+ const payload = r.data;
2261
+ if (sub === 'list') {
2262
+ output({ appId, devAppVersionId: versionId, tables: payload }, flags);
2263
+ return;
2264
+ }
2265
+ if (sub === 'info') {
2266
+ if (!sourceId)
2267
+ fail('用法: qmuse data-source info <formId> --app-id <appId>');
2268
+ const flat = [];
2269
+ const walk = (node) => {
2270
+ if (Array.isArray(node)) {
2271
+ node.forEach(walk);
2272
+ return;
2273
+ }
2274
+ if (node && typeof node === 'object') {
2275
+ const record = node;
2276
+ if (typeof record.name === 'string' && Array.isArray(record.properties))
2277
+ flat.push(record);
2278
+ Object.values(record).forEach(walk);
2279
+ }
2280
+ };
2281
+ walk(payload);
2282
+ const found = flat.find(item => item.name === sourceId);
2283
+ output(found ?? { formId: sourceId, found: false, hint: 'data-source list 查看全部表' }, flags);
2284
+ return;
2285
+ }
2286
+ fail(`未知 data-source 子命令: ${sub ?? '(空)'}。可用: list / info`);
2287
+ }
2288
+ async function cmdDb(command, flags) {
2289
+ const cfg = resolveCfg(flags);
2290
+ requireAuth(cfg);
2291
+ const sub = command[1];
2292
+ const appId = pickString(flags, '--app-id');
2293
+ const formId = pickString(flags, '--form-id');
2294
+ const env = pickString(flags, '--cloud-env') ?? 'dev';
2295
+ if (!appId || !formId)
2296
+ fail('用法: qmuse db <子命令> --app-id <appId> --form-id <formId>');
2297
+ if (sub === 'tables') {
2298
+ const r = await queryAppMetadata(appId, ((await listAppVersions(appId, cfg)).data?.list ?? [])[0]?.id ?? '', cfg);
2299
+ if (!r.ok || !r.data)
2300
+ failApi(r, `查询数据表失败`);
2301
+ output(r.data, flags);
2302
+ return;
2303
+ }
2304
+ if (sub === 'permission' && command[2] === 'get') {
2305
+ const r = await queryFormAuth(appId, formId, cfg);
2306
+ if (!r.ok || !r.data)
2307
+ failApi(r, `读取表权限失败`);
2308
+ output(r.data, flags);
2309
+ return;
2310
+ }
2311
+ if (sub === 'permission' && command[2] === 'set') {
2312
+ const file = pickString(flags, '--file');
2313
+ if (!file)
2314
+ fail('用法: qmuse db permission set --file <permissions.json>');
2315
+ const permissions = JSON.parse(readFileSync(path.resolve(file), 'utf8'));
2316
+ const r = await syncFormAuth(appId, formId, permissions, cfg);
2317
+ if (!r.ok)
2318
+ failApi(r, `更新表权限失败`);
2319
+ output({ success: true, formId }, flags);
2320
+ return;
2321
+ }
2322
+ if (sub === 'records') {
2323
+ const op = command[2];
2324
+ const conditionsFile = pickString(flags, '--conditions-file');
2325
+ if (op === 'list') {
2326
+ const r = await listRecords(appId, formId, {
2327
+ env,
2328
+ pageNumber: Number(pickString(flags, '--page-number') ?? 1),
2329
+ pageSize: Number(pickString(flags, '--page-size') ?? 20),
2330
+ ...(conditionsFile ? { conditions: JSON.parse(readFileSync(path.resolve(conditionsFile), 'utf8')) } : {}),
2331
+ }, cfg);
2332
+ if (!r.ok || !r.data)
2333
+ failApi(r, `查询记录失败`);
2334
+ output(r.data, flags);
2335
+ return;
2336
+ }
2337
+ if (op === 'get') {
2338
+ const code = command[3];
2339
+ if (!code)
2340
+ fail('用法: qmuse db records get <code> ...');
2341
+ const r = await getRecord(appId, formId, code, cfg);
2342
+ if (!r.ok || !r.data)
2343
+ failApi(r, `读取记录失败`);
2344
+ output(r.data, flags);
2345
+ return;
2346
+ }
2347
+ if (op === 'create' || op === 'update') {
2348
+ const file = pickString(flags, '--file');
2349
+ if (!file)
2350
+ fail(`用法: qmuse db records ${op} --file <parameters.json>`);
2351
+ const parameters = JSON.parse(readFileSync(path.resolve(file), 'utf8'));
2352
+ const r = op === 'create'
2353
+ ? await createRecord(appId, formId, parameters, cfg)
2354
+ : await updateRecord(appId, formId, parameters, cfg);
2355
+ if (!r.ok) {
2356
+ const reason = r.errorMessage ?? r.errorCode ?? `HTTP ${r.status}`;
2357
+ // 服务器对缺必填/类型问题只回“系统异常”:失败后按表元数据补字段级诊断(成功路径零开销)。
2358
+ let hints = [];
2359
+ try {
2360
+ hints = await diagnoseRecordParams(appId, formId, parameters, cfg);
2361
+ }
2362
+ catch { /* 诊断仅尽力而为 */ }
2363
+ const hint = hints.length > 0 ? `;字段诊断: ${hints.join(';')}` : '';
2364
+ fail(`${op === 'create' ? '创建' : '更新'}失败: ${reason}${hint}`);
2365
+ }
2366
+ output(r.data, flags);
2367
+ return;
2368
+ }
2369
+ if (op === 'delete') {
2370
+ const code = command[3];
2371
+ if (!code || flags['--yes'] !== true)
2372
+ fail('用法: qmuse db records delete <code> ... --yes');
2373
+ const r = await deleteRecord(appId, formId, code, cfg);
2374
+ if (!r.ok)
2375
+ failApi(r, `删除失败`);
2376
+ output({ success: true, code, deleted: r.data === true }, flags);
2377
+ return;
2378
+ }
2379
+ fail('用法: qmuse db records list | get <code> | create --file | update --file | delete <code> --yes');
2380
+ return;
2381
+ }
2382
+ fail(`未知 db 子命令: ${sub ?? '(空)'}。可用: tables / records / permission`);
2383
+ }
2384
+ // ---------------- 账户域(C29) ----------------
2385
+ async function cmdAccount(command, flags) {
2386
+ const cfg = resolveCfg(flags);
2387
+ requireAuth(cfg);
2388
+ const sub = command[1];
2389
+ const spaceId = pickString(flags, '--space-id') ?? cfg.spaceId;
2390
+ if (!spaceId)
2391
+ fail('用法: qmuse account <子命令> --space-id <spaceId>(或先用 space use)');
2392
+ if (sub === undefined || sub === 'overview') {
2393
+ // C29.01:同一输出汇总套餐/订阅、余额、权益、额度与可取得的消耗摘要。
2394
+ const [bal, ent, stats] = await Promise.all([
2395
+ pointBalance(spaceId, cfg),
2396
+ benefitEntitlements(spaceId, cfg),
2397
+ pointConsumeStats(spaceId, { pageIndex: '1', pageSize: '8' }, cfg),
2398
+ ]);
2399
+ if (!bal.ok || !bal.data)
2400
+ fail(`查询余额失败: ${bal.errorMessage ?? bal.errorCode ?? `HTTP ${bal.status}`}`);
2401
+ const appId = pickString(flags, '--app-id') ?? loadAppBinding(process.cwd())?.appId;
2402
+ const [subscription, dbUsage] = await Promise.all([
2403
+ sectionOf(async () => {
2404
+ // 订阅状态为只读面(GET /api/order/subscription/current);无契约时显式标注。
2405
+ const r = await apiCall('GET', '/api/order/subscription/current', undefined, cfg);
2406
+ if (!r.ok)
2407
+ return { unavailable: r.errorMessage ?? r.errorCode ?? `HTTP ${r.status}` };
2408
+ return r.data ?? null;
2409
+ }),
2410
+ appId ? sectionOf(async () => {
2411
+ const r = await databaseOverview(appId, cfg);
2412
+ if (!r.ok)
2413
+ return { unavailable: r.errorMessage ?? r.errorCode ?? `HTTP ${r.status}` };
2414
+ return r.data ?? null;
2415
+ }) : { unavailable: '未绑定应用:传 --app-id 或执行 qmuse app use' },
2416
+ ]);
2417
+ output({
2418
+ schemaVersion: 1,
2419
+ spaceId,
2420
+ balance: bal.data,
2421
+ entitlements: ent.ok ? ent.data : { unavailable: ent.errorMessage ?? ent.errorCode ?? ent.status },
2422
+ consumeStats: stats.ok ? stats.data : { unavailable: stats.errorMessage ?? stats.errorCode ?? stats.status },
2423
+ subscription,
2424
+ ...(appId ? { appId } : {}),
2425
+ dbUsage,
2426
+ notes: [
2427
+ '套餐/订阅购买与计划管理在 PC 端承载;CLI 仅只读订阅状态。',
2428
+ ...(appId ? [] : ['数据库/存储用量需绑定应用后查询(cloud status / db 亦可参考)。']),
2429
+ ],
2430
+ }, flags);
2431
+ return;
2432
+ }
2433
+ if (sub === 'benefit') {
2434
+ const r = await benefitEntitlements(spaceId, cfg);
2435
+ if (!r.ok || !r.data)
2436
+ failApi(r, `查询权益失败`);
2437
+ output(r.data, flags);
2438
+ return;
2439
+ }
2440
+ if (sub === 'points') {
2441
+ const op = command[2] ?? 'balance';
2442
+ if (op === 'balance') {
2443
+ const r = await pointBalance(spaceId, cfg);
2444
+ if (!r.ok || !r.data)
2445
+ failApi(r, `查询余额失败`);
2446
+ output(r.data, flags);
2447
+ return;
2448
+ }
2449
+ if (op === 'records') {
2450
+ const r = await pointRecords(spaceId, {
2451
+ pageIndex: pickString(flags, '--page-index') ?? '1',
2452
+ pageSize: pickString(flags, '--page-size') ?? '20',
2453
+ ...(pickString(flags, '--flow-type') ? { flowType: pickString(flags, '--flow-type') } : {}),
2454
+ }, cfg);
2455
+ if (!r.ok || !r.data)
2456
+ failApi(r, `查询消费记录失败`);
2457
+ output(r.data, flags);
2458
+ return;
2459
+ }
2460
+ if (op === 'stats') {
2461
+ const bizCodes = pickString(flags, '--biz-codes');
2462
+ const r = await pointConsumeStats(spaceId, {
2463
+ pageIndex: pickString(flags, '--page-index') ?? '1',
2464
+ pageSize: pickString(flags, '--page-size') ?? '20',
2465
+ ...(bizCodes ? { bizCodeList: bizCodes.split(',').map(v => v.trim()).filter(Boolean) } : {}),
2466
+ ...(pickString(flags, '--after') ? { gmtCreate: pickString(flags, '--after') } : {}),
2467
+ }, cfg);
2468
+ if (!r.ok || !r.data)
2469
+ failApi(r, `查询消耗统计失败`);
2470
+ output(r.data, flags);
2471
+ return;
2472
+ }
2473
+ fail('用法: qmuse account points balance|records|stats');
2474
+ return;
2475
+ }
2476
+ if (sub === 'plan' || sub === 'subscription') {
2477
+ fail('套餐与订阅由 PC 端承载(暂无 CLI 可用服务端契约),清单该项标注平台依赖');
2478
+ }
2479
+ fail(`未知 account 子命令: ${sub ?? '(空)'}。可用: overview / benefit / points balance|records|stats`);
2480
+ }
2481
+ async function cmdInfo(command, flags) {
2482
+ // C04.01 + X01.16:同一入口输出 CLI 版本、目录绑定、实际环境、运行约束、云架构与可用操作。
2483
+ if (command.length > 1)
2484
+ fail('用法: qmuse info [--app-id <appId>] [--json]');
2485
+ const cfg = resolveCfg(flags);
2486
+ const apiBaseUrl = `${cfg.baseUrl}${cfg.apiPrefix || ''}`;
2487
+ const binding = loadAppBinding(process.cwd());
2488
+ const managed = await (async () => {
2489
+ try {
2490
+ return await loadManagedProject(process.cwd(), cfg, str(flags, '--env'));
2491
+ }
2492
+ catch {
2493
+ return undefined;
2494
+ }
2495
+ })();
2496
+ const appId = pickString(flags, '--app-id') ?? binding?.appId ?? managed?.target?.appId;
2497
+ const section = async (label, run) => {
2498
+ try {
2499
+ return await run();
2500
+ }
2501
+ catch (err) {
2502
+ return { unavailable: err instanceof Error ? err.message : String(err) };
2503
+ }
2504
+ };
2505
+ const session = !hasAuth(cfg)
2506
+ ? { loggedIn: false }
2507
+ : await section('session', async () => {
2508
+ const r = await api.whoami({ ...cfg, spaceId: undefined });
2509
+ if (!r.ok || !r.data?.userId)
2510
+ return { loggedIn: false, reason: r.status === 401 ? 'session-expired-or-absent' : r.errorMessage ?? r.errorCode };
2511
+ return { loggedIn: true, userId: r.data.userId, userName: r.data.userName, spaceId: r.data.spaceId ?? cfg.spaceId, spaceName: r.data.spaceName ?? null };
2512
+ });
2513
+ const appInfo = appId && hasAuth(cfg)
2514
+ ? await section('app', async () => {
2515
+ const r = await queryAppInfo(appId, cfg);
2516
+ if (!r.ok || !r.data)
2517
+ return { appId, unavailable: r.errorMessage ?? r.errorCode ?? `HTTP ${r.status}` };
2518
+ return { appId, title: r.data.title, appType: r.data.appType, status: r.data.status };
2519
+ })
2520
+ : undefined;
2521
+ const cloud = appId && hasAuth(cfg)
2522
+ ? await section('cloud', async () => {
2523
+ const [ent, env] = await Promise.all([
2524
+ checkCloudEntitlement({ appId: appId, spaceId: cfg.spaceId ?? '' }, cfg).catch(err => ({ unavailable: err instanceof Error ? err.message : String(err) })),
2525
+ cloudEnvironmentStatus(appId, cfg).then(r => r.ok ? r.data : { unavailable: r.errorMessage ?? r.errorCode ?? `HTTP ${r.status}` }),
2526
+ ]);
2527
+ return { entitlement: ent, environment: env };
2528
+ })
2529
+ : undefined;
2530
+ const envFlag = str(flags, '--env');
2531
+ const platformEnv = envFlag === 'prod' || envFlag === 'pre' || envFlag === 'local' ? envFlag : (cfg.baseUrl.includes('-pre.') ? 'pre' : 'prod');
2532
+ const info = {
2533
+ schemaVersion: 1,
2534
+ cli: { name: QMUSE_CLI_NAME, version: QMUSE_CLI_VERSION, distribution: QMUSE_CLI_DISTRIBUTION },
2535
+ environment: {
2536
+ platformEnv,
2537
+ apiBaseUrl,
2538
+ webBaseUrl: cfg.webBaseUrl,
2539
+ },
2540
+ session,
2541
+ bindings: {
2542
+ app: binding ? { appId: binding.appId, title: binding.title ?? null, boundAt: binding.boundAt ?? null } : null,
2543
+ project: managed
2544
+ ? {
2545
+ appId: managed.target?.appId ?? null,
2546
+ kind: managed.manifest?.projectKind ?? null,
2547
+ cloudArchitecture: managed.manifest?.cloudArchitecture ?? null,
2548
+ templateCommit: managed.manifest?.templateCommit ?? null,
2549
+ baselineVersionId: managed.target?.lastVersionId ?? null,
2550
+ baselineSourceHash: managed.target?.lastSourceHash ?? null,
2551
+ }
2552
+ : null,
2553
+ },
2554
+ ...(appId && appInfo ? { app: appInfo } : {}),
2555
+ ...(cloud ? { cloud } : {}),
2556
+ template: {
2557
+ source: 'QMuse GENERAL 项目模板',
2558
+ fetch: 'create/import 时通过 /api/project-template/download 下发(appType=GENERAL)',
2559
+ constraints: [
2560
+ 'React + Vite 单页工程;导入前执行 qmuse import <dir> 预检',
2561
+ '远端应用创建与首轮生成走 app create/qmuse chat;本地模板创建走 qmuse create <dir>',
2562
+ '源码上传走 import;下载最新已保存源码走 pull / code export',
2563
+ ],
2564
+ },
2565
+ capabilities: [
2566
+ { domain: '身份与空间', entry: 'login/whoami/logout、space list|use|info', requires: '会话' },
2567
+ { domain: '应用创建与管理', entry: 'app create/list/info/use/current/unlink/update/delete', requires: '会话+空间' },
2568
+ { domain: '初始化与导入', entry: 'create <dir>、import <dir>|status|configure、pull', requires: '会话;导入需要云权益预检' },
2569
+ { domain: '对话与恢复', entry: 'chat send|history|state|cancel|clear|resume、chat attachment upload、models', requires: '会话+应用绑定或显式 appId' },
2570
+ { domain: '本地开发', entry: 'dev [dir]|status|stop', requires: '会话+云资源(云绑定应用)' },
2571
+ { domain: '版本与源码', entry: 'version list|info、code export', requires: '会话+appId' },
2572
+ { domain: '发布与结果', entry: 'deploy --version、release status|list', requires: '会话+appId;发布走平台安全门禁' },
2573
+ { domain: '数据调试', entry: 'data-source list|info、db tables|records|permission', requires: '会话+appId(记录写操作依赖平台数据权限)' },
2574
+ { domain: '云服务与环境', entry: 'cloud init|enable|status|environment retry|pull-env|set-secret|schema', requires: '会话+权益' },
2575
+ { domain: '函数、秘密、沙箱', entry: 'fn …、secret list|create|delete|sync …、sandbox status|logs|health|restart|setting-get', requires: '会话+appId;沙箱写操作依赖平台沙箱通道' },
2576
+ { domain: '官方 Skill', entry: 'agent-skill install|status(指南随 CLI 包内分发)', requires: '本地安装目标' },
2577
+ ],
2578
+ unsupported: [
2579
+ { capability: '套餐与订阅(购买型)', reason: 'PC 端承载;CLI 仅读订阅状态(account overview 汇总,缺少契约时标注 unsupported)' },
2580
+ ...(str(flags, '--env') === 'local' ? [{ capability: '预发专用链路(沙箱/发布门禁)', reason: '本地环境不包含预发后端部署的部分平台组件' }] : []),
2581
+ ],
2582
+ };
2583
+ if (flags['--json'] === true) {
2584
+ output(info, flags);
2585
+ return;
2586
+ }
2587
+ log.info(`QMuse CLI ${QMUSE_CLI_VERSION}(${QMUSE_CLI_DISTRIBUTION})`);
2588
+ log.info(`API:${apiBaseUrl} Web:${cfg.webBaseUrl}`);
2589
+ log.info(`会话:${JSON.stringify(session)}`);
2590
+ log.info(`绑定:${JSON.stringify(info.bindings)}`);
2591
+ if (cloud)
2592
+ log.info(`云服务:${JSON.stringify(cloud)}`);
2593
+ log.info('能力入口见 qmuse help;约束见 qmuse info --json 的 template.constraints 与使用规则。');
2594
+ }
2595
+ /**
2596
+ * 版本闸门执行:blocked 拒绝执行(可绕过项除外),warning 告警,notice 静默(由退出提示负责)。
2597
+ * 策略读不到时一律放行——闸门不可用绝不锁死用户。
2598
+ */
2599
+ async function enforceVersionGate(root, flags) {
2600
+ if (root === undefined)
2601
+ return;
2602
+ const gateEnabled = process.env.QMUSE_VERSION_GATE !== 'off';
2603
+ if (!gateEnabled)
2604
+ return;
2605
+ // 策略来源:显式环境变量优先(自测/内网固定策略),否则读 registry 包元数据。
2606
+ const provider = process.env.QMUSE_VERSION_POLICY
2607
+ ? envPolicyProvider()
2608
+ : registryPolicyProvider({ distribution: QMUSE_CLI_DISTRIBUTION, timeoutMs: 1500 });
2609
+ const check = await checkVersionGate({
2610
+ command: root,
2611
+ distribution: QMUSE_CLI_DISTRIBUTION,
2612
+ provider,
2613
+ });
2614
+ const { decision } = check;
2615
+ if (decision.level === 'blocked' && check.blocked) {
2616
+ // 只抛结构化错误,由统一出口渲染一次(文本/JSON 皆然),避免重复输出。
2617
+ throw new CliError(buildCliError({
2618
+ errorCode: 'VERSION_BLOCKED',
2619
+ errorMessage: decision.message ?? '当前 CLI 版本已停止支持,请升级后继续使用',
2620
+ hint: `升级命令:${decision.upgradeCommand}`,
2621
+ details: {
2622
+ current: decision.current,
2623
+ ...(decision.latest ? { latest: decision.latest } : {}),
2624
+ ...(decision.minRequired ? { minRequired: decision.minRequired } : {}),
2625
+ },
2626
+ }));
2627
+ }
2628
+ if (decision.level === 'warning' && flags['--json'] !== true) {
2629
+ log.warn(`${decision.message ?? 'CLI 版本过旧,建议尽快升级'}(当前 ${decision.current}${decision.latest ? `,最新 ${decision.latest}` : ''})`);
2630
+ log.info(`升级命令:${decision.upgradeCommand}`);
2631
+ }
2632
+ }
2633
+ /** qmuse upgrade:自查最新版本并执行升级(闸门对自身放行)。 */
2634
+ async function cmdUpgrade(command, flags) {
2635
+ if (command.length > 1)
2636
+ fail('用法:qmuse upgrade [--json]', { code: 'INVALID_ARGUMENTS' });
2637
+ log.info('检查最新版本…');
2638
+ const provider = registryPolicyProvider({ distribution: QMUSE_CLI_DISTRIBUTION, timeoutMs: 5000 });
2639
+ const snapshot = await provider();
2640
+ if (!snapshot?.latest) {
2641
+ fail('无法获取最新版本信息', {
2642
+ code: 'CHECK_FAILED',
2643
+ hint: '请检查网络连接;也可手动执行 ' + defaultUpgradeCommand(QMUSE_CLI_DISTRIBUTION),
2644
+ });
2645
+ }
2646
+ const latest = snapshot.latest;
2647
+ const current = QMUSE_CLI_VERSION;
2648
+ const upToDate = !shouldUpgrade(current, latest);
2649
+ if (upToDate) {
2650
+ if (flags['--json'] === true) {
2651
+ output({ success: true, current, latest, upToDate: true, skillSync: syncInstalledSkills() }, flags);
2652
+ return;
2653
+ }
2654
+ log.success(`当前已是最新版本(${current})`);
2655
+ // 即使 CLI 已最新,仍顺带把已安装的官方指南对齐到当前包(不新建目标、不覆盖用户修改)。
2656
+ reportSkillSync(syncInstalledSkills());
2657
+ return;
2658
+ }
2659
+ if (flags['--json'] !== true) {
2660
+ log.info(`当前版本:${current}`);
2661
+ log.info(`最新版本:${latest}`);
2662
+ log.info('正在升级…');
2663
+ }
2664
+ const distribution = QMUSE_CLI_DISTRIBUTION;
2665
+ const args = distribution === 'PUBLIC'
2666
+ ? ['install', '-g', `@qmuse/qmuse-cli@${latest}`, '--registry=https://registry.npmjs.org/']
2667
+ : ['install', '-g', `@alipay/qmuse-cli@${latest}`];
2668
+ const result = await new Promise((resolve) => {
2669
+ const child = spawn('npm', args, { stdio: flags['--json'] === true ? 'pipe' : 'inherit', shell: process.platform === 'win32' });
2670
+ let stderr = '';
2671
+ child.stderr?.on('data', (chunk) => { stderr += chunk.toString('utf8'); });
2672
+ child.on('close', (code) => resolve(code === 0 ? { ok: true } : { ok: false, detail: stderr.slice(-500) }));
2673
+ child.on('error', (error) => resolve({ ok: false, detail: error.message }));
2674
+ });
2675
+ if (!result.ok) {
2676
+ fail('升级失败', {
2677
+ code: 'UPGRADE_FAILED',
2678
+ hint: `尝试手动执行:${defaultUpgradeCommand(distribution)}`,
2679
+ ...(result.detail ? { details: { stderr: sanitizeMessage(result.detail) } } : {}),
2680
+ });
2681
+ }
2682
+ // 升级成功后顺带同步「已存在」的官方指南安装(不新建目标、不覆盖用户修改、失败不影响升级结论)。
2683
+ const skillSync = syncInstalledSkills();
2684
+ if (flags['--json'] === true) {
2685
+ output({ success: true, from: current, to: latest, skillSync }, flags);
2686
+ return;
2687
+ }
2688
+ log.success(`已升级到 ${latest}`);
2689
+ reportSkillSync(skillSync);
2690
+ }
2691
+ /** 输出指南同步结果(升级前后共用)。 */
2692
+ function reportSkillSync(items) {
2693
+ if (items.length === 0) {
2694
+ log.info('未检测到已安装的官方指南;需要时执行 qmuse agent-skill install');
2695
+ return;
2696
+ }
2697
+ for (const item of items) {
2698
+ const label = item.agent ? `${item.agent}` : item.address;
2699
+ if (item.outcome === 'refreshed')
2700
+ log.success(`官方指南已同步(${label})`);
2701
+ else if (item.outcome === 'up-to-date')
2702
+ log.info(`官方指南已是当前版本(${label})`);
2703
+ else if (item.outcome === 'skipped-user-modified')
2704
+ log.warn(`保留你对官方指南的修改(${label}):${item.detail ?? ''}`);
2705
+ else
2706
+ log.warn(`官方指南同步失败(${label}):${item.detail ?? ''}`);
2707
+ }
2708
+ }
2709
+ async function cmdLogin(flags) {
2710
+ const cfg = resolveCfg(flags);
2711
+ if (flags['--status'] === true) {
2712
+ const out = await login.loginStatus(cfg);
2713
+ // T01.03:会话查询结果来源(匿名请求 traceId 由 cli.http.request 记录)。
2714
+ recordEvent({ kind: 'cli.auth.event', stage: 'status', result: out.success ? 'ok' : 'failed' });
2715
+ if (out.success)
2716
+ log.success(out.message ?? '会话有效');
2717
+ else
2718
+ log.error(out.message ?? '会话无效');
2719
+ return;
2720
+ }
2721
+ log.info(`登录方式:browser 目标:${cfg.baseUrl}${cfg.apiPrefix || ''}`);
2722
+ const out = await login.loginBrowser(cfg, {
2723
+ logRequestDetails: flags['--log'] === true,
2724
+ });
2725
+ // T01.03:登录结果事件(匿名请求 traceId 已随 cli.http.request 保留;凭证仅入系统凭证库)。
2726
+ recordEvent({ kind: 'cli.auth.event', stage: 'browser-login', result: out.success ? 'ok' : 'failed',
2727
+ ...(out.method ? { status: out.method } : {}) });
2728
+ if (out.success) {
2729
+ login.closePrompt();
2730
+ log.success(`登录成功(${out.method})`);
2731
+ if (flags['--json'] === true)
2732
+ output({ success: true, method: out.method }, flags);
2733
+ }
2734
+ else {
2735
+ login.closePrompt();
2736
+ fail(out.message ?? '登录失败');
2737
+ }
2738
+ }
2739
+ async function cmdLogout(flags) {
2740
+ const cfg = resolveCfg(flags);
2741
+ const hadSession = !!(cfg.sessionId || '').trim();
2742
+ deleteSession(cfg);
2743
+ if (hadSession) {
2744
+ log.success('已清除当前环境的登录态和空间选择');
2745
+ }
2746
+ else {
2747
+ log.info('当前未配置登录态;已清除空间选择');
2748
+ }
2749
+ }
2750
+ // ---------------- 帮助 & 入口 ----------------
2751
+ async function cmdCreate(command, flags) {
2752
+ if (command.length !== 2)
2753
+ fail('用法:qmuse create <directory>');
2754
+ const result = await createProject(command[1], resolveCfg(flags));
2755
+ if (flags['--json'] === true) {
2756
+ output(result, flags);
2757
+ return;
2758
+ }
2759
+ log.success(`模板已下载到 ${result.directory}`);
2760
+ log.info('进入项目目录后执行:npm install');
2761
+ log.info(`开发完成后执行:qmuse${str(flags, '--env') ? ` --env ${str(flags, '--env')}` : ''} import .`);
2762
+ }
2763
+ const HELP = `QMuse CLI —— 将本地 React 项目导入 QMuse
2764
+
2765
+ 用法:
2766
+ qmuse [global flags] <command> [subcommand] [flags]
2767
+
2768
+ 全局 flags:
2769
+ --env <env> 环境预设:prod / pre / local
2770
+ --json 结果以 JSON 输出
2771
+ --log 打印登录请求接口和页面地址(不含登录凭证)
2772
+
2773
+ 全局 flags 可放在命令前或命令后
2774
+
2775
+ 命令:
2776
+ whoami 查看当前环境、登录和空间状态
2777
+ login [--status] [--log] [--json] 通过 QMuse 网页登录
2778
+ logout 清除保存的登录态和空间选择
2779
+ config [get|path] 查看配置
2780
+
2781
+ create <directory> 下载 QMuse 模板并解压到新项目目录
2782
+ pull [directory] 免费同步已导入应用的最新已保存源码(本地有改动时停止)
2783
+
2784
+ chat history --app-id <id> --sprint-id <id> 按应用和迭代查询会话与历史
2785
+ chat state <chatId> 查询会话执行状态与阶段
2786
+ chat cancel <chatId> --app-id <id> 发送停止指令(以服务端状态为准)
2787
+ chat clear <chatId> --app-id --sprint-id 清理会话上下文
2788
+ chat send --app-id --chat-id --file <路径|-> 已有应用继续对话(统一 continue 接口)
2789
+ [--model <model>] [--skill <skillId> --skill-version <versionId>]
2790
+ chat resume <chatId> --interrupt-id <id> 恢复等待输入/云资源中断(--file 回答 JSON)
2791
+ --file <路径|-> --app-id <id> [--ndjson]
2792
+ chat attachment upload <path> --app-id <id> 上传应用版本附件
2793
+ models 查询可选模型列表
2794
+ version list|info --app-id <id> 查询应用版本列表/详情
2795
+ code export --app-id <id> --output <dir> 导出应用最新已保存源码
2796
+ deploy --version <versionId> [--no-wait] 固定版本发布(受理后退出用 --no-wait)
2797
+ [--app-id <id>]
2798
+ release status <releaseId> [--wait] 查询发布任务详情/检查/地址/失败原因
2799
+ release list --app-id <id> 查询应用发布记录(--last 查最近一次)
2800
+ info [--app-id <id>] CLI 版本/环境/绑定/模板约束/云架构/能力
2801
+ agent-skill install --agent codex 安装包内官方研发指南(--target 覆盖;--force 覆盖修改)
2802
+ agent-skill status [--agent codex|--target <dir>] 查询指南安装状态与用户修改
2803
+ upgrade 自查最新版本并升级 CLI(--json 结构化输出)
2804
+ account overview|benefit|points … 空间余额/权益/喵币消耗/订阅状态(--space-id)
2805
+ data-source list|info 应用数据源与表元数据(--app-id)
2806
+ db tables|record|list|get|create|update|delete|permission 应用数据表与记录(--app-id --form-id)
2807
+ fn list|info|retry|deployment-*|execution-* 云函数查询与发布重试(--app-id)
2808
+ secret list|create|enable|disable|delete 云秘密管理(--app-id;删除需先停用且 --yes)
2809
+ [--cloud-env dev|prod]
2810
+ secret sync status|retry 查询/重试秘密同步状态(--app-id)
2811
+ sandbox status --app-id <id> 汇总沙箱配置/预览/健康及失败原因
2812
+ [--sandbox-id <id>] [--json]
2813
+ sandbox logs --app-id <id> 现有接口可提供的构建/任务诊断日志
2814
+ [--sandbox-id <id>] [--version-id <id>] [--build-id <id>]
2815
+ sandbox health|restart|setting-get|ensure 沙箱其余只读与显式重启入口
2816
+ app create <web|h5> --file <路径|-> 统一创建应用并开始首轮生成(预发接口)
2817
+ [--title <title>] [--space-id <id>] [--request-id <cc_时间戳_随机hex>]
2818
+ [--model <model>] [--no-open] [--json|--ndjson]
2819
+ app list [--space-id <id>] [--keyword <kw>] 查询空间应用列表
2820
+ app info <appId> 查询应用详情
2821
+ app use / current / unlink 绑定、查看、解除本地应用绑定
2822
+ app update <appId> [--title <t>] [--description <d>] [--require-login 0|1] [ --default-device <d>]
2823
+ 修改应用信息
2824
+ app delete <appId> --yes 删除应用(不可恢复)
2825
+
2826
+ cloud init -d "<应用功能描述>" 校验当前空间的 QMuse 云服务权益
2827
+ cloud enable [--app-id <id>] 实际开通云服务(复用既有权益判断)
2828
+ cloud status [--app-id <id>] 查询架构/云环境/初始化状态与失败原因
2829
+ cloud environment retry --app-id <id> [--wait] 重试云环境初始化(复用原任务)
2830
+ cloud pull-env --app-id <id> --output <path> 导出实际环境公开连接与运行配置
2831
+ cloud set-secret KEY1,KEY2 --stdin 从标准输入 JSON 保存密钥,不输出密钥值
2832
+ [--app-id <id>] [--cloud-env dev|prod]
2833
+ cloud schema preflight --candidate <schema.json> 只读校验候选与云端 READY 基线
2834
+ cloud schema export [--app-id <id>] 输出当前开发环境的 READY schema
2835
+ dev [directory] [--port <port>] 连接已导入应用的开发云资源并启动本地 Vite
2836
+
2837
+ assets upload <file> [more-files...] --json 上传媒体到 CDN,返回引用替换清单
2838
+ import <directory> [--space-id <id>] 导入本地 React 仓库
2839
+ [--title <title>] [--force] [--cloud-mode AUTO|EXTERNAL|QMUSE]
2840
+ [--public-config '<json>'|@file] [--acknowledge-empty-cloud-data]
2841
+ [--confirm-cloud-service]
2842
+ [--no-open] [--json] [--log]
2843
+ import status <jobId> [--wait] [--no-open] 查询或恢复等待导入任务
2844
+ import configure <jobId> --cloud-mode <mode> 补充云服务配置后继续任务
2845
+
2846
+ space list 我加入的空间列表
2847
+ space use <spaceId> 设为当前空间(后续带 x-space-id 头)
2848
+ space info 当前空间信息
2849
+
2850
+ 典型流程:
2851
+ qmuse login
2852
+ qmuse space list
2853
+ qmuse space use S640459450135
2854
+ qmuse create my-app
2855
+ qmuse import /path/to/react-vite-app
2856
+ `;
2857
+ async function main() {
2858
+ const argv = process.argv.slice(2);
2859
+ if (argv.length === 0) {
2860
+ recordCommandEvent('help', 0, Date.now());
2861
+ process.stdout.write(HELP);
2862
+ return;
2863
+ }
2864
+ const { command, flags } = parseArgs(argv);
2865
+ if (flags['--help'] === true || flags['-h'] === true) {
2866
+ recordCommandEvent(currentCommandLabel(command), 0, Date.now());
2867
+ process.stdout.write(HELP);
2868
+ return;
2869
+ }
2870
+ if (flags['--version'] === true || flags['-V'] === true) {
2871
+ recordCommandEvent('version', 0, Date.now());
2872
+ process.stdout.write(`qmuse ${QMUSE_CLI_VERSION}\n`);
2873
+ return;
2874
+ }
2875
+ const root = command[0];
2876
+ const commandLabel = currentCommandLabel(command);
2877
+ setTelemetryCommand(commandLabel, str(flags, '--env'));
2878
+ const commandStartedAt = Date.now();
2879
+ recordEvent({ kind: 'cli.command.start', command: commandLabel,
2880
+ environment: str(flags, '--env') });
2881
+ // MAU:模拟 Tracert dwcookie 协议上报到蚂蚁采集平台
2882
+ trackCommand(commandLabel.replace(/ /gu, '-'), resolveCfg(flags));
2883
+ const finishUpdateNotice = startUpdateNotice({
2884
+ enabled: root === 'import' && process.stderr.isTTY === true && flags['--json'] !== true,
2885
+ notify: message => process.stderr.write(`${message}\n`),
2886
+ });
2887
+ process.once('exit', finishUpdateNotice);
2888
+ try {
2889
+ // 版本闸门(失败降级放行):blocked 拒绝执行,warning 告警,notice 仅退出提示。
2890
+ await enforceVersionGate(root, flags);
2891
+ switch (root) {
2892
+ case 'whoami':
2893
+ await cmdWhoami(flags);
2894
+ return;
2895
+ case 'login':
2896
+ await cmdLogin(flags);
2897
+ return;
2898
+ case 'logout':
2899
+ await cmdLogout(flags);
2900
+ return;
2901
+ case 'config':
2902
+ await cmdConfig(command, flags);
2903
+ return;
2904
+ case 'create':
2905
+ await cmdCreate(command, flags);
2906
+ return;
2907
+ case 'account':
2908
+ await cmdAccount(command, flags);
2909
+ return;
2910
+ case 'data-source':
2911
+ await cmdDataSources(command, flags);
2912
+ return;
2913
+ case 'db':
2914
+ await cmdDb(command, flags);
2915
+ return;
2916
+ case 'app':
2917
+ await cmdApp(command, flags);
2918
+ return;
2919
+ case 'chat':
2920
+ await cmdChat(command, flags);
2921
+ return;
2922
+ case 'models':
2923
+ await cmdModels(command, flags);
2924
+ return;
2925
+ case 'version':
2926
+ await cmdVersions(command, flags);
2927
+ return;
2928
+ case 'code':
2929
+ await cmdCodeExport(command, flags);
2930
+ return;
2931
+ case 'fn':
2932
+ await cmdFn(command, flags);
2933
+ return;
2934
+ case 'secret':
2935
+ await cmdSecret(command, flags);
2936
+ return;
2937
+ case 'sandbox':
2938
+ await cmdSandbox(command, flags);
2939
+ return;
2940
+ case 'pull': {
2941
+ if (command.length > 2 || Object.keys(flags).some(key => !['--env', '--json'].includes(key))) {
2942
+ throw new Error('用法:qmuse pull [directory] [--env prod|pre|local] [--json]');
2943
+ }
2944
+ const result = await withOperationLog({ directory: command[1] || '.', command: 'pull',
978
2945
  report: summary => reportCliOperation(summary, resolveCfg(flags)) }, () => pullProject(command[1] || '.', resolveCfg(flags), str(flags, '--env')));
979
2946
  if (flags['--json'] === true)
980
2947
  output(result, flags);
@@ -985,11 +2952,30 @@ async function main() {
985
2952
  if (!result.noChanges)
986
2953
  log.info('修改完成后执行 qmuse import,更新同一应用。');
987
2954
  }
2955
+ // T01.05:下载来源与同步基线(noChanges 显式区分,不静默覆盖本地修改)。
2956
+ recordEvent({ kind: 'cli.transfer.result', route: '/api/app-import/pull',
2957
+ result: result.noChanges ? 'no-changes' : 'ok',
2958
+ ...(result.versionId ? { status: 'synced' } : {}) });
988
2959
  return;
989
2960
  }
990
2961
  case 'cloud':
991
2962
  await cmdCloud(command, flags);
992
2963
  return;
2964
+ case 'deploy':
2965
+ await cmdDeploy(command, flags);
2966
+ return;
2967
+ case 'release':
2968
+ await cmdRelease(command, flags);
2969
+ return;
2970
+ case 'info':
2971
+ await cmdInfo(command, flags);
2972
+ return;
2973
+ case 'agent-skill':
2974
+ await cmdAgentSkill(command, flags);
2975
+ return;
2976
+ case 'upgrade':
2977
+ await cmdUpgrade(command, flags);
2978
+ return;
993
2979
  case 'dev':
994
2980
  await cmdDev(command, flags);
995
2981
  return;
@@ -1017,17 +3003,38 @@ async function main() {
1017
3003
  process.stdout.write(HELP);
1018
3004
  return;
1019
3005
  default:
1020
- fail(`未知命令: ${root ?? '(空)'}。\n\n${HELP}`);
3006
+ fail(`未知命令: ${root ?? '(空)'}`, {
3007
+ code: 'UNKNOWN_COMMAND',
3008
+ hint: '运行 qmuse help 查看全部命令与参数',
3009
+ details: { help: HELP },
3010
+ });
1021
3011
  }
1022
3012
  }
1023
3013
  catch (err) {
1024
3014
  finishUpdateNotice();
1025
- const msg = err instanceof Error ? err.message : String(err);
3015
+ const info = err instanceof CliError
3016
+ ? err.info
3017
+ : buildCliError({ errorMessage: sanitizeMessage(err instanceof Error ? err.message : String(err)) });
1026
3018
  if (flags['--json'] === true) {
1027
- output({ success: false, message: msg }, flags);
3019
+ output({
3020
+ success: false,
3021
+ error: {
3022
+ code: info.code,
3023
+ message: info.message,
3024
+ ...(info.hint ? { hint: info.hint } : {}),
3025
+ ...(info.traceId ? { traceId: info.traceId } : {}),
3026
+ ...(info.details ? { details: info.details } : {}),
3027
+ },
3028
+ // 兼容:老消费者读 message
3029
+ message: info.message,
3030
+ }, flags);
1028
3031
  }
1029
3032
  else {
1030
- log.error(msg);
3033
+ log.error(info.message);
3034
+ if (info.hint)
3035
+ log.info(info.hint);
3036
+ if (info.traceId)
3037
+ log.info(`traceId: ${info.traceId}`);
1031
3038
  }
1032
3039
  process.exitCode = 1;
1033
3040
  }
@@ -1035,6 +3042,70 @@ async function main() {
1035
3042
  closePrompt();
1036
3043
  finishUpdateNotice();
1037
3044
  process.off('exit', finishUpdateNotice);
3045
+ // T01.01:命令结束事件(含帮助/版本/失败与 exitCode)。
3046
+ recordEvent({ kind: 'cli.command.end', command: commandLabel, durationMs: Date.now() - commandStartedAt,
3047
+ result: process.exitCode ? 'error' : 'ok', environment: str(flags, '--env') });
3048
+ // T01.15:网络补报已由服务端 AOP 中间件替代——CLI 每个业务请求都在
3049
+ // 请求头携带 x-qmuse-client/-version/-distribution/-invocation-id/-command,
3050
+ // 服务端 cliSourceMetrics 中间件自动观测并落库(调用侧不感知,无独立端点)。
3051
+ // 本地 JSONL 队列保留为排障用(telemetry.ts),不再向服务端发送。
3052
+ }
3053
+ }
3054
+ /** S01:官方 CLI Skill 的离线安装与状态查询(指南随包内分发,不新增对话命令)。 */
3055
+ async function cmdAgentSkill(command, flags) {
3056
+ const sub = command[1];
3057
+ const agent = pickString(flags, '--agent') ?? (pickString(flags, '--target') ? undefined : 'codex');
3058
+ const explicitTarget = pickString(flags, '--target');
3059
+ const targetRoot = explicitTarget
3060
+ ? explicitTarget.replace(/^\$HOME(?=\/|$)/u, homedir())
3061
+ : agent ? defaultTargetForAgent(agent) : undefined;
3062
+ if (sub === 'install') {
3063
+ if (!targetRoot)
3064
+ fail('用法: qmuse agent-skill install --agent codex [--target <dir>] [--force] [--json]');
3065
+ const result = installAgentSkill({
3066
+ targetRoot,
3067
+ agent: agent ?? 'custom',
3068
+ force: flags['--force'] === true,
3069
+ });
3070
+ // T01.11:安装事件(记录指南版本与目标助手类型,不记录本机私有路径)。
3071
+ recordEvent({ kind: 'cli.skill.event', stage: 'install', result: 'ok',
3072
+ agent: agent ?? 'custom', target: 'skill-content' });
3073
+ if (flags['--json'] === true)
3074
+ output({ success: true, ...result }, flags);
3075
+ else {
3076
+ log.success(result.replaced ? `官方指南已安装到 ${result.address}` : `官方指南已是最新(${result.manifest.cliVersion}):${result.address}`);
3077
+ log.info(`包含 SKILL.md、references/cloud.md、references/core-flows.md(共 ${result.installedFiles.length} 个文件)`);
3078
+ }
3079
+ return;
3080
+ }
3081
+ if (sub === 'status') {
3082
+ if (!targetRoot)
3083
+ fail('用法: qmuse agent-skill status [--agent codex] [--target <dir>] [--json]');
3084
+ const report = skillStatus(targetRoot);
3085
+ // T01.11:状态查询事件。
3086
+ recordEvent({ kind: 'cli.skill.event', stage: 'status', result: report.installed ? 'ok' : 'not-installed' });
3087
+ if (flags['--json'] === true) {
3088
+ output(report, flags);
3089
+ return;
3090
+ }
3091
+ log.info(`包内指南版本:${report.packagedVersion}(${report.packagedAvailable ? `${report.packagedFiles} 个文件` : '缺失,请重装 CLI'})`);
3092
+ if (!report.installed)
3093
+ log.warn('当前目标未安装官方指南;执行 qmuse agent-skill install --agent codex');
3094
+ else {
3095
+ log.info(`已安装:${report.installed.address}`);
3096
+ log.info(`安装版本:${report.installed.cliVersion ?? '(早期安装,无元数据)'}${report.installed.matchesPackagedVersion ? '(与 CLI 一致)' : '(与当前 CLI 不一致,建议重新安装)'}`);
3097
+ if (report.installed.modifiedFiles.length > 0) {
3098
+ log.warn(`用户修改保留中:${report.installed.modifiedFiles.join(', ')}(覆盖需 --force)`);
3099
+ }
3100
+ }
3101
+ return;
1038
3102
  }
3103
+ fail(`未知 agent-skill 子命令: ${sub ?? '(空)'}。可用: install / status(指南随 CLI 包内分发)`);
3104
+ }
3105
+ /** 帮助/版本等无命令上下文路径的结束事件(T01.01 覆盖帮助与版本输出)。 */ function recordCommandEvent(label, exitCode, startedAt) {
3106
+ setTelemetryCommand(label);
3107
+ recordEvent({ kind: 'cli.command.start', command: label });
3108
+ recordEvent({ kind: 'cli.command.end', command: label, durationMs: Math.max(0, Date.now() - startedAt),
3109
+ result: exitCode ? 'error' : 'ok' });
1039
3110
  }
1040
3111
  main();