@josephyan/qingflow-cli 0.2.0-beta.1000

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 (92) hide show
  1. package/README.md +31 -0
  2. package/docs/local-agent-install.md +309 -0
  3. package/entry_point.py +13 -0
  4. package/npm/bin/qingflow.mjs +5 -0
  5. package/npm/lib/runtime.mjs +346 -0
  6. package/npm/scripts/postinstall.mjs +16 -0
  7. package/package.json +34 -0
  8. package/pyproject.toml +67 -0
  9. package/qingflow +15 -0
  10. package/src/qingflow_mcp/__init__.py +37 -0
  11. package/src/qingflow_mcp/__main__.py +5 -0
  12. package/src/qingflow_mcp/backend_client.py +649 -0
  13. package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
  14. package/src/qingflow_mcp/builder_facade/models.py +1846 -0
  15. package/src/qingflow_mcp/builder_facade/service.py +16502 -0
  16. package/src/qingflow_mcp/cli/__init__.py +1 -0
  17. package/src/qingflow_mcp/cli/commands/__init__.py +18 -0
  18. package/src/qingflow_mcp/cli/commands/app.py +40 -0
  19. package/src/qingflow_mcp/cli/commands/auth.py +112 -0
  20. package/src/qingflow_mcp/cli/commands/builder.py +539 -0
  21. package/src/qingflow_mcp/cli/commands/chart.py +18 -0
  22. package/src/qingflow_mcp/cli/commands/common.py +62 -0
  23. package/src/qingflow_mcp/cli/commands/imports.py +96 -0
  24. package/src/qingflow_mcp/cli/commands/portal.py +25 -0
  25. package/src/qingflow_mcp/cli/commands/record.py +331 -0
  26. package/src/qingflow_mcp/cli/commands/repo.py +80 -0
  27. package/src/qingflow_mcp/cli/commands/task.py +141 -0
  28. package/src/qingflow_mcp/cli/commands/view.py +18 -0
  29. package/src/qingflow_mcp/cli/commands/workspace.py +110 -0
  30. package/src/qingflow_mcp/cli/context.py +60 -0
  31. package/src/qingflow_mcp/cli/formatters.py +573 -0
  32. package/src/qingflow_mcp/cli/json_io.py +50 -0
  33. package/src/qingflow_mcp/cli/main.py +186 -0
  34. package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
  35. package/src/qingflow_mcp/cli/terminal_ui.py +173 -0
  36. package/src/qingflow_mcp/config.py +407 -0
  37. package/src/qingflow_mcp/errors.py +66 -0
  38. package/src/qingflow_mcp/id_utils.py +49 -0
  39. package/src/qingflow_mcp/import_store.py +121 -0
  40. package/src/qingflow_mcp/json_types.py +18 -0
  41. package/src/qingflow_mcp/list_type_labels.py +76 -0
  42. package/src/qingflow_mcp/public_surface.py +243 -0
  43. package/src/qingflow_mcp/repository_store.py +71 -0
  44. package/src/qingflow_mcp/response_trim.py +841 -0
  45. package/src/qingflow_mcp/server.py +216 -0
  46. package/src/qingflow_mcp/server_app_builder.py +543 -0
  47. package/src/qingflow_mcp/server_app_user.py +386 -0
  48. package/src/qingflow_mcp/session_store.py +369 -0
  49. package/src/qingflow_mcp/solution/__init__.py +6 -0
  50. package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
  51. package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
  52. package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
  53. package/src/qingflow_mcp/solution/compiler/form_compiler.py +495 -0
  54. package/src/qingflow_mcp/solution/compiler/icon_utils.py +187 -0
  55. package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
  56. package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
  57. package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
  58. package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
  59. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  60. package/src/qingflow_mcp/solution/design_session.py +222 -0
  61. package/src/qingflow_mcp/solution/design_store.py +100 -0
  62. package/src/qingflow_mcp/solution/executor.py +2398 -0
  63. package/src/qingflow_mcp/solution/normalizer.py +23 -0
  64. package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
  65. package/src/qingflow_mcp/solution/run_store.py +244 -0
  66. package/src/qingflow_mcp/solution/spec_models.py +855 -0
  67. package/src/qingflow_mcp/tools/__init__.py +1 -0
  68. package/src/qingflow_mcp/tools/ai_builder_tools.py +3449 -0
  69. package/src/qingflow_mcp/tools/app_tools.py +926 -0
  70. package/src/qingflow_mcp/tools/approval_tools.py +1062 -0
  71. package/src/qingflow_mcp/tools/auth_tools.py +1133 -0
  72. package/src/qingflow_mcp/tools/base.py +281 -0
  73. package/src/qingflow_mcp/tools/code_block_tools.py +777 -0
  74. package/src/qingflow_mcp/tools/custom_button_tools.py +202 -0
  75. package/src/qingflow_mcp/tools/directory_tools.py +675 -0
  76. package/src/qingflow_mcp/tools/feedback_tools.py +238 -0
  77. package/src/qingflow_mcp/tools/file_tools.py +409 -0
  78. package/src/qingflow_mcp/tools/import_tools.py +2223 -0
  79. package/src/qingflow_mcp/tools/navigation_tools.py +210 -0
  80. package/src/qingflow_mcp/tools/package_tools.py +326 -0
  81. package/src/qingflow_mcp/tools/portal_tools.py +158 -0
  82. package/src/qingflow_mcp/tools/qingbi_report_tools.py +374 -0
  83. package/src/qingflow_mcp/tools/record_tools.py +14291 -0
  84. package/src/qingflow_mcp/tools/repository_dev_tools.py +552 -0
  85. package/src/qingflow_mcp/tools/resource_read_tools.py +503 -0
  86. package/src/qingflow_mcp/tools/role_tools.py +112 -0
  87. package/src/qingflow_mcp/tools/solution_tools.py +4054 -0
  88. package/src/qingflow_mcp/tools/task_context_tools.py +2986 -0
  89. package/src/qingflow_mcp/tools/task_tools.py +889 -0
  90. package/src/qingflow_mcp/tools/view_tools.py +335 -0
  91. package/src/qingflow_mcp/tools/workflow_tools.py +376 -0
  92. package/src/qingflow_mcp/tools/workspace_tools.py +266 -0
