xtc-app-knowledge-mcp 0.1.0__py3-none-any.whl
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.
- app_knowledge_mcp/__init__.py +0 -0
- app_knowledge_mcp/__main__.py +4 -0
- app_knowledge_mcp/cli.py +50 -0
- app_knowledge_mcp/cloud_client.py +387 -0
- app_knowledge_mcp/frontmatter.py +50 -0
- app_knowledge_mcp/installer.py +529 -0
- app_knowledge_mcp/local_embedding.py +177 -0
- app_knowledge_mcp/local_search.py +228 -0
- app_knowledge_mcp/server.py +957 -0
- app_knowledge_mcp/templates/dev-toolkit/skills/knowledge-doc-format-rewrite/SKILL.md +197 -0
- app_knowledge_mcp/templates/dev-toolkit/skills/knowledge-doc-format-rewrite/eval.yaml +670 -0
- app_knowledge_mcp/templates/dev-toolkit/skills/knowledge-doc-format-rewrite/prompt.md +550 -0
- app_knowledge_mcp/templates/dev-toolkit/skills/knowledge-lifecycle/SKILL.md +349 -0
- app_knowledge_mcp/templates/dev-toolkit/skills/knowledge-lifecycle/design/architecture-lens-design.md +249 -0
- app_knowledge_mcp/templates/dev-toolkit/skills/knowledge-lifecycle/design/card-generator-design.md +178 -0
- app_knowledge_mcp/templates/dev-toolkit/skills/knowledge-lifecycle/design/card-splitter-design.md +158 -0
- app_knowledge_mcp/templates/dev-toolkit/skills/knowledge-lifecycle/design/knowledge-lifecycle-design.md +342 -0
- app_knowledge_mcp/templates/dev-toolkit/skills/knowledge-lifecycle/design/knowledge-wiki-design.md +367 -0
- app_knowledge_mcp/templates/dev-toolkit/skills/knowledge-lifecycle/prompts/architecture-lens.md +289 -0
- app_knowledge_mcp/templates/dev-toolkit/skills/knowledge-lifecycle/prompts/artifact-validator.md +89 -0
- app_knowledge_mcp/templates/dev-toolkit/skills/knowledge-lifecycle/prompts/card-generator.md +114 -0
- app_knowledge_mcp/templates/dev-toolkit/skills/knowledge-lifecycle/prompts/card-splitter.md +217 -0
- app_knowledge_mcp/templates/dev-toolkit/skills/knowledge-lifecycle/prompts/impact-analyzer.md +164 -0
- app_knowledge_mcp/templates/dev-toolkit/skills/knowledge-lifecycle/prompts/wiki-analyzer.md +134 -0
- app_knowledge_mcp/templates/dev-toolkit/skills/knowledge-lifecycle/scripts/generate-wiki.py +293 -0
- app_knowledge_mcp/templates/dev-toolkit/skills/knowledge-lifecycle/scripts/validate-mermaid.py +65 -0
- app_knowledge_mcp/templates/dev-toolkit/skills/knowledge-lifecycle/template/architecture-overview.template.md +80 -0
- app_knowledge_mcp/templates/dev-toolkit/skills/knowledge-lifecycle/template/impact-report.template.md +75 -0
- app_knowledge_mcp/templates/dev-toolkit/skills/knowledge-lifecycle/template/knowledge-graph-structure.md +187 -0
- app_knowledge_mcp/templates/dev-toolkit/skills/knowledge-lifecycle/template/module-source-manifest.template.md +78 -0
- app_knowledge_mcp/templates/dev-toolkit/skills/knowledge-lifecycle/template/summary.template.md +21 -0
- xtc_app_knowledge_mcp-0.1.0.dist-info/METADATA +9 -0
- xtc_app_knowledge_mcp-0.1.0.dist-info/RECORD +36 -0
- xtc_app_knowledge_mcp-0.1.0.dist-info/WHEEL +5 -0
- xtc_app_knowledge_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- xtc_app_knowledge_mcp-0.1.0.dist-info/top_level.txt +1 -0
|
File without changes
|
app_knowledge_mcp/cli.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""CLI 入口:app-knowledge-mcp init / enable / disable"""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .installer import (
|
|
8
|
+
init_project,
|
|
9
|
+
enable_global,
|
|
10
|
+
disable_global,
|
|
11
|
+
log,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main() -> int:
|
|
16
|
+
parser = argparse.ArgumentParser(description="app-knowledge-mcp — 聚合 MCP 本地网关")
|
|
17
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
18
|
+
|
|
19
|
+
init_p = sub.add_parser("init", help="为项目安装 MCP 能力")
|
|
20
|
+
init_p.add_argument("project_dir", nargs="?", default=".",
|
|
21
|
+
help="业务仓库目录(默认当前目录)")
|
|
22
|
+
init_p.add_argument("--project", default=None,
|
|
23
|
+
help="平台 project 标识(如 callwatch)")
|
|
24
|
+
|
|
25
|
+
sub.add_parser("enable", help="注册到全局 opencode config")
|
|
26
|
+
sub.add_parser("disable", help="从全局 opencode config 移除")
|
|
27
|
+
|
|
28
|
+
args = parser.parse_args()
|
|
29
|
+
|
|
30
|
+
if args.command == "init":
|
|
31
|
+
project_dir = Path(args.project_dir).resolve()
|
|
32
|
+
if not project_dir.is_dir():
|
|
33
|
+
log(f"目录不存在: {project_dir}", "ERROR")
|
|
34
|
+
return 1
|
|
35
|
+
init_project(project_dir, args.project)
|
|
36
|
+
return 0
|
|
37
|
+
|
|
38
|
+
if args.command == "enable":
|
|
39
|
+
enable_global()
|
|
40
|
+
return 0
|
|
41
|
+
|
|
42
|
+
if args.command == "disable":
|
|
43
|
+
disable_global()
|
|
44
|
+
return 0
|
|
45
|
+
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
sys.exit(main())
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
CLOUD_MCP_URL = os.environ.get("CLOUD_MCP_URL", "http://172.28.4.103:18000/mcp")
|
|
9
|
+
|
|
10
|
+
CLOUD_DOC_URL = os.environ.get("CLOUD_DOC_URL", "http://172.28.4.103:18001")
|
|
11
|
+
|
|
12
|
+
CLOUD_SKILL_URL = os.environ.get("CLOUD_SKILL_URL", "http://172.28.4.103:18003")
|
|
13
|
+
|
|
14
|
+
_HEADERS = {
|
|
15
|
+
"Accept": "application/json, text/event-stream",
|
|
16
|
+
"Content-Type": "application/json",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def cloud_knowledge_search(
|
|
21
|
+
query: str,
|
|
22
|
+
knowledge_type: str = "",
|
|
23
|
+
platform: str = "",
|
|
24
|
+
domain: str = "",
|
|
25
|
+
project: str = "",
|
|
26
|
+
team: str = "",
|
|
27
|
+
scope: str = "",
|
|
28
|
+
status: str = "",
|
|
29
|
+
tags: list[str] | None = None,
|
|
30
|
+
files: list[str] | None = None,
|
|
31
|
+
top_k: int = 5,
|
|
32
|
+
exclude_doc_ids: list[str] | None = None,
|
|
33
|
+
client: httpx.AsyncClient | None = None,
|
|
34
|
+
) -> dict[str, Any]:
|
|
35
|
+
"""直连 retrieval:18001/search(平替 cloud MCP _knowledge_search)。"""
|
|
36
|
+
should_close = False
|
|
37
|
+
if client is None:
|
|
38
|
+
client = httpx.AsyncClient(timeout=10.0)
|
|
39
|
+
should_close = True
|
|
40
|
+
try:
|
|
41
|
+
r = await client.post(
|
|
42
|
+
f"{CLOUD_DOC_URL}/search",
|
|
43
|
+
json={
|
|
44
|
+
"query": query, "knowledge_type": knowledge_type, "platform": platform,
|
|
45
|
+
"domain": domain, "project": project, "team": team,
|
|
46
|
+
"scope": scope, "status": status, "tags": tags or [], "files": files or [],
|
|
47
|
+
"top_k": top_k, "exclude_doc_ids": exclude_doc_ids or [],
|
|
48
|
+
},
|
|
49
|
+
)
|
|
50
|
+
r.raise_for_status()
|
|
51
|
+
return r.json()
|
|
52
|
+
finally:
|
|
53
|
+
if should_close:
|
|
54
|
+
await client.aclose()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def cloud_skill_search(
|
|
58
|
+
query: str,
|
|
59
|
+
platform: str = "",
|
|
60
|
+
domain: str = "",
|
|
61
|
+
project: str = "",
|
|
62
|
+
task: str = "",
|
|
63
|
+
files: list[str] | None = None,
|
|
64
|
+
change_type: str = "",
|
|
65
|
+
when: str = "",
|
|
66
|
+
tags: list[str] | None = None,
|
|
67
|
+
top_k: int = 5,
|
|
68
|
+
client: httpx.AsyncClient | None = None,
|
|
69
|
+
) -> dict[str, Any]:
|
|
70
|
+
"""直连 skill:18003/search(平替 cloud MCP _skill_search)。"""
|
|
71
|
+
should_close = False
|
|
72
|
+
if client is None:
|
|
73
|
+
client = httpx.AsyncClient(timeout=10.0)
|
|
74
|
+
should_close = True
|
|
75
|
+
try:
|
|
76
|
+
r = await client.post(
|
|
77
|
+
f"{CLOUD_SKILL_URL}/search",
|
|
78
|
+
json={
|
|
79
|
+
"query": query, "platform": platform, "domain": domain,
|
|
80
|
+
"project": project, "task": task, "files": files or [],
|
|
81
|
+
"change_type": change_type, "when": when, "tags": tags or [], "top_k": top_k,
|
|
82
|
+
},
|
|
83
|
+
)
|
|
84
|
+
r.raise_for_status()
|
|
85
|
+
return r.json()
|
|
86
|
+
finally:
|
|
87
|
+
if should_close:
|
|
88
|
+
await client.aclose()
|
|
89
|
+
|
|
90
|
+
def build_cloud_search_args(config: dict, query: str, top_k: int = 5, **overrides) -> dict:
|
|
91
|
+
"""构建 cloud search 参数字典,profile 默认值 + LLM 显式覆盖。"""
|
|
92
|
+
user = config.get("user") or {}
|
|
93
|
+
profile = config.get("profile") or {}
|
|
94
|
+
repo = profile.get("repo") or {}
|
|
95
|
+
platforms = repo.get("platforms") or []
|
|
96
|
+
args = {
|
|
97
|
+
"query": query,
|
|
98
|
+
"_topic": query,
|
|
99
|
+
"_user_name": user.get("user_name", ""),
|
|
100
|
+
"_user_email": user.get("user_email", ""),
|
|
101
|
+
"platform": platforms[0] if platforms else "",
|
|
102
|
+
"domain": "",
|
|
103
|
+
"project": repo.get("project", ""),
|
|
104
|
+
"task": "",
|
|
105
|
+
"files": [],
|
|
106
|
+
"change_type": "",
|
|
107
|
+
"when": "on_request",
|
|
108
|
+
"knowledge_type": "",
|
|
109
|
+
"scope": "",
|
|
110
|
+
"status": "",
|
|
111
|
+
"tags": [],
|
|
112
|
+
"top_k": top_k,
|
|
113
|
+
"exclude_doc_ids": [],
|
|
114
|
+
}
|
|
115
|
+
args.update(overrides)
|
|
116
|
+
return args
|
|
117
|
+
|
|
118
|
+
_INIT_PARAMS = {
|
|
119
|
+
"protocolVersion": "2024-11-05",
|
|
120
|
+
"capabilities": {},
|
|
121
|
+
"clientInfo": {"name": "app-knowledge-mcp", "version": "0.1.0"},
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
_MCP_ID = 0
|
|
125
|
+
_initialize_done = False
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _next_id() -> str:
|
|
129
|
+
global _MCP_ID
|
|
130
|
+
_MCP_ID += 1
|
|
131
|
+
return str(_MCP_ID)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _parse_sse(text: str) -> list[dict]:
|
|
135
|
+
events = []
|
|
136
|
+
for block in re.split(r"\n\n+", text.strip()):
|
|
137
|
+
data_match = re.search(r"^data:\s*(.+)$", block, re.MULTILINE)
|
|
138
|
+
if data_match:
|
|
139
|
+
try:
|
|
140
|
+
events.append(json.loads(data_match.group(1)))
|
|
141
|
+
except json.JSONDecodeError:
|
|
142
|
+
continue
|
|
143
|
+
return events
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _parse_cloud_response(resp: httpx.Response) -> dict:
|
|
147
|
+
content_type = resp.headers.get("content-type", "")
|
|
148
|
+
if "text/event-stream" in content_type:
|
|
149
|
+
events = _parse_sse(resp.text)
|
|
150
|
+
data = events[0] if events else {}
|
|
151
|
+
else:
|
|
152
|
+
data = resp.json()
|
|
153
|
+
|
|
154
|
+
if "error" in data:
|
|
155
|
+
raise RuntimeError(f"Cloud MCP error: {data['error']}")
|
|
156
|
+
|
|
157
|
+
content = data.get("result", {}).get("content", [])
|
|
158
|
+
for item in content:
|
|
159
|
+
if item.get("type") == "text":
|
|
160
|
+
return json.loads(item["text"])
|
|
161
|
+
return data.get("result", {})
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
async def _call_cloud(
|
|
165
|
+
name: str,
|
|
166
|
+
arguments: dict,
|
|
167
|
+
client: httpx.AsyncClient | None = None,
|
|
168
|
+
) -> dict:
|
|
169
|
+
global _initialize_done
|
|
170
|
+
should_close = False
|
|
171
|
+
if client is None:
|
|
172
|
+
client = httpx.AsyncClient(timeout=15.0)
|
|
173
|
+
should_close = True
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
payload = {
|
|
177
|
+
"jsonrpc": "2.0",
|
|
178
|
+
"method": "tools/call",
|
|
179
|
+
"params": {"name": name, "arguments": arguments},
|
|
180
|
+
"id": _next_id(),
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if _initialize_done:
|
|
184
|
+
resp = await client.post(CLOUD_MCP_URL, headers=_HEADERS, json=payload)
|
|
185
|
+
if resp.status_code == 200:
|
|
186
|
+
return _parse_cloud_response(resp)
|
|
187
|
+
|
|
188
|
+
init_payload = {
|
|
189
|
+
"jsonrpc": "2.0",
|
|
190
|
+
"method": "initialize",
|
|
191
|
+
"params": _INIT_PARAMS,
|
|
192
|
+
"id": _next_id(),
|
|
193
|
+
}
|
|
194
|
+
init_resp = await client.post(CLOUD_MCP_URL, headers=_HEADERS, json=init_payload)
|
|
195
|
+
init_resp.raise_for_status()
|
|
196
|
+
_parse_sse(init_resp.text)
|
|
197
|
+
_initialize_done = True
|
|
198
|
+
|
|
199
|
+
resp = await client.post(CLOUD_MCP_URL, headers=_HEADERS, json=payload)
|
|
200
|
+
resp.raise_for_status()
|
|
201
|
+
return _parse_cloud_response(resp)
|
|
202
|
+
except Exception:
|
|
203
|
+
_initialize_done = False
|
|
204
|
+
raise
|
|
205
|
+
finally:
|
|
206
|
+
if should_close:
|
|
207
|
+
await client.aclose()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
async def cloud_search(
|
|
211
|
+
query: str,
|
|
212
|
+
_topic: str = "",
|
|
213
|
+
_user_name: str = "",
|
|
214
|
+
_user_email: str = "",
|
|
215
|
+
platform: str = "",
|
|
216
|
+
domain: str = "",
|
|
217
|
+
project: str = "",
|
|
218
|
+
task: str = "",
|
|
219
|
+
files: list[str] | None = None,
|
|
220
|
+
change_type: str = "",
|
|
221
|
+
when: str = "",
|
|
222
|
+
knowledge_type: str = "",
|
|
223
|
+
scope: str = "",
|
|
224
|
+
status: str = "",
|
|
225
|
+
tags: list[str] | None = None,
|
|
226
|
+
top_k: int = 5,
|
|
227
|
+
exclude_doc_ids: list[str] | None = None,
|
|
228
|
+
client: httpx.AsyncClient | None = None,
|
|
229
|
+
) -> dict[str, Any]:
|
|
230
|
+
return await _call_cloud(
|
|
231
|
+
"search",
|
|
232
|
+
{
|
|
233
|
+
"query": query,
|
|
234
|
+
"_topic": _topic,
|
|
235
|
+
"_user_name": _user_name,
|
|
236
|
+
"_user_email": _user_email,
|
|
237
|
+
"platform": platform,
|
|
238
|
+
"domain": domain,
|
|
239
|
+
"project": project,
|
|
240
|
+
"task": task,
|
|
241
|
+
"files": files or [],
|
|
242
|
+
"change_type": change_type,
|
|
243
|
+
"when": when,
|
|
244
|
+
"knowledge_type": knowledge_type,
|
|
245
|
+
"scope": scope,
|
|
246
|
+
"status": status,
|
|
247
|
+
"tags": tags or [],
|
|
248
|
+
"top_k": top_k,
|
|
249
|
+
"exclude_doc_ids": exclude_doc_ids or [],
|
|
250
|
+
},
|
|
251
|
+
client=client,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
async def cloud_skill_get(
|
|
256
|
+
skill_id: str,
|
|
257
|
+
include_prompt: bool = False,
|
|
258
|
+
_topic: str = "",
|
|
259
|
+
_user_name: str = "",
|
|
260
|
+
_user_email: str = "",
|
|
261
|
+
client: httpx.AsyncClient | None = None,
|
|
262
|
+
) -> dict[str, Any]:
|
|
263
|
+
return await _call_cloud(
|
|
264
|
+
"skill_get",
|
|
265
|
+
{
|
|
266
|
+
"skill_id": skill_id,
|
|
267
|
+
"include_prompt": include_prompt,
|
|
268
|
+
"_topic": _topic,
|
|
269
|
+
"_user_name": _user_name,
|
|
270
|
+
"_user_email": _user_email,
|
|
271
|
+
},
|
|
272
|
+
client=client,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
async def cloud_rule_check(
|
|
277
|
+
content: str,
|
|
278
|
+
rule_types: list[str] | None = None,
|
|
279
|
+
_topic: str = "",
|
|
280
|
+
_user_name: str = "",
|
|
281
|
+
_user_email: str = "",
|
|
282
|
+
client: httpx.AsyncClient | None = None,
|
|
283
|
+
) -> dict[str, Any]:
|
|
284
|
+
return await _call_cloud(
|
|
285
|
+
"rule_check",
|
|
286
|
+
{
|
|
287
|
+
"content": content,
|
|
288
|
+
"rule_types": rule_types or [],
|
|
289
|
+
"_topic": _topic,
|
|
290
|
+
"_user_name": _user_name,
|
|
291
|
+
"_user_email": _user_email,
|
|
292
|
+
},
|
|
293
|
+
client=client,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
async def cloud_knowledge_sync(
|
|
298
|
+
repo_id: str,
|
|
299
|
+
profile: dict,
|
|
300
|
+
local_manifest: dict | None = None,
|
|
301
|
+
_topic: str = "",
|
|
302
|
+
_user_name: str = "",
|
|
303
|
+
_user_email: str = "",
|
|
304
|
+
client: httpx.AsyncClient | None = None,
|
|
305
|
+
) -> dict[str, Any]:
|
|
306
|
+
return await _call_cloud(
|
|
307
|
+
"knowledge_sync",
|
|
308
|
+
{
|
|
309
|
+
"repo_id": repo_id,
|
|
310
|
+
"profile": profile,
|
|
311
|
+
"local_manifest": local_manifest or {},
|
|
312
|
+
"_topic": _topic,
|
|
313
|
+
"_user_name": _user_name,
|
|
314
|
+
"_user_email": _user_email,
|
|
315
|
+
},
|
|
316
|
+
client=client,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
async def cloud_knowledge_get(
|
|
321
|
+
doc_id: str,
|
|
322
|
+
canonical_path: str = "",
|
|
323
|
+
source_commit: str = "main",
|
|
324
|
+
_user_name: str = "",
|
|
325
|
+
_user_email: str = "",
|
|
326
|
+
_topic: str = "",
|
|
327
|
+
client: httpx.AsyncClient | None = None,
|
|
328
|
+
) -> dict[str, Any]:
|
|
329
|
+
"""获取云端知识文档全文(POST /doc 接口,直连 retrieval 服务)。"""
|
|
330
|
+
should_close = False
|
|
331
|
+
if client is None:
|
|
332
|
+
client = httpx.AsyncClient(timeout=15.0)
|
|
333
|
+
should_close = True
|
|
334
|
+
|
|
335
|
+
try:
|
|
336
|
+
body: dict[str, Any] = {"doc_id": doc_id}
|
|
337
|
+
if canonical_path:
|
|
338
|
+
body["canonical_path"] = canonical_path
|
|
339
|
+
if source_commit and source_commit != "main":
|
|
340
|
+
body["source_commit"] = source_commit
|
|
341
|
+
if _user_name:
|
|
342
|
+
body["_user_name"] = _user_name
|
|
343
|
+
if _user_email:
|
|
344
|
+
body["_user_email"] = _user_email
|
|
345
|
+
resp = await client.post(
|
|
346
|
+
f"{CLOUD_DOC_URL}/doc",
|
|
347
|
+
json=body,
|
|
348
|
+
headers={"Content-Type": "application/json"},
|
|
349
|
+
)
|
|
350
|
+
resp.raise_for_status()
|
|
351
|
+
data = resp.json()
|
|
352
|
+
if data.get("error"):
|
|
353
|
+
raise ValueError(f"Document not found: {doc_id}")
|
|
354
|
+
return data
|
|
355
|
+
finally:
|
|
356
|
+
if should_close:
|
|
357
|
+
await client.aclose()
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
async def cloud_skill_get_files(
|
|
361
|
+
skill_id: str,
|
|
362
|
+
client: httpx.AsyncClient | None = None,
|
|
363
|
+
) -> dict[str, Any]:
|
|
364
|
+
"""获取云端 Skill 的文件全文(POST /get-files 接口)。
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
{"files": {"skill.md": {"content": "..."}}}, or {"files": {}, "error": "..."}
|
|
368
|
+
"""
|
|
369
|
+
should_close = False
|
|
370
|
+
if client is None:
|
|
371
|
+
client = httpx.AsyncClient(timeout=15.0)
|
|
372
|
+
should_close = True
|
|
373
|
+
|
|
374
|
+
try:
|
|
375
|
+
resp = await client.post(
|
|
376
|
+
f"{CLOUD_SKILL_URL}/get-files",
|
|
377
|
+
json={"skill_id": skill_id},
|
|
378
|
+
headers={"Content-Type": "application/json"},
|
|
379
|
+
)
|
|
380
|
+
resp.raise_for_status()
|
|
381
|
+
data = resp.json()
|
|
382
|
+
return data
|
|
383
|
+
except Exception as e:
|
|
384
|
+
return {"files": {}, "error": str(e)}
|
|
385
|
+
finally:
|
|
386
|
+
if should_close:
|
|
387
|
+
await client.aclose()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import yaml
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def parse_frontmatter(text: str) -> tuple[dict[str, Any], str]:
|
|
7
|
+
"""解析 Markdown 文件的 YAML frontmatter。
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
text: 文件原始内容。
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
(metadata, body) — metadata 为解析后的字典,body 为 frontmatter 之后的内容。
|
|
14
|
+
无 frontmatter 或解析失败时 metadata 为空字典,body 为原始文本。
|
|
15
|
+
"""
|
|
16
|
+
if not text.startswith("---"):
|
|
17
|
+
return {}, text
|
|
18
|
+
|
|
19
|
+
parts = text.split("---", 2)
|
|
20
|
+
if len(parts) < 3:
|
|
21
|
+
return {}, text
|
|
22
|
+
|
|
23
|
+
raw_yaml = parts[1].strip()
|
|
24
|
+
try:
|
|
25
|
+
meta = yaml.safe_load(raw_yaml) or {}
|
|
26
|
+
except Exception:
|
|
27
|
+
meta = {}
|
|
28
|
+
|
|
29
|
+
if not isinstance(meta, dict):
|
|
30
|
+
meta = {}
|
|
31
|
+
|
|
32
|
+
return meta, parts[2].strip()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def extract_title(meta: dict[str, Any], file_path: str) -> str:
|
|
36
|
+
"""从 frontmatter 中提取标题,回退到文件名。"""
|
|
37
|
+
for key in ("name", "title", "id"):
|
|
38
|
+
val = meta.get(key)
|
|
39
|
+
if val and isinstance(val, str):
|
|
40
|
+
return val
|
|
41
|
+
return file_path.rsplit("/", 1)[-1].rsplit(".", 1)[0]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def extract_description(meta: dict[str, Any], body: str, max_len: int = 160) -> str:
|
|
45
|
+
"""从 frontmatter 或正文提取描述。"""
|
|
46
|
+
desc = meta.get("description", "")
|
|
47
|
+
if desc and isinstance(desc, str):
|
|
48
|
+
return desc[:max_len]
|
|
49
|
+
first_line = body.split("\n", 1)[0].strip().strip("#").strip()
|
|
50
|
+
return first_line[:max_len] if first_line else ""
|