draftgo-cli 3.0.0 → 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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"""
|
|
3
3
|
DraftGo Push Script
|
|
4
4
|
推送本地修改到云端。覆盖所有 init 拉取的类型:
|
|
5
|
-
pages / nav / db_meta / aihub /
|
|
5
|
+
pages / nav / db_meta / aihub / system_config / roles / users
|
|
6
6
|
docs / doc_categories / custom_scripts
|
|
7
7
|
|
|
8
8
|
用法:
|
|
@@ -10,19 +10,20 @@ DraftGo Push Script
|
|
|
10
10
|
python draftgo_push.py nav [nav_id ...]
|
|
11
11
|
python draftgo_push.py db_meta [db_meta_id ...]
|
|
12
12
|
python draftgo_push.py aihub [aihub_id ...]
|
|
13
|
-
python draftgo_push.py external_apis [api_id ...]
|
|
14
13
|
python draftgo_push.py system_config [config_key ...]
|
|
15
14
|
python draftgo_push.py docs [article_id ...]
|
|
16
15
|
python draftgo_push.py doc_categories [category_id ...]
|
|
17
16
|
python draftgo_push.py custom_scripts [script_id ...]
|
|
18
|
-
python draftgo_push.py roles [role_id ...]
|
|
19
|
-
python draftgo_push.py users [user_id ...]
|
|
17
|
+
python draftgo_push.py roles [role_id ...]
|
|
18
|
+
python draftgo_push.py users [user_id ...]
|
|
20
19
|
"""
|
|
21
20
|
import json, sys, re
|
|
22
21
|
from pathlib import Path
|
|
23
22
|
import urllib.request, urllib.error
|
|
24
23
|
|
|
25
24
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
25
|
+
PROBE_ROUTES = False
|
|
26
|
+
DRY_RUN = False
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
def find_project_root(start: Path) -> Path:
|
|
@@ -43,46 +44,6 @@ def find_project_root(start: Path) -> Path:
|
|
|
43
44
|
|
|
44
45
|
DEFAULT_ROOT = find_project_root(SCRIPT_DIR)
|
|
45
46
|
|
|
46
|
-
# ── 冲突检测 ──
|
|
47
|
-
FORCE = False
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def fetch_get(server, token, path):
|
|
51
|
-
"""GET 请求,返回解析后的 JSON 或 None。"""
|
|
52
|
-
ok, _status, body = api_call("GET", server, token, path)
|
|
53
|
-
if not ok:
|
|
54
|
-
return None
|
|
55
|
-
try:
|
|
56
|
-
return json.loads(body)
|
|
57
|
-
except Exception:
|
|
58
|
-
return None
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def check_conflict(server, token, path, local_updated_at, label):
|
|
62
|
-
"""检查云端 updated_at 是否与本地基线一致。返回 True 表示可以继续推送。"""
|
|
63
|
-
if FORCE:
|
|
64
|
-
return True
|
|
65
|
-
if not local_updated_at:
|
|
66
|
-
return True
|
|
67
|
-
raw = fetch_get(server, token, path)
|
|
68
|
-
if raw is None:
|
|
69
|
-
print(f" WARN [{label}] 无法获取云端状态,跳过冲突检测")
|
|
70
|
-
return True
|
|
71
|
-
data = raw.get("data") if isinstance(raw, dict) else None
|
|
72
|
-
if not isinstance(data, dict):
|
|
73
|
-
return True
|
|
74
|
-
remote_updated = data.get("updated_at")
|
|
75
|
-
if not remote_updated:
|
|
76
|
-
return True
|
|
77
|
-
if str(remote_updated) != str(local_updated_at):
|
|
78
|
-
print(f" SKIP [{label}] 云端已被他人修改")
|
|
79
|
-
print(f" 本地基线: {local_updated_at}")
|
|
80
|
-
print(f" 云端时间: {remote_updated}")
|
|
81
|
-
print(f" 请先 pull 同步后再 push,或使用 --force 强制覆盖")
|
|
82
|
-
return False
|
|
83
|
-
return True
|
|
84
|
-
|
|
85
|
-
|
|
86
47
|
def load_config():
|
|
87
48
|
cfg_path = DEFAULT_ROOT / ".draftgo/config.json"
|
|
88
49
|
if not cfg_path.exists():
|
|
@@ -93,6 +54,8 @@ def load_config():
|
|
|
93
54
|
|
|
94
55
|
|
|
95
56
|
def _lessons_reminder(cfg):
|
|
57
|
+
if DRY_RUN:
|
|
58
|
+
return
|
|
96
59
|
if not cfg.get("lessons_on_push", False):
|
|
97
60
|
return
|
|
98
61
|
print("")
|
|
@@ -159,8 +122,9 @@ def _extract_created_id(body_text):
|
|
|
159
122
|
def _writeback_index(rel_index, items):
|
|
160
123
|
"""将更新后的 index 列表写回 .draftgo/ 下的 index.json。"""
|
|
161
124
|
path = DEFAULT_ROOT / ".draftgo" / rel_index
|
|
162
|
-
if not path.exists():
|
|
125
|
+
if not path.exists() and not (DEFAULT_ROOT / ".draftgo").exists():
|
|
163
126
|
path = DEFAULT_ROOT / rel_index
|
|
127
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
164
128
|
path.write_text(json.dumps(items, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
165
129
|
|
|
166
130
|
|
|
@@ -185,22 +149,196 @@ def _rename_to_canonical(sub_dir, old_rel, prefix, new_id, slug_src, ext):
|
|
|
185
149
|
return old_rel
|
|
186
150
|
|
|
187
151
|
|
|
188
|
-
def _load_index(rel_index):
|
|
152
|
+
def _load_index(rel_index, allow_missing=False):
|
|
189
153
|
"""读取 .draftgo/ 下的 index.json;支持旧 (pages/index.json) 与新 (.draftgo/pages/index.json) 两种布局。"""
|
|
190
154
|
new_path = DEFAULT_ROOT / ".draftgo" / rel_index
|
|
191
155
|
legacy_path = DEFAULT_ROOT / rel_index
|
|
192
156
|
path = new_path if new_path.exists() else legacy_path
|
|
193
157
|
if not path.exists():
|
|
158
|
+
if allow_missing:
|
|
159
|
+
print(f" WARN {rel_index} 不存在,将尝试从云端恢复指定目标的索引")
|
|
160
|
+
return []
|
|
194
161
|
print(f"ERR: {rel_index} not found under .draftgo/, run /draftgo init first", file=sys.stderr)
|
|
195
162
|
sys.exit(1)
|
|
196
163
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
197
164
|
|
|
198
165
|
|
|
166
|
+
def _filter_items_by_ids(items, ids, id_key, type_name, orphan=None):
|
|
167
|
+
"""按用户指定 id 过滤;指定 id 不存在时直接失败,避免 0 目标假成功。"""
|
|
168
|
+
if not ids:
|
|
169
|
+
return items
|
|
170
|
+
wanted = [str(x) for x in ids]
|
|
171
|
+
found = {str(it.get(id_key)) for it in items if it.get(id_key) is not None}
|
|
172
|
+
missing = [x for x in wanted if x not in found]
|
|
173
|
+
if missing:
|
|
174
|
+
print(f"ERR: {type_name} index.json 未登记目标 {id_key}: {', '.join(missing)}", file=sys.stderr)
|
|
175
|
+
if orphan:
|
|
176
|
+
hints = orphan(missing)
|
|
177
|
+
if hints:
|
|
178
|
+
print(" 发现疑似未入索引的本地文件:", file=sys.stderr)
|
|
179
|
+
for hint in hints[:8]:
|
|
180
|
+
print(f" - {hint}", file=sys.stderr)
|
|
181
|
+
print(" 请先恢复/追加对应 index 元数据,或重新运行 pull 后再 push。", file=sys.stderr)
|
|
182
|
+
sys.exit(1)
|
|
183
|
+
return [it for it in items if str(it.get(id_key)) in wanted]
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _response_items(body):
|
|
187
|
+
"""解析列表接口的常见统一信封格式。"""
|
|
188
|
+
try:
|
|
189
|
+
raw = json.loads(body)
|
|
190
|
+
except Exception:
|
|
191
|
+
return []
|
|
192
|
+
if isinstance(raw, dict) and "data" in raw:
|
|
193
|
+
raw = raw["data"]
|
|
194
|
+
if isinstance(raw, list):
|
|
195
|
+
return raw
|
|
196
|
+
if isinstance(raw, dict):
|
|
197
|
+
for key in ("items", "results", "data"):
|
|
198
|
+
if isinstance(raw.get(key), list):
|
|
199
|
+
return raw[key]
|
|
200
|
+
return [raw]
|
|
201
|
+
return []
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _recover_missing_index_entries(
|
|
205
|
+
server, token, items, ids, id_key, type_name, rel_index, endpoint,
|
|
206
|
+
file_spec=None, drop_fields=(),
|
|
207
|
+
):
|
|
208
|
+
"""补齐缺失的 index 条目,且绝不覆盖本地受管内容。
|
|
209
|
+
|
|
210
|
+
只有显式指定的目标才可恢复,防止 `push --all` 把不明文件自动纳管。
|
|
211
|
+
file_spec 为 (目录, index 文件字段, 文件名前缀, 扩展名列表)。
|
|
212
|
+
"""
|
|
213
|
+
if not ids:
|
|
214
|
+
return items
|
|
215
|
+
wanted = [str(value) for value in ids]
|
|
216
|
+
registered = {str(item.get(id_key)) for item in items if item.get(id_key) is not None}
|
|
217
|
+
missing = [value for value in wanted if value not in registered]
|
|
218
|
+
if not missing:
|
|
219
|
+
return items
|
|
220
|
+
if DRY_RUN:
|
|
221
|
+
print(
|
|
222
|
+
f"ERR: {type_name} index.json 未登记目标 {id_key}: {', '.join(missing)}。"
|
|
223
|
+
"dry-run 不会回读云端或修改本地索引。",
|
|
224
|
+
file=sys.stderr,
|
|
225
|
+
)
|
|
226
|
+
sys.exit(1)
|
|
227
|
+
|
|
228
|
+
ok, status, body = api_call("GET", server, token, endpoint)
|
|
229
|
+
if not ok:
|
|
230
|
+
print(
|
|
231
|
+
f"ERR: 无法恢复 {type_name} index.json:云端回读失败 -> {_info(ok, status, body)}",
|
|
232
|
+
file=sys.stderr,
|
|
233
|
+
)
|
|
234
|
+
sys.exit(1)
|
|
235
|
+
remote_by_id = {
|
|
236
|
+
str(item.get(id_key)): item
|
|
237
|
+
for item in _response_items(body)
|
|
238
|
+
if isinstance(item, dict) and item.get(id_key) is not None
|
|
239
|
+
}
|
|
240
|
+
absent = [value for value in missing if value not in remote_by_id]
|
|
241
|
+
if absent:
|
|
242
|
+
print(
|
|
243
|
+
f"ERR: {type_name} 云端不存在目标 {id_key}: {', '.join(absent)}。"
|
|
244
|
+
"本地资源可能已成为孤儿文件,请确认后删除本地文件及其引用,"
|
|
245
|
+
"或改为新建资源推送。",
|
|
246
|
+
file=sys.stderr,
|
|
247
|
+
)
|
|
248
|
+
if file_spec:
|
|
249
|
+
rel_dir, _, prefix, exts = file_spec
|
|
250
|
+
hints = _orphan_hints(rel_dir, prefix, absent, exts)
|
|
251
|
+
for hint in hints:
|
|
252
|
+
print(f" 建议删除:{hint}", file=sys.stderr)
|
|
253
|
+
sys.exit(1)
|
|
254
|
+
|
|
255
|
+
recovered = []
|
|
256
|
+
for value in missing:
|
|
257
|
+
meta = dict(remote_by_id[value])
|
|
258
|
+
if file_spec:
|
|
259
|
+
rel_dir, file_field, prefix, exts = file_spec
|
|
260
|
+
candidates = _orphan_hints(rel_dir, prefix, [value], exts)
|
|
261
|
+
if len(candidates) != 1:
|
|
262
|
+
detail = "未找到" if not candidates else f"找到 {len(candidates)} 个"
|
|
263
|
+
print(
|
|
264
|
+
f"ERR: {type_name} {id_key}={value} 云端存在,但本地孤儿文件{detail},"
|
|
265
|
+
"无法安全补齐索引。请恢复唯一的本地文件或运行 pull 后重试。",
|
|
266
|
+
file=sys.stderr,
|
|
267
|
+
)
|
|
268
|
+
sys.exit(1)
|
|
269
|
+
meta[file_field] = candidates[0]
|
|
270
|
+
for field in drop_fields:
|
|
271
|
+
meta.pop(field, None)
|
|
272
|
+
items.append(meta)
|
|
273
|
+
recovered.append(value)
|
|
274
|
+
|
|
275
|
+
_writeback_index(rel_index, items)
|
|
276
|
+
print(f" OK {type_name}: 已从云端补齐 index.json 目标 {', '.join(recovered)},继续推送本地内容")
|
|
277
|
+
return items
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _orphan_hints(rel_dir, prefix, missing_ids, exts):
|
|
281
|
+
base = DEFAULT_ROOT / ".draftgo" / rel_dir
|
|
282
|
+
if not base.exists():
|
|
283
|
+
return []
|
|
284
|
+
hints = []
|
|
285
|
+
for mid in missing_ids:
|
|
286
|
+
for ext in exts:
|
|
287
|
+
for p in base.glob(f"{prefix}_{mid}_*{ext}"):
|
|
288
|
+
hints.append(f".draftgo/{rel_dir}/{p.name}")
|
|
289
|
+
return hints
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _warn_orphan_files(rel_dir, items, file_field, prefix, exts, type_name):
|
|
293
|
+
base = DEFAULT_ROOT / ".draftgo" / rel_dir
|
|
294
|
+
if not base.exists():
|
|
295
|
+
return
|
|
296
|
+
indexed = {str(it.get(file_field, "")).replace("\\", "/") for it in items}
|
|
297
|
+
orphans = []
|
|
298
|
+
for p in base.iterdir():
|
|
299
|
+
if not p.is_file():
|
|
300
|
+
continue
|
|
301
|
+
if not p.name.startswith(f"{prefix}_") or p.suffix not in exts:
|
|
302
|
+
continue
|
|
303
|
+
rel = f".draftgo/{rel_dir}/{p.name}"
|
|
304
|
+
if rel not in indexed:
|
|
305
|
+
orphans.append(rel)
|
|
306
|
+
if orphans:
|
|
307
|
+
print(f" WARN {type_name}: 发现 {len(orphans)} 个未登记到 index.json 的本地文件,本次不会推送:")
|
|
308
|
+
for rel in orphans[:8]:
|
|
309
|
+
print(f" - {rel}")
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _summary(type_name, targets):
|
|
313
|
+
print(f" SUMMARY {type_name}: targets={targets}")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _preview(type_name, items, id_key="id"):
|
|
317
|
+
"""打印不会产生云端或本地副作用的推送预览。"""
|
|
318
|
+
if not DRY_RUN:
|
|
319
|
+
return False
|
|
320
|
+
for item in items:
|
|
321
|
+
identity = item.get(id_key)
|
|
322
|
+
label = item.get("title") or item.get("name") or item.get("code") or identity or "(new)"
|
|
323
|
+
action = "UPDATE" if identity else "CREATE"
|
|
324
|
+
print(f" DRY-RUN {action} [{label}]")
|
|
325
|
+
_summary(type_name, len(items))
|
|
326
|
+
return True
|
|
327
|
+
|
|
328
|
+
|
|
199
329
|
def sync_pages(server, token, ids=None):
|
|
200
|
-
all_pages = _load_index("pages/index.json")
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
pages
|
|
330
|
+
all_pages = _load_index("pages/index.json", allow_missing=bool(ids))
|
|
331
|
+
all_pages = _recover_missing_index_entries(
|
|
332
|
+
server, token, all_pages, ids, "id", "pages", "pages/index.json", "/api/pages/",
|
|
333
|
+
file_spec=("pages", "html_file", "page", [".html"]), drop_fields=("value",),
|
|
334
|
+
)
|
|
335
|
+
_warn_orphan_files("pages", all_pages, "html_file", "page", [".html"], "pages")
|
|
336
|
+
pages = _filter_items_by_ids(
|
|
337
|
+
all_pages, ids, "id", "pages",
|
|
338
|
+
orphan=lambda missing: _orphan_hints("pages", "page", missing, [".html"]),
|
|
339
|
+
)
|
|
340
|
+
if _preview("pages", pages):
|
|
341
|
+
return
|
|
204
342
|
dirty = False
|
|
205
343
|
for page in pages:
|
|
206
344
|
pid = page.get("id")
|
|
@@ -221,8 +359,6 @@ def sync_pages(server, token, ids=None):
|
|
|
221
359
|
"value": {"html": html},
|
|
222
360
|
}
|
|
223
361
|
if pid:
|
|
224
|
-
if not check_conflict(server, token, f"/api/pages/{pid}", page.get("updated_at"), title):
|
|
225
|
-
continue
|
|
226
362
|
ok, status, body = api_call("PUT", server, token, f"/api/pages/{pid}", payload)
|
|
227
363
|
# PUT 404:本地 id 与云端不一致(删了重建 / 跨环境),按 route 自动创建
|
|
228
364
|
if not ok and status == 404:
|
|
@@ -245,13 +381,17 @@ def sync_pages(server, token, ids=None):
|
|
|
245
381
|
print(f" ERR [{title}] 创建失败 -> {_info(ok, status, body)}")
|
|
246
382
|
if dirty:
|
|
247
383
|
_writeback_index("pages/index.json", all_pages)
|
|
384
|
+
_summary("pages", len(pages))
|
|
248
385
|
|
|
249
386
|
|
|
250
387
|
def sync_db_meta(server, token, ids=None):
|
|
251
|
-
all_metas = _load_index("db_meta/index.json")
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
388
|
+
all_metas = _load_index("db_meta/index.json", allow_missing=bool(ids))
|
|
389
|
+
all_metas = _recover_missing_index_entries(
|
|
390
|
+
server, token, all_metas, ids, "id", "db_meta", "db_meta/index.json", "/api/db-meta",
|
|
391
|
+
)
|
|
392
|
+
metas = _filter_items_by_ids(all_metas, ids, "id", "db_meta")
|
|
393
|
+
if _preview("db_meta", metas):
|
|
394
|
+
return
|
|
255
395
|
dirty = False
|
|
256
396
|
for meta in metas:
|
|
257
397
|
mid = meta.get("id")
|
|
@@ -266,8 +406,6 @@ def sync_db_meta(server, token, ids=None):
|
|
|
266
406
|
"extra": meta.get("extra"),
|
|
267
407
|
}
|
|
268
408
|
if mid:
|
|
269
|
-
if not check_conflict(server, token, f"/api/db-meta/{mid}", meta.get("updated_at"), label):
|
|
270
|
-
continue
|
|
271
409
|
ok, status, body = api_call("PUT", server, token, f"/api/db-meta/{mid}", payload)
|
|
272
410
|
# 本地 index 的 id 与云端不一致(删了重建 / 跨环境同步)时 PUT 404,按 type 创建
|
|
273
411
|
if not ok and status == 404:
|
|
@@ -287,13 +425,22 @@ def sync_db_meta(server, token, ids=None):
|
|
|
287
425
|
print(f" ERR [{label}] 创建失败 -> {_info(ok, status, body)}")
|
|
288
426
|
if dirty:
|
|
289
427
|
_writeback_index("db_meta/index.json", all_metas)
|
|
428
|
+
_summary("db_meta", len(metas))
|
|
290
429
|
|
|
291
430
|
|
|
292
431
|
def sync_nav(server, token, ids=None):
|
|
293
|
-
all_navs = _load_index("navigations/index.json")
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
432
|
+
all_navs = _load_index("navigations/index.json", allow_missing=bool(ids))
|
|
433
|
+
all_navs = _recover_missing_index_entries(
|
|
434
|
+
server, token, all_navs, ids, "id", "nav", "navigations/index.json", "/api/navigations",
|
|
435
|
+
file_spec=("navigations", "html_file", "nav", [".html"]), drop_fields=("html",),
|
|
436
|
+
)
|
|
437
|
+
_warn_orphan_files("navigations", all_navs, "html_file", "nav", [".html"], "nav")
|
|
438
|
+
navs = _filter_items_by_ids(
|
|
439
|
+
all_navs, ids, "id", "nav",
|
|
440
|
+
orphan=lambda missing: _orphan_hints("navigations", "nav", missing, [".html"]),
|
|
441
|
+
)
|
|
442
|
+
if _preview("nav", navs):
|
|
443
|
+
return
|
|
297
444
|
dirty = False
|
|
298
445
|
for nav in navs:
|
|
299
446
|
nid = nav.get("id")
|
|
@@ -304,8 +451,6 @@ def sync_nav(server, token, ids=None):
|
|
|
304
451
|
continue
|
|
305
452
|
html = html_file.read_text(encoding="utf-8")
|
|
306
453
|
if nid:
|
|
307
|
-
if not check_conflict(server, token, f"/api/navigations/{nid}", nav.get("updated_at"), name):
|
|
308
|
-
continue
|
|
309
454
|
ok, status, body = api_call("PUT", server, token, f"/api/navigations/{nid}", {"html": html})
|
|
310
455
|
if not ok and status == 404:
|
|
311
456
|
print(f" WARN [{name}] nav_id={nid} 不存在,尝试创建")
|
|
@@ -336,13 +481,17 @@ def sync_nav(server, token, ids=None):
|
|
|
336
481
|
print(f" ERR [{name}] 创建失败 -> {_info(ok, status, body)}")
|
|
337
482
|
if dirty:
|
|
338
483
|
_writeback_index("navigations/index.json", all_navs)
|
|
484
|
+
_summary("nav", len(navs))
|
|
339
485
|
|
|
340
486
|
|
|
341
487
|
def sync_aihub(server, token, ids=None):
|
|
342
|
-
all_items = _load_index("aihub/index.json")
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
488
|
+
all_items = _load_index("aihub/index.json", allow_missing=bool(ids))
|
|
489
|
+
all_items = _recover_missing_index_entries(
|
|
490
|
+
server, token, all_items, ids, "id", "aihub", "aihub/index.json", "/api/aihub",
|
|
491
|
+
)
|
|
492
|
+
items = _filter_items_by_ids(all_items, ids, "id", "aihub")
|
|
493
|
+
if _preview("aihub", items):
|
|
494
|
+
return
|
|
346
495
|
dirty = False
|
|
347
496
|
for it in items:
|
|
348
497
|
iid = it.get("id")
|
|
@@ -353,8 +502,6 @@ def sync_aihub(server, token, ids=None):
|
|
|
353
502
|
"tags", "describe", "permission", "status",
|
|
354
503
|
) if it.get(k) is not None}
|
|
355
504
|
if iid:
|
|
356
|
-
if not check_conflict(server, token, f"/api/aihub/{iid}", it.get("updated_at"), name):
|
|
357
|
-
continue
|
|
358
505
|
ok, status, body = api_call("PUT", server, token, f"/api/aihub/{iid}", payload)
|
|
359
506
|
if not ok and status == 404:
|
|
360
507
|
print(f" WARN [{name}] aihub_id={iid} 不存在,尝试创建")
|
|
@@ -376,61 +523,25 @@ def sync_aihub(server, token, ids=None):
|
|
|
376
523
|
print(f" ERR [{name}] 创建失败 -> {_info(ok, status, body)}")
|
|
377
524
|
if dirty:
|
|
378
525
|
_writeback_index("aihub/index.json", all_items)
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
def sync_external_apis(server, token, ids=None):
|
|
382
|
-
all_items = _load_index("external_apis/index.json")
|
|
383
|
-
items = all_items
|
|
384
|
-
if ids:
|
|
385
|
-
items = [it for it in all_items if str(it.get("id")) in ids]
|
|
386
|
-
dirty = False
|
|
387
|
-
for it in items:
|
|
388
|
-
iid = it.get("id")
|
|
389
|
-
code = it.get("code", iid or "(new)")
|
|
390
|
-
# 服务端 ExternalAPIUpdate 接受字段
|
|
391
|
-
payload = {k: it.get(k) for k in (
|
|
392
|
-
"name", "base_url", "method", "path", "headers",
|
|
393
|
-
"auth_type", "auth_config", "timeout_ms", "permission",
|
|
394
|
-
"param_schema", "tags", "description", "status",
|
|
395
|
-
) if it.get(k) is not None}
|
|
396
|
-
if iid:
|
|
397
|
-
if not check_conflict(server, token, f"/api/external-apis/{iid}", it.get("updated_at"), code):
|
|
398
|
-
continue
|
|
399
|
-
ok, status, body = api_call("PUT", server, token, f"/api/external-apis/{iid}", payload)
|
|
400
|
-
if not ok and status == 404:
|
|
401
|
-
print(f" WARN [{code}] api_id={iid} 不存在,尝试创建")
|
|
402
|
-
iid = None
|
|
403
|
-
else:
|
|
404
|
-
print(f" {'OK' if ok else 'ERR'} [{code}] api_id={iid}{'' if ok else ' -> ' + _info(ok, status, body)}")
|
|
405
|
-
if not iid:
|
|
406
|
-
create_payload = {k: it.get(k) for k in (
|
|
407
|
-
"code", "name", "base_url", "method", "path", "headers",
|
|
408
|
-
"auth_type", "auth_config", "timeout_ms", "permission",
|
|
409
|
-
"param_schema", "tags", "description",
|
|
410
|
-
) if it.get(k) is not None}
|
|
411
|
-
ok, status, body = api_call("POST", server, token, "/api/external-apis", create_payload)
|
|
412
|
-
new_id = _extract_created_id(body) if ok else None
|
|
413
|
-
if ok and new_id:
|
|
414
|
-
it["id"] = new_id
|
|
415
|
-
dirty = True
|
|
416
|
-
print(f" OK [{code}] 已创建 api_id={new_id}(已回写 index)")
|
|
417
|
-
else:
|
|
418
|
-
print(f" ERR [{code}] 创建失败 -> {_info(ok, status, body)}")
|
|
419
|
-
if dirty:
|
|
420
|
-
_writeback_index("external_apis/index.json", all_items)
|
|
526
|
+
_summary("aihub", len(items))
|
|
421
527
|
|
|
422
528
|
|
|
423
529
|
def sync_system_config(server, token, keys=None):
|
|
424
|
-
items = _load_index("system_config/index.json")
|
|
425
|
-
|
|
426
|
-
|
|
530
|
+
items = _load_index("system_config/index.json", allow_missing=bool(keys))
|
|
531
|
+
items = _recover_missing_index_entries(
|
|
532
|
+
server, token, items, keys, "config_key", "system_config", "system_config/index.json", "/api/system/config",
|
|
533
|
+
)
|
|
534
|
+
items = _filter_items_by_ids(items, keys, "config_key", "system_config")
|
|
535
|
+
if _preview("system_config", items, "config_key"):
|
|
536
|
+
return
|
|
427
537
|
for it in items:
|
|
428
538
|
ck = it.get("config_key")
|
|
429
539
|
if not ck:
|
|
430
540
|
continue
|
|
431
541
|
# 优先使用 parsed_value(init 时 GET 返回的解析值),其次 config_value
|
|
432
542
|
value = it.get("parsed_value") if "parsed_value" in it else it.get("config_value")
|
|
433
|
-
|
|
543
|
+
value_payload = {"config_value": value}
|
|
544
|
+
meta_payload = {
|
|
434
545
|
"config_value": value,
|
|
435
546
|
"value_type": it.get("value_type"),
|
|
436
547
|
"category": it.get("category"),
|
|
@@ -438,23 +549,66 @@ def sync_system_config(server, token, keys=None):
|
|
|
438
549
|
"is_sensitive": it.get("is_sensitive"),
|
|
439
550
|
"status": it.get("status"),
|
|
440
551
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
552
|
+
meta_payload = {k: v for k, v in meta_payload.items() if v is not None}
|
|
553
|
+
# 前端全局层属于系统默认配置,默认字段的描述/分类/状态由基座维护。
|
|
554
|
+
# 推送时只更新值,避免旧 index 中的元信息触发“系统默认字段不允许修改字段描述”。
|
|
555
|
+
payload = value_payload if _is_frontend_global_config(it) else meta_payload
|
|
444
556
|
ok, status, body = api_call("PUT", server, token, f"/api/system/{ck}", payload)
|
|
557
|
+
if not ok and _is_protected_system_config_error(body) and payload != value_payload:
|
|
558
|
+
print(f" WARN [{ck}] 系统默认字段元信息受保护,改为仅推送 config_value")
|
|
559
|
+
ok, status, body = api_call("PUT", server, token, f"/api/system/{ck}", value_payload)
|
|
445
560
|
if not ok and status == 404:
|
|
446
561
|
print(f" WARN [{ck}] system_config 不存在,尝试创建")
|
|
447
|
-
create_payload =
|
|
562
|
+
create_payload = _system_config_create_payload(ck, it, value)
|
|
448
563
|
ok, status, body = api_call("POST", server, token, "/api/system/", create_payload)
|
|
449
564
|
print(f" {'OK' if ok else 'ERR'} [{ck}] 已创建{'' if ok else ' -> ' + _info(ok, status, body)}")
|
|
450
565
|
else:
|
|
451
566
|
print(f" {'OK' if ok else 'ERR'} [{ck}]{'' if ok else ' -> ' + _info(ok, status, body)}")
|
|
567
|
+
_summary("system_config", len(items))
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def _is_frontend_global_config(item):
|
|
571
|
+
return (
|
|
572
|
+
str(item.get("category") or "") == "frontend_global"
|
|
573
|
+
or str(item.get("config_key") or "").startswith("frontend_global_")
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def _is_protected_system_config_error(body):
|
|
578
|
+
text = body if isinstance(body, str) else json.dumps(body, ensure_ascii=False)
|
|
579
|
+
return "系统默认字段不允许修改字段" in text
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _system_config_create_payload(config_key, item, value):
|
|
583
|
+
return {
|
|
584
|
+
"config_key": config_key,
|
|
585
|
+
"config_value": value,
|
|
586
|
+
"value_type": item.get("value_type") or _infer_system_config_value_type(value),
|
|
587
|
+
"category": item.get("category") or "custom",
|
|
588
|
+
"description": item.get("description") or "",
|
|
589
|
+
"is_sensitive": bool(item.get("is_sensitive", False)),
|
|
590
|
+
"status": item.get("status") or "active",
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _infer_system_config_value_type(value):
|
|
595
|
+
if isinstance(value, bool):
|
|
596
|
+
return "bool"
|
|
597
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
598
|
+
return "int"
|
|
599
|
+
if isinstance(value, (dict, list)):
|
|
600
|
+
return "json"
|
|
601
|
+
return "string"
|
|
452
602
|
|
|
453
603
|
|
|
454
604
|
def sync_roles(server, token, ids=None):
|
|
455
|
-
items = _load_index("roles/index.json")
|
|
456
|
-
|
|
457
|
-
|
|
605
|
+
items = _load_index("roles/index.json", allow_missing=bool(ids))
|
|
606
|
+
items = _recover_missing_index_entries(
|
|
607
|
+
server, token, items, ids, "id", "roles", "roles/index.json", "/api/roles",
|
|
608
|
+
)
|
|
609
|
+
items = _filter_items_by_ids(items, ids, "id", "roles")
|
|
610
|
+
if _preview("roles", items):
|
|
611
|
+
return
|
|
458
612
|
for it in items:
|
|
459
613
|
rid = it.get("id")
|
|
460
614
|
code = it.get("code", rid)
|
|
@@ -462,16 +616,19 @@ def sync_roles(server, token, ids=None):
|
|
|
462
616
|
payload = {k: it.get(k) for k in (
|
|
463
617
|
"name", "description", "status", "sort_order", "user_visible",
|
|
464
618
|
) if it.get(k) is not None}
|
|
465
|
-
if not check_conflict(server, token, f"/api/roles/{rid}", it.get("updated_at"), code):
|
|
466
|
-
continue
|
|
467
619
|
ok, status, body = api_call("PUT", server, token, f"/api/roles/{rid}", payload)
|
|
468
620
|
print(f" {'OK' if ok else 'ERR'} [{code}] role_id={rid}{'' if ok else ' -> ' + _info(ok, status, body)}")
|
|
621
|
+
_summary("roles", len(items))
|
|
469
622
|
|
|
470
623
|
|
|
471
624
|
def sync_users(server, token, ids=None):
|
|
472
|
-
items = _load_index("users/index.json")
|
|
473
|
-
|
|
474
|
-
|
|
625
|
+
items = _load_index("users/index.json", allow_missing=bool(ids))
|
|
626
|
+
items = _recover_missing_index_entries(
|
|
627
|
+
server, token, items, ids, "id", "users", "users/index.json", "/api/users",
|
|
628
|
+
)
|
|
629
|
+
items = _filter_items_by_ids(items, ids, "id", "users")
|
|
630
|
+
if _preview("users", items):
|
|
631
|
+
return
|
|
475
632
|
for it in items:
|
|
476
633
|
uid = it.get("id")
|
|
477
634
|
uname = it.get("username", uid)
|
|
@@ -480,18 +637,25 @@ def sync_users(server, token, ids=None):
|
|
|
480
637
|
"username", "email", "phone_number", "nickname",
|
|
481
638
|
"avatar", "status", "notes",
|
|
482
639
|
) if it.get(k) is not None}
|
|
483
|
-
if not check_conflict(server, token, f"/api/users/{uid}", it.get("updated_at"), uname):
|
|
484
|
-
continue
|
|
485
640
|
ok, status, body = api_call("PUT", server, token, f"/api/users/{uid}", payload)
|
|
486
641
|
print(f" {'OK' if ok else 'ERR'} [{uname}] user_id={uid}{'' if ok else ' -> ' + _info(ok, status, body)}")
|
|
642
|
+
_summary("users", len(items))
|
|
487
643
|
|
|
488
644
|
|
|
489
645
|
def sync_docs(server, token, ids=None):
|
|
490
646
|
"""文档文章:从 .draftgo/docs/articles/index.json + 同目录 .md 文件回填正文。"""
|
|
491
|
-
all_items = _load_index("docs/articles/index.json")
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
647
|
+
all_items = _load_index("docs/articles/index.json", allow_missing=bool(ids))
|
|
648
|
+
all_items = _recover_missing_index_entries(
|
|
649
|
+
server, token, all_items, ids, "id", "docs", "docs/articles/index.json", "/api/docs/admin/articles",
|
|
650
|
+
file_spec=("docs/articles", "content_file", "article", [".html", ".md"]), drop_fields=("content",),
|
|
651
|
+
)
|
|
652
|
+
_warn_orphan_files("docs/articles", all_items, "content_file", "article", [".html", ".md"], "docs")
|
|
653
|
+
items = _filter_items_by_ids(
|
|
654
|
+
all_items, ids, "id", "docs",
|
|
655
|
+
orphan=lambda missing: _orphan_hints("docs/articles", "article", missing, [".html", ".md"]),
|
|
656
|
+
)
|
|
657
|
+
if _preview("docs", items):
|
|
658
|
+
return
|
|
495
659
|
dirty = False
|
|
496
660
|
for it in items:
|
|
497
661
|
aid = it.get("id")
|
|
@@ -509,9 +673,8 @@ def sync_docs(server, token, ids=None):
|
|
|
509
673
|
"permission",
|
|
510
674
|
) if it.get(k) is not None}
|
|
511
675
|
payload["content"] = content
|
|
676
|
+
payload["content_type"] = "html"
|
|
512
677
|
if aid:
|
|
513
|
-
if not check_conflict(server, token, f"/api/docs/articles/{aid}", it.get("updated_at"), title):
|
|
514
|
-
continue
|
|
515
678
|
ok, status, body = api_call("PUT", server, token, f"/api/docs/articles/{aid}", payload)
|
|
516
679
|
if not ok and status == 404:
|
|
517
680
|
print(f" WARN [{title}] article_id={aid} 不存在,尝试创建")
|
|
@@ -525,7 +688,7 @@ def sync_docs(server, token, ids=None):
|
|
|
525
688
|
it["id"] = new_id
|
|
526
689
|
new_rel = _rename_to_canonical(
|
|
527
690
|
"docs/articles", it.get("content_file", ""), "article", new_id,
|
|
528
|
-
it.get("slug") or it.get("title") or new_id, "
|
|
691
|
+
it.get("slug") or it.get("title") or new_id, "html")
|
|
529
692
|
it["content_file"] = new_rel
|
|
530
693
|
dirty = True
|
|
531
694
|
print(f" OK [{title}] 已创建 article_id={new_id}(已回写 index)")
|
|
@@ -533,13 +696,17 @@ def sync_docs(server, token, ids=None):
|
|
|
533
696
|
print(f" ERR [{title}] 创建失败 -> {_info(ok, status, body)}")
|
|
534
697
|
if dirty:
|
|
535
698
|
_writeback_index("docs/articles/index.json", all_items)
|
|
699
|
+
_summary("docs", len(items))
|
|
536
700
|
|
|
537
701
|
|
|
538
702
|
def sync_doc_categories(server, token, ids=None):
|
|
539
|
-
all_items = _load_index("doc_categories/index.json")
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
703
|
+
all_items = _load_index("doc_categories/index.json", allow_missing=bool(ids))
|
|
704
|
+
all_items = _recover_missing_index_entries(
|
|
705
|
+
server, token, all_items, ids, "id", "doc_categories", "doc_categories/index.json", "/api/docs/categories?flat=true",
|
|
706
|
+
)
|
|
707
|
+
items = _filter_items_by_ids(all_items, ids, "id", "doc_categories")
|
|
708
|
+
if _preview("doc_categories", items):
|
|
709
|
+
return
|
|
543
710
|
dirty = False
|
|
544
711
|
for it in items:
|
|
545
712
|
cid = it.get("id")
|
|
@@ -549,8 +716,6 @@ def sync_doc_categories(server, token, ids=None):
|
|
|
549
716
|
"name", "slug", "description", "icon", "parent_id", "sort_order", "status",
|
|
550
717
|
) if it.get(k) is not None}
|
|
551
718
|
if cid:
|
|
552
|
-
if not check_conflict(server, token, f"/api/docs/categories/{cid}", it.get("updated_at"), name):
|
|
553
|
-
continue
|
|
554
719
|
ok, status, body = api_call("PUT", server, token, f"/api/docs/categories/{cid}", payload)
|
|
555
720
|
if not ok and status == 404:
|
|
556
721
|
print(f" WARN [{name}] category_id={cid} 不存在,尝试创建")
|
|
@@ -568,26 +733,45 @@ def sync_doc_categories(server, token, ids=None):
|
|
|
568
733
|
print(f" ERR [{name}] 创建失败 -> {_info(ok, status, body)}")
|
|
569
734
|
if dirty:
|
|
570
735
|
_writeback_index("doc_categories/index.json", all_items)
|
|
736
|
+
_summary("doc_categories", len(items))
|
|
571
737
|
|
|
572
738
|
|
|
573
|
-
def
|
|
574
|
-
|
|
739
|
+
def _route_specs_from_code(code):
|
|
740
|
+
specs = []
|
|
741
|
+
for m in re.finditer(r'@route\(\s*["\']([A-Za-z]+)\s+([^"\']+)["\']\s*\)', code):
|
|
742
|
+
specs.append((m.group(1).upper(), m.group(2)))
|
|
743
|
+
for m in re.finditer(r'\.Route\(\s*["\']([A-Za-z]+)["\']\s*,\s*["\']([^"\']+)["\']', code):
|
|
744
|
+
specs.append((m.group(1).upper(), m.group(2)))
|
|
745
|
+
return specs
|
|
575
746
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
747
|
+
|
|
748
|
+
def _probe_script_routes(server, token, script_meta, code):
|
|
749
|
+
slug = script_meta.get("slug")
|
|
750
|
+
if not slug:
|
|
751
|
+
return
|
|
752
|
+
for method, route_path in _route_specs_from_code(code):
|
|
753
|
+
if method != "GET":
|
|
754
|
+
print(f" SKIP probe [{slug}] {method} {route_path}(只自动探测 GET,避免副作用)")
|
|
755
|
+
continue
|
|
756
|
+
path = route_path if route_path.startswith("/") else f"/{route_path}"
|
|
757
|
+
ok, status, body = api_call("GET", server, token, f"/api/x/{slug}{path}")
|
|
758
|
+
print(f" {'OK' if ok else 'ERR'} probe GET /api/x/{slug}{path}{'' if ok else ' -> ' + _info(ok, status, body)}")
|
|
583
759
|
|
|
584
760
|
|
|
585
761
|
def sync_custom_scripts(server, token, ids=None):
|
|
586
762
|
"""自定义脚本:从 .draftgo/custom_scripts/index.json + 同目录代码文件回填 code。"""
|
|
587
|
-
all_items = _load_index("custom_scripts/index.json")
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
763
|
+
all_items = _load_index("custom_scripts/index.json", allow_missing=bool(ids))
|
|
764
|
+
all_items = _recover_missing_index_entries(
|
|
765
|
+
server, token, all_items, ids, "id", "custom_scripts", "custom_scripts/index.json", "/api/scripts/",
|
|
766
|
+
file_spec=("custom_scripts", "code_file", "script", [".py", ".js", ".ts", ".sh", ".go", ".txt"]), drop_fields=("code",),
|
|
767
|
+
)
|
|
768
|
+
_warn_orphan_files("custom_scripts", all_items, "code_file", "script", [".go"], "custom_scripts")
|
|
769
|
+
items = _filter_items_by_ids(
|
|
770
|
+
all_items, ids, "id", "custom_scripts",
|
|
771
|
+
orphan=lambda missing: _orphan_hints("custom_scripts", "script", missing, [".go"]),
|
|
772
|
+
)
|
|
773
|
+
if _preview("custom_scripts", items):
|
|
774
|
+
return
|
|
591
775
|
dirty = False
|
|
592
776
|
for it in items:
|
|
593
777
|
sid = it.get("id")
|
|
@@ -598,15 +782,11 @@ def sync_custom_scripts(server, token, ids=None):
|
|
|
598
782
|
print(f" SKIP [{name}] code_file not found: {code_rel}")
|
|
599
783
|
continue
|
|
600
784
|
code = code_path.read_text(encoding="utf-8")
|
|
601
|
-
normalized_triggers = _normalize_script_triggers(it.get("mode"), it.get("triggers"))
|
|
602
785
|
if sid:
|
|
603
|
-
if not check_conflict(server, token, f"/api/scripts/{sid}", it.get("updated_at"), name):
|
|
604
|
-
continue
|
|
605
786
|
# ScriptUpdate 接受字段(不含 slug/mode,避免误改启停/路由)
|
|
606
787
|
payload = {k: it.get(k) for k in (
|
|
607
|
-
"name", "description", "config", "permission",
|
|
788
|
+
"name", "description", "config", "permission", "go_mod", "go_sum",
|
|
608
789
|
) if it.get(k) is not None}
|
|
609
|
-
payload["triggers"] = normalized_triggers
|
|
610
790
|
payload["code"] = code
|
|
611
791
|
ok, status, body = api_call("PUT", server, token, f"/api/scripts/{sid}", payload)
|
|
612
792
|
if not ok and status == 404:
|
|
@@ -614,17 +794,20 @@ def sync_custom_scripts(server, token, ids=None):
|
|
|
614
794
|
sid = None
|
|
615
795
|
else:
|
|
616
796
|
print(f" {'OK' if ok else 'ERR'} [{name}] script_id={sid}{'' if ok else ' -> ' + _info(ok, status, body)}")
|
|
797
|
+
if ok and PROBE_ROUTES and str(it.get("mode") or "").lower() in ("route", "mixed"):
|
|
798
|
+
_probe_script_routes(server, token, it, code)
|
|
617
799
|
if not sid:
|
|
618
|
-
# ScriptCreate 必填 name/slug/code/mode
|
|
800
|
+
# ScriptCreate 必填 name/slug/code/mode;触发来源统一以代码装饰器为准
|
|
619
801
|
payload = {
|
|
620
802
|
"name": it.get("name", ""),
|
|
621
803
|
"slug": it.get("slug", ""),
|
|
622
804
|
"code": code,
|
|
623
805
|
"mode": it.get("mode", "route"),
|
|
624
|
-
"triggers": normalized_triggers,
|
|
625
806
|
"config": it.get("config"),
|
|
626
807
|
"permission": it.get("permission"),
|
|
627
808
|
"description": it.get("description"),
|
|
809
|
+
"go_mod": it.get("go_mod"),
|
|
810
|
+
"go_sum": it.get("go_sum"),
|
|
628
811
|
}
|
|
629
812
|
payload = {k: v for k, v in payload.items() if v is not None}
|
|
630
813
|
ok, status, body = api_call("POST", server, token, "/api/scripts/", payload)
|
|
@@ -638,10 +821,13 @@ def sync_custom_scripts(server, token, ids=None):
|
|
|
638
821
|
it["code_file"] = new_rel
|
|
639
822
|
dirty = True
|
|
640
823
|
print(f" OK [{name}] 已创建 script_id={new_id}(已回写 index)")
|
|
824
|
+
if PROBE_ROUTES and str(it.get("mode") or "").lower() in ("route", "mixed"):
|
|
825
|
+
_probe_script_routes(server, token, it, code)
|
|
641
826
|
else:
|
|
642
827
|
print(f" ERR [{name}] 创建失败 -> {_info(ok, status, body)}")
|
|
643
828
|
if dirty:
|
|
644
829
|
_writeback_index("custom_scripts/index.json", all_items)
|
|
830
|
+
_summary("custom_scripts", len(items))
|
|
645
831
|
|
|
646
832
|
|
|
647
833
|
def _lint_page_html(html, label):
|
|
@@ -693,7 +879,6 @@ HANDLERS = {
|
|
|
693
879
|
"nav": sync_nav,
|
|
694
880
|
"db_meta": sync_db_meta,
|
|
695
881
|
"aihub": sync_aihub,
|
|
696
|
-
"external_apis": sync_external_apis,
|
|
697
882
|
"system_config": sync_system_config,
|
|
698
883
|
"roles": sync_roles,
|
|
699
884
|
"users": sync_users,
|
|
@@ -723,28 +908,32 @@ def run_batch(server, token, args):
|
|
|
723
908
|
for mode, ids in groups:
|
|
724
909
|
print(f"\n--- {mode} {'(all)' if not ids else ','.join(ids)} ---")
|
|
725
910
|
HANDLERS[mode](server, token, ids)
|
|
726
|
-
|
|
911
|
+
result = "previewed" if DRY_RUN else "pushed"
|
|
912
|
+
print(f"\n[batch] done, {len(groups)} group(s) {result}")
|
|
727
913
|
|
|
728
914
|
|
|
729
915
|
def main():
|
|
730
|
-
global
|
|
731
|
-
|
|
916
|
+
global PROBE_ROUTES, DRY_RUN
|
|
732
917
|
if len(sys.argv) < 2:
|
|
733
918
|
print(f"usage: draftgo_push.py {{{'|'.join(HANDLERS)}}} [id ...]\n"
|
|
734
|
-
f" draftgo_push.py --batch <mode> <ids> [<mode> <ids> ...]
|
|
735
|
-
f" --force 跳过冲突检测,强制覆盖")
|
|
919
|
+
f" draftgo_push.py --batch <mode> <ids> [<mode> <ids> ...]")
|
|
736
920
|
sys.exit(1)
|
|
737
921
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
922
|
+
args = sys.argv[1:]
|
|
923
|
+
if "--probe-routes" in args:
|
|
924
|
+
PROBE_ROUTES = True
|
|
925
|
+
args = [a for a in args if a != "--probe-routes"]
|
|
926
|
+
if "--dry-run" in args:
|
|
927
|
+
DRY_RUN = True
|
|
928
|
+
args = [a for a in args if a != "--dry-run"]
|
|
743
929
|
|
|
744
930
|
if not args:
|
|
745
931
|
print(f"usage: draftgo_push.py {{{'|'.join(HANDLERS)}}} [id ...]")
|
|
746
932
|
sys.exit(1)
|
|
747
933
|
|
|
934
|
+
if args[0] == "--all":
|
|
935
|
+
args = ["--batch", *HANDLERS]
|
|
936
|
+
|
|
748
937
|
if args[0] == "--batch":
|
|
749
938
|
server, token, cfg = load_config()
|
|
750
939
|
run_batch(server, token, args[1:])
|