@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.
- package/README.md +31 -0
- package/docs/local-agent-install.md +309 -0
- package/entry_point.py +13 -0
- package/npm/bin/qingflow.mjs +5 -0
- package/npm/lib/runtime.mjs +346 -0
- package/npm/scripts/postinstall.mjs +16 -0
- package/package.json +34 -0
- package/pyproject.toml +67 -0
- package/qingflow +15 -0
- package/src/qingflow_mcp/__init__.py +37 -0
- package/src/qingflow_mcp/__main__.py +5 -0
- package/src/qingflow_mcp/backend_client.py +649 -0
- package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
- package/src/qingflow_mcp/builder_facade/models.py +1846 -0
- package/src/qingflow_mcp/builder_facade/service.py +16502 -0
- package/src/qingflow_mcp/cli/__init__.py +1 -0
- package/src/qingflow_mcp/cli/commands/__init__.py +18 -0
- package/src/qingflow_mcp/cli/commands/app.py +40 -0
- package/src/qingflow_mcp/cli/commands/auth.py +112 -0
- package/src/qingflow_mcp/cli/commands/builder.py +539 -0
- package/src/qingflow_mcp/cli/commands/chart.py +18 -0
- package/src/qingflow_mcp/cli/commands/common.py +62 -0
- package/src/qingflow_mcp/cli/commands/imports.py +96 -0
- package/src/qingflow_mcp/cli/commands/portal.py +25 -0
- package/src/qingflow_mcp/cli/commands/record.py +331 -0
- package/src/qingflow_mcp/cli/commands/repo.py +80 -0
- package/src/qingflow_mcp/cli/commands/task.py +141 -0
- package/src/qingflow_mcp/cli/commands/view.py +18 -0
- package/src/qingflow_mcp/cli/commands/workspace.py +110 -0
- package/src/qingflow_mcp/cli/context.py +60 -0
- package/src/qingflow_mcp/cli/formatters.py +573 -0
- package/src/qingflow_mcp/cli/json_io.py +50 -0
- package/src/qingflow_mcp/cli/main.py +186 -0
- package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
- package/src/qingflow_mcp/cli/terminal_ui.py +173 -0
- package/src/qingflow_mcp/config.py +407 -0
- package/src/qingflow_mcp/errors.py +66 -0
- package/src/qingflow_mcp/id_utils.py +49 -0
- package/src/qingflow_mcp/import_store.py +121 -0
- package/src/qingflow_mcp/json_types.py +18 -0
- package/src/qingflow_mcp/list_type_labels.py +76 -0
- package/src/qingflow_mcp/public_surface.py +243 -0
- package/src/qingflow_mcp/repository_store.py +71 -0
- package/src/qingflow_mcp/response_trim.py +841 -0
- package/src/qingflow_mcp/server.py +216 -0
- package/src/qingflow_mcp/server_app_builder.py +543 -0
- package/src/qingflow_mcp/server_app_user.py +386 -0
- package/src/qingflow_mcp/session_store.py +369 -0
- package/src/qingflow_mcp/solution/__init__.py +6 -0
- package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
- package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
- package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +495 -0
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +187 -0
- package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
- package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
- package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
- package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
- package/src/qingflow_mcp/solution/design_session.py +222 -0
- package/src/qingflow_mcp/solution/design_store.py +100 -0
- package/src/qingflow_mcp/solution/executor.py +2398 -0
- package/src/qingflow_mcp/solution/normalizer.py +23 -0
- package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
- package/src/qingflow_mcp/solution/run_store.py +244 -0
- package/src/qingflow_mcp/solution/spec_models.py +855 -0
- package/src/qingflow_mcp/tools/__init__.py +1 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +3449 -0
- package/src/qingflow_mcp/tools/app_tools.py +926 -0
- package/src/qingflow_mcp/tools/approval_tools.py +1062 -0
- package/src/qingflow_mcp/tools/auth_tools.py +1133 -0
- package/src/qingflow_mcp/tools/base.py +281 -0
- package/src/qingflow_mcp/tools/code_block_tools.py +777 -0
- package/src/qingflow_mcp/tools/custom_button_tools.py +202 -0
- package/src/qingflow_mcp/tools/directory_tools.py +675 -0
- package/src/qingflow_mcp/tools/feedback_tools.py +238 -0
- package/src/qingflow_mcp/tools/file_tools.py +409 -0
- package/src/qingflow_mcp/tools/import_tools.py +2223 -0
- package/src/qingflow_mcp/tools/navigation_tools.py +210 -0
- package/src/qingflow_mcp/tools/package_tools.py +326 -0
- package/src/qingflow_mcp/tools/portal_tools.py +158 -0
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +374 -0
- package/src/qingflow_mcp/tools/record_tools.py +14291 -0
- package/src/qingflow_mcp/tools/repository_dev_tools.py +552 -0
- package/src/qingflow_mcp/tools/resource_read_tools.py +503 -0
- package/src/qingflow_mcp/tools/role_tools.py +112 -0
- package/src/qingflow_mcp/tools/solution_tools.py +4054 -0
- package/src/qingflow_mcp/tools/task_context_tools.py +2986 -0
- package/src/qingflow_mcp/tools/task_tools.py +889 -0
- package/src/qingflow_mcp/tools/view_tools.py +335 -0
- package/src/qingflow_mcp/tools/workflow_tools.py +376 -0
- 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
|
+
}
|