@josephyan/qingflow-cli 0.2.0-beta.58 → 0.2.0-beta.59
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 +3 -2
- package/docs/local-agent-install.md +9 -0
- package/npm/bin/qingflow.mjs +1 -1
- package/npm/lib/runtime.mjs +156 -21
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/qingflow_mcp/builder_facade/service.py +137 -5
- package/src/qingflow_mcp/cli/commands/app.py +16 -16
- package/src/qingflow_mcp/cli/commands/auth.py +19 -16
- package/src/qingflow_mcp/cli/commands/builder.py +124 -162
- package/src/qingflow_mcp/cli/commands/common.py +21 -95
- package/src/qingflow_mcp/cli/commands/imports.py +42 -34
- package/src/qingflow_mcp/cli/commands/record.py +131 -133
- package/src/qingflow_mcp/cli/commands/task.py +43 -44
- package/src/qingflow_mcp/cli/commands/workspace.py +10 -10
- package/src/qingflow_mcp/cli/context.py +35 -32
- package/src/qingflow_mcp/cli/formatters.py +124 -121
- package/src/qingflow_mcp/cli/main.py +52 -17
- package/src/qingflow_mcp/server_app_builder.py +122 -190
- package/src/qingflow_mcp/server_app_user.py +63 -662
- package/src/qingflow_mcp/tools/solution_tools.py +95 -3
- package/src/qingflow_mcp/ops/__init__.py +0 -3
- package/src/qingflow_mcp/ops/apps.py +0 -64
- package/src/qingflow_mcp/ops/auth.py +0 -121
- package/src/qingflow_mcp/ops/base.py +0 -290
- package/src/qingflow_mcp/ops/builder.py +0 -357
- package/src/qingflow_mcp/ops/context.py +0 -120
- package/src/qingflow_mcp/ops/directory.py +0 -171
- package/src/qingflow_mcp/ops/feedback.py +0 -49
- package/src/qingflow_mcp/ops/files.py +0 -78
- package/src/qingflow_mcp/ops/imports.py +0 -140
- package/src/qingflow_mcp/ops/records.py +0 -415
- package/src/qingflow_mcp/ops/tasks.py +0 -171
- package/src/qingflow_mcp/ops/workspace.py +0 -76
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @josephyan/qingflow-cli@0.2.0-beta.
|
|
6
|
+
npm install @josephyan/qingflow-cli@0.2.0-beta.59
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @josephyan/qingflow-cli@0.2.0-beta.
|
|
12
|
+
npx -y -p @josephyan/qingflow-cli@0.2.0-beta.59 qingflow
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
|
@@ -28,3 +28,4 @@ Note:
|
|
|
28
28
|
|
|
29
29
|
- The skill files are included in the npm package.
|
|
30
30
|
- On install, the package copies them to `$CODEX_HOME/skills` (or `~/.codex/skills` if `CODEX_HOME` is unset).
|
|
31
|
+
- If a stdio MCP client reports `Transport closed`, delete `.npm-python`, reinstall the package, and make sure CLI/user/builder packages are on the same version. The stdio entrypoints refuse runtime bootstrap so install logs never corrupt MCP stdout.
|
|
@@ -233,3 +233,12 @@ rm -rf .npm-python
|
|
|
233
233
|
```bash
|
|
234
234
|
npm install
|
|
235
235
|
```
|
|
236
|
+
|
|
237
|
+
如果 MCP 客户端一调用工具就报 `Transport closed`,优先检查这几件事:
|
|
238
|
+
|
|
239
|
+
1. 不要混用不同版本的 `@josephyan/qingflow-cli`、`@josephyan/qingflow-app-user-mcp`、`@josephyan/qingflow-app-builder-mcp`
|
|
240
|
+
2. 删除安装目录下的 `.npm-python`
|
|
241
|
+
3. 重新执行 `npm install` 或重新安装对应 tgz/npm 包
|
|
242
|
+
4. 再启动 MCP 客户端
|
|
243
|
+
|
|
244
|
+
现在 stdio MCP 入口会拒绝在启动瞬间“边启动边重建 Python 运行时”,因为安装日志一旦写进 stdout,就会破坏 MCP 握手并表现成 `Transport closed`。如果运行时缺失或版本不一致,入口会直接报错并提示重装,而不是静默自修复。
|
package/npm/bin/qingflow.mjs
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
import { spawnServer, getPackageRoot } from "../lib/runtime.mjs";
|
|
3
3
|
|
|
4
4
|
const packageRoot = getPackageRoot(import.meta.url);
|
|
5
|
-
spawnServer(packageRoot, process.argv.slice(2), "qingflow");
|
|
5
|
+
spawnServer(packageRoot, process.argv.slice(2), "qingflow", { allowRuntimeBootstrap: true });
|
package/npm/lib/runtime.mjs
CHANGED
|
@@ -7,14 +7,16 @@ const WINDOWS = process.platform === "win32";
|
|
|
7
7
|
|
|
8
8
|
function runChecked(command, args, options = {}) {
|
|
9
9
|
const result = spawnSync(command, args, {
|
|
10
|
-
|
|
10
|
+
encoding: "utf8",
|
|
11
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
11
12
|
...options,
|
|
12
13
|
});
|
|
13
14
|
if (result.error) {
|
|
14
15
|
throw result.error;
|
|
15
16
|
}
|
|
16
17
|
if (result.status !== 0) {
|
|
17
|
-
|
|
18
|
+
const details = [result.stderr, result.stdout].filter((value) => typeof value === "string" && value.trim()).join("\n");
|
|
19
|
+
throw new Error(details ? `Command failed: ${command} ${args.join(" ")}\n${details}` : `Command failed: ${command} ${args.join(" ")}`);
|
|
18
20
|
}
|
|
19
21
|
}
|
|
20
22
|
|
|
@@ -92,6 +94,95 @@ function readPackageVersion(packageRoot) {
|
|
|
92
94
|
}
|
|
93
95
|
}
|
|
94
96
|
|
|
97
|
+
function readBootstrapStamp(stampPath) {
|
|
98
|
+
if (!fs.existsSync(stampPath)) {
|
|
99
|
+
return { exists: false, version: null };
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const payload = JSON.parse(fs.readFileSync(stampPath, "utf8"));
|
|
103
|
+
const version = typeof payload.package_version === "string" && payload.package_version.trim() ? payload.package_version.trim() : null;
|
|
104
|
+
return { exists: true, version };
|
|
105
|
+
} catch {
|
|
106
|
+
return { exists: true, version: null };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function inspectPythonEnv(packageRoot, commandName = "qingflow-mcp") {
|
|
111
|
+
const venvDir = getVenvDir(packageRoot);
|
|
112
|
+
const serverCommand = getVenvServerCommand(packageRoot, commandName);
|
|
113
|
+
const stampPath = path.join(venvDir, ".bootstrap.json");
|
|
114
|
+
const packageVersion = readPackageVersion(packageRoot);
|
|
115
|
+
const stamp = readBootstrapStamp(stampPath);
|
|
116
|
+
const problems = [];
|
|
117
|
+
|
|
118
|
+
if (!packageVersion) {
|
|
119
|
+
problems.push("package-version-missing");
|
|
120
|
+
}
|
|
121
|
+
if (!fs.existsSync(serverCommand)) {
|
|
122
|
+
problems.push("server-command-missing");
|
|
123
|
+
}
|
|
124
|
+
if (!stamp.exists) {
|
|
125
|
+
problems.push("bootstrap-stamp-missing");
|
|
126
|
+
} else if (!stamp.version) {
|
|
127
|
+
problems.push("bootstrap-stamp-invalid");
|
|
128
|
+
} else if (packageVersion && stamp.version !== packageVersion) {
|
|
129
|
+
problems.push("bootstrap-version-mismatch");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
ready: problems.length === 0,
|
|
134
|
+
packageVersion,
|
|
135
|
+
stampVersion: stamp.version,
|
|
136
|
+
stampExists: stamp.exists,
|
|
137
|
+
stampPath,
|
|
138
|
+
serverCommand,
|
|
139
|
+
venvDir,
|
|
140
|
+
problems,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function formatRuntimeProblem(runtime, commandName, { allowRuntimeBootstrap = false } = {}) {
|
|
145
|
+
const problemLines = [];
|
|
146
|
+
for (const problem of runtime.problems) {
|
|
147
|
+
switch (problem) {
|
|
148
|
+
case "package-version-missing":
|
|
149
|
+
problemLines.push("- package.json is missing a valid version field");
|
|
150
|
+
break;
|
|
151
|
+
case "server-command-missing":
|
|
152
|
+
problemLines.push(`- missing Python entrypoint: ${runtime.serverCommand}`);
|
|
153
|
+
break;
|
|
154
|
+
case "bootstrap-stamp-missing":
|
|
155
|
+
problemLines.push(`- missing bootstrap stamp: ${runtime.stampPath}`);
|
|
156
|
+
break;
|
|
157
|
+
case "bootstrap-stamp-invalid":
|
|
158
|
+
problemLines.push(`- bootstrap stamp is unreadable or invalid: ${runtime.stampPath}`);
|
|
159
|
+
break;
|
|
160
|
+
case "bootstrap-version-mismatch":
|
|
161
|
+
problemLines.push(
|
|
162
|
+
`- bootstrap version mismatch: package=${runtime.packageVersion ?? "unknown"}, runtime=${runtime.stampVersion ?? "unknown"}`
|
|
163
|
+
);
|
|
164
|
+
break;
|
|
165
|
+
default:
|
|
166
|
+
problemLines.push(`- ${problem}`);
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const action = allowRuntimeBootstrap
|
|
172
|
+
? "Delete .npm-python and retry, or rerun npm install to rebuild the embedded Python runtime."
|
|
173
|
+
: "Delete .npm-python and rerun npm install, or reinstall the npm package before starting the MCP server.";
|
|
174
|
+
|
|
175
|
+
const bootstrapNote = allowRuntimeBootstrap
|
|
176
|
+
? ""
|
|
177
|
+
: "\nRuntime bootstrap is disabled for stdio MCP entrypoints so install logs can never corrupt the MCP stdout transport.";
|
|
178
|
+
|
|
179
|
+
return [
|
|
180
|
+
`[qingflow-mcp] Python runtime for ${commandName} is not ready.`,
|
|
181
|
+
...problemLines,
|
|
182
|
+
action + bootstrapNote,
|
|
183
|
+
].join("\n");
|
|
184
|
+
}
|
|
185
|
+
|
|
95
186
|
function getVenvPip(packageRoot) {
|
|
96
187
|
return WINDOWS
|
|
97
188
|
? path.join(getVenvDir(packageRoot), "Scripts", "pip.exe")
|
|
@@ -129,20 +220,9 @@ export function findPython() {
|
|
|
129
220
|
|
|
130
221
|
export function ensurePythonEnv(packageRoot, { force = false, commandName = "qingflow-mcp" } = {}) {
|
|
131
222
|
const python = findPython();
|
|
132
|
-
const
|
|
223
|
+
const runtime = inspectPythonEnv(packageRoot, commandName);
|
|
133
224
|
const venvPython = getVenvPython(packageRoot);
|
|
134
|
-
const serverCommand
|
|
135
|
-
const stampPath = path.join(venvDir, ".bootstrap.json");
|
|
136
|
-
const packageVersion = readPackageVersion(packageRoot);
|
|
137
|
-
let stampVersion = null;
|
|
138
|
-
if (fs.existsSync(stampPath)) {
|
|
139
|
-
try {
|
|
140
|
-
const payload = JSON.parse(fs.readFileSync(stampPath, "utf8"));
|
|
141
|
-
stampVersion = typeof payload.package_version === "string" && payload.package_version.trim() ? payload.package_version.trim() : null;
|
|
142
|
-
} catch {
|
|
143
|
-
stampVersion = null;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
225
|
+
const { packageVersion, serverCommand, stampPath, venvDir, stampVersion } = runtime;
|
|
146
226
|
|
|
147
227
|
if (!force && fs.existsSync(serverCommand) && fs.existsSync(stampPath) && stampVersion && stampVersion === packageVersion) {
|
|
148
228
|
return serverCommand;
|
|
@@ -179,17 +259,72 @@ export function ensurePythonEnv(packageRoot, { force = false, commandName = "qin
|
|
|
179
259
|
return serverCommand;
|
|
180
260
|
}
|
|
181
261
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
262
|
+
function proxyStreams(child) {
|
|
263
|
+
if (process.stdin.readable && child.stdin) {
|
|
264
|
+
process.stdin.pipe(child.stdin);
|
|
265
|
+
child.stdin.on("error", (error) => {
|
|
266
|
+
if (error.code !== "EPIPE") {
|
|
267
|
+
console.error(`[qingflow-mcp] Failed to forward stdin: ${error.message}`);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
} else if (child.stdin) {
|
|
271
|
+
child.stdin.end();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (child.stdout) {
|
|
275
|
+
child.stdout.pipe(process.stdout);
|
|
276
|
+
}
|
|
277
|
+
if (child.stderr) {
|
|
278
|
+
child.stderr.pipe(process.stderr);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function forwardSignal(child, signal) {
|
|
283
|
+
process.on(signal, () => {
|
|
284
|
+
if (!child.killed) {
|
|
285
|
+
child.kill(signal);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function spawnServer(packageRoot, args, commandName = "qingflow-mcp", { allowRuntimeBootstrap = false } = {}) {
|
|
291
|
+
let runtime = inspectPythonEnv(packageRoot, commandName);
|
|
292
|
+
let serverCommand = runtime.serverCommand;
|
|
293
|
+
|
|
294
|
+
if (!runtime.ready) {
|
|
295
|
+
if (!allowRuntimeBootstrap) {
|
|
296
|
+
console.error(formatRuntimeProblem(runtime, commandName, { allowRuntimeBootstrap }));
|
|
297
|
+
process.exit(1);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
serverCommand = ensurePythonEnv(packageRoot, { commandName });
|
|
303
|
+
runtime = inspectPythonEnv(packageRoot, commandName);
|
|
304
|
+
} catch (error) {
|
|
305
|
+
console.error(`[qingflow-mcp] Failed to prepare Python runtime for ${commandName}: ${error.message}`);
|
|
306
|
+
process.exit(1);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (!runtime.ready) {
|
|
311
|
+
console.error(formatRuntimeProblem(runtime, commandName, { allowRuntimeBootstrap }));
|
|
312
|
+
process.exit(1);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
186
316
|
|
|
187
317
|
const child = spawn(serverCommand, args, {
|
|
188
|
-
stdio: "
|
|
318
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
189
319
|
env: process.env,
|
|
320
|
+
windowsHide: true,
|
|
190
321
|
});
|
|
191
322
|
|
|
192
|
-
child
|
|
323
|
+
proxyStreams(child);
|
|
324
|
+
forwardSignal(child, "SIGINT");
|
|
325
|
+
forwardSignal(child, "SIGTERM");
|
|
326
|
+
|
|
327
|
+
child.on("close", (code, signal) => {
|
|
193
328
|
if (signal) {
|
|
194
329
|
process.kill(process.pid, signal);
|
|
195
330
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@josephyan/qingflow-cli",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.59",
|
|
4
4
|
"description": "Human-friendly Qingflow command line interface for auth, record operations, import, tasks, and stable builder flows.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/pyproject.toml
CHANGED
|
@@ -161,7 +161,18 @@ class AiBuilderFacade:
|
|
|
161
161
|
"package_name is required",
|
|
162
162
|
suggested_next_call=None,
|
|
163
163
|
)
|
|
164
|
-
|
|
164
|
+
normalized_args = {"package_name": requested}
|
|
165
|
+
try:
|
|
166
|
+
listing = self.packages.package_list(profile=profile, trial_status="all", include_raw=False)
|
|
167
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
168
|
+
api_error = _coerce_api_error(error)
|
|
169
|
+
return _failed_from_api_error(
|
|
170
|
+
"PACKAGE_RESOLVE_FAILED",
|
|
171
|
+
api_error,
|
|
172
|
+
normalized_args=normalized_args,
|
|
173
|
+
details={"package_name": requested},
|
|
174
|
+
suggested_next_call={"tool_name": "package_list", "arguments": {"profile": profile, "trial_status": "all"}},
|
|
175
|
+
)
|
|
165
176
|
items = listing.get("items") if isinstance(listing.get("items"), list) else []
|
|
166
177
|
matches = [
|
|
167
178
|
{"tag_id": item.get("tagId"), "tag_name": item.get("tagName")}
|
|
@@ -212,6 +223,7 @@ class AiBuilderFacade:
|
|
|
212
223
|
suggested_next_call=None,
|
|
213
224
|
)
|
|
214
225
|
existing = self.package_resolve(profile=profile, package_name=requested)
|
|
226
|
+
lookup_permission_blocked = None
|
|
215
227
|
if existing.get("status") == "success":
|
|
216
228
|
return {
|
|
217
229
|
"status": "success",
|
|
@@ -231,6 +243,16 @@ class AiBuilderFacade:
|
|
|
231
243
|
}
|
|
232
244
|
if existing.get("error_code") == "AMBIGUOUS_PACKAGE":
|
|
233
245
|
return existing
|
|
246
|
+
if existing.get("error_code") == "PACKAGE_RESOLVE_FAILED":
|
|
247
|
+
if existing.get("backend_code") not in {40002, 40027}:
|
|
248
|
+
return existing
|
|
249
|
+
lookup_permission_blocked = {
|
|
250
|
+
"backend_code": existing.get("backend_code"),
|
|
251
|
+
"http_status": existing.get("http_status"),
|
|
252
|
+
"request_id": existing.get("request_id"),
|
|
253
|
+
}
|
|
254
|
+
elif existing.get("error_code") not in {"PACKAGE_NOT_FOUND"}:
|
|
255
|
+
return existing
|
|
234
256
|
try:
|
|
235
257
|
created = self.packages.package_create(profile=profile, payload={"tagName": requested})
|
|
236
258
|
except (QingflowApiError, RuntimeError) as error:
|
|
@@ -239,7 +261,10 @@ class AiBuilderFacade:
|
|
|
239
261
|
"PACKAGE_CREATE_FAILED",
|
|
240
262
|
api_error,
|
|
241
263
|
normalized_args=normalized_args,
|
|
242
|
-
details={
|
|
264
|
+
details={
|
|
265
|
+
"package_name": requested,
|
|
266
|
+
**({"lookup_permission_blocked": lookup_permission_blocked} if lookup_permission_blocked is not None else {}),
|
|
267
|
+
},
|
|
243
268
|
suggested_next_call={"tool_name": "package_create", "arguments": {"profile": profile, "package_name": requested}},
|
|
244
269
|
)
|
|
245
270
|
result = created.get("result") if isinstance(created.get("result"), dict) else {}
|
|
@@ -259,7 +284,7 @@ class AiBuilderFacade:
|
|
|
259
284
|
"normalized_args": normalized_args,
|
|
260
285
|
"missing_fields": [],
|
|
261
286
|
"allowed_values": {},
|
|
262
|
-
"details": {},
|
|
287
|
+
"details": {"lookup_permission_blocked": lookup_permission_blocked} if lookup_permission_blocked is not None else {},
|
|
263
288
|
"request_id": None,
|
|
264
289
|
"suggested_next_call": None
|
|
265
290
|
if verified
|
|
@@ -1012,7 +1037,20 @@ class AiBuilderFacade:
|
|
|
1012
1037
|
details={"app_name": requested, "package_tag_id": package_tag_id, "matches": package_matches},
|
|
1013
1038
|
suggested_next_call=None,
|
|
1014
1039
|
)
|
|
1015
|
-
|
|
1040
|
+
search_error: QingflowApiError | None = None
|
|
1041
|
+
try:
|
|
1042
|
+
search = self.apps.app_search(profile=profile, keyword=requested, page_num=1, page_size=200)
|
|
1043
|
+
except (QingflowApiError, RuntimeError) as exc:
|
|
1044
|
+
api_error = _coerce_api_error(exc)
|
|
1045
|
+
if package_tag_id is None or package_tag_id <= 0 or api_error.backend_code not in {40002, 40027}:
|
|
1046
|
+
return _failed_from_api_error(
|
|
1047
|
+
"APP_NOT_FOUND" if api_error.http_status == 404 else "APP_RESOLVE_FAILED",
|
|
1048
|
+
api_error,
|
|
1049
|
+
details={"app_name": requested, "package_tag_id": package_tag_id},
|
|
1050
|
+
suggested_next_call=None,
|
|
1051
|
+
)
|
|
1052
|
+
search = {}
|
|
1053
|
+
search_error = api_error
|
|
1016
1054
|
apps = search.get("apps") if isinstance(search.get("apps"), list) else []
|
|
1017
1055
|
matches = []
|
|
1018
1056
|
for item in apps:
|
|
@@ -1046,11 +1084,72 @@ class AiBuilderFacade:
|
|
|
1046
1084
|
"tag_ids": tag_ids,
|
|
1047
1085
|
}
|
|
1048
1086
|
)
|
|
1087
|
+
if not matches and package_tag_id is not None and package_tag_id > 0 and search_error is not None:
|
|
1088
|
+
visible_matches = self._resolve_app_matches_in_visible_apps(
|
|
1089
|
+
profile=profile,
|
|
1090
|
+
app_name=requested,
|
|
1091
|
+
package_tag_id=package_tag_id,
|
|
1092
|
+
)
|
|
1093
|
+
if len(visible_matches) == 1:
|
|
1094
|
+
match = visible_matches[0]
|
|
1095
|
+
return {
|
|
1096
|
+
"status": "success",
|
|
1097
|
+
"error_code": None,
|
|
1098
|
+
"recoverable": False,
|
|
1099
|
+
"message": "resolved app",
|
|
1100
|
+
"normalized_args": {"app_name": requested, "package_tag_id": package_tag_id},
|
|
1101
|
+
"missing_fields": [],
|
|
1102
|
+
"allowed_values": {},
|
|
1103
|
+
"details": {
|
|
1104
|
+
"match_scope": "visible_apps",
|
|
1105
|
+
"search_permission_blocked": {
|
|
1106
|
+
"backend_code": search_error.backend_code,
|
|
1107
|
+
"http_status": search_error.http_status,
|
|
1108
|
+
"request_id": search_error.request_id,
|
|
1109
|
+
},
|
|
1110
|
+
},
|
|
1111
|
+
"request_id": None,
|
|
1112
|
+
"suggested_next_call": None,
|
|
1113
|
+
"noop": False,
|
|
1114
|
+
"verification": {},
|
|
1115
|
+
**match,
|
|
1116
|
+
}
|
|
1117
|
+
if len(visible_matches) > 1:
|
|
1118
|
+
return _failed(
|
|
1119
|
+
"AMBIGUOUS_APP",
|
|
1120
|
+
f"multiple apps matched '{requested}' inside package {package_tag_id}",
|
|
1121
|
+
details={
|
|
1122
|
+
"app_name": requested,
|
|
1123
|
+
"package_tag_id": package_tag_id,
|
|
1124
|
+
"matches": visible_matches,
|
|
1125
|
+
"search_permission_blocked": {
|
|
1126
|
+
"backend_code": search_error.backend_code,
|
|
1127
|
+
"http_status": search_error.http_status,
|
|
1128
|
+
"request_id": search_error.request_id,
|
|
1129
|
+
},
|
|
1130
|
+
},
|
|
1131
|
+
suggested_next_call=None,
|
|
1132
|
+
)
|
|
1049
1133
|
if not matches:
|
|
1050
1134
|
return _failed(
|
|
1051
1135
|
"APP_NOT_FOUND",
|
|
1052
1136
|
f"app '{requested}' was not found",
|
|
1053
|
-
details={
|
|
1137
|
+
details={
|
|
1138
|
+
"app_name": requested,
|
|
1139
|
+
"package_tag_id": package_tag_id,
|
|
1140
|
+
**(
|
|
1141
|
+
{
|
|
1142
|
+
"search_permission_blocked": {
|
|
1143
|
+
"backend_code": search_error.backend_code,
|
|
1144
|
+
"http_status": search_error.http_status,
|
|
1145
|
+
"request_id": search_error.request_id,
|
|
1146
|
+
},
|
|
1147
|
+
"match_scope": "visible_apps_fallback",
|
|
1148
|
+
}
|
|
1149
|
+
if search_error is not None
|
|
1150
|
+
else {}
|
|
1151
|
+
),
|
|
1152
|
+
},
|
|
1054
1153
|
suggested_next_call=None,
|
|
1055
1154
|
)
|
|
1056
1155
|
if len(matches) > 1:
|
|
@@ -1077,6 +1176,39 @@ class AiBuilderFacade:
|
|
|
1077
1176
|
**match,
|
|
1078
1177
|
}
|
|
1079
1178
|
|
|
1179
|
+
def _resolve_app_matches_in_visible_apps(
|
|
1180
|
+
self,
|
|
1181
|
+
*,
|
|
1182
|
+
profile: str,
|
|
1183
|
+
app_name: str,
|
|
1184
|
+
package_tag_id: int,
|
|
1185
|
+
) -> list[JSONObject]:
|
|
1186
|
+
try:
|
|
1187
|
+
listing = self.apps.app_list(profile=profile, ship_auth=False)
|
|
1188
|
+
except (QingflowApiError, RuntimeError):
|
|
1189
|
+
return []
|
|
1190
|
+
items = listing.get("items") if isinstance(listing.get("items"), list) else []
|
|
1191
|
+
matches: list[JSONObject] = []
|
|
1192
|
+
seen_app_keys: set[str] = set()
|
|
1193
|
+
for item in items:
|
|
1194
|
+
if not isinstance(item, dict):
|
|
1195
|
+
continue
|
|
1196
|
+
title = str(item.get("title") or item.get("app_name") or "").strip()
|
|
1197
|
+
if title != app_name:
|
|
1198
|
+
continue
|
|
1199
|
+
candidate_key = str(item.get("app_key") or item.get("appKey") or "").strip()
|
|
1200
|
+
if not candidate_key or candidate_key in seen_app_keys:
|
|
1201
|
+
continue
|
|
1202
|
+
tag_ids = _coerce_int_list(item.get("tag_ids"))
|
|
1203
|
+
tag_id = _coerce_positive_int(item.get("tag_id"))
|
|
1204
|
+
if tag_id is not None and tag_id not in tag_ids:
|
|
1205
|
+
tag_ids.append(tag_id)
|
|
1206
|
+
if package_tag_id not in tag_ids:
|
|
1207
|
+
continue
|
|
1208
|
+
seen_app_keys.add(candidate_key)
|
|
1209
|
+
matches.append({"app_key": candidate_key, "app_name": title, "tag_ids": tag_ids})
|
|
1210
|
+
return matches
|
|
1211
|
+
|
|
1080
1212
|
def _resolve_app_matches_in_package(
|
|
1081
1213
|
self,
|
|
1082
1214
|
*,
|
|
@@ -6,29 +6,29 @@ from ..context import CliContext
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
9
|
-
parser = subparsers.add_parser("
|
|
10
|
-
app_subparsers = parser.add_subparsers(dest="
|
|
9
|
+
parser = subparsers.add_parser("app", help="应用发现")
|
|
10
|
+
app_subparsers = parser.add_subparsers(dest="app_command", required=True)
|
|
11
11
|
|
|
12
12
|
list_parser = app_subparsers.add_parser("list", help="列出可见应用")
|
|
13
|
-
list_parser.set_defaults(handler=_handle_list, format_hint="
|
|
13
|
+
list_parser.set_defaults(handler=_handle_list, format_hint="app_list")
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
search = app_subparsers.add_parser("search", help="搜索应用")
|
|
16
|
+
search.add_argument("--keyword", default="")
|
|
17
|
+
search.add_argument("--page", type=int, default=1)
|
|
18
|
+
search.add_argument("--page-size", type=int, default=50)
|
|
19
|
+
search.set_defaults(handler=_handle_search, format_hint="app_search")
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
get = app_subparsers.add_parser("get", help="读取应用可访问视图与导入能力")
|
|
22
|
+
get.add_argument("--app-key", required=True)
|
|
23
|
+
get.set_defaults(handler=_handle_get, format_hint="app_get")
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
def _handle_list(args: argparse.Namespace, context: CliContext) -> dict:
|
|
27
|
-
return context.
|
|
27
|
+
return context.app.app_list(profile=args.profile)
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
def
|
|
31
|
-
return context.
|
|
30
|
+
def _handle_search(args: argparse.Namespace, context: CliContext) -> dict:
|
|
31
|
+
return context.app.app_search(
|
|
32
32
|
profile=args.profile,
|
|
33
33
|
keyword=args.keyword,
|
|
34
34
|
page_num=args.page,
|
|
@@ -36,5 +36,5 @@ def _handle_find(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
36
36
|
)
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
def
|
|
40
|
-
return context.
|
|
39
|
+
def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
40
|
+
return context.app.app_get(profile=args.profile, app_key=args.app_key)
|
|
@@ -10,34 +10,33 @@ from .common import read_secret_arg
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
parser = subparsers.add_parser("auth", help="认证与会话")
|
|
14
|
+
auth_subparsers = parser.add_subparsers(dest="auth_command", required=True)
|
|
15
15
|
|
|
16
|
-
login =
|
|
16
|
+
login = auth_subparsers.add_parser("login", help="邮箱密码登录")
|
|
17
17
|
login.add_argument("--base-url")
|
|
18
18
|
login.add_argument("--qf-version")
|
|
19
19
|
login.add_argument("--email", required=True)
|
|
20
20
|
login.add_argument("--password")
|
|
21
21
|
login.add_argument("--password-stdin", action="store_true")
|
|
22
22
|
login.add_argument("--persist", action=argparse.BooleanOptionalAction, default=True)
|
|
23
|
-
login.set_defaults(handler=_handle_login, format_hint="
|
|
23
|
+
login.set_defaults(handler=_handle_login, format_hint="auth_whoami")
|
|
24
24
|
|
|
25
|
-
use_token =
|
|
25
|
+
use_token = auth_subparsers.add_parser("use-token", help="直接注入 token")
|
|
26
26
|
use_token.add_argument("--base-url")
|
|
27
27
|
use_token.add_argument("--qf-version")
|
|
28
28
|
use_token.add_argument("--token")
|
|
29
29
|
use_token.add_argument("--token-stdin", action="store_true")
|
|
30
|
-
use_token.add_argument("--ws",
|
|
30
|
+
use_token.add_argument("--ws-id", type=int)
|
|
31
31
|
use_token.add_argument("--persist", action=argparse.BooleanOptionalAction, default=False)
|
|
32
|
-
use_token.set_defaults(handler=_handle_use_token, format_hint="
|
|
33
|
-
|
|
34
|
-
logout = subparsers.add_parser("logout", help="退出当前会话")
|
|
35
|
-
logout.add_argument("--forget-persisted", action="store_true")
|
|
36
|
-
logout.set_defaults(handler=_handle_logout, format_hint="session")
|
|
32
|
+
use_token.set_defaults(handler=_handle_use_token, format_hint="auth_whoami")
|
|
37
33
|
|
|
34
|
+
whoami = auth_subparsers.add_parser("whoami", help="查看当前登录态")
|
|
35
|
+
whoami.set_defaults(handler=_handle_whoami, format_hint="auth_whoami")
|
|
38
36
|
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
logout = auth_subparsers.add_parser("logout", help="退出登录")
|
|
38
|
+
logout.add_argument("--forget-persisted", action="store_true")
|
|
39
|
+
logout.set_defaults(handler=_handle_logout, format_hint="")
|
|
41
40
|
|
|
42
41
|
|
|
43
42
|
def _handle_login(args: argparse.Namespace, context: CliContext) -> dict:
|
|
@@ -49,7 +48,7 @@ def _handle_login(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
49
48
|
password = getpass.getpass("Password: ")
|
|
50
49
|
else:
|
|
51
50
|
raise QingflowApiError.config_error("password is required; use --password or --password-stdin")
|
|
52
|
-
return context.auth.
|
|
51
|
+
return context.auth.auth_login(
|
|
53
52
|
profile=args.profile,
|
|
54
53
|
base_url=args.base_url,
|
|
55
54
|
qf_version=args.qf_version,
|
|
@@ -61,7 +60,7 @@ def _handle_login(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
61
60
|
|
|
62
61
|
def _handle_use_token(args: argparse.Namespace, context: CliContext) -> dict:
|
|
63
62
|
token = read_secret_arg(args.token, stdin_enabled=bool(args.token_stdin), label="token")
|
|
64
|
-
return context.auth.
|
|
63
|
+
return context.auth.auth_use_token(
|
|
65
64
|
profile=args.profile,
|
|
66
65
|
base_url=args.base_url,
|
|
67
66
|
qf_version=args.qf_version,
|
|
@@ -71,5 +70,9 @@ def _handle_use_token(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
71
70
|
)
|
|
72
71
|
|
|
73
72
|
|
|
73
|
+
def _handle_whoami(args: argparse.Namespace, context: CliContext) -> dict:
|
|
74
|
+
return context.auth.auth_whoami(profile=args.profile)
|
|
75
|
+
|
|
76
|
+
|
|
74
77
|
def _handle_logout(args: argparse.Namespace, context: CliContext) -> dict:
|
|
75
|
-
return context.auth.
|
|
78
|
+
return context.auth.auth_logout(profile=args.profile, forget_persisted=bool(args.forget_persisted))
|