@josephyan/qingflow-app-builder-mcp 0.2.0-beta.7 → 0.2.0-beta.71

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.
Files changed (70) hide show
  1. package/README.md +5 -3
  2. package/docs/local-agent-install.md +21 -5
  3. package/npm/bin/qingflow-app-builder-mcp.mjs +1 -1
  4. package/npm/lib/runtime.mjs +168 -12
  5. package/package.json +1 -1
  6. package/pyproject.toml +4 -1
  7. package/skills/qingflow-app-builder/SKILL.md +155 -22
  8. package/skills/qingflow-app-builder/references/create-app.md +51 -21
  9. package/skills/qingflow-app-builder/references/environments.md +1 -1
  10. package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
  11. package/skills/qingflow-app-builder/references/gotchas.md +28 -1
  12. package/skills/qingflow-app-builder/references/solution-playbooks.md +14 -12
  13. package/skills/qingflow-app-builder/references/tool-selection.md +47 -19
  14. package/skills/qingflow-app-builder/references/update-flow.md +112 -25
  15. package/skills/qingflow-app-builder/references/update-layout.md +11 -24
  16. package/skills/qingflow-app-builder/references/update-schema.md +1 -23
  17. package/skills/qingflow-app-builder/references/update-views.md +87 -21
  18. package/skills/qingflow-app-builder-code-integrations/SKILL.md +137 -0
  19. package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +4 -0
  20. package/skills/qingflow-app-builder-code-integrations/references/code-block.md +66 -0
  21. package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +77 -0
  22. package/src/qingflow_mcp/__init__.py +1 -1
  23. package/src/qingflow_mcp/backend_client.py +210 -0
  24. package/src/qingflow_mcp/builder_facade/models.py +1252 -3
  25. package/src/qingflow_mcp/builder_facade/service.py +11367 -2389
  26. package/src/qingflow_mcp/cli/__init__.py +1 -0
  27. package/src/qingflow_mcp/cli/commands/__init__.py +15 -0
  28. package/src/qingflow_mcp/cli/commands/app.py +40 -0
  29. package/src/qingflow_mcp/cli/commands/auth.py +78 -0
  30. package/src/qingflow_mcp/cli/commands/builder.py +515 -0
  31. package/src/qingflow_mcp/cli/commands/common.py +62 -0
  32. package/src/qingflow_mcp/cli/commands/imports.py +96 -0
  33. package/src/qingflow_mcp/cli/commands/record.py +304 -0
  34. package/src/qingflow_mcp/cli/commands/task.py +89 -0
  35. package/src/qingflow_mcp/cli/commands/workspace.py +33 -0
  36. package/src/qingflow_mcp/cli/context.py +48 -0
  37. package/src/qingflow_mcp/cli/formatters.py +355 -0
  38. package/src/qingflow_mcp/cli/json_io.py +50 -0
  39. package/src/qingflow_mcp/cli/main.py +149 -0
  40. package/src/qingflow_mcp/config.py +39 -0
  41. package/src/qingflow_mcp/import_store.py +121 -0
  42. package/src/qingflow_mcp/list_type_labels.py +24 -0
  43. package/src/qingflow_mcp/response_trim.py +668 -0
  44. package/src/qingflow_mcp/server.py +160 -18
  45. package/src/qingflow_mcp/server_app_builder.py +275 -68
  46. package/src/qingflow_mcp/server_app_user.py +219 -191
  47. package/src/qingflow_mcp/session_store.py +41 -1
  48. package/src/qingflow_mcp/solution/compiler/form_compiler.py +43 -4
  49. package/src/qingflow_mcp/solution/compiler/icon_utils.py +119 -45
  50. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +41 -2
  51. package/src/qingflow_mcp/solution/executor.py +107 -11
  52. package/src/qingflow_mcp/solution/spec_models.py +2 -0
  53. package/src/qingflow_mcp/tools/ai_builder_tools.py +2032 -127
  54. package/src/qingflow_mcp/tools/app_tools.py +419 -12
  55. package/src/qingflow_mcp/tools/approval_tools.py +571 -72
  56. package/src/qingflow_mcp/tools/auth_tools.py +398 -2
  57. package/src/qingflow_mcp/tools/code_block_tools.py +756 -0
  58. package/src/qingflow_mcp/tools/custom_button_tools.py +179 -0
  59. package/src/qingflow_mcp/tools/directory_tools.py +203 -31
  60. package/src/qingflow_mcp/tools/feedback_tools.py +230 -0
  61. package/src/qingflow_mcp/tools/file_tools.py +1 -0
  62. package/src/qingflow_mcp/tools/import_tools.py +2150 -0
  63. package/src/qingflow_mcp/tools/package_tools.py +18 -4
  64. package/src/qingflow_mcp/tools/portal_tools.py +31 -0
  65. package/src/qingflow_mcp/tools/qingbi_report_tools.py +109 -7
  66. package/src/qingflow_mcp/tools/record_tools.py +9894 -1104
  67. package/src/qingflow_mcp/tools/solution_tools.py +115 -3
  68. package/src/qingflow_mcp/tools/task_context_tools.py +2040 -0
  69. package/src/qingflow_mcp/tools/task_tools.py +376 -225
  70. package/src/qingflow_mcp/tools/workspace_tools.py +163 -19
