@qmuse/qmuse-cli 1.0.11 → 1.0.13-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/README.md CHANGED
@@ -10,6 +10,20 @@
10
10
  npm install -g @qmuse/qmuse-cli
11
11
  ```
12
12
 
13
+ ## AI 编程工具技能
14
+
15
+ 官方技能随 CLI 包离线分发,支持 Codex、Claude Code、Cursor、Trae、CodeBuddy、Qoder 和 Qwen Code。
16
+
17
+ ```bash
18
+ qmuse agent-skill list
19
+ qmuse agent-skill install --agent cursor
20
+ qmuse agent-skill status --agent cursor --json
21
+ ```
22
+
23
+ 工具标识依次为 `codex`、`claude`、`cursor`、`trae`、`codebuddy`、`qoder`、`qwen`。省略 `--agent` 时按本机目录探测;多个目录时在终端选择,脚本需显式指定目标。自定义路径用 `--target "$HOME/my-tool/skills"`,CLI 自动追加 `/qmuse-cli`。
24
+
25
+ 安装后核对 `installed.address` 和 `installed.upToDate`;状态同时检查版本、缺失文件及内容变化。`qmuse upgrade` 升级后同步已有的默认安装和已登记的自定义目录,保留用户修改。详细步骤见 [技能安装说明](https://www.qmuse.cn/skill-setup.md)。
26
+
13
27
  ## 快速开始
14
28
 
15
29
  ```bash
@@ -15,11 +15,59 @@ export function packagedSkillRoot() {
15
15
  return fileURLToPath(new URL('../skills/qmuse-cli/', import.meta.url));
16
16
  }
17
17
  export function defaultTargetForAgent(agent) {
18
- if (agent === 'codex')
19
- return path.join(homedir(), '.codex', 'skills');
20
- if (agent === 'claude')
21
- return path.join(homedir(), '.claude', 'skills');
22
- return path.join(homedir(), '.qwen', 'skills');
18
+ const supported = SKILL_AGENTS.find(item => item.id === agent);
19
+ if (!supported)
20
+ throw new Error(`未知工具 ${agent}。支持:${SKILL_AGENTS.map(item => item.id).join(', ')};其他工具请使用 --target <skills 目录>。`);
21
+ return path.join(homedir(), supported.directory, 'skills');
22
+ }
23
+ /** 安装、发现和升级同步共用的工具目录。 */
24
+ export const SKILL_AGENTS = [
25
+ { id: 'codex', name: 'Codex', directory: '.codex' },
26
+ { id: 'claude', name: 'Claude Code', directory: '.claude' },
27
+ { id: 'cursor', name: 'Cursor', directory: '.cursor' },
28
+ { id: 'trae', name: 'Trae', directory: '.trae' },
29
+ { id: 'codebuddy', name: 'CodeBuddy', directory: '.codebuddy' },
30
+ { id: 'qoder', name: 'Qoder', directory: '.qoder' },
31
+ { id: 'qwen', name: 'Qwen Code', directory: '.qwen' },
32
+ ];
33
+ function skillTargetsFile() {
34
+ return path.join(homedir(), '.qmuse-cli', 'skill-targets.json');
35
+ }
36
+ function rememberedSkillTargets(file) {
37
+ return existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : [];
38
+ }
39
+ /** 仅在 CLI 安装成功后登记,便于升级找到非默认目录;不存凭证或上报本机路径。 */
40
+ export function rememberSkillTarget(targetRoot, agent, file = skillTargetsFile()) {
41
+ const root = path.resolve(targetRoot);
42
+ const targets = rememberedSkillTargets(file);
43
+ const previous = targets.find(item => item.targetRoot === root);
44
+ if (previous && previous.agent === agent)
45
+ return;
46
+ const next = targets.filter(item => item.targetRoot !== root);
47
+ next.push({ targetRoot: root, ...(agent ? { agent } : {}) });
48
+ mkdirSync(path.dirname(file), { recursive: true });
49
+ writeFileSync(file, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
50
+ }
51
+ export function listSkillTargets(file = skillTargetsFile()) {
52
+ const targets = SKILL_AGENTS.map(agent => ({
53
+ agent: agent.id,
54
+ name: agent.name,
55
+ targetRoot: defaultTargetForAgent(agent.id),
56
+ detected: existsSync(path.join(homedir(), agent.directory)),
57
+ custom: false,
58
+ }));
59
+ for (const saved of rememberedSkillTargets(file)) {
60
+ if (targets.some(item => item.targetRoot === saved.targetRoot))
61
+ continue;
62
+ targets.push({
63
+ agent: saved.agent ?? 'custom',
64
+ name: `${SKILL_AGENTS.find(item => item.id === saved.agent)?.name ?? '自定义工具'}(自定义目录)`,
65
+ targetRoot: saved.targetRoot,
66
+ detected: existsSync(saved.targetRoot),
67
+ custom: true,
68
+ });
69
+ }
70
+ return targets.map(target => ({ ...target, ...skillStatus(target.targetRoot) }));
23
71
  }
24
72
  export function manifestPath(targetRoot) {
25
73
  return path.join(targetRoot, 'qmuse-cli', MANIFEST_FILE);
@@ -81,27 +129,51 @@ export function skillStatus(targetRoot) {
81
129
  };
82
130
  walk(installDir);
83
131
  }
84
- let modifiedFiles = [];
85
- for (const [relative, digest] of packaged) {
132
+ const modifiedFiles = [];
133
+ const missingFiles = [];
134
+ const outdatedFiles = [];
135
+ const staleFiles = [];
136
+ for (const relative of new Set([...packaged.keys(), ...(manifest?.installedFiles ?? [])])) {
137
+ if (relative === MANIFEST_FILE)
138
+ continue;
139
+ const digest = packaged.get(relative);
86
140
  const file = path.join(installDir, relative);
87
- if (!existsSync(file))
141
+ if (!present.has(relative)) {
142
+ if (digest)
143
+ missingFiles.push(relative);
88
144
  continue;
145
+ }
89
146
  try {
90
- if (fileSha256(readFileSync(file, 'utf8')) !== digest)
147
+ const installed = fileSha256(readFileSync(file, 'utf8'));
148
+ if (installed === digest)
149
+ continue;
150
+ if (installed !== manifest?.installedDigests?.[relative])
91
151
  modifiedFiles.push(relative);
152
+ else if (digest)
153
+ outdatedFiles.push(relative);
154
+ else
155
+ staleFiles.push(relative);
156
+ }
157
+ catch {
158
+ modifiedFiles.push(relative);
92
159
  }
93
- catch { /* 不可读视为已修改 */ }
94
160
  }
95
- modifiedFiles = modifiedFiles.filter(name => name !== MANIFEST_FILE);
96
- report.installed = manifest || present.size > 0 ? {
161
+ const matchesPackagedContent = missingFiles.length === 0 && modifiedFiles.length === 0 && outdatedFiles.length === 0 && staleFiles.length === 0;
162
+ report.installed = existsSync(installDir) ? {
97
163
  address: installDir,
98
164
  agent: manifest?.agent,
99
165
  cliVersion: manifest?.cliVersion,
100
166
  matchesPackagedVersion: manifest?.cliVersion === QMUSE_CLI_VERSION,
101
167
  installedAt: manifest?.installedAt,
102
- presentFiles: present.size,
168
+ presentFiles: packaged.size - missingFiles.length,
103
169
  expectedFiles: packaged.size,
104
170
  modifiedFiles,
171
+ missingFiles,
172
+ outdatedFiles,
173
+ staleFiles,
174
+ complete: missingFiles.length === 0,
175
+ matchesPackagedContent,
176
+ upToDate: report.packagedAvailable && manifest?.cliVersion === QMUSE_CLI_VERSION && matchesPackagedContent,
105
177
  } : undefined;
106
178
  return report;
107
179
  }
@@ -116,11 +188,15 @@ export function installAgentSkill(options) {
116
188
  // 已装文件与包内不一致时分两类:用户确实改过(保护)/上游内容变更(应刷新)。
117
189
  const userModified = [];
118
190
  const upstreamChanged = [];
191
+ const missingFiles = [];
192
+ const staleFiles = [];
119
193
  if (existsSync(installDir)) {
120
194
  for (const [relative, digest] of packaged) {
121
195
  const file = path.join(installDir, relative);
122
- if (!existsSync(file))
196
+ if (!existsSync(file)) {
197
+ missingFiles.push(relative);
123
198
  continue;
199
+ }
124
200
  let installed;
125
201
  try {
126
202
  installed = fileSha256(readFileSync(file, 'utf8'));
@@ -137,10 +213,29 @@ export function installAgentSkill(options) {
137
213
  else
138
214
  userModified.push(relative);
139
215
  }
216
+ for (const relative of previous?.installedFiles ?? []) {
217
+ if (packaged.has(relative) || relative === MANIFEST_FILE)
218
+ continue;
219
+ const file = path.join(installDir, relative);
220
+ if (!existsSync(file))
221
+ continue;
222
+ let installed;
223
+ try {
224
+ installed = fileSha256(readFileSync(file, 'utf8'));
225
+ }
226
+ catch {
227
+ installed = undefined;
228
+ }
229
+ const baseline = previous?.installedDigests?.[relative];
230
+ if (baseline && installed === baseline)
231
+ staleFiles.push(relative);
232
+ else
233
+ userModified.push(relative);
234
+ }
140
235
  }
141
236
  const sameVersion = previous?.cliVersion === QMUSE_CLI_VERSION;
142
237
  if (options.force !== true) {
143
- if (sameVersion && userModified.length === 0 && upstreamChanged.length === 0) {
238
+ if (sameVersion && userModified.length === 0 && upstreamChanged.length === 0 && missingFiles.length === 0 && staleFiles.length === 0) {
144
239
  // 已安装内容与包完全一致:幂等返回,不重写文件。
145
240
  const manifest = previous;
146
241
  return { address: installDir, installedFiles: [...packaged.keys()], manifest, replaced: false };
@@ -171,13 +266,9 @@ export function installAgentSkill(options) {
171
266
  rm(installDir);
172
267
  }
173
268
  }
174
- else if (previous?.installedFiles?.length) {
175
- // 升级残留清理:删“上次装的、新包不再提供且未被用户改动核对需要”的文件(无旧 digest,直接按清单删)。
176
- for (const staleFile of previous.installedFiles) {
177
- if (packaged.has(staleFile))
178
- continue; // 新包仍提供 → cpSync 覆盖
179
- if (staleFile === MANIFEST_FILE)
180
- continue; // 清单本体重写
269
+ else {
270
+ // 只清理新包不再提供、且内容仍与上次安装摘要一致的旧文件。
271
+ for (const staleFile of staleFiles) {
181
272
  const target = path.join(installDir, staleFile);
182
273
  try {
183
274
  if (existsSync(target))
@@ -217,15 +308,18 @@ export function installAgentSkill(options) {
217
308
  export function syncInstalledSkills(options = {}) {
218
309
  const candidates = [];
219
310
  const agentRoots = options.agentRoots ?? {};
220
- for (const agent of options.agents ?? ['codex', 'claude']) {
311
+ for (const agent of options.agents ?? SKILL_AGENTS.map(item => item.id)) {
221
312
  candidates.push({ root: agentRoots[agent] ?? defaultTargetForAgent(agent), agent });
222
313
  }
223
314
  for (const root of options.extraTargets ?? [])
224
315
  candidates.push({ root });
316
+ for (const saved of rememberedSkillTargets(options.targetsFile ?? skillTargetsFile())) {
317
+ candidates.push({ root: saved.targetRoot, agent: saved.agent });
318
+ }
225
319
  const results = [];
226
320
  const seen = new Set();
227
321
  for (const candidate of candidates) {
228
- const installDir = path.join(candidate.root, 'qmuse-cli');
322
+ const installDir = path.resolve(candidate.root, 'qmuse-cli');
229
323
  if (!existsSync(installDir) || seen.has(installDir))
230
324
  continue;
231
325
  seen.add(installDir);
package/dist/api.js CHANGED
@@ -12,8 +12,8 @@ export function consumeSessionExchangeToken(token, opts) {
12
12
  return apiCall('POST', '/api/login/session-exchange/consume', { token }, { anonymous: true, ...opts });
13
13
  }
14
14
  /** 查询服务端实际登录身份,不触发 /api/user/me 的奖励和活跃副作用。 */
15
- export function whoami(cfg) {
16
- return apiCall('GET', '/api/user/whoami', undefined, cfg);
15
+ export function whoami(cfg, options = {}) {
16
+ return apiCall('GET', '/api/user/whoami', undefined, { cfg, ...options });
17
17
  }
18
18
  export function listSpaces(pageIndex = 1, pageSize = 20, cfg) {
19
19
  const path = `/api/space/list?pageIndex=${pageIndex}&pageSize=${pageSize}`;
package/dist/app/chat.js CHANGED
@@ -150,7 +150,7 @@ async function readChatStream(res, result, opts) {
150
150
  const type = chatEventType(payload);
151
151
  opts.onEvent?.({ sequence: ++result.eventCount, type, payload });
152
152
  if (type === 'finish') {
153
- terminal = 'SUCCEEDED';
153
+ terminal = sawInterrupt ? 'WAITING_INPUT' : 'SUCCEEDED';
154
154
  return true;
155
155
  }
156
156
  if (type === 'error' || type === 'llm-error' || type === 'data-llm-error') {
@@ -162,7 +162,9 @@ async function readChatStream(res, result, opts) {
162
162
  const record = payload;
163
163
  if (type === 'tool-input-start' || type === 'tool-input-end')
164
164
  result.toolConsumed = true;
165
- const interrupts = record.interrupts
165
+ const interrupts = record.__interrupt__
166
+ ?? record.data?.__interrupt__
167
+ ?? record.interrupts
166
168
  ?? record.data?.interrupts;
167
169
  if (Array.isArray(interrupts) && interrupts.length > 0)
168
170
  sawInterrupt = true;
@@ -138,7 +138,9 @@ function extractInterrupts(payload) {
138
138
  return false;
139
139
  const data = payload.data;
140
140
  const candidates = [
141
+ payload.__interrupt__,
141
142
  payload.interrupts,
143
+ data && typeof data === 'object' ? data.__interrupt__ : undefined,
142
144
  data && typeof data === 'object' ? data.interrupts : undefined,
143
145
  ];
144
146
  return candidates.some(candidate => Array.isArray(candidate) && candidate.length > 0);
@@ -269,7 +271,7 @@ export async function createAppChat(cfg, input, opts = {}) {
269
271
  const type = chatEventType(payload);
270
272
  opts.onEvent?.({ sequence, type, payload });
271
273
  if (type === 'finish') {
272
- terminal = 'SUCCEEDED';
274
+ terminal = sawInterrupt ? 'WAITING_INPUT' : 'SUCCEEDED';
273
275
  return true;
274
276
  }
275
277
  if (type === 'error' || type === 'llm-error' || type === 'data-llm-error') {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 发布域(C10):POST /api/deploy/create + update(触发执行),GET /api/deploy/query | list | last。
2
+ * 发布域(C10):POST /api/deploy/create,GET /api/deploy/query | list | last。
3
3
  * 部署单状态机:INIT → INITED(安全检查通过)→ DEPLOYING → DEPLOYED / DEPLOY_FAILED / CANCEL。
4
4
  * query 返回 content(infosec/mosec/域名发布状态/发布地址),供 release status 汇总。
5
5
  */
@@ -18,10 +18,6 @@ export function parseDeployTicketContent(ticket) {
18
18
  export function createDeployTicket(appId, versionId, cfg) {
19
19
  return apiCall('POST', '/api/deploy/create', { appId, versionId }, cfg);
20
20
  }
21
- /** 触发已就绪(INITED)部署单执行发布;服务端异步执行 DEPLOYING → 终态。 */
22
- export function triggerDeployTicket(deployId, cfg) {
23
- return apiCall('POST', '/api/deploy/update', { id: deployId }, cfg);
24
- }
25
21
  /** C10.03 查询发布结果:同一查询返回任务详情、产物/安全检查、地址与失败原因。 */
26
22
  export function queryDeployTicket(deployId, cfg) {
27
23
  return apiCall('GET', `/api/deploy/query?id=${encodeURIComponent(deployId)}`, undefined, cfg);
@@ -38,7 +34,7 @@ export function sleep(ms) {
38
34
  return new Promise(resolve => setTimeout(resolve, ms));
39
35
  }
40
36
  /**
41
- * 发布等待循环:CREATE 后轮询 query;INITED 时触发 update(复用现有发布流程);
37
+ * 发布等待循环:CREATE 后轮询 query;服务端 query 将 INITED 推进为 DEPLOYING 并执行发布;
42
38
  * 终态、--no-wait 受理(DEPLOYING)或超时返回最近一次 ticket。
43
39
  * 温和退避(5s→15s 封顶)降低轮询请求量;连续网络错误快速失败。
44
40
  */
@@ -47,7 +43,6 @@ export async function waitDeployResult(deployId, cfg, options = {}) {
47
43
  const timeoutMs = options.timeoutMs ?? (noWait ? 5 * 60 * 1000 : 8 * 60 * 1000);
48
44
  const baseIntervalMs = options.intervalMs ?? 5000;
49
45
  const startedAt = Date.now();
50
- let triggered = false;
51
46
  let lastError;
52
47
  let polls = 0;
53
48
  let netErrors = 0;
@@ -59,7 +54,7 @@ export async function waitDeployResult(deployId, cfg, options = {}) {
59
54
  netErrors += 1;
60
55
  if (netErrors >= 3) {
61
56
  lastError = `连续 ${netErrors} 次网络错误:${r.errorMessage ?? 'network'}`;
62
- return { ticket: undefined, triggered, timedOut: false, lastError };
57
+ return { ticket: undefined, timedOut: false, lastError };
63
58
  }
64
59
  }
65
60
  else {
@@ -67,37 +62,27 @@ export async function waitDeployResult(deployId, cfg, options = {}) {
67
62
  }
68
63
  const ticket = r.ok ? r.data : undefined;
69
64
  const status = typeof ticket?.['status'] === 'string' ? ticket['status'] : undefined;
70
- if (!r.ok) {
65
+ if (r.ok) {
66
+ lastError = undefined;
67
+ }
68
+ else {
71
69
  lastError = r.errorMessage ?? r.errorCode ?? `HTTP ${r.status}`;
72
70
  // 快速失败:单据不存在/无权限时继续轮询没有意义(避免长达数分钟的空转)。
73
71
  const notFound = r.status === 404
74
72
  || r.errorCode === 'NOT_FOUND'
75
73
  || /not found|不存在|无权限|forbidden/iu.test(String(lastError));
76
74
  if (notFound) {
77
- return { ticket: ticket ?? undefined, triggered, timedOut: false, lastError };
75
+ return { ticket: ticket ?? undefined, timedOut: false, lastError };
78
76
  }
79
77
  }
80
78
  if (status && APP_DEPLOY_TERMINAL_STATUSES.has(status)) {
81
- return { ticket: ticket ?? undefined, triggered, timedOut: false, lastError };
82
- }
83
- if (status === 'INITED' && !triggered) {
84
- // 安全门禁通过:触发现有发布流程(INITED → DEPLOYING,服务端异步执行)。
85
- const up = await triggerDeployTicket(deployId, cfg);
86
- triggered = true;
87
- if (!up.ok)
88
- lastError = up.errorMessage ?? up.errorCode ?? `HTTP ${up.status}`; // 触发失败语义保留(不重触发)
89
- const updated = up.ok ? up.data : undefined;
90
- const next = typeof updated?.['status'] === 'string' ? updated['status'] : undefined;
91
- if (next && APP_DEPLOY_TERMINAL_STATUSES.has(next))
92
- return { ticket: updated, triggered, timedOut: false };
93
- if (updated && noWait)
94
- return { ticket: updated, triggered, timedOut: false };
79
+ return { ticket: ticket ?? undefined, timedOut: false, lastError };
95
80
  }
96
- if (noWait && (triggered || (status && status !== 'INIT'))) {
97
- return { ticket: ticket ?? undefined, triggered, timedOut: false, lastError };
81
+ if (noWait && status === 'DEPLOYING') {
82
+ return { ticket: ticket ?? undefined, timedOut: false, lastError };
98
83
  }
99
84
  if (Date.now() - startedAt > timeoutMs)
100
- return { ticket: ticket ?? undefined, triggered, timedOut: true, lastError };
85
+ return { ticket: ticket ?? undefined, timedOut: true, lastError };
101
86
  // 温和退避:每 4 轮 ×1.6,封顶 15s(8 分钟窗口轮询请求减半以上)。
102
87
  const intervalMs = Math.min(baseIntervalMs * Math.pow(1.6, Math.floor(polls / 4)), 15000);
103
88
  await sleep(Math.round(intervalMs));
package/dist/cli.js CHANGED
@@ -8,12 +8,14 @@ import { recordOperation, withOperationLog } from './operation-log.js';
8
8
  */
9
9
  import { loadConfig, saveConfig, deleteSession, findConfigPath } from './config.js';
10
10
  import { hasAuth, apiCall } from './client.js';
11
- import { log } from './logger.js';
11
+ import { log, setStructuredOutput } 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
15
  import { randomBytes } from 'node:crypto';
16
- import { spawn } from 'node:child_process';
16
+ import { execFile, spawn } from 'node:child_process';
17
+ import { promisify } from 'node:util';
18
+ import { pathToFileURL } from 'node:url';
17
19
  import path from 'node:path';
18
20
  import { homedir } from 'node:os';
19
21
  import { createImportArchive } from './import/archive.js';
@@ -45,7 +47,7 @@ import { recordEvent, setTelemetryCommand, currentCommandLabel } from './telemet
45
47
  import { trackCommand } from './tracker.js';
46
48
  import { buildCliError, sanitizeMessage } from './errors.js';
47
49
  import { checkVersionGate, defaultUpgradeCommand, envPolicyProvider, registryPolicyProvider, shouldUpgrade } from './version-gate.js';
48
- import { installAgentSkill, skillStatus, syncInstalledSkills, defaultTargetForAgent } from './agent-skill.js';
50
+ import { installAgentSkill, skillStatus, syncInstalledSkills, defaultTargetForAgent, listSkillTargets, rememberSkillTarget } from './agent-skill.js';
49
51
  import { startDevelopment } from './dev/index.js';
50
52
  import { createProject } from './create.js';
51
53
  import { pullProject } from './pull.js';
@@ -874,6 +876,22 @@ function summarizeDeployTicket(ticket) {
874
876
  updatedAt: ticket.updatedAt ?? ticket.gmtModified ?? undefined,
875
877
  };
876
878
  }
879
+ /** 同一应用的发布结果共用一次详情查询,补齐发布单未携带的正式访问地址。 */
880
+ async function summarizeDeployTickets(tickets, cfg) {
881
+ const summaries = tickets.map(summarizeDeployTicket);
882
+ const published = summaries.find(item => item.status === 'DEPLOYED' && !item.publishUrl);
883
+ if (typeof published?.appId === 'string') {
884
+ const app = await queryAppInfo(published.appId, cfg);
885
+ if (app.ok && app.data) {
886
+ const publishUrl = app.data.domain ? `https://${app.data.domain}` : app.data.url;
887
+ for (const summary of summaries) {
888
+ if (summary.status === 'DEPLOYED' && !summary.publishUrl)
889
+ summary.publishUrl = publishUrl;
890
+ }
891
+ }
892
+ }
893
+ return summaries;
894
+ }
877
895
  async function cmdDeploy(command, flags) {
878
896
  const cfg = resolveCfg(flags);
879
897
  requireAuth(cfg);
@@ -895,14 +913,18 @@ async function cmdDeploy(command, flags) {
895
913
  recordEvent({ kind: 'cli.task.state', route: '/api/deploy/create', stage: 'accepted',
896
914
  operationId: deployId || undefined, result: 'ok' });
897
915
  const wait = await waitDeployResult(deployId, cfg, { noWait: flags['--no-wait'] === true });
916
+ const success = !wait.timedOut && !!wait.ticket
917
+ && wait.ticket.status !== 'DEPLOY_FAILED' && wait.ticket.status !== 'CANCEL'
918
+ && (!wait.lastError || wait.ticket.status === 'DEPLOYED');
919
+ const [summary] = await summarizeDeployTickets([wait.ticket ?? seed], cfg);
898
920
  const payload = {
899
- success: !wait.timedOut,
921
+ success,
900
922
  appId,
901
923
  versionId,
902
924
  deployId: deployId || undefined,
903
925
  timedOut: wait.timedOut || undefined,
904
926
  error: wait.lastError,
905
- ...summarizeDeployTicket(wait.ticket ?? seed),
927
+ ...summary,
906
928
  };
907
929
  if (flags['--json'] === true) {
908
930
  output(payload, flags);
@@ -918,8 +940,7 @@ async function cmdDeploy(command, flags) {
918
940
  else if (!flags['--no-wait'])
919
941
  log.info('继续查询:qmuse release status ' + deployId);
920
942
  }
921
- if (wait.timedOut || payload.status === 'DEPLOY_FAILED' || payload.status === 'CANCEL')
922
- process.exitCode = 1;
943
+ process.exitCode = success ? 0 : 1;
923
944
  // T01.08:发布终态来源(receipt 由服务端任务记录,这里保留 CLI 端可观测尾部)。
924
945
  recordEvent({ kind: 'cli.task.state', route: '/api/deploy/query', stage: 'terminal',
925
946
  operationId: deployId || undefined, status: wait.timedOut ? 'timeout' : String(payload.status ?? 'unknown').toLowerCase(),
@@ -935,16 +956,19 @@ async function cmdRelease(command, flags) {
935
956
  if (!releaseId)
936
957
  fail('用法: qmuse release status <releaseId> [--wait] 或 qmuse release status --last --app-id <appId>');
937
958
  const wait = await waitDeployResult(releaseId, cfg, { noWait: flags['--wait'] !== true });
938
- const summary = summarizeDeployTicket(wait.ticket);
959
+ const [summary] = await summarizeDeployTickets([wait.ticket], cfg);
939
960
  const lookupFailed = !wait.ticket;
961
+ const success = !wait.timedOut && !lookupFailed
962
+ && summary.status !== 'DEPLOY_FAILED' && summary.status !== 'CANCEL'
963
+ && (!wait.lastError || summary.status === 'DEPLOYED');
964
+ process.exitCode = success ? 0 : 1;
940
965
  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);
966
+ output({ success, timedOut: wait.timedOut || undefined, error: wait.lastError, ...summary }, flags);
942
967
  return;
943
968
  }
944
969
  log.info(`发布单 ${releaseId} 状态:${String(summary.status ?? 'UNKNOWN')}${wait.timedOut ? '(等待超时)' : ''}`);
945
970
  if (summary.publishUrl)
946
971
  log.success(`发布地址:${String(summary.publishUrl)}`);
947
- process.exitCode = wait.timedOut || summary.status === 'DEPLOY_FAILED' || summary.status === 'CANCEL' ? 1 : 0;
948
972
  return;
949
973
  }
950
974
  if (sub === 'list') {
@@ -955,7 +979,8 @@ async function cmdRelease(command, flags) {
955
979
  const r = await lastDeployTicket(appId, cfg);
956
980
  if (!r.ok)
957
981
  failApi(r, `查询最近发布失败`);
958
- output(summarizeDeployTicket(r.data), flags);
982
+ const [summary] = await summarizeDeployTickets([r.data], cfg);
983
+ output(summary, flags);
959
984
  return;
960
985
  }
961
986
  const r = await listDeployTickets(appId, cfg);
@@ -964,7 +989,7 @@ async function cmdRelease(command, flags) {
964
989
  // 防御性兼容:老网关可能返回包装对象而非数组,运行时仍做数组判定。
965
990
  const payload = r.data;
966
991
  const list = Array.isArray(payload)
967
- ? payload.map(item => summarizeDeployTicket(item))
992
+ ? await summarizeDeployTickets(payload, cfg)
968
993
  : payload;
969
994
  output({ appId, releases: list }, flags);
970
995
  return;
@@ -1141,6 +1166,7 @@ async function cmdWhoami(flags) {
1141
1166
  failApi(result, '查询当前用户失败');
1142
1167
  }
1143
1168
  const currentUser = result.data;
1169
+ trackCommand('whoami', cfg, currentUser.userId);
1144
1170
  const tenantName = tenantDisplayName(currentUser.tenantCode, currentUser.tenantName);
1145
1171
  if (flags['--json'] === true) {
1146
1172
  output({
@@ -1243,7 +1269,7 @@ async function readCreateContent(flags) {
1243
1269
  }
1244
1270
  }
1245
1271
  if (!file && process.stdin.isTTY === true) {
1246
- fail('用法: qmuse app create <web|h5> --file <路径|->(--file - 从管道读取)');
1272
+ fail('用法: qmuse app create <web|h5|ai> --file <路径|->(--file - 从管道读取)');
1247
1273
  }
1248
1274
  const chunks = [];
1249
1275
  for await (const chunk of process.stdin) {
@@ -1688,8 +1714,10 @@ async function cmdChat(command, flags) {
1688
1714
  }
1689
1715
  else {
1690
1716
  log.error(`恢复未完成:${result.status}${result.errorText ? `,${result.errorText}` : ''}`);
1691
- process.exitCode = result.status === 'WAITING_INPUT' ? 2 : 1;
1692
1717
  }
1718
+ process.exitCode = result.status === 'SUCCEEDED' ? 0
1719
+ : result.status === 'WAITING_INPUT' ? 2
1720
+ : result.status === 'INTERRUPTED' ? 130 : 1;
1693
1721
  // T01.04:恢复流终态来源(同一会话 checkpoint,不重跑首轮生成)。
1694
1722
  recordEvent({ kind: 'cli.sse.result', route: '/api/chat', result: result.status.toLowerCase(), operationId });
1695
1723
  return;
@@ -1773,8 +1801,10 @@ async function cmdChat(command, flags) {
1773
1801
  }
1774
1802
  else {
1775
1803
  log.error(`对话未完成:${result.status}${result.errorText ? `,${result.errorText}` : ''}`);
1776
- process.exitCode = result.status === 'WAITING_INPUT' ? 2 : 1;
1777
1804
  }
1805
+ process.exitCode = result.status === 'SUCCEEDED' ? 0
1806
+ : result.status === 'WAITING_INPUT' ? 2
1807
+ : result.status === 'INTERRUPTED' ? 130 : 1;
1778
1808
  // T01.04:已有会话 continue 流终态来源。
1779
1809
  recordEvent({ kind: 'cli.sse.result', route: '/api/chat', result: result.status.toLowerCase(), operationId });
1780
1810
  return;
@@ -2292,8 +2322,8 @@ async function cmdDb(command, flags) {
2292
2322
  const appId = pickString(flags, '--app-id');
2293
2323
  const formId = pickString(flags, '--form-id');
2294
2324
  const env = pickString(flags, '--cloud-env') ?? 'dev';
2295
- if (!appId || !formId)
2296
- fail('用法: qmuse db <子命令> --app-id <appId> --form-id <formId>');
2325
+ if (!appId)
2326
+ fail('用法: qmuse db <子命令> --app-id <appId>');
2297
2327
  if (sub === 'tables') {
2298
2328
  const r = await queryAppMetadata(appId, ((await listAppVersions(appId, cfg)).data?.list ?? [])[0]?.id ?? '', cfg);
2299
2329
  if (!r.ok || !r.data)
@@ -2301,6 +2331,8 @@ async function cmdDb(command, flags) {
2301
2331
  output(r.data, flags);
2302
2332
  return;
2303
2333
  }
2334
+ if (!formId)
2335
+ fail('用法: qmuse db <子命令> --app-id <appId> --form-id <formId>');
2304
2336
  if (sub === 'permission' && command[2] === 'get') {
2305
2337
  const r = await queryFormAuth(appId, formId, cfg);
2306
2338
  if (!r.ok || !r.data)
@@ -2573,7 +2605,7 @@ async function cmdInfo(command, flags) {
2573
2605
  { domain: '数据调试', entry: 'data-source list|info、db tables|records|permission', requires: '会话+appId(记录写操作依赖平台数据权限)' },
2574
2606
  { domain: '云服务与环境', entry: 'cloud init|enable|status|environment retry|pull-env|set-secret|schema', requires: '会话+权益' },
2575
2607
  { 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: '本地安装目标' },
2608
+ { domain: '官方 Skill', entry: 'agent-skill list|install|status(指南随 CLI 包内分发)', requires: '本地安装目标' },
2577
2609
  ],
2578
2610
  unsupported: [
2579
2611
  { capability: '套餐与订阅(购买型)', reason: 'PC 端承载;CLI 仅读订阅状态(account overview 汇总,缺少契约时标注 unsupported)' },
@@ -2597,7 +2629,7 @@ async function cmdInfo(command, flags) {
2597
2629
  * 策略读不到时一律放行——闸门不可用绝不锁死用户。
2598
2630
  */
2599
2631
  async function enforceVersionGate(root, flags) {
2600
- if (root === undefined)
2632
+ if (root === undefined || root === 'upgrade')
2601
2633
  return;
2602
2634
  const gateEnabled = process.env.QMUSE_VERSION_GATE !== 'off';
2603
2635
  if (!gateEnabled)
@@ -2635,7 +2667,7 @@ async function cmdUpgrade(command, flags) {
2635
2667
  if (command.length > 1)
2636
2668
  fail('用法:qmuse upgrade [--json]', { code: 'INVALID_ARGUMENTS' });
2637
2669
  log.info('检查最新版本…');
2638
- const provider = registryPolicyProvider({ distribution: QMUSE_CLI_DISTRIBUTION, timeoutMs: 5000 });
2670
+ const provider = registryPolicyProvider({ distribution: QMUSE_CLI_DISTRIBUTION, timeoutMs: 5000, refresh: true });
2639
2671
  const snapshot = await provider();
2640
2672
  if (!snapshot?.latest) {
2641
2673
  fail('无法获取最新版本信息', {
@@ -2662,11 +2694,13 @@ async function cmdUpgrade(command, flags) {
2662
2694
  log.info('正在升级…');
2663
2695
  }
2664
2696
  const distribution = QMUSE_CLI_DISTRIBUTION;
2697
+ const packageName = distribution === 'PUBLIC' ? '@qmuse/qmuse-cli' : '@alipay/qmuse-cli';
2665
2698
  const args = distribution === 'PUBLIC'
2666
- ? ['install', '-g', `@qmuse/qmuse-cli@${latest}`, '--registry=https://registry.npmjs.org/']
2667
- : ['install', '-g', `@alipay/qmuse-cli@${latest}`];
2699
+ ? ['install', '-g', `${packageName}@${latest}`, '--registry=https://registry.npmjs.org/']
2700
+ : ['install', '-g', `${packageName}@${latest}`];
2668
2701
  const result = await new Promise((resolve) => {
2669
2702
  const child = spawn('npm', args, { stdio: flags['--json'] === true ? 'pipe' : 'inherit', shell: process.platform === 'win32' });
2703
+ child.stdout?.resume();
2670
2704
  let stderr = '';
2671
2705
  child.stderr?.on('data', (chunk) => { stderr += chunk.toString('utf8'); });
2672
2706
  child.on('close', (code) => resolve(code === 0 ? { ok: true } : { ok: false, detail: stderr.slice(-500) }));
@@ -2680,7 +2714,7 @@ async function cmdUpgrade(command, flags) {
2680
2714
  });
2681
2715
  }
2682
2716
  // 升级成功后顺带同步「已存在」的官方指南安装(不新建目标、不覆盖用户修改、失败不影响升级结论)。
2683
- const skillSync = syncInstalledSkills();
2717
+ const skillSync = await syncUpgradedSkills(packageName);
2684
2718
  if (flags['--json'] === true) {
2685
2719
  output({ success: true, from: current, to: latest, skillSync }, flags);
2686
2720
  return;
@@ -2688,6 +2722,24 @@ async function cmdUpgrade(command, flags) {
2688
2722
  log.success(`已升级到 ${latest}`);
2689
2723
  reportSkillSync(skillSync);
2690
2724
  }
2725
+ /** 从安装后的全局包启动新进程,避免旧进程的模块缓存把旧版本写入安装清单。 */
2726
+ async function syncUpgradedSkills(packageName) {
2727
+ const execFileAsync = promisify(execFile);
2728
+ let address = packageName;
2729
+ try {
2730
+ const { stdout: root } = await execFileAsync('npm', ['root', '-g'], {
2731
+ encoding: 'utf8', shell: process.platform === 'win32',
2732
+ });
2733
+ address = path.join(root.trim(), packageName);
2734
+ const moduleUrl = pathToFileURL(path.join(address, 'dist', 'agent-skill.js')).href;
2735
+ const script = `import { syncInstalledSkills } from ${JSON.stringify(moduleUrl)}; process.stdout.write(JSON.stringify(syncInstalledSkills()));`;
2736
+ const { stdout } = await execFileAsync(process.execPath, ['--input-type=module', '--eval', script], { encoding: 'utf8' });
2737
+ return JSON.parse(stdout);
2738
+ }
2739
+ catch (error) {
2740
+ return [{ address, outcome: 'failed', detail: sanitizeMessage(error instanceof Error ? error.message : String(error)) }];
2741
+ }
2742
+ }
2691
2743
  /** 输出指南同步结果(升级前后共用)。 */
2692
2744
  function reportSkillSync(items) {
2693
2745
  if (items.length === 0) {
@@ -2798,8 +2850,11 @@ const HELP = `QMuse CLI —— 将本地 React 项目导入 QMuse
2798
2850
  release status <releaseId> [--wait] 查询发布任务详情/检查/地址/失败原因
2799
2851
  release list --app-id <id> 查询应用发布记录(--last 查最近一次)
2800
2852
  info [--app-id <id>] CLI 版本/环境/绑定/模板约束/云架构/能力
2801
- agent-skill install --agent codex 安装包内官方研发指南(--target 覆盖;--force 覆盖修改)
2802
- agent-skill status [--agent codex|--target <dir>] 查询指南安装状态与用户修改
2853
+ agent-skill list 列出支持的工具、探测目录和安装状态
2854
+ agent-skill install [--agent <tool>] 安装包内指南;省略工具时探测并选择
2855
+ --agent codex|claude|cursor|trae|codebuddy|qoder|qwen
2856
+ --target <skills-dir> 自定义父目录(自动追加 /qmuse-cli);--force 覆盖修改
2857
+ agent-skill status [--agent <tool>|--target <dir>] 查询版本、缺失文件与用户修改;省略目标时列出全部
2803
2858
  upgrade 自查最新版本并升级 CLI(--json 结构化输出)
2804
2859
  account overview|benefit|points … 空间余额/权益/喵币消耗/订阅状态(--space-id)
2805
2860
  data-source list|info 应用数据源与表元数据(--app-id)
@@ -2813,7 +2868,7 @@ const HELP = `QMuse CLI —— 将本地 React 项目导入 QMuse
2813
2868
  sandbox logs --app-id <id> 现有接口可提供的构建/任务诊断日志
2814
2869
  [--sandbox-id <id>] [--version-id <id>] [--build-id <id>]
2815
2870
  sandbox health|restart|setting-get|ensure 沙箱其余只读与显式重启入口
2816
- app create <web|h5> --file <路径|-> 统一创建应用并开始首轮生成(预发接口)
2871
+ app create <web|h5|ai> --file <路径|-> 创建应用并开始首轮生成
2817
2872
  [--title <title>] [--space-id <id>] [--request-id <cc_时间戳_随机hex>]
2818
2873
  [--model <model>] [--no-open] [--json|--ndjson]
2819
2874
  app list [--space-id <id>] [--keyword <kw>] 查询空间应用列表
@@ -2862,6 +2917,7 @@ async function main() {
2862
2917
  return;
2863
2918
  }
2864
2919
  const { command, flags } = parseArgs(argv);
2920
+ setStructuredOutput(flags['--json'] === true || flags['--ndjson'] === true);
2865
2921
  if (flags['--help'] === true || flags['-h'] === true) {
2866
2922
  recordCommandEvent(currentCommandLabel(command), 0, Date.now());
2867
2923
  process.stdout.write(HELP);
@@ -2878,14 +2934,15 @@ async function main() {
2878
2934
  const commandStartedAt = Date.now();
2879
2935
  recordEvent({ kind: 'cli.command.start', command: commandLabel,
2880
2936
  environment: str(flags, '--env') });
2881
- // MAU:模拟 Tracert dwcookie 协议上报到蚂蚁采集平台
2882
- trackCommand(commandLabel.replace(/ /gu, '-'), resolveCfg(flags));
2883
2937
  const finishUpdateNotice = startUpdateNotice({
2884
2938
  enabled: root === 'import' && process.stderr.isTTY === true && flags['--json'] !== true,
2885
2939
  notify: message => process.stderr.write(`${message}\n`),
2886
2940
  });
2887
2941
  process.once('exit', finishUpdateNotice);
2888
2942
  try {
2943
+ // whoami 直接复用业务响应中的身份,其余命令的身份查询与采集共享短预算。
2944
+ if (root !== 'whoami')
2945
+ trackCommand(commandLabel.replace(/ /gu, '-'), resolveCfg(flags));
2889
2946
  // 版本闸门(失败降级放行):blocked 拒绝执行,warning 告警,notice 仅退出提示。
2890
2947
  await enforceVersionGate(root, flags);
2891
2948
  switch (root) {
@@ -3054,19 +3111,69 @@ async function main() {
3054
3111
  /** S01:官方 CLI Skill 的离线安装与状态查询(指南随包内分发,不新增对话命令)。 */
3055
3112
  async function cmdAgentSkill(command, flags) {
3056
3113
  const sub = command[1];
3057
- const agent = pickString(flags, '--agent') ?? (pickString(flags, '--target') ? undefined : 'codex');
3114
+ if (!['list', 'install', 'status'].includes(sub)) {
3115
+ fail(`未知 agent-skill 子命令: ${sub ?? '(空)'}。可用: list / install / status(指南随 CLI 包内分发)`);
3116
+ }
3117
+ let agent = pickString(flags, '--agent');
3058
3118
  const explicitTarget = pickString(flags, '--target');
3059
- const targetRoot = explicitTarget
3060
- ? explicitTarget.replace(/^\$HOME(?=\/|$)/u, homedir())
3119
+ // 即使指定了 --target,也拒绝未知的工具名,避免拼写错误被静默接受。
3120
+ if (agent)
3121
+ defaultTargetForAgent(agent);
3122
+ let targetRoot = explicitTarget
3123
+ ? path.resolve(explicitTarget.replace(/^(?:\$HOME|~)(?=\/|$)/u, homedir()))
3061
3124
  : agent ? defaultTargetForAgent(agent) : undefined;
3125
+ const printStatus = (report) => {
3126
+ const installed = report.installed;
3127
+ if (!installed)
3128
+ return;
3129
+ log.info(`已安装:${installed.address}`);
3130
+ log.info(`安装版本:${installed.cliVersion ?? '(手动安装,无元数据)'}${installed.upToDate ? '(与 CLI 一致,文件完整)' : '(需检查或重新安装)'}`);
3131
+ if (installed.missingFiles.length)
3132
+ log.warn(`缺失文件:${installed.missingFiles.join(', ')}`);
3133
+ if (installed.outdatedFiles.length || installed.staleFiles.length)
3134
+ log.warn('指南内容已过期,重新安装可更新。');
3135
+ if (installed.modifiedFiles.length)
3136
+ log.warn(`用户修改保留中:${installed.modifiedFiles.join(', ')}(覆盖需 --force)`);
3137
+ };
3138
+ if (sub === 'list' || (sub === 'status' && !targetRoot)) {
3139
+ const targets = listSkillTargets();
3140
+ if (flags['--json'] === true)
3141
+ output({ targets }, flags);
3142
+ else {
3143
+ for (const target of targets) {
3144
+ const selection = target.custom ? `--target ${target.targetRoot}` : `--agent ${target.agent}`;
3145
+ log.info(`${target.name}(${selection}):${target.targetRoot};${target.detected ? '已发现目录' : '未发现目录'};${target.installed ? (target.installed.upToDate ? '指南已更新' : '指南需检查') : '未安装指南'}`);
3146
+ if (sub === 'status')
3147
+ printStatus(target);
3148
+ }
3149
+ }
3150
+ return;
3151
+ }
3062
3152
  if (sub === 'install') {
3063
- if (!targetRoot)
3064
- fail('用法: qmuse agent-skill install --agent codex [--target <dir>] [--force] [--json]');
3153
+ if (!targetRoot) {
3154
+ const detected = listSkillTargets().filter(target => target.detected);
3155
+ if (detected.length === 0)
3156
+ fail('未发现支持的工具目录。请执行 qmuse agent-skill list,再用 --agent <工具> 或 --target <skills 目录> 指定安装位置。');
3157
+ let chosen = detected[0];
3158
+ if (detected.length > 1) {
3159
+ const choices = detected.map((target, index) => `${index + 1}. ${target.name}:${target.targetRoot}`).join('\n');
3160
+ if (process.stdin.isTTY !== true || flags['--json'] === true) {
3161
+ fail(`发现多个工具目录,请用 --agent <工具> 或 --target <skills 目录> 选择当前工具:\n${choices}`);
3162
+ }
3163
+ const answer = Number(await prompt(`选择当前工具:\n${choices}\n请输入编号:`));
3164
+ if (!Number.isInteger(answer) || answer < 1 || answer > detected.length)
3165
+ fail('无效编号,未安装指南。');
3166
+ chosen = detected[answer - 1];
3167
+ }
3168
+ targetRoot = chosen.targetRoot;
3169
+ agent = chosen.agent;
3170
+ }
3065
3171
  const result = installAgentSkill({
3066
3172
  targetRoot,
3067
3173
  agent: agent ?? 'custom',
3068
3174
  force: flags['--force'] === true,
3069
3175
  });
3176
+ rememberSkillTarget(targetRoot, agent ?? 'custom');
3070
3177
  // T01.11:安装事件(记录指南版本与目标助手类型,不记录本机私有路径)。
3071
3178
  recordEvent({ kind: 'cli.skill.event', stage: 'install', result: 'ok',
3072
3179
  agent: agent ?? 'custom', target: 'skill-content' });
@@ -3080,7 +3187,7 @@ async function cmdAgentSkill(command, flags) {
3080
3187
  }
3081
3188
  if (sub === 'status') {
3082
3189
  if (!targetRoot)
3083
- fail('用法: qmuse agent-skill status [--agent codex] [--target <dir>] [--json]');
3190
+ fail('用法: qmuse agent-skill status [--agent <工具>] [--target <skills 目录>] [--json]');
3084
3191
  const report = skillStatus(targetRoot);
3085
3192
  // T01.11:状态查询事件。
3086
3193
  recordEvent({ kind: 'cli.skill.event', stage: 'status', result: report.installed ? 'ok' : 'not-installed' });
@@ -3090,17 +3197,11 @@ async function cmdAgentSkill(command, flags) {
3090
3197
  }
3091
3198
  log.info(`包内指南版本:${report.packagedVersion}(${report.packagedAvailable ? `${report.packagedFiles} 个文件` : '缺失,请重装 CLI'})`);
3092
3199
  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
- }
3200
+ log.warn('当前目标未安装官方指南;用相同的 --agent 或 --target 执行 qmuse agent-skill install。');
3201
+ else
3202
+ printStatus(report);
3101
3203
  return;
3102
3204
  }
3103
- fail(`未知 agent-skill 子命令: ${sub ?? '(空)'}。可用: install / status(指南随 CLI 包内分发)`);
3104
3205
  }
3105
3206
  /** 帮助/版本等无命令上下文路径的结束事件(T01.01 覆盖帮助与版本输出)。 */ function recordCommandEvent(label, exitCode, startedAt) {
3106
3207
  setTelemetryCommand(label);
package/dist/logger.js CHANGED
@@ -13,10 +13,13 @@ const ICON = {
13
13
  error: '✗',
14
14
  dim: '·',
15
15
  };
16
+ let structuredOutput = false;
17
+ export function setStructuredOutput(enabled) {
18
+ structuredOutput = enabled;
19
+ }
16
20
  function emit(level, msg) {
17
21
  const icon = COLORS[level](`${ICON[level]}`);
18
- // eslint-disable-next-line no-console
19
- console.log(`${icon} ${msg}`);
22
+ (structuredOutput ? process.stderr : process.stdout).write(`${icon} ${msg}\n`);
20
23
  }
21
24
  export const log = {
22
25
  info: (m) => emit('info', m),
package/dist/tracker.js CHANGED
@@ -11,6 +11,8 @@
11
11
  * - C 位:每个命令一个 click 事件
12
12
  */
13
13
  import { randomUUID } from 'node:crypto';
14
+ import { hasAuth } from './client.js';
15
+ const TRACKING_TIMEOUT_MS = 500;
14
16
  const COLLECT_URL = 'https://collect.alipay.com/dwcookie';
15
17
  const QMUSE_WEB_ORIGIN = 'https://www.qmuse.cn';
16
18
  const SPM_A = 'a5400'; // QMuse Web 的 SPM A 位(config/config.ts tracert.spmAPos)
@@ -71,31 +73,29 @@ const SPM_MAP = {
71
73
  };
72
74
  const CLIENT_ID = randomUUID();
73
75
  const BIZ_TYPE = 'qmuse';
74
- let cachedUserId = null;
75
- /** 从 CLI 会话获取 userId(fire-and-forget;拿不到用空串,采集平台会丢这条但主流程不受影响) */
76
- async function fetchUserId(cfg) {
77
- if (cachedUserId !== null)
78
- return cachedUserId;
76
+ /** 身份查询与采集共享短预算,不占用业务请求的默认超时。 */
77
+ async function fetchUserId(cfg, signal) {
79
78
  try {
80
79
  const { whoami } = await import('./api.js');
81
- const r = await whoami(cfg);
82
- cachedUserId = r.data?.userId?.trim() || '';
80
+ const r = await whoami(cfg, { signal, timeoutMs: TRACKING_TIMEOUT_MS });
81
+ return r.data?.userId?.trim() || '';
83
82
  }
84
83
  catch {
85
- cachedUserId = '';
84
+ return '';
86
85
  }
87
- return cachedUserId;
88
86
  }
89
87
  /** 发送 dwcookie 埋点(fire-and-forget,绝不抛出) */
90
- export function trackCommand(command, cfg) {
88
+ export function trackCommand(command, cfg, knownUserId) {
91
89
  const spmC = SPM_MAP[command];
92
- if (!spmC)
90
+ if (!spmC || !hasAuth(cfg))
93
91
  return;
94
92
  const spm = `${SPM_A}.${SPM_B}.${spmC}`;
95
93
  const sessionId = `${CLIENT_ID}_${Date.now()}`;
96
- void fetchUserId(cfg)
97
- .catch(() => '')
98
- .then(userId => {
94
+ const signal = AbortSignal.timeout(TRACKING_TIMEOUT_MS);
95
+ void (async () => {
96
+ const userId = knownUserId?.trim() || await fetchUserId(cfg, signal);
97
+ if (!userId || signal.aborted)
98
+ return;
99
99
  const fields = [
100
100
  'DW-COOKIE',
101
101
  sessionId,
@@ -115,16 +115,17 @@ export function trackCommand(command, cfg) {
115
115
  fields[22] = '1';
116
116
  const data = fields.join(',');
117
117
  const body = `data=${encodeURIComponent(data)}`;
118
- void fetch(COLLECT_URL, {
118
+ const response = await fetch(COLLECT_URL, {
119
119
  method: 'POST',
120
120
  headers: {
121
121
  'content-type': 'application/x-www-form-urlencoded',
122
122
  referer: `${QMUSE_WEB_ORIGIN}/`,
123
123
  },
124
124
  body,
125
- signal: AbortSignal.timeout(5000),
126
- }).catch(() => {
127
- /* 采集失败不影响业务 */
125
+ signal,
128
126
  });
127
+ await response.body?.cancel();
128
+ })().catch(() => {
129
+ /* 埋点失败或超时不影响业务 */
129
130
  });
130
131
  }
@@ -65,15 +65,10 @@ export function gateLevel(current, snapshot) {
65
65
  const minRequired = policy?.minRequired;
66
66
  const warnBelow = policy?.warnBelow;
67
67
  try {
68
- // 预发布版本(1.0.9-dev.3)代表领先于同名正式版,因此「有无更新」不提示;
69
- // 但它仍然可能落后于 minRequired(例如要求 1.5.0 时 1.0.9 必须阻断),故阻断判据不豁免。
70
- const isPrerelease = !!semver.prerelease(current);
71
68
  if (minRequired && semver.valid(minRequired) && semver.compare(current, minRequired) < 0)
72
69
  return 'blocked';
73
70
  if (warnBelow && semver.valid(warnBelow) && semver.compare(current, warnBelow) < 0)
74
71
  return 'warning';
75
- if (isPrerelease)
76
- return 'ok';
77
72
  if (snapshot.latest && semver.valid(snapshot.latest) && semver.lt(current, snapshot.latest))
78
73
  return 'notice';
79
74
  return 'ok';
@@ -82,12 +77,10 @@ export function gateLevel(current, snapshot) {
82
77
  return 'ok';
83
78
  }
84
79
  }
85
- /** 是否需要升级:预发布版本不判「落后」;缺少最新版本信息时返回 false(降级)。 */
80
+ /** 按 semver 比较是否需要升级;缺少最新版本信息时返回 false(降级)。 */
86
81
  export function shouldUpgrade(current, latest) {
87
82
  if (!latest || !semver.valid(latest) || !semver.valid(current))
88
83
  return false;
89
- if (semver.prerelease(current))
90
- return false;
91
84
  try {
92
85
  return semver.lt(current, latest);
93
86
  }
@@ -167,8 +160,8 @@ export function registryPolicyProvider(options = {}) {
167
160
  const url = options.url ?? policyRegistryUrl(distribution);
168
161
  const timeoutMs = options.timeoutMs ?? 2500;
169
162
  return async () => {
170
- // 缓存优先:6 小时内不重复打 registry(每条命令一次请求代价过高)。
171
- const cached = readPolicyCache(distribution);
163
+ // 普通命令复用一小时缓存,主动升级直接查询 registry。
164
+ const cached = options.refresh ? undefined : readPolicyCache(distribution);
172
165
  if (cached)
173
166
  return cached;
174
167
  const controller = new AbortController();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qmuse/qmuse-cli",
3
- "version": "1.0.11",
3
+ "version": "1.0.13-dev.1",
4
4
  "description": "QMuse 命令行客户端:在本地创建/导入 React 项目、与 QMuse 对话、拉取发布与云端调试",
5
5
  "type": "module",
6
6
  "bin": {
@@ -45,7 +45,6 @@
45
45
  "vite": "7.3.6"
46
46
  },
47
47
  "license": "MIT",
48
- "repository": "git@code.alipay.com:q-muse/qmuse-cli.git",
49
48
  "publishConfig": {
50
49
  "access": "public",
51
50
  "registry": "https://registry.npmjs.org/"
@@ -1,30 +1,33 @@
1
1
  ---
2
2
  name: qmuse-cli
3
- description: QMuse CLI 官方研发指南:本地工程初始化、导入、应用绑定、远端对话、本地开发、源码同步、云数据与发布。当用户要求使用 QMuse CLI 创建/开发/导入/发布应用到 QMuse,或排查 qmuse 命令问题时使用。
3
+ description: 通过 QMuse 创建、导入、修改、调试、同步或发布应用。用户要求使用 QMuse、操作 qmuse.cn 应用链接,或当前工程已有 QMuse 绑定(.qmuse/project.json 或 .qmuse/app.json)时使用,包括“用 QMuse 做一个应用”“继续修改这个作品”“把本地 React 项目导入 QMuse”及 qmuse 命令排障。普通项目、QMuse 平台或 CLI 自身源码维护沿用仓库研发流程。
4
4
  ---
5
5
 
6
6
  # QMuse CLI 官方指南
7
7
 
8
- 总原则:先 `qmuse info --json` 判定上下文(登录态、环境、绑定、云架构),再选择下方分流入口;
9
- 每个命令都可以 `qmuse help` 查看参数,`--json` 拿结构化输出。执行失败时:
10
- 先读取命令输出中的诊断字段(`message`、`errorCode`、`diagnostics`、`.qmuse/last-import-error.json`),
11
- 不要凭猜测重复执行可能创建新资源的操作(重复创建请求只恢复身份,不重跑首轮生成)。
8
+ 先识别任务:安装与状态查询在本地完成;操作应用时用 `qmuse info --json` 查看身份、空间和绑定,缺少登录再执行 `qmuse login`。已有应用沿用其 appId 和工程;用户给出应用链接时先读取链接中的 appId 并核对应用,避免另建应用。
9
+
10
+ 使用 `qmuse help` 查参数,`--json` 获取结构化结果。导入、对话和发布失败时,读取输出的诊断字段或 `.qmuse/last-import-error.json`;有 jobId、chatId、requestId 时先查原任务,恢复原操作。
12
11
 
13
12
  ## 工程与任务分流(先读这里)
14
13
 
15
14
  | 用户意图 | 入口 | 关键约束 |
16
15
  | --- | --- | --- |
17
- | 初始化一个新的本地工程 | `qmuse create <dir>` | 只负责本地模板与说明;远端应用用 `qmuse import` 或 `qmuse app create` |
18
- | 已有 React 工程导入 QMuse | `qmuse import <dir>` | 安全预检 → 打包上传 → 应用/版本生成;预发/正式环境用 `--env` 区分 |
19
- | 修改已导入的同一应用 | `qmuse import .`(项目目录内) | 自动走 UPDATE 语义;远端基线变化时要求 pull 或 `--force` |
20
- | 自然语言创建应用并首轮生成 | `qmuse app create <web\|h5> --file <路径\|->` | POST /api/chat mode=create 一次完成;同 requestId 重发只恢复身份(REUSED) |
21
- | 已有应用继续对话/生成 | `qmuse chat send / resume --app-id … --chat-id …` | 需要应用绑定或显式 appId;中断恢复传 `--interrupt-id` 与 `--file` |
22
- | 本地开发(云资源连接) | `qmuse dev [dir]` | 云绑定应用;`qmuse dev status/stop` 管理会话 |
23
- | 拉取最新已保存源码 | `qmuse pull [dir]` / `qmuse code export` | 本地有改动会先备份再停止;不静默覆盖 |
24
- | 发布与发布结果 | `qmuse deploy --version <id>`、`qmuse release status <releaseId>` | 受理后可 `--no-wait` 退出;结果含发布地址与安全检查 |
25
-
26
- ## 与官方 Skill 的关系
27
-
28
- - 本指南随 CLI 包内分发(离线可用)。升级 CLI 后执行 `qmuse agent-skill install --agent codex` 重新安装对应版本。
29
- - `qmuse agent-skill status` 查看安装目标、版本与文件状态。
30
- - 云服务规则不复制到本指南:以 `qmuse info --json` / `qmuse cloud status` 返回的实际架构与平台权威文档为准(见 references/cloud.md)。
16
+ | 初始化新的本地 React 工程 | `qmuse create <dir>` | 创建本地模板;开发后用 import 创建远端应用 |
17
+ | 将本地 React 工程导入 QMuse | `qmuse import <dir>` | 首次导入创建应用,已有绑定则更新同一应用 |
18
+ | 修改已导入的应用 | 修改源码后 `qmuse import .` | 平台基线变化先处理同步冲突,再更新 |
19
+ | 用自然语言创建应用 | `qmuse app create <web\|h5\|ai> --file <路径\|->` | web 网页、h5 移动页面、ai AI 应用;返回 appId、chatId 等身份 |
20
+ | 继续已有应用的对话 | `qmuse chat send --app-id <id> --chat-id <id> --file <路径\|->` | send 必须传 appId 和 chatId;缺少会话时先查询历史 |
21
+ | 回答等待输入的对话 | `qmuse chat resume <chatId> --app-id <id> --interrupt-id <id> --file <回答.json>` | interruptId 来自 chat state 或事件,沿用原会话 |
22
+ | 连接真实云资源本地开发 | `qmuse dev [dir]` | 云绑定应用;用 dev status / stop 管理会话 |
23
+ | 拉取平台最新已保存源码 | `qmuse pull [dir]` | 本地源码偏离同步基线时停止;成功更新前备份原源码和绑定,再应用远端源码 |
24
+ | 单独导出源码 | `qmuse code export --app-id <id> --output <dir>` | 导出最新已保存版;上传使用 import |
25
+ | 发布指定版本 | `qmuse deploy --version <id>`、`qmuse release status <releaseId>` | 按用户要求发布;受理后可用 --no-wait 退出,再查结果 |
26
+
27
+ 首次使用身份、导入、对话、同步和发布命令时,按需阅读 [核心流程](references/core-flows.md);涉及数据库、云函数、配置或密钥时阅读 [云服务指南](references/cloud.md)。
28
+
29
+ ## 技能安装与升级
30
+
31
+ 本指南随 CLI 包离线分发。用 `qmuse agent-skill list` 查看 Codex、Claude Code、Cursor、Trae、CodeBuddy、Qoder 和 Qwen Code 的目录;`qmuse agent-skill install --agent <工具>` 安装到当前工具。自定义安装用 `--target <skills 父目录>`,CLI 自动追加 `qmuse-cli`。
32
+
33
+ `qmuse upgrade` 更新 CLI 后会同步已安装的默认目录和已登记的自定义目录,保留用户修改。`qmuse agent-skill status --agent <工具> --json` 中 `installed.upToDate` 同时核对版本与文件内容;缺失文件可重新安装修复。更新后重新读取 SKILL.md,必要时刷新工具的技能列表或开启新会话。
@@ -1,4 +1,4 @@
1
- # 云服务指南(S01.11:匹配现有云服务)
1
+ # 云服务指南
2
2
 
3
3
  CLI 不复制云规则,只做两件事:识别当前应用的实际架构,然后把操作路由到现有权威入口。
4
4
 
@@ -21,9 +21,9 @@ qmuse cloud status --app-id <appId> --json # effectiveMode、activationStatus
21
21
  | 状态/失败原因 | `qmuse cloud status --app-id <appId>` |
22
22
  | 初始化失败重试 | `qmuse cloud environment retry --app-id <appId> --wait` |
23
23
  | 公开连接与运行配置 | `qmuse cloud pull-env --app-id <appId> --output .qmuse/local-dev.json` |
24
- | 数据库/表结构与用量 | `qmuse data-source list|info`、`qmuse db tables`、`qmuse db records …` |
25
- | 云函数 | `qmuse fn list|info|retry|deployment-*|execution-*` |
26
- | 秘密(安全输入) | `qmuse cloud set-secret K1,K2 --stdin`;查询 `qmuse secret list` / `secret sync status|retry` |
24
+ | 数据库/表结构与用量 | `qmuse data-source list\|info`、`qmuse db tables`、`qmuse db records …` |
25
+ | 云函数 | `qmuse fn list\|info\|retry\|deployment-*\|execution-*` |
26
+ | 秘密(安全输入) | `qmuse cloud set-secret K1,K2 --stdin`;查询 `qmuse secret list` / `secret sync status\|retry` |
27
27
 
28
28
  ## 3. 约束
29
29
 
@@ -1,93 +1,84 @@
1
- # 核心流程参考(S01.12)
1
+ # 核心流程参考
2
2
 
3
- 仅覆盖当前交付入口:身份与应用、初始化与导入、本地开发、对话恢复、源码同步、云数据、发布及错误恢复。
3
+ ## 身份与应用
4
4
 
5
- ## 0. 通用建议
6
-
7
- - 结构化输出加 `--json`;日志与进度走 stderr。
8
- - 凭证保存在系统凭证存储(1.0.9+);`--env prod|pre|local` 隔离身份,凭证不跨 Origin 发送。
9
- - 空间选择全局保存:`qmuse space list` → `qmuse space use <spaceId>`(保存前校验可访问)。
10
-
11
- ## 1. 身份与应用
5
+ 业务命令用 `--json` 获取结构化结果,日志和进度写入 stderr。应用操作前通过 `qmuse info --json` 查看当前身份、空间和绑定。
12
6
 
13
7
  ```bash
14
- qmuse login [--status] # 浏览器确认登录
15
- qmuse whoami # 验证真实登录身份
16
- qmuse app create web --file 构思.md # 统一创建(身份头 → 首轮 SSE 终态)
17
- qmuse app list / info / use <appId> / current / unlink / update / delete --yes
8
+ qmuse login
9
+ qmuse whoami
10
+ qmuse space list
11
+ qmuse space use <spaceId>
12
+ qmuse app create web --file 构思.md --json
13
+ qmuse app create h5 --file 移动应用.md --json
14
+ qmuse app create ai --file AI应用.md --json
15
+ qmuse app info <appId>
16
+ qmuse app use <appId>
18
17
  ```
19
18
 
20
- 创建恢复:同 `--request-id` 重发 → `REUSED`,返回已有身份 JSON,不重跑首轮生成。
21
- 断流恢复顺序:`chat state` → `chat history` → 显式 `chat cancel`(如需停止)。
19
+ 创建结果中的 appId、chatId、sprintId 用于后续命令。相同 `--request-id` 重发会恢复已有身份(REUSED),不重新执行首轮生成。生成退出码为 0(完成)、1(失败)、2(等待输入)或 130(取消);等待输入时查询状态并回答原会话。
22
20
 
23
- ## 2. 初始化与导入
21
+ ## 初始化与导入
24
22
 
25
23
  ```bash
26
- qmuse create my-app # 仅本地模板与说明
27
- qmuse import /path/to/react-app # 预检→上传→应用/版本生成
28
- qmuse import . # 已绑定目录的持续更新(UPDATE 语义)
24
+ qmuse create my-app
25
+ qmuse import /path/to/react-app
26
+ qmuse import . # 已绑定目录更新同一应用
29
27
  qmuse import status <jobId> --wait
30
28
  qmuse import configure <jobId> --cloud-mode EXTERNAL --public-config '@public.json'
31
29
  ```
32
30
 
33
- - 云模式:`AUTO`(默认)/`EXTERNAL`(外部云,`--public-config`)/`QMUSE`(需 `--confirm-cloud-service`)。
34
- - 外部数据不自动迁移:`--acknowledge-empty-cloud-data` 显式确认,不伪装已迁移。
35
- - 失败诊断:`.qmuse/last-import-error.json`(含 stage/diagnostic/retry 建议)。
31
+ - 云模式:AUTO(默认)、EXTERNAL(外部云,传公开配置)或 QMUSE(传 `--confirm-cloud-service`)。
32
+ - 外部数据不会自动迁移;选择切换时,用 `--acknowledge-empty-cloud-data` 确认。
33
+ - 失败时读取 `.qmuse/last-import-error.json` 的阶段、诊断和恢复建议。
36
34
 
37
- ## 3. 本地开发
35
+ ## 本地开发与源码同步
38
36
 
39
37
  ```bash
40
- qmuse dev [dir] --port 5173 # 连接开发云资源 + 启动本地 Vite
41
- qmuse dev status # 当前会话 pid/appId/localUrl
42
- qmuse dev stop # SIGTERM→8s→SIGKILL,清理会话文件
43
- qmuse pull [dir] # 拉取最新已保存源码(本地有改动先备份并停止)
38
+ qmuse dev . --port 5173 # 连接云资源并启动本地 Vite
39
+ qmuse dev status
40
+ qmuse dev stop
41
+ qmuse pull . # 拉取平台最新已保存源码
42
+ qmuse version list --app-id <id>
43
+ qmuse version info <versionId> --app-id <id>
44
+ qmuse code export --app-id <id> --output ./exported
44
45
  ```
45
46
 
46
- CLI 凭证不进入浏览器:开发连接由 CLI 代理,浏览器只访问本地服务。
47
-
48
- ## 4. 对话恢复
47
+ `pull` 要求本地源码与上次导入或拉取的基线一致;有新增、修改、删除时会停止,Git 提交不会改变同步基线。成功更新前会将原源码和绑定存入返回的 backupDirectory,再应用远端源码。上传改动用 `import`;下载用 `pull` 或 `code export`。未保存的远端对话改动不会被拉取。
49
48
 
50
- ```bash
51
- qmuse chat send --app-id <id> --chat-id <id> --file - \
52
- [--model <model>] [--ndjson]
53
- qmuse chat resume <chatId> --interrupt-id <id> --file 回答.json --app-id <id>
54
- qmuse chat history --app-id <id> --sprint-id <id>
55
- qmuse chat state <chatId> # 先查询再恢复
56
- qmuse models # 模型可用性来自平台
57
- qmuse chat attachment upload <path> --app-id <id>
58
- ```
49
+ CLI 登录凭证留在本地凭证存储,不放入业务源码或浏览器配置。
59
50
 
60
- 中断恢复要点:同一会话与 checkpoint;`--interrupt-id` 来自 chat state/事件;
61
- 重复创建请求只恢复身份(REUSED),不重跑首轮生成。
62
-
63
- ## 5. 源码同步
51
+ ## 对话与恢复
64
52
 
65
53
  ```bash
66
- qmuse version list|info --app-id <id>
67
- qmuse code export --app-id <id> --output ./exported
68
- qmuse pull . # 最新已保存源码;覆盖前自动备份
54
+ qmuse chat history --app-id <appId> --sprint-id <sprintId>
55
+ qmuse chat send --app-id <appId> --chat-id <chatId> --file 需求.md --json
56
+ qmuse chat state <chatId>
57
+ qmuse chat resume <chatId> --app-id <appId> --interrupt-id <interruptId> --file 回答.json --json
58
+ qmuse chat attachment upload <path> --app-id <appId>
59
+ qmuse models
69
60
  ```
70
61
 
71
- 上传走 `import`;下载用 `pull`(最新已保存版)或 `code export`(只提供最新已保存版源码)。
62
+ send 的 appId 和 chatId 必须显式传入;history 的 appId 和 sprintId 也必须显式传入。断流后先查 state 和 history;等待输入时,沿用原 chatId 和返回的 interruptId,用 JSON 文件回答。需要流式结构化事件时用 `--ndjson`。
72
63
 
73
- ## 6. 云数据
64
+ ## 云数据
74
65
 
75
66
  ```bash
76
- qmuse data-source list|info --app-id <id>
67
+ qmuse data-source list --app-id <id>
77
68
  qmuse db tables --app-id <id>
78
- qmuse db records list|get|create|update|delete --app-id <id> --form-id <formId>
69
+ qmuse db records list --app-id <id> --form-id <formId>
79
70
  qmuse db permission get --app-id <id> --form-id <formId>
80
71
  ```
81
72
 
82
- 记录写操作受平台数据权限约束;失败以命令输出的 `errorCode`/`errorMessage` 为准。
73
+ 表列表不需要 formId;记录读写和表权限查询需要 formId。云架构、函数和密钥命令见 [云服务指南](cloud.md)。
83
74
 
84
- ## 7. 发布与错误恢复
75
+ ## 发布与恢复
85
76
 
86
77
  ```bash
87
- qmuse deploy --version <versionId> [--no-wait] # 固定版本发布(平台安全门禁)
88
- qmuse release status <releaseId> [--wait] # 同一查询返回任务详情/检查/地址/失败原因
89
- qmuse release list --app-id <id>
78
+ qmuse deploy --version <versionId> --no-wait
79
+ qmuse release status <releaseId> --wait
80
+ qmuse release list --app-id <appId>
81
+ qmuse account
90
82
  ```
91
83
 
92
- 失败恢复:`release status` 的 `error`/`mosecStatus`/`infosecResult` + `deploy --version` 重发(幂等受理)。
93
- 账户与成本观测:`qmuse account`(余额/权益/消耗摘要/订阅状态只读)。
84
+ 按用户指定版本发布,结果以 release status 返回的状态、检查结果和地址为准;受理不等于发布成功。失败先查原发布任务的错误,再按诊断恢复。