@@ -0,0 +1,552 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from typing import Any
6
+
7
+ from ..config import (
8
+ get_repository_develop_branch,
9
+ get_repository_generate_default_agent_id,
10
+ get_repository_generate_default_route_prefix,
11
+ get_repository_generate_default_token_name,
12
+ get_repository_internal_base_url,
13
+ get_repository_internal_share_token,
14
+ get_repository_internal_share_token_key,
15
+ get_repository_preview_address_template,
16
+ get_repository_prod_branch,
17
+ )
18
+ from ..errors import QingflowApiError, raise_tool_error
19
+ from ..json_types import JSONObject, JSONValue
20
+ from ..repository_store import RepositoryMetadataStore
21
+ from ..session_store import SessionProfile
22
+ from .base import ToolBase, tool_cn_name
23
+
24
+
25
+ _STREAM_EVENT_RE = re.compile(r"^type=(?P<type>[^,]+),timestamp=(?P<timestamp>[^,]+),data=(?P<data>.*)$")
26
+ _STREAM_NEWLINE_TOKEN = "<wingsBr>"
27
+
28
+
29
+ class RepositoryDevTools(ToolBase):
30
+ """仓库开发工具(中文名:仓库初始化与绑定)。
31
+
32
+ 类型:开发辅助工具。
33
+ 主要职责:
34
+ 1. 初始化仓库开发元数据;
35
+ 2. 维护仓库模板与分组映射;
36
+ 3. 为 AI Builder 的仓库流程提供状态读写。
37
+ """
38
+
39
+ def __init__(self, sessions, backend, *, metadata_store: RepositoryMetadataStore | None = None) -> None:
40
+ """执行内部辅助逻辑。"""
41
+ super().__init__(sessions, backend)
42
+ self._metadata = metadata_store or RepositoryMetadataStore()
43
+
44
+ @tool_cn_name("仓库初始化")
45
+ def repository_init(self, *, profile: str, group_name: str, repo_template: str) -> JSONObject:
46
+ """执行工具方法逻辑。"""
47
+ normalized_group = str(group_name or "").strip()
48
+ normalized_template = str(repo_template or "").strip()
49
+ if not normalized_group:
50
+ raise_tool_error(QingflowApiError.config_error("group_name is required"))
51
+ if not normalized_template:
52
+ raise_tool_error(QingflowApiError.config_error("repo_template is required"))
53
+
54
+ def runner(session_profile: SessionProfile, context):
55
+ payload = self._request_custom_page_json(
56
+ session_profile,
57
+ context,
58
+ "POST",
59
+ "/ultron/custom_page/v1/_init",
60
+ params={"groupName": normalized_group, "repoTemplate": normalized_template},
61
+ )
62
+ repo_name = _extract_repo_name(payload)
63
+ preview_address = _format_preview_address(repo_name)
64
+ stored = self._metadata.put(
65
+ repo_name,
66
+ {
67
+ "group_name": normalized_group,
68
+ "repo_template": normalized_template,
69
+ "preview_address": preview_address,
70
+ },
71
+ )
72
+ return {
73
+ "status": "success",
74
+ "repo_name": repo_name,
75
+ "group_name": stored.get("group_name"),
76
+ "repo_template": stored.get("repo_template"),
77
+ "preview_address": preview_address,
78
+ "verification": {"repo_initialized": True},
79
+ "warnings": self._custom_page_route_warning(),
80
+ }
81
+
82
+ return self._run(profile, runner, require_workspace=True)
83
+
84
+ @tool_cn_name("仓库生成")
85
+ def repository_generate(
86
+ self,
87
+ *,
88
+ profile: str,
89
+ repo_name: str,
90
+ query: str,
91
+ tag_id: int | None = None,
92
+ app_keys: list[str] | None = None,
93
+ extra_info: JSONObject | None = None,
94
+ file_messages: list[JSONObject] | None = None,
95
+ being_trace_log_enabled: bool = True,
96
+ agent_id: int | None = None,
97
+ allow_create_table: bool = False,
98
+ route_prefix: str | None = None,
99
+ token_name: str | None = None,
100
+ session_id: str | None = None,
101
+ round_version: int | None = None,
102
+ ) -> JSONObject:
103
+ """执行工具方法逻辑。"""
104
+ normalized_repo = str(repo_name or "").strip()
105
+ normalized_query = str(query or "").strip()
106
+ if not normalized_repo:
107
+ raise_tool_error(QingflowApiError.config_error("repo_name is required"))
108
+ if not normalized_query:
109
+ raise_tool_error(QingflowApiError.config_error("query is required"))
110
+
111
+ metadata = self._metadata.get(normalized_repo) or {}
112
+ normalized_tag_id = _normalize_optional_int(tag_id, "tag_id") or _normalize_optional_int(metadata.get("tag_id"), "tag_id")
113
+ normalized_app_keys = _normalize_string_list(app_keys or metadata.get("app_keys") or [], field_name="app_keys")
114
+ normalized_extra_info = _normalize_optional_object(extra_info, field_name="extra_info")
115
+ normalized_file_messages = _normalize_optional_list_of_objects(file_messages, field_name="file_messages")
116
+ normalized_agent_id = _normalize_optional_int(agent_id, "agent_id") or get_repository_generate_default_agent_id()
117
+ normalized_route_prefix = _normalize_optional_string(route_prefix) or _normalize_optional_string(metadata.get("route_prefix")) or get_repository_generate_default_route_prefix()
118
+ normalized_token_name = _normalize_optional_string(token_name) or _normalize_optional_string(metadata.get("token_name")) or get_repository_generate_default_token_name()
119
+ normalized_session_id = _normalize_optional_string(session_id)
120
+ normalized_round_version = _normalize_optional_int(round_version, "round_version")
121
+
122
+ def runner(session_profile: SessionProfile, context):
123
+ payload: JSONObject = {
124
+ "query": normalized_query,
125
+ "repoName": normalized_repo,
126
+ "uid": session_profile.uid,
127
+ "beingTraceLogEnabled": bool(being_trace_log_enabled),
128
+ "allowCreateTable": bool(allow_create_table),
129
+ }
130
+ if normalized_tag_id is not None:
131
+ payload["tagId"] = normalized_tag_id
132
+ if normalized_app_keys:
133
+ payload["appKeys"] = normalized_app_keys
134
+ if normalized_extra_info is not None:
135
+ payload["extraInfo"] = normalized_extra_info
136
+ if normalized_file_messages:
137
+ payload["fileMessages"] = normalized_file_messages
138
+ if normalized_agent_id is not None:
139
+ payload["agentId"] = normalized_agent_id
140
+ if normalized_route_prefix is not None:
141
+ payload["routePrefix"] = normalized_route_prefix
142
+ if normalized_token_name is not None:
143
+ payload["tokenName"] = normalized_token_name
144
+ if normalized_session_id is not None:
145
+ payload["sessionId"] = normalized_session_id
146
+ if normalized_round_version is not None:
147
+ payload["roundVersion"] = normalized_round_version
148
+
149
+ stream_lines = self._request_custom_page_stream(
150
+ session_profile,
151
+ context,
152
+ "POST",
153
+ "/ultron/custom_page/v1/_generate",
154
+ json_body=payload,
155
+ )
156
+ summarized = _summarize_generate_stream(
157
+ repo_name=normalized_repo,
158
+ query=normalized_query,
159
+ stream_lines=stream_lines,
160
+ route_warning=self._custom_page_route_warning(),
161
+ )
162
+ self._metadata.put(
163
+ normalized_repo,
164
+ {
165
+ "tag_id": normalized_tag_id,
166
+ "app_keys": normalized_app_keys,
167
+ "route_prefix": normalized_route_prefix,
168
+ "token_name": normalized_token_name,
169
+ "agent_id": normalized_agent_id,
170
+ "last_generate_query": normalized_query,
171
+ "last_generate_session_id": summarized.get("session_id"),
172
+ "last_generate_round_version": summarized.get("round_version"),
173
+ },
174
+ )
175
+ return summarized
176
+
177
+ return self._run(profile, runner, require_workspace=True)
178
+
179
+ @tool_cn_name("仓库发布生产")
180
+ def repository_publish_prod(
181
+ self,
182
+ *,
183
+ profile: str,
184
+ repo_name: str,
185
+ confirm: bool = False,
186
+ ) -> JSONObject:
187
+ """执行工具方法逻辑。"""
188
+ normalized_repo = str(repo_name or "").strip()
189
+ if not normalized_repo:
190
+ raise_tool_error(QingflowApiError.config_error("repo_name is required"))
191
+ if confirm is not True:
192
+ raise_tool_error(QingflowApiError.config_error("confirm=true is required for repository_publish_prod"))
193
+
194
+ def runner(session_profile: SessionProfile, context):
195
+ self._request_custom_page_json(
196
+ session_profile,
197
+ context,
198
+ "POST",
199
+ "/ultron/custom_page/v1/_publish",
200
+ params={"repoName": normalized_repo},
201
+ )
202
+ preview_address = _lookup_preview_address(normalized_repo, self._metadata)
203
+ return {
204
+ "status": "success",
205
+ "repo_name": normalized_repo,
206
+ "source_branch": get_repository_develop_branch(),
207
+ "target_branch": get_repository_prod_branch(),
208
+ "merge_status": "success",
209
+ "pipeline_status": "success",
210
+ "published": True,
211
+ "preview_address": preview_address,
212
+ "verification": {"publish_confirmed": True, "pipeline_verified": True},
213
+ "warnings": self._custom_page_route_warning(),
214
+ }
215
+
216
+ return self._run(profile, runner, require_workspace=True)
217
+
218
+ def _custom_page_route_warning(self) -> list[JSONObject]:
219
+ """执行内部辅助逻辑。"""
220
+ if get_repository_internal_base_url() and get_repository_internal_share_token():
221
+ return []
222
+ return [
223
+ {
224
+ "code": "CUSTOM_PAGE_ROUTE_PUBLIC_FALLBACK",
225
+ "message": (
226
+ "repository tools are using the current Qingflow session route. "
227
+ "If the official custom-page internal route is not exposed in this environment, "
228
+ "configure repository.internal_base_url and repository.internal_share_token."
229
+ ),
230
+ }
231
+ ]
232
+
233
+ def _request_custom_page_json(
234
+ self,
235
+ session_profile: SessionProfile,
236
+ context,
237
+ method: str,
238
+ path: str,
239
+ *,
240
+ params: JSONObject | None = None,
241
+ json_body: JSONValue = None,
242
+ ) -> JSONValue:
243
+ """执行内部辅助逻辑。"""
244
+ internal_route = _resolve_internal_custom_page_route(session_profile)
245
+ if internal_route is not None:
246
+ return self.backend.public_request_with_headers(
247
+ method,
248
+ internal_route["base_url"],
249
+ path,
250
+ params=params,
251
+ json_body=json_body,
252
+ headers=internal_route["headers"],
253
+ qf_version=context.qf_version,
254
+ ).data
255
+ return self.backend.request(method, context, path, params=params, json_body=json_body)
256
+
257
+ def _request_custom_page_stream(
258
+ self,
259
+ session_profile: SessionProfile,
260
+ context,
261
+ method: str,
262
+ path: str,
263
+ *,
264
+ params: JSONObject | None = None,
265
+ json_body: JSONValue = None,
266
+ ) -> list[str]:
267
+ """执行内部辅助逻辑。"""
268
+ internal_route = _resolve_internal_custom_page_route(session_profile)
269
+ if internal_route is not None:
270
+ return self.backend.public_stream_request(
271
+ method,
272
+ internal_route["base_url"],
273
+ path,
274
+ params=params,
275
+ json_body=json_body,
276
+ headers=internal_route["headers"],
277
+ qf_version=context.qf_version,
278
+ )
279
+ return self.backend.stream_request(method, context, path, params=params, json_body=json_body)
280
+
281
+
282
+ def _extract_repo_name(payload: Any) -> str:
283
+ if isinstance(payload, str) and payload.strip():
284
+ return payload.strip()
285
+ if isinstance(payload, dict):
286
+ for key in ("repoName", "repo_name"):
287
+ value = payload.get(key)
288
+ if isinstance(value, str) and value.strip():
289
+ return value.strip()
290
+ raise_tool_error(QingflowApiError(category="runtime", message="repository init did not return repo_name"))
291
+ raise AssertionError("unreachable")
292
+
293
+
294
+ def _format_preview_address(repo_name: str) -> str:
295
+ template = get_repository_preview_address_template()
296
+ short_name = repo_name.split("/")[-1]
297
+ if "%s" in template:
298
+ return template % short_name
299
+ return template.format(repo=short_name)
300
+
301
+
302
+ def _lookup_preview_address(repo_name: str, store: RepositoryMetadataStore) -> str | None:
303
+ normalized = repo_name.strip()
304
+ short_name = normalized.split("/")[-1]
305
+ stored = store.get(short_name) or {}
306
+ preview_address = stored.get("preview_address")
307
+ return preview_address if isinstance(preview_address, str) and preview_address.strip() else None
308
+
309
+
310
+ def _resolve_internal_custom_page_route(session_profile: SessionProfile) -> dict[str, Any] | None:
311
+ base_url = get_repository_internal_base_url()
312
+ share_token = get_repository_internal_share_token()
313
+ token_key = get_repository_internal_share_token_key()
314
+ if not base_url and not share_token:
315
+ return None
316
+ if not base_url or not share_token:
317
+ raise_tool_error(
318
+ QingflowApiError.config_error(
319
+ "repository.internal_base_url and repository.internal_share_token must be configured together"
320
+ )
321
+ )
322
+ if session_profile.selected_ws_id is None:
323
+ raise_tool_error(
324
+ QingflowApiError.config_error("auth_use_credential must return a valid wsId before using the internal custom-page route")
325
+ )
326
+ return {
327
+ "base_url": base_url,
328
+ "headers": {
329
+ token_key: share_token,
330
+ "wsId": str(session_profile.selected_ws_id),
331
+ },
332
+ }
333
+
334
+
335
+ def _normalize_optional_string(value: Any) -> str | None:
336
+ normalized = str(value or "").strip()
337
+ return normalized or None
338
+
339
+
340
+ def _normalize_optional_int(value: Any, field_name: str) -> int | None:
341
+ if value is None or value == "":
342
+ return None
343
+ try:
344
+ return int(value)
345
+ except (TypeError, ValueError):
346
+ raise_tool_error(QingflowApiError.config_error(f"{field_name} must be an integer"))
347
+ raise AssertionError("unreachable")
348
+
349
+
350
+ def _normalize_string_list(values: list[Any], *, field_name: str) -> list[str]:
351
+ normalized: list[str] = []
352
+ for item in values:
353
+ text = str(item or "").strip()
354
+ if not text:
355
+ continue
356
+ normalized.append(text)
357
+ return normalized
358
+
359
+
360
+ def _normalize_optional_object(value: Any, *, field_name: str) -> JSONObject | None:
361
+ if value is None:
362
+ return None
363
+ if not isinstance(value, dict):
364
+ raise_tool_error(QingflowApiError.config_error(f"{field_name} must be an object"))
365
+ return value
366
+
367
+
368
+ def _normalize_optional_list_of_objects(value: Any, *, field_name: str) -> list[JSONObject]:
369
+ if value is None:
370
+ return []
371
+ if not isinstance(value, list):
372
+ raise_tool_error(QingflowApiError.config_error(f"{field_name} must be a list"))
373
+ normalized: list[JSONObject] = []
374
+ for item in value:
375
+ if not isinstance(item, dict):
376
+ raise_tool_error(QingflowApiError.config_error(f"each {field_name} item must be an object"))
377
+ normalized.append(item)
378
+ return normalized
379
+
380
+
381
+ def _parse_stream_line(raw_line: str) -> JSONObject | None:
382
+ line = str(raw_line or "").strip()
383
+ if not line:
384
+ return None
385
+ if not line.startswith("data: "):
386
+ return {"type": "raw", "data": line}
387
+ body = line[6:]
388
+ if body == "[done]":
389
+ return {"type": "done", "data": "[done]"}
390
+ match = _STREAM_EVENT_RE.match(body)
391
+ if not match:
392
+ return {"type": "raw", "data": body}
393
+ event_type = match.group("type")
394
+ timestamp_raw = str(match.group("timestamp") or "").strip()
395
+ try:
396
+ timestamp = int(timestamp_raw) if timestamp_raw else None
397
+ except ValueError:
398
+ timestamp = None
399
+ data_text = match.group("data").replace(_STREAM_NEWLINE_TOKEN, "\n")
400
+ data = _maybe_parse_json(data_text)
401
+ return {
402
+ "type": event_type,
403
+ "timestamp": timestamp,
404
+ "data": data,
405
+ }
406
+
407
+
408
+ def _maybe_parse_json(value: str) -> JSONValue:
409
+ text = str(value or "").strip()
410
+ if not text:
411
+ return ""
412
+ try:
413
+ return json.loads(text)
414
+ except json.JSONDecodeError:
415
+ return text
416
+
417
+
418
+ def _summarize_generate_stream(
419
+ *,
420
+ repo_name: str,
421
+ query: str,
422
+ stream_lines: list[str],
423
+ route_warning: list[JSONObject],
424
+ ) -> JSONObject:
425
+ parsed_events = [event for event in (_parse_stream_line(line) for line in stream_lines) if event is not None]
426
+ done_seen = any(str(event.get("type")) == "done" for event in parsed_events)
427
+ result_event = next((event for event in reversed(parsed_events) if event.get("type") == "result"), None)
428
+ error_event = next((event for event in reversed(parsed_events) if event.get("type") == "error"), None)
429
+ repository_events = [
430
+ event.get("data")
431
+ for event in parsed_events
432
+ if event.get("type") == "event"
433
+ and isinstance(event.get("data"), dict)
434
+ and event["data"].get("action") == "REPOSITORY_COMMIT"
435
+ ]
436
+ other_events = [
437
+ event.get("data")
438
+ for event in parsed_events
439
+ if event.get("type") == "event"
440
+ and not (
441
+ isinstance(event.get("data"), dict)
442
+ and event["data"].get("action") == "REPOSITORY_COMMIT"
443
+ )
444
+ ]
445
+ token_count = sum(1 for event in parsed_events if event.get("type") == "token")
446
+ running_count = sum(1 for event in parsed_events if event.get("type") == "runing")
447
+ waiting_count = sum(1 for event in parsed_events if event.get("type") == "waiting")
448
+ raw_count = sum(1 for event in parsed_events if event.get("type") == "raw")
449
+ warnings = list(route_warning)
450
+ if raw_count:
451
+ warnings.append(
452
+ {
453
+ "code": "GENERATE_STREAM_UNPARSED_CHUNKS",
454
+ "message": f"generate stream contained {raw_count} unparsed chunk(s); raw chunks are returned for debugging",
455
+ }
456
+ )
457
+ if not done_seen:
458
+ warnings.append(
459
+ {
460
+ "code": "GENERATE_STREAM_DONE_MISSING",
461
+ "message": "generate stream ended without an explicit [done] marker",
462
+ }
463
+ )
464
+
465
+ if error_event is not None:
466
+ error_data = error_event.get("data")
467
+ error_code: str | None = None
468
+ message = "repository generation failed"
469
+ if isinstance(error_data, dict):
470
+ error_code = str(error_data.get("errorCode") or error_data.get("code") or "").strip() or None
471
+ message = str(error_data.get("errorMessage") or error_data.get("message") or message)
472
+ elif isinstance(error_data, str) and error_data.strip():
473
+ message = error_data.strip()
474
+ return {
475
+ "status": "failed",
476
+ "error_code": error_code or "REPOSITORY_GENERATE_FAILED",
477
+ "message": message,
478
+ "repo_name": repo_name,
479
+ "query": query,
480
+ "repository_events": repository_events,
481
+ "events": other_events,
482
+ "stream_summary": {
483
+ "token_events": token_count,
484
+ "running_events": running_count,
485
+ "waiting_events": waiting_count,
486
+ "done_seen": done_seen,
487
+ "raw_chunks": raw_count,
488
+ },
489
+ "verification": {
490
+ "stream_done": done_seen,
491
+ "result_received": False,
492
+ "repository_commit_detected": bool(repository_events),
493
+ },
494
+ "warnings": warnings,
495
+ }
496
+
497
+ result_payload = result_event.get("data") if isinstance(result_event, dict) else None
498
+ if not isinstance(result_payload, dict):
499
+ return {
500
+ "status": "failed",
501
+ "error_code": "REPOSITORY_GENERATE_EMPTY_RESULT",
502
+ "message": "repository generation stream completed without a structured result payload",
503
+ "repo_name": repo_name,
504
+ "query": query,
505
+ "repository_events": repository_events,
506
+ "events": other_events,
507
+ "stream_summary": {
508
+ "token_events": token_count,
509
+ "running_events": running_count,
510
+ "waiting_events": waiting_count,
511
+ "done_seen": done_seen,
512
+ "raw_chunks": raw_count,
513
+ },
514
+ "verification": {
515
+ "stream_done": done_seen,
516
+ "result_received": False,
517
+ "repository_commit_detected": bool(repository_events),
518
+ },
519
+ "warnings": warnings,
520
+ }
521
+
522
+ response_status = str(result_payload.get("responseStatus") or "SUCCESS")
523
+ return {
524
+ "status": "success" if response_status.upper() == "SUCCESS" else "failed",
525
+ "repo_name": repo_name,
526
+ "query": query,
527
+ "response_status": response_status,
528
+ "session_id": result_payload.get("sessionId"),
529
+ "round_version": result_payload.get("roundVersion"),
530
+ "thread_id": result_payload.get("threadId"),
531
+ "answer": result_payload.get("answer"),
532
+ "answer_json": result_payload.get("answerJson"),
533
+ "trace_logs": result_payload.get("traceLogs"),
534
+ "total_tokens": result_payload.get("totalTokens"),
535
+ "credit_consume": result_payload.get("creditConsume"),
536
+ "history_messages": result_payload.get("historyMessages"),
537
+ "repository_events": repository_events,
538
+ "events": other_events,
539
+ "stream_summary": {
540
+ "token_events": token_count,
541
+ "running_events": running_count,
542
+ "waiting_events": waiting_count,
543
+ "done_seen": done_seen,
544
+ "raw_chunks": raw_count,
545
+ },
546
+ "verification": {
547
+ "stream_done": done_seen,
548
+ "result_received": True,
549
+ "repository_commit_detected": bool(repository_events),
550
+ },
551
+ "warnings": warnings,
552
+ }