draftgo-cli 3.0.1 → 3.0.29
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 +67 -17
- package/package.json +13 -8
- package/resources/skill/SKILL.md +118 -22
- package/resources/skill/core/architecture.md +4 -24
- package/resources/skill/core/modules.md +14 -4
- package/resources/skill/init/SKILL.md +3 -4
- package/resources/skill/practices/anti-patterns.md +14 -4
- package/resources/skill/practices/best-practices.md +25 -6
- package/resources/skill/practices/dev-declaration.md +23 -3
- package/resources/skill/pull/SKILL.md +9 -1
- package/resources/skill/push/SKILL.md +103 -68
- package/resources/skill/quickref/api-endpoints.md +63 -41
- package/resources/skill/quickref/api.json +5084 -4975
- package/resources/skill/quickref/app-api.md +4 -14
- package/resources/skill/rules/dev-workflow.md +154 -57
- package/resources/skill/rules/frontend.md +569 -21
- package/resources/skill/rules/parallel.md +10 -10
- package/resources/skill/scripts/__pycache__/draftgo_pull.cpython-312.pyc +0 -0
- package/resources/skill/scripts/__pycache__/draftgo_push.cpython-312.pyc +0 -0
- package/resources/skill/scripts/draftgo_delete.py +0 -2
- package/resources/skill/scripts/draftgo_init.py +15 -3
- package/resources/skill/scripts/draftgo_pull.py +154 -87
- package/resources/skill/scripts/draftgo_push.py +363 -174
- package/resources/skill/specs/custom-services.md +199 -0
- package/resources/skill/specs/data.md +195 -5
- package/resources/skill/specs/db-relations.md +227 -0
- package/resources/skill/specs/runtime.md +30 -0
- package/resources/skill/specs/security.md +3 -3
- package/resources/skill/specs/ui-protocol.md +79 -48
- package/resources/skill/story/SKILL.md +2 -7
- package/src/cli.js +9 -0
- package/src/commands/api.js +59 -0
- package/src/commands/autoPush.js +41 -0
- package/src/commands/check.js +27 -17
- package/src/commands/delete.js +6 -4
- package/src/commands/deploy.js +31 -0
- package/src/commands/doctor.js +1 -1
- package/src/commands/help.js +27 -9
- package/src/commands/init.js +17 -2
- package/src/commands/map.js +18 -7
- package/src/commands/new.js +20 -17
- package/src/commands/sync.js +10 -3
- package/src/commands/update.js +15 -56
- package/src/commands/upgrade.js +52 -0
- package/src/commands/verifyUi.js +199 -0
- package/src/index.js +12 -1
- package/src/localdev/compose.js +8 -1
- package/src/platforms.js +3 -3
- package/src/projectConfig.js +11 -1
- package/src/projectMap.js +274 -39
- package/src/skill.js +113 -29
- package/src/updateCheck.js +37 -5
- package/resources/skill/quickref/dg-components.md +0 -198
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: draftgo-parallel-execution
|
|
3
|
-
description: DraftGo
|
|
3
|
+
description: DraftGo 并行开发协议。仅在当前任务按 dev-workflow 分级后确实需要多任务/多代理协作,且存在无依赖关系的任务组时启用。
|
|
4
4
|
version: 1.0.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# DraftGo 并行开发协议
|
|
8
8
|
|
|
9
|
-
> 本协议是 dev-workflow.md
|
|
9
|
+
> 本协议是 dev-workflow.md 的按需扩展,不是额外门禁。小修、轻功能和普通串行标准功能不启用;只有多任务、多资源或多代理协作确有收益,且存在 **2 个及以上无依赖任务** 时才启用。
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
@@ -14,7 +14,7 @@ version: 1.0.0
|
|
|
14
14
|
|
|
15
15
|
```
|
|
16
16
|
主代理(Orchestrator)
|
|
17
|
-
├──
|
|
17
|
+
├── 按 dev-workflow 完成任务分级与必要范围确认
|
|
18
18
|
├── 分析任务依赖图
|
|
19
19
|
├── 分发无依赖任务组给子代理
|
|
20
20
|
├── 收集子代理结果
|
|
@@ -32,7 +32,7 @@ version: 1.0.0
|
|
|
32
32
|
|
|
33
33
|
## 依赖分析规则
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
当 dev-workflow 判定需要 Task、并行或多资源协作时,为相关任务标注 `depends` 字段:
|
|
36
36
|
|
|
37
37
|
```markdown
|
|
38
38
|
- [ ] T1. 用户列表页增加导出按钮 ⬜
|
|
@@ -74,7 +74,7 @@ version: 1.0.0
|
|
|
74
74
|
|
|
75
75
|
### 主代理职责
|
|
76
76
|
|
|
77
|
-
1.
|
|
77
|
+
1. **短计划或 Task 明确后**,按依赖图将任务分为并行批次(waves):
|
|
78
78
|
```
|
|
79
79
|
Wave 1: [T1, T2, T3] — 全部 depends: [],可并行
|
|
80
80
|
Wave 2: [T4] — depends T1+T3,等 Wave 1 结束
|
|
@@ -116,7 +116,7 @@ version: 1.0.0
|
|
|
116
116
|
4. **收集结果后**:
|
|
117
117
|
- 将子代理产出的文件内容写入对应路径
|
|
118
118
|
- 汇总所有 changelog 条目,按时间排序写入 changelog.md
|
|
119
|
-
- 批量推送:`
|
|
119
|
+
- 批量推送:`draftgo auto-push --batch pages 1,2,3 nav 4`(由 `auto_push` 开关决定是否真正推送)
|
|
120
120
|
- 统一更新 Task 文档标记
|
|
121
121
|
- 进入闭环门
|
|
122
122
|
|
|
@@ -173,10 +173,10 @@ version: 1.0.0
|
|
|
173
173
|
并行执行完毕后,主代理使用批量推送减少网络开销:
|
|
174
174
|
|
|
175
175
|
```bash
|
|
176
|
-
|
|
176
|
+
draftgo auto-push --batch pages 1,2,3 nav 4 custom_scripts 7
|
|
177
177
|
```
|
|
178
178
|
|
|
179
|
-
|
|
179
|
+
开关为 `true` 时,等价于依次推送 pages 1、2、3 + nav 4 + custom_scripts 7,但一次加载 config、一次建立连接;为 `false` 时仅执行本地检查并返回未推送信息。
|
|
180
180
|
|
|
181
181
|
---
|
|
182
182
|
|
|
@@ -244,7 +244,7 @@ python {{SKILL_SCRIPTS}}/draftgo_push.py --batch pages 1,2,3 nav 4 custom_script
|
|
|
244
244
|
|
|
245
245
|
## 启用条件与判断
|
|
246
246
|
|
|
247
|
-
|
|
247
|
+
主代理在完成任务分级和必要范围确认后自动判断:
|
|
248
248
|
|
|
249
249
|
```
|
|
250
250
|
IF 无依赖任务数 >= 2
|
|
@@ -254,7 +254,7 @@ THEN 启用并行分发
|
|
|
254
254
|
ELSE 串行执行(静默,不告知用户)
|
|
255
255
|
```
|
|
256
256
|
|
|
257
|
-
|
|
257
|
+
不需要用户显式启用。用户也可以明确说"串行做"来关闭并行。
|
|
258
258
|
|
|
259
259
|
---
|
|
260
260
|
|
|
Binary file
|
|
Binary file
|
|
@@ -10,7 +10,6 @@ DraftGo Delete Script
|
|
|
10
10
|
python draftgo_delete.py custom_scripts <id>
|
|
11
11
|
python draftgo_delete.py docs <id>
|
|
12
12
|
python draftgo_delete.py doc_categories <id>
|
|
13
|
-
python draftgo_delete.py external_apis <id>
|
|
14
13
|
python draftgo_delete.py aihub <id>
|
|
15
14
|
"""
|
|
16
15
|
import json, sys
|
|
@@ -92,7 +91,6 @@ TYPE_MAP = {
|
|
|
92
91
|
"custom_scripts": ("/api/scripts", "custom_scripts/index.json", "id", "code_file"),
|
|
93
92
|
"docs": ("/api/docs/articles", "docs/articles/index.json", "id", "content_file"),
|
|
94
93
|
"doc_categories": ("/api/docs/categories", "doc_categories/index.json", "id", None),
|
|
95
|
-
"external_apis": ("/api/external-apis", "external_apis/index.json", "id", None),
|
|
96
94
|
"aihub": ("/api/aihub", "aihub/index.json", "id", None),
|
|
97
95
|
}
|
|
98
96
|
|
|
@@ -38,9 +38,21 @@ def main():
|
|
|
38
38
|
dg_dir = root / ".draftgo"
|
|
39
39
|
dg_dir.mkdir(parents=True, exist_ok=True)
|
|
40
40
|
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
# 写入配置。重复初始化时保留用户明确启用的自动推送开关。
|
|
42
|
+
config_path = dg_dir / "config.json"
|
|
43
|
+
old_cfg = {}
|
|
44
|
+
if config_path.exists():
|
|
45
|
+
try:
|
|
46
|
+
old_cfg = json.loads(config_path.read_text(encoding="utf-8"))
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
cfg = {
|
|
50
|
+
"server": server,
|
|
51
|
+
"token": token,
|
|
52
|
+
"lessons_on_push": old_cfg.get("lessons_on_push", True),
|
|
53
|
+
"auto_push": old_cfg.get("auto_push", False) is True,
|
|
54
|
+
}
|
|
55
|
+
config_path.write_text(
|
|
44
56
|
json.dumps(cfg, indent=2),
|
|
45
57
|
encoding="utf-8",
|
|
46
58
|
)
|
|
@@ -8,7 +8,6 @@ DraftGo Pull Script
|
|
|
8
8
|
python draftgo_pull.py nav [nav_id ...]
|
|
9
9
|
python draftgo_pull.py db_meta [db_meta_id ...]
|
|
10
10
|
python draftgo_pull.py aihub [aihub_id ...]
|
|
11
|
-
python draftgo_pull.py external_apis [api_id ...]
|
|
12
11
|
python draftgo_pull.py system_config [config_key ...]
|
|
13
12
|
python draftgo_pull.py docs [article_id ...]
|
|
14
13
|
python draftgo_pull.py doc_categories [category_id ...]
|
|
@@ -19,7 +18,7 @@ DraftGo Pull Script
|
|
|
19
18
|
"""
|
|
20
19
|
import json, sys
|
|
21
20
|
from pathlib import Path
|
|
22
|
-
import urllib.request, urllib.error
|
|
21
|
+
import urllib.request, urllib.error, urllib.parse
|
|
23
22
|
|
|
24
23
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
25
24
|
|
|
@@ -43,28 +42,18 @@ DEFAULT_ROOT = find_project_root(SCRIPT_DIR)
|
|
|
43
42
|
|
|
44
43
|
# ── 端点映射 ──
|
|
45
44
|
ENDPOINTS = {
|
|
46
|
-
"pages": "/api/pages
|
|
45
|
+
"pages": "/api/pages/",
|
|
47
46
|
"navigations": "/api/navigations",
|
|
48
47
|
"roles": "/api/roles",
|
|
49
|
-
"users": "/api/users
|
|
48
|
+
"users": "/api/users",
|
|
50
49
|
"db_meta": "/api/db-meta",
|
|
51
|
-
"aihub": "/api/aihub
|
|
52
|
-
"external_apis": "/api/external-apis?page=1&page_size=500",
|
|
50
|
+
"aihub": "/api/aihub",
|
|
53
51
|
"system_config": "/api/system/config",
|
|
54
52
|
"doc_categories": "/api/docs/categories?flat=true",
|
|
55
|
-
"docs": "/api/docs/admin/articles
|
|
56
|
-
"custom_scripts": "/api/scripts
|
|
53
|
+
"docs": "/api/docs/admin/articles",
|
|
54
|
+
"custom_scripts": "/api/scripts/",
|
|
57
55
|
}
|
|
58
56
|
|
|
59
|
-
_LANG_EXT = {
|
|
60
|
-
"python": "py", "py": "py",
|
|
61
|
-
"javascript": "js", "js": "js",
|
|
62
|
-
"typescript": "ts", "ts": "ts",
|
|
63
|
-
"shell": "sh", "bash": "sh",
|
|
64
|
-
"go": "go",
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
57
|
# ── 通用工具函数 ──
|
|
69
58
|
|
|
70
59
|
def load_config(root=None):
|
|
@@ -129,7 +118,31 @@ def safe_slug(s):
|
|
|
129
118
|
return str(s).strip("/").replace("/", "_") or "root"
|
|
130
119
|
|
|
131
120
|
|
|
132
|
-
def write_index(folder, items):
|
|
121
|
+
def write_index(folder, items, preserve_existing=False, id_key="id"):
|
|
122
|
+
if preserve_existing:
|
|
123
|
+
index_path = folder / "index.json"
|
|
124
|
+
try:
|
|
125
|
+
existing = json.loads(index_path.read_text(encoding="utf-8"))
|
|
126
|
+
if not isinstance(existing, list):
|
|
127
|
+
existing = []
|
|
128
|
+
except (OSError, json.JSONDecodeError):
|
|
129
|
+
existing = []
|
|
130
|
+
replacements = {
|
|
131
|
+
str(item.get(id_key)): item for item in items if item.get(id_key) is not None
|
|
132
|
+
}
|
|
133
|
+
retained = []
|
|
134
|
+
seen = set()
|
|
135
|
+
for item in existing:
|
|
136
|
+
key = str(item.get(id_key)) if item.get(id_key) is not None else None
|
|
137
|
+
if key in replacements:
|
|
138
|
+
retained.append(replacements[key])
|
|
139
|
+
seen.add(key)
|
|
140
|
+
else:
|
|
141
|
+
retained.append(item)
|
|
142
|
+
items = retained + [
|
|
143
|
+
item for item in items
|
|
144
|
+
if item.get(id_key) is None or str(item.get(id_key)) not in seen
|
|
145
|
+
]
|
|
133
146
|
folder.mkdir(parents=True, exist_ok=True)
|
|
134
147
|
(folder / "index.json").write_text(
|
|
135
148
|
json.dumps(items, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
@@ -138,7 +151,7 @@ def write_index(folder, items):
|
|
|
138
151
|
|
|
139
152
|
# ── 各类型的 explode 逻辑 ──
|
|
140
153
|
|
|
141
|
-
def explode_pages(out_dir, items):
|
|
154
|
+
def explode_pages(out_dir, items, preserve_existing=False):
|
|
142
155
|
index = []
|
|
143
156
|
for p in items:
|
|
144
157
|
pid = p.get("id", "x")
|
|
@@ -148,66 +161,95 @@ def explode_pages(out_dir, items):
|
|
|
148
161
|
meta = {k: v for k, v in p.items() if k != "value"}
|
|
149
162
|
meta["html_file"] = f".draftgo/pages/{fname}"
|
|
150
163
|
index.append(meta)
|
|
151
|
-
write_index(out_dir, index)
|
|
164
|
+
write_index(out_dir, index, preserve_existing)
|
|
152
165
|
return len(index)
|
|
153
166
|
|
|
154
167
|
|
|
155
|
-
def explode_navigations(out_dir, items):
|
|
168
|
+
def explode_navigations(out_dir, items, preserve_existing=False):
|
|
156
169
|
index = []
|
|
157
170
|
for item in items:
|
|
158
171
|
iid = item.get("id", "x")
|
|
159
172
|
name_slug = safe_slug(item.get("code") or item.get("route") or iid)
|
|
160
173
|
fname = f"nav_{iid}_{name_slug}.html"
|
|
161
|
-
|
|
174
|
+
# 列表接口只返回导航元数据。详情拉取失败时不能以空内容覆盖本地导航。
|
|
175
|
+
if isinstance(item.get("html"), str):
|
|
176
|
+
(out_dir / fname).write_text(item["html"], encoding="utf-8")
|
|
177
|
+
else:
|
|
178
|
+
print(
|
|
179
|
+
f" WARN nav_id={iid} 未取得 HTML,保留本地文件: {fname}",
|
|
180
|
+
file=sys.stderr,
|
|
181
|
+
)
|
|
162
182
|
meta = {k: v for k, v in item.items() if k != "html"}
|
|
163
183
|
meta["html_file"] = f".draftgo/navigations/{fname}"
|
|
164
184
|
index.append(meta)
|
|
165
|
-
write_index(out_dir, index)
|
|
185
|
+
write_index(out_dir, index, preserve_existing)
|
|
166
186
|
return len(index)
|
|
167
187
|
|
|
168
188
|
|
|
169
|
-
def explode_docs(out_dir, items):
|
|
189
|
+
def explode_docs(out_dir, items, preserve_existing=False):
|
|
170
190
|
index = []
|
|
171
191
|
for art in items:
|
|
172
192
|
aid = art.get("id", "x")
|
|
173
193
|
slug = safe_slug(art.get("slug") or art.get("title") or aid)
|
|
174
|
-
fname = f"article_{aid}_{slug}.
|
|
194
|
+
fname = f"article_{aid}_{slug}.html"
|
|
175
195
|
content = art.get("content") or ""
|
|
176
196
|
(out_dir / fname).write_text(content, encoding="utf-8")
|
|
177
197
|
meta = {k: v for k, v in art.items() if k != "content"}
|
|
178
198
|
meta["content_file"] = f".draftgo/docs/articles/{fname}"
|
|
179
199
|
index.append(meta)
|
|
180
|
-
write_index(out_dir, index)
|
|
200
|
+
write_index(out_dir, index, preserve_existing)
|
|
181
201
|
return len(index)
|
|
182
202
|
|
|
183
203
|
|
|
184
|
-
def explode_scripts(out_dir, items):
|
|
204
|
+
def explode_scripts(out_dir, items, preserve_existing=False):
|
|
185
205
|
index = []
|
|
186
206
|
for sc in items:
|
|
187
207
|
sid = sc.get("id", "x")
|
|
188
208
|
slug = safe_slug(sc.get("slug") or sc.get("name") or sid)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
209
|
+
fname = f"script_{sid}_{slug}.go"
|
|
210
|
+
# 列表接口不含 code;详情拉取失败时绝不能用空内容覆盖受管源码。
|
|
211
|
+
if "code" in sc:
|
|
212
|
+
(out_dir / fname).write_text(sc["code"], encoding="utf-8")
|
|
213
|
+
else:
|
|
214
|
+
print(f" WARN script_id={sid} 未取得代码,保留本地文件: {fname}", file=sys.stderr)
|
|
215
|
+
meta = {k: v for k, v in sc.items() if k not in ("code", "language")}
|
|
193
216
|
meta["code_file"] = f".draftgo/custom_scripts/{fname}"
|
|
194
217
|
index.append(meta)
|
|
195
|
-
write_index(out_dir, index)
|
|
218
|
+
write_index(out_dir, index, preserve_existing)
|
|
196
219
|
return len(index)
|
|
197
220
|
|
|
198
221
|
|
|
199
222
|
# ── 按类型拉取 ──
|
|
200
223
|
|
|
224
|
+
def select_items(items, ids, id_key, type_name):
|
|
225
|
+
"""筛选指定目标,并拒绝用空结果覆盖现有索引。"""
|
|
226
|
+
if not ids:
|
|
227
|
+
return items
|
|
228
|
+
wanted = [str(value) for value in ids]
|
|
229
|
+
found = {str(item.get(id_key)) for item in items if item.get(id_key) is not None}
|
|
230
|
+
missing = [value for value in wanted if value not in found]
|
|
231
|
+
if missing:
|
|
232
|
+
print(
|
|
233
|
+
f" ERR {type_name} 云端未返回目标 {id_key}: {', '.join(missing)}",
|
|
234
|
+
file=sys.stderr,
|
|
235
|
+
)
|
|
236
|
+
return None
|
|
237
|
+
return [item for item in items if str(item.get(id_key)) in wanted]
|
|
238
|
+
|
|
201
239
|
def pull_pages(server, token, root, ids=None):
|
|
202
240
|
raw = fetch(server, token, ENDPOINTS["pages"])
|
|
203
241
|
if raw is None:
|
|
204
|
-
return
|
|
242
|
+
return None
|
|
205
243
|
items = extract_items(raw)
|
|
206
|
-
|
|
207
|
-
|
|
244
|
+
items = select_items(items, ids, "id", "pages")
|
|
245
|
+
if items is None:
|
|
246
|
+
return None
|
|
247
|
+
if any(not isinstance(p.get("value"), dict) or not isinstance(p["value"].get("html"), str) for p in items):
|
|
248
|
+
print(" ERR pages 响应缺少 HTML,未写入本地缓存", file=sys.stderr)
|
|
249
|
+
return None
|
|
208
250
|
out = root / ".draftgo/pages"
|
|
209
251
|
out.mkdir(parents=True, exist_ok=True)
|
|
210
|
-
count = explode_pages(out, items)
|
|
252
|
+
count = explode_pages(out, items, preserve_existing=bool(ids))
|
|
211
253
|
print(f" OK pages ({count} items)")
|
|
212
254
|
return count
|
|
213
255
|
|
|
@@ -215,13 +257,33 @@ def pull_pages(server, token, root, ids=None):
|
|
|
215
257
|
def pull_navigations(server, token, root, ids=None):
|
|
216
258
|
raw = fetch(server, token, ENDPOINTS["navigations"])
|
|
217
259
|
if raw is None:
|
|
218
|
-
return
|
|
260
|
+
return None
|
|
219
261
|
items = extract_items(raw)
|
|
220
|
-
|
|
221
|
-
|
|
262
|
+
items = select_items(items, ids, "id", "navigations")
|
|
263
|
+
if items is None:
|
|
264
|
+
return None
|
|
265
|
+
hydrated = []
|
|
266
|
+
for item in items:
|
|
267
|
+
code = item.get("code")
|
|
268
|
+
if not code:
|
|
269
|
+
print(
|
|
270
|
+
f" ERR nav_id={item.get('id', '?')} 缺少 code,未写入本地缓存",
|
|
271
|
+
file=sys.stderr,
|
|
272
|
+
)
|
|
273
|
+
return None
|
|
274
|
+
detail = unwrap(fetch(
|
|
275
|
+
server, token, f"/api/navigations/{urllib.parse.quote(str(code), safe='')}"
|
|
276
|
+
))
|
|
277
|
+
if not isinstance(detail, dict) or not isinstance(detail.get("html"), str):
|
|
278
|
+
print(
|
|
279
|
+
f" ERR nav_id={item.get('id', '?')} 未取得 HTML,未写入本地缓存",
|
|
280
|
+
file=sys.stderr,
|
|
281
|
+
)
|
|
282
|
+
return None
|
|
283
|
+
hydrated.append({**item, **detail})
|
|
222
284
|
out = root / ".draftgo/navigations"
|
|
223
285
|
out.mkdir(parents=True, exist_ok=True)
|
|
224
|
-
count = explode_navigations(out,
|
|
286
|
+
count = explode_navigations(out, hydrated, preserve_existing=bool(ids))
|
|
225
287
|
print(f" OK navigations ({count} items)")
|
|
226
288
|
return count
|
|
227
289
|
|
|
@@ -230,50 +292,40 @@ def pull_simple(server, token, root, type_name, ids=None):
|
|
|
230
292
|
"""拉取仅需 index.json 的简单类型(db_meta, aihub, roles, users, system_config)。"""
|
|
231
293
|
raw = fetch(server, token, ENDPOINTS[type_name])
|
|
232
294
|
if raw is None:
|
|
233
|
-
return
|
|
295
|
+
return None
|
|
234
296
|
items = extract_items(raw)
|
|
235
|
-
if
|
|
236
|
-
|
|
237
|
-
|
|
297
|
+
id_key = "config_key" if type_name == "system_config" else "id"
|
|
298
|
+
items = select_items(items, ids, id_key, type_name)
|
|
299
|
+
if items is None:
|
|
300
|
+
return None
|
|
238
301
|
out = root / f".draftgo/{type_name}"
|
|
239
|
-
write_index(out, items)
|
|
302
|
+
write_index(out, items, preserve_existing=bool(ids), id_key=id_key)
|
|
240
303
|
print(f" OK {type_name} ({len(items)} items)")
|
|
241
304
|
return len(items)
|
|
242
305
|
|
|
243
306
|
|
|
244
|
-
def pull_external_apis(server, token, root, ids=None):
|
|
245
|
-
raw = fetch(server, token, ENDPOINTS["external_apis"])
|
|
246
|
-
if raw is None:
|
|
247
|
-
return 0
|
|
248
|
-
items = extract_items(raw)
|
|
249
|
-
if ids:
|
|
250
|
-
items = [it for it in items if str(it.get("id")) in ids]
|
|
251
|
-
hydrated = []
|
|
252
|
-
for it in items:
|
|
253
|
-
api_id = it.get("id")
|
|
254
|
-
detail = unwrap(fetch(server, token, f"/api/external-apis/{api_id}")) if api_id else None
|
|
255
|
-
hydrated.append({**it, **detail} if isinstance(detail, dict) else it)
|
|
256
|
-
out = root / ".draftgo/external_apis"
|
|
257
|
-
write_index(out, hydrated)
|
|
258
|
-
print(f" OK external_apis ({len(hydrated)} items)")
|
|
259
|
-
return len(hydrated)
|
|
260
|
-
|
|
261
|
-
|
|
262
307
|
def pull_docs(server, token, root, ids=None):
|
|
263
308
|
raw = fetch(server, token, ENDPOINTS["docs"])
|
|
264
309
|
if raw is None:
|
|
265
|
-
return
|
|
310
|
+
return None
|
|
266
311
|
items = extract_items(raw)
|
|
267
|
-
|
|
268
|
-
|
|
312
|
+
items = select_items(items, ids, "id", "docs")
|
|
313
|
+
if items is None:
|
|
314
|
+
return None
|
|
269
315
|
hydrated = []
|
|
270
316
|
for it in items:
|
|
271
317
|
aid = it.get("id")
|
|
272
318
|
detail = unwrap(fetch(server, token, f"/api/docs/articles/{aid}")) if aid else None
|
|
273
|
-
|
|
319
|
+
if not isinstance(detail, dict) or not isinstance(detail.get("content"), str):
|
|
320
|
+
print(
|
|
321
|
+
f" ERR article_id={aid or '?'} 未取得正文,未写入本地缓存",
|
|
322
|
+
file=sys.stderr,
|
|
323
|
+
)
|
|
324
|
+
return None
|
|
325
|
+
hydrated.append({**it, **detail})
|
|
274
326
|
out = root / ".draftgo/docs/articles"
|
|
275
327
|
out.mkdir(parents=True, exist_ok=True)
|
|
276
|
-
count = explode_docs(out, hydrated)
|
|
328
|
+
count = explode_docs(out, hydrated, preserve_existing=bool(ids))
|
|
277
329
|
print(f" OK docs ({count} articles)")
|
|
278
330
|
return count
|
|
279
331
|
|
|
@@ -281,12 +333,13 @@ def pull_docs(server, token, root, ids=None):
|
|
|
281
333
|
def pull_doc_categories(server, token, root, ids=None):
|
|
282
334
|
raw = fetch(server, token, ENDPOINTS["doc_categories"])
|
|
283
335
|
if raw is None:
|
|
284
|
-
return
|
|
336
|
+
return None
|
|
285
337
|
items = extract_items(raw)
|
|
286
|
-
|
|
287
|
-
|
|
338
|
+
items = select_items(items, ids, "id", "doc_categories")
|
|
339
|
+
if items is None:
|
|
340
|
+
return None
|
|
288
341
|
out = root / ".draftgo/doc_categories"
|
|
289
|
-
write_index(out, items)
|
|
342
|
+
write_index(out, items, preserve_existing=bool(ids))
|
|
290
343
|
print(f" OK doc_categories ({len(items)} items)")
|
|
291
344
|
return len(items)
|
|
292
345
|
|
|
@@ -294,14 +347,26 @@ def pull_doc_categories(server, token, root, ids=None):
|
|
|
294
347
|
def pull_custom_scripts(server, token, root, ids=None):
|
|
295
348
|
raw = fetch(server, token, ENDPOINTS["custom_scripts"])
|
|
296
349
|
if raw is None:
|
|
297
|
-
return
|
|
350
|
+
return None
|
|
298
351
|
items = extract_items(raw)
|
|
299
|
-
|
|
300
|
-
|
|
352
|
+
items = select_items(items, ids, "id", "custom_scripts")
|
|
353
|
+
if items is None:
|
|
354
|
+
return None
|
|
355
|
+
hydrated = []
|
|
356
|
+
for it in items:
|
|
357
|
+
script_id = it.get("id")
|
|
358
|
+
detail = unwrap(fetch(server, token, f"/api/scripts/{script_id}")) if script_id else None
|
|
359
|
+
if not isinstance(detail, dict) or not isinstance(detail.get("code"), str):
|
|
360
|
+
print(
|
|
361
|
+
f" ERR script_id={script_id or '?'} 未取得代码,未写入本地缓存",
|
|
362
|
+
file=sys.stderr,
|
|
363
|
+
)
|
|
364
|
+
return None
|
|
365
|
+
hydrated.append({**it, **detail})
|
|
301
366
|
out = root / ".draftgo/custom_scripts"
|
|
302
367
|
out.mkdir(parents=True, exist_ok=True)
|
|
303
|
-
count = explode_scripts(out,
|
|
304
|
-
print(f" OK custom_scripts ({count} scripts)")
|
|
368
|
+
count = explode_scripts(out, hydrated, preserve_existing=bool(ids))
|
|
369
|
+
print(f" OK custom_scripts ({count} scripts, code from details)")
|
|
305
370
|
return count
|
|
306
371
|
|
|
307
372
|
|
|
@@ -316,7 +381,6 @@ PULL_HANDLERS = {
|
|
|
316
381
|
"roles": lambda s, t, r, ids=None: pull_simple(s, t, r, "roles", ids),
|
|
317
382
|
"users": lambda s, t, r, ids=None: pull_simple(s, t, r, "users", ids),
|
|
318
383
|
"system_config": lambda s, t, r, ids=None: pull_simple(s, t, r, "system_config", ids),
|
|
319
|
-
"external_apis": pull_external_apis,
|
|
320
384
|
"docs": pull_docs,
|
|
321
385
|
"doc_categories": pull_doc_categories,
|
|
322
386
|
"custom_scripts": pull_custom_scripts,
|
|
@@ -325,14 +389,15 @@ PULL_HANDLERS = {
|
|
|
325
389
|
|
|
326
390
|
def pull_all(server, token, root):
|
|
327
391
|
"""拉取全部类型,供 init 复用。"""
|
|
328
|
-
pull_pages(server, token, root)
|
|
329
|
-
pull_navigations(server, token, root)
|
|
392
|
+
results = [pull_pages(server, token, root), pull_navigations(server, token, root)]
|
|
330
393
|
for t in ("db_meta", "aihub", "roles", "users", "system_config"):
|
|
331
|
-
pull_simple(server, token, root, t)
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
394
|
+
results.append(pull_simple(server, token, root, t))
|
|
395
|
+
results.extend([
|
|
396
|
+
pull_doc_categories(server, token, root),
|
|
397
|
+
pull_docs(server, token, root),
|
|
398
|
+
pull_custom_scripts(server, token, root),
|
|
399
|
+
])
|
|
400
|
+
return None if any(result is None for result in results) else results
|
|
336
401
|
|
|
337
402
|
|
|
338
403
|
def main():
|
|
@@ -345,7 +410,8 @@ def main():
|
|
|
345
410
|
root = DEFAULT_ROOT
|
|
346
411
|
|
|
347
412
|
if mode == "--all":
|
|
348
|
-
pull_all(server, token, root)
|
|
413
|
+
if pull_all(server, token, root) is None:
|
|
414
|
+
sys.exit(1)
|
|
349
415
|
return
|
|
350
416
|
|
|
351
417
|
if mode not in PULL_HANDLERS:
|
|
@@ -353,7 +419,8 @@ def main():
|
|
|
353
419
|
sys.exit(1)
|
|
354
420
|
|
|
355
421
|
ids = sys.argv[2:] or None
|
|
356
|
-
PULL_HANDLERS[mode](server, token, root, ids)
|
|
422
|
+
if PULL_HANDLERS[mode](server, token, root, ids) is None:
|
|
423
|
+
sys.exit(1)
|
|
357
424
|
|
|
358
425
|
|
|
359
426
|
if __name__ == "__main__":
|