@@ -105,21 +105,27 @@ class WorkspaceTools(ToolBase):
105
105
  result = self.backend.request("GET", call_context, f"/user/workspace/{ws_id}")
106
106
  except QingflowApiError as e:
107
107
  if e.http_status == 404:
108
- # Fallback: try to find the workspace name from the list
109
- try:
110
- workspaces_data = self.backend.request("POST", call_context, "/user/workspaceList/pageQuery", json_body={"pageNum": 1, "pageSize": 100, "authList": [0, 1, 2]})
111
- workspaces = workspaces_data.get("list", []) if isinstance(workspaces_data, dict) else []
112
- found = next((ws for ws in workspaces if ws.get("wsId") == ws_id), None)
113
- if found:
114
- result = found
115
- else:
116
- result = {"wsId": ws_id, "workspaceName": f"Workspace {ws_id}"}
117
- except Exception:
118
- result = {"wsId": ws_id, "workspaceName": f"Workspace {ws_id}"}
108
+ result = self._workspace_from_list(call_context, ws_id) or {"wsId": ws_id, "workspaceName": f"Workspace {ws_id}"}
119
109
  else:
120
110
  raise
121
111
 
122
- ws_name = result.get("workspaceName") or result.get("wsName") if isinstance(result, dict) else None
112
+ if isinstance(result, dict):
113
+ ws_name = (
114
+ str(result.get("workspaceName") or result.get("wsName") or "").strip()
115
+ or None
116
+ )
117
+ if ws_name is None:
118
+ fallback_workspace = self._workspace_from_list(call_context, ws_id)
119
+ if isinstance(fallback_workspace, dict):
120
+ result = fallback_workspace
121
+ ws_name = (
122
+ str(result.get("workspaceName") or result.get("wsName") or "").strip()
123
+ or None
124
+ )
125
+ else:
126
+ ws_name = None
127
+ if ws_name is None:
128
+ ws_name = f"Workspace {ws_id}"
123
129
  session_profile = self.sessions.select_workspace(profile, ws_id=ws_id, ws_name=ws_name)
124
130
  workspace_qf_version = self._workspace_system_version(result)
125
131
  should_refresh_route = (
@@ -133,26 +139,54 @@ class WorkspaceTools(ToolBase):
133
139
  qf_version=workspace_qf_version,
134
140
  qf_version_source="workspace_system_version",
135
141
  )
142
+ active_context = BackendRequestContext(
143
+ base_url=session_profile.base_url,
144
+ token=context.token,
145
+ ws_id=session_profile.selected_ws_id,
146
+ qf_version=session_profile.qf_version,
147
+ qf_version_source=session_profile.qf_version_source,
148
+ )
149
+ workspace_version = self._workspace_version_summary(
150
+ self._fetch_workspace_account_info(active_context),
151
+ workspace_base_info=self._fetch_workspace_base_info(active_context),
152
+ workspace_detail=result,
153
+ )
136
154
  return {
137
155
  "profile": profile,
138
156
  "selected_ws_id": session_profile.selected_ws_id,
139
157
  "selected_ws_name": session_profile.selected_ws_name,
140
158
  "workspace": result,
159
+ "workspace_version": workspace_version,
141
160
  "qf_version": session_profile.qf_version,
142
161
  "qf_version_source": session_profile.qf_version_source,
143
162
  "request_route": self.backend.describe_route(
144
- BackendRequestContext(
145
- base_url=session_profile.base_url,
146
- token=context.token,
147
- ws_id=session_profile.selected_ws_id,
148
- qf_version=session_profile.qf_version,
149
- qf_version_source=session_profile.qf_version_source,
150
- )
163
+ active_context
151
164
  ),
152
165
  }
153
166
 
154
167
  return self._run(profile, runner, require_workspace=False)
155
168
 
169
+ def _workspace_from_list(self, context: BackendRequestContext, ws_id: int) -> dict[str, Any] | None:
170
+ try:
171
+ payload = self.backend.request(
172
+ "POST",
173
+ context,
174
+ "/user/workspaceList/pageQuery",
175
+ json_body={"pageNum": 1, "pageSize": 100, "authList": [0, 1, 2]},
176
+ )
177
+ except Exception:
178
+ return None
179
+ workspaces = payload.get("list", []) if isinstance(payload, dict) else []
180
+ found = next(
181
+ (
182
+ item
183
+ for item in workspaces
184
+ if isinstance(item, dict) and item.get("wsId") == ws_id
185
+ ),
186
+ None,
187
+ )
188
+ return found if isinstance(found, dict) else None
189
+
156
190
  def _workspace_system_version(self, workspace: Any) -> str | None:
157
191
  if not isinstance(workspace, dict):
158
192
  return None
@@ -162,6 +196,116 @@ class WorkspaceTools(ToolBase):
162
196
  normalized = str(value).strip()
163
197
  return normalized or None
164
198
 
199
+ def _fetch_workspace_base_info(self, context: BackendRequestContext) -> dict[str, Any] | None:
200
+ try:
201
+ payload = self.backend.request("GET", context, "/ws/baseInfo")
202
+ except QingflowApiError:
203
+ return None
204
+ return payload if isinstance(payload, dict) else None
205
+
206
+ def _fetch_workspace_account_info(self, context: BackendRequestContext) -> dict[str, Any] | None:
207
+ try:
208
+ payload = self.backend.request("GET", context, "/ws/account")
209
+ except QingflowApiError:
210
+ return None
211
+ return payload if isinstance(payload, dict) else None
212
+
213
+ def _workspace_version_summary(
214
+ self,
215
+ payload: Any,
216
+ *,
217
+ workspace_base_info: Any,
218
+ workspace_detail: Any,
219
+ ) -> dict[str, Any]:
220
+ account_info = payload if isinstance(payload, dict) else {}
221
+ base_info = workspace_base_info if isinstance(workspace_base_info, dict) else {}
222
+ detail_info = workspace_detail if isinstance(workspace_detail, dict) else {}
223
+ level_code = self._first_present_int(
224
+ account_info.get("accountLevel"),
225
+ base_info.get("accountLevel"),
226
+ detail_info.get("accountLevel"),
227
+ )
228
+ level_name = self._account_level_name(level_code)
229
+ return {
230
+ "level_code": level_code,
231
+ "level_name": level_name,
232
+ "display_name": self._account_level_display_name(level_name),
233
+ "being_trial": self._first_present_bool(
234
+ account_info.get("trial"),
235
+ base_info.get("trial"),
236
+ detail_info.get("trial"),
237
+ ),
238
+ "expire_date": self._first_present_value(
239
+ account_info.get("expireDate"),
240
+ base_info.get("expireDate"),
241
+ detail_info.get("expireDate"),
242
+ ),
243
+ }
244
+
245
+ def _account_level_name(self, level_code: int | None) -> str | None:
246
+ mapping = {
247
+ 0: "FREE",
248
+ 10: "AIR",
249
+ 20: "BASIC",
250
+ 30: "TEAM",
251
+ 35: "PROFESSIONAL",
252
+ 40: "ENTERPRISE",
253
+ }
254
+ return mapping.get(level_code)
255
+
256
+ def _account_level_display_name(self, level_name: str | None) -> str | None:
257
+ mapping = {
258
+ "FREE": "免费版",
259
+ "AIR": "Air版",
260
+ "BASIC": "Pro版",
261
+ "TEAM": "团队版",
262
+ "PROFESSIONAL": "专业版",
263
+ "ENTERPRISE": "企业版",
264
+ }
265
+ return mapping.get(level_name)
266
+
267
+ def _coerce_int(self, value: Any) -> int | None:
268
+ if isinstance(value, bool) or value is None:
269
+ return None
270
+ if isinstance(value, int):
271
+ return value
272
+ try:
273
+ return int(str(value).strip())
274
+ except (TypeError, ValueError):
275
+ return None
276
+
277
+ def _coerce_bool(self, value: Any) -> bool | None:
278
+ if isinstance(value, bool):
279
+ return value
280
+ if value is None:
281
+ return None
282
+ normalized = str(value).strip().lower()
283
+ if normalized in {"true", "1"}:
284
+ return True
285
+ if normalized in {"false", "0"}:
286
+ return False
287
+ return None
288
+
289
+ def _first_present_int(self, *values: Any) -> int | None:
290
+ for value in values:
291
+ coerced = self._coerce_int(value)
292
+ if coerced is not None:
293
+ return coerced
294
+ return None
295
+
296
+ def _first_present_bool(self, *values: Any) -> bool | None:
297
+ for value in values:
298
+ coerced = self._coerce_bool(value)
299
+ if coerced is not None:
300
+ return coerced
301
+ return None
302
+
303
+ def _first_present_value(self, *values: Any) -> Any:
304
+ for value in values:
305
+ if value is not None:
306
+ return value
307
+ return None
308
+
165
309
  def workspace_set_plugin_status(
166
310
  self,
167
311
  *,