@qmuse/qmuse-cli 1.0.10 → 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 +14 -0
- package/dist/agent-skill.js +118 -24
- package/dist/api.js +2 -2
- package/dist/app/chat.js +4 -2
- package/dist/app/create.js +3 -1
- package/dist/cli.js +122 -39
- package/dist/logger.js +5 -2
- package/dist/tracker.js +19 -18
- package/dist/version-gate.js +6 -11
- package/package.json +1 -2
- package/skills/qmuse-cli/SKILL.md +22 -19
- package/skills/qmuse-cli/references/cloud.md +4 -4
- package/skills/qmuse-cli/references/core-flows.md +48 -57
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
|
package/dist/agent-skill.js
CHANGED
|
@@ -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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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 (!
|
|
141
|
+
if (!present.has(relative)) {
|
|
142
|
+
if (digest)
|
|
143
|
+
missingFiles.push(relative);
|
|
88
144
|
continue;
|
|
145
|
+
}
|
|
89
146
|
try {
|
|
90
|
-
|
|
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
|
-
|
|
96
|
-
report.installed =
|
|
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:
|
|
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
|
|
175
|
-
//
|
|
176
|
-
for (const staleFile of
|
|
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 ??
|
|
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.
|
|
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.
|
|
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;
|
package/dist/app/create.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
2296
|
-
fail('用法: qmuse db <子命令> --app-id <appId>
|
|
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',
|
|
2667
|
-
: ['install', '-g',
|
|
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 =
|
|
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
|
|
2802
|
-
agent-skill
|
|
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
|
-
|
|
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
|
-
|
|
3060
|
-
|
|
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
|
-
|
|
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
|
|
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('
|
|
3094
|
-
else
|
|
3095
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
80
|
+
const r = await whoami(cfg, { signal, timeoutMs: TRACKING_TIMEOUT_MS });
|
|
81
|
+
return r.data?.userId?.trim() || '';
|
|
83
82
|
}
|
|
84
83
|
catch {
|
|
85
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
|
126
|
-
}).catch(() => {
|
|
127
|
-
/* 采集失败不影响业务 */
|
|
125
|
+
signal,
|
|
128
126
|
});
|
|
127
|
+
await response.body?.cancel();
|
|
128
|
+
})().catch(() => {
|
|
129
|
+
/* 埋点失败或超时不影响业务 */
|
|
129
130
|
});
|
|
130
131
|
}
|
package/dist/version-gate.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
}
|
|
@@ -130,7 +123,9 @@ export function envPolicyProvider(json = process.env.QMUSE_VERSION_POLICY) {
|
|
|
130
123
|
}
|
|
131
124
|
};
|
|
132
125
|
}
|
|
133
|
-
|
|
126
|
+
// 版本策略缓存有效期:兼顾「每条命令不重复打 registry」与「新版本能较快触达用户」。
|
|
127
|
+
// 1 小时可在发版后一小时内覆盖绝大多数活跃用户;6 小时会让发版当天的大量用户错过提示。
|
|
128
|
+
const POLICY_CACHE_TTL_MS = 60 * 60 * 1000;
|
|
134
129
|
/** 缓存目录:QMUSE_CLI_CACHE_DIR 可覆盖(测试与多环境隔离)。 */
|
|
135
130
|
function policyCacheDir() {
|
|
136
131
|
return process.env.QMUSE_CLI_CACHE_DIR ?? join(homedir(), '.cache', 'qmuse-cli');
|
|
@@ -165,8 +160,8 @@ export function registryPolicyProvider(options = {}) {
|
|
|
165
160
|
const url = options.url ?? policyRegistryUrl(distribution);
|
|
166
161
|
const timeoutMs = options.timeoutMs ?? 2500;
|
|
167
162
|
return async () => {
|
|
168
|
-
//
|
|
169
|
-
const cached = readPolicyCache(distribution);
|
|
163
|
+
// 普通命令复用一小时缓存,主动升级直接查询 registry。
|
|
164
|
+
const cached = options.refresh ? undefined : readPolicyCache(distribution);
|
|
170
165
|
if (cached)
|
|
171
166
|
return cached;
|
|
172
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.
|
|
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
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
|
18
|
-
|
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
#
|
|
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
|
|
25
|
-
| 云函数 | `qmuse fn list
|
|
26
|
-
| 秘密(安全输入) | `qmuse cloud set-secret K1,K2 --stdin`;查询 `qmuse secret list` / `secret sync status
|
|
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
|
-
#
|
|
1
|
+
# 核心流程参考
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## 身份与应用
|
|
4
4
|
|
|
5
|
-
|
|
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
|
|
15
|
-
qmuse whoami
|
|
16
|
-
qmuse
|
|
17
|
-
qmuse
|
|
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
|
-
|
|
21
|
-
断流恢复顺序:`chat state` → `chat history` → 显式 `chat cancel`(如需停止)。
|
|
19
|
+
创建结果中的 appId、chatId、sprintId 用于后续命令。相同 `--request-id` 重发会恢复已有身份(REUSED),不重新执行首轮生成。生成退出码为 0(完成)、1(失败)、2(等待输入)或 130(取消);等待输入时查询状态并回答原会话。
|
|
22
20
|
|
|
23
|
-
##
|
|
21
|
+
## 初始化与导入
|
|
24
22
|
|
|
25
23
|
```bash
|
|
26
|
-
qmuse create my-app
|
|
27
|
-
qmuse import /path/to/react-app
|
|
28
|
-
qmuse import .
|
|
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
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
31
|
+
- 云模式:AUTO(默认)、EXTERNAL(外部云,传公开配置)或 QMUSE(传 `--confirm-cloud-service`)。
|
|
32
|
+
- 外部数据不会自动迁移;选择切换时,用 `--acknowledge-empty-cloud-data` 确认。
|
|
33
|
+
- 失败时读取 `.qmuse/last-import-error.json` 的阶段、诊断和恢复建议。
|
|
36
34
|
|
|
37
|
-
##
|
|
35
|
+
## 本地开发与源码同步
|
|
38
36
|
|
|
39
37
|
```bash
|
|
40
|
-
qmuse dev
|
|
41
|
-
qmuse dev status
|
|
42
|
-
qmuse dev stop
|
|
43
|
-
qmuse pull
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
## 4. 对话恢复
|
|
47
|
+
`pull` 要求本地源码与上次导入或拉取的基线一致;有新增、修改、删除时会停止,Git 提交不会改变同步基线。成功更新前会将原源码和绑定存入返回的 backupDirectory,再应用远端源码。上传改动用 `import`;下载用 `pull` 或 `code export`。未保存的远端对话改动不会被拉取。
|
|
49
48
|
|
|
50
|
-
|
|
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
|
-
|
|
61
|
-
重复创建请求只恢复身份(REUSED),不重跑首轮生成。
|
|
62
|
-
|
|
63
|
-
## 5. 源码同步
|
|
51
|
+
## 对话与恢复
|
|
64
52
|
|
|
65
53
|
```bash
|
|
66
|
-
qmuse
|
|
67
|
-
qmuse
|
|
68
|
-
qmuse
|
|
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
|
-
|
|
62
|
+
send 的 appId 和 chatId 必须显式传入;history 的 appId 和 sprintId 也必须显式传入。断流后先查 state 和 history;等待输入时,沿用原 chatId 和返回的 interruptId,用 JSON 文件回答。需要流式结构化事件时用 `--ndjson`。
|
|
72
63
|
|
|
73
|
-
##
|
|
64
|
+
## 云数据
|
|
74
65
|
|
|
75
66
|
```bash
|
|
76
|
-
qmuse data-source list
|
|
67
|
+
qmuse data-source list --app-id <id>
|
|
77
68
|
qmuse db tables --app-id <id>
|
|
78
|
-
qmuse db records list
|
|
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
|
-
|
|
73
|
+
表列表不需要 formId;记录读写和表权限查询需要 formId。云架构、函数和密钥命令见 [云服务指南](cloud.md)。
|
|
83
74
|
|
|
84
|
-
##
|
|
75
|
+
## 发布与恢复
|
|
85
76
|
|
|
86
77
|
```bash
|
|
87
|
-
qmuse deploy --version <versionId>
|
|
88
|
-
qmuse release status <releaseId>
|
|
89
|
-
qmuse release list --app-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
|
-
|
|
93
|
-
账户与成本观测:`qmuse account`(余额/权益/消耗摘要/订阅状态只读)。
|
|
84
|
+
按用户指定版本发布,结果以 release status 返回的状态、检查结果和地址为准;受理不等于发布成功。失败先查原发布任务的错误,再按诊断恢复。
|