@qmuse/qmuse-cli 1.0.11 → 1.0.12

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') {
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';
@@ -895,8 +897,11 @@ async function cmdDeploy(command, flags) {
895
897
  recordEvent({ kind: 'cli.task.state', route: '/api/deploy/create', stage: 'accepted',
896
898
  operationId: deployId || undefined, result: 'ok' });
897
899
  const wait = await waitDeployResult(deployId, cfg, { noWait: flags['--no-wait'] === true });
900
+ const success = !wait.timedOut && !!wait.ticket
901
+ && wait.ticket.status !== 'DEPLOY_FAILED' && wait.ticket.status !== 'CANCEL'
902
+ && (!wait.lastError || wait.ticket.status === 'DEPLOYED');
898
903
  const payload = {
899
- success: !wait.timedOut,
904
+ success,
900
905
  appId,
901
906
  versionId,
902
907
  deployId: deployId || undefined,
@@ -918,8 +923,7 @@ async function cmdDeploy(command, flags) {
918
923
  else if (!flags['--no-wait'])
919
924
  log.info('继续查询:qmuse release status ' + deployId);
920
925
  }
921
- if (wait.timedOut || payload.status === 'DEPLOY_FAILED' || payload.status === 'CANCEL')
922
- process.exitCode = 1;
926
+ process.exitCode = success ? 0 : 1;
923
927
  // T01.08:发布终态来源(receipt 由服务端任务记录,这里保留 CLI 端可观测尾部)。
924
928
  recordEvent({ kind: 'cli.task.state', route: '/api/deploy/query', stage: 'terminal',
925
929
  operationId: deployId || undefined, status: wait.timedOut ? 'timeout' : String(payload.status ?? 'unknown').toLowerCase(),
@@ -937,14 +941,17 @@ async function cmdRelease(command, flags) {
937
941
  const wait = await waitDeployResult(releaseId, cfg, { noWait: flags['--wait'] !== true });
938
942
  const summary = summarizeDeployTicket(wait.ticket);
939
943
  const lookupFailed = !wait.ticket;
944
+ const success = !wait.timedOut && !lookupFailed
945
+ && summary.status !== 'DEPLOY_FAILED' && summary.status !== 'CANCEL'
946
+ && (!wait.lastError || summary.status === 'DEPLOYED');
947
+ process.exitCode = success ? 0 : 1;
940
948
  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);
949
+ output({ success, timedOut: wait.timedOut || undefined, error: wait.lastError, ...summary }, flags);
942
950
  return;
943
951
  }
944
952
  log.info(`发布单 ${releaseId} 状态:${String(summary.status ?? 'UNKNOWN')}${wait.timedOut ? '(等待超时)' : ''}`);
945
953
  if (summary.publishUrl)
946
954
  log.success(`发布地址:${String(summary.publishUrl)}`);
947
- process.exitCode = wait.timedOut || summary.status === 'DEPLOY_FAILED' || summary.status === 'CANCEL' ? 1 : 0;
948
955
  return;
949
956
  }
950
957
  if (sub === 'list') {
@@ -1141,6 +1148,7 @@ async function cmdWhoami(flags) {
1141
1148
  failApi(result, '查询当前用户失败');
1142
1149
  }
1143
1150
  const currentUser = result.data;
1151
+ trackCommand('whoami', cfg, currentUser.userId);
1144
1152
  const tenantName = tenantDisplayName(currentUser.tenantCode, currentUser.tenantName);
1145
1153
  if (flags['--json'] === true) {
1146
1154
  output({
@@ -1243,7 +1251,7 @@ async function readCreateContent(flags) {
1243
1251
  }
1244
1252
  }
1245
1253
  if (!file && process.stdin.isTTY === true) {
1246
- fail('用法: qmuse app create <web|h5> --file <路径|->(--file - 从管道读取)');
1254
+ fail('用法: qmuse app create <web|h5|ai> --file <路径|->(--file - 从管道读取)');
1247
1255
  }
1248
1256
  const chunks = [];
1249
1257
  for await (const chunk of process.stdin) {
@@ -1688,8 +1696,10 @@ async function cmdChat(command, flags) {
1688
1696
  }
1689
1697
  else {
1690
1698
  log.error(`恢复未完成:${result.status}${result.errorText ? `,${result.errorText}` : ''}`);
1691
- process.exitCode = result.status === 'WAITING_INPUT' ? 2 : 1;
1692
1699
  }
1700
+ process.exitCode = result.status === 'SUCCEEDED' ? 0
1701
+ : result.status === 'WAITING_INPUT' ? 2
1702
+ : result.status === 'INTERRUPTED' ? 130 : 1;
1693
1703
  // T01.04:恢复流终态来源(同一会话 checkpoint,不重跑首轮生成)。
1694
1704
  recordEvent({ kind: 'cli.sse.result', route: '/api/chat', result: result.status.toLowerCase(), operationId });
1695
1705
  return;
@@ -1773,8 +1783,10 @@ async function cmdChat(command, flags) {
1773
1783
  }
1774
1784
  else {
1775
1785
  log.error(`对话未完成:${result.status}${result.errorText ? `,${result.errorText}` : ''}`);
1776
- process.exitCode = result.status === 'WAITING_INPUT' ? 2 : 1;
1777
1786
  }
1787
+ process.exitCode = result.status === 'SUCCEEDED' ? 0
1788
+ : result.status === 'WAITING_INPUT' ? 2
1789
+ : result.status === 'INTERRUPTED' ? 130 : 1;
1778
1790
  // T01.04:已有会话 continue 流终态来源。
1779
1791
  recordEvent({ kind: 'cli.sse.result', route: '/api/chat', result: result.status.toLowerCase(), operationId });
1780
1792
  return;
@@ -2292,8 +2304,8 @@ async function cmdDb(command, flags) {
2292
2304
  const appId = pickString(flags, '--app-id');
2293
2305
  const formId = pickString(flags, '--form-id');
2294
2306
  const env = pickString(flags, '--cloud-env') ?? 'dev';
2295
- if (!appId || !formId)
2296
- fail('用法: qmuse db <子命令> --app-id <appId> --form-id <formId>');
2307
+ if (!appId)
2308
+ fail('用法: qmuse db <子命令> --app-id <appId>');
2297
2309
  if (sub === 'tables') {
2298
2310
  const r = await queryAppMetadata(appId, ((await listAppVersions(appId, cfg)).data?.list ?? [])[0]?.id ?? '', cfg);
2299
2311
  if (!r.ok || !r.data)
@@ -2301,6 +2313,8 @@ async function cmdDb(command, flags) {
2301
2313
  output(r.data, flags);
2302
2314
  return;
2303
2315
  }
2316
+ if (!formId)
2317
+ fail('用法: qmuse db <子命令> --app-id <appId> --form-id <formId>');
2304
2318
  if (sub === 'permission' && command[2] === 'get') {
2305
2319
  const r = await queryFormAuth(appId, formId, cfg);
2306
2320
  if (!r.ok || !r.data)
@@ -2573,7 +2587,7 @@ async function cmdInfo(command, flags) {
2573
2587
  { domain: '数据调试', entry: 'data-source list|info、db tables|records|permission', requires: '会话+appId(记录写操作依赖平台数据权限)' },
2574
2588
  { domain: '云服务与环境', entry: 'cloud init|enable|status|environment retry|pull-env|set-secret|schema', requires: '会话+权益' },
2575
2589
  { 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: '本地安装目标' },
2590
+ { domain: '官方 Skill', entry: 'agent-skill list|install|status(指南随 CLI 包内分发)', requires: '本地安装目标' },
2577
2591
  ],
2578
2592
  unsupported: [
2579
2593
  { capability: '套餐与订阅(购买型)', reason: 'PC 端承载;CLI 仅读订阅状态(account overview 汇总,缺少契约时标注 unsupported)' },
@@ -2597,7 +2611,7 @@ async function cmdInfo(command, flags) {
2597
2611
  * 策略读不到时一律放行——闸门不可用绝不锁死用户。
2598
2612
  */
2599
2613
  async function enforceVersionGate(root, flags) {
2600
- if (root === undefined)
2614
+ if (root === undefined || root === 'upgrade')
2601
2615
  return;
2602
2616
  const gateEnabled = process.env.QMUSE_VERSION_GATE !== 'off';
2603
2617
  if (!gateEnabled)
@@ -2635,7 +2649,7 @@ async function cmdUpgrade(command, flags) {
2635
2649
  if (command.length > 1)
2636
2650
  fail('用法:qmuse upgrade [--json]', { code: 'INVALID_ARGUMENTS' });
2637
2651
  log.info('检查最新版本…');
2638
- const provider = registryPolicyProvider({ distribution: QMUSE_CLI_DISTRIBUTION, timeoutMs: 5000 });
2652
+ const provider = registryPolicyProvider({ distribution: QMUSE_CLI_DISTRIBUTION, timeoutMs: 5000, refresh: true });
2639
2653
  const snapshot = await provider();
2640
2654
  if (!snapshot?.latest) {
2641
2655
  fail('无法获取最新版本信息', {
@@ -2662,11 +2676,13 @@ async function cmdUpgrade(command, flags) {
2662
2676
  log.info('正在升级…');
2663
2677
  }
2664
2678
  const distribution = QMUSE_CLI_DISTRIBUTION;
2679
+ const packageName = distribution === 'PUBLIC' ? '@qmuse/qmuse-cli' : '@alipay/qmuse-cli';
2665
2680
  const args = distribution === 'PUBLIC'
2666
- ? ['install', '-g', `@qmuse/qmuse-cli@${latest}`, '--registry=https://registry.npmjs.org/']
2667
- : ['install', '-g', `@alipay/qmuse-cli@${latest}`];
2681
+ ? ['install', '-g', `${packageName}@${latest}`, '--registry=https://registry.npmjs.org/']
2682
+ : ['install', '-g', `${packageName}@${latest}`];
2668
2683
  const result = await new Promise((resolve) => {
2669
2684
  const child = spawn('npm', args, { stdio: flags['--json'] === true ? 'pipe' : 'inherit', shell: process.platform === 'win32' });
2685
+ child.stdout?.resume();
2670
2686
  let stderr = '';
2671
2687
  child.stderr?.on('data', (chunk) => { stderr += chunk.toString('utf8'); });
2672
2688
  child.on('close', (code) => resolve(code === 0 ? { ok: true } : { ok: false, detail: stderr.slice(-500) }));
@@ -2680,7 +2696,7 @@ async function cmdUpgrade(command, flags) {
2680
2696
  });
2681
2697
  }
2682
2698
  // 升级成功后顺带同步「已存在」的官方指南安装(不新建目标、不覆盖用户修改、失败不影响升级结论)。
2683
- const skillSync = syncInstalledSkills();
2699
+ const skillSync = await syncUpgradedSkills(packageName);
2684
2700
  if (flags['--json'] === true) {
2685
2701
  output({ success: true, from: current, to: latest, skillSync }, flags);
2686
2702
  return;
@@ -2688,6 +2704,24 @@ async function cmdUpgrade(command, flags) {
2688
2704
  log.success(`已升级到 ${latest}`);
2689
2705
  reportSkillSync(skillSync);
2690
2706
  }
2707
+ /** 从安装后的全局包启动新进程,避免旧进程的模块缓存把旧版本写入安装清单。 */
2708
+ async function syncUpgradedSkills(packageName) {
2709
+ const execFileAsync = promisify(execFile);
2710
+ let address = packageName;
2711
+ try {
2712
+ const { stdout: root } = await execFileAsync('npm', ['root', '-g'], {
2713
+ encoding: 'utf8', shell: process.platform === 'win32',
2714
+ });
2715
+ address = path.join(root.trim(), packageName);
2716
+ const moduleUrl = pathToFileURL(path.join(address, 'dist', 'agent-skill.js')).href;
2717
+ const script = `import { syncInstalledSkills } from ${JSON.stringify(moduleUrl)}; process.stdout.write(JSON.stringify(syncInstalledSkills()));`;
2718
+ const { stdout } = await execFileAsync(process.execPath, ['--input-type=module', '--eval', script], { encoding: 'utf8' });
2719
+ return JSON.parse(stdout);
2720
+ }
2721
+ catch (error) {
2722
+ return [{ address, outcome: 'failed', detail: sanitizeMessage(error instanceof Error ? error.message : String(error)) }];
2723
+ }
2724
+ }
2691
2725
  /** 输出指南同步结果(升级前后共用)。 */
2692
2726
  function reportSkillSync(items) {
2693
2727
  if (items.length === 0) {
@@ -2798,8 +2832,11 @@ const HELP = `QMuse CLI —— 将本地 React 项目导入 QMuse
2798
2832
  release status <releaseId> [--wait] 查询发布任务详情/检查/地址/失败原因
2799
2833
  release list --app-id <id> 查询应用发布记录(--last 查最近一次)
2800
2834
  info [--app-id <id>] CLI 版本/环境/绑定/模板约束/云架构/能力
2801
- agent-skill install --agent codex 安装包内官方研发指南(--target 覆盖;--force 覆盖修改)
2802
- agent-skill status [--agent codex|--target <dir>] 查询指南安装状态与用户修改
2835
+ agent-skill list 列出支持的工具、探测目录和安装状态
2836
+ agent-skill install [--agent <tool>] 安装包内指南;省略工具时探测并选择
2837
+ --agent codex|claude|cursor|trae|codebuddy|qoder|qwen
2838
+ --target <skills-dir> 自定义父目录(自动追加 /qmuse-cli);--force 覆盖修改
2839
+ agent-skill status [--agent <tool>|--target <dir>] 查询版本、缺失文件与用户修改;省略目标时列出全部
2803
2840
  upgrade 自查最新版本并升级 CLI(--json 结构化输出)
2804
2841
  account overview|benefit|points … 空间余额/权益/喵币消耗/订阅状态(--space-id)
2805
2842
  data-source list|info 应用数据源与表元数据(--app-id)
@@ -2813,7 +2850,7 @@ const HELP = `QMuse CLI —— 将本地 React 项目导入 QMuse
2813
2850
  sandbox logs --app-id <id> 现有接口可提供的构建/任务诊断日志
2814
2851
  [--sandbox-id <id>] [--version-id <id>] [--build-id <id>]
2815
2852
  sandbox health|restart|setting-get|ensure 沙箱其余只读与显式重启入口
2816
- app create <web|h5> --file <路径|-> 统一创建应用并开始首轮生成(预发接口)
2853
+ app create <web|h5|ai> --file <路径|-> 创建应用并开始首轮生成
2817
2854
  [--title <title>] [--space-id <id>] [--request-id <cc_时间戳_随机hex>]
2818
2855
  [--model <model>] [--no-open] [--json|--ndjson]
2819
2856
  app list [--space-id <id>] [--keyword <kw>] 查询空间应用列表
@@ -2862,6 +2899,7 @@ async function main() {
2862
2899
  return;
2863
2900
  }
2864
2901
  const { command, flags } = parseArgs(argv);
2902
+ setStructuredOutput(flags['--json'] === true || flags['--ndjson'] === true);
2865
2903
  if (flags['--help'] === true || flags['-h'] === true) {
2866
2904
  recordCommandEvent(currentCommandLabel(command), 0, Date.now());
2867
2905
  process.stdout.write(HELP);
@@ -2878,14 +2916,15 @@ async function main() {
2878
2916
  const commandStartedAt = Date.now();
2879
2917
  recordEvent({ kind: 'cli.command.start', command: commandLabel,
2880
2918
  environment: str(flags, '--env') });
2881
- // MAU:模拟 Tracert dwcookie 协议上报到蚂蚁采集平台
2882
- trackCommand(commandLabel.replace(/ /gu, '-'), resolveCfg(flags));
2883
2919
  const finishUpdateNotice = startUpdateNotice({
2884
2920
  enabled: root === 'import' && process.stderr.isTTY === true && flags['--json'] !== true,
2885
2921
  notify: message => process.stderr.write(`${message}\n`),
2886
2922
  });
2887
2923
  process.once('exit', finishUpdateNotice);
2888
2924
  try {
2925
+ // whoami 直接复用业务响应中的身份,其余命令的身份查询与采集共享短预算。
2926
+ if (root !== 'whoami')
2927
+ trackCommand(commandLabel.replace(/ /gu, '-'), resolveCfg(flags));
2889
2928
  // 版本闸门(失败降级放行):blocked 拒绝执行,warning 告警,notice 仅退出提示。
2890
2929
  await enforceVersionGate(root, flags);
2891
2930
  switch (root) {
@@ -3054,19 +3093,69 @@ async function main() {
3054
3093
  /** S01:官方 CLI Skill 的离线安装与状态查询(指南随包内分发,不新增对话命令)。 */
3055
3094
  async function cmdAgentSkill(command, flags) {
3056
3095
  const sub = command[1];
3057
- const agent = pickString(flags, '--agent') ?? (pickString(flags, '--target') ? undefined : 'codex');
3096
+ if (!['list', 'install', 'status'].includes(sub)) {
3097
+ fail(`未知 agent-skill 子命令: ${sub ?? '(空)'}。可用: list / install / status(指南随 CLI 包内分发)`);
3098
+ }
3099
+ let agent = pickString(flags, '--agent');
3058
3100
  const explicitTarget = pickString(flags, '--target');
3059
- const targetRoot = explicitTarget
3060
- ? explicitTarget.replace(/^\$HOME(?=\/|$)/u, homedir())
3101
+ // 即使指定了 --target,也拒绝未知的工具名,避免拼写错误被静默接受。
3102
+ if (agent)
3103
+ defaultTargetForAgent(agent);
3104
+ let targetRoot = explicitTarget
3105
+ ? path.resolve(explicitTarget.replace(/^(?:\$HOME|~)(?=\/|$)/u, homedir()))
3061
3106
  : agent ? defaultTargetForAgent(agent) : undefined;
3107
+ const printStatus = (report) => {
3108
+ const installed = report.installed;
3109
+ if (!installed)
3110
+ return;
3111
+ log.info(`已安装:${installed.address}`);
3112
+ log.info(`安装版本:${installed.cliVersion ?? '(手动安装,无元数据)'}${installed.upToDate ? '(与 CLI 一致,文件完整)' : '(需检查或重新安装)'}`);
3113
+ if (installed.missingFiles.length)
3114
+ log.warn(`缺失文件:${installed.missingFiles.join(', ')}`);
3115
+ if (installed.outdatedFiles.length || installed.staleFiles.length)
3116
+ log.warn('指南内容已过期,重新安装可更新。');
3117
+ if (installed.modifiedFiles.length)
3118
+ log.warn(`用户修改保留中:${installed.modifiedFiles.join(', ')}(覆盖需 --force)`);
3119
+ };
3120
+ if (sub === 'list' || (sub === 'status' && !targetRoot)) {
3121
+ const targets = listSkillTargets();
3122
+ if (flags['--json'] === true)
3123
+ output({ targets }, flags);
3124
+ else {
3125
+ for (const target of targets) {
3126
+ const selection = target.custom ? `--target ${target.targetRoot}` : `--agent ${target.agent}`;
3127
+ log.info(`${target.name}(${selection}):${target.targetRoot};${target.detected ? '已发现目录' : '未发现目录'};${target.installed ? (target.installed.upToDate ? '指南已更新' : '指南需检查') : '未安装指南'}`);
3128
+ if (sub === 'status')
3129
+ printStatus(target);
3130
+ }
3131
+ }
3132
+ return;
3133
+ }
3062
3134
  if (sub === 'install') {
3063
- if (!targetRoot)
3064
- fail('用法: qmuse agent-skill install --agent codex [--target <dir>] [--force] [--json]');
3135
+ if (!targetRoot) {
3136
+ const detected = listSkillTargets().filter(target => target.detected);
3137
+ if (detected.length === 0)
3138
+ fail('未发现支持的工具目录。请执行 qmuse agent-skill list,再用 --agent <工具> 或 --target <skills 目录> 指定安装位置。');
3139
+ let chosen = detected[0];
3140
+ if (detected.length > 1) {
3141
+ const choices = detected.map((target, index) => `${index + 1}. ${target.name}:${target.targetRoot}`).join('\n');
3142
+ if (process.stdin.isTTY !== true || flags['--json'] === true) {
3143
+ fail(`发现多个工具目录,请用 --agent <工具> 或 --target <skills 目录> 选择当前工具:\n${choices}`);
3144
+ }
3145
+ const answer = Number(await prompt(`选择当前工具:\n${choices}\n请输入编号:`));
3146
+ if (!Number.isInteger(answer) || answer < 1 || answer > detected.length)
3147
+ fail('无效编号,未安装指南。');
3148
+ chosen = detected[answer - 1];
3149
+ }
3150
+ targetRoot = chosen.targetRoot;
3151
+ agent = chosen.agent;
3152
+ }
3065
3153
  const result = installAgentSkill({
3066
3154
  targetRoot,
3067
3155
  agent: agent ?? 'custom',
3068
3156
  force: flags['--force'] === true,
3069
3157
  });
3158
+ rememberSkillTarget(targetRoot, agent ?? 'custom');
3070
3159
  // T01.11:安装事件(记录指南版本与目标助手类型,不记录本机私有路径)。
3071
3160
  recordEvent({ kind: 'cli.skill.event', stage: 'install', result: 'ok',
3072
3161
  agent: agent ?? 'custom', target: 'skill-content' });
@@ -3080,7 +3169,7 @@ async function cmdAgentSkill(command, flags) {
3080
3169
  }
3081
3170
  if (sub === 'status') {
3082
3171
  if (!targetRoot)
3083
- fail('用法: qmuse agent-skill status [--agent codex] [--target <dir>] [--json]');
3172
+ fail('用法: qmuse agent-skill status [--agent <工具>] [--target <skills 目录>] [--json]');
3084
3173
  const report = skillStatus(targetRoot);
3085
3174
  // T01.11:状态查询事件。
3086
3175
  recordEvent({ kind: 'cli.skill.event', stage: 'status', result: report.installed ? 'ok' : 'not-installed' });
@@ -3090,17 +3179,11 @@ async function cmdAgentSkill(command, flags) {
3090
3179
  }
3091
3180
  log.info(`包内指南版本:${report.packagedVersion}(${report.packagedAvailable ? `${report.packagedFiles} 个文件` : '缺失,请重装 CLI'})`);
3092
3181
  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
- }
3182
+ log.warn('当前目标未安装官方指南;用相同的 --agent 或 --target 执行 qmuse agent-skill install。');
3183
+ else
3184
+ printStatus(report);
3101
3185
  return;
3102
3186
  }
3103
- fail(`未知 agent-skill 子命令: ${sub ?? '(空)'}。可用: install / status(指南随 CLI 包内分发)`);
3104
3187
  }
3105
3188
  /** 帮助/版本等无命令上下文路径的结束事件(T01.01 覆盖帮助与版本输出)。 */ function recordCommandEvent(label, exitCode, startedAt) {
3106
3189
  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.12",
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 返回的状态、检查结果和地址为准;受理不等于发布成功。失败先查原发布任务的错误,再按诊断恢